diff options
1008 files changed, 24348 insertions, 12131 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000000..877c67873d --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,27 @@ +engines: + rubocop: + enabled: true + +ratings: + paths: + - "**.rb" + +exclude_paths: + - actioncable/lib/rails/generators/ + - actioncable/test/ + - actionmailer/lib/rails/generators/ + - actionmailer/test/ + - actionpack/test/ + - actionview/test/ + - activejob/lib/rails/generators/ + - activejob/test/ + - activemodel/test/ + - activerecord/lib/rails/generators/ + - activerecord/test/ + - activesupport/test/ + - railties/lib/rails/generators/ + - railties/test/ + - ci/ + - guides/ + - tasks/ + - tools/ 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/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000000..dd8db6af3a --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,27 @@ +AllCops: + TargetRubyVersion: 2.3 + DisabledByDefault: true + +# Two spaces, no tabs (for indentation). +Style/IndentationWidth: + enabled: true + +# No trailing whitespace. +Style/TrailingWhitespace: + enabled: true + +# Blank lines should not have any spaces. +Style/TrailingBlankLines: + enabled: true + +# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }. +Style/HashSyntax: + enabled: true + +# Prefer &&/|| over and/or. +Style/AndOr: + enabled: true + +# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg. +Lint/RequireParentheses: + enabled: true diff --git a/.travis.yml b/.travis.yml index ae38617b99..461bd172c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,36 +1,81 @@ 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" - - curl -L https://github.com/kr/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp - - cd /tmp/beanstalkd-1.10/ - - make - - ./beanstalkd & - - cd $TRAVIS_BUILD_DIR + - "[ -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 + + # Set Sauce Labs username and access key. Obfuscated, purposefully not encrypted. + # Decodes to e.g. `export VARIABLE=VALUE` + - $(base64 --decode <<< "ZXhwb3J0IFNBVUNFX0FDQ0VTU19LRVk9YTAzNTM0M2YtZTkyMi00MGIzLWFhM2MtMDZiM2VhNjM1YzQ4") + - $(base64 --decode <<< "ZXhwb3J0IFNBVUNFX1VTRVJOQU1FPXJ1YnlvbnJhaWxz") + +script: 'ci/travis.rb' + env: matrix: - "GEM=railties" - "GEM=ap" - "GEM=ac" + - "GEM=ac FAYE=1" + - "GEM=ac:integration" + - "GEM=ac:integration FAYE=1" - "GEM=am,amo,as,av,aj" + - "GEM=as PRESERVE_TIMEZONES=1" - "GEM=ar:mysql2" - "GEM=ar:sqlite3" - "GEM=ar:postgresql" - "GEM=aj:integration" - "GEM=guides" + rvm: - - 2.2.4 - - 2.3.0 + - 2.2.5 + - 2.3.1 - ruby-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: - rvm: ruby-head + - rvm: jruby-9.0.5.0 + - env: "GEM=ac:integration" + - env: "GEM=ac:integration FAYE=1" fast_finish: true + notifications: email: false irc: @@ -43,10 +88,6 @@ 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.4" + +git: + depth: 1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 961b48733c..f6ebef7e89 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,6 +21,10 @@ * 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. @@ -3,27 +3,21 @@ 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 to ensure correct loading order, as has to +# 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' -gem 'coffee-rails', '~> 4.1.0' -gem 'turbolinks' +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 Active Model (and by extension the entire framework) # being dependent on a binary library. -platforms :mingw, :x64_mingw, :mswin, :mswin64 do - gem 'bcrypt-ruby', '~> 3.0.0', require: false -end - -platforms :ruby, :jruby, :rbx do - gem 'bcrypt', '~> 3.1.10', require: false -end +gem 'bcrypt', '~> 3.1.11', require: false # This needs to be with require false to avoid it being automatically loaded by # sprockets. @@ -45,18 +39,19 @@ gem 'listen', '~> 3.0.5', require: false # 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 '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 @@ -65,7 +60,16 @@ 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 + + # Lock to 1.1.1 until the fix for https://github.com/faye/faye/issues/394 is released + gem 'faye', '1.1.1', require: false + + gem 'blade', require: false + gem 'blade-sauce_labs_plugin', require: false end # Add your own local bundler stuff. @@ -85,7 +89,7 @@ group :test do end platforms :ruby, :mswin, :mswin64, :mingw, :x64_mingw do - gem 'nokogiri', '>= 1.6.7.1' + gem 'nokogiri', '>= 1.6.8' # Needed for compiling the ActionDispatch::Journey parser. gem 'racc', '>=1.4.6', require: false @@ -95,12 +99,11 @@ platforms :ruby, :mswin, :mswin64, :mingw, :x64_mingw do group :db do gem 'pg', '>= 0.18.0' - gem 'mysql2', '>= 0.4.0' + 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 @@ -133,3 +136,4 @@ end # 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 f02a85f70d..8b31934301 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,90 +1,103 @@ GIT remote: git://github.com/QueueClassic/queue_classic.git - revision: d144db29f1436e9e8b3c7a1a1ecd4442316a9ecd + revision: c26f2c9f6f6133b946fbcdd7b7ec905a4aca9f94 branch: master specs: - queue_classic (3.2.0.alpha) + queue_classic (3.2.0.RC1) pg (>= 0.17, < 0.19) GIT - remote: git://github.com/bkeepers/qu.git - revision: d098e2657c92e89a6413bebd9c033930759c061f - branch: master + remote: git://github.com/collectiveidea/delayed_job.git + revision: 71f1d5faf934d3057abca942f0d410327bc69087 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 + delayed_job (4.1.1) + activesupport (>= 3.0, < 5.1) + +GIT + remote: git://github.com/collectiveidea/delayed_job_active_record.git + revision: 61e688e03b2ef4004b08de6d1e0a123fda8fffad + specs: + delayed_job_active_record (4.1.0) + activerecord (>= 3.0, < 5.1) + delayed_job (>= 3.0, < 5) + +GIT + remote: git://github.com/rails/coffee-rails.git + revision: aa2e623cbda4f3c789a0a15d1f707239e68f5736 + specs: + 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: bce9509f396225d721501ea1070a6871b708abb1 + revision: 3fda1cbe70d615e7ef96e28db4fd1f8a3ebb5505 branch: stable specs: - sass (3.4.20) + 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: - actioncable (5.0.0.beta1.1) - actionpack (= 5.0.0.beta1.1) - coffee-rails (~> 4.1.0) - eventmachine (~> 1.0) - faye-websocket (~> 0.10.0) + actioncable (5.1.0.alpha) + actionpack (= 5.1.0.alpha) + nio4r (~> 1.2) websocket-driver (~> 0.6.1) - actionmailer (5.0.0.beta1.1) - actionpack (= 5.0.0.beta1.1) - actionview (= 5.0.0.beta1.1) - activejob (= 5.0.0.beta1.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.beta1.1) - actionview (= 5.0.0.beta1.1) - activesupport (= 5.0.0.beta1.1) + 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.beta1.1) - activesupport (= 5.0.0.beta1.1) + 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.beta1.1) - activesupport (= 5.0.0.beta1.1) + activejob (5.1.0.alpha) + activesupport (= 5.1.0.alpha) globalid (>= 0.3.6) - activemodel (5.0.0.beta1.1) - activesupport (= 5.0.0.beta1.1) - activerecord (5.0.0.beta1.1) - activemodel (= 5.0.0.beta1.1) - activesupport (= 5.0.0.beta1.1) + 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.0.0.beta1.1) - concurrent-ruby (~> 1.0) + activesupport (5.1.0.alpha) + concurrent-ruby (~> 1.0, >= 1.0.2) i18n (~> 0.7) - json (~> 1.7, >= 1.7.7) - method_source minitest (~> 5.1) tzinfo (~> 1.1) - rails (5.0.0.beta1.1) - actioncable (= 5.0.0.beta1.1) - actionmailer (= 5.0.0.beta1.1) - actionpack (= 5.0.0.beta1.1) - actionview (= 5.0.0.beta1.1) - activejob (= 5.0.0.beta1.1) - activemodel (= 5.0.0.beta1.1) - activerecord (= 5.0.0.beta1.1) - activesupport (= 5.0.0.beta1.1) + rails (5.1.0.alpha) + actioncable (= 5.1.0.alpha) + actionmailer (= 5.1.0.alpha) + actionpack (= 5.1.0.alpha) + actionview (= 5.1.0.alpha) + activejob (= 5.1.0.alpha) + activemodel (= 5.1.0.alpha) + activerecord (= 5.1.0.alpha) + activesupport (= 5.1.0.alpha) bundler (>= 1.3.0, < 2.0) - railties (= 5.0.0.beta1.1) + railties (= 5.1.0.alpha) sprockets-rails (>= 2.0.0) - railties (5.0.0.beta1.1) - actionpack (= 5.0.0.beta1.1) - activesupport (= 5.0.0.beta1.1) + 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) @@ -92,43 +105,79 @@ PATH GEM remote: https://rubygems.org/ specs: - amq-protocol (2.0.0) + addressable (2.4.0) + amq-protocol (2.0.1) arel (7.0.0) - backburner (1.2.0) + backburner (1.3.0) beaneater (~> 1.0) dante (> 0.1.5) - bcrypt (3.1.10) - bcrypt-ruby (3.0.1) - bcrypt-ruby (3.0.1-x86-mingw32) + bcrypt (3.1.11) + bcrypt (3.1.11-x64-mingw32) + bcrypt (3.1.11-x86-mingw32) beaneater (1.0.0) - benchmark-ips (2.3.0) + benchmark-ips (2.6.1) + blade (0.5.6) + activesupport (>= 3.0.0) + blade-qunit_adapter (~> 1.20.0) + coffee-script + coffee-script-source + curses (~> 1.0.0) + eventmachine + faye + sprockets (>= 3.0) + sprockets-export (~> 0.9.1) + thin (>= 1.6.0) + thor (~> 0.19.1) + useragent (~> 0.16.7) + blade-qunit_adapter (1.20.0) + blade-sauce_labs_plugin (0.5.2) + childprocess + faraday + selenium-webdriver builder (3.2.2) - bunny (2.2.1) - amq-protocol (>= 2.0.0) - byebug (8.2.1) - coffee-rails (4.1.0) - coffee-script (>= 2.2.0) - railties (>= 4.0.0, < 5.0) + bunny (2.2.2) + amq-protocol (>= 2.0.1) + byebug (8.2.5) + childprocess (0.5.9) + ffi (~> 1.0, >= 1.0.11) coffee-script (2.4.1) coffee-script-source execjs coffee-script-source (1.10.0) - concurrent-ruby (1.0.0) + concurrent-ruby (1.0.2) connection_pool (2.2.0) - dalli (2.7.5) + cookiejar (0.3.0) + curses (1.0.2) + daemons (1.2.3) + dalli (2.7.6) dante (0.2.0) - delayed_job (4.1.1) - activesupport (>= 3.0, < 5.0) - delayed_job_active_record (4.1.0) - activerecord (>= 3.0, < 5) - delayed_job (>= 3.0, < 5) - em-hiredis (0.3.0) + em-hiredis (0.3.1) eventmachine (~> 1.0) - hiredis (~> 0.5.0) + hiredis (~> 0.6.0) + em-http-request (1.1.3) + addressable (>= 2.3.4) + cookiejar (<= 0.3.0) + em-socksify (>= 0.3) + eventmachine (>= 1.0.3) + http_parser.rb (>= 0.6.0) + em-socksify (0.3.1) + eventmachine (>= 1.0.0.beta.4) erubis (2.7.0) - eventmachine (1.0.9.1) - execjs (2.6.0) - faye-websocket (0.10.2) + eventmachine (1.2.0.1) + eventmachine (1.2.0.1-x64-mingw32) + eventmachine (1.2.0.1-x86-mingw32) + execjs (2.7.0) + faraday (0.9.2) + multipart-post (>= 1.2, < 3) + faye (1.1.1) + cookiejar (>= 0.3.0) + em-http-request (>= 0.3.0) + eventmachine (>= 0.12.0) + faye-websocket (>= 0.9.1) + multi_json (>= 1.0.0) + rack (>= 1.0.0) + websocket-driver (>= 0.5.1) + faye-websocket (0.10.4) eventmachine (>= 0.12.0) websocket-driver (>= 0.5.1) ffi (1.9.10) @@ -136,70 +185,83 @@ GEM ffi (1.9.10-x86-mingw32) globalid (0.3.6) activesupport (>= 4.1.0) - hiredis (0.5.2) + hiredis (0.6.1) + http_parser.rb (0.6.0) i18n (0.7.0) - jquery-rails (4.0.5) - rails-dom-testing (~> 1.0) + 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 - listen (3.0.5) + listen (3.0.7) rb-fsevent (>= 0.9.3) - rb-inotify (>= 0.9) + rb-inotify (>= 0.9.7) loofah (2.0.3) nokogiri (>= 1.5.9) - mail (2.6.3) - mime-types (>= 1.16, < 3) + mail (2.6.4) + mime-types (>= 1.16, < 4) metaclass (0.0.4) method_source (0.8.2) - mime-types (2.99) - mini_portile2 (2.0.0) + mime-types (3.0) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0221) + mini_portile2 (2.1.0) minitest (5.3.3) mocha (0.14.0) metaclass (~> 0.0.1) mono_logger (1.1.0) - multi_json (1.11.2) - mustache (1.0.2) - mysql2 (0.4.2) - mysql2 (0.4.2-x64-mingw32) - mysql2 (0.4.2-x86-mingw32) - nokogiri (1.6.7.1) - mini_portile2 (~> 2.0.0.rc2) - nokogiri (1.6.7.1-x64-mingw32) - mini_portile2 (~> 2.0.0.rc2) - nokogiri (1.6.7.1-x86-mingw32) - mini_portile2 (~> 2.0.0.rc2) + multi_json (1.12.1) + multipart-post (2.0.0) + mustache (1.0.3) + mysql2 (0.4.4) + mysql2 (0.4.4-x64-mingw32) + mysql2 (0.4.4-x86-mingw32) + nio4r (1.2.1) + nokogiri (1.6.8) + mini_portile2 (~> 2.1.0) + pkg-config (~> 1.1.7) + nokogiri (1.6.8-x64-mingw32) + mini_portile2 (~> 2.1.0) + pkg-config (~> 1.1.7) + nokogiri (1.6.8-x86-mingw32) + mini_portile2 (~> 2.1.0) + pkg-config (~> 1.1.7) pg (0.18.4) pg (0.18.4-x64-mingw32) pg (0.18.4-x86-mingw32) - psych (2.0.16) - puma (2.15.3) - que (0.11.2) + pkg-config (1.1.7) + 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.alpha) + rack (2.0.0.rc1) json - rack-cache (1.5.1) + rack-cache (1.6.1) rack (>= 0.4) rack-test (0.6.3) rack (>= 1.0) - rails-deprecated_sanitizer (1.0.3) - activesupport (>= 4.2.0.alpha) - rails-dom-testing (1.0.7) - 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) - rb-fsevent (0.9.6) - rb-inotify (0.9.5) + rake (11.1.2) + rb-fsevent (0.9.7) + rb-inotify (0.9.7) ffi (>= 0.5.0) - rdoc (4.2.0) + rdoc (4.2.2) + json (~> 1.4) redcarpet (3.2.3) - redis (3.2.2) + redis (3.3.0) redis-namespace (1.5.2) redis (~> 3.0, >= 3.0.4) resque (1.25.2) @@ -208,24 +270,29 @@ 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) - rufus-scheduler (3.1.10) + rufus-scheduler (~> 3.2) + rubyzip (1.2.0) + rufus-scheduler (3.2.1) sdoc (0.4.1) json (~> 1.7, >= 1.7.7) rdoc (~> 4.0) - sequel (4.29.0) + selenium-webdriver (2.53.0) + childprocess (~> 0.5) + rubyzip (~> 1.0) + websocket (~> 1.0) + sequel (4.34.0) serverengine (1.5.11) sigdump (~> 0.2.2) - sidekiq (4.0.1) + sidekiq (4.1.2) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) - json (~> 1.0) redis (~> 3.2, >= 3.2.1) - sigdump (0.2.3) + sigdump (0.2.4) + simple_uuid (0.4.0) sinatra (1.0) rack (>= 1.0) sneakers (2.3.5) @@ -233,37 +300,43 @@ GEM serverengine (~> 1.5.11) thor thread (~> 0.1.7) - sprockets (3.5.2) + sprockets (3.6.0) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.0.0) + sprockets-export (0.9.1) + 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.7) - sucker_punch (2.0.0) + stackprof (0.2.9) + sucker_punch (2.0.2) concurrent-ruby (~> 1.0.0) + thin (1.7.0) + daemons (~> 1.0, >= 1.0.9) + eventmachine (~> 1.0, >= 1.0.4) + rack (>= 1, < 3) thor (0.19.1) thread (0.1.7) thread_safe (0.3.5) - turbolinks (2.5.3) - coffee-rails + turbolinks-source (5.0.0.beta4) tzinfo (1.2.2) thread_safe (~> 0.1) - tzinfo-data (1.2015.7) + tzinfo-data (1.2016.4) tzinfo (>= 1.0.0) - uglifier (2.7.2) - execjs (>= 0.3.0) - json (>= 1.8.0) + uglifier (3.0.0) + execjs (>= 0.3.0, < 3) + useragent (0.16.7) vegas (0.1.11) rack (>= 1.0.0) w3c_validators (1.2) json nokogiri - websocket-driver (0.6.3) + wdm (0.1.1) + websocket (1.2.3) + websocket-driver (0.6.4) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) @@ -277,37 +350,39 @@ DEPENDENCIES activerecord-jdbcpostgresql-adapter (>= 1.3.0) activerecord-jdbcsqlite3-adapter (>= 1.3.0) backburner - bcrypt (~> 3.1.10) - bcrypt-ruby (~> 3.0.0) + bcrypt (~> 3.1.11) benchmark-ips + blade + blade-sauce_labs_plugin byebug - coffee-rails (~> 4.1.0) + coffee-rails! dalli (>= 2.2.1) - delayed_job - delayed_job_active_record + delayed_job! + delayed_job_active_record! em-hiredis + faye (= 1.1.1) + faye-websocket + hiredis jquery-rails - json kindlerb (= 0.1.1) listen (~> 3.0.5) minitest (< 5.3.4) mocha (~> 0.14) - mysql2 (>= 0.4.0) - nokogiri (>= 1.6.7.1) + mysql2 (>= 0.4.4) + nokogiri (>= 1.6.8) pg (>= 0.18.0) psych (~> 2.0) puma - qu-rails! qu-redis que queue_classic! racc (>= 1.4.6) rack-cache (~> 1.2) rails! - rake (>= 10.3) + rake (>= 11.1) redcarpet (~> 3.2.3) redis - resque + resque (< 1.26) resque-scheduler sass! sdoc (~> 0.4.0) @@ -317,10 +392,11 @@ DEPENDENCIES 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 8422265fdd..8ea1016081 100644 --- a/RAILS_VERSION +++ b/RAILS_VERSION @@ -1 +1 @@ -5.0.0.beta1.1 +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 @@ -35,7 +35,7 @@ or to generate the body of an email. In Rails, View generation is handled by Act You can read more about Action View in its [README](actionview/README.rdoc). Active Record, Active Model, 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 +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; Action Cable ([README](actioncable/README.md)), a framework to @@ -64,7 +64,7 @@ and may also be used independently outside Rails. 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: diff --git a/RELEASING_RAILS.md b/RELEASING_RAILS.md index faf8fa7f0d..4e6b811d3e 100644 --- a/RELEASING_RAILS.md +++ b/RELEASING_RAILS.md @@ -19,13 +19,13 @@ http://travis-ci.org/rails/rails ### 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. @@ -36,7 +36,7 @@ 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. @@ -47,7 +47,7 @@ Ruby implementors have high stakes in making sure Rails works. Be kind and give them a heads up that Rails will be released soonish. This is only required for major and minor releases, bugfix releases aren't a -big enough deal, and are supposed to be backwards compatible. +big enough deal, and are supposed to be backward compatible. Send an email just giving a heads up about the upcoming release to these lists: @@ -103,18 +103,24 @@ branch. Run `rake install` to generate the gems and install them locally. Then try generating a new app and ensure that nothing explodes. +Verify that Action Cable's package.json is updated with the RC version. + This will stop you from looking silly when you push an RC to rubygems.org and then realize it is broken. -### Release the gem. +### Release to RubyGems and NPM. + +IMPORTANT: The Action Cable client is released as an NPM package, so you must +have Node.js installed, have an NPM account (npmjs.com), and be an actioncable +package owner (`npm owner ls actioncable`) to do a full release. Do not release +until you're set up with NPM! -IMPORTANT: 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 @@ -122,7 +128,7 @@ $ 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 +$ for i in $(ls pkg); do gem push $i; npm publish; done ``` ### Send Rails release announcements @@ -144,7 +150,7 @@ break existing applications. ### Post the announcement to the Rails blog. -If you used Markdown format for your email, you can just paste it in to the +If you used Markdown format for your email, you can just paste it into the blog. * http://weblog.rubyonrails.org @@ -212,4 +218,4 @@ There are two simple steps for fixing the CI: 1. Identify the problem 2. Fix it -Repeat these steps until the CI is green.
\ No newline at end of file +Repeat these steps until the CI is green. @@ -21,7 +21,10 @@ task :default => %w(test test:isolated) task task_name do errors = [] FRAMEWORKS.each do |project| - system(%(cd #{project} && #{$0} #{task_name})) || errors << 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 @@ -30,9 +33,10 @@ end desc "Smoke-test all projects" task :smoke do (FRAMEWORKS - %w(activerecord)).each do |project| - system %(cd #{project} && #{$0} test:isolated) + 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 index a33b7b6b4b..a767857607 100644 --- a/actioncable/CHANGELOG.md +++ b/actioncable/CHANGELOG.md @@ -1,14 +1,2 @@ -* Create notion of an `ActionCable::SubscriptionAdapter`. - Separate out Redis functionality into - `ActionCable::SubscriptionAdapter::Redis`, and add a - PostgreSQL adapter as well. Configuration file for - ActionCable was changed from`config/redis/cable.yml` to - `config/cable.yml`. - *Jon Moss* - -## Rails 5.0.0.beta1 (December 18, 2015) ## - -* Added to Rails! - - *DHH* +Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/actioncable/CHANGELOG.md) for previous changes. diff --git a/actioncable/README.md b/actioncable/README.md index cad71ddf94..58d23d2834 100644 --- a/actioncable/README.md +++ b/actioncable/README.md @@ -7,7 +7,6 @@ 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 @@ -17,7 +16,7 @@ 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 a `AppearancesChannel`, and a consumer could be subscribed to either +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 @@ -39,7 +38,7 @@ reflections of each unit. ### 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 +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 @@ -73,7 +72,7 @@ use that to set the `current_user`. By identifying the connection by this same c 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). -Then you should define your `ApplicationCable::Channel` class in Ruby. This is the place where you put +Next, you should define your `ApplicationCable::Channel` class in Ruby. This is the place where you put shared logic between your channels. ```ruby @@ -86,15 +85,20 @@ end The client-side needs to setup a consumer instance of this connection. That's done like so: -```coffeescript -# app/assets/javascripts/cable.coffee -#= require action_cable +```js +// app/assets/javascripts/cable.js +//= require action_cable +//= require_self +//= require_tree ./channels + +(function() { + this.App || (this.App = {}); -@App = {} -App.cable = ActionCable.createConsumer("ws://cable.example.com") + App.cable = ActionCable.createConsumer("ws://cable.example.com"); +}).call(this); ``` -The ws://cable.example.com address must point to your set of Action Cable servers, and it +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. @@ -105,8 +109,8 @@ is defined by declaring channels on the server and allowing the consumer to subs ### Channel 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). +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: @@ -178,9 +182,9 @@ App.cable.subscriptions.create "AppearanceChannel", ``` Simply calling `App.cable.subscriptions.create` will setup the subscription, which will call `AppearanceChannel#subscribed`, -which in turn is linked to original `App.cable` -> `ApplicationCable::Connection` instances. +which in turn is linked to the original `App.cable` -> `ApplicationCable::Connection` instances. -We then link the client-side `appear` method to `AppearanceChannel#appear(data)`. This is possible because the server-side +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. @@ -188,7 +192,7 @@ can be reached as remote procedure calls via a subscription's `perform` method. 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 -action on the client. +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: @@ -215,7 +219,7 @@ 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 Redis' 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 `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`. @@ -234,7 +238,7 @@ class ChatChannel < ApplicationCable::Channel end ``` -Pass an object as the first argument to `subscriptions.create`, and that object will become your params hash in your cable channel. The keyword `channel` is required. +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 @@ -293,165 +297,17 @@ The rebroadcast will be received by all connected clients, _including_ the clien ### More complete examples -See the [rails/actioncable-examples](http://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 three required configurations: the Redis connection, 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 a Redis url for each Rails environment. It may use the following format: - -```yaml -production: &production - url: redis://10.10.3.153:6381 -development: &development - url: redis://localhost:6379 -test: *development -``` - -You can also change the location of the Redis 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 -ActionCable.server.config.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 -ActionCable.server.config.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 ActionCable 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 -ActionCable.server.config.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 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 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! - -require 'action_cable/process/logging' - -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 threaded server like Puma or Thin, the current implementation of ActionCable can run side-along with your Rails application. For example, to listen for WebSocket requests on `/cable`, mount the server at that path: - -```ruby -# config/routes.rb -Example::Application.routes.draw do - mount ActionCable.server => '/cable' -end -``` - -For every instance of your server you create and for every worker your server spawns, you will also have a new instance of ActionCable, 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 is currently tied to Redis through its use of the pubsub feature to route -messages back and forth over the WebSocket cable connection. This dependency may well -be alleviated in the future, but for the moment that's what it is. So be sure to have -Redis installed and running. +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. -The Ruby side of things is built on top of [faye-websocket](https://github.com/faye/faye-websocket-ruby) and [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby). +## Download and installation +The latest version of Action Cable can be installed with RubyGems: -## Deployment + $ gem install actioncable -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. +Source code can be downloaded as part of the Rails project on GitHub -But this also means that Action Cable needs to run in its own server process. -So you'll have one set of server processes for your normal web work, and another -set of server processes for the Action Cable. The former can be single-threaded, -like Unicorn, but the latter must be multi-threaded, like Puma. +* https://github.com/rails/rails/tree/master/actioncable ## License diff --git a/actioncable/Rakefile b/actioncable/Rakefile index b6c56e9195..a72142deb5 100644 --- a/actioncable/Rakefile +++ b/actioncable/Rakefile @@ -1,9 +1,14 @@ require 'rake/testtask' +require 'pathname' +require 'action_cable' +require 'blade' dir = File.dirname(__FILE__) task :default => :test +task :package => "assets:compile" + Rake::TestTask.new do |t| t.libs << "test" t.test_files = Dir.glob("#{dir}/test/**/*_test.rb") @@ -11,3 +16,26 @@ Rake::TestTask.new do |t| 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 :integration do + if ENV['CI'] + Blade.start(interface: :ci) + else + Blade.start(interface: :runner) + end + end +end + +namespace :assets do + desc "Compile Action Cable assets" + task :compile do + Blade.build + end +end diff --git a/actioncable/actioncable.gemspec b/actioncable/actioncable.gemspec index a36acc8f6f..c65ff7871f 100644 --- a/actioncable/actioncable.gemspec +++ b/actioncable/actioncable.gemspec @@ -20,14 +20,6 @@ Gem::Specification.new do |s| s.add_dependency 'actionpack', version - s.add_dependency 'coffee-rails', '~> 4.1.0' - s.add_dependency 'eventmachine', '~> 1.0' - s.add_dependency 'faye-websocket', '~> 0.10.0' + s.add_dependency 'nio4r', '~> 1.2' s.add_dependency 'websocket-driver', '~> 0.6.1' - - s.add_development_dependency 'em-hiredis', '~> 0.3.0' - s.add_development_dependency 'mocha' - s.add_development_dependency 'pg' - s.add_development_dependency 'puma' - s.add_development_dependency 'redis', '~> 3.0' end diff --git a/actioncable/lib/assets/javascripts/action_cable.coffee.erb b/actioncable/app/assets/javascripts/action_cable.coffee.erb index 7daea4ebcd..e0758dae72 100644 --- a/actioncable/lib/assets/javascripts/action_cable.coffee.erb +++ b/actioncable/app/assets/javascripts/action_cable.coffee.erb @@ -1,10 +1,14 @@ +#= export ActionCable #= require_self -#= require action_cable/consumer +#= require ./action_cable/consumer @ActionCable = INTERNAL: <%= ActionCable::INTERNAL.to_json %> + WebSocket: window.WebSocket + logger: window.console - createConsumer: (url = @getConfig("url")) -> + createConsumer: (url) -> + url ?= @getConfig("url") ? @INTERNAL.default_mount_path new ActionCable.Consumer @createWebSocketURL(url) getConfig: (name) -> @@ -21,3 +25,14 @@ a.href else url + + startDebugging: -> + @debugging = true + + stopDebugging: -> + @debugging = null + + log: (messages...) -> + if @debugging + messages.push(Date.now()) + @logger.log("[ActionCable]", messages...) 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..29ad676290 --- /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 ActionCable.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/lib/assets/javascripts/action_cable/consumer.coffee b/actioncable/app/assets/javascripts/action_cable/consumer.coffee index fcd8d0fb6c..3298be717f 100644 --- a/actioncable/lib/assets/javascripts/action_cable/consumer.coffee +++ b/actioncable/app/assets/javascripts/action_cable/consumer.coffee @@ -1,7 +1,6 @@ -#= require action_cable/connection -#= require action_cable/connection_monitor -#= require action_cable/subscriptions -#= require action_cable/subscription +#= 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. @@ -15,11 +14,33 @@ # 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 - @connectionMonitor = new ActionCable.ConnectionMonitor this send: (data) -> @connection.send(data) + + connect: -> + @connection.open() + + disconnect: -> + @connection.close(allowReconnect: false) + + ensureActiveConnection: -> + unless @connection.isActive() + @connection.open() diff --git a/actioncable/lib/assets/javascripts/action_cable/subscription.coffee b/actioncable/app/assets/javascripts/action_cable/subscription.coffee index 339d676933..8e0805a174 100644 --- a/actioncable/lib/assets/javascripts/action_cable/subscription.coffee +++ b/actioncable/app/assets/javascripts/action_cable/subscription.coffee @@ -1,5 +1,5 @@ -# 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 +# 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: @@ -7,13 +7,19 @@ # 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' # @@ -27,15 +33,15 @@ # 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 @@ -44,11 +50,9 @@ # 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: (@subscriptions, params = {}, mixin) -> + constructor: (@consumer, params = {}, mixin) -> @identifier = JSON.stringify(params) extend(this, mixin) - @subscriptions.add(this) - @consumer = @subscriptions.consumer # Perform a channel action with the optional data passed as an attribute perform: (action, data = {}) -> @@ -59,7 +63,7 @@ class ActionCable.Subscription @consumer.send(command: "message", identifier: @identifier, data: JSON.stringify(data)) unsubscribe: -> - @subscriptions.remove(this) + @consumer.subscriptions.remove(this) extend = (object, properties) -> if properties? diff --git a/actioncable/lib/assets/javascripts/action_cable/subscriptions.coffee b/actioncable/app/assets/javascripts/action_cable/subscriptions.coffee index ae041ffa2b..aa052bf5d8 100644 --- a/actioncable/lib/assets/javascripts/action_cable/subscriptions.coffee +++ b/actioncable/app/assets/javascripts/action_cable/subscriptions.coffee @@ -13,28 +13,33 @@ class ActionCable.Subscriptions create: (channelName, mixin) -> channel = channelName params = if typeof channel is "object" then channel else {channel} - new ActionCable.Subscription this, params, mixin + 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 @@ -58,7 +63,4 @@ class ActionCable.Subscriptions sendCommand: (subscription, command) -> {identifier} = subscription - if identifier is ActionCable.INTERNAL.identifiers.ping - @consumer.connection.isOpen() - else - @consumer.send({command, identifier}) + @consumer.send({command, identifier}) diff --git a/actioncable/blade.yml b/actioncable/blade.yml new file mode 100644 index 0000000000..e38e9b2f1d --- /dev/null +++ b/actioncable/blade.yml @@ -0,0 +1,34 @@ +load_paths: + - app/assets/javascripts + - test/javascript/src + - test/javascript/vendor + +logical_paths: + - test.js + +build: + logical_paths: + - action_cable.js + path: lib/assets/compiled + clean: true + +plugins: + sauce_labs: + browsers: + Google Chrome: + os: Mac, Windows + version: -1 + Firefox: + os: Mac, Windows + version: -1 + Safari: + platform: Mac + version: -1 + Microsoft Edge: + version: -1 + Internet Explorer: + version: 11 + iPhone: + version: -1 + Motorola Droid 4 Emulator: + version: -1 diff --git a/actioncable/lib/action_cable.rb b/actioncable/lib/action_cable.rb index 1dc66ef3ad..b6d2842867 100644 --- a/actioncable/lib/action_cable.rb +++ b/actioncable/lib/action_cable.rb @@ -29,13 +29,14 @@ module ActionCable extend ActiveSupport::Autoload INTERNAL = { - identifiers: { - ping: '_ping'.freeze - }, 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 diff --git a/actioncable/lib/action_cable/channel/base.rb b/actioncable/lib/action_cable/channel/base.rb index 88cdc1cab1..845b747fc5 100644 --- a/actioncable/lib/action_cable/channel/base.rb +++ b/actioncable/lib/action_cable/channel/base.rb @@ -32,8 +32,11 @@ module ActionCable # # == Action processing # - # Unlike Action Controllers, channels do not follow a REST constraint form for its actions. It's a remote-procedure call model. You can - # declare any public method on the channel (optionally taking a data argument), and this method is automatically exposed as callable to the client. + # 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: # @@ -60,18 +63,22 @@ module ActionCable # end # end # - # In this example, subscribed/unsubscribed are not callable methods, as they were already declared in ActionCable::Channel::Base, but #appear/away - # are. #generate_connection_token is also not callable as its a private method. You'll see that appear accepts a data parameter, which it then - # uses as part of its model call. #away does not, it's simply a trigger action. + # 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, current_user 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. + # 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 #reject! - # - # Example: + # A channel can reject a subscription request in the #subscribed callback by + # invoking the #reject method: # # class ChatChannel < ApplicationCable::Channel # def subscribed @@ -80,8 +87,10 @@ module ActionCable # end # end # - # In this example, the subscription will be rejected if the current_user does not have access to the chat room. - # On the client-side, Channel#rejected callback will get invoked when the server rejects the subscription request. + # 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 @@ -116,7 +125,7 @@ module ActionCable 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 + # you run action_methods, they will be recalculated. def clear_action_methods! @action_methods = nil end @@ -151,15 +160,18 @@ module ActionCable action = extract_action(data) if processable_action?(action) - dispatch_action(action, data) + 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 its cut so the channel has a chance to cleanup with callbacks. + # 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 + def unsubscribe_from_channel # :nodoc: run_callbacks :unsubscribe do unsubscribed end @@ -174,7 +186,7 @@ module ActionCable end # Called once a consumer has cut its cable connection. Can be used for cleaning up connections or marking - # people as offline or the like. + # users as offline or the like. def unsubscribed # Override in subclasses end @@ -182,8 +194,12 @@ module ActionCable # 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}".tap { |m| m << " (via #{via})" if via } - connection.transmit ActiveSupport::JSON.encode(identifier: @identifier, message: data) + 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! @@ -215,7 +231,6 @@ module ActionCable end end - def subscribe_to_channel run_callbacks :subscribe do subscribed @@ -228,7 +243,6 @@ module ActionCable end end - def extract_action(data) (data['action'].presence || :receive).to_sym end @@ -258,8 +272,11 @@ module ActionCable def transmit_subscription_confirmation unless subscription_confirmation_sent? logger.info "#{self.class.name} is transmitting the subscription confirmation" - connection.transmit ActiveSupport::JSON.encode(identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:confirmation]) - @subscription_confirmation_sent = true + + 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 @@ -270,7 +287,10 @@ module ActionCable def transmit_subscription_rejection logger.info "#{self.class.name} is transmitting the subscription rejection" - connection.transmit ActiveSupport::JSON.encode(identifier: @identifier, type: ActionCable::INTERNAL[:message_types][: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 diff --git a/actioncable/lib/action_cable/channel/naming.rb b/actioncable/lib/action_cable/channel/naming.rb index 4c9d53b15a..8e1b2a4af0 100644 --- a/actioncable/lib/action_cable/channel/naming.rb +++ b/actioncable/lib/action_cable/channel/naming.rb @@ -10,6 +10,7 @@ module ActionCable # # 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 diff --git a/actioncable/lib/action_cable/channel/periodic_timers.rb b/actioncable/lib/action_cable/channel/periodic_timers.rb index 7f0fb37afc..dab604440f 100644 --- a/actioncable/lib/action_cable/channel/periodic_timers.rb +++ b/actioncable/lib/action_cable/channel/periodic_timers.rb @@ -12,11 +12,42 @@ module ActionCable end module ClassMethods - # Allow you to call a private method <tt>every</tt> so often seconds. This periodic timer can be useful - # for sending a steady flow of updates to a client based off an object that was configured on subscription. - # It's an alternative to using streams if the channel is able to do the work internally. - def periodically(callback, every:) - self.periodic_timers += [ [ callback, every: every ] ] + # 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 @@ -27,14 +58,21 @@ module ActionCable def start_periodic_timers self.class.periodic_timers.each do |callback, options| - active_periodic_timers << EventMachine::PeriodicTimer.new(options[:every]) do - connection.worker_pool.async_run_periodic_timer(self, callback) + 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.cancel } + active_periodic_timers.each { |timer| timer.shutdown } + active_periodic_timers.clear end end end diff --git a/actioncable/lib/action_cable/channel/streams.rb b/actioncable/lib/action_cable/channel/streams.rb index e2876ef6fa..561750d713 100644 --- a/actioncable/lib/action_cable/channel/streams.rb +++ b/actioncable/lib/action_cable/channel/streams.rb @@ -1,8 +1,8 @@ module ActionCable module Channel - # Streams allow channels to route broadcastings to the subscriber. A broadcasting is, as discussed elsewhere, a pub/sub queue where any data - # put 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'll not get that update when connecting later. + # 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 @@ -18,13 +18,15 @@ module ActionCable # end # end # - # So the subscribers of this channel will get whatever data is put into the, let's say, `comments_for_45` broadcasting as soon as it's put there. - # That looks like so from that side of things: + # 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 `comments:Z2lkOi8vVGVzdEFwcC9Qb3N0LzE` + # The following example would subscribe to a broadcasting like <tt>comments:Z2lkOi8vVGVzdEFwcC9Qb3N0LzE</tt>. # # class CommentsChannel < ApplicationCable::Channel # def subscribed @@ -37,26 +39,25 @@ module ActionCable # # CommentsChannel.broadcast_to(@post, @comment) # - # If you don't just want to parlay the broadcast unfiltered to the subscriber, you can supply a callback that lets you alter what goes out. - # Example below shows how you can use this to provide performance introspection in the process: + # 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, -> (encoded_message) do - # message = ActiveSupport::JSON.decode(encoded_message) + # def subscribed + # @room = Chat::Room[params[:room_number]] # - # if message['originated_at'].present? - # elapsed_time = (Time.now.to_f - message['originated_at']).round(2) + # 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 + # 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 + # transmit message + # end + # end + # end # # You can stop streaming from all broadcasts by calling #stop_all_streams. module Streams @@ -68,15 +69,21 @@ module ActionCable # 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. - def stream_from(broadcasting, callback = nil) - # Hold off the confirmation until pubsub#subscribe is successful + # 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! - callback ||= default_stream_callback(broadcasting) - streams << [ broadcasting, callback ] + # 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 ] - EM.next_tick do - pubsub.subscribe(broadcasting, callback, lambda do + 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) @@ -86,10 +93,14 @@ module ActionCable # 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. - def stream_for(model, callback = nil) - stream_from(broadcasting_for([ channel_name, model ]), callback) + # + # 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 @@ -104,11 +115,60 @@ module ActionCable @_streams ||= [] end - def default_stream_callback(broadcasting) + # 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 the 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 ActiveSupport::JSON.decode(message), via: "streamed from #{broadcasting}" + 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 index b672e00682..5f813cf8e0 100644 --- a/actioncable/lib/action_cable/connection.rb +++ b/actioncable/lib/action_cable/connection.rb @@ -5,12 +5,17 @@ module ActionCable eager_autoload do autoload :Authorization autoload :Base + autoload :ClientSocket autoload :Identification autoload :InternalChannel + autoload :FayeClientSocket + autoload :FayeEventLoop autoload :MessageBuffer - autoload :WebSocket + autoload :Stream + autoload :StreamEventLoop autoload :Subscriptions autoload :TaggedLoggerProxy + autoload :WebSocket end end end diff --git a/actioncable/lib/action_cable/connection/base.rb b/actioncable/lib/action_cable/connection/base.rb index bb8850aaa0..75c1299e36 100644 --- a/actioncable/lib/action_cable/connection/base.rb +++ b/actioncable/lib/action_cable/connection/base.rb @@ -2,9 +2,9 @@ require 'action_dispatch' module ActionCable module Connection - # For every WebSocket the cable server is accepting, a Connection object will be instantiated. This instance becomes the parent - # of all 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 cable consumer. The Connection itself does not deal with any specific application logic beyond + # For every WebSocket connection 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: @@ -33,14 +33,14 @@ module ActionCable # end # end # - # First, we declare that this connection can be identified by its current_user. This allows us later to be able to find all connections - # established for that current_user (and potentially disconnect them if the user was removed from an account). You can declare as many - # identification indexes as you like. Declaring an identification means that a attr_accessor is automatically set for that key. + # 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 name of the current user to easily distinguish their messages in the log. + # 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 @@ -48,15 +48,16 @@ module ActionCable include InternalChannel include Authorization - attr_reader :server, :env, :subscriptions, :logger - delegate :worker_pool, :pubsub, to: :server + attr_reader :server, :env, :subscriptions, :logger, :worker_pool, :protocol + delegate :event_loop, :pubsub, to: :server - def initialize(server, env) - @server, @env = server, env + 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) + @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) @@ -65,35 +66,33 @@ module ActionCable 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. Rely on the #connect (and #disconnect) callback instead. - def process + # 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? - websocket.on(:open) { |event| send_async :on_open } - websocket.on(:message) { |event| on_message event.data } - websocket.on(:close) { |event| send_async :on_close } - respond_to_successful_request else respond_to_invalid_request end end - # Data received over the cable is handled by this method. It's expected that everything inbound is JSON encoded. - # The data is routed to the proper channel that the connection has subscribed to. - def receive(data_in_json) + # 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 ActiveSupport::JSON.decode(data_in_json) + subscriptions.execute_command decode(websocket_message) else - logger.error "Received data without a live WebSocket (#{data_in_json.inspect})" + logger.error "Ignoring message processed after the WebSocket was closed: #{websocket_message.inspect})" end end - # Send raw data straight back down the WebSocket. This is not intended to be called directly. Use the #transmit available on the - # Channel instead, as that'll automatically address the correct subscriber and wrap the message in JSON. - def transmit(data) - websocket.transmit data + def transmit(cable_message) # :nodoc: + websocket.transmit encode(cable_message) end # Close the WebSocket connection. @@ -118,7 +117,23 @@ module ActionCable end def beat - transmit ActiveSupport::JSON.encode(identifier: ActionCable::INTERNAL[:identifiers][:ping], message: Time.now.to_i) + 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 @@ -139,10 +154,19 @@ module ActionCable attr_reader :message_buffer private - def on_open + 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 - beat + send_welcome_message message_buffer.process! server.add_connection(self) @@ -150,11 +174,7 @@ module ActionCable respond_to_invalid_request end - def on_message(message) - message_buffer.append message - end - - def on_close + def handle_close logger.info finished_request_message server.remove_connection(self) @@ -165,6 +185,13 @@ module ActionCable 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 @@ -177,12 +204,14 @@ module ActionCable 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 @@ -197,7 +226,7 @@ module ActionCable 'Started %s "%s"%s for %s at %s' % [ request.request_method, request.filtered_path, - websocket.possible? ? ' [WebSocket]' : '', + websocket.possible? ? ' [WebSocket]' : '[non-WebSocket]', request.ip, Time.now.to_s ] end @@ -205,10 +234,22 @@ module ActionCable def finished_request_message 'Finished "%s"%s for %s at %s' % [ request.filtered_path, - websocket.possible? ? ' [WebSocket]' : '', + 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 index 885ff3f102..4a54044aff 100644 --- a/actioncable/lib/action_cable/connection/identification.rb +++ b/actioncable/lib/action_cable/connection/identification.rb @@ -12,7 +12,7 @@ module ActionCable 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. + # 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. diff --git a/actioncable/lib/action_cable/connection/internal_channel.rb b/actioncable/lib/action_cable/connection/internal_channel.rb index 54ed7672d2..f70d52f99b 100644 --- a/actioncable/lib/action_cable/connection/internal_channel.rb +++ b/actioncable/lib/action_cable/connection/internal_channel.rb @@ -11,24 +11,22 @@ module ActionCable def subscribe_to_internal_channel if connection_identifier.present? - callback = -> (message) { process_internal_message(message) } + callback = -> (message) { process_internal_message decode(message) } @_internal_subscriptions ||= [] @_internal_subscriptions << [ internal_channel, callback ] - EM.next_tick { pubsub.subscribe(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| EM.next_tick { pubsub.unsubscribe(channel, callback) } } + @_internal_subscriptions.each { |channel, callback| server.event_loop.post { pubsub.unsubscribe(channel, callback) } } end end def process_internal_message(message) - message = ActiveSupport::JSON.decode(message) - case message['type'] when 'disconnect' logger.info "Removing connection (#{connection_identifier})" diff --git a/actioncable/lib/action_cable/connection/message_buffer.rb b/actioncable/lib/action_cable/connection/message_buffer.rb index 2f65a1e84a..6a80770cae 100644 --- a/actioncable/lib/action_cable/connection/message_buffer.rb +++ b/actioncable/lib/action_cable/connection/message_buffer.rb @@ -1,8 +1,7 @@ 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. - # Entirely internal operation and should not be used directly by the user. - class MessageBuffer + # 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 = [] @@ -31,7 +30,7 @@ module ActionCable protected attr_reader :connection - attr_accessor :buffered_messages + attr_reader :buffered_messages private def valid?(message) @@ -39,7 +38,7 @@ module ActionCable end def receive(message) - connection.send_async :receive, message + connection.receive message end def buffer(message) 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 index d7f95e6a62..6051818bfb 100644 --- a/actioncable/lib/action_cable/connection/subscriptions.rb +++ b/actioncable/lib/action_cable/connection/subscriptions.rb @@ -3,8 +3,8 @@ 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. Should not be used directly by the user. - class Subscriptions + # the connection to the proper channel. + class Subscriptions # :nodoc: def initialize(connection) @connection = connection @subscriptions = {} @@ -26,12 +26,12 @@ module ActionCable id_key = data['identifier'] id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access - subscription_klass = connection.server.channel_classes[id_options[:channel]] + subscription_klass = id_options[:channel].safe_constantize - if subscription_klass + if subscription_klass && ActionCable::Channel::Base >= subscription_klass subscriptions[id_key] ||= subscription_klass.new(connection, id_key, id_options) else - logger.error "Subscription class not found (#{data.inspect})" + logger.error "Subscription class not found: #{id_options[:channel].inspect}" end end @@ -54,7 +54,7 @@ module ActionCable end def unsubscribe_from_all - subscriptions.each { |id, channel| channel.unsubscribe_from_channel } + subscriptions.each { |id, channel| remove_subscription(channel) } end protected diff --git a/actioncable/lib/action_cable/connection/web_socket.rb b/actioncable/lib/action_cable/connection/web_socket.rb index 670d5690ae..11f28c37e8 100644 --- a/actioncable/lib/action_cable/connection/web_socket.rb +++ b/actioncable/lib/action_cable/connection/web_socket.rb @@ -1,13 +1,11 @@ -require 'faye/websocket' +require 'websocket/driver' module ActionCable module Connection - # Decorate the Faye::WebSocket with helpers we need. + # Wrap the real socket to minimize the externally-presented API class WebSocket - delegate :rack_response, :close, :on, to: :websocket - - def initialize(env) - @websocket = Faye::WebSocket.websocket?(env) ? Faye::WebSocket.new(env) : nil + 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? @@ -15,11 +13,23 @@ module ActionCable end def alive? - websocket && websocket.ready_state == Faye::WebSocket::API::OPEN + websocket && websocket.alive? end def transmit(data) - websocket.send data + websocket.transmit data + end + + def close + websocket.close + end + + def protocol + websocket.protocol + end + + def rack_response + websocket.rack_response end protected diff --git a/actioncable/lib/action_cable/engine.rb b/actioncable/lib/action_cable/engine.rb index f5e233e091..96176e0014 100644 --- a/actioncable/lib/action_cable/engine.rb +++ b/actioncable/lib/action_cable/engine.rb @@ -4,9 +4,9 @@ require "action_cable/helpers/action_cable_helper" require "active_support/core_ext/hash/indifferent_access" module ActionCable - class Railtie < Rails::Engine # :nodoc: + class Engine < Rails::Engine # :nodoc: config.action_cable = ActiveSupport::OrderedOptions.new - config.action_cable.url = '/cable' + config.action_cable.mount_path = ActionCable::INTERNAL[:default_mount_path] config.eager_load_namespaces << ActionCable @@ -31,8 +31,47 @@ module ActionCable self.cable = Rails.application.config_for(config_path).with_indifferent_access end + previous_connection_class = self.connection_class + self.connection_class = -> { 'ApplicationCable::Connection'.safe_constantize || previous_connection_class.call } + 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 index c652fb91ae..8ba0230d47 100644 --- a/actioncable/lib/action_cable/gem_version.rb +++ b/actioncable/lib/action_cable/gem_version.rb @@ -6,9 +6,9 @@ module ActionCable module VERSION MAJOR = 5 - MINOR = 0 + MINOR = 1 TINY = 0 - PRE = "beta1.1" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/actioncable/lib/action_cable/helpers/action_cable_helper.rb b/actioncable/lib/action_cable/helpers/action_cable_helper.rb index b82751468a..2081a37db6 100644 --- a/actioncable/lib/action_cable/helpers/action_cable_helper.rb +++ b/actioncable/lib/action_cable/helpers/action_cable_helper.rb @@ -1,28 +1,39 @@ 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: + # 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 ActionCable to determine the url of your websocket server. + # 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: + # URL directly: # # #= require cable # @App = {} # App.cable = Cable.createConsumer() # - # Make sure to specify the correct server location in each of your environments - # config file: + # 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" /> # - # config.action_cable.url = "ws://example.com:28080" def action_cable_meta_tag - tag "meta", name: "action-cable-url", content: Rails.application.config.action_cable.url + 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 diff --git a/actioncable/lib/action_cable/process/logging.rb b/actioncable/lib/action_cable/process/logging.rb deleted file mode 100644 index dce637b3ca..0000000000 --- a/actioncable/lib/action_cable/process/logging.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'action_cable/server' -require 'eventmachine' - -EM.error_handler do |e| - puts "Error raised inside the event loop: #{e.message}" - puts e.backtrace.join("\n") -end diff --git a/actioncable/lib/action_cable/remote_connections.rb b/actioncable/lib/action_cable/remote_connections.rb index aa2fc95d2f..a528024427 100644 --- a/actioncable/lib/action_cable/remote_connections.rb +++ b/actioncable/lib/action_cable/remote_connections.rb @@ -1,6 +1,7 @@ module ActionCable - # If you need to disconnect a given connection, you go through the RemoteConnections. You find the connections you're looking for by - # searching the identifier declared on the connection. Example: + # 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 @@ -11,8 +12,9 @@ module ActionCable # # ActionCable.server.remote_connections.where(current_user: User.find(1)).disconnect # - # That will disconnect all the connections established for User.find(1) across all servers running on all machines (because it uses - # the internal channel that all these servers are subscribed to). + # 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 @@ -25,8 +27,8 @@ module ActionCable end private - # Represents a single remote connection found via ActionCable.server.remote_connections.where(*). - # Exists for the solely for the purpose of calling #disconnect on that connection. + # 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 diff --git a/actioncable/lib/action_cable/server.rb b/actioncable/lib/action_cable/server.rb index a2a89d5f1e..bd6a3826a3 100644 --- a/actioncable/lib/action_cable/server.rb +++ b/actioncable/lib/action_cable/server.rb @@ -1,7 +1,3 @@ -require 'eventmachine' -EventMachine.epoll if EventMachine.epoll? -EventMachine.kqueue if EventMachine.kqueue? - module ActionCable module Server extend ActiveSupport::Autoload diff --git a/actioncable/lib/action_cable/server/base.rb b/actioncable/lib/action_cable/server/base.rb index 3385a4c9f3..0ad1e408a9 100644 --- a/actioncable/lib/action_cable/server/base.rb +++ b/actioncable/lib/action_cable/server/base.rb @@ -1,9 +1,11 @@ +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 cable server, but - # also by the user to reach the RemoteConnections instead for finding and disconnecting connections across all servers. + # 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 details. + # Also, this is the server instance used for broadcasting. See Broadcasting for more information. class Base include ActionCable::Server::Broadcasting include ActionCable::Server::Connections @@ -13,13 +15,17 @@ module ActionCable def self.logger; config.logger; end delegate :logger, to: :config + attr_reader :mutex + def initialize + @mutex = Monitor.new + @remote_connections = @event_loop = @worker_pool = @pubsub = nil end - # Called by rack to setup the server. + # Called by Rack to setup the server. def call(env) setup_heartbeat_timer - config.connection_class.new(self, env).process + config.connection_class.call.new(self, env).process end # Disconnect all the connections identified by `identifiers` on this server or any others via RemoteConnections. @@ -27,32 +33,48 @@ module ActionCable 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 ||= RemoteConnections.new(self) + @remote_connections || @mutex.synchronize { @remote_connections ||= RemoteConnections.new(self) } end - # The thread worker pool for handling all the connection work on this server. Default size is set by config.worker_pool_size. - def worker_pool - @worker_pool ||= ActionCable::Server::Worker.new(max_size: config.worker_pool_size) + def event_loop + @event_loop || @mutex.synchronize { @event_loop ||= config.event_loop_class.new } end - # Requires and returns a hash of all the channel class constants keyed by name. - def channel_classes - @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 + # 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 database connection pool and block while they wait for other workers to release their connections. Use a smaller worker pool or a larger + # database connection pool instead. + def worker_pool + @worker_pool || @mutex.synchronize { @worker_pool ||= ActionCable::Server::Worker.new(max_size: config.worker_pool_size) } end # Adapter used for all streams/broadcasting. def pubsub - @pubsub ||= config.pubsub_adapter.new(self) + @pubsub || @mutex.synchronize { @pubsub ||= config.pubsub_adapter.new(self) } end - # All the identifiers applied to the connection class associated with this server. + # All of the identifiers applied to the connection class associated with this server. def connection_identifiers - config.connection_class.identifiers + config.connection_class.call.identifiers end end diff --git a/actioncable/lib/action_cable/server/broadcasting.rb b/actioncable/lib/action_cable/server/broadcasting.rb index 4a26ed9269..8f93564113 100644 --- a/actioncable/lib/action_cable/server/broadcasting.rb +++ b/actioncable/lib/action_cable/server/broadcasting.rb @@ -1,45 +1,46 @@ module ActionCable module Server - # Broadcasting is how other parts of your application can send messages to the channel subscribers. As explained in Channel, most of the time, these + # 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 + # 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 shit fit for print' } + # # 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 coffescript, 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'] + # # 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>. It'll automatically be JSON encoded. - def broadcast(broadcasting, message) - broadcaster_for(broadcasting).broadcast(message) + # 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 a object that + # 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) - Broadcaster.new(self, broadcasting) + def broadcaster_for(broadcasting, coder: ActiveSupport::JSON) + Broadcaster.new(self, String(broadcasting), coder: coder) end private class Broadcaster - attr_reader :server, :broadcasting + attr_reader :server, :broadcasting, :coder - def initialize(server, broadcasting) - @server, @broadcasting = server, broadcasting + def initialize(server, broadcasting, coder:) + @server, @broadcasting, @coder = server, broadcasting, coder end def broadcast(message) - server.logger.info "[ActionCable] Broadcasting to #{broadcasting}: #{message}" - server.pubsub.broadcast broadcasting, ActiveSupport::JSON.encode(message) + server.logger.info "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}" + encoded = coder ? coder.encode(message) : message + server.pubsub.broadcast broadcasting, encoded end end end diff --git a/actioncable/lib/action_cable/server/configuration.rb b/actioncable/lib/action_cable/server/configuration.rb index 9a248933c4..ada1ac22cc 100644 --- a/actioncable/lib/action_cable/server/configuration.rb +++ b/actioncable/lib/action_cable/server/configuration.rb @@ -1,37 +1,22 @@ module ActionCable module Server - # An instance of this configuration object is available via ActionCable.server.config, which allows you to tweak the configuration points + # 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 :connection_class, :worker_pool_size - attr_accessor :channel_load_paths + attr_accessor :use_faye, :connection_class, :worker_pool_size attr_accessor :disable_request_forgery_protection, :allowed_request_origins - attr_accessor :cable, :url + attr_accessor :cable, :url, :mount_path def initialize @log_tags = [] - @connection_class = ApplicationCable::Connection - @worker_pool_size = 100 - - @channel_load_paths = [Rails.root.join('app/channels')] + @connection_class = -> { ActionCable::Connection::Base } + @worker_pool_size = 4 @disable_request_forgery_protection = false end - def channel_paths - @channel_paths ||= channel_load_paths.flat_map do |path| - Dir["#{path}/**/*_channel.rb"] - end - 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. @@ -50,6 +35,22 @@ module ActionCable adapter = 'PostgreSQL' if adapter == 'Postgresql' "ActionCable::SubscriptionAdapter::#{adapter}".constantize end + + def event_loop_class + if use_faye + ActionCable::Connection::FayeEventLoop + else + ActionCable::Connection::StreamEventLoop + end + end + + def client_socket_class + if use_faye + ActionCable::Connection::FayeClientSocket + else + ActionCable::Connection::ClientSocket + end + end end end end diff --git a/actioncable/lib/action_cable/server/connections.rb b/actioncable/lib/action_cable/server/connections.rb index 47dcea8c20..5e61b4e335 100644 --- a/actioncable/lib/action_cable/server/connections.rb +++ b/actioncable/lib/action_cable/server/connections.rb @@ -1,9 +1,8 @@ module ActionCable module Server - # Collection class for all the connections that's been established on this specific server. Remember, usually you'll run many cable servers, so - # you can't use this collection as an full list of all the connections established against your application. Use RemoteConnections for that. - # As such, this is primarily for internal use. - module Connections + # 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 @@ -19,13 +18,11 @@ module ActionCable 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 receive and send to it. So there's a 3 second heartbeat running on all connections. If the beat fails, we automatically + # 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 - EM.next_tick do - @heartbeat_timer ||= EventMachine.add_periodic_timer(BEAT_INTERVAL) do - EM.next_tick { connections.map(&:beat) } - end + @heartbeat_timer ||= event_loop.timer(BEAT_INTERVAL) do + event_loop.post { connections.map(&:beat) } end end diff --git a/actioncable/lib/action_cable/server/worker.rb b/actioncable/lib/action_cable/server/worker.rb index 3b6c6d44a1..a638ff72e7 100644 --- a/actioncable/lib/action_cable/server/worker.rb +++ b/actioncable/lib/action_cable/server/worker.rb @@ -4,60 +4,60 @@ require 'concurrent' module ActionCable module Server - # Worker used by Server.send_async to do connection work in threads. Only for internal use. - class Worker + # 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) - @pool = Concurrent::ThreadPoolExecutor.new( + @executor = Concurrent::ThreadPoolExecutor.new( min_threads: 1, max_threads: max_size, max_queue: 0, ) end - def async_invoke(receiver, method, *args) - @pool.post do - invoke(receiver, method, *args) - end + # Stop processing work: any work that has not already started + # running will be discarded from the queue + def halt + @executor.kill end - def invoke(receiver, method, *args) - begin - self.connection = receiver + def stopping? + @executor.shuttingdown? + end - run_callbacks :work do - receiver.send method, *args - end - rescue Exception => e - logger.error "There was an exception - #{e.class}(#{e.message})" - logger.error e.backtrace.join("\n") + def work(connection) + self.connection = connection - receiver.handle_exception if receiver.respond_to?(:handle_exception) - ensure - self.connection = nil + run_callbacks :work do + yield end + ensure + self.connection = nil end - def async_run_periodic_timer(channel, callback) - @pool.post do - run_periodic_timer(channel, callback) + def async_invoke(receiver, method, *args, connection: receiver) + @executor.post do + invoke(receiver, method, *args, connection: connection) end end - def run_periodic_timer(channel, callback) - begin - self.connection = channel.connection + 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") - run_callbacks :work do - callback.respond_to?(:call) ? channel.instance_exec(&callback) : channel.send(callback) + receiver.handle_exception if receiver.respond_to?(:handle_exception) end - ensure - self.connection = nil 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 index ecece4e270..c1e4aa8103 100644 --- a/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb +++ b/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb @@ -1,7 +1,6 @@ module ActionCable module Server class Worker - # Clear active connections between units of work so the long-running channel or connection processes do not hoard connections. module ActiveRecordConnectionManagement extend ActiveSupport::Concern @@ -13,10 +12,8 @@ module ActionCable def with_database_connections connection.logger.tag(ActiveRecord::Base.logger) { yield } - ensure - ActiveRecord::Base.clear_active_connections! end end end end -end
\ No newline at end of file +end diff --git a/actioncable/lib/action_cable/subscription_adapter/async.rb b/actioncable/lib/action_cable/subscription_adapter/async.rb index 85d4892e4c..10b3ac8cd8 100644 --- a/actioncable/lib/action_cable/subscription_adapter/async.rb +++ b/actioncable/lib/action_cable/subscription_adapter/async.rb @@ -4,17 +4,22 @@ module ActionCable module SubscriptionAdapter class Async < Inline # :nodoc: private - def subscriber_map - @subscriber_map ||= AsyncSubscriberMap.new + 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(*) - ::EM.next_tick { super } + @event_loop.post { super } end def invoke_callback(*) - ::EM.next_tick { super } + @event_loop.post { super } 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 index 4a2a8d23a2..81357faead 100644 --- a/actioncable/lib/action_cable/subscription_adapter/inline.rb +++ b/actioncable/lib/action_cable/subscription_adapter/inline.rb @@ -1,6 +1,11 @@ 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 @@ -19,7 +24,11 @@ module ActionCable private def subscriber_map - @subscriber_map ||= SubscriberMap.new + @subscriber_map || @server.mutex.synchronize { @subscriber_map ||= new_subscriber_map } + end + + def new_subscriber_map + SubscriberMap.new end end end diff --git a/actioncable/lib/action_cable/subscription_adapter/postgresql.rb b/actioncable/lib/action_cable/subscription_adapter/postgresql.rb index 78f8aeb599..66c7852f6e 100644 --- a/actioncable/lib/action_cable/subscription_adapter/postgresql.rb +++ b/actioncable/lib/action_cable/subscription_adapter/postgresql.rb @@ -5,6 +5,11 @@ 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)}'") @@ -37,14 +42,15 @@ module ActionCable private def listener - @listener ||= Listener.new(self) + @listener || @server.mutex.synchronize { @listener ||= Listener.new(self, @server.event_loop) } end class Listener < SubscriberMap - def initialize(adapter) + def initialize(adapter, event_loop) super() @adapter = adapter + @event_loop = event_loop @queue = Queue.new @thread = Thread.new do @@ -63,7 +69,7 @@ module ActionCable case action when :listen pg_conn.exec("LISTEN #{pg_conn.escape_identifier channel}") - ::EM.next_tick(&callback) if callback + @event_loop.post(&callback) if callback when :unlisten pg_conn.exec("UNLISTEN #{pg_conn.escape_identifier channel}") when :shutdown @@ -93,7 +99,7 @@ module ActionCable end def invoke_callback(*) - ::EM.next_tick { super } + @event_loop.post { super } end end end diff --git a/actioncable/lib/action_cable/subscription_adapter/redis.rb b/actioncable/lib/action_cable/subscription_adapter/redis.rb index 3b86354621..65434f7107 100644 --- a/actioncable/lib/action_cable/subscription_adapter/redis.rb +++ b/actioncable/lib/action_cable/subscription_adapter/redis.rb @@ -1,41 +1,171 @@ -gem 'em-hiredis', '~> 0.3.0' +require 'thread' + gem 'redis', '~> 3.0' -require 'em-hiredis' 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, 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 + def subscribe(channel, callback, success_callback = nil) + listener.add_subscriber(channel, callback, success_callback) end - def unsubscribe(channel, message_callback) - redis_connection_for_subscriptions.pubsub.unsubscribe_proc(channel, message_callback) + def unsubscribe(channel, callback) + listener.remove_subscriber(channel, callback) end def shutdown - redis_connection_for_subscriptions.pubsub.close_connection - @redis_connection_for_subscriptions = nil + @listener.shutdown if @listener + end + + def redis_connection_for_subscriptions + redis_connection end private - def redis_connection_for_subscriptions - @redis_connection_for_subscriptions ||= EM::Hiredis.connect(@server.config.cable[:url]).tap do |redis| - redis.on(:reconnect_failed) do - @logger.info "[ActionCable] Redis reconnect failed." - end - end + def listener + @listener || @server.mutex.synchronize { @listener ||= Listener.new(self, @server.event_loop) } end def redis_connection_for_broadcasts - @redis_connection_for_broadcasts ||= ::Redis.new(@server.config.cable) + @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 diff --git a/actioncable/lib/assets/javascripts/action_cable/connection.coffee b/actioncable/lib/assets/javascripts/action_cable/connection.coffee deleted file mode 100644 index fbd7dbd35b..0000000000 --- a/actioncable/lib/assets/javascripts/action_cable/connection.coffee +++ /dev/null @@ -1,81 +0,0 @@ -# Encapsulate the cable connection held by the consumer. This is an internal class not intended for direct user manipulation. - -{message_types} = ActionCable.INTERNAL - -class ActionCable.Connection - @reopenDelay: 500 - - constructor: (@consumer) -> - @open() - - send: (data) -> - if @isOpen() - @webSocket.send(JSON.stringify(data)) - true - else - false - - open: => - if @webSocket and not @isState("closed") - throw new Error("Existing connection must be closed before opening") - else - @webSocket = new WebSocket(@consumer.url) - @installEventHandlers() - true - - close: -> - @webSocket?.close() - - reopen: -> - if @isState("closed") - @open() - else - try - @close() - finally - setTimeout(@open, @constructor.reopenDelay) - - isOpen: -> - @isState("open") - - # Private - - 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 - - events: - message: (event) -> - {identifier, message, type} = JSON.parse(event.data) - - switch type - when message_types.confirmation - @consumer.subscriptions.notify(identifier, "connected") - when message_types.rejection - @consumer.subscriptions.reject(identifier) - else - @consumer.subscriptions.notify(identifier, "received", message) - - open: -> - @disconnected = false - @consumer.subscriptions.reload() - - close: -> - @disconnect() - - error: -> - @disconnect() - - disconnect: -> - return if @disconnected - @disconnected = true - @consumer.subscriptions.notifyAll("disconnected") diff --git a/actioncable/lib/assets/javascripts/action_cable/connection_monitor.coffee b/actioncable/lib/assets/javascripts/action_cable/connection_monitor.coffee deleted file mode 100644 index 99b9a1c6d5..0000000000 --- a/actioncable/lib/assets/javascripts/action_cable/connection_monitor.coffee +++ /dev/null @@ -1,79 +0,0 @@ -# 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) - - identifier: ActionCable.INTERNAL.identifiers.ping - - constructor: (@consumer) -> - @consumer.subscriptions.add(this) - @start() - - connected: -> - @reset() - @pingedAt = now() - delete @disconnectedAt - - disconnected: -> - @disconnectedAt = now() - - received: -> - @pingedAt = now() - - reset: -> - @reconnectAttempts = 0 - - start: -> - @reset() - delete @stoppedAt - @startedAt = now() - @poll() - document.addEventListener("visibilitychange", @visibilityDidChange) - - stop: -> - @stoppedAt = now() - document.removeEventListener("visibilitychange", @visibilityDidChange) - - poll: -> - setTimeout => - unless @stoppedAt - @reconnectIfStale() - @poll() - , @getInterval() - - getInterval: -> - {min, max} = @constructor.pollInterval - interval = 5 * Math.log(@reconnectAttempts + 1) - clamp(interval, min, max) * 1000 - - reconnectIfStale: -> - if @connectionIsStale() - @reconnectAttempts++ - unless @disconnectedRecently() - @consumer.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 @consumer.connection.isOpen() - @consumer.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/lib/rails/generators/channel/USAGE b/actioncable/lib/rails/generators/channel/USAGE index 27a934c689..6249553c22 100644 --- a/actioncable/lib/rails/generators/channel/USAGE +++ b/actioncable/lib/rails/generators/channel/USAGE @@ -3,7 +3,7 @@ Description: Stubs out a new cable channel for the server (in Ruby) and client (in CoffeeScript). Pass the channel name, either CamelCased or under_scored, and an optional list of channel actions as arguments. - Note: Turn on the cable connection in app/assets/javascript/cable.coffee after generating any channels. + Note: Turn on the cable connection in app/assets/javascript/cable.js after generating any channels. Example: ======== diff --git a/actioncable/lib/rails/generators/channel/channel_generator.rb b/actioncable/lib/rails/generators/channel/channel_generator.rb index c5d398810a..47232252e3 100644 --- a/actioncable/lib/rails/generators/channel/channel_generator.rb +++ b/actioncable/lib/rails/generators/channel/channel_generator.rb @@ -13,13 +13,34 @@ module Rails template "channel.rb", File.join('app/channels', class_path, "#{file_name}_channel.rb") if options[:assets] - template "assets/channel.coffee", File.join('app/assets/javascripts/channels', class_path, "#{file_name}.coffee") + if self.behavior == :invoke + template "assets/cable.js", "app/assets/javascripts/cable.js" + end + + js_template "assets/channel", File.join('app/assets/javascripts/channels', class_path, "#{file_name}") end + + generate_application_cable_files end protected def file_name - @_file_name ||= super.gsub(/\_channel/i, '') + @_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 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..17a85f60f9 --- /dev/null +++ b/actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb @@ -0,0 +1,6 @@ +# 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..93f28c4306 --- /dev/null +++ b/actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb @@ -0,0 +1,6 @@ +# 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.js b/actioncable/lib/rails/generators/channel/templates/assets/channel.js new file mode 100644 index 0000000000..ab0e68b11a --- /dev/null +++ b/actioncable/lib/rails/generators/channel/templates/assets/channel.js @@ -0,0 +1,18 @@ +App.<%= class_name.underscore %> = App.cable.subscriptions.create("<%= class_name %>Channel", { + connected: function() { + // Called when the subscription is ready for use on the server + }, + + disconnected: function() { + // Called when the subscription has been terminated by the server + }, + + received: function(data) { + // Called when there's incoming data on the websocket for this channel + }<%= actions.any? ? ",\n" : '' %> +<% actions.each do |action| -%> + <%=action %>: function() { + return this.perform('<%= action %>'); + }<%= action == actions[-1] ? '' : ",\n" %> +<% end -%> +}); diff --git a/actioncable/lib/rails/generators/channel/templates/channel.rb b/actioncable/lib/rails/generators/channel/templates/channel.rb index 6cf04ee61f..7bff3341c1 100644 --- a/actioncable/lib/rails/generators/channel/templates/channel.rb +++ b/actioncable/lib/rails/generators/channel/templates/channel.rb @@ -1,4 +1,4 @@ -# Be sure to restart your server when you modify this file. Action Cable runs in an EventMachine loop that does not support auto reloading. +# 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 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 index d41bf3064b..daa782eeb3 100644 --- a/actioncable/test/channel/base_test.rb +++ b/actioncable/test/channel/base_test.rb @@ -146,12 +146,12 @@ class ActionCable::Channel::BaseTest < ActiveSupport::TestCase test "transmitting data" do @channel.perform_action 'action' => :get_latest - expected = ActiveSupport::JSON.encode "identifier" => "{id: 1}", "message" => { "data" => "latest" } + expected = { "identifier" => "{id: 1}", "message" => { "data" => "latest" }} assert_equal expected, @connection.last_transmission end test "subscription confirmation" do - expected = ActiveSupport::JSON.encode "identifier" => "{id: 1}", "type" => "confirm_subscription" + expected = { "identifier" => "{id: 1}", "type" => "confirm_subscription" } assert_equal expected, @connection.last_transmission end @@ -166,6 +166,81 @@ class ActionCable::Channel::BaseTest < ActiveSupport::TestCase 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 diff --git a/actioncable/test/channel/periodic_timers_test.rb b/actioncable/test/channel/periodic_timers_test.rb index 1590a12f09..03464003cf 100644 --- a/actioncable/test/channel/periodic_timers_test.rb +++ b/actioncable/test/channel/periodic_timers_test.rb @@ -1,12 +1,21 @@ 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 - periodically -> { ping }, every: 5 + # 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 @@ -19,22 +28,41 @@ class ActionCable::Channel::PeriodicTimersTest < ActiveSupport::TestCase test "periodic timers definition" do timers = ChatChannel.periodic_timers - assert_equal 2, timers.size + assert_equal 3, timers.size - first_timer = timers[0] - assert_kind_of Proc, first_timer[0] - assert_equal 5, first_timer[1][:every] + timers.each_with_index do |timer, i| + assert_kind_of Proc, timer[0] + assert_equal i+1, timer[1][:every] + end + end - second_timer = timers[1] - assert_equal :send_updates, second_timer[0] - assert_equal 1, second_timer[1][:every] + 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 - EventMachine::PeriodicTimer.expects(:new).times(2).returns(true) + @connection.server.event_loop.expects(:timer).times(3).returns(stub(shutdown: nil)) channel = ChatChannel.new @connection, "{id: 1}", { id: 1 } - channel.expects(:stop_periodic_timers).once 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 index aa93396d44..15db57d6ba 100644 --- a/actioncable/test/channel/rejection_test.rb +++ b/actioncable/test/channel/rejection_test.rb @@ -18,7 +18,7 @@ class ActionCable::Channel::RejectionTest < ActiveSupport::TestCase @connection.expects(:subscriptions).returns mock().tap { |m| m.expects(:remove_subscription).with instance_of(SecretChannel) } @channel = SecretChannel.new @connection, "{id: 1}", { id: 1 } - expected = ActiveSupport::JSON.encode "identifier" => "{id: 1}", "type" => "reject_subscription" + expected = { "identifier" => "{id: 1}", "type" => "reject_subscription" } assert_equal expected, @connection.last_transmission end diff --git a/actioncable/test/channel/stream_test.rb b/actioncable/test/channel/stream_test.rb index 3fa2b291b7..38543920d3 100644 --- a/actioncable/test/channel/stream_test.rb +++ b/actioncable/test/channel/stream_test.rb @@ -2,12 +2,20 @@ require 'test_helper' require 'stubs/test_connection' require 'stubs/room' -class ActionCable::Channel::StreamTest < ActionCable::TestCase +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}" + stream_from "test_room_#{@room.id}", coder: pick_coder(params[:coder]) end end @@ -15,66 +23,157 @@ class ActionCable::Channel::StreamTest < ActionCable::TestCase 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 - 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 } + module DummyEncoder + extend self + def encode(*) '{ "foo": "encoded" }' end + def decode(*) { foo: 'decoded' } end + end - connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe) } - channel.unsubscribe_from_channel + class SymbolChannel < ActionCable::Channel::Base + def subscribed + stream_from :channel end end - test "stream_for" do - run_in_eventmachine do - connection = TestConnection.new - EM.next_tick do - connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("action_cable:channel:stream_test:chat:Room#1-Campfire", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) } + 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 - channel = ChatChannel.new connection, "" - channel.stream_for Room.new(1) + 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 - end - test "stream_from subscription confirmation" do - EM.run do - connection = TestConnection.new + 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 + ChatChannel.new connection, "{id: 1}", { id: 1 } + assert_nil connection.last_transmission - EM::Timer.new(0.1) do - expected = ActiveSupport::JSON.encode "identifier" => "{id: 1}", "type" => "confirm_subscription" - connection.transmit(expected) + wait_for_async - assert_equal expected, connection.last_transmission, "Did not receive subscription confirmation within 0.1s" + confirmation = { "identifier" => "{id: 1}", "type" => "confirm_subscription" } + connection.transmit(confirmation) - EM.run_deferred_callbacks - EM.stop + assert_equal confirmation, connection.last_transmission, "Did not receive subscription confirmation within 0.1s" end end - end - test "subscription confirmation should only be sent out once" do - EM.run do - connection = TestConnection.new + 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 + channel = ChatChannel.new connection, "test_channel" + channel.send_confirmation + channel.send_confirmation - EM.run_deferred_callbacks + wait_for_async - expected = ActiveSupport::JSON.encode "identifier" => "test_channel", "type" => "confirm_subscription" - assert_equal expected, connection.last_transmission, "Did not receive subscription confirmation" + expected = { "identifier" => "test_channel", "type" => "confirm_subscription" } + assert_equal expected, connection.last_transmission, "Did not receive subscription confirmation" - assert_equal 1, connection.transmissions.size - EM.stop + 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 ) + 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_test.rb b/actioncable/test/client_test.rb new file mode 100644 index 0000000000..f4b4a53aea --- /dev/null +++ b/actioncable/test/client_test.rb @@ -0,0 +1,275 @@ +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 + + 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 + + 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 + + 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: 'ClientTest::EchoChannel') + assert_equal({"identifier"=>"{\"channel\":\"ClientTest::EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message) + c.send_message command: 'message', identifier: JSON.generate(channel: 'ClientTest::EchoChannel'), data: JSON.generate(action: 'ding', message: 'hello') + assert_equal({"identifier"=>"{\"channel\":\"ClientTest::EchoChannel\"}", "message"=>{"dong"=>"hello"}}, c.read_message) + 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: 'ClientTest::EchoChannel') + assert_equal({"identifier"=>'{"channel":"ClientTest::EchoChannel"}', "type"=>"confirm_subscription"}, c.read_message) + c.send_message command: 'message', identifier: JSON.generate(channel: 'ClientTest::EchoChannel'), data: JSON.generate(action: 'ding', message: 'hello') + assert_equal({"identifier"=>'{"channel":"ClientTest::EchoChannel"}', "message"=>{"dong"=>"hello"}}, c.read_message) + barrier_1.wait WAIT_WHEN_EXPECTING_EVENT + c.send_message command: 'message', identifier: JSON.generate(channel: 'ClientTest::EchoChannel'), data: JSON.generate(action: 'bulk', message: 'hello') + barrier_2.wait WAIT_WHEN_EXPECTING_EVENT + assert_equal clients.size, c.read_messages(clients.size).size + } }.each(&:wait!) + + 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: 'ClientTest::EchoChannel') + assert_equal({"identifier"=>'{"channel":"ClientTest::EchoChannel"}', "type"=>"confirm_subscription"}, c.read_message) + c.send_message command: 'message', identifier: JSON.generate(channel: 'ClientTest::EchoChannel'), data: JSON.generate(action: 'ding', message: 'hello') + assert_equal({"identifier"=>'{"channel":"ClientTest::EchoChannel"}', "message"=>{"dong"=>"hello"}}, c.read_message) + } }.each(&:wait!) + + 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: 'ClientTest::EchoChannel') + assert_equal({"identifier"=>"{\"channel\":\"ClientTest::EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message) + c.send_message command: 'message', identifier: JSON.generate(channel: 'ClientTest::EchoChannel'), data: JSON.generate(action: 'delay', message: 'hello') + c.close # disappear before write + + c = faye_client(port) + assert_equal({"type" => "welcome"}, c.read_message) # pop the first welcome message off the stack + c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'ClientTest::EchoChannel') + assert_equal({"identifier"=>"{\"channel\":\"ClientTest::EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message) + c.send_message command: 'message', identifier: JSON.generate(channel: 'ClientTest::EchoChannel'), data: JSON.generate(action: 'ding', message: 'hello') + assert_equal({"identifier"=>'{"channel":"ClientTest::EchoChannel"}', "message"=>{"dong"=>"hello"}}, c.read_message) + c.close # disappear before read + end + end + + def test_unsubscribe_client + with_puma_server do |port| + app = ActionCable.server + identifier = JSON.generate(channel: 'ClientTest::EchoChannel') + + c = faye_client(port) + assert_equal({"type" => "welcome"}, c.read_message) + c.send_message command: 'subscribe', identifier: identifier + assert_equal({"identifier"=>"{\"channel\":\"ClientTest::EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message) + assert_equal(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: 'ClientTest::EchoChannel') + assert_equal({"identifier"=>"{\"channel\":\"ClientTest::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 index 87d0e79ef3..a0506cb9c0 100644 --- a/actioncable/test/connection/authorization_test.rb +++ b/actioncable/test/connection/authorization_test.rb @@ -20,7 +20,7 @@ class ActionCable::Connection::AuthorizationTest < ActionCable::TestCase server.config.allowed_request_origins = %w( http://rubyonrails.com ) env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket', - 'HTTP_ORIGIN' => 'http://rubyonrails.com' + 'HTTP_HOST' => 'localhost', 'HTTP_ORIGIN' => 'http://rubyonrails.com' connection = Connection.new(server, env) connection.websocket.expects(:close) diff --git a/actioncable/test/connection/base_test.rb b/actioncable/test/connection/base_test.rb index 182562db82..d7e1041e68 100644 --- a/actioncable/test/connection/base_test.rb +++ b/actioncable/test/connection/base_test.rb @@ -1,5 +1,6 @@ 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 @@ -37,6 +38,8 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase connection.process assert connection.websocket.possible? + + wait_for_async assert connection.websocket.alive? end end @@ -53,16 +56,15 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase test "on connection open" do run_in_eventmachine do connection = open_connection - connection.process - connection.websocket.expects(:transmit).with(regexp_matches(/\_ping/)) + connection.websocket.expects(:transmit).with({ type: "welcome" }.to_json) connection.message_buffer.expects(:process!) - # Allow EM to run on_open callback - EM.next_tick do - assert_equal [ connection ], @server.connections - assert connection.connected - end + connection.process + wait_for_async + + assert_equal [ connection ], @server.connections + assert connection.connected end end @@ -72,12 +74,12 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase connection.process # Setup the connection - EventMachine.stubs(:add_periodic_timer).returns(true) - connection.send :on_open + connection.server.stubs(:timer).returns(true) + connection.send :handle_open assert connection.connected connection.subscriptions.expects(:unsubscribe_from_all) - connection.send :on_close + connection.send :handle_close assert ! connection.connected assert_equal [], @server.connections @@ -107,10 +109,30 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase 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_ORIGIN' => 'http://rubyonrails.com' + 'HTTP_HOST' => 'localhost', 'HTTP_ORIGIN' => 'http://rubyonrails.com' Connection.new(@server, env) 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 index a29f65fb97..2d516b0533 100644 --- a/actioncable/test/connection/cross_site_forgery_test.rb +++ b/actioncable/test/connection/cross_site_forgery_test.rb @@ -76,6 +76,6 @@ class ActionCable::Connection::CrossSiteForgeryTest < ActionCable::TestCase def env_for_origin(origin) Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket', 'SERVER_NAME' => HOST, - 'HTTP_ORIGIN' => origin + 'HTTP_HOST' => HOST, 'HTTP_ORIGIN' => origin end end diff --git a/actioncable/test/connection/identifier_test.rb b/actioncable/test/connection/identifier_test.rb index a110dfdee0..b48d9af809 100644 --- a/actioncable/test/connection/identifier_test.rb +++ b/actioncable/test/connection/identifier_test.rb @@ -40,8 +40,7 @@ class ActionCable::Connection::IdentifierTest < ActionCable::TestCase open_connection_with_stubbed_pubsub @connection.websocket.expects(:close) - message = ActiveSupport::JSON.encode('type' => 'disconnect') - @connection.process_internal_message message + @connection.process_internal_message 'type' => 'disconnect' end end @@ -50,8 +49,7 @@ class ActionCable::Connection::IdentifierTest < ActionCable::TestCase open_connection_with_stubbed_pubsub @connection.websocket.expects(:close).never - message = ActiveSupport::JSON.encode('type' => 'unknown') - @connection.process_internal_message message + @connection.process_internal_message 'type' => 'unknown' end end @@ -64,14 +62,14 @@ class ActionCable::Connection::IdentifierTest < ActionCable::TestCase end def open_connection(server:) - env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket' + 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 + @connection.send :handle_open end def close_connection - @connection.send :on_close + @connection.send :handle_close end end diff --git a/actioncable/test/connection/multiple_identifiers_test.rb b/actioncable/test/connection/multiple_identifiers_test.rb index 55a9f96cb3..484e73bb30 100644 --- a/actioncable/test/connection/multiple_identifiers_test.rb +++ b/actioncable/test/connection/multiple_identifiers_test.rb @@ -28,14 +28,14 @@ class ActionCable::Connection::MultipleIdentifiersTest < ActionCable::TestCase end def open_connection(server:) - env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket' + 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 + @connection.send :handle_open end def close_connection - @connection.send :on_close + @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 index 9d0bda83ef..eca0c31060 100644 --- a/actioncable/test/connection/string_identifier_test.rb +++ b/actioncable/test/connection/string_identifier_test.rb @@ -30,7 +30,7 @@ class ActionCable::Connection::StringIdentifierTest < ActionCable::TestCase end def open_connection - env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket' + env = Rack::MockRequest.env_for "/test", 'HTTP_HOST' => 'localhost', 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket' @connection = Connection.new(@server, env) @connection.process diff --git a/actioncable/test/connection/subscriptions_test.rb b/actioncable/test/connection/subscriptions_test.rb index 62e41484fe..a5b1e5dcf3 100644 --- a/actioncable/test/connection/subscriptions_test.rb +++ b/actioncable/test/connection/subscriptions_test.rb @@ -24,7 +24,6 @@ class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase 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 @@ -82,7 +81,7 @@ class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase end end - test "unsubscrib from all" do + test "unsubscribe from all" do run_in_eventmachine do setup_connection @@ -107,7 +106,7 @@ class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase end def setup_connection - env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket' + 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) diff --git a/actioncable/test/javascript/src/test.coffee b/actioncable/test/javascript/src/test.coffee new file mode 100644 index 0000000000..eb95fb2604 --- /dev/null +++ b/actioncable/test/javascript/src/test.coffee @@ -0,0 +1,3 @@ +#= require action_cable +#= require ./test_helpers +#= require_tree ./unit diff --git a/actioncable/test/javascript/src/test_helpers/consumer_test_helper.coffee b/actioncable/test/javascript/src/test_helpers/consumer_test_helper.coffee new file mode 100644 index 0000000000..6b145dede8 --- /dev/null +++ b/actioncable/test/javascript/src/test_helpers/consumer_test_helper.coffee @@ -0,0 +1,37 @@ +#= require mock-socket + +{TestHelpers} = ActionCable + +TestHelpers.consumerTest = (name, options = {}, callback) -> + unless callback? + callback = options + options = {} + + options.url ?= TestHelpers.testURL + + QUnit.test name, (assert) -> + doneAsync = assert.async() + + ActionCable.WebSocket = MockWebSocket + server = new MockServer options.url + consumer = ActionCable.createConsumer(options.url) + + server.on "connection", -> + clients = server.clients() + assert.equal clients.length, 1 + assert.equal clients[0].readyState, WebSocket.OPEN + + done = -> + consumer.disconnect() + server.close() + doneAsync() + + testData = {assert, consumer, server, done} + + if options.connect is false + callback(testData) + else + server.on "connection", -> + testData.client = server.clients()[0] + callback(testData) + consumer.connect() 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..d36524d9cc --- /dev/null +++ b/actioncable/test/javascript/src/test_helpers/index.coffee @@ -0,0 +1,8 @@ +#= require_self +#= require_tree . + +ActionCable.TestHelpers = + testURL: "ws://cable.example.com/" + +originalWebSocket = ActionCable.WebSocket +QUnit.testDone -> ActionCable.WebSocket = originalWebSocket 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..3944f3a7f6 --- /dev/null +++ b/actioncable/test/javascript/src/unit/action_cable_test.coffee @@ -0,0 +1,41 @@ +{module, test} = QUnit +{testURL} = ActionCable.TestHelpers + +module "ActionCable", -> + module "Adapters", -> + module "WebSocket", -> + test "default is window.WebSocket", (assert) -> + assert.equal ActionCable.WebSocket, window.WebSocket + + test "configurable", (assert) -> + ActionCable.WebSocket = "" + assert.equal ActionCable.WebSocket, "" + + module "logger", -> + test "default is window.console", (assert) -> + assert.equal ActionCable.logger, window.console + + test "configurable", (assert) -> + ActionCable.logger = "" + assert.equal ActionCable.logger, "" + + 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..cf8a592255 --- /dev/null +++ b/actioncable/test/javascript/src/unit/consumer_test.coffee @@ -0,0 +1,11 @@ +{module, test} = QUnit +{consumerTest} = ActionCable.TestHelpers + +module "ActionCable.Consumer", -> + consumerTest "#connect", connect: false, ({consumer, server, done}) -> + server.on("connection", done) + consumer.connect() + + consumerTest "#disconnect", ({consumer, client, done}) -> + client.addEventListener("close", done) + consumer.disconnect() 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/test_connection.rb b/actioncable/test/stubs/test_connection.rb index da98201900..885450dda6 100644 --- a/actioncable/test/stubs/test_connection.rb +++ b/actioncable/test/stubs/test_connection.rb @@ -1,25 +1,33 @@ require 'stubs/user' class TestConnection - attr_reader :identifiers, :logger, :current_user, :transmissions + attr_reader :identifiers, :logger, :current_user, :server, :transmissions - def initialize(user = User.new("lifo")) + 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 pubsub - SuccessAdapter.new(TestServer.new) + def transmit(cable_message) + @transmissions << encode(cable_message) end - def transmit(data) - @transmissions << data + def last_transmission + decode @transmissions.last if @transmissions.any? end - def last_transmission - @transmissions.last + 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 index 6e6541a952..b86f422a13 100644 --- a/actioncable/test/stubs/test_server.rb +++ b/actioncable/test/stubs/test_server.rb @@ -2,18 +2,37 @@ require 'ostruct' class TestServer include ActionCable::Server::Connections + include ActionCable::Server::Broadcasting - attr_reader :logger, :config + attr_reader :logger, :config, :mutex - def initialize + def initialize(subscription_adapter: SuccessAdapter) @logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new) - @config = OpenStruct.new(log_tags: [], subscription_adapter: SuccessAdapter) + + @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 - @config.subscription_adapter.new(self) + @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 send_async + def worker_pool + @worker_pool ||= ActionCable::Server::Worker.new(max_size: 5) end end diff --git a/actioncable/test/subscription_adapter/base_test.rb b/actioncable/test/subscription_adapter/base_test.rb index 7a7ae131e6..256dce673f 100644 --- a/actioncable/test/subscription_adapter/base_test.rb +++ b/actioncable/test/subscription_adapter/base_test.rb @@ -43,7 +43,7 @@ class ActionCable::SubscriptionAdapter::BaseTest < ActionCable::TestCase assert_respond_to(SuccessAdapter.new(@server), :broadcast) - assert_nothing_raised NotImplementedError do + assert_nothing_raised do broadcast end end @@ -55,7 +55,7 @@ class ActionCable::SubscriptionAdapter::BaseTest < ActionCable::TestCase assert_respond_to(SuccessAdapter.new(@server), :subscribe) - assert_nothing_raised NotImplementedError do + assert_nothing_raised do subscribe end end @@ -66,7 +66,7 @@ class ActionCable::SubscriptionAdapter::BaseTest < ActionCable::TestCase assert_respond_to(SuccessAdapter.new(@server), :unsubscribe) - assert_nothing_raised NotImplementedError do + assert_nothing_raised do unsubscribe end end diff --git a/actioncable/test/subscription_adapter/common.rb b/actioncable/test/subscription_adapter/common.rb index d4a13be889..285c690df0 100644 --- a/actioncable/test/subscription_adapter/common.rb +++ b/actioncable/test/subscription_adapter/common.rb @@ -1,7 +1,6 @@ require 'test_helper' require 'concurrent' -require 'action_cable/process/logging' require 'active_support/core_ext/hash/indifferent_access' require 'pathname' @@ -10,23 +9,9 @@ module CommonSubscriptionAdapterTest WAIT_WHEN_NOT_EXPECTING_EVENT = 0.2 def setup - # TODO: ActionCable requires a *lot* of setup at the moment... - ::Object.const_set(:ApplicationCable, Module.new) - ::ApplicationCable.const_set(:Connection, Class.new(ActionCable::Connection::Base)) - - ::Object.const_set(:Rails, Module.new) - ::Rails.singleton_class.send(:define_method, :root) { Pathname.new(__dir__) } - server = ActionCable::Server::Base.new - server.config = ActionCable::Server::Configuration.new - inner_logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN } - server.config.logger = ActionCable::Connection::TaggedLoggerProxy.new(inner_logger, tags: []) - - - # and now the "real" setup for our test: - spawn_eventmachine - server.config.cable = cable_config.with_indifferent_access + server.config.use_faye = ENV['FAYE'].present? adapter_klass = server.config.pubsub_adapter @@ -35,17 +20,7 @@ module CommonSubscriptionAdapterTest end def teardown - @tx_adapter.shutdown if @tx_adapter && @tx_adapter != @rx_adapter - @rx_adapter.shutdown if @rx_adapter - - begin - ::Object.send(:remove_const, :ApplicationCable) - rescue NameError - end - begin - ::Object.send(:remove_const, :Rails) - rescue NameError - end + [@rx_adapter, @tx_adapter].uniq.each(&:shutdown) 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/postgresql_test.rb b/actioncable/test/subscription_adapter/postgresql_test.rb index 64c632b0cd..214352a0b2 100644 --- a/actioncable/test/subscription_adapter/postgresql_test.rb +++ b/actioncable/test/subscription_adapter/postgresql_test.rb @@ -15,8 +15,16 @@ class PostgresqlAdapterTest < ActionCable::TestCase 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 diff --git a/actioncable/test/subscription_adapter/redis_test.rb b/actioncable/test/subscription_adapter/redis_test.rb index 8d52832c87..4f34dd86c9 100644 --- a/actioncable/test/subscription_adapter/redis_test.rb +++ b/actioncable/test/subscription_adapter/redis_test.rb @@ -5,6 +5,12 @@ class RedisAdapterTest < ActionCable::TestCase include CommonSubscriptionAdapterTest def cable_config - { adapter: 'redis', url: 'redis://127.0.0.1:6379/12' } + { 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 index 6636ce078b..0a9ee7ce77 100644 --- a/actioncable/test/test_helper.rb +++ b/actioncable/test/test_helper.rb @@ -1,40 +1,73 @@ -require File.expand_path('../../../load_paths', __FILE__) - 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 } -require 'faye/websocket' -class << Faye::WebSocket - remove_method :ensure_reactor_running +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 + # 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 -class ActionCable::TestCase < ActiveSupport::TestCase +module EventMachineConcurrencyHelpers + def wait_for_async + EM.run_deferred_callbacks + end + def run_in_eventmachine + failure = nil EM.run do - yield - - EM.run_deferred_callbacks - EM.stop + 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 spawn_eventmachine - Thread.new { EventMachine.run } unless EventMachine.reactor_running? + 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 index 4a699cde27..e2c81fe312 100644 --- a/actioncable/test/worker_test.rb +++ b/actioncable/test/worker_test.rb @@ -17,7 +17,9 @@ class WorkerTest < ActiveSupport::TestCase end def logger - ActionCable.server.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 @@ -31,22 +33,12 @@ class WorkerTest < ActiveSupport::TestCase end test "invoke" do - @worker.invoke @receiver, :run + @worker.invoke @receiver, :run, connection: @receiver.connection assert_equal :run, @receiver.last_action end test "invoke with arguments" do - @worker.invoke @receiver, :process, "Hello" + @worker.invoke @receiver, :process, "Hello", connection: @receiver.connection assert_equal [ :process, "Hello" ], @receiver.last_action end - - test "running periodic timers with a proc" do - @worker.run_periodic_timer @receiver, @receiver.method(:run) - assert_equal :run, @receiver.last_action - end - - test "running periodic timers with a method" do - @worker.run_periodic_timer @receiver, :run - assert_equal :run, @receiver.last_action - end end diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index e74e0bc20a..3b9f503a0b 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,94 +1,6 @@ -## Rails 5.0.0.beta1 (December 18, 2015) ## +* Exception handling: use `rescue_from` to handle exceptions raised by + mailer actions, by message delivery, and by deferred delivery jobs. -* `config.force_ssl = true` will set - `config.action_mailer.default_url_options = { protocol: 'https' }`. + *Jeremy Daer* - *Andrew Kampjes* - -* Add `config.action_mailer.deliver_later_queue_name` configuration to set the - mailer queue name. - - *Chris McGrath* - -* `assert_emails` in block form use the given number as expected value. - This makes the error message much easier to understand. - - *Yuji Yaginuma* - -* Add support for inline images in mailer previews by using an interceptor - class to convert cid: urls in image src attributes to data urls. - - *Andrew White* - -* Mailer preview now uses `url_for` to fix links to emails for apps running on - a subdirectory. - - *Remo Mueller* - -* Mailer previews no longer crash when the `mail` method wasn't called - (`NullMail`). - - Fixes #19849. - - *Yves Senn* - -* Make sure labels and values line up in mailer previews. - - *Yves Senn* - -* Add `assert_enqueued_emails` and `assert_no_enqueued_emails`. - - Example: - - def test_emails - assert_enqueued_emails 2 do - ContactMailer.welcome.deliver_later - ContactMailer.welcome.deliver_later - end - end - - def test_no_emails - assert_no_enqueued_emails do - # No emails enqueued here - end - end - - *George Claghorn* - -* Add `_mailer` suffix to mailers created via generator, following the same - naming convention used in controllers and jobs. - - *Carlos Souza* - -* Remove deprecated `*_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/Rakefile b/actionmailer/Rakefile index 7197ea5e27..416c9ee33a 100644 --- a/actionmailer/Rakefile +++ b/actionmailer/Rakefile @@ -3,6 +3,8 @@ require 'rake/testtask' desc "Default Task" task default: [ :test ] +task :package + # Run the unit tests Rake::TestTask.new { |t| t.libs << "test" diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index fa6043fdd7..25e3bcb2e9 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -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/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index bb3cb1be45..e766221008 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. # @@ -144,7 +145,7 @@ module ActionMailer # mail.deliver_now # generates and sends the email now # # The <tt>ActionMailer::MessageDelivery</tt> class is a wrapper around a delegate that will call - # your method to generate the mail. If you want direct access to delegator, or <tt>Mail::Message</tt>, + # 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 @@ -163,7 +164,7 @@ module ActionMailer # # 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 @@ -288,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 default 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>: @@ -389,9 +390,9 @@ module ActionMailer # to use it. Defaults to <tt>true</tt>. # * <tt>:openssl_verify_mode</tt> - 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 (<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>, ...). + # of an OpenSSL verify constant (<tt>'none'</tt> or <tt>'peer'</tt>) or directly the constant + # (<tt>OpenSSL::SSL::VERIFY_NONE</tt> or <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>. @@ -419,6 +420,7 @@ module ActionMailer # * <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! @@ -430,6 +432,7 @@ module ActionMailer include AbstractController::Translation include AbstractController::AssetPaths include AbstractController::Callbacks + include AbstractController::Caching include ActionView::Layouts @@ -484,7 +487,7 @@ module ActionMailer end private :observer_class_for - # Returns the name of current mailer. This method is also being used as a path for a view lookup. + # Returns the name of the 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. def mailer_name @mailer_name ||= anonymous? ? "anonymous" : name.underscore @@ -664,7 +667,7 @@ module ActionMailer # # 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 encoding other than Base64 then you will need to pass encoding @@ -672,7 +675,7 @@ module ActionMailer # 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 } # @@ -947,6 +950,18 @@ module ActionMailer container.add_part(part) end + # 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) end end diff --git a/actionmailer/lib/action_mailer/delivery_job.rb b/actionmailer/lib/action_mailer/delivery_job.rb index 52772af2d3..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 { 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 4758b55a2a..571c8e7d2a 100644 --- a/actionmailer/lib/action_mailer/delivery_methods.rb +++ b/actionmailer/lib/action_mailer/delivery_methods.rb @@ -36,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 d28568b6a1..7dafceef2b 100644 --- a/actionmailer/lib/action_mailer/gem_version.rb +++ b/actionmailer/lib/action_mailer/gem_version.rb @@ -6,9 +6,9 @@ module ActionMailer module VERSION MAJOR = 5 - MINOR = 0 + MINOR = 1 TINY = 0 - PRE = "beta1.1" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb index 6d02b39225..419d6c7b93 100644 --- a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb +++ b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb @@ -3,7 +3,7 @@ require 'base64' module ActionMailer # Implements a mailer preview interceptor that converts image tag src attributes # that use inline cid: style urls to data: style urls so that they are visible - # when previewing a HTML email in a web browser. + # 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: diff --git a/actionmailer/lib/action_mailer/message_delivery.rb b/actionmailer/lib/action_mailer/message_delivery.rb index 5fcb5a0c88..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,29 +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 ||= begin - mailer = @mailer.new - mailer.process @mail_method, *@args - mailer.message - end + @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+ @@ -77,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: @@ -85,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/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb index fa707021c7..a727ed38e9 100644 --- a/actionmailer/lib/action_mailer/railtie.rb +++ b/actionmailer/lib/action_mailer/railtie.rb @@ -25,6 +25,7 @@ module ActionMailer 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 @@ -44,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 @@ -60,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 0aa15e31ba..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 @@ -61,12 +78,12 @@ module ActionMailer set_delivery_method :test @old_perform_deliveries = ActionMailer::Base.perform_deliveries ActionMailer::Base.perform_deliveries = true + ActionMailer::Base.deliveries.clear end def restore_test_deliveries # :nodoc: restore_delivery_method ActionMailer::Base.perform_deliveries = @old_perform_deliveries - ActionMailer::Base.deliveries.clear end def set_delivery_method(method) # :nodoc: @@ -75,6 +92,7 @@ module ActionMailer end def restore_delivery_method # :nodoc: + ActionMailer::Base.deliveries.clear ActionMailer::Base.delivery_method = @old_delivery_method end diff --git a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb index 5a5c9d32bb..01bdfb0685 100644 --- a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb +++ b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb @@ -9,13 +9,28 @@ module Rails def create_mailer_file template "mailer.rb", File.join('app/mailers', class_path, "#{file_name}_mailer.rb") + + in_root do + if self.behavior == :invoke && !File.exist?(application_mailer_file_name) + template 'application_mailer.rb', application_mailer_file_name + end + end end hook_for :template_engine, :test_framework 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 new file mode 100644 index 0000000000..00fb9bd48f --- /dev/null +++ b/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb @@ -0,0 +1,6 @@ +<% module_namespacing do -%> +class ApplicationMailer < ActionMailer::Base + default from: 'from@example.com' + layout 'mailer' +end +<% end %> diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index 285b2cfcb5..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 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 2786fe0d07..bcbd036f26 100644 --- a/actionmailer/test/delivery_methods_test.rb +++ b/actionmailer/test/delivery_methods_test.rb @@ -39,7 +39,7 @@ class DefaultsDeliveryMethodsTest < ActiveSupport::TestCase test "default sendmail settings" do settings = { location: '/usr/sbin/sendmail', - arguments: '-i -t' + arguments: '-i' } assert_equal settings, ActionMailer::Base.sendmail_settings end 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 6124ffeb52..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 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 b834cdd08c..aaed94d519 100644 --- a/actionmailer/test/message_delivery_test.rb +++ b/actionmailer/test/message_delivery_test.rb @@ -12,16 +12,23 @@ class MessageDeliveryTest < ActiveSupport::TestCase 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 @@ -93,4 +100,54 @@ class MessageDeliveryTest < ActiveSupport::TestCase @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/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 f6ffe45490..be911b147c 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,653 +1,2 @@ -* Response etags to always be weak: Prefixes 'W/' to value returned by - `ActionDispatch::Http::Cache::Response#etag=`, such that etags set in - `fresh_when` and `stale?` are weak. - Fixes #17556. - - *Abhishek Yadav* - -* Provide the name of HTTP Status code in assertions. - - *Sean Collins* - -* More explicit error message when running `rake routes`. `CONTROLLER` argument - can now be supplied in different ways: - `Rails::WelcomeController`, `Rails::Welcome`, `rails/welcome`. - - Fixes #22918. - - *Edouard Chin* - -* Allow `ActionController::Parameters` instances as an argument to URL - helper methods. An `ArgumentError` will be raised if the passed parameters - are not secure. - - Fixes #22832. - - *Prathamesh Sonpatki* - -* Add option for per-form CSRF tokens. - - *Greg Ose & Ben Toews* - -* Add tests and documentation for `ActionController::Renderers::use_renderers`. - - *Benjamin Fleischer* - -* Fix `ActionController::Parameters#convert_parameters_to_hashes` to return filtered - or unfiltered values based on from where it is called, `to_h` or `to_unsafe_h` - respectively. - - Fixes #22841. - - *Prathamesh Sonpatki* - -* Add `ActionController::Parameters#include?` - - *Justin Coyne* - -## Rails 5.0.0.beta1 (December 18, 2015) ## - -* Deprecate `redirect_to :back` in favor of `redirect_back`, which accepts a - required `fallback_location` argument, thus eliminating the possibility of a - `RedirectBackError`. - - *Derek Prior* - -* Add `redirect_back` method to `ActionController::Redirecting` to provide a - way to safely redirect to the `HTTP_REFERER` if it is present, falling back - to a provided redirect otherwise. - - *Derek Prior* - -* `ActionController::TestCase` will be moved to it's own gem in Rails 5.1 - - With the speed improvements made to `ActionDispatch::IntegrationTest` we no - longer need to keep two separate code bases for testing controllers. In - Rails 5.1 `ActionController::TestCase` will be deprecated and moved into a - gem outside of Rails source. - - This is a documentation deprecation so that going forward so new tests will use - `ActionDispatch::IntegrationTest` instead of `ActionController::TestCase`. - - *Eileen M. Uchitelle* - -* Add a `response_format` option to `ActionDispatch::DebugExceptions` - to configure the format of the response when errors occur in - development mode. - - If `response_format` is `:default` the debug info will be rendered - in an HTML page. In the other hand, if the provided value is `:api` - the debug info will be rendered in the original response format. - - *Jorge Bejar* - -* Change the `protect_from_forgery` prepend default to `false`. - - Per this comment - https://github.com/rails/rails/pull/18334#issuecomment-69234050 we want - `protect_from_forgery` to default to `prepend: false`. - - `protect_from_forgery` will now be inserted into the callback chain at the - point it is called in your application. This is useful for cases where you - want to `protect_from_forgery` after you perform required authentication - callbacks or other callbacks that are required to run after forgery protection. - - If you want `protect_from_forgery` callbacks to always run first, regardless of - position they are called in your application then you can add `prepend: true` - to your `protect_from_forgery` call. - - Example: - - ```ruby - protect_from_forgery prepend: true - ``` - - *Eileen M. Uchitelle* - -* In url_for, never append a question mark to the URL when the query string - is empty anyway. (It used to do that when called like `url_for(controller: - 'x', action: 'y', q: {})`.) - - *Paul Grayson* - -* Catch invalid UTF-8 querystring values and respond with BadRequest - - Check querystring params for invalid UTF-8 characters, and raise an - ActionController::BadRequest error if present. Previously these strings - would typically trigger errors further down the stack. - - *Grey Baker* - -* Parse RSS/ATOM responses as XML, not HTML. - - *Alexander Kaupanin* - -* Show helpful message in `BadRequest` exceptions due to invalid path - parameter encodings. - - Fixes #21923. - - *Agis Anastasopoulos* - -* Add the ability of returning arbitrary headers to `ActionDispatch::Static`. - - Now ActionDispatch::Static can accept HTTP headers so that developers - will have control of returning arbitrary headers like - 'Access-Control-Allow-Origin' when a response is delivered. They can be - configured with `#config`: - - Example: - - config.public_file_server.headers = { - "Cache-Control" => "public, max-age=60", - "Access-Control-Allow-Origin" => "http://rubyonrails.org" - } - - *Yuki Nishijima* - -* Allow multiple `root` routes in same scope level. Example: - - Example: - - root 'blog#show', constraints: ->(req) { Hostname.blog_site?(req.host) } - root 'landing#show' - - *Rafael Sales* - -* Fix regression in mounted engine named routes generation for app deployed to - a subdirectory. `relative_url_root` was prepended to the path twice (e.g. - "/subdir/subdir/engine_path" instead of "/subdir/engine_path") - - Fixes #20920. Fixes #21459. - - *Matthew Erhard* - -* `ActionDispatch::Response#new` no longer applies default headers. If you want - default headers applied to the response object, then call - `ActionDispatch::Response.create`. This change only impacts people who are - directly constructing an `ActionDispatch::Response` object. - -* Accessing mime types via constants like `Mime::HTML` is deprecated. Please - change code like this: - - Mime::HTML - - To this: - - Mime[:html] - - This change is so that Rails will not manage a list of constants, and fixes - an issue where if a type isn't registered you could possibly get the wrong - object. - - `Mime[:html]` is available in older versions of Rails, too, so you can - safely change libraries and plugins and maintain compatibility with - multiple versions of Rails. - -* `url_for` does not modify its arguments when generating polymorphic URLs. - - *Bernerd Schaefer* - -* 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. - - SSL redirect: - * Move `:host` and `:port` options within `redirect: { … }`. Deprecate. - * Introduce `:status` and `:body` to customize the redirect response. - The 301 permanent default makes it difficult to test the redirect and - back out of it since browsers remember the 301. Test with a 302 or 307 - instead, then switch to 301 once you're confident that all is well. - - HTTP Strict Transport Security (HSTS): - * Shorter max-age. Shorten the default max-age from 1 year to 180 days, - the low end for https://www.ssllabs.com/ssltest/ grading and greater - than the 18-week minimum to qualify for browser preload lists. - * Disabling HSTS. Setting `hsts: false` now sets `hsts { expires: 0 }` - instead of omitting the header. Omitting does nothing to disable HSTS - since browsers hang on to your previous settings until they expire. - Sending `{ hsts: { expires: 0 }}` flushes out old browser settings and - actually disables HSTS: - http://tools.ietf.org/html/rfc6797#section-6.1.1 - * HSTS Preload. Introduce `preload: true` to set the `preload` flag, - indicating that your site may be included in browser preload lists, - including Chrome, Firefox, Safari, IE11, and Edge. Submit your site: - https://hstspreload.appspot.com - - *Jeremy Daer* - -* Update `ActionController::TestSession#fetch` to behave more like - `ActionDispatch::Request::Session#fetch` when using non-string keys. - - *Jeremy Friesen* - -* Using strings or symbols for middleware class names is deprecated. Convert - things like this: - - middleware.use "Foo::Bar" - - to this: - - middleware.use Foo::Bar - -* `ActionController::TestSession` now accepts a default value as well as - a block for generating a default value based off the key provided. - - This fixes calls to `session#fetch` in `ApplicationController` instances that - take more two arguments or a block from raising `ArgumentError: wrong - number of arguments (2 for 1)` when performing controller tests. - - *Matthew Gerrior* - -* Fix `ActionController::Parameters#fetch` overwriting `KeyError` returned by - default block. - - *Jonas Schuber Erlandsson*, *Roque Pinel* - -* `ActionController::Parameters` no longer inherits from - `HashWithIndifferentAccess` - - Inheriting from `HashWithIndifferentAccess` allowed users to call any - enumerable methods on `Parameters` object, resulting in a risk of losing the - `permitted?` status or even getting back a pure `Hash` object instead of - a `Parameters` object with proper sanitization. - - By not inheriting from `HashWithIndifferentAccess`, we are able to make - sure that all methods that are defined in `Parameters` object will return - a proper `Parameters` object with a correct `permitted?` flag. - - *Prem Sichanugrist* - -* Replaced `ActiveSupport::Concurrency::Latch` with `Concurrent::CountDownLatch` - from the concurrent-ruby gem. - - *Jerry D'Antonio* - -* Add ability to filter parameters based on parent keys. - - # matches {credit_card: {code: "xxxx"}} - # doesn't match {file: { code: "xxxx"}} - config.filter_parameters += [ "credit_card.code" ] - - See #13897. - - *Guillaume Malette* - -* Deprecate passing first parameter as `Hash` and default status code for `head` method. - - *Mehmet Emin İNAÇ* - -* Adds`Rack::Utils::ParameterTypeError` and `Rack::Utils::InvalidParameterError` - to the rescue_responses hash in `ExceptionWrapper` (Rack recommends - integrators serve 400s for both of these). - - *Grey Baker* - -* Add support for API only apps. - `ActionController::API` is added as a replacement of - `ActionController::Base` for this kind of applications. - - *Santiago Pastorino*, *Jorge Bejar* - -* Remove `assigns` and `assert_template`. Both methods have been extracted - into a gem at https://github.com/rails/rails-controller-testing. - - See #18950. - - *Alan Guo Xiang Tan* - -* `FileHandler` and `Static` middleware initializers accept `index` argument - to configure the directory index file name. Defaults to `index` (as in - `index.html`). - - See #20017. - - *Eliot Sykes* - -* Deprecate `:nothing` option for `render` method. - - *Mehmet Emin İNAÇ* - -* Fix `rake routes` not showing the right format when - nesting multiple routes. - - See #18373. - - *Ravil Bayramgalin* - -* Add ability to override default form builder for a controller. - - class AdminController < ApplicationController - default_form_builder AdminFormBuilder - end - - *Kevin McPhillips* - -* For actions with no corresponding templates, render `head :no_content` - instead of raising an error. This allows for slimmer API controller - methods that simply work, without needing further instructions. - - See #19036. - - *Stephen Bussey* - -* Provide friendlier access to request variants. - - request.variant = :phone - request.variant.phone? # true - request.variant.tablet? # false - - request.variant = [:phone, :tablet] - request.variant.phone? # true - request.variant.desktop? # false - request.variant.any?(:phone, :desktop) # true - request.variant.any?(:desktop, :watch) # false - - *George Claghorn* - -* Fix regression where a gzip file response would have a Content-type, - even when it was a 304 status code. - - See #19271. - - *Kohei Suzuki* - -* Fix handling of empty `X_FORWARDED_HOST` header in `raw_host_with_port`. - - Previously, an empty `X_FORWARDED_HOST` header would cause - `Actiondispatch::Http:URL.raw_host_with_port` to return `nil`, causing - `Actiondispatch::Http:URL.host` to raise a `NoMethodError`. - - *Adam Forsyth* - -* Allow `Bearer` as token-keyword in `Authorization-Header`. - - Additionally to `Token`, the keyword `Bearer` is acceptable as a keyword - for the auth-token. The `Bearer` keyword is described in the original - OAuth RFC and used in libraries like Angular-JWT. - - See #19094. - - *Peter Schröder* - -* Drop request class from `RouteSet` constructor. - - If you would like to use a custom request class, please subclass and implement - the `request_class` method. - - *tenderlove@ruby-lang.org* - -* Fallback to `ENV['RAILS_RELATIVE_URL_ROOT']` in `url_for`. - - Fixed an issue where the `RAILS_RELATIVE_URL_ROOT` environment variable is not - prepended to the path when `url_for` is called. If `SCRIPT_NAME` (used by Rack) - is set, it takes precedence. - - Fixes #5122. - - *Yasyf Mohamedali* - -* Partitioning of routes is now done when the routes are being drawn. This - helps to decrease the time spent filtering the routes during the first request. - - *Guo Xiang Tan* - -* Fix regression in functional tests. Responses should have default headers - assigned. - - See #18423. - - *Jeremy Kemper*, *Yves Senn* - -* Deprecate `AbstractController#skip_action_callback` in favor of individual skip_callback methods - (which can be made to raise an error if no callback was removed). - - *Iain Beeston* - -* Alias the `ActionDispatch::Request#uuid` method to `ActionDispatch::Request#request_id`. - Due to implementation, `config.log_tags = [:request_id]` also works in substitute - for `config.log_tags = [:uuid]`. - - *David Ilizarov* - -* Change filter on /rails/info/routes to use an actual path regexp from rails - and not approximate javascript version. Oniguruma supports much more - extensive list of features than javascript regexp engine. - - Fixes #18402. - - *Ravil Bayramgalin* - -* Non-string authenticity tokens do not raise NoMethodError when decoding - the masked token. - - *Ville Lautanala* - -* Add `http_cache_forever` to Action Controller, so we can cache a response - that never gets expired. - - *arthurnn* - -* `ActionController#translate` supports symbols as shortcuts. - When a shortcut is given it also performs the lookup without the 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 additional - payload data is not available. - - See #14903. - - *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 behavior was introduced in response to CVE-2012-2660, CVE-2012-2694 - and CVE-2013-0155 - - ActiveRecord now issues a safe query when passing an empty array into - 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/Rakefile b/actionpack/Rakefile index 601263bfac..4a05c067a9 100644 --- a/actionpack/Rakefile +++ b/actionpack/Rakefile @@ -5,6 +5,8 @@ test_files = Dir.glob('test/**/*_test.rb') desc "Default Task" task :default => :test +task :package + # Run the unit tests Rake::TestTask.new do |t| t.libs << 'test' diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index 28d8bc3091..965fafff5f 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -13,7 +13,7 @@ Gem::Specification.new do |s| 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,7 +24,7 @@ Gem::Specification.new do |s| 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/lib/abstract_controller.rb b/actionpack/lib/abstract_controller.rb index 56c4033387..1e57cbaac4 100644 --- a/actionpack/lib/abstract_controller.rb +++ b/actionpack/lib/abstract_controller.rb @@ -6,6 +6,7 @@ module AbstractController extend ActiveSupport::Autoload autoload :Base + autoload :Caching autoload :Callbacks autoload :Collector autoload :DoubleRenderError, "abstract_controller/rendering" @@ -15,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 8edea0f52b..aa06f70433 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -1,13 +1,11 @@ require 'erubis' +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 @@ -78,7 +76,7 @@ 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. def clear_action_methods! @@ -152,6 +150,13 @@ module AbstractController _find_action_name(action_name) end + # Tests if a response body is set. Used to determine if the + # +process_action+ callback needs to be terminated in + # +AbstractController::Callbacks+. + def performed? + response_body + end + # Returns true if the given controller is capable of rendering # a path. A subclass of +AbstractController::Base+ # may return false. An Email controller for example does not 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 b9ad51a9cf..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 @@ -135,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 d63ce9c1c3..3ef8da86fa 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -9,7 +9,7 @@ module AbstractController included do define_callbacks :process_action, - terminator: ->(controller, result_lambda) { result_lambda.call if result_lambda.is_a?(Proc); controller.response_body }, + terminator: ->(controller, result_lambda) { result_lambda.call if result_lambda.is_a?(Proc); controller.performed? }, skip_after_callbacks_if_terminated: true end 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 d84c238a62..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? # diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index e765d73ce4..4ba2c26949 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' @@ -59,9 +60,7 @@ module AbstractController end DEFAULT_PROTECTED_INSTANCE_VARIABLES = Set.new %i( - @_action_name @_response_body @_formats @_prefixes @_config - @_view_context_class @_view_renderer @_lookup_context - @_routes @_db_runtime + @_action_name @_response_body @_formats @_prefixes ) # This method should return a hash with assigns. @@ -123,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.variant.present? + if defined?(request) && !request.nil? && 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 40f33a9de0..62f5905205 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -9,12 +9,15 @@ module ActionController 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 :ConditionalGet autoload :Cookies @@ -47,11 +50,6 @@ module ActionController 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 index ff12705abe..6bbebb7b4c 100644 --- a/actionpack/lib/action_controller/api.rb +++ b/actionpack/lib/action_controller/api.rb @@ -14,22 +14,22 @@ module ActionController # 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. + # your application, they're just not part of the default API controller stack. # - # By default, only the ApplicationController in a \Rails application inherits - # from <tt>ActionController::API</tt>. All other controllers in turn inherit - # from ApplicationController. + # 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 + # posts = Post.all + # render json: posts # end # end # - # Request, response and parameters objects all work the exact same way as + # Request, response, and parameters objects all work the exact same way as # <tt>ActionController::Base</tt>. # # == Renders @@ -37,18 +37,18 @@ module ActionController # The default API Controller stack includes all renderers, which means you # can use <tt>render :json</tt> and brothers freely in your controllers. Keep # in mind that templates are not going to be rendered, so you need to ensure - # your controller is calling either <tt>render</tt> or <tt>redirect</tt> in - # all actions, otherwise it will return 204 No Content response. + # 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 + # post = Post.find(params[:id]) + # render json: post # end # # == Redirects # # Redirects are used to move from one action to another. You can use the - # <tt>redirect</tt> method in your controllers in the same way as + # <tt>redirect_to</tt> method in your controllers in the same way as in # <tt>ActionController::Base</tt>. For example: # # def create @@ -56,7 +56,7 @@ module ActionController # # do stuff here # end # - # == Adding new behavior + # == 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 @@ -72,18 +72,19 @@ module ActionController # # class PostsController < ApplicationController # def index - # @posts = Post.all + # posts = Post.all # # respond_to do |format| - # format.json { render json: @posts } - # format.xml { render xml: @posts } + # format.json { render json: posts } + # format.xml { render xml: posts } # end # end # end # - # Quite straightforward. Make sure to check <tt>ActionController::Base</tt> - # available modules if you want to include any other functionality that is - # not provided by <tt>ActionController::API</tt> out of the box. + # 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! diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 04e5922ce8..d546d7260c 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -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, @@ -251,9 +251,10 @@ module ActionController setup_renderer! # Define some internal variables that should not be propagated to the view. - PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [ - :@_params, :@_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 diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index 0b8fa2ea09..a9a8508abc 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -1,6 +1,3 @@ -require 'fileutils' -require 'uri' - 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. @@ -23,65 +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 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/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index 4c9f14e409..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(" | ".freeze)})" 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 f6a93a8940..f6e67b02d7 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -174,10 +174,8 @@ module ActionController def response_body=(body) body = [body] unless body.nil? || body.respond_to?(:each) response.reset_body! - body.each { |part| - next if part.empty? - response.write part - } + return unless body + response.body = body super end diff --git a/actionpack/lib/action_controller/metal/basic_implicit_render.rb b/actionpack/lib/action_controller/metal/basic_implicit_render.rb index 6c6f8381ff..cef65a362c 100644 --- a/actionpack/lib/action_controller/metal/basic_implicit_render.rb +++ b/actionpack/lib/action_controller/metal/basic_implicit_render.rb @@ -1,5 +1,5 @@ module ActionController - module BasicImplicitRender + module BasicImplicitRender # :nodoc: def send_action(method, *args) super.tap { default_render unless performed? } end diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index f8e0d9cf6c..e21449f376 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -36,8 +36,23 @@ 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 cacheable by other devices (proxy caches). # * <tt>:template</tt> By default, the template digest for the current @@ -86,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 @@ -107,8 +126,23 @@ 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 cacheable by other devices (proxy caches). # * <tt>:template</tt> By default, the template digest for the current @@ -180,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 @@ -195,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, @@ -208,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) @@ -216,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}", + 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 f8efb2b076..44925641a1 100644 --- a/actionpack/lib/action_controller/metal/cookies.rb +++ b/actionpack/lib/action_controller/metal/cookies.rb @@ -3,7 +3,7 @@ module ActionController #:nodoc: extend ActiveSupport::Concern 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 957e7a3019..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). # @@ -79,14 +78,14 @@ module ActionController #:nodoc: # <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. diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index e31d65aac2..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 diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index d3853e2e83..295f0cb66f 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -5,7 +5,7 @@ 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 which # matches the name of the controller, e.g., <tt>MyController</tt> will automatically @@ -71,7 +71,7 @@ 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 proxy = ActionView::Base.new @@ -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 35be6d9300..4639348509 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -310,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? @@ -347,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 diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb index 17fcc2fa02..6192fc0f9c 100644 --- a/actionpack/lib/action_controller/metal/implicit_render.rb +++ b/actionpack/lib/action_controller/metal/implicit_render.rb @@ -1,29 +1,62 @@ +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 + # :stopdoc: include BasicImplicitRender - # Renders the template corresponding to the controller action, if it exists. - # The action name, format, and variant are all taken into account. - # For example, the "new" action with an HTML format and variant "phone" - # would try to render the <tt>new.html+phone.erb</tt> template. - # - # If no template is found <tt>ActionController::BasicImplicitRender</tt>'s implementation is called, unless - # a block is passed. In that case, it will override the super implementation. - # - # default_render do - # head 404 # No template was found - # end def default_render(*args) if template_exists?(action_name.to_s, _prefixes, variants: request.variant) render(*args) + 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 - if block_given? - yield(*args) - else - logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger - super - end + logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger + super end end @@ -32,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 3dbf34eb2a..624a6d5b76 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -19,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) @@ -74,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 e3c540bf5f..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 @@ -163,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. # @@ -210,6 +202,14 @@ module ActionController def call_on_error @error_callback.call end + + private + + def each_chunk(&block) + while str = @buf.pop + yield str + end + end end class Response < ActionDispatch::Response #:nodoc: all @@ -237,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 diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 173a14a1d2..2e89af1a5e 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -198,7 +198,7 @@ module ActionController #:nodoc: _process_format(format) _set_rendered_content_type format response = collector.response - response ? response.call : render({}) + response.call if response else raise ActionController::UnknownFormat end diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index b13ba06962..3c7cc15627 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -84,7 +84,7 @@ module ActionController # 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 indetical. + # options and the behavior is identical. def redirect_back(fallback_location:, **args) if referer = request.headers["Referer"] redirect_to referer, **args diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 90fb34e386..1735609cd9 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -103,7 +103,7 @@ module ActionController # # Both <tt>ActionController::Base</tt> and <tt>ActionController::API</tt> # include <tt>ActionController::Renderers::All</tt>, making all renderers - # avaialable in the controller. See <tt>Renderers::RENDERERS</tt> and <tt>Renderers.add</tt>. + # 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>, diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 91b3403ad5..0559fbc6ce 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -213,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 @@ -235,7 +235,9 @@ module ActionController #:nodoc: # we aren't serving an unauthorized cross-origin response. def verify_same_origin_request if marked_for_same_origin_verification? && non_xhr_javascript_response? - logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING if logger + if logger && log_warning_on_csrf_failure + logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING + end raise ActionController::InvalidCrossOriginRequest, CROSS_ORIGIN_JAVASCRIPT_WARNING end end @@ -378,7 +380,9 @@ module ActionController #:nodoc: 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. @@ -403,7 +407,8 @@ module ActionController #:nodoc: end def normalize_action_path(action_path) - action_path.split('?').first.to_s.chomp('/') + 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 81b9a7b9ed..17f4030f25 100644 --- a/actionpack/lib/action_controller/metal/rescue.rb +++ b/actionpack/lib/action_controller/metal/rescue.rb @@ -1,18 +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.cause && handler_for_rescue(exception.cause) - exception = exception.cause - 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 @@ -27,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/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index d3382ef296..46589901fd 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -43,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 @@ -109,7 +109,7 @@ module ActionController 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?, :inspect, + delegate :keys, :key?, :has_key?, :values, :has_value?, :value?, :empty?, :include?, :as_json, to: :@parameters # By default, never raise an UnpermittedParameters exception if these @@ -122,16 +122,6 @@ module ActionController cattr_accessor :always_permitted_parameters self.always_permitted_parameters = %w( controller action ) - def self.const_missing(const_name) - return super unless const_name == :NEVER_UNPERMITTED_PARAMS - ActiveSupport::Deprecation.warn(<<-MSG.squish) - `ActionController::Parameters::NEVER_UNPERMITTED_PARAMS` has been deprecated. - Use `ActionController::Parameters.always_permitted_parameters` instead. - 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>. @@ -154,17 +144,21 @@ module ActionController end # Returns true if another +Parameters+ object contains the same content and - # permitted flag, or other Hash-like object contains the same content. This - # override is in place so you can perform a comparison with `Hash`. - def ==(other_hash) - if other_hash.respond_to?(:permitted?) - super + # 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 - if other_hash.is_a?(Hash) - @parameters == other_hash.with_indifferent_access - else - @parameters == other_hash - end + @parameters == other end end @@ -190,12 +184,19 @@ module ActionController # 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 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) @parameters.each_pair do |key, value| @@ -277,7 +278,7 @@ module ActionController # params = ActionController::Parameters.new(user: { ... }, profile: { ... }) # user_params, profile_params = params.require(:user, :profile) # - # Otherwise, the method reraises the first exception found: + # Otherwise, the method re-raises the first exception found: # # params = ActionController::Parameters.new(user: {}, profile: {}) # user_params, profile_params = params.require(:user, :profile) @@ -436,6 +437,21 @@ module ActionController ) 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 # includes only the given +keys+. If the given +keys+ # don't exist, returns an empty hash. @@ -584,6 +600,10 @@ module ActionController 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 @@ -603,12 +623,14 @@ module ActionController 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) } + @parameters.all? { |k, v| k =~ /\A-?\d+\z/ && (v.is_a?(Hash) || v.is_a?(Parameters)) } end private @@ -734,6 +756,10 @@ module ActionController end end + def non_scalar?(value) + value.is_a?(Array) || value.is_a?(Parameters) + end + EMPTY_ARRAY = [] def hash_filter(params, filter) filter = filter.with_indifferent_access @@ -748,7 +774,7 @@ module ActionController array_of_permitted_scalars?(self[key]) do |val| params[key] = val end - else + elsif non_scalar?(value) # Declaration { user: :name } or { user: [:name, :age, { address: ... }] }. params[key] = each_element(value) do |element| element.permit(*Array.wrap(filter[key])) @@ -771,7 +797,7 @@ module ActionController # # class PeopleController < ActionController::Base # # Using "Person.create(params[:person])" would raise an - # # ActiveModel::ForbiddenAttributes exception because it'd + # # ActiveModel::ForbiddenAttributesError exception because it'd # # be using mass assignment without an explicit permit step. # # This is the recommended form: # def create @@ -799,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 @@ -819,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/renderer.rb b/actionpack/lib/action_controller/renderer.rb index e4d19e9dba..a8c8d66682 100644 --- a/actionpack/lib/action_controller/renderer.rb +++ b/actionpack/lib/action_controller/renderer.rb @@ -1,7 +1,7 @@ require 'active_support/core_ext/hash/keys' module ActionController - # ActionController::Renderer allows to render arbitrary templates + # ActionController::Renderer allows you to render arbitrary templates # without requirement of being in controller actions. # # You get a concrete renderer class by invoking ActionController::Base#renderer. @@ -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: { ... } @@ -45,7 +45,7 @@ module ActionController }.freeze # Create a new renderer instance for a specific controller class. - def self.for(controller, env = {}, defaults = DEFAULTS) + def self.for(controller, env = {}, defaults = DEFAULTS.dup) new(controller, env, defaults) end diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index b43bb9dc17..b1b3e87934 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -12,6 +12,17 @@ module ActionController include Testing::Functional end + 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 + # 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: @@ -41,7 +52,7 @@ module ActionController self.session = session self.session_options = TestSession::DEFAULT_OPTIONS @custom_param_parsers = { - Mime[:xml] => lambda { |raw_post| Hash.from_xml(raw_post)['hash'] } + xml: lambda { |raw_post| Hash.from_xml(raw_post)['hash'] } } end @@ -94,7 +105,7 @@ module ActionController when :url_encoded_form data = non_path_parameters.to_query else - @custom_param_parsers[content_mime_type] = ->(_) { non_path_parameters } + @custom_param_parsers[content_mime_type.symbol] = ->(_) { non_path_parameters } data = non_path_parameters.to_query end end @@ -417,7 +428,7 @@ module ActionController end alias xhr :xml_http_request - # 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. @@ -516,32 +527,37 @@ module ActionController @request.set_header k, @controller.config.relative_url_root end - @controller.recycle! - @controller.dispatch(action, @request, @response) - @request = @controller.request - @response = @controller.response + begin + @controller.recycle! + @controller.dispatch(action, @request, @response) + ensure + @request = @controller.request + @response = @controller.response - @request.delete_header 'HTTP_COOKIE' + @request.delete_header 'HTTP_COOKIE' - 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)) + 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 - end - @response.prepare! + @response.prepare! - if flash_value = @request.flash.to_session_value - @request.session['flash'] = flash_value - else - @request.session.delete('flash') - end + if flash_value = @request.flash.to_session_value + @request.session['flash'] = flash_value + else + @request.session.delete('flash') + end - if xhr - @request.delete_header 'HTTP_X_REQUESTED_WITH' - @request.delete_header 'HTTP_ACCEPT' + if xhr + @request.delete_header 'HTTP_X_REQUESTED_WITH' + @request.delete_header 'HTTP_ACCEPT' + end + @request.query_string = '' + + @response.sent! end - @request.query_string = '' @response end diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 1e4df07d6e..01d49475de 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -51,8 +51,8 @@ module ActionDispatch autoload :Cookies autoload :DebugExceptions autoload :ExceptionWrapper + autoload :Executor autoload :Flash - autoload :LoadInterlock autoload :ParamsParser autoload :PublicExceptions autoload :Reloader diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index 0f7898a3f8..9fa2e38ae3 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -17,9 +17,7 @@ module ActionDispatch 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) @@ -28,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 @@ -80,19 +78,63 @@ module ActionDispatch set_header DATE, utc_time.httpdate end - def etag=(etag) - key = ActiveSupport::Cache.expand_cache_key(etag) - super %(W/"#{Digest::MD5.hexdigest(key)}") + # 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 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 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 = _cache_control cache_control.delete(' ').split(',') diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index 9dcab79c3a..041eca48ca 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -4,9 +4,11 @@ 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]" @@ -14,6 +16,10 @@ 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"] = [ "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 diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb index 12f81dc1a5..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 @@ -101,7 +115,7 @@ module ActionDispatch 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 e9b25339dc..0a58ce2b96 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -164,7 +164,7 @@ module ActionDispatch end def format_from_path_extension - path = @env['action_dispatch.original_path'] || @env['PATH_INFO'] + path = get_header('action_dispatch.original_path') || get_header('PATH_INFO') if match = path && path.match(/\.(\w+)\z/) Mime[match.captures.first] end diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index 463b5fe405..4672ea7199 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -1,3 +1,5 @@ +# -*- frozen-string-literal: true -*- + require 'singleton' require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/string/starts_ends_with' @@ -108,57 +110,56 @@ module Mime 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 self, 'text/xml' - app_xml_idx = find_item_by_name self, Mime[:xml].to_s + 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 = self[app_xml_idx] - text_xml = self[text_xml_idx] + app_xml = list[app_xml_idx] + text_xml = list[text_xml_idx] app_xml.q = [text_xml.q, app_xml.q].max # set the q value to the max of the two if app_xml_idx > text_xml_idx # make sure app_xml is ahead of text_xml in the list - self[app_xml_idx], self[text_xml_idx] = text_xml, app_xml + 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 - delete_at(text_xml_idx) # delete text_xml from the list + list.delete_at(text_xml_idx) # delete text_xml from the list elsif text_xml_idx - self[text_xml_idx].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 = self[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 + 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 find_item_by_name(array, name) + 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) @@ -198,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 = parse_trailing_star(params) || [params] - params.each do |m| - list << AcceptItem.new(index, m.to_s, q) - index += 1 - end + params.each do |m| + list << AcceptItem.new(index, m.to_s, q) + index += 1 end end - list.assort! + AcceptList.sort! list end end diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb index 87715205d9..8b04174f1f 100644 --- a/actionpack/lib/action_dispatch/http/mime_types.rb +++ b/actionpack/lib/action_dispatch/http/mime_types.rb @@ -14,20 +14,22 @@ 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 # http://www.ietf.org/rfc/rfc4627.txt # http://www.json.org/JSONRequest.html -Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest application/vnd.api+json ) +Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest ) Mime::Type.register "application/pdf", :pdf, [], %w(pdf) Mime::Type.register "application/zip", :zip, [], %w(zip) +Mime::Type.register "application/gzip", :gzip, %w(application/x-gzip), %w(gz) diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index cca7376ffa..ff5031d7d5 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -1,22 +1,31 @@ module ActionDispatch module Http module Parameters + extend ActiveSupport::Concern + PARAMETERS_KEY = 'action_dispatch.request.path_parameters' DEFAULT_PARSERS = { - Mime[:json] => lambda { |raw_post| + Mime[:json].symbol => -> (raw_post) { data = ActiveSupport::JSON.decode(raw_post) data.is_a?(Hash) ? data : {:_json => data} } } - def self.included(klass) - class << klass - attr_accessor :parameter_parsers + included do + class << self + attr_reader :parameter_parsers end - klass.parameter_parsers = DEFAULT_PARSERS + 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 params = get_header("action_dispatch.request.parameters") @@ -51,7 +60,7 @@ module ActionDispatch def parse_formatted_parameters(parsers) return yield if content_length.zero? - strategy = parsers.fetch(content_mime_type) { return yield } + strategy = parsers.fetch(content_mime_type.symbol) { return yield } begin strategy.call(raw_post) diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 5427425ef7..b0ed681623 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -337,7 +337,6 @@ module ActionDispatch else self.session = {} end - self.flash = nil end def session=(session) #:nodoc: @@ -403,6 +402,10 @@ module ActionDispatch def commit_flash end + def ssl? + super || scheme == 'wss'.freeze + end + private def check_method(name) HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS[0...-1].join(', ')}, and #{HTTP_METHODS[-1]}") diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 14f86c7c07..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: @@ -67,7 +68,13 @@ module ActionDispatch # :nodoc: alias_method :headers, :header delegate :[], :[]=, :to => :@header - delegate :each, :to => :@stream + + def each(&block) + sending! + x = @stream.each(&block) + sent! + x + end CONTENT_TYPE = "Content-Type".freeze SET_COOKIE = "Set-Cookie".freeze @@ -96,10 +103,10 @@ module ActionDispatch # :nodoc: def body @str_body ||= begin - buf = '' - each { |chunk| buf << chunk } - buf - end + buf = '' + each { |chunk| buf << chunk } + buf + end end def write(string) @@ -111,10 +118,13 @@ module ActionDispatch # :nodoc: 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 @@ -128,6 +138,12 @@ 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) diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 37f41ae988..7a1350a46d 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -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,6 +226,9 @@ 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 @@ -236,7 +239,7 @@ module ActionDispatch 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 @@ -249,12 +252,16 @@ module ActionDispatch 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,6 +354,17 @@ 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 get_header('SERVER_PORT').to_i end 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 0323360faa..200477b002 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -32,8 +32,13 @@ module ActionDispatch defaults = route.defaults required_parts = route.required_parts - parameterized_parts.keep_if do |key, value| - (defaults[key].nil? && value.present?) || 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] diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb index 35c2b1b86e..cfd6681dd1 100644 --- a/actionpack/lib/action_dispatch/journey/route.rb +++ b/actionpack/lib/action_dispatch/journey/route.rb @@ -3,7 +3,7 @@ module ActionDispatch class Route # :nodoc: attr_reader :app, :path, :defaults, :name, :precedence - attr_reader :constraints + attr_reader :constraints, :internal alias :conditions :constraints module VerbMatchers @@ -55,7 +55,7 @@ module ActionDispatch ## # +path+ is a path constraint. # +constraints+ is a hash of constraints to be applied to this route. - def initialize(name, app, path, constraints, required_defaults, defaults, request_method_match, precedence) + def initialize(name, app, path, constraints, required_defaults, defaults, request_method_match, precedence, internal = false) @name = name @app = app @path = path @@ -70,6 +70,7 @@ module ActionDispatch @decorated_ast = nil @precedence = precedence @path_formatter = @path.build_formatter + @internal = internal end def ast @@ -81,7 +82,7 @@ module ActionDispatch end def requirements # :nodoc: - # needed for rails `rake routes` + # needed for rails `rails routes` @defaults.merge(path.requirements).delete_if { |_,v| /.+?/ == v } diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb index f649588520..06cdce1724 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -16,9 +16,6 @@ module ActionDispatch class RoutingError < ::StandardError # :nodoc: end - # :nodoc: - VERSION = '2.0.0' - attr_accessor :routes def initialize(routes) 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 3477aa8b29..f2f3150b56 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -2,6 +2,7 @@ require 'active_support/core_ext/hash/keys' require 'active_support/key_generator' require 'active_support/message_verifier' require 'active_support/json' +require 'rack/utils' module ActionDispatch class Request @@ -337,7 +338,7 @@ module ActionDispatch end def to_header - @cookies.map { |k,v| "#{k}=#{v}" }.join ';' + @cookies.map { |k,v| "#{escape(k)}=#{escape(v)}" }.join '; ' end def handle_options(options) #:nodoc: @@ -419,6 +420,10 @@ module ActionDispatch private + 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) diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index b55c937e0c..5f758d641a 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -67,18 +67,19 @@ module ActionDispatch log_error(request, wrapper) if request.get_header('action_dispatch.show_detailed_exceptions') - case @response_format - when :api - render_for_api_application(request, wrapper) - when :default - render_for_default_application(request, wrapper) + content_type = request.formats.first + + if api_request?(content_type) + render_for_api_request(content_type, wrapper) + else + render_for_browser_request(request, wrapper) end else raise exception end end - def render_for_default_application(request, wrapper) + def render_for_browser_request(request, wrapper) template = create_template(request, wrapper) file = "rescues/#{wrapper.rescue_template}" @@ -92,7 +93,7 @@ module ActionDispatch render(wrapper.status_code, body, format) end - def render_for_api_application(request, wrapper) + def render_for_api_request(content_type, wrapper) body = { status: wrapper.status_code, error: Rack::Utils::HTTP_STATUS_CODES.fetch( @@ -103,7 +104,6 @@ module ActionDispatch traces: wrapper.traces } - content_type = request.formats.first to_format = "to_#{content_type.to_sym}" if content_type && body.respond_to?(to_format) @@ -156,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 log_array(logger, array) + array.map { |line| logger.fatal line } + end + def logger(request) - request.logger || stderr_logger + request.logger || ActionView::Base.logger || stderr_logger end def stderr_logger @@ -176,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 3b61824cc9..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' 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 c51dcd542a..80703940ed 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -70,6 +70,11 @@ module ActionDispatch session.delete('flash') end end + + def reset_session # :nodoc + super + self.flash = nil + end end class FlashNow #:nodoc: @@ -128,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: diff --git a/actionpack/lib/action_dispatch/middleware/load_interlock.rb b/actionpack/lib/action_dispatch/middleware/load_interlock.rb deleted file mode 100644 index 07f498319c..0000000000 --- a/actionpack/lib/action_dispatch/middleware/load_interlock.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'active_support/dependencies' -require 'rack/body_proxy' - -module ActionDispatch - class LoadInterlock - def initialize(app) - @app = app - end - - def call(env) - interlock = ActiveSupport::Dependencies.interlock - interlock.start_running - response = @app.call(env) - body = Rack::BodyProxy.new(response[2]) { interlock.done_running } - response[2] = body - response - ensure - interlock.done_running unless body - end - end -end diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb index c2a4f46e67..faf3262b8f 100644 --- a/actionpack/lib/action_dispatch/middleware/params_parser.rb +++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb @@ -37,6 +37,8 @@ module ActionDispatch # 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 diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb index af9a29eb07..112bde6596 100644 --- a/actionpack/lib/action_dispatch/middleware/reloader.rb +++ b/actionpack/lib/action_dispatch/middleware/reloader.rb @@ -23,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 + default_reloader.reload! end - def call(env) - @validated = @condition.call - prepare! - - response = @app.call(env) - response[2] = ::Rack::BodyProxy.new(response[2]) { cleanup! } + class << self + attr_accessor :default_reloader # :nodoc: - response - rescue Exception - cleanup! - raise + 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 - def prepare! #:nodoc: - run_callbacks :prepare if validated? - end - - def cleanup! #:nodoc: - run_callbacks :cleanup if validated? - ensure - @validated = true - end - - private - - def validated? #:nodoc: - @validated - end + self.default_reloader = ActiveSupport::Reloader end end diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index 429a98f236..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. # diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb index 735b5939dd..ab3077b308 100644 --- a/actionpack/lib/action_dispatch/middleware/ssl.rb +++ b/actionpack/lib/action_dispatch/middleware/ssl.rb @@ -23,7 +23,7 @@ module ActionDispatch # 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 `false`. + # vulnerable site on a subdomain. Defaults to `true`. # * `preload`: Advertise that this site may be included in browsers' # preloaded HSTS lists. HSTS protects your site on every visit *except the # first visit* since it hasn't seen your HSTS header yet. To close this @@ -34,6 +34,10 @@ module ActionDispatch # 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 # Default to 180 days, the low end for https://www.ssllabs.com/ssltest/ # and greater than the 18-week requirement for browser preload lists. @@ -49,14 +53,26 @@ module ActionDispatch 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: … }}`. + `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 + @hsts_header = build_hsts_header(normalize_hsts_options(hsts)) end @@ -69,7 +85,7 @@ module ActionDispatch flag_cookies_as_secure! headers if @secure_cookies end else - return redirect_to_https request if @redirect + return redirect_to_https request unless @exclude.call(request) @app.call(env) end end diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index 41c220236a..2c5721dc22 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -27,8 +27,8 @@ module ActionDispatch # in the server's `public/` directory (see Static#call). def match?(path) path = ::Rack::Utils.unescape_path path - return false unless valid_path?(path) - path = Rack::Utils.clean_path_info path + return false unless ::Rack::Utils.valid_path? path + path = ::Rack::Utils.clean_path_info path paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"] @@ -94,10 +94,6 @@ module ActionDispatch false end end - - def valid_path?(path) - path.valid_encoding? && !path.include?("\0") - end end # This middleware will attempt to return the contents of a file's body from 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 42890225fa..47568f6ad0 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -9,7 +9,7 @@ module ActionDispatch # Singleton object used to determine if an optional param wasn't specified Unspecified = Object.new - + # Creates a session hash, merging the properties of the previous session if any def self.create(store, req, default_options) session_was = find req @@ -198,6 +198,10 @@ module ActionDispatch @delegate.merge!(other) end + def each(&block) + to_hash.each(&block) + end + private def load_for_read! diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index d00b2c3eb5..61ebd0b8db 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -73,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, @@ -89,7 +89,7 @@ module ActionDispatch # # Example: # - # # In routes.rb + # # In config/routes.rb # get '/login' => 'accounts#login', as: 'login' # # # With render, redirect_to, tests, etc. @@ -101,7 +101,7 @@ module ActionDispatch # # Use <tt>root</tt> as a shorthand to name a route for the root path "/". # - # # In routes.rb + # # In config/routes.rb # root to: 'blogs#index' # # # would recognize http://www.example.com/ as @@ -114,15 +114,15 @@ module ActionDispatch # Note: when using +controller+, the route is simply named after the # method you call on the block parameter rather than map. # - # # In routes.rb + # # In config/routes.rb # controller :blog do # get 'blog/show' => :list # get 'blog/delete' => :delete - # get 'blog/edit/:id' => :edit + # get 'blog/edit' => :edit # end # # # provides named routes for show, delete, and edit - # link_to @article.title, show_path(id: @article.id) + # link_to @article.title, blog_show_path(id: @article.id) # # == Pretty URLs # @@ -159,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 @@ -196,7 +196,7 @@ module ActionDispatch # # Rails.application.reload_routes! # - # This will clear all named routes and reload routes.rb if the file has been modified from + # This will clear all named routes and reload config/routes.rb if the file has been modified from # last load. To absolutely force reloading, use <tt>reload!</tt>. # # == Testing Routes @@ -239,7 +239,7 @@ module ActionDispatch # # 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 @@ -252,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 69e6dd5215..2459a45827 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -33,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)} + internal end def engine? @@ -51,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) @@ -60,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(collect_routes(@routes), filter) + formatter.no_routes(collect_routes(@routes)) return formatter.result end @@ -82,10 +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 - filter_name = filter.underscore.sub(/_controller$/, '') - @routes.select { |route| route.defaults[:controller] == filter_name } + @routes.select do |route| + route_wrapper = RouteWrapper.new(route) + filter.any? { |default, value| route_wrapper.send(default) =~ value } + end else @routes end @@ -137,7 +145,7 @@ module ActionDispatch @buffer << draw_header(routes) end - def no_routes(routes, filter) + def no_routes(routes) @buffer << if routes.none? <<-MESSAGE.strip_heredoc @@ -145,8 +153,6 @@ module ActionDispatch Please add some routes in config/routes.rb. MESSAGE - elsif missing_controller?(filter) - "The controller #{filter} does not exist!" else "No routes were found for this controller" end @@ -154,10 +160,6 @@ module ActionDispatch end private - def missing_controller?(controller_name) - [ controller_name.camelize, "#{controller_name.camelize}Controller" ].none?(&:safe_constantize) - end - def draw_section(routes) header_lengths = ['Prefix', 'Verb', 'URI Pattern'].map(&:length) name_width, verb_width, path_width = widths(routes).zip(header_lengths).map(&:max) diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index afbaa45d20..40b6500553 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1,4 +1,3 @@ -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' @@ -107,6 +106,7 @@ module ActionDispatch @ast = ast @anchor = anchor @via = via + @internal = options.delete(:internal) path_params = ast.find_all(&:symbol?).map(&:to_sym) @@ -120,7 +120,7 @@ module ActionDispatch if options_constraints.is_a?(Hash) @defaults = Hash[options_constraints.find_all { |key, default| - URL_OPTIONS.include?(key) && (String === default || Fixnum === default) + URL_OPTIONS.include?(key) && (String === default || Integer === default) }].merge @defaults @blocks = blocks constraints.merge! options_constraints @@ -137,6 +137,10 @@ module ActionDispatch @conditions = Hash[conditions] @defaults = formats[:defaults].merge(@defaults).merge(normalize_defaults(options)) + if path_params.include?(:action) && !@requirements.key?(:action) + @defaults[:action] ||= 'index' + end + @required_defaults = (split_options[:required_defaults] || []).map(&:first) end @@ -148,7 +152,8 @@ module ActionDispatch required_defaults, defaults, request_method, - precedence) + precedence, + @internal) route end @@ -819,10 +824,10 @@ module ActionDispatch 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)) + URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Integer)) end - (options[:defaults] ||= {}).reverse_merge!(defaults) + options[:defaults] = defaults.merge(options[:defaults] || {}) else block, options[:constraints] = options[:constraints], {} end @@ -1596,7 +1601,7 @@ module ActionDispatch route_options = options.dup if _path && option_path ActiveSupport::Deprecation.warn <<-eowarn -Specifying strings for both :path and the route path is deprecated. Change things like this: +Specifying strings for both :path and the route path is deprecated. Change things like this: match #{_path.inspect}, :path => #{option_path.inspect} @@ -2083,8 +2088,7 @@ to this: def each node = self - loop do - break if node.equal? NULL + until node.equal? NULL yield node node = node.parent end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 846b5fa1fc..ed7130b58e 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -289,7 +289,7 @@ module ActionDispatch if last.permitted? args.pop.to_h else - raise ArgumentError, "Generating an URL from non sanitized request parameters is insecure!" + raise ArgumentError, ActionDispatch::Routing::INSECURE_URL_PARAMETERS_MESSAGE end end helper.call self, args, options @@ -513,6 +513,21 @@ module ActionDispatch route = @set.add_route(name, mapping) named_routes[name] = route if name + + 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 + + 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 @@ -533,12 +548,10 @@ module ActionDispatch @recall = recall @set = set - normalize_recall! normalize_options! normalize_controller_action_id! use_relative_controller! normalize_controller! - normalize_action! end def controller @@ -557,11 +570,6 @@ module ActionDispatch 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 @@ -615,13 +623,6 @@ module ActionDispatch end end - # Move 'index' action from options to recall - def normalize_action! - if @options[:action] == 'index'.freeze - @recall[:action] = @options.delete(:action) - end - end - # Generates a path from routes, returns [path, params]. # If no route is generated the formatter will raise ActionController::UrlGenerationError def generate diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index f91679593e..5ee138e6c6 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -173,7 +173,7 @@ module ActionDispatch route_name) when ActionController::Parameters unless options.permitted? - raise ArgumentError.new("Generating an URL from non sanitized request parameters is insecure!") + raise ArgumentError.new(ActionDispatch::Routing::INSECURE_URL_PARAMETERS_MESSAGE) end route_name = options.delete :use_route _routes.url_for(options.to_h.symbolize_keys. diff --git a/actionpack/lib/action_dispatch/testing/assertion_response.rb b/actionpack/lib/action_dispatch/testing/assertion_response.rb index 3fb81ff083..404b96bbcd 100644 --- a/actionpack/lib/action_dispatch/testing/assertion_response.rb +++ b/actionpack/lib/action_dispatch/testing/assertion_response.rb @@ -1,14 +1,7 @@ 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 + # 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. class AssertionResponse attr_reader :code, :name @@ -19,6 +12,9 @@ module ActionDispatch error: "5XX" } + # Accepts a specific response status code as an Integer (404) or String + # ('404') or a response status range as a Symbol pseudo-code (:success, + # indicating any 200-299 status code). def initialize(code_or_name) if code_or_name.is_a?(Symbol) @name = code_or_name diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index 78ef860548..44ad2c10d8 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -86,6 +86,7 @@ module ActionDispatch end # Load routes.rb if it hasn't been loaded. + options = options.clone generated_path, query_string_keys = @routes.generate_extras(options, defaults) found_extras = options.reject { |k, _| ! query_string_keys.include? k } @@ -116,7 +117,7 @@ module ActionDispatch # # 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) diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 711ca10419..8777666f9f 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -71,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 @@ -95,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) @@ -122,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? @@ -131,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 Rails 5.1. 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 Rails 5.1. 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 Rails 5.1. 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 Rails 5.1. 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 Rails 5.1. 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 @@ -299,7 +300,7 @@ module ActionDispatch end end - REQUEST_KWARGS = %i(params headers env xhr) + REQUEST_KWARGS = %i(params headers env xhr as) def kwarg_request?(args) args[0].respond_to?(:keys) && args[0].keys.any? { |k| REQUEST_KWARGS.include?(k) } end @@ -316,12 +317,15 @@ module ActionDispatch params: { id: 1 }, headers: { 'X-Extra-Header' => '123' }, env: { 'action_dispatch.custom' => 'custom' }, - xhr: true + xhr: true, + as: :json MSG 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 @@ -330,14 +334,17 @@ module ActionDispatch url_host += ":#{location.port}" if default != location.port host! url_host end - path = location.query ? "#{location.path}?#{location.query}" : location.path + 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"), @@ -347,7 +354,7 @@ 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 } @@ -376,6 +383,7 @@ module ActionDispatch 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 @@ -387,6 +395,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 @@ -643,33 +701,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 eca0439909..1ecd7d14a7 100644 --- a/actionpack/lib/action_dispatch/testing/test_process.rb +++ b/actionpack/lib/action_dispatch/testing/test_process.rb @@ -1,6 +1,5 @@ require 'action_dispatch/middleware/cookies' require 'action_dispatch/middleware/flash' -require 'active_support/core_ext/hash/indifferent_access' module ActionDispatch module TestProcess diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb index ad1a7f7109..46523a8600 100644 --- a/actionpack/lib/action_dispatch/testing/test_request.rb +++ b/actionpack/lib/action_dispatch/testing/test_request.rb @@ -22,23 +22,23 @@ module ActionDispatch 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) @@ -46,24 +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(",") + 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 4b79a90242..9d4b73a43d 100644 --- a/actionpack/lib/action_dispatch/testing/test_response.rb +++ b/actionpack/lib/action_dispatch/testing/test_response.rb @@ -18,5 +18,11 @@ module ActionDispatch # 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/gem_version.rb b/actionpack/lib/action_pack/gem_version.rb index 3a1410d492..d8f86630b1 100644 --- a/actionpack/lib/action_pack/gem_version.rb +++ b/actionpack/lib/action_pack/gem_version.rb @@ -6,9 +6,9 @@ module ActionPack module VERSION MAJOR = 5 - MINOR = 0 + MINOR = 1 TINY = 0 - PRE = "beta1.1" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 1ef10ff956..1e1d6f5429 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') @@ -36,7 +34,6 @@ require 'action_dispatch' require 'active_support/dependencies' require 'active_model' require 'active_record' -require 'action_controller/caching' require 'pp' # require 'pp' early to prevent hidden_methods from not picking up the pretty-print methods until too late @@ -60,16 +57,14 @@ ActiveSupport::Deprecation.debug = true # Disable available locale checks to avoid warnings running the test suite. I18n.enforce_available_locales = false -# Register danish language for testing -I18n.backend.store_translations 'da', {} -I18n.backend.store_translations 'pt-BR', {} - FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures') SharedTestRoutes = ActionDispatch::Routing::RouteSet.new SharedTestRoutes.draw do - get ':controller(/:action)' + ActiveSupport::Deprecation.silence do + get ':controller(/:action)' + end end module ActionDispatch @@ -118,7 +113,9 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase self.app = build_app app.routes.draw do - get ':controller(/:action)' + ActiveSupport::Deprecation.silence do + get ':controller(/:action)' + end end class DeadEndRoutes < ActionDispatch::Routing::RouteSet @@ -254,24 +251,6 @@ end class ::ApplicationController < ActionController::Base end -class Workshop - extend ActiveModel::Naming - include ActiveModel::Conversion - attr_accessor :id - - def initialize(id) - @id = id - end - - def persisted? - id.present? - end - - def to_s - id.to_s - end -end - module ActionDispatch class DebugExceptions private @@ -375,37 +354,15 @@ module RoutingTestHelpers end end -class MetalRenderingController < ActionController::Metal - include AbstractController::Rendering - include ActionController::Rendering - include ActionController::Renderers -end - class ResourcesController < ActionController::Base def index() head :ok end alias_method :show, :index end -class ThreadsController < ResourcesController; end -class MessagesController < ResourcesController; end class CommentsController < ResourcesController; end -class ReviewsController < ResourcesController; end - class AccountsController < ResourcesController; end -class AdminController < ResourcesController; end -class ProductsController < ResourcesController; end class ImagesController < ResourcesController; end -module Backoffice - class ProductsController < ResourcesController; end - class ImagesController < ResourcesController; end - - module Admin - class ProductsController < ResourcesController; end - class ImagesController < ResourcesController; end - end -end - # Skips the current run on Rubinius using Minitest::Assertions#skip def rubinius_skip(message = '') skip message if RUBY_ENGINE == 'rbx' diff --git a/actionpack/test/assertions/response_assertions_test.rb b/actionpack/test/assertions/response_assertions_test.rb index 579ce0ed29..57a67a48b5 100644 --- a/actionpack/test/assertions/response_assertions_test.rb +++ b/actionpack/test/assertions/response_assertions_test.rb @@ -35,7 +35,7 @@ module ActionDispatch end end - def test_assert_response_fixnum + def test_assert_response_integer @response = FakeResponse.new 400 assert_response 400 diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index 899d92f815..db71aa2160 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -177,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 @@ -201,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"}> @@ -215,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 @@ -231,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 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 e3f669dbb5..577a3d5800 100644 --- a/actionpack/test/controller/base_test.rb +++ b/actionpack/test/controller/base_test.rb @@ -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 d19b3810c2..7faf3cd8c6 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -172,6 +172,9 @@ class FunctionalCachingController < CachingController def fragment_cached_without_digest end + + def fragment_cached_with_options + end end class FunctionalFragmentCachingTest < ActionController::TestCase @@ -215,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 @@ -369,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 @@ -387,12 +402,13 @@ class AutomaticCollectionCacheTest < ActionController::TestCase @controller.perform_caching = true @controller.partial_rendered_times = 0 @controller.cache_store = ActiveSupport::Cache::MemoryStore.new - ActionView::PartialRenderer.collection_cache = @controller.cache_store + 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' get :index assert_equal 1, @controller.partial_rendered_times @@ -400,13 +416,16 @@ class AutomaticCollectionCacheTest < ActionController::TestCase 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 @@ -418,6 +437,12 @@ class AutomaticCollectionCacheTest < ActionController::TestCase 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 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 b063d769a4..eef48e8480 100644 --- a/actionpack/test/controller/flash_test.rb +++ b/actionpack/test/controller/flash_test.rb @@ -323,7 +323,9 @@ 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| diff --git a/actionpack/test/controller/force_ssl_test.rb b/actionpack/test/controller/force_ssl_test.rb index 22f1cc7c22..03a9c9ae78 100644 --- a/actionpack/test/controller/force_ssl_test.rb +++ b/actionpack/test/controller/force_ssl_test.rb @@ -322,3 +322,12 @@ class RedirectToSSLTest < ActionController::TestCase 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/helper_test.rb b/actionpack/test/controller/helper_test.rb index feb882a2b3..ef85e141a0 100644 --- a/actionpack/test/controller/helper_test.rb +++ b/actionpack/test/controller/helper_test.rb @@ -207,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' diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index d0a1d1285f..34fb3b1003 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -35,7 +35,7 @@ class SessionTest < ActiveSupport::TestCase path = "/somepath"; args = {:id => '1'}; headers = {"X-Test-Header" => "testvalue"} assert_called_with @session, :process, [:put, path, params: args, headers: headers] do @session.stub :redirect?, false do - @session.request_via_redirect(:put, path, params: args, headers: headers) + assert_deprecated { @session.request_via_redirect(:put, path, params: args, headers: headers) } end end end @@ -54,7 +54,7 @@ class SessionTest < ActiveSupport::TestCase value_series = [true, true, false] assert_called @session, :follow_redirect!, times: 2 do @session.stub :redirect?, ->{ value_series.shift } do - @session.request_via_redirect(:get, path, params: args, headers: headers) + assert_deprecated { @session.request_via_redirect(:get, path, params: args, headers: headers) } end end end @@ -63,7 +63,9 @@ class SessionTest < ActiveSupport::TestCase path = "/somepath"; args = {:id => '1'}; headers = {"X-Test-Header" => "testvalue"} @session.stub :redirect?, false do @session.stub :status, 200 do - assert_equal 200, @session.request_via_redirect(:get, path, params: args, headers: headers) + assert_deprecated do + assert_equal 200, @session.request_via_redirect(:get, path, params: args, headers: headers) + end end end end @@ -390,7 +392,7 @@ class IntegrationTestUsesCorrectClass < ActionDispatch::IntegrationTest reset! %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 @@ -730,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) @@ -1105,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 @@ -1126,3 +1135,93 @@ 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_standard_json_encoding_works + with_routing do |routes| + routes.draw do + ActiveSupport::Deprecation.silence do + post ':action' => FooController + end + end + + post '/foos_json.json', params: { foo: 'fighters' }.to_json, + headers: { 'Content-Type' => 'application/json' } + + assert_response :success + assert_equal({ 'foo' => 'fighters' }, response.parsed_body) + 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 2ef9734269..5977124594 100644 --- a/actionpack/test/controller/live_stream_test.rb +++ b/actionpack/test/controller/live_stream_test.rb @@ -152,7 +152,6 @@ module ActionController 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| @@ -206,7 +205,7 @@ module ActionController def overfill_buffer_and_die logger = ActionController::Base.logger || Logger.new($stdout) response.stream.on_error do - logger.warn 'Error while streaming' + logger.warn 'Error while streaming.' error_latch.count_down end @@ -247,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 @@ -261,6 +261,14 @@ module ActionController end end + def setup + super + + def @controller.new_controller_thread + Thread.new { yield } + end + end + def test_set_cookie get :set_cookie assert_equal({'hello' => 'world'}, @response.cookies) diff --git a/actionpack/test/controller/log_subscriber_test.rb b/actionpack/test/controller/log_subscriber_test.rb index 6ae33be3c8..57cf2dafdf 100644 --- a/actionpack/test/controller/log_subscriber_test.rb +++ b/actionpack/test/controller/log_subscriber_test.rb @@ -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 index 007866a559..247e872674 100644 --- a/actionpack/test/controller/metal/renderers_test.rb +++ b/actionpack/test/controller/metal/renderers_test.rb @@ -1,6 +1,12 @@ require 'abstract_unit' require 'active_support/core_ext/hash/conversions' +class MetalRenderingController < ActionController::Metal + include AbstractController::Rendering + include ActionController::Rendering + include ActionController::Renderers +end + class MetalRenderingJsonController < MetalRenderingController class Model def to_json(options = {}) diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb index 76e2d3ff43..993f4001de 100644 --- a/actionpack/test/controller/mime/respond_to_test.rb +++ b/actionpack/test/controller/mime/respond_to_test.rb @@ -74,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 @@ -160,7 +168,14 @@ 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 @@ -272,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" @@ -615,31 +632,77 @@ 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 + assert_raises(ActionController::UnknownFormat) do + get :variant_with_implicit_template_rendering, params: { v: :invalid } + end + end + + 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_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_with_implicit_rendering, params: { v: :invalid } + 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 template found/ }.size, "Implicit head :no_content not logged" + + 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_not_set_regular_template_missing - get :variant_with_implicit_rendering + 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_with_implicit_rendering - get :variant_with_implicit_rendering, params: { v: :implicit } + 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 - end - def test_variant_with_implicit_template_rendering - get :variant_with_implicit_rendering, params: { v: :mobile } - assert_equal "text/html", @response.content_type - assert_equal "mobile", @response.body + 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 @@ -778,24 +841,3 @@ class RespondToControllerTest < ActionController::TestCase assert_equal "phone", @response.body end end - -class RespondToWithBlockOnDefaultRenderController < ActionController::Base - def show - default_render do - render body: 'default_render yielded' - end - end -end - -class RespondToWithBlockOnDefaultRenderControllerTest < ActionController::TestCase - def setup - super - @request.host = "www.example.com" - end - - def test_default_render_uses_block_when_no_template_exists - get :show - assert_equal "default_render yielded", @response.body - assert_equal "text/plain", @response.content_type - end -end diff --git a/actionpack/test/controller/new_base/bare_metal_test.rb b/actionpack/test/controller/new_base/bare_metal_test.rb index c226fa57ee..ee3c498b1c 100644 --- a/actionpack/test/controller/new_base/bare_metal_test.rb +++ b/actionpack/test/controller/new_base/bare_metal_test.rb @@ -40,6 +40,22 @@ module BareMetalTest 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 + class HeadController < ActionController::Metal include ActionController::Head diff --git a/actionpack/test/controller/new_base/content_type_test.rb b/actionpack/test/controller/new_base/content_type_test.rb index a9dcdde4b8..0b3a26807d 100644 --- a/actionpack/test/controller/new_base/content_type_test.rb +++ b/actionpack/test/controller/new_base/content_type_test.rb @@ -43,7 +43,9 @@ module ContentType 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" 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 e9ea57e329..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" diff --git a/actionpack/test/controller/new_base/render_plain_test.rb b/actionpack/test/controller/new_base/render_plain_test.rb index 0881442bd0..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" 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 963f2c2f5c..1fb852a2c4 100644 --- a/actionpack/test/controller/new_base/render_test.rb +++ b/actionpack/test/controller/new_base/render_test.rb @@ -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 048458178c..d4111d432c 100644 --- a/actionpack/test/controller/new_base/render_text_test.rb +++ b/actionpack/test/controller/new_base/render_text_test.rb @@ -83,7 +83,7 @@ module RenderText 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" @@ -96,7 +96,7 @@ 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' } } ActiveSupport::Deprecation.silence do get "/render_text/with_layout" diff --git a/actionpack/test/controller/parameters/accessors_test.rb b/actionpack/test/controller/parameters/accessors_test.rb index bd43ff7697..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', @@ -129,9 +131,87 @@ class ParametersAccessorsTest < ActiveSupport::TestCase assert_not @params[:person].values_at(:name).first.permitted? end - test "equality with another hash works" do + test "equality with a hash is deprecated" do hash1 = { foo: :bar } params1 = ActionController::Parameters.new(hash1) - assert(params1 == 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 efaf8a96c3..c5bfb10b53 100644 --- a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb +++ b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb @@ -12,12 +12,6 @@ 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 - end - end - test "returns super on missing constant other than NEVER_UNPERMITTED_PARAMS" do ActionController::Parameters.superclass.stub :const_missing, "super" do assert_equal "super", ActionController::Parameters::NON_EXISTING_CONSTANT diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb index 3299f2d9d0..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 @@ -339,4 +360,13 @@ class ParametersPermitTest < ActiveSupport::TestCase 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/redirect_test.rb b/actionpack/test/controller/redirect_test.rb index 0b184eace9..4af3c628d0 100644 --- a/actionpack/test/controller/redirect_test.rb +++ b/actionpack/test/controller/redirect_test.rb @@ -1,5 +1,23 @@ require 'abstract_unit' +class Workshop + extend ActiveModel::Naming + include ActiveModel::Conversion + attr_accessor :id + + def initialize(id) + @id = id + end + + def persisted? + id.present? + end + + def to_s + id.to_s + end +end + class RedirectController < ActionController::Base # empty method not used anywhere to ensure methods like # `status` and `location` aren't called on `redirect_to` calls @@ -176,7 +194,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 @@ -286,7 +303,10 @@ class RedirectTest < ActionController::TestCase 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 @@ -310,7 +330,7 @@ class RedirectTest < ActionController::TestCase error = assert_raise(ArgumentError) do get :redirect_to_params end - assert_equal "Generating an URL from non sanitized request parameters is insecure!", error.message + assert_equal ActionDispatch::Routing::INSECURE_URL_PARAMETERS_MESSAGE, error.message end def test_redirect_to_with_block @@ -328,7 +348,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_test.rb b/actionpack/test/controller/render_test.rb index d1b9586533..8d3134630d 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -16,6 +16,10 @@ class TestControllerWithExtraEtags < ActionController::Base 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 plain: 'stale' @@ -26,6 +30,9 @@ end class ImplicitRenderTestController < ActionController::Base def empty_action end + + def empty_action_with_template + end end class TestController < ActionController::Base @@ -270,6 +277,18 @@ class ExpiresInRenderTest < ActionController::TestCase 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 @@ -370,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 @@ -399,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 @@ -427,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 @@ -462,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 @@ -473,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 @@ -497,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 @@ -508,9 +545,14 @@ class EtagRenderTest < ActionController::TestCase end end - def etag(record) - %(W/"#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(record))}") - 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 @@ -522,13 +564,38 @@ class MetalRenderTest < ActionController::TestCase end end +class ActionControllerBaseRenderTest < ActionController::TestCase + def test_direct_render_to_string + ac = ActionController::Base.new() + assert_equal "Hello world!", ac.render_to_string(template: 'test/hello_world') + end +end + class ImplicitRenderTest < ActionController::TestCase tests ImplicitRenderTestController - def test_implicit_no_content_response - get :empty_action + 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 @@ -582,7 +649,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 @@ -667,7 +737,7 @@ end class HttpCacheForeverTest < ActionController::TestCase class HttpCacheForeverController < ActionController::Base def cache_me_forever - http_cache_forever(public: params[:public], version: params[:version] || 'v1') do + http_cache_forever(public: params[:public]) do render plain: 'hello' end end @@ -677,20 +747,24 @@ class HttpCacheForeverTest < ActionController::TestCase def test_cache_with_public get :cache_me_forever, params: {public: true} + 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_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 @@ -698,21 +772,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 f0fd7ddc5e..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 diff --git a/actionpack/test/controller/renderer_test.rb b/actionpack/test/controller/renderer_test.rb index 16d24fa82a..372c09bc23 100644 --- a/actionpack/test/controller/renderer_test.rb +++ b/actionpack/test/controller/renderer_test.rb @@ -87,6 +87,14 @@ class RendererTest < ActiveSupport::TestCase assert_equal "<p>1\n<br />2</p>", render[inline: '<%= simple_format "1\n2" %>'] end + 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 def render @render ||= ApplicationController.renderer.method(:render) diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index 1984ad8825..d3f2ec6aa1 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -133,7 +133,11 @@ class PerFormTokensController < ActionController::Base self.per_form_csrf_tokens = true def index - render inline: "<%= form_tag (params[:form_path] || '/per_form_tokens/post_one'), method: (params[:form_method] || :post) %>" + 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 @@ -403,6 +407,37 @@ module RequestForgeryProtectionTests end end + def test_should_warn_on_not_same_origin_js + old_logger = ActionController::Base.logger + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + ActionController::Base.logger = logger + + begin + assert_cross_origin_blocked { get :same_origin_js } + + assert_equal 1, logger.logged(:warn).size + assert_match(/<script> tag on another site requested protected JavaScript/, logger.logged(:warn).last) + ensure + ActionController::Base.logger = old_logger + end + end + + def test_should_not_warn_if_csrf_logging_disabled_and_not_same_origin_js + old_logger = ActionController::Base.logger + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + ActionController::Base.logger = logger + ActionController::Base.log_warning_on_csrf_failure = false + + begin + assert_cross_origin_blocked { get :same_origin_js } + + assert_equal 0, logger.logged(:warn).size + ensure + ActionController::Base.logger = old_logger + ActionController::Base.log_warning_on_csrf_failure = true + end + end + # Allow non-GET requests since GET is all a remote <script> tag can muster. def test_should_allow_non_get_js_without_xhr_header session[:_csrf_token] = @token @@ -652,15 +687,9 @@ class PerFormTokensControllerTest < ActionController::TestCase def test_accepts_token_for_correct_path_and_method get :index - form_token = nil - assert_select 'input[name=custom_authenticity_token]' do |elts| - form_token = elts.first['value'] - assert_not_nil form_token - end + form_token = assert_presence_and_fetch_form_csrf_token - actual = @controller.send(:unmask_token, Base64.strict_decode64(form_token)) - expected = @controller.send(:per_form_csrf_token, session, '/per_form_tokens/post_one', 'post') - assert_equal expected, actual + 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' @@ -673,15 +702,9 @@ class PerFormTokensControllerTest < ActionController::TestCase def test_rejects_token_for_incorrect_path get :index - form_token = nil - assert_select 'input[name=custom_authenticity_token]' do |elts| - form_token = elts.first['value'] - assert_not_nil form_token - end + form_token = assert_presence_and_fetch_form_csrf_token - actual = @controller.send(:unmask_token, Base64.strict_decode64(form_token)) - expected = @controller.send(:per_form_csrf_token, session, '/per_form_tokens/post_one', 'post') - assert_equal expected, actual + 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' @@ -693,15 +716,9 @@ class PerFormTokensControllerTest < ActionController::TestCase def test_rejects_token_for_incorrect_method get :index - form_token = nil - assert_select 'input[name=custom_authenticity_token]' do |elts| - form_token = elts.first['value'] - assert_not_nil form_token - end + form_token = assert_presence_and_fetch_form_csrf_token - actual = @controller.send(:unmask_token, Base64.strict_decode64(form_token)) - expected = @controller.send(:per_form_csrf_token, session, '/per_form_tokens/post_one', 'post') - assert_equal expected, actual + 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' @@ -710,6 +727,50 @@ class PerFormTokensControllerTest < ActionController::TestCase 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 @@ -726,15 +787,9 @@ class PerFormTokensControllerTest < ActionController::TestCase def test_ignores_params get :index, params: {form_path: '/per_form_tokens/post_one?foo=bar'} - form_token = nil - assert_select 'input[name=custom_authenticity_token]' do |elts| - form_token = elts.first['value'] - assert_not_nil form_token - end + form_token = assert_presence_and_fetch_form_csrf_token - actual = @controller.send(:unmask_token, Base64.strict_decode64(form_token)) - expected = @controller.send(:per_form_csrf_token, session, '/per_form_tokens/post_one', 'post') - assert_equal expected, actual + 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' @@ -747,11 +802,20 @@ class PerFormTokensControllerTest < ActionController::TestCase def test_ignores_trailing_slash_during_generation get :index, params: {form_path: '/per_form_tokens/post_one/'} - form_token = nil - assert_select 'input[name=custom_authenticity_token]' do |elts| - form_token = elts.first['value'] - assert_not_nil form_token + 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' @@ -764,11 +828,7 @@ class PerFormTokensControllerTest < ActionController::TestCase def test_ignores_trailing_slash_during_validation get :index - form_token = nil - assert_select 'input[name=custom_authenticity_token]' do |elts| - form_token = elts.first['value'] - assert_not_nil form_token - end + 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/' @@ -781,12 +841,7 @@ class PerFormTokensControllerTest < ActionController::TestCase def test_method_is_case_insensitive get :index, params: {form_method: "POST"} - form_token = nil - assert_select 'input[name=custom_authenticity_token]' do |elts| - form_token = elts.first['value'] - assert_not_nil form_token - end - + 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 @@ -794,4 +849,19 @@ class PerFormTokensControllerTest < ActionController::TestCase 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/rescue_test.rb b/actionpack/test/controller/rescue_test.rb index f42bef883f..c088e5a043 100644 --- a/actionpack/test/controller/rescue_test.rb +++ b/actionpack/test/controller/rescue_test.rb @@ -131,20 +131,22 @@ class RescueController < ActionController::Base def missing_template end - def io_error_in_view - begin - raise IOError.new('this is io error') - rescue - raise ActionView::TemplateError.new(nil) - end + def exception_with_more_specific_handler_for_wrapper + raise RecordInvalid + rescue + raise NotAuthorized end - def zero_division_error_in_view - begin - raise ZeroDivisionError.new('this is zero division error') - rescue - raise ActionView::TemplateError.new(nil) - end + 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 @@ -233,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 @@ -258,7 +249,6 @@ class RescueControllerTest < ActionController::TestCase get :record_invalid end end - def test_rescue_handler_with_argument_as_string assert_called_with @controller, :show_errors, [Exception] do get :record_invalid_raise_as_string @@ -296,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 diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb index 4490abf7b2..8e38af5025 100644 --- a/actionpack/test/controller/resources_test.rb +++ b/actionpack/test/controller/resources_test.rb @@ -3,8 +3,22 @@ require 'active_support/core_ext/object/try' require 'active_support/core_ext/object/with_options' require 'active_support/core_ext/array/extract_options' -class ResourcesTest < ActionController::TestCase +class AdminController < ResourcesController; end +class MessagesController < ResourcesController; end +class ProductsController < ResourcesController; end +class ThreadsController < ResourcesController; end + +module Backoffice + class ProductsController < ResourcesController; end + class ImagesController < ResourcesController; end + + module Admin + class ProductsController < ResourcesController; end + class ImagesController < ResourcesController; end + end +end +class ResourcesTest < ActionController::TestCase def test_default_restful_routes with_restful_routing :messages do assert_simply_restful_for :messages diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index a39fede5b9..03bf8f8295 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -15,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(^ ? # [ ]) @@ -300,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")) @@ -323,7 +325,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_route_with_colon_first rs.draw do - get '/:controller/:action/:id', action: 'index', id: nil + ActiveSupport::Deprecation.silence do + get '/:controller/:action/:id', action: 'index', id: nil + end + get ':url', controller: 'content', action: 'translate' end @@ -331,7 +336,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase end def test_route_with_regexp_for_action - rs.draw { get '/:controller/:action', action: /auth[-|_].+/ } + 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')) @@ -342,8 +347,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase 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"}, @@ -357,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")) @@ -366,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', @@ -457,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 @@ -509,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 @@ -519,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') @@ -530,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. @@ -555,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 @@ -593,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') @@ -603,10 +626,13 @@ class LegacyRouteSetTests < ActiveSupport::TestCase assert_equal '/pages/boo', url_for(rs, { :controller => 'pages', :action => 'boo' }) end - def test_route_with_fixnum_default + def test_route_with_integer_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' }) @@ -623,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' }) @@ -638,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 @@ -661,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' }) @@ -673,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', @@ -720,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' }) @@ -736,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, { @@ -776,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" @@ -801,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" }) @@ -827,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" @@ -839,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,29 +922,34 @@ class RouteSetTest < ActiveSupport::TestCase @default_route_set ||= begin 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 @@ -902,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" }) @@ -911,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 @@ -1044,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 @@ -1059,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')) @@ -1314,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') @@ -1327,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') @@ -1338,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) @@ -1349,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 @@ -1360,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 @@ -1372,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', @@ -1392,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') @@ -1407,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') @@ -1435,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') @@ -1612,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' }) @@ -1622,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' }) @@ -1632,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' }) @@ -1647,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' }) @@ -1738,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 }) @@ -1761,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 @@ -1850,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" } @@ -1964,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 2820425c31..9df70dacbf 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -11,6 +11,8 @@ class SendFileController < ActionController::Base include ActionController::Testing layout "layouts/standard" # to make sure layouts don't interfere + before_action :file, only: :file_from_before_action + attr_writer :options def options @options ||= {} @@ -20,6 +22,10 @@ class SendFileController < ActionController::Base send_file(file_path, options) end + def file_from_before_action + raise 'No file sent from before action.' + end + def test_send_file_headers_bang options = { :type => Mime[:png], @@ -192,6 +198,15 @@ class SendFileTest < ActionController::TestCase assert_nil @controller.headers['Content-Disposition'] end + def test_send_file_from_before_action + response = nil + assert_nothing_raised { response = process('file_from_before_action') } + assert_not_nil response + + assert_kind_of String, response.body + assert_equal file_data, response.body + end + %w(file data).each do |method| define_method "test_send_#{method}_status" do @controller.options = { :stream => false, :status => 500 } diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index b9caddcdb7..ea59156f65 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -137,6 +137,10 @@ XML head :created, location: 'created resource' end + def render_cookie + render plain: cookies["foo"] + end + def delete_cookie cookies.delete("foo") render plain: 'ok' @@ -150,6 +154,10 @@ XML render html: '<body class="foo"></body>'.html_safe end + def boom + raise 'boom!' + end + private def generate_url(opts) @@ -163,7 +171,9 @@ XML @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 @@ -547,7 +557,7 @@ XML assert_equal 'created', flash[:notice] end - def test_params_passing_with_fixnums + def test_params_passing_with_integer get :test_params, params: { page: { name: "Page name", month: 4, year: 2004, day: 6 } } @@ -559,7 +569,7 @@ XML ) end - def test_params_passing_with_fixnums_when_not_html_request + def test_params_passing_with_integers_when_not_html_request get :test_params, params: { format: 'json', count: 999 } parsed_params = ::JSON.parse(@response.body) assert_equal( @@ -668,7 +678,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'] } @@ -829,6 +842,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 @@ -966,6 +985,26 @@ XML assert_redirected_to 'created resource' end end + + def test_exception_in_action_reaches_test + assert_raise(RuntimeError) do + process :boom, method: "GET" + end + end + + def test_request_state_is_cleared_after_exception + assert_raise(RuntimeError) do + process :boom, + method: "GET", + params: { q: 'test1' } + end + + process :test_query_string, + method: "GET", + params: { q: 'test2' } + + assert_equal "q=test2", @response.body + end end class ResponseDefaultHeadersTest < ActionController::TestCase @@ -998,7 +1037,9 @@ class ResponseDefaultHeadersTest < ActionController::TestCase @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 @@ -1125,7 +1166,9 @@ class AnonymousControllerTest < ActionController::TestCase @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/url_for_integration_test.rb b/actionpack/test/controller/url_for_integration_test.rb index dfc2712e3e..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" } diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb index 67212fea38..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. diff --git a/actionpack/test/controller/url_rewriter_test.rb b/actionpack/test/controller/url_rewriter_test.rb index 5f2abc9606..bc0d215530 100644 --- a/actionpack/test/controller/url_rewriter_test.rb +++ b/actionpack/test/controller/url_rewriter_test.rb @@ -20,7 +20,9 @@ class UrlRewriterTests < ActionController::TestCase @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 6d377c4691..daf17558aa 100644 --- a/actionpack/test/controller/webservice_test.rb +++ b/actionpack/test/controller/webservice_test.rb @@ -99,7 +99,7 @@ class WebServiceTest < ActionDispatch::IntegrationTest def test_parsing_json_doesnot_rescue_exception req = Class.new(ActionDispatch::Request) do def params_parsers - { Mime[:json] => Proc.new { |data| raise Interrupt } } + { json: Proc.new { |data| raise Interrupt } } end def content_length; get_header('rack.input').length; end diff --git a/actionpack/test/dispatch/callbacks_test.rb b/actionpack/test/dispatch/callbacks_test.rb index 5ba76d9ab9..7b707df7f6 100644 --- a/actionpack/test/dispatch/callbacks_test.rb +++ b/actionpack/test/dispatch/callbacks_test.rb @@ -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 dfcef14344..c7194cde4a 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -520,7 +520,7 @@ class CookiesTest < ActionController::TestCase def test_accessing_nonexistent_signed_cookie_should_not_raise_an_invalid_signature get :set_signed_cookie - assert_nil @controller.send(:cookies).signed[:non_existant_attribute] + assert_nil @controller.send(:cookies).signed[:non_existent_attribute] end def test_encrypted_cookie_using_default_serializer @@ -638,7 +638,7 @@ class CookiesTest < ActionController::TestCase def test_accessing_nonexistent_encrypted_cookie_should_not_raise_invalid_message get :set_encrypted_cookie - assert_nil @controller.send(:cookies).encrypted[:non_existant_attribute] + assert_nil @controller.send(:cookies).encrypted[:non_existent_attribute] end def test_setting_invalid_encrypted_cookie_should_return_nil_when_accessing_it diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb index 159bf10545..5a39db145e 100644 --- a/actionpack/test/dispatch/debug_exceptions_test.rb +++ b/actionpack/test/dispatch/debug_exceptions_test.rb @@ -27,37 +27,37 @@ 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" + when %r{/not_found_original_exception} begin raise AbstractController::ActionNotFound.new rescue raise ActionView::Template::Error.new('template') end - when "/missing_template" + 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 @@ -67,7 +67,7 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest {}) raise ActionView::Template::Error.new(template) end - when "/framework_raises" + when %r{/framework_raises} method_that_raises else raise "puke!" @@ -75,13 +75,6 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest end end - class BoomerAPI < Boomer - def call(env) - env['action_dispatch.show_detailed_exceptions'] = @detailed - raise "puke!" - end - end - RoutesApp = Struct.new(:routes).new(SharedTestRoutes) ProductionApp = ActionDispatch::DebugExceptions.new(Boomer.new(false), RoutesApp) DevelopmentApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp) @@ -212,61 +205,60 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest assert_match(/ActionController::ParameterMissing/, body) end - test "rescue with json error for API request" do + 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 } + 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 } + 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 } + 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 } + 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 } + 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 } + 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 json on API request returns only allowed formats or json as a fallback" do + test "rescue with HTML format for HTML API request" do @app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api) - get "/index.json", headers: { 'action_dispatch.show_exceptions' => true } - assert_response 500 - assert_equal "application/json", response.content_type - assert_match(/RuntimeError: puke/, body) - get "/index.html", headers: { 'action_dispatch.show_exceptions' => true } 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) + 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 @@ -274,6 +266,25 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest 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 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/live_response_test.rb b/actionpack/test/dispatch/live_response_test.rb index e4475f4233..de57c4be1d 100644 --- a/actionpack/test/dispatch/live_response_test.rb +++ b/actionpack/test/dispatch/live_response_test.rb @@ -65,7 +65,7 @@ module ActionController latch = Concurrent::CountDownLatch.new t = Thread.new { - @response.stream.each do + @response.each do latch.count_down end } diff --git a/actionpack/test/dispatch/mapper_test.rb b/actionpack/test/dispatch/mapper_test.rb index df27e41997..69098326b9 100644 --- a/actionpack/test/dispatch/mapper_test.rb +++ b/actionpack/test/dispatch/mapper_test.rb @@ -178,6 +178,19 @@ module ActionDispatch 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/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb index 149e37bf3d..672b272590 100644 --- a/actionpack/test/dispatch/mime_type_test.rb +++ b/actionpack/test/dispatch/mime_type_test.rb @@ -49,7 +49,7 @@ class MimeTypeTest < ActiveSupport::TestCase 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 diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb index d75e31db62..b8f0ffb64a 100644 --- a/actionpack/test/dispatch/prefix_generation_test.rb +++ b/actionpack/test/dispatch/prefix_generation_test.rb @@ -166,7 +166,7 @@ module TestGenerationPrefix assert_equal "/pure-awesomeness/blog/posts/1", last_response.body end - test "[ENGINE] url_helpers from engine have higher priotity than application's url_helpers" do + test "[ENGINE] url_helpers from engine have higher priority than application's url_helpers" do get "/awesome/blog/conflicting_url" assert_equal "engine", last_response.body end @@ -319,14 +319,14 @@ module TestGenerationPrefix path = engine_object.polymorphic_url(Post.new, :host => "www.example.com") assert_equal "http://www.example.com/awesome/blog/posts/1", path end - + private def verify_redirect(url, status = 301) assert_equal status, last_response.status assert_equal url, last_response.headers["Location"] assert_equal expected_redirect_body(url), last_response.body end - + def expected_redirect_body(url) %(<html><body>You are being <a href="#{url}">redirected</a>.</body></html>) end @@ -450,14 +450,14 @@ module TestGenerationPrefix get "/absolute_custom_redirect" verify_redirect "http://example.org/foo" end - + private def verify_redirect(url, status = 301) assert_equal status, last_response.status assert_equal url, last_response.headers["Location"] assert_equal expected_redirect_body(url), last_response.body end - + def expected_redirect_body(url) %(<html><body>You are being <a href="#{url}">redirected</a>.</body></html>) 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 a3992ad008..a07138b55e 100644 --- a/actionpack/test/dispatch/request/json_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/json_params_parsing_test.rb @@ -37,9 +37,9 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest ) end - test "parses json params for application/vnd.api+json" do + test "does not parse unregistered media types such as application/vnd.api+json" do assert_parses( - {"person" => {"name" => "David"}}, + {}, "{\"person\": {\"name\": \"David\"}}", { 'CONTENT_TYPE' => 'application/vnd.api+json' } ) end @@ -103,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 @@ -143,13 +145,6 @@ class RootLessJSONParamsParsingTest < ActionDispatch::IntegrationTest ) end - test "parses json params for application/vnd.api+json" do - assert_parses( - {"user" => {"username" => "sikachu"}, "username" => "sikachu"}, - "{\"username\": \"sikachu\"}", { 'CONTENT_TYPE' => 'application/vnd.api+json' } - ) - end - test "parses json with non-object JSON content" do assert_parses( {"user" => {"_json" => "string content" }, "_json" => "string content" }, @@ -157,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 @@ -170,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 b36fbd3c76..bab4413b2a 100644 --- a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb @@ -159,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 @@ -188,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 7dcbcc5c21..e022e7e21e 100644 --- a/actionpack/test/dispatch/request/session_test.rb +++ b/actionpack/test/dispatch/request/session_test.rb @@ -114,5 +114,31 @@ module ActionDispatch }.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 0edad72fd9..8a5d85ab84 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -358,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 @@ -417,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 @@ -427,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 @@ -1152,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 diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 8b3849cb7a..aa90433505 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -37,6 +37,39 @@ 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!" @@ -189,7 +222,7 @@ class ResponseTest < ActiveSupport::TestCase 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' @@ -197,6 +230,9 @@ class ResponseTest < ActiveSupport::TestCase } resp.to_a + 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) @@ -204,6 +240,20 @@ class ResponseTest < ActiveSupport::TestCase 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' @@ -445,4 +495,20 @@ class ResponseIntegrationTest < ActionDispatch::IntegrationTest 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/concerns_test.rb b/actionpack/test/dispatch/routing/concerns_test.rb index 7ef513b0c8..6934271846 100644 --- a/actionpack/test/dispatch/routing/concerns_test.rb +++ b/actionpack/test/dispatch/routing/concerns_test.rb @@ -1,5 +1,7 @@ require 'abstract_unit' +class ReviewsController < ResourcesController; end + class RoutingConcernsTest < ActionDispatch::IntegrationTest class Reviewable def self.call(mapper, options = {}) diff --git a/actionpack/test/dispatch/routing/inspector_test.rb b/actionpack/test/dispatch/routing/inspector_test.rb index 7382c267c7..5aafcb23c2 100644 --- a/actionpack/test/dispatch/routing/inspector_test.rb +++ b/actionpack/test/dispatch/routing/inspector_test.rb @@ -17,10 +17,10 @@ module ActionDispatch @set = ActionDispatch::Routing::RouteSet.new 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 @@ -133,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 [ @@ -144,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 [ @@ -155,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 [ @@ -164,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 @@ -175,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 @@ -186,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' @@ -209,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 @@ -220,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 @@ -231,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 @@ -242,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 )" @@ -261,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 @@ -269,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' @@ -297,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 @@ -313,13 +319,35 @@ 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 @@ -336,18 +364,18 @@ module ActionDispatch end def test_routes_with_undefined_filter - output = draw(:filter => 'Rails::MissingController') do + output = draw(controller: 'Rails::MissingController') do get 'photos/:id' => 'photos#show', :id => /[A-Z]\d{5}/ end assert_equal [ - "The controller Rails::MissingController does not exist!", + "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(:filter => 'rails/dummy') do + output = draw('rails/dummy') do get 'photos/:id' => 'photos#show', :id => /[A-Z]\d{5}/ end @@ -358,7 +386,7 @@ module ActionDispatch end def test_no_routes_were_defined - output = draw(:filter => 'Rails::DummyController') { } + output = draw('Rails::DummyController') {} assert_equal [ "You don't have any routes defined!", @@ -369,6 +397,29 @@ module ActionDispatch ], 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_test.rb b/actionpack/test/dispatch/routing_test.rb index 5ead9357ae..75fdc9469f 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -116,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 @@ -125,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' @@ -427,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 @@ -450,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' @@ -1506,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' @@ -1542,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 @@ -1837,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' @@ -1849,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' @@ -3125,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 @@ -3599,6 +3610,22 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest 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) @@ -3964,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 @@ -3985,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 @@ -4122,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 @@ -4292,11 +4324,16 @@ class TestInvalidUrls < ActionDispatch::IntegrationTest 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" @@ -4627,7 +4664,9 @@ class TestErrorsInController < ActionDispatch::IntegrationTest Routes = ActionDispatch::Routing::RouteSet.new Routes.draw do - get '/:controller(/:action)' + ActiveSupport::Deprecation.silence do + get '/:controller(/:action)' + end end APP = build_app Routes @@ -4717,3 +4756,72 @@ class TestPartialDynamicPathSegments < ActionDispatch::IntegrationTest 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 + +class TestInternalRoutingParams < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + get '/test_internal/:internal' => 'internal#internal' + end + end + + class ::InternalController < ActionController::Base + def internal + head :ok + end + end + + APP = build_app Routes + + def app + APP + end + + def test_paths_with_partial_dynamic_segments_are_recognised + get '/test_internal/123' + assert_equal 200, response.status + + assert_equal( + { controller: 'internal', action: 'internal', internal: '123' }, + request.path_parameters + ) + end +end diff --git a/actionpack/test/dispatch/session/cache_store_test.rb b/actionpack/test/dispatch/session/cache_store_test.rb index dbb996973d..769de1a1e0 100644 --- a/actionpack/test/dispatch/session/cache_store_test.rb +++ b/actionpack/test/dispatch/session/cache_store_test.rb @@ -164,7 +164,9 @@ 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| diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb index f07e215e3a..09cb1d925f 100644 --- a/actionpack/test/dispatch/session/cookie_store_test.rb +++ b/actionpack/test/dispatch/session/cookie_store_test.rb @@ -345,7 +345,9 @@ 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) diff --git a/actionpack/test/dispatch/session/mem_cache_store_test.rb b/actionpack/test/dispatch/session/mem_cache_store_test.rb index 3fed9bad4f..18cb227dad 100644 --- a/actionpack/test/dispatch/session/mem_cache_store_test.rb +++ b/actionpack/test/dispatch/session/mem_cache_store_test.rb @@ -187,7 +187,9 @@ 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| diff --git a/actionpack/test/dispatch/ssl_test.rb b/actionpack/test/dispatch/ssl_test.rb index c66a0e6a7a..668b2b6cfe 100644 --- a/actionpack/test/dispatch/ssl_test.rb +++ b/actionpack/test/dispatch/ssl_test.rb @@ -7,7 +7,7 @@ class SSLTest < ActionDispatch::IntegrationTest def build_app(headers: {}, ssl_options: {}) headers = HEADERS.merge(headers) - ActionDispatch::SSL.new lambda { |env| [200, headers, []] }, ssl_options + ActionDispatch::SSL.new lambda { |env| [200, headers, []] }, ssl_options.reverse_merge(hsts: { subdomains: true }) end end @@ -39,6 +39,13 @@ class RedirectSSLTest < SSLTest assert_equal redirect[:body].join, @response.body end + 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 + test 'https is not redirected' do assert_not_redirected 'https://example.org' end @@ -98,15 +105,16 @@ end class StrictTransportSecurityTest < SSLTest EXPECTED = 'max-age=15552000' + EXPECTED_WITH_SUBDOMAINS = 'max-age=15552000; includeSubDomains' - def assert_hsts(expected, url: 'https://example.org', hsts: {}, headers: {}) + 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 test 'enabled by default' do - assert_hsts EXPECTED + assert_hsts EXPECTED_WITH_SUBDOMAINS end test 'not sent with http:// responses' do @@ -126,11 +134,15 @@ class StrictTransportSecurityTest < SSLTest end test ':expires sets max-age' do - assert_hsts 'max-age=500', hsts: { expires: 500 } + assert_deprecated do + assert_hsts 'max-age=500', hsts: { expires: 500 } + end end test ':expires supports AS::Duration arguments' do - assert_hsts 'max-age=31557600', hsts: { expires: 1.year } + assert_deprecated do + assert_hsts 'max-age=31557600', hsts: { expires: 1.year } + end end test 'include subdomains' do @@ -142,11 +154,15 @@ class StrictTransportSecurityTest < SSLTest end test 'opt in to browser preload lists' do - assert_hsts "#{EXPECTED}; preload", hsts: { preload: true } + assert_deprecated do + assert_hsts "#{EXPECTED}; preload", hsts: { preload: true } + end end test 'opt out of browser preload lists' do - assert_hsts EXPECTED, hsts: { preload: false } + assert_deprecated do + assert_hsts EXPECTED, hsts: { preload: false } + end end end diff --git a/actionpack/test/dispatch/test_request_test.rb b/actionpack/test/dispatch/test_request_test.rb index 51c469a61a..3c19cbd68a 100644 --- a/actionpack/test/dispatch/test_request_test.rb +++ b/actionpack/test/dispatch/test_request_test.rb @@ -88,6 +88,33 @@ class TestRequestTest < ActiveSupport::TestCase 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/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/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/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/journey/router_test.rb b/actionpack/test/journey/router_test.rb index 15d51e5d6c..75caf56d32 100644 --- a/actionpack/test/journey/router_test.rb +++ b/actionpack/test/journey/router_test.rb @@ -3,7 +3,7 @@ require 'abstract_unit' module ActionDispatch module Journey class TestRouter < ActiveSupport::TestCase - attr_reader :routes, :mapper + attr_reader :mapper, :routes, :route_set, :router def setup @app = Routing::RouteSet::Dispatcher.new({}) @@ -15,36 +15,36 @@ module ActionDispatch end def test_dashes - mapper.get '/foo-bar-baz', to: 'foo#bar' + get '/foo-bar-baz', to: 'foo#bar' env = rails_env 'PATH_INFO' => '/foo-bar-baz' called = false - @router.recognize(env) do |r, params| + router.recognize(env) do |r, params| called = true end assert called end def test_unicode - mapper.get '/ほげ', to: 'foo#bar' + get '/ほげ', to: 'foo#bar' #match the escaped version of /ほげ env = rails_env 'PATH_INFO' => '/%E3%81%BB%E3%81%92' called = false - @router.recognize(env) do |r, params| + router.recognize(env) do |r, params| called = true end assert called end def test_regexp_first_precedence - mapper.get "/whois/:domain", :domain => /\w+\.[\w\.]+/, to: "foo#bar" - mapper.get "/whois/:id(.:format)", to: "foo#baz" + 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 @@ -55,7 +55,7 @@ module ActionDispatch end def test_required_parts_verified_are_anchored - mapper.get "/foo/:id", :id => /\d/, anchor: false, to: "foo#bar" + get "/foo/:id", :id => /\d/, anchor: false, to: "foo#bar" assert_raises(ActionController::UrlGenerationError) do @formatter.generate(nil, { :controller => "foo", :action => "bar", :id => '10' }, { }) @@ -63,7 +63,7 @@ module ActionDispatch end def test_required_parts_are_verified_when_building - mapper.get "/foo/:id", :id => /\d+/, anchor: false, to: "foo#bar" + get "/foo/:id", :id => /\d+/, anchor: false, to: "foo#bar" path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar", :id => '10' }, { }) assert_equal '/foo/10', path @@ -74,7 +74,7 @@ module ActionDispatch end def test_only_required_parts_are_verified - mapper.get "/foo(/:id)", :id => /\d/, :to => "foo#bar" + get "/foo(/:id)", :id => /\d/, :to => "foo#bar" path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar", :id => '10' }, { }) assert_equal '/foo/10', path @@ -88,8 +88,7 @@ module ActionDispatch def test_knows_what_parts_are_missing_from_named_route route_name = "gorby_thunderhorse" - mapper = ActionDispatch::Routing::Mapper.new @route_set - mapper.get "/foo/:id", :as => route_name, :id => /\d+/, :to => "foo#bar" + get "/foo/:id", :as => route_name, :id => /\d+/, :to => "foo#bar" error = assert_raises(ActionController::UrlGenerationError) do @formatter.generate(route_name, { }, { }) @@ -109,19 +108,16 @@ module ActionDispatch end def test_X_Cascade - mapper.get "/messages(.:format)", to: "foo#bar" - 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 @@ -130,38 +126,38 @@ module ActionDispatch end def test_defaults_merge_correctly - mapper.get '/foo(/:id)', to: "foo#bar", id: nil + get '/foo(/:id)', to: "foo#bar", id: nil env = rails_env 'PATH_INFO' => '/foo/10' - @router.recognize(env) do |r, 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| + router.recognize(env) do |r, params| assert_equal({:id => nil, :controller => "foo", :action => "bar"}, params) end end def test_recognize_with_unbound_regexp - mapper.get "/foo", anchor: false, to: "foo#bar" + 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 - mapper.get "/foo", to: "foo#bar" + 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'] @@ -174,41 +170,41 @@ module ActionDispatch "/messages/:id/edit(.:format)", "/messages/:id(.:format)" ].each do |path| - mapper.get path, to: "foo#bar" + 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 - mapper.get "/messages/:a/:b", to: "foo#bar" + get "/messages/:a/:b", to: "foo#bar" path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar", :a => 'a' }, { :b => 'b' }) assert_equal "/messages/a/b", path end def test_splat_in_recall - mapper.get "/*path", to: "foo#bar" + get "/*path", to: "foo#bar" path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar" }, { :path => 'b' }) assert_equal "/b", path end def test_recall_should_be_used_when_scoring - mapper.get "/messages/:action(/:id(.:format))", to: 'foo#bar' - mapper.get "/messages/:id(.:format)", to: 'bar#baz' + get "/messages/:action(/:id(.:format))", to: 'foo#bar' + get "/messages/:id(.:format)", to: 'bar#baz' path, _ = @formatter.generate(nil, { :controller => "foo", :id => 10 }, { :action => 'index' }) assert_equal "/messages/index/10", path end def test_nil_path_parts_are_ignored - mapper.get "/:controller(/:action(.:format))", to: "tasks#lol" + get "/:controller(/:action(.:format))", to: "tasks#lol" params = { :controller => "tasks", :format => nil } extras = { :action => 'lol' } @@ -220,14 +216,14 @@ module ActionDispatch def test_generate_slash params = [ [:controller, "tasks"], [:action, "show"] ] - mapper.get "/", Hash[params] + get "/", Hash[params] path, _ = @formatter.generate(nil, Hash[params], {}) assert_equal '/', path end def test_generate_calls_param_proc - mapper.get '/:controller(/:action)', to: "foo#bar" + get '/:controller(/:action)', to: "foo#bar" parameterized = [] params = [ [:controller, "tasks"], @@ -243,7 +239,7 @@ module ActionDispatch end def test_generate_id - mapper.get '/:controller(/:action)', to: 'foo#bar' + get '/:controller(/:action)', to: 'foo#bar' path, params = @formatter.generate( nil, {:id=>1, :controller=>"tasks", :action=>"show"}, {}) @@ -252,7 +248,7 @@ module ActionDispatch end def test_generate_escapes - mapper.get '/:controller(/:action)', to: "foo#bar" + get '/:controller(/:action)', to: "foo#bar" path, _ = @formatter.generate(nil, { :controller => "tasks", @@ -262,7 +258,7 @@ module ActionDispatch end def test_generate_escapes_with_namespaced_controller - mapper.get '/:controller(/:action)', to: "foo#bar" + get '/:controller(/:action)', to: "foo#bar" path, _ = @formatter.generate( nil, { :controller => "admin/tasks", @@ -272,7 +268,7 @@ module ActionDispatch end def test_generate_extra_params - mapper.get '/:controller(/:action)', to: "foo#bar" + get '/:controller(/:action)', to: "foo#bar" path, params = @formatter.generate( nil, { :id => 1, @@ -285,7 +281,7 @@ module ActionDispatch end def test_generate_missing_keys_no_matches_different_format_keys - mapper.get '/:controller/:action/:name', to: "foo#bar" + get '/:controller/:action/:name', to: "foo#bar" primarty_parameters = { :id => 1, :controller => "tasks", @@ -311,7 +307,7 @@ module ActionDispatch end def test_generate_uses_recall_if_needed - mapper.get '/:controller(/:action(/:id))', to: "foo#bar" + get '/:controller(/:action(/:id))', to: "foo#bar" path, params = @formatter.generate( nil, @@ -322,7 +318,7 @@ module ActionDispatch end def test_generate_with_name - mapper.get '/:controller(/:action)', to: 'foo#bar', as: 'tasks' + get '/:controller(/:action)', to: 'foo#bar', as: 'tasks' path, params = @formatter.generate( "tasks", @@ -338,13 +334,13 @@ 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 - mapper.get "/:controller(/:action(/:id))", to: 'foo#bar' + 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({ :action => "bar" }.merge(expected), params) called = true @@ -359,13 +355,13 @@ 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 - mapper.get '/:segment/*splat', to: 'foo#bar' + 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.merge(:controller=>"foo", :action=>"bar"), params) called = true @@ -376,7 +372,7 @@ module ActionDispatch end def test_namespaced_controller - mapper.get "/:controller(/:action(/:id))", { :controller => /.+?/ } + get "/:controller(/:action(/:id))", { :controller => /.+?/ } route = @routes.first env = rails_env 'PATH_INFO' => '/admin/users/show/10' @@ -387,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 @@ -396,13 +392,13 @@ module ActionDispatch end def test_recognize_literal - mapper.get "/books(/:action(.:format))", 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 @@ -412,7 +408,7 @@ module ActionDispatch end def test_recognize_head_route - mapper.match "/books(/:action(.:format))", via: 'head', to: 'foo#bar' + match "/books(/:action(.:format))", via: 'head', to: 'foo#bar' env = rails_env( 'PATH_INFO' => '/books/list.rss', @@ -420,7 +416,7 @@ module ActionDispatch ) called = false - @router.recognize(env) do |r, params| + router.recognize(env) do |r, params| called = true end @@ -428,13 +424,13 @@ module ActionDispatch end def test_recognize_head_request_as_get_route - mapper.get "/books(/:action(.:format))", to: 'foo#bar' + 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 @@ -442,13 +438,13 @@ module ActionDispatch end def test_recognize_cares_about_get_verbs - mapper.match "/books(/:action(.:format))", to: "foo#bar", via: :get + 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 @@ -456,13 +452,13 @@ module ActionDispatch end def test_recognize_cares_about_post_verbs - mapper.match "/books(/:action(.:format))", to: "foo#bar", via: :post + match "/books(/:action(.:format))", to: "foo#bar", via: :post 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 @@ -470,14 +466,14 @@ module ActionDispatch end def test_multi_verb_recognition - mapper.match "/books(/:action(.:format))", to: "foo#bar", via: [:post, :get] + match "/books(/:action(.:format))", to: "foo#bar", via: [:post, :get] %w( POST GET ).each do |verb| env = rails_env 'PATH_INFO' => '/books/list.rss', "REQUEST_METHOD" => verb called = false - @router.recognize(env) do |r, params| + router.recognize(env) do |r, params| called = true end @@ -488,7 +484,7 @@ module ActionDispatch "REQUEST_METHOD" => 'PUT' called = false - @router.recognize(env) do |r, params| + router.recognize(env) do |r, params| called = true end @@ -497,6 +493,18 @@ module ActionDispatch 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 + def rails_env env, klass = ActionDispatch::Request klass.new(rack_env(env)) end diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index d85681e0d1..9d669c7cd8 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,274 +1,19 @@ -* Fix stripping the digest from the automatically generated img tag alt - attribute when assets are handled by Sprockets >=3.0. +* `select_tag`'s `include_blank` option for generation for blank option tag, now adds an empty space label, + when the value as well as content for option tag are empty, so that we confirm with html specification. + Ref: https://www.w3.org/TR/html5/forms.html#the-option-element. - *Bart de Water* + Generation of option before: -* Create a new `ActiveSupport::SafeBuffer` instance when `content_for` is flushed. - - Fixes #19890 - - *Yoong Kang Lim* - -* Fix `collection_radio_buttons` hidden_field name and make it appear - before the actual input radio tags to make the real value override - the hidden when passed. - - Fixes #22773. - - *Santiago Pastorino* - -* `ActionView::TestCase::Controller#params` returns an instance of - `ActionController::Parameters`. - - *Justin Coyne* - -* Fix regression in `submit_tag` when a symbol is used as label argument. - - *Yuuji Yaginuma* - - -## Rails 5.0.0.beta1 (December 18, 2015) ## - -* `I18n.translate` helper will wrap the missing translation keys - in a <span> tag only if `debug_missing_translation` configuration - be true. Default value is `true`. For example in `application.rb`: - - # in order to turn off missing key wrapping - config.action_view.debug_missing_translation = false - - *Sameer Rahmani* - -* Respect value of `:object` if `:object` is false when rendering. - - Fixes #22260. - - *Yuichiro Kaneko* - -* Generate `week_field` input values using a 1-based index and not a 0-based index - as per the W3 spec: http://www.w3.org/TR/html-markup/datatypes.html#form.data.week - - *Christoph Geschwind* - -* Allow `host` option in `javascript_include_tag` and `stylesheet_link_tag` helpers - - *Grzegorz Witek* - -* Restrict `url_for :back` to valid, non-JavaScript URLs. GH#14444 - - *Damien Burke* - -* Allow `date_select` helper selected option to accept hash like the default options. - - *Lecky Lao* - -* Collection input propagates input's `id` to the label's `for` attribute when - using html options as the last element of collection. - - *Vasiliy Ermolovich* - -* Add a `hidden_field` on the `collection_radio_buttons` to avoid raising a error - when the only input on the form is the `collection_radio_buttons`. - - *Mauro George* - -* `url_for` does not modify its arguments when generating polymorphic URLs. - - *Bernerd Schaefer* - -* `number_to_currency` and `number_with_delimiter` now accept a custom `delimiter_pattern` option - to handle placement of delimiter, to support currency formats like INR. - - Example: - - number_to_currency(1230000, delimiter_pattern: /(\d+?)(?=(\d\d)+(\d)(?!\d))/, unit: '₹', format: "%u %n") - # => '₹ 12,30,000.00' - - *Vipul A M* - -* Make `disable_with` the default behavior for submit tags. Disables the - button on submit to prevent double submits. - - *Justin Schiff* - -* Add a break_sequence option to word_wrap so you can specify a custom break. - - *Mauricio Gomez* - -* Add wildcard matching to explicit dependencies. - - Turns: - - ```erb - <% # Template Dependency: recordings/threads/events/subscribers_changed %> - <% # Template Dependency: recordings/threads/events/completed %> - <% # Template Dependency: recordings/threads/events/uncompleted %> - ``` - - Into: - - ```erb - <% # Template Dependency: recordings/threads/events/* %> - ``` - - *Kasper Timm Hansen* - -* Allow defining explicit collection caching using a `# Template Collection: ...` - directive inside templates. - - *Dov Murik* - -* Asset helpers raise `ArgumentError` when `nil` is passed as a source. - - *Anton Kolomiychuk* - -* Always attach the template digest to the cache key for collection caching - even when `virtual_path` is not available from the view context. - Which could happen if the rendering was done directly in the controller - and not in a template. - - Fixes #20535. - - *Roque Pinel* - -* Improve detection of partial templates eligible for collection caching, - now allowing multi-line comments at the beginning of the template file. - - *Dov Murik* - -* Raise an `ArgumentError` when a false value for `include_blank` is passed to a - required select field (to comply with the HTML5 spec). - - *Grey Baker* - -* Do not put partial name to `local_assigns` when rendering without - an object or a collection. - - *Henrik Nygren* - -* Remove `:rescue_format` option for `translate` helper since it's no longer - supported by I18n. - - *Bernard Potocki* - -* `translate` should handle `raise` flag correctly in case of both main and default - translation is missing. - - Fixes #19967. - - *Bernard Potocki* - -* Load the `default_form_builder` from the controller on initialization, which overrides - the global config if it is present. - - *Kevin McPhillips* - -* Accept lambda as `child_index` option in `fields_for` method. - - *Karol Galanciak* - -* `translate` allows `default: [[]]` again for a default value of `[]`. - - Fixes #19640. - - *Adam Prescott* - -* `translate` should accept nils as members of the `:default` - parameter without raising a translation missing error. - - Fixes #19419. - - *Justin Coyne* - -* `number_to_percentage` does not crash with `Float::NAN` or `Float::INFINITY` - as input when `precision: 0` is used. - - Fixes #19227. - - *Yves Senn* - -* Fixed the translation helper method to accept different default values types - besides String. - - *Ulisses Almeida* - -* Collection rendering automatically caches and fetches multiple partials. - - Collections rendered as: - - ```ruby - <%= render @notifications %> - <%= render partial: 'notifications/notification', collection: @notifications, as: :notification %> + ```html + <option value=""></option> ``` - will now read several partials from cache at once, if the template starts with a cache call: + Generation of option after: - ```ruby - # notifications/_notification.html.erb - <% cache notification do %> - <%# ... %> - <% end %> + ```html + <option value="" label=" "></option> ``` - *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 raising an 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* + *Vipul A M* -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/Rakefile b/actionview/Rakefile index 93be50721d..f0f9bda3e4 100644 --- a/actionview/Rakefile +++ b/actionview/Rakefile @@ -3,6 +3,8 @@ require 'rake/testtask' desc "Default Task" task :default => :test +task :package + # Run the unit tests desc "Run all unit tests" diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec index 612e94021d..75c5045ec0 100644 --- a/actionview/actionview.gemspec +++ b/actionview/actionview.gemspec @@ -9,11 +9,11 @@ Gem::Specification.new do |s| 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/lib/action_view/dependency_tracker.rb b/actionview/lib/action_view/dependency_tracker.rb index 5a4c3ea3fe..7731773040 100644 --- a/actionview/lib/action_view/dependency_tracker.rb +++ b/actionview/lib/action_view/dependency_tracker.rb @@ -7,18 +7,20 @@ module ActionView def self.find_dependencies(name, template, view_paths = nil) tracker = @trackers[template.handler] - return [] unless tracker.present? + return [] unless tracker - if tracker.respond_to?(:supports_view_paths?) && tracker.supports_view_paths? - tracker.call(name, template, view_paths) - else - tracker.call(name, template) - 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) @@ -151,11 +153,11 @@ module ActionView def resolve_directories(wildcard_dependencies) return [] unless @view_paths - wildcard_dependencies.each_with_object([]) do |query, templates| - @view_paths.find_all_with_query(query).each do |template| - templates << "#{File.dirname(query)}/#{File.basename(template).split('.').first}" + 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 - end + }.sort end def explicit_dependencies diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index 6f2f9ca53c..f3c29d663c 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -4,16 +4,7 @@ require 'monitor' module ActionView class Digestor - cattr_reader(:cache) - @@cache = Concurrent::Map.new - @@digest_monitor = Monitor.new - - class PerRequestDigestCacheExpiry < Struct.new(:app) # :nodoc: - def call(env) - ActionView::Digestor.cache.clear - app.call(env) - end - end + @@digest_mutex = Mutex.new class << self # Supported options: @@ -22,106 +13,108 @@ module ActionView # * <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, finder.rendered_format, dependencies ].flatten.compact.join('.') # this is a correctly done double-checked locking idiom # (Concurrent::Map'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) + 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 - - @@cache[cache_key] = stored_digest = klass.new(options).digest - ensure - # something went wrong or ActionView::Resolver.caching? is false, make sure not to corrupt the @@cache - @@cache.delete_pair(cache_key, false) if pre_stored && !stored_digest - end - end + def logger + ActionView::Base.logger || NullLogger + end - attr_reader :name, :finder, :options + # Create a dependency tree for template named +name+. + def tree(name, finder, partial = false, seen = {}) + logical_name = name.gsub(%r|/_|, "/") - def initialize(options) - @name, @finder = options.values_at(:name, :finder) - @options = options.except(:name, :finder) - end + options = {} + options[:formats] = [finder.rendered_format] if finder.rendered_format - def digest - Digest::MD5.hexdigest("#{source}-#{dependency_digest}").tap do |digest| - logger.try :debug, " Cache digest for #{template.inspect}: #{digest}" - end - rescue ActionView::MissingTemplate - logger.try :error, " Couldn't find template for digesting: #{name}" - '' - end + if finder.disable_cache { finder.exists?(logical_name, [], partial, [], options) } + template = finder.disable_cache { finder.find(logical_name, [], partial, [], options) } + finder.rendered_format ||= template.formats.first - def dependencies - DependencyTracker.find_dependencies(name, template, finder.view_paths) - rescue ActionView::MissingTemplate - logger.try :error, " '#{name}' file doesn't exist, so no dependencies" - [] - 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 nested_dependencies - dependencies.collect do |dependency| - dependencies = PartialDigestor.new(name: dependency, finder: finder).nested_dependencies - dependencies.any? ? { dependency => dependencies } : dependency + 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 end - private - def logger - ActionView::Base.logger + class Node + attr_reader :name, :logical_name, :template, :children + + def self.create(name, logical_name, template, partial) + klass = partial ? Partial : Node + klass.new(name, logical_name, template, []) 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 bc61920848..4b912f0b2b 100644 --- a/actionview/lib/action_view/flows.rb +++ b/actionview/lib/action_view/flows.rb @@ -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 23d5319579..5fc4f3f1b9 100644 --- a/actionview/lib/action_view/gem_version.rb +++ b/actionview/lib/action_view/gem_version.rb @@ -6,9 +6,9 @@ module ActionView module VERSION MAJOR = 5 - MINOR = 0 + MINOR = 1 TINY = 0 - PRE = "beta1.1" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/actionview/lib/action_view/helpers/atom_feed_helper.rb b/actionview/lib/action_view/helpers/atom_feed_helper.rb index dba70e284e..c875f5870f 100644 --- a/actionview/lib/action_view/helpers/atom_feed_helper.rb +++ b/actionview/lib/action_view/helpers/atom_feed_helper.rb @@ -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 18b2102d73..4eaaa239e2 100644 --- a/actionview/lib/action_view/helpers/cache_helper.rb +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -41,11 +41,11 @@ module ActionView # # ==== \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. # @@ -118,7 +118,7 @@ module ActionView # # 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 + # 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 %> @@ -126,47 +126,33 @@ module ActionView # # 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 as: + # 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. - # - # See ActionView::Template::Handlers::ERB.resource_cache_call_pattern for - # more information on what cache calls make a template eligible for this - # collection caching. - # - # The automatic cache multi read can be turned off like so: + # Templates in the collection not already cached are written to cache. # - # <%= render @notifications, cache: false %> + # Works great alongside individual template fragment caching. + # For instance if the template the collection renders is cached like: # - # === Explicit Collection Caching - # - # If the partial template doesn't start with a clean cache call as - # mentioned above, you can still benefit from collection caching by - # adding a special comment format anywhere in the template, like: - # - # <%# Template Collection: notification %> - # <% my_helper_that_calls_cache(some_arg, notification) do %> - # <%= notification.name %> + # # notifications/_notification.html.erb + # <% cache notification do %> + # <%# ... %> # <% end %> # - # The pattern used to match these is <tt>/# Template Collection: (\S+)/</tt>, - # so it's important that you type it out just so. - # You can only declare one collection in a partial template file. + # 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 - safe_concat(fragment_for(cache_fragment_name(name, options), options, &block)) + name_options = options.slice(:skip_digest, :virtual_path) + safe_concat(fragment_for(cache_fragment_name(name, name_options), options, &block)) else yield end diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb index 233e613e97..04dcf01bb7 100644 --- a/actionview/lib/action_view/helpers/date_helper.rb +++ b/actionview/lib/action_view/helpers/date_helper.rb @@ -3,6 +3,7 @@ require 'action_view/helpers/tag_helper' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/date/conversions' require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/object/acts_like' require 'active_support/core_ext/object/with_options' module ActionView @@ -226,8 +227,10 @@ 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. @@ -994,7 +997,7 @@ 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".freeze, '', :value => '') + "\n" if @options[:include_blank] @@ -1004,6 +1007,20 @@ module ActionView (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. # prompt_option_tag(:month, prompt: 'Select month') # => "<option value="">Select month</option>" diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index c1015ffe89..be5010cd9c 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -1066,7 +1066,7 @@ module ActionView # Returns a text_field of type "time". # # The default value is generated by trying to call +strftime+ with "%T.%L" - # on the objects's value. It is still possible to override that + # on the object's value. It is still possible to override that # by passing the "value" option. # # === Options @@ -1118,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 @@ -1922,8 +1926,6 @@ module ActionView @object_name.to_s.humanize end - model = model.downcase - defaults = [] defaults << :"helpers.submit.#{object_name}.#{key}" defaults << :"helpers.submit.#{key}" diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb index 430051379d..b277efd7b6 100644 --- a/actionview/lib/action_view/helpers/form_options_helper.rb +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -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+. diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index 55dac74d00..82f2fd30c7 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -134,13 +134,15 @@ module ActionView if options.include?(:include_blank) include_blank = options.delete(:include_blank) + options_for_blank_options_tag = { value: '' } if include_blank == true include_blank = '' + options_for_blank_options_tag[:label] = ' ' end if include_blank - option_tags = content_tag("option".freeze, include_blank, value: '').safe_concat(option_tags) + option_tags = content_tag("option".freeze, include_blank, options_for_blank_options_tag).safe_concat(option_tags) end end @@ -691,6 +693,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 @@ -862,13 +868,13 @@ 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, form_options: { action: html_options["action"], diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb index 161aa031c6..23081c5f07 100644 --- a/actionview/lib/action_view/helpers/number_helper.rb +++ b/actionview/lib/action_view/helpers/number_helper.rb @@ -23,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 @@ -35,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. # @@ -52,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 @@ -256,8 +263,6 @@ module ActionView # * <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) # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when # the argument is invalid. # diff --git a/actionview/lib/action_view/helpers/output_safety_helper.rb b/actionview/lib/action_view/helpers/output_safety_helper.rb index c0fc3b820f..d4b55423a8 100644 --- a/actionview/lib/action_view/helpers/output_safety_helper.rb +++ b/actionview/lib/action_view/helpers/output_safety_helper.rb @@ -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/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb index 191a881de0..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 compatibility 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 2562504896..42e7358a1d 100644 --- a/actionview/lib/action_view/helpers/tag_helper.rb +++ b/actionview/lib/action_view/helpers/tag_helper.rb @@ -154,6 +154,7 @@ module ActionView options.each_pair do |key, value| if TAG_PREFIXES.include?(key) && value.is_a?(Hash) value.each_pair do |k, v| + next if v.nil? output << sep output << prefix_tag_option(key, k, v, escape) end diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index 3a4561a083..11c7daf4da 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -302,7 +302,7 @@ 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') || {} @@ -312,9 +312,10 @@ module ActionView form_options[:'data-remote'] = true if remote request_token_tag = if form_method == 'post' - token_tag(nil, form_options: form_options) + 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) @@ -329,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) @@ -480,7 +481,7 @@ module ActionView option = html_options.delete(item).presence || next "#{item.dasherize}=#{ERB::Util.url_encode(option)}" }.compact - extras = extras.empty? ? '' : '?' + extras.join('&') + extras = extras.empty? ? ''.freeze : '?' + extras.join('&') encoded_email_address = ERB::Util.url_encode(email_address).gsub("%40", "@") html_options["href"] = "mailto:#{encoded_email_address}#{extras}" @@ -558,29 +559,29 @@ 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, form_options: {}) @@ -588,13 +589,49 @@ module ActionView 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/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 6a76d80c47..9db1460ee7 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -22,7 +22,7 @@ module ActionView 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 @@ -34,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 @@ -59,9 +55,7 @@ module ActionView class DetailsKey #:nodoc: alias :eql? :equal? - alias :object_hash :hash - attr_reader :hash @details_keys = Concurrent::Map.new def self.get(details) @@ -69,15 +63,15 @@ module ActionView details = details.dup details[:formats] &= Template::Types.symbols end - @details_keys[details] ||= new + @details_keys[details] ||= Concurrent::Map.new end def self.clear @details_keys.clear end - def initialize - @hash = object_hash + def self.digest_caches + @details_keys.values end end @@ -136,6 +130,11 @@ module ActionView 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 @@ -172,6 +171,32 @@ 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. @@ -195,15 +220,27 @@ 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 + 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) diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb index 59d869d92d..dfb99f4ea9 100644 --- a/actionview/lib/action_view/railtie.rb +++ b/actionview/lib/action_view/railtie.rb @@ -37,16 +37,10 @@ module ActionView end end - initializer "action_view.collection_caching" do |app| - ActiveSupport.on_load(:action_controller) do - PartialRenderer.collection_cache = app.config.action_controller.cache_store - end - end - initializer "action_view.per_request_digest_cache" do |app| ActiveSupport.on_load(:action_view) do if app.config.consider_all_requests_local - app.middleware.use ActionView::Digestor::PerRequestDigestCacheExpiry + app.executor.to_run { ActionView::LookupContext::DetailsKey.clear } end end end @@ -57,9 +51,13 @@ module ActionView end end + 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/dependencies.rake" + load "action_view/tasks/cache_digests.rake" end end end diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb index aa77a77acf..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, :find_file, :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 bdbf03191a..13b4ec6133 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -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 @@ -521,7 +521,7 @@ module ActionView def retrieve_variable(path, as) variable = as || begin base = path[-1] == "/".freeze ? "".freeze : File.basename(path) - raise_invalid_identifier(path) unless base =~ /\A_?(.*)(?:\.\w+)*\z/ + 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 1147963882..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,5 +1,3 @@ -require 'active_support/core_ext/object/try' - module ActionView module CollectionCaching # :nodoc: extend ActiveSupport::Concern @@ -11,42 +9,25 @@ module ActionView 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 @@ -56,12 +37,10 @@ module ActionView 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/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb index 9d15bbfca7..1d6afb90fe 100644 --- a/actionview/lib/action_view/renderer/template_renderer.rb +++ b/actionview/lib/action_view/renderer/template_renderer.rb @@ -84,13 +84,13 @@ module ActionView 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(formats), keys, formats) 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 15fc2b71a3..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_name @compiled = false @original_encoding = nil @locals = details[:locals] || [] @@ -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 @@ -355,23 +350,5 @@ module ActionView ActiveSupport::Notifications.instrument("#{action}.action_view".freeze, payload, &block) end end - - EXPLICIT_COLLECTION = /# Template Collection: (?<resource_name>\w+)/ - - def extract_resource_cache_name - if match = @source.match(EXPLICIT_COLLECTION) || resource_cache_call_match - match[:resource_name] - end - end - - def resource_cache_call_match - if @handler.respond_to?(:resource_cache_call_pattern) - @source.match(@handler.resource_cache_call_pattern) - end - end - - def inferred_cache_name - @inferred_cache_name ||= @virtual_path.split('/'.freeze).last.sub('_'.freeze, ''.freeze) - end end end diff --git a/actionview/lib/action_view/template/error.rb b/actionview/lib/action_view/template/error.rb index b03b197cb5..3f38c3d2b9 100644 --- a/actionview/lib/action_view/template/error.rb +++ b/actionview/lib/action_view/template/error.rb @@ -59,6 +59,9 @@ module ActionView class Error < ActionViewError #:nodoc: SOURCE_CODE_RADIUS = 3 + # Override to prevent #cause resetting during re-raise. + attr_reader :cause + def initialize(template, original_exception = nil) if original_exception ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ @@ -67,6 +70,7 @@ module ActionView super($!.message) set_backtrace($!.backtrace) + @cause = $! @template, @sub_templates = template, nil end @@ -131,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/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb index 1f8459c24b..85a100ed4c 100644 --- a/actionview/lib/action_view/template/handlers/erb.rb +++ b/actionview/lib/action_view/template/handlers/erb.rb @@ -123,31 +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 captured as :resource_name. - # - # <% 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 - (?:<%\#.*%>)* # optional initial comment - \s* # followed by optional spaces or newlines - <%\s*cache[\(\s] # followed by an ERB call to cache - \s* # followed by optional spaces or newlines - (?<resource_name>\w+) # capture the cache call argument as :resource_name - [\s\)] # followed by a space or close paren - /xm - end - private def valid_encoding(string, encoding) diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb index 8a675cd521..bf68e93c58 100644 --- a/actionview/lib/action_view/template/resolver.rb +++ b/actionview/lib/action_view/template/resolver.rb @@ -55,6 +55,10 @@ module ActionView @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 def cache(key, name, prefix, partial, locals) if Resolver.caching? @@ -84,6 +88,23 @@ module ActionView @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 def canonical_no_templates(templates) @@ -222,7 +243,7 @@ module ActionView end def find_template_paths(query) - Dir[query].reject do |filename| + Dir[query].uniq.reject do |filename| File.directory?(filename) || # deals with case-insensitive file systems. !File.fnmatch(query, filename, File::FNM_EXTGLOB) @@ -245,8 +266,12 @@ module ActionView partial = escape_entry(path.partial? ? "_#{path.name}" : path.name) 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) @@ -340,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 c233d06ccb..b32567cd66 100644 --- a/actionview/lib/action_view/template/types.rb +++ b/actionview/lib/action_view/template/types.rb @@ -1,4 +1,3 @@ -require 'set' require 'active_support/core_ext/module/attribute_accessors' module ActionView diff --git a/actionview/lib/action_view/view_paths.rb b/actionview/lib/action_view/view_paths.rb index 37722013ce..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 diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb index 79173f730f..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') @@ -95,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 diff --git a/actionview/test/actionpack/abstract/layouts_test.rb b/actionview/test/actionpack/abstract/layouts_test.rb index 80bc665b0a..78f6e78c61 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" @@ -39,6 +49,10 @@ module AbstractControllerTests render :template => ActionView::Template::Text.new("Hello string!") end + def action_has_layout_false + render template: ActionView::Template::Text.new("Hello string!") + end + def overwrite_default render :template => ActionView::Template::Text.new("Hello string!"), :layout => :default end @@ -82,7 +96,7 @@ module AbstractControllerTests end end - class WithProcReturningNil < Base + class WithProcReturningNil < WithString layout proc { nil } def index @@ -90,6 +104,14 @@ module AbstractControllerTests end end + class WithProcReturningFalse < WithString + layout proc { false } + + def index + render template: ActionView::Template::Text.new("Hello false!") + end + end + class WithZeroArityProc < Base layout proc { "overwrite" } @@ -189,6 +211,14 @@ module AbstractControllerTests end end + class WithOnlyConditionalFlipped < WithOnlyConditional + layout "hello_override", only: :index + end + + class WithOnlyConditionalFlippedAndInheriting < WithOnlyConditional + layout nil, only: :index + end + class WithExceptConditional < WithStringImpliedChild layout "overwrite", :except => :show @@ -201,6 +231,45 @@ module AbstractControllerTests end end + class AbstractWithString < Base + layout "hello" + abstract! + end + + class AbstractWithStringChild < AbstractWithString + def index + render template: ActionView::Template::Text.new("Hello abstract child!") + end + end + + class AbstractWithStringChildDefaultsToInherited < AbstractWithString + layout nil + + def index + render template: ActionView::Template::Text.new("Hello abstract child!") + end + end + + class WithConditionalOverride < WithString + layout "overwrite", only: :overwritten + + def non_overwritten + render template: ActionView::Template::Text.new("Hello non overwritten!") + end + + def overwritten + render template: ActionView::Template::Text.new("Hello overwritten!") + end + end + + class WithConditionalOverrideFlipped < WithConditionalOverride + layout "hello_override", only: :non_overwritten + end + + class WithConditionalOverrideFlippedAndInheriting < WithConditionalOverride + layout nil, only: :non_overwritten + end + class TestBase < ActiveSupport::TestCase test "when no layout is specified, and no default is available, render without a layout" do controller = Blank.new @@ -208,6 +277,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) @@ -264,10 +358,16 @@ module AbstractControllerTests assert_equal "Overwrite Hello proc!", controller.response_body end - test "when layout is specified as a proc and the proc returns nil, don't use a layout" do + test "when layout is specified as a proc and the proc returns nil, use inherited layout" do controller = WithProcReturningNil.new controller.process(:index) - assert_equal "Hello nil!", controller.response_body + assert_equal "With String Hello nil!", controller.response_body + end + + test "when layout is specified as a proc and the proc returns false, use no layout instead of inherited layout" do + controller = WithProcReturningFalse.new + controller.process(:index) + assert_equal "Hello false!", controller.response_body end test "when layout is specified as a proc without parameters it works just the same" do @@ -328,12 +428,24 @@ module AbstractControllerTests end test "when a grandchild has nil layout specified, the child has an implied layout, and the " \ - "parent has specified a layout, use the child controller layout" do + "parent has specified a layout, use the grand child controller layout" do controller = WithGrandChildOfImplied.new controller.process(:index) assert_equal "With Grand Child Hello string!", controller.response_body end + test "a child inherits layout from abstract controller" do + controller = AbstractWithStringChild.new + controller.process(:index) + assert_equal "With String Hello abstract child!", controller.response_body + end + + test "a child inherits layout from abstract controller2" do + controller = AbstractWithStringChildDefaultsToInherited.new + controller.process(:index) + assert_equal "With String Hello abstract child!", controller.response_body + end + test "raises an exception when specifying layout true" do assert_raises ArgumentError do Object.class_eval do @@ -356,6 +468,30 @@ module AbstractControllerTests assert_equal "With Implied Hello index!", controller.response_body end + test "when specify an :only option which match current action name and is opposite from parent controller" do + controller = WithOnlyConditionalFlipped.new + controller.process(:show) + assert_equal "With Implied Hello show!", controller.response_body + end + + test "when specify an :only option which does not match current action name and is opposite from parent controller" do + controller = WithOnlyConditionalFlipped.new + controller.process(:index) + assert_equal "With Override Hello index!", controller.response_body + end + + test "when specify to inherit and an :only option which match current action name and is opposite from parent controller" do + controller = WithOnlyConditionalFlippedAndInheriting.new + controller.process(:show) + assert_equal "With Implied Hello show!", controller.response_body + end + + test "when specify to inherit and an :only option which does not match current action name and is opposite from parent controller" do + controller = WithOnlyConditionalFlippedAndInheriting.new + controller.process(:index) + assert_equal "Overwrite Hello index!", controller.response_body + end + test "when specify an :except option which match current action name" do controller = WithExceptConditional.new controller.process(:show) @@ -368,6 +504,42 @@ module AbstractControllerTests assert_equal "Overwrite Hello index!", controller.response_body end + test "when specify overwrite as an :only option which match current action name" do + controller = WithConditionalOverride.new + controller.process(:overwritten) + assert_equal "Overwrite Hello overwritten!", controller.response_body + end + + test "when specify overwrite as an :only option which does not match current action name" do + controller = WithConditionalOverride.new + controller.process(:non_overwritten) + assert_equal "Hello non overwritten!", controller.response_body + end + + test "when specify overwrite as an :only option which match current action name and is opposite from parent controller" do + controller = WithConditionalOverrideFlipped.new + controller.process(:overwritten) + assert_equal "Hello overwritten!", controller.response_body + end + + test "when specify overwrite as an :only option which does not match current action name and is opposite from parent controller" do + controller = WithConditionalOverrideFlipped.new + controller.process(:non_overwritten) + assert_equal "With Override Hello non overwritten!", controller.response_body + end + + test "when specify to inherit and overwrite as an :only option which match current action name and is opposite from parent controller" do + controller = WithConditionalOverrideFlippedAndInheriting.new + controller.process(:overwritten) + assert_equal "Hello overwritten!", controller.response_body + end + + test "when specify to inherit and overwrite as an :only option which does not match current action name and is opposite from parent controller" do + controller = WithConditionalOverrideFlippedAndInheriting.new + controller.process(:non_overwritten) + assert_equal "Overwrite Hello non overwritten!", controller.response_body + end + test "layout for anonymous controller" do klass = Class.new(WithString) do def index @@ -379,6 +551,17 @@ module AbstractControllerTests controller.process(:index) assert_equal "With String index", controller.response_body end + + test "when layout is disabled with #action_has_layout? returning false, render no layout" do + controller = WithString.new + controller.instance_eval do + def action_has_layout? + false + end + end + controller.process(:action_has_layout_false) + assert_equal "Hello string!", controller.response_body + end end end end diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb index bdb9e0397b..7b69ffc628 100644 --- a/actionview/test/actionpack/controller/render_test.rb +++ b/actionview/test/actionpack/controller/render_test.rb @@ -726,11 +726,6 @@ class RenderTest < ActionController::TestCase 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) - end - # :ported: def test_render_from_variable get :render_hello_world_from_variable diff --git a/actionview/test/activerecord/debug_helper_test.rb b/actionview/test/activerecord/debug_helper_test.rb index 03cb1d5a91..ed1c08e134 100644 --- a/actionview/test/activerecord/debug_helper_test.rb +++ b/actionview/test/activerecord/debug_helper_test.rb @@ -4,7 +4,10 @@ require 'nokogiri' class DebugHelperTest < ActionView::TestCase def test_debug company = Company.new(name: "firebase") - assert_match "name: firebase", debug(company) + output = debug(company) + assert_match "name: name", output + assert_match "value_before_type_cast: firebase", output + assert_match "active_record_yaml_version: 2", output end def test_debug_with_marshal_error diff --git a/actionview/test/fixtures/digestor/api/comments/_comment.json.erb b/actionview/test/fixtures/digestor/api/comments/_comment.json.erb new file mode 100644 index 0000000000..696eb13917 --- /dev/null +++ b/actionview/test/fixtures/digestor/api/comments/_comment.json.erb @@ -0,0 +1 @@ +{"content": "Great story!"} diff --git a/actionview/test/fixtures/digestor/api/comments/_comments.json.erb b/actionview/test/fixtures/digestor/api/comments/_comments.json.erb new file mode 100644 index 0000000000..c28646a283 --- /dev/null +++ b/actionview/test/fixtures/digestor/api/comments/_comments.json.erb @@ -0,0 +1 @@ +<%= render partial: "comments/comment", collection: commentable.comments %> 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/digestor/messages/thread.json.erb b/actionview/test/fixtures/digestor/messages/thread.json.erb new file mode 100644 index 0000000000..e4c1ba97cd --- /dev/null +++ b/actionview/test/fixtures/digestor/messages/thread.json.erb @@ -0,0 +1 @@ +<%= render "comments/comments" %> 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/_🍣.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/lib/controller/fake_models.rb b/actionview/test/lib/controller/fake_models.rb index a122fe17c9..65c68fc34a 100644 --- a/actionview/test/lib/controller/fake_models.rb +++ b/actionview/test/lib/controller/fake_models.rb @@ -31,27 +31,6 @@ end class GoodCustomer < Customer end -class TicketType < Struct.new(:name) - extend ActiveModel::Naming - include ActiveModel::Conversion - extend ActiveModel::Translation - - def initialize(*args) - super - @persisted = false - end - - def persisted=(boolean) - @persisted = boolean - end - - def persisted? - @persisted - end - - attr_accessor :name -end - class Post < Struct.new(:title, :author_name, :body, :secret, :persisted, :written_on, :cost) extend ActiveModel::Naming include ActiveModel::Conversion diff --git a/actionview/test/template/date_helper_test.rb b/actionview/test/template/date_helper_test.rb index 4678998bdc..e67d5d0e8c 100644 --- a/actionview/test/template/date_helper_test.rb +++ b/actionview/test/template/date_helper_test.rb @@ -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) @@ -408,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) @@ -487,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) @@ -559,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) @@ -647,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) @@ -711,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) @@ -1018,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) @@ -1034,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) @@ -1270,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) @@ -1486,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) @@ -2014,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) @@ -2220,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) @@ -2493,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) } diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb index dde757b5a2..410f562f07 100644 --- a/actionview/test/template/digestor_test.rb +++ b/actionview/test/template/digestor_test.rb @@ -13,34 +13,18 @@ class FixtureTemplate end end -class FixtureFinder +class FixtureFinder < ActionView::LookupContext FIXTURES_DIR = "#{File.dirname(__FILE__)}/../fixtures/digestor" - attr_reader :details, :view_paths - attr_accessor :formats - attr_accessor :variants - - def initialize - @details = {} - @view_paths = ActionView::PathSet.new(['digestor']) - @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") + def initialize(details = {}) + super(ActionView::PathSet.new(['digestor', 'digestor/api']), details, []) + @rendered_format = :html end +end - def disable_cache(&block) - yield +class ActionView::Digestor::Node + def flatten + [self] + children.flat_map(&:flatten) end end @@ -49,6 +33,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 @@ -56,7 +41,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 @@ -84,25 +68,21 @@ class TemplateDigestorTest < ActionView::TestCase end def test_explicit_dependency_wildcard_picks_up_added_file - old_caching, ActionView::Resolver.caching = ActionView::Resolver.caching, false - - assert_digest_difference("events/index") do - add_template("events/_uncompleted") + disable_resolver_caching do + assert_digest_difference("events/index") do + add_template("events/_uncompleted") + end end - ensure - remove_template("events/_uncompleted") - ActionView::Resolver.caching = old_caching end def test_explicit_dependency_wildcard_picks_up_removed_file - old_caching, ActionView::Resolver.caching = ActionView::Resolver.caching, false - add_template("events/_subscribers_changed") + disable_resolver_caching do + add_template("events/_subscribers_changed") - assert_digest_difference("events/index") do - remove_template("events/_subscribers_changed") + assert_digest_difference("events/index") do + remove_template("events/_subscribers_changed") + end end - ensure - ActionView::Resolver.caching = old_caching end def test_second_level_dependency @@ -153,12 +133,42 @@ class TemplateDigestorTest < ActionView::TestCase 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_nested_template_deps_with_non_default_rendered_format + finder.rendered_format = nil + nested_deps = [{"comments/comments"=>["comments/comment"]}] + assert_equal nested_deps, nested_dependencies("messages/thread") + end + + def test_template_formats_of_nested_deps_with_non_default_rendered_format + finder.rendered_format = nil + assert_equal [:json], tree_template_formats("messages/thread").uniq + end + + def test_template_formats_of_dependencies_with_same_logical_name_and_different_rendered_format + assert_equal [:html], tree_template_formats("messages/show").uniq + end + def test_recursion_in_renders assert digest("level/recursion") # assert recursion is possible assert_not_nil digest("level/recursion") # assert digest is stored @@ -206,13 +216,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") @@ -269,6 +280,13 @@ class TemplateDigestorTest < ActionView::TestCase assert_not_equal digest_phone, digest_fridge_phone end + def test_different_formats_with_same_logical_template_names_results_in_different_digests + html_digest = digest("comments/_comment", format: :html) + json_digest = digest("comments/_comment", format: :json) + + assert_not_equal html_digest, json_digest + end + def test_digest_cache_cleanup_with_recursion first_digest = digest("level/_recursion") second_digest = digest("level/_recursion") @@ -280,21 +298,17 @@ 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 - - first_digest = digest("level/_recursion") - second_digest = digest("level/_recursion") + disable_resolver_caching do + 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 - private def assert_logged(message) old_logger = ActionView::Base.logger @@ -313,29 +327,44 @@ class TemplateDigestorTest < ActionView::TestCase def assert_digest_difference(template_name, options = {}) previous_digest = digest(template_name, options) - ActionView::Digestor.cache.clear + 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_options = options.extract!(:variants, :format) - finder.formats = [:html] - finder.variants = options.delete(:variants) || [] + finder.variants = finder_options[:variants] || [] + finder.rendered_format = finder_options[:format] if finder_options[:format] - ActionView::Digestor.digest({ name: template_name, finder: finder }.merge(options)) + ActionView::Digestor.digest(name: template_name, finder: finder, dependencies: (options[:dependencies] || [])) end def dependencies(template_name) - ActionView::Digestor.new({ name: template_name, finder: finder }).dependencies + tree = ActionView::Digestor.tree(template_name, finder) + tree.children.map(&:name) end def nested_dependencies(template_name) - ActionView::Digestor.new({ name: template_name, finder: finder }).nested_dependencies + tree = ActionView::Digestor.tree(template_name, finder) + tree.children.map(&:to_dep_map) + end + + def tree_template_formats(template_name) + tree = ActionView::Digestor.tree(template_name, finder) + tree.flatten.map(&:template).compact.flat_map(&:formats) + end + + def disable_resolver_caching + old_caching, ActionView::Resolver.caching = ActionView::Resolver.caching, false + yield + ensure + ActionView::Resolver.caching = old_caching end def finder diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index 034b8a4bf6..310d0ce514 100644 --- a/actionview/test/template/form_helper_test.rb +++ b/actionview/test/template/form_helper_test.rb @@ -128,8 +128,6 @@ class FormHelperTest < ActionView::TestCase @post_delegator.title = 'Hello World' @car = Car.new("#000FFF") - - @ticket_type = TicketType.new end Routes = ActionDispatch::Routing::RouteSet.new @@ -138,8 +136,6 @@ class FormHelperTest < ActionView::TestCase resources :comments end - resources :ticket_types - namespace :admin do resources :posts do resources :comments @@ -1143,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 @@ -1158,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 @@ -1179,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 @@ -1187,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 @@ -1195,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 @@ -1876,20 +1888,6 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end - def test_lowercase_model_name_default_submit_button_value - form_for(@ticket_type) do |f| - concat f.submit - end - - expected = - '<form class="new_ticket_type" id="new_ticket_type" action="/ticket_types" accept-charset="UTF-8" method="post">' + - hidden_fields + - '<input type="submit" name="commit" value="Create ticket type" data-disable-with="Create ticket type" />' + - '</form>' - - assert_dom_equal expected, output_buffer - end - def test_form_for_with_symbol_object_name form_for(@post, as: "other_name", html: { id: "create-post" }) do |f| concat f.label(:title, class: 'post_title') @@ -2257,7 +2255,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form('/posts', 'new_post', 'new_post') do - "<input name='commit' data-disable-with='Create post' 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 @@ -2272,7 +2270,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do - "<input name='commit' data-disable-with='Confirm post changes' 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 @@ -2300,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' data-disable-with='Update your post' type='submit' value='Update your post' />" + "<input name='commit' data-disable-with='Update your Post' type='submit' value='Update your Post' />" end assert_dom_equal expected, output_buffer diff --git a/actionview/test/template/form_options_helper_test.rb b/actionview/test/template/form_options_helper_test.rb index c5b63d33f1..7a5904f151 100644 --- a/actionview/test/template/form_options_helper_test.rb +++ b/actionview/test/template/form_options_helper_test.rb @@ -798,7 +798,7 @@ class FormOptionsHelperTest < ActionView::TestCase ) end - def test_select_with_fixnum + def test_select_with_integer @post = Post.new @post.category = "" assert_dom_equal( diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb index 07b3fba754..5b0b708618 100644 --- a/actionview/test/template/form_tag_helper_test.rb +++ b/actionview/test/template/form_tag_helper_test.rb @@ -239,8 +239,8 @@ class FormTagHelperTest < ActionView::TestCase end def test_select_tag_with_include_blank - 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>) + 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="" label=" "></option><option>Home</option><option>Work</option><option>Pub</option></select>) assert_dom_equal expected, actual end @@ -269,14 +269,14 @@ class FormTagHelperTest < ActionView::TestCase end def test_select_tag_with_prompt_and_include_blank - 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>) + 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="" label=" "></option><option>Home</option><option>Work</option><option>Pub</option></select>) assert_dom_equal expected, actual end def test_select_tag_with_nil_option_tags_and_include_blank actual = select_tag "places", nil, :include_blank => true - expected = %(<select id="places" name="places"><option value=""></option></select>) + expected = %(<select id="places" name="places"><option value="" label=" "></option></select>) assert_dom_equal expected, actual end @@ -622,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 4776c18b0b..7683444bf0 100644 --- a/actionview/test/template/log_subscriber_test.rb +++ b/actionview/test/template/log_subscriber_test.rb @@ -35,7 +35,8 @@ class AVLogSubscriberTest < ActiveSupport::TestCase @view.render(:file => "test/hello_world") wait - assert_equal 1, @logger.logged(:info).size + 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 @@ -45,7 +46,8 @@ class AVLogSubscriberTest < ActiveSupport::TestCase @view.render(:text => "TEXT") wait - assert_equal 1, @logger.logged(:info).size + 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 @@ -55,7 +57,8 @@ class AVLogSubscriberTest < ActiveSupport::TestCase @view.render(:inline => "<%= 'TEXT' %>") wait - assert_equal 1, @logger.logged(:info).size + 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 @@ -86,7 +89,7 @@ class AVLogSubscriberTest < ActiveSupport::TestCase wait assert_equal 1, @logger.logged(:info).size - assert_match(/Rendered test\/_customer.erb/, @logger.logged(:info).last) + assert_match(/Rendered collection of test\/_customer.erb \[2 times\]/, @logger.logged(:info).last) end end @@ -96,7 +99,7 @@ class AVLogSubscriberTest < ActiveSupport::TestCase wait assert_equal 1, @logger.logged(:info).size - assert_match(/Rendered customers\/_customer\.html\.erb/, @logger.logged(:info).last) + assert_match(/Rendered collection of customers\/_customer\.html\.erb \[2 times\]/, @logger.logged(:info).last) end end @@ -106,7 +109,21 @@ class AVLogSubscriberTest < ActiveSupport::TestCase wait assert_equal 1, @logger.logged(:info).size - assert_match(/Rendered collection/, @logger.logged(:info).last) + 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 of customers\/_customer\.html\.erb \[0 \/ 2 cache hits\]/, @logger.logged(:info).last) end end end diff --git a/actionview/test/template/output_safety_helper_test.rb b/actionview/test/template/output_safety_helper_test.rb index 8de0ae2f6f..b940c9dd36 100644 --- a/actionview/test/template/output_safety_helper_test.rb +++ b/actionview/test/template/output_safety_helper_test.rb @@ -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/render_test.rb b/actionview/test/template/render_test.rb index 333e0cca11..ad93236d32 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -207,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; " + @@ -226,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 @@ -252,7 +256,7 @@ 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 @@ -270,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') @@ -628,56 +637,59 @@ class LazyViewRenderTest < ActiveSupport::TestCase end end -class CachedCollectionViewRenderTest < CachedViewRenderTest +class CachedCollectionViewRenderTest < ActiveSupport::TestCase class CachedCustomer < Customer; end - teardown do - ActionView::PartialRenderer.collection_cache.clear - end + include RenderTestCases - test "with custom key" do - customer = Customer.new("david") - key = cache_key([customer, 'key'], "test/_customer") + # 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.write(key, 'Hello') + ActionView::PartialRenderer.collection_cache = ActiveSupport::Cache::MemoryStore.new - assert_equal "Hello", - @view.render(partial: "test/customer", collection: [customer], cache: ->(item) { [item, 'key'] }) + setup_view(view_paths) end - test "with caching with custom key and rendering with different key" do - customer = Customer.new("david") - key = cache_key([customer, 'key'], "test/_customer") + teardown do + GC.start + I18n.reload! + end - ActionView::PartialRenderer.collection_cache.write(key, 'Hello') + test "collection caching does not cache by default" do + customer = Customer.new("david", 1) + key = cache_key(customer, "test/_customer") - assert_equal "Hello: david", - @view.render(partial: "test/customer", collection: [customer], cache: ->(item) { [item, 'another_key'] }) + ActionView::PartialRenderer.collection_cache.write(key, 'Cached') + + 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 = cache_key(customer, "test/_cached_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 = cache_key(customer, "test/_cached_customer_as") + 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) + def cache_key(*names, virtual_path) digest = ActionView::Digestor.digest name: virtual_path, finder: @view.lookup_context, dependencies: [] - @view.fragment_cache_key([ *Array(names), digest ]) + @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 6f7a78ccef..f3956a31f6 100644 --- a/actionview/test/template/tag_helper_test.rb +++ b/actionview/test/template/tag_helper_test.rb @@ -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_test.rb b/actionview/test/template/template_test.rb index 921011b073..533c1c3219 100644 --- a/actionview/test/template/template_test.rb +++ b/actionview/test/template/template_test.rb @@ -192,38 +192,6 @@ class TestERBTemplate < ActiveSupport::TestCase assert_match(/\xFC/, e.message) end - def test_not_eligible_for_collection_caching_without_cache_call - [ - "<%= 'Hello' %>", - "<% cache_customer = 42 %>", - "<% cache customer.name do %><% end %>", - "<% my_cache customer do %><% end %>" - ].each do |body| - template = new_template(body, virtual_path: "test/foo/_customer") - assert_not template.eligible_for_collection_caching?, "Template #{body.inspect} should not be eligible for collection caching" - end - end - - def test_eligible_for_collection_caching_with_cache_call_or_explicit - [ - "<% cache customer do %><% end %>", - "<% cache(customer) do %><% end %>", - "<% cache( customer) do %><% end %>", - "<% cache( customer ) do %><% end %>", - "<%cache customer do %><% end %>", - "<% cache customer do %><% end %>", - " <% cache customer do %>\n<% end %>\n", - "<%# comment %><% cache customer do %><% end %>", - "<%# comment %>\n<% cache customer do %><% end %>", - "<%# comment\n line 2\n line 3 %>\n<% cache customer do %><% end %>", - "<%# comment 1 %>\n<%# comment 2 %>\n<% cache customer do %><% end %>", - "<%# comment 1 %>\n<%# Template Collection: customer %>\n<% my_cache customer do %><% end %>" - ].each do |body| - template = new_template(body, virtual_path: "test/foo/_customer") - assert template.eligible_for_collection_caching?, "Template #{body.inspect} should be eligible for collection caching" - end - end - def with_external_encoding(encoding) old = Encoding.default_external Encoding::Converter.new old, encoding if old != encoding diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index 3010656166..ab56d80de3 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -71,6 +71,34 @@ class UrlHelperTest < ActiveSupport::TestCase 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 @@ -189,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 @@ -613,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), diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md index 8687af5eba..5e72c67aea 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -1,134 +1,2 @@ -## Rails 5.0.0.beta1 (December 18, 2015) ## -* Fixed serializing `:at` option for `assert_enqueued_with` - and `assert_performed_with`. - - *Wojciech Wnętrzak* - -* Support passing array to `assert_enqueued_jobs` in `:only` option. - - *Wojciech Wnętrzak* - -* Add job priorities to Active Job. - - *wvengen* - -* Implement a simple `AsyncJob` processor and associated `AsyncAdapter` that - queue jobs to a `concurrent-ruby` thread pool. - - *Jerry D'Antonio* - -* Implement `provider_job_id` for `queue_classic` adapter. This requires the - latest, currently unreleased, version of queue_classic. - - *Yves Senn* - -* `assert_enqueued_with` and `assert_performed_with` now returns the matched - job instance for further assertions. - - *Jean Boussier* - -* Include `I18n.locale` into job serialization/deserialization and use it around - `perform`. - - Fixes #20799. - - *Johannes Opper* - -* Allow `DelayedJob`, `Sidekiq`, `qu`, and `que` to report the job id back to - `ActiveJob::Base` as `provider_job_id`. - - Fixes #18821. - - *Kevin Deisz*, *Jeroen van Baarsen* - -* `assert_enqueued_jobs` and `assert_performed_jobs` in block form use the - given number as expected value. This makes the error message much easier to - understand. - - *y-yagi* - -* A generated job now inherits from `app/jobs/application_job.rb` by default. - - *Jeroen van Baarsen* - -* Add an `:only` option to `perform_enqueued_jobs` to filter jobs based on - type. - - This allows specific jobs to be tested, while preventing others from - being performed unnecessarily. - - 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/README.md b/activejob/README.md index 7268186c00..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). diff --git a/activejob/Rakefile b/activejob/Rakefile index d9648a7f16..e47f17b740 100644 --- a/activejob/Rakefile +++ b/activejob/Rakefile @@ -1,11 +1,14 @@ require 'rake/testtask' -ACTIVEJOB_ADAPTERS = %w(async 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 + namespace :test do desc 'Run all adapter tests' task :default do diff --git a/activejob/activejob.gemspec b/activejob/activejob.gemspec index bc1671b508..e97bb40abf 100644 --- a/activejob/activejob.gemspec +++ b/activejob/activejob.gemspec @@ -13,7 +13,7 @@ Gem::Specification.new do |s| 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' diff --git a/activejob/lib/active_job.rb b/activejob/lib/active_job.rb index f7e05f7cd3..4b9065cf50 100644 --- a/activejob/lib/active_job.rb +++ b/activejob/lib/active_job.rb @@ -32,7 +32,6 @@ module ActiveJob autoload :Base autoload :QueueAdapters autoload :ConfiguredJob - autoload :AsyncJob autoload :TestCase autoload :TestHelper end diff --git a/activejob/lib/active_job/arguments.rb b/activejob/lib/active_job/arguments.rb index 33bd5b4eb3..a5c749e5e7 100644 --- a/activejob/lib/active_job/arguments.rb +++ b/activejob/lib/active_job/arguments.rb @@ -24,7 +24,7 @@ module ActiveJob end # Raised when an unsupported argument type is set as a job argument. We - # currently support NilClass, Fixnum, Float, String, TrueClass, FalseClass, + # currently support NilClass, Integer, Fixnum, Float, String, TrueClass, FalseClass, # 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 @@ -34,7 +34,8 @@ module ActiveJob module Arguments extend self # :nodoc: - TYPE_WHITELIST = [ NilClass, Fixnum, Float, String, TrueClass, FalseClass, Bignum, BigDecimal ] + # Calls #uniq since Integer, Fixnum, and Bignum are all the same class on Ruby 2.4+ + TYPE_WHITELIST = [ NilClass, String, Integer, Fixnum, Bignum, Float, BigDecimal, TrueClass, FalseClass ].uniq # Serializes a set of arguments. Whitelisted types are returned # as-is. Arrays/Hashes are serialized element by element. diff --git a/activejob/lib/active_job/async_job.rb b/activejob/lib/active_job/async_job.rb deleted file mode 100644 index ed7a6e8d9b..0000000000 --- a/activejob/lib/active_job/async_job.rb +++ /dev/null @@ -1,77 +0,0 @@ -require 'concurrent/map' -require 'concurrent/scheduled_task' -require 'concurrent/executor/thread_pool_executor' -require 'concurrent/utility/processor_counter' - -module ActiveJob - # == Active Job Async Job - # - # When enqueueing jobs with Async Job each job will be executed asynchronously - # on a +concurrent-ruby+ thread pool. All job data is retained in memory. - # Because job data is not saved to a persistent datastore there is no - # additional infrastructure needed and jobs process quickly. The lack of - # persistence, however, means that all unprocessed jobs will be lost on - # application restart. Therefore in-memory queue adapters are unsuitable for - # most production environments but are excellent for development and testing. - # - # Read more about Concurrent Ruby {here}[https://github.com/ruby-concurrency/concurrent-ruby]. - # - # To use Async Job set the queue_adapter config to +:async+. - # - # Rails.application.config.active_job.queue_adapter = :async - # - # Async Job supports job queues specified with +queue_as+. Queues are created - # automatically as needed and each has its own thread pool. - class AsyncJob - - DEFAULT_EXECUTOR_OPTIONS = { - min_threads: [2, Concurrent.processor_count].max, - max_threads: Concurrent.processor_count * 10, - auto_terminate: true, - idletime: 60, # 1 minute - max_queue: 0, # unlimited - fallback_policy: :caller_runs # shouldn't matter -- 0 max queue - }.freeze - - QUEUES = Concurrent::Map.new do |hash, queue_name| #:nodoc: - hash.compute_if_absent(queue_name) { ActiveJob::AsyncJob.create_thread_pool } - end - - class << self - # Forces jobs to process immediately when testing the Active Job gem. - # This should only be called from within unit tests. - def perform_immediately! #:nodoc: - @perform_immediately = true - end - - # Allows jobs to run asynchronously when testing the Active Job gem. - # This should only be called from within unit tests. - def perform_asynchronously! #:nodoc: - @perform_immediately = false - end - - def create_thread_pool #:nodoc: - if @perform_immediately - Concurrent::ImmediateExecutor.new - else - Concurrent::ThreadPoolExecutor.new(DEFAULT_EXECUTOR_OPTIONS) - end - end - - def enqueue(job_data, queue: 'default') #:nodoc: - QUEUES[queue].post(job_data) { |job| ActiveJob::Base.execute(job) } - end - - def enqueue_at(job_data, timestamp, queue: 'default') #:nodoc: - delay = timestamp - Time.current.to_f - if delay > 0 - Concurrent::ScheduledTask.execute(delay, args: [job_data], executor: QUEUES[queue]) do |job| - ActiveJob::Base.execute(job) - end - else - enqueue(job_data, queue: queue) - end - end - end - end -end diff --git a/activejob/lib/active_job/callbacks.rb b/activejob/lib/active_job/callbacks.rb index 2b6149e84e..b206522a60 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 @@ -121,8 +126,8 @@ module ActiveJob set_callback(:enqueue, :after, *filters, &blk) end - # Defines a callback that will get called before and after the - # job is enqueued. + # Defines a callback that will get called around the enqueueing + # of the job. # # class VideoProcessJob < ActiveJob::Base # queue_as :default diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb index 19b900a285..f7f882c998 100644 --- a/activejob/lib/active_job/core.rb +++ b/activejob/lib/active_job/core.rb @@ -79,7 +79,7 @@ module ActiveJob 'queue_name' => queue_name, 'priority' => priority, 'arguments' => serialize_arguments(arguments), - 'locale' => I18n.locale + 'locale' => I18n.locale.to_s } end @@ -108,7 +108,7 @@ module ActiveJob 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 + 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 22154457fd..9dc3c0fa57 100644 --- a/activejob/lib/active_job/enqueuing.rb +++ b/activejob/lib/active_job/enqueuing.rb @@ -36,7 +36,7 @@ module ActiveJob # # ==== Examples # - # class SiteScrapperJob < ActiveJob::Base + # class SiteScraperJob < ActiveJob::Base # rescue_from(ErrorLoadingSite) do # retry_job queue: :low_priority # end 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 0bdeca76a8..0d50c27938 100644 --- a/activejob/lib/active_job/gem_version.rb +++ b/activejob/lib/active_job/gem_version.rb @@ -6,9 +6,9 @@ module ActiveJob module VERSION MAJOR = 5 - MINOR = 0 + MINOR = 1 TINY = 0 - PRE = "beta1.1" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activejob/lib/active_job/queue_adapter.rb b/activejob/lib/active_job/queue_adapter.rb index 457015b741..72e4ebf935 100644 --- a/activejob/lib/active_job/queue_adapter.rb +++ b/activejob/lib/active_job/queue_adapter.rb @@ -4,25 +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 +:inline+ queue. See QueueAdapters for more information. + # 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 2c5039ef4d..71154d8785 100644 --- a/activejob/lib/active_job/queue_adapters.rb +++ b/activejob/lib/active_job/queue_adapters.rb @@ -33,7 +33,8 @@ module ActiveJob # # ==== Async # - # Yes: The Queue Adapter runs the jobs in a separate or forked process. + # 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. # diff --git a/activejob/lib/active_job/queue_adapters/async_adapter.rb b/activejob/lib/active_job/queue_adapters/async_adapter.rb index 3fc27f56e7..922bc4afce 100644 --- a/activejob/lib/active_job/queue_adapters/async_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/async_adapter.rb @@ -1,22 +1,113 @@ -require 'active_job/async_job' +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 # - # When enqueueing jobs with the Async adapter the job will be executed - # asynchronously using {AsyncJob}[http://api.rubyonrails.org/classes/ActiveJob/AsyncJob.html]. + # The Async adapter runs jobs with an in-process thread pool. # - # To use +AsyncJob+ set the queue_adapter config to +:async+. + # 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. # - # Rails.application.config.active_job.queue_adapter = :async + # 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: - ActiveJob::AsyncJob.enqueue(job.serialize, queue: job.queue_name) + @scheduler.enqueue JobWrapper.new(job), queue_name: job.queue_name end def enqueue_at(job, timestamp) #:nodoc: - ActiveJob::AsyncJob.enqueue_at(job.serialize, timestamp, queue: job.queue_name) + @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 diff --git a/activejob/lib/active_job/queue_adapters/inline_adapter.rb b/activejob/lib/active_job/queue_adapters/inline_adapter.rb index 8ad5f4de07..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+. 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 311109e958..163c8eb212 100644 --- a/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb @@ -5,7 +5,7 @@ module ActiveJob # == Sucker Punch adapter for Active Job # # Sucker Punch is a single-process Ruby asynchronous processing library. - # This reduces the cost of of hosting on a service like Heroku along + # This reduces the cost 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. 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 44ddfa5f69..e16af1947f 100644 --- a/activejob/lib/active_job/test_helper.rb +++ b/activejob/lib/active_job/test_helper.rb @@ -4,325 +4,321 @@ 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 # :nodoc: - test_adapter = ActiveJob::QueueAdapters::TestAdapter.new + def before_setup # :nodoc: + test_adapter = queue_adapter_for_test - @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 # :nodoc: - 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 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 + 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 - end + # Specifies the queue adapter to use with all active job test helpers. + # + # Returns an instance of the queue adapter and defaults to + # <tt>ActiveJob::QueueAdapters::TestAdapter</tt>. + # + # Note: The adapter provided by this method must provide some additional + # methods from those expected of a standard <tt>ActiveJob::QueueAdapter</tt> + # in order to be used with the active job test helpers. Refer to + # <tt>ActiveJob::QueueAdapters::TestAdapter</tt>. + def queue_adapter_for_test + ActiveJob::QueueAdapters::TestAdapter.new + 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 + # 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 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 - 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 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) + # 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 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 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 - 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 + # 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 - 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 + # 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 - def queue_adapter - ActiveJob::Base.queue_adapter + def perform_enqueued_jobs(only: nil) + old_perform_enqueued_jobs = queue_adapter.perform_enqueued_jobs + old_perform_enqueued_at_jobs = queue_adapter.perform_enqueued_at_jobs + old_filter = queue_adapter.filter + + begin + queue_adapter.perform_enqueued_jobs = true + queue_adapter.perform_enqueued_at_jobs = true + queue_adapter.filter = only + yield + ensure + queue_adapter.perform_enqueued_jobs = old_perform_enqueued_jobs + queue_adapter.perform_enqueued_at_jobs = old_perform_enqueued_at_jobs + queue_adapter.filter = old_filter end + end - delegate :enqueued_jobs, :enqueued_jobs=, - :performed_jobs, :performed_jobs=, - to: :queue_adapter + def queue_adapter + ActiveJob::Base.queue_adapter + end - private - def clear_enqueued_jobs # :nodoc: - enqueued_jobs.clear - end + private + def clear_enqueued_jobs # :nodoc: + enqueued_jobs.clear + end - def clear_performed_jobs # :nodoc: - performed_jobs.clear - end + def clear_performed_jobs # :nodoc: + performed_jobs.clear + end - def enqueued_jobs_size(only: nil) # :nodoc: - if only - enqueued_jobs.count { |job| Array(only).include?(job.fetch(:job)) } - else - enqueued_jobs.count - end + 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 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 + 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 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 + 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/rails/generators/job/job_generator.rb b/activejob/lib/rails/generators/job/job_generator.rb index 2115fb9f71..6e43e4a269 100644 --- a/activejob/lib/rails/generators/job/job_generator.rb +++ b/activejob/lib/rails/generators/job/job_generator.rb @@ -17,7 +17,22 @@ module Rails # :nodoc: 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/test/adapters/async.rb b/activejob/test/adapters/async.rb index 5fcfb89566..08eb9658cd 100644 --- a/activejob/test/adapters/async.rb +++ b/activejob/test/adapters/async.rb @@ -1,4 +1,2 @@ -require 'active_job/async_job' - ActiveJob::Base.queue_adapter = :async -ActiveJob::AsyncJob.perform_immediately! +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 eb8ad185aa..59dc3d7f78 100644 --- a/activejob/test/cases/argument_serialization_test.rb +++ b/activejob/test/cases/argument_serialization_test.rb @@ -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 diff --git a/activejob/test/cases/async_job_test.rb b/activejob/test/cases/async_job_test.rb deleted file mode 100644 index 2642cfc608..0000000000 --- a/activejob/test/cases/async_job_test.rb +++ /dev/null @@ -1,42 +0,0 @@ -require 'helper' -require 'jobs/hello_job' -require 'jobs/queue_as_job' - -class AsyncJobTest < ActiveSupport::TestCase - def using_async_adapter? - ActiveJob::Base.queue_adapter.is_a? ActiveJob::QueueAdapters::AsyncAdapter - end - - setup do - ActiveJob::AsyncJob.perform_asynchronously! - end - - teardown do - ActiveJob::AsyncJob::QUEUES.clear - ActiveJob::AsyncJob.perform_immediately! - end - - test "#create_thread_pool returns a thread_pool" do - thread_pool = ActiveJob::AsyncJob.create_thread_pool - assert thread_pool.is_a? Concurrent::ExecutorService - assert_not thread_pool.is_a? Concurrent::ImmediateExecutor - end - - test "#create_thread_pool returns an ImmediateExecutor after #perform_immediately! is called" do - ActiveJob::AsyncJob.perform_immediately! - thread_pool = ActiveJob::AsyncJob.create_thread_pool - assert thread_pool.is_a? Concurrent::ImmediateExecutor - end - - test "enqueuing without specifying a queue uses the default queue" do - skip unless using_async_adapter? - HelloJob.perform_later - assert ActiveJob::AsyncJob::QUEUES.key? 'default' - end - - test "enqueuing to a queue that does not exist creates the queue" do - skip unless using_async_adapter? - QueueAsJob.perform_later - assert ActiveJob::AsyncJob::QUEUES.key? QueueAsJob::MY_QUEUE.to_s - end -end diff --git a/activejob/test/cases/job_serialization_test.rb b/activejob/test/cases/job_serialization_test.rb index 229517774e..fa94209889 100644 --- a/activejob/test/cases/job_serialization_test.rb +++ b/activejob/test/cases/job_serialization_test.rb @@ -2,6 +2,7 @@ require 'helper' require 'jobs/gid_job' require 'jobs/hello_job' require 'models/person' +require 'json' class JobSerializationTest < ActiveSupport::TestCase setup do @@ -15,18 +16,32 @@ class JobSerializationTest < ActiveSupport::TestCase end test 'serialize includes current locale' do - assert_equal :en, HelloJob.new.serialize['locale'] + 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 + 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 + assert_equal 'en', job.locale end end diff --git a/activejob/test/cases/test_helper_test.rb b/activejob/test/cases/test_helper_test.rb index f7ee763e8a..3d863f5e65 100644 --- a/activejob/test/cases/test_helper_test.rb +++ b/activejob/test/cases/test_helper_test.rb @@ -509,3 +509,15 @@ class PerformedJobsTest < ActiveJob::TestCase assert_equal 2, ActiveJob::Base.queue_adapter.performed_jobs.count end end + +class OverrideQueueAdapterTest < ActiveJob::TestCase + class CustomQueueAdapter < ActiveJob::QueueAdapters::TestAdapter; end + + def queue_adapter_for_test + CustomQueueAdapter.new + end + + def test_assert_job_has_custom_queue_adapter_set + assert_instance_of CustomQueueAdapter, HelloJob.queue_adapter + end +end diff --git a/activejob/test/helper.rb b/activejob/test/helper.rb index 7e86415f48..54b6076f09 100644 --- a/activejob/test/helper.rb +++ b/activejob/test/helper.rb @@ -1,5 +1,3 @@ -require File.expand_path('../../../load_paths', __FILE__) - require 'active_job' require 'support/job_buffer' diff --git a/activejob/test/integration/queuing_test.rb b/activejob/test/integration/queuing_test.rb index d8425c9706..40f27500a5 100644 --- a/activejob/test/integration/queuing_test.rb +++ b/activejob/test/integration/queuing_test.rb @@ -57,13 +57,13 @@ class QueuingTest < ActiveSupport::TestCase end test 'should supply a provider_job_id when available for immediate jobs' do - skip unless adapter_is?(:delayed_job, :sidekiq, :qu, :que, :queue_classic) + 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?(:delayed_job, :sidekiq, :que, :queue_classic) + 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 diff --git a/activejob/test/support/integration/adapters/async.rb b/activejob/test/support/integration/adapters/async.rb index 42beb12b1f..44ab98437a 100644 --- a/activejob/test/support/integration/adapters/async.rb +++ b/activejob/test/support/integration/adapters/async.rb @@ -1,9 +1,10 @@ module AsyncJobsManager def setup ActiveJob::Base.queue_adapter = :async + ActiveJob::Base.queue_adapter.immediate = false end def clear_jobs - ActiveJob::AsyncJob::QUEUES.clear + ActiveJob::Base.queue_adapter.shutdown end end diff --git a/activejob/test/support/integration/dummy_app_template.rb b/activejob/test/support/integration/dummy_app_template.rb index 262ca72327..a0ef38b0b2 100644 --- a/activejob/test/support/integration/dummy_app_template.rb +++ b/activejob/test/support/integration/dummy_app_template.rb @@ -2,7 +2,7 @@ if ENV['AJ_ADAPTER'] == 'delayed_job' generate "delayed_job:active_record", "--quiet" end -rake("db:migrate") +rails_command("db:migrate") initializer 'activejob.rb', <<-CODE require "#{File.expand_path("../jobs_manager.rb", __FILE__)}" diff --git a/activejob/test/support/integration/test_case_helpers.rb b/activejob/test/support/integration/test_case_helpers.rb index 9897f76fd0..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 @@ -28,7 +29,8 @@ module TestCaseHelpers end def adapter_is?(*adapter_class_symbols) - adapter_class_symbols.map(&:to_s).include?(ActiveJob::Base.queue_adapter.class.name.split("::").last.gsub(/Adapter$/, '').underscore) + 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) diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index e4769d2f40..206699c036 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,132 +1,2 @@ -## Rails 5.0.0.beta1 (December 18, 2015) ## -* Validate multiple contexts on `valid?` and `invalid?` at once. - - Example: - - class Person - include ActiveModel::Validations - - attr_reader :name, :title - validates_presence_of :name, on: :create - validates_presence_of :title, on: :update - end - - person = Person.new - person.valid?([:create, :update]) # => false - person.errors.messages # => {:name=>["can't be blank"], :title=>["can't be blank"]} - - *Dmitry Polushkin* - -* Add case_sensitive option for confirmation validator in models. - - *Akshat Sharma* - -* Ensure `method_missing` is called for methods passed to - `ActiveModel::Serialization#serializable_hash` that don't exist. - - *Jay Elaraj* - -* Remove `ActiveModel::Serializers::Xml` from core. - - *Zachary Scott* - -* Add `ActiveModel::Dirty#[attr_name]_previously_changed?` and - `ActiveModel::Dirty#[attr_name]_previous_change` to improve access - to recorded changes after the model has been saved. - - It makes the dirty-attributes query methods consistent before and after - saving. - - *Fernando Tapia Rico* - -* Deprecate the `:tokenizer` option for `validates_length_of`, in favor of - plain Ruby. - - *Sean Griffin* - -* Deprecate `ActiveModel::Errors#add_on_empty` and `ActiveModel::Errors#add_on_blank` - with no replacement. - - *Wojciech Wnętrzak* - -* Deprecate `ActiveModel::Errors#get`, `ActiveModel::Errors#set` and - `ActiveModel::Errors#[]=` methods that have inconsistent behavior. - - *Wojciech Wnętrzak* - -* Allow symbol as values for `tokenize` of `LengthValidator`. - - *Kensuke Naito* - -* Assigning an unknown attribute key to an `ActiveModel` instance during initialization - will now raise `ActiveModel::AttributeAssignment::UnknownAttributeError` instead of - `NoMethodError`. - - Example: - - User.new(foo: 'some value') - # => ActiveModel::AttributeAssignment::UnknownAttributeError: unknown attribute 'foo' for User. - - *Eugene Gilburg* - -* Extracted `ActiveRecord::AttributeAssignment` to `ActiveModel::AttributeAssignment` - allowing to use it for any object as an includable module. - - Example: - - class Cat - include ActiveModel::AttributeAssignment - attr_accessor :name, :status - end - - cat = Cat.new - cat.assign_attributes(name: "Gorby", status: "yawning") - cat.name # => 'Gorby' - cat.status # => 'yawning' - cat.assign_attributes(status: "sleeping") - cat.name # => 'Gorby' - cat.status # => 'sleeping' - - *Bogdan Gusiev* - -* Add `ActiveModel::Errors#details` - - To be able to return type of used validator, one can now call `details` - on errors instance. - - Example: - - class User < ActiveRecord::Base - validates :name, presence: true - end - - user = User.new; user.valid?; user.errors.details - => {name: [{error: :blank}]} - - *Wojciech Wnętrzak* - -* Change validates_acceptance_of to accept true by default. - - The default for validates_acceptance_of is now "1" and true. - In the past, only "1" was the default and you were required to add - accept: true. - -* Remove deprecated `ActiveModel::Dirty#reset_#{attribute}` and - `ActiveModel::Dirty#reset_changes`. - - *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 Active Model `before_` callback had - the side effect of halting the callback chain. - This is not recommended anymore and, depending on the value of the - `ActiveSupport.halt_callback_chains_on_return_false` option, will - either not work at all or display a deprecation warning. - - *claudiob* - -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/Rakefile b/activemodel/Rakefile index 5a67f0a151..036815a987 100644 --- a/activemodel/Rakefile +++ b/activemodel/Rakefile @@ -4,6 +4,8 @@ dir = File.dirname(__FILE__) task :default => :test +task :package + Rake::TestTask.new do |t| t.libs << "test" t.test_files = Dir.glob("#{dir}/test/cases/**/*_test.rb") diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index 53206580f0..1c3997b864 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -13,7 +13,7 @@ Gem::Specification.new do |s| 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' diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index b95c174d6e..7de259a60d 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -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 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 6e2e5afd1b..90047c3c12 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -119,6 +119,9 @@ 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' @@ -174,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+. @@ -187,7 +189,7 @@ module ActiveModel end # Handles <tt>*_previously_changed?</tt> for +method_missing+. - def attribute_previously_changed?(attr, options = {}) #:nodoc: + def attribute_previously_changed?(attr) #:nodoc: previous_changes_include?(attr) end diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index ef6141a51d..6f2c8c1c53 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -71,8 +71,8 @@ module ActiveModel # end def initialize(base) @base = base - @messages = Hash.new { |messages, attribute| messages[attribute] = [] } - @details = Hash.new { |details, attribute| details[attribute] = [] } + @messages = apply_default_array({}) + @details = apply_default_array({}) end def initialize_dup(other) # :nodoc: @@ -110,7 +110,7 @@ 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 alias :has_key? :include? alias :key? :include? @@ -160,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 @@ -301,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 # => {} # @@ -318,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 @@ -337,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: @@ -359,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: @@ -477,12 +486,23 @@ module ActiveModel 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) end + def marshal_dump + [@base, without_default_proc(@messages), without_default_proc(@details)] + end + + def marshal_load(array) + @base, @messages, @details = array + apply_default_array(@messages) + apply_default_array(@details) + end + private def normalize_message(attribute, message, options) case message @@ -493,9 +513,20 @@ 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 + + def without_default_proc(hash) + hash.dup.tap do |new_h| + new_h.default_proc = nil + end + end + + def apply_default_array(hash) + hash.default_proc = proc { |h, key| h[key] = [] } + hash + end end # Raised when a validation cannot be corrected by end users and are considered @@ -516,7 +547,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/gem_version.rb b/activemodel/lib/active_model/gem_version.rb index 4563f860c3..4a8ee915cf 100644 --- a/activemodel/lib/active_model/gem_version.rb +++ b/activemodel/lib/active_model/gem_version.rb @@ -6,9 +6,9 @@ module ActiveModel module VERSION MAJOR = 5 - MINOR = 0 + MINOR = 1 TINY = 0 - PRE = "beta1.1" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activemodel/lib/active_model/type.rb b/activemodel/lib/active_model/type.rb index bec851594f..6ec3452478 100644 --- a/activemodel/lib/active_model/type.rb +++ b/activemodel/lib/active_model/type.rb @@ -47,7 +47,7 @@ module ActiveModel register(:binary, Type::Binary) register(:boolean, Type::Boolean) register(:date, Type::Date) - register(:date_time, Type::DateTime) + register(:datetime, Type::DateTime) register(:decimal, Type::Decimal) register(:float, Type::Float) register(:immutable_string, Type::ImmutableString) diff --git a/activemodel/lib/active_model/type/decimal.rb b/activemodel/lib/active_model/type/decimal.rb index d19d8baada..11ea327026 100644 --- a/activemodel/lib/active_model/type/decimal.rb +++ b/activemodel/lib/active_model/type/decimal.rb @@ -29,12 +29,12 @@ module ActiveModel end end - scale ? casted_value.round(scale) : casted_value + 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 @@ -47,6 +47,14 @@ module ActiveModel precision.to_i end end + + def apply_scale(value) + if scale + value.round(scale) + else + value + end + end end end end diff --git a/activemodel/lib/active_model/type/integer.rb b/activemodel/lib/active_model/type/integer.rb index 2f73ede009..eea2280b22 100644 --- a/activemodel/lib/active_model/type/integer.rb +++ b/activemodel/lib/active_model/type/integer.rb @@ -46,7 +46,7 @@ module ActiveModel def ensure_in_range(value) unless range.cover?(value) - raise RangeError, "#{value} is out of range for #{self.class} with limit #{_limit}" + raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit}" end end diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 8159b9b1d3..a10d2285a2 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -304,8 +304,6 @@ module ActiveModel # Runs all the specified validations and returns +true+ if no errors were # added otherwise +false+. # - # Aliased as validate. - # # class Person # include ActiveModel::Validations # diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index c5c0cd4636..a04e5f347e 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -64,11 +64,7 @@ module ActiveModel private def convert_to_reader_name(method_name) - attr_name = method_name.to_s - if attr_name.end_with?("=") - attr_name = attr_name[0..-2] - end - attr_name + method_name.to_s.chomp('=') end end end diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index 52111e5442..a201f72ed0 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -29,8 +29,7 @@ module ActiveModel 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 @@ -65,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/length.rb b/activemodel/lib/active_model/validations/length.rb index 910cca2f49..79297ac119 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -136,7 +136,7 @@ module ActiveModel # * <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)"). diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index ad7012df48..9a0a0655de 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -120,7 +120,7 @@ module ActiveModel # * <tt>:only_integer</tt> - Specifies whether the value has to be an # integer, e.g. an integral value (default is +false+). # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is - # +false+). Notice that for fixnum and float columns empty strings are + # +false+). Notice that for Integer and Float columns empty strings are # converted to +nil+. # * <tt>:greater_than</tt> - Specifies the value must be greater than the # supplied value. 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/errors_test.rb b/activemodel/test/cases/errors_test.rb index a5ac055033..c90ee7021c 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! @@ -420,4 +427,13 @@ class ErrorsTest < ActiveModel::TestCase assert_equal [:name], person.errors.messages.keys assert_equal [:name], person.errors.details.keys end + + test "errors are marshalable" do + errors = ActiveModel::Errors.new(Person.new) + errors.add(:name, :invalid) + serialized = Marshal.load(Marshal.dump(errors)) + + assert_equal errors.messages, serialized.messages + assert_equal errors.details, serialized.details + end end diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb index 27fdbc739c..c589fd2c33 100644 --- a/activemodel/test/cases/helper.rb +++ b/activemodel/test/cases/helper.rb @@ -1,7 +1,4 @@ -require File.expand_path('../../../../load_paths', __FILE__) - require 'active_model' -require 'active_support/core_ext/string/access' # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true diff --git a/activemodel/test/cases/type/decimal_test.rb b/activemodel/test/cases/type/decimal_test.rb index 353dbf84ad..1950566c0e 100644 --- a/activemodel/test/cases/type/decimal_test.rb +++ b/activemodel/test/cases/type/decimal_test.rb @@ -52,6 +52,13 @@ module ActiveModel 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 index dac922db42..6603f25c9a 100644 --- a/activemodel/test/cases/type/integer_test.rb +++ b/activemodel/test/cases/type/integer_test.rb @@ -53,25 +53,25 @@ module ActiveModel end test "values below int min value are out of range" do - assert_raises(::RangeError) 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(::RangeError) do + assert_raises(ActiveModel::RangeError) do Integer.new.serialize(2147483648) end end test "very small numbers are out of range" do - assert_raises(::RangeError) do + assert_raises(ActiveModel::RangeError) do Integer.new.serialize(-9999999999999999999999999999999) end end test "very large numbers are out of range" do - assert_raises(::RangeError) do + assert_raises(ActiveModel::RangeError) do Integer.new.serialize(9999999999999999999999999999999) end end @@ -96,10 +96,10 @@ module ActiveModel assert_equal(9223372036854775807, type.serialize(9223372036854775807)) assert_equal(-9223372036854775808, type.serialize(-9223372036854775808)) - assert_raises(::RangeError) do + assert_raises(ActiveModel::RangeError) do type.serialize(-9999999999999999999999999999999) end - assert_raises(::RangeError) do + assert_raises(ActiveModel::RangeError) do type.serialize(9999999999999999999999999999999) end end diff --git a/activemodel/test/cases/type/unsigned_integer_test.rb b/activemodel/test/cases/type/unsigned_integer_test.rb index 16301b3ac0..026cb08a06 100644 --- a/activemodel/test/cases/type/unsigned_integer_test.rb +++ b/activemodel/test/cases/type/unsigned_integer_test.rb @@ -9,7 +9,7 @@ module ActiveModel 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/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/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 ee901b75fb..11dce1df20 100644 --- a/activemodel/test/cases/validations/length_validation_test.rb +++ b/activemodel/test/cases/validations/length_validation_test.rb @@ -355,7 +355,7 @@ class LengthValidationTest < ActiveModel::TestCase assert_equal ["Your essay must be at least 5 words."], t.errors[:content] end - def test_validates_length_of_for_fixnum + def test_validates_length_of_for_integer Topic.validates_length_of(:approved, is: 4) t = Topic.new("title" => "uhohuhoh", "content" => "whatever", approved: 1) 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/models/project.rb b/activemodel/test/models/project.rb deleted file mode 100644 index 581b6dc0b3..0000000000 --- a/activemodel/test/models/project.rb +++ /dev/null @@ -1,3 +0,0 @@ -class Project - include ActiveModel::DeprecatedMassAssignmentSecurity -end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index b59c9b4635..0bd3c2e78c 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,1815 +1,35 @@ -* Improve schema_migrations insertion performance by inserting all versions - in one INSERT SQL. - - *Akira Matsuda*, *Naoto Koshikawa* - -* Using `references` or `belongs_to` in migrations will always add index - for the referenced column by default, without adding `index: true` option - to generated migration file. Users can opt out of this by passing - `index: false`. - - Fixes #18146. - - *Matthew Draper*, *Prathamesh Sonpatki* - -* Run `type` attributes through attributes API type-casting before - instantiating the corresponding subclass. This makes it possible to define - custom STI mappings. - - Fixes #21986. - - *Yves Senn* - -* Don't try to quote functions or expressions passed to `:default` option if - they are passed as procs. - - This will generate proper query with the passed function or expression for - the default option, instead of trying to quote it in incorrect fashion. - - Example: - - create_table :posts do |t| - t.datetime :published_at, default: -> { 'NOW()' } - end - - *Ryuta Kamizono* - -* Fix regression when loading fixture files with symbol keys. - - Fixes #22584. - - *Yves Senn* - -* Use `version` column as primary key for schema_migrations table because - `schema_migrations` versions are guaranteed to be unique. - - This makes it possible to use `update_attributes` on models that do - not have a primary key. - - *Richard Schneeman* - -* Add short-hand methods for text and blob types in MySQL. - - In Pg and Sqlite3, `:text` and `:binary` have variable unlimited length. - But in MySQL, these have limited length for each types (ref #21591, #21619). - This change adds short-hand methods for each text and blob types. - - Example: - - create_table :foos do |t| - t.tinyblob :tiny_blob - t.mediumblob :medium_blob - t.longblob :long_blob - t.tinytext :tiny_text - t.mediumtext :medium_text - t.longtext :long_text - end - - *Ryuta Kamizono* - -* Take into account UTC offset when assigning string representation of - timestamp with offset specified to attribute of time type. - - *Andrey Novikov* - -* When calling `first` with a `limit` argument, return directly from the - `loaded?` records if available. - - *Ben Woosley* - -* Deprecate sending the `offset` argument to `find_nth`. Please use the - `offset` method on relation instead. - - *Ben Woosley* - -## Rails 5.0.0.beta1 (December 18, 2015) ## - -* Order the result of `find(ids)` to match the passed array, if the relation - has no explicit order defined. - - Fixes #20338. - - *Miguel Grazziotin*, *Matthew Draper* - -* Omit default limit values in dumped schema. It's tidier, and if the defaults - change in the future, we can address that via Migration API Versioning. - - *Jean Boussier* - -* Support passing the schema name as a prefix to table name in - `ConnectionAdapters::SchemaStatements#indexes`. Previously the prefix would - be considered a full part of the index name, and only the schema in the - current search path would be considered. - - *Grey Baker* - -* Ignore index name in `index_exists?` and `remove_index` when not passed a - name to check for. - - *Grey Baker* - -* Extract 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`. - - *Abdelkader Boudih* - -* 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. - - Newly generated applications have `app/models/application_record.rb` - present by default. - - *Genadi Samokovarov* - -* 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. - - New migrations specify the Rails version they were written for: - - class AddStatusToOrders < ActiveRecord::Migration[5.0] - def change - # ... - end - end - - *Matthew Draper*, *Ravil Bayramgalin* - -* Use bind params for `limit` and `offset`. This will generate significantly - fewer prepared statements for common tasks like pagination. To support this - change, passing a string containing a comma to `limit` has been deprecated, - and passing an Arel node to `limit` is no longer supported. - - Fixes #22250. - - *Sean Griffin* - -* Introduce after_{create,update,delete}_commit callbacks. - - Before: - - after_commit :add_to_index_later, on: :create - after_commit :update_in_index_later, on: :update - after_commit :remove_from_index_later, on: :destroy - - After: - - after_create_commit :add_to_index_later - after_update_commit :update_in_index_later - after_destroy_commit :remove_from_index_later - - Fixes #22515. - - *Genadi Samokovarov* - -* Respect the column default values for `inheritance_column` when - instantiating records through the base class. - - Fixes #17121. - - Example: - - # The schema of BaseModel has `t.string :type, default: 'SubType'` - subtype = BaseModel.new - assert_equals SubType, subtype.class - - *Kuldeep Aggarwal* - -* Fix `rake db:structure:dump` on Postgres when multiple schemas are used. - - Fixes #22346. - - *Nick Muerdter*, *ckoenig* - -* Add schema dumping support for PostgreSQL geometric data types. - - *Ryuta Kamizono* - -* Except keys of `build_record`'s argument from `create_scope` in `initialize_attributes`. - - Fixes #21893. - - *Yuichiro Kaneko* - -* Deprecate `connection.tables` on the SQLite3 and MySQL adapters. - Also deprecate passing arguments to `#tables`. - And deprecate `table_exists?`. - - The `#tables` method of some adapters (mysql, 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. - - 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. - - *Yuichiro Kaneko* - -* Improve support for non Active Record objects on `validates_associated` - - Skipping `marked_for_destruction?` when the associated object does not responds - to it make easier to validate virtual associations built on top of Active Model - objects and/or serialized objects that implement a `valid?` instance method. - - *Kassio Borges*, *Lucas Mazza* - -* Change connection management middleware to return a new response with - a body proxy, rather than mutating the original. - - *Kevin Buchanan* - -* Make `db:migrate:status` to render `1_some.rb` format migrate files. - - These files are in `db/migrate`: - - * 1_valid_people_have_last_names.rb - * 20150819202140_irreversible_migration.rb - * 20150823202140_add_admin_flag_to_users.rb - * 20150823202141_migration_tests.rb - * 2_we_need_reminders.rb - * 3_innocent_jointable.rb - - Before: - - $ bundle exec rake db:migrate:status - ... - - Status Migration ID Migration Name - -------------------------------------------------- - up 001 ********** NO FILE ********** - up 002 ********** NO FILE ********** - up 003 ********** NO FILE ********** - up 20150819202140 Irreversible migration - up 20150823202140 Add admin flag to users - up 20150823202141 Migration tests - - After: - - $ bundle exec rake db:migrate:status - ... - - Status Migration ID Migration Name - -------------------------------------------------- - up 001 Valid people have last names - up 002 We need reminders - up 003 Innocent jointable - up 20150819202140 Irreversible migration - up 20150823202140 Add admin flag to users - up 20150823202141 Migration tests - - *Yuichiro Kaneko* - -* Define `ActiveRecord::Sanitization.sanitize_sql_for_order` and use it inside - `preprocess_order_args`. - - *Yuichiro Kaneko* - -* Allow bigint with default nil for avoiding auto increment primary key. - - *Ryuta Kamizono* - -* Remove `DEFAULT_CHARSET` and `DEFAULT_COLLATION` in `MySQLDatabaseTasks`. - - We should omit the collation entirely rather than providing a default. - Then the choice is the responsibility of the server and MySQL distribution. - - *Ryuta Kamizono* - -* Alias `ActiveRecord::Relation#left_joins` to - `ActiveRecord::Relation#left_outer_joins`. - - *Takashi Kokubun* - -* Use advisory locking to raise a `ConcurrentMigrationError` instead of - attempting to migrate when another migration is currently running. - - *Sam Davies* - -* Added `ActiveRecord::Relation#left_outer_joins`. - - Example: - - User.left_outer_joins(:posts) - # => SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON - "posts"."user_id" = "users"."id" - - *Florian Thomas* - -* Support passing an array to `order` for SQL parameter sanitization. - - *Aaron Suggs* - -* Avoid disabling errors on the PostgreSQL connection when enabling the - `standard_conforming_strings` setting. Errors were previously disabled because - the setting wasn't writable in Postgres 8.1 and didn't exist in earlier - versions. Now Rails only supports Postgres 8.2+ we're fine to assume the - setting exists. Disabling errors caused problems when using a connection - pooling tool like PgBouncer because it's not guaranteed to have the same - connection between calls to `execute` and it could leave the connection - with errors disabled. - - Fixes #22101. - - *Harry Marr* - -* Set `scope.reordering_value` to `true` if `:reordering`-values are specified. - - Fixes #21886. - - *Hiroaki Izu* - -* Add support for bidirectional destroy dependencies. - - Fixes #13609. - - Example: - - class Content < ActiveRecord::Base - has_one :position, dependent: :destroy - end - - class Position < ActiveRecord::Base - belongs_to :content, dependent: :destroy - end - - *Seb Jacobs* - -* Includes HABTM returns correct size now. It's caused by the join dependency - only instantiates one HABTM object because the join table hasn't a primary key. - - Fixes #16032. - - Examples: - - before: - - Project.first.salaried_developers.size # => 3 - Project.includes(:salaried_developers).first.salaried_developers.size # => 1 - - after: - - Project.first.salaried_developers.size # => 3 - Project.includes(:salaried_developers).first.salaried_developers.size # => 3 - - *Bigxiang* - -* Add option to index errors in nested attributes - - For models which have nested attributes, errors within those models will - now be indexed if :index_errors is specified when defining a - has_many relationship, or if its set in the global config. - - Example: - - class Guitar < ActiveRecord::Base - has_many :tuning_pegs - accepts_nested_attributes_for :tuning_pegs - end - - class TuningPeg < ActiveRecord::Base - belongs_to :guitar - validates_numericality_of :pitch - end - - # Old style - guitar.errors["tuning_pegs.pitch"] = ["is not a number"] - - # New style (if defined globally, or set in has_many_relationship) - guitar.errors["tuning_pegs[1].pitch"] = ["is not a number"] - - *Michael Probber*, *Terence Sun* - -* Exit with non-zero status for failed database rake tasks. - - *Jay Hayes* - -* Queries such as `Computer.joins(:monitor).group(:status).count` will now be - interpreted as `Computer.joins(:monitor).group('computers.status').count` - so that when `Computer` and `Monitor` have both `status` columns we don't - have conflicts in projection. - - *Rafael Sales* - -* Add ability to default to `uuid` as primary key when generating database migrations. - - Example: - - config.generators do |g| - g.orm :active_record, primary_key_type: :uuid - end - - *Jon McCartie* - -* Don't cache arguments in `#find_by` if they are an `ActiveRecord::Relation`. - - Fixes #20817. - - *Hiroaki Izu* - -* Qualify column name inserted by `group` in calculation. - - Giving `group` an unqualified column name now works, even if the relation - has `JOIN` with another table which also has a column of the name. - - *Soutaro Matsumoto* - -* Don't cache prepared statements containing an IN clause or a SQL literal, as - these queries will change often and are unlikely to have a cache hit. - - *Sean Griffin* - -* Fix `rewhere` in a `has_many` association. - - Fixes #21955. - - *Josh Branchaud*, *Kal* - -* `where` raises ArgumentError on unsupported types. - - Fixes #20473. - - *Jake Worth* - -* Add an immutable string type to help reduce memory usage for apps which do - not need mutation detection on strings. - - *Sean Griffin* - -* Give `ActiveRecord::Relation#update` its own deprecation warning when - passed an `ActiveRecord::Base` instance. - - Fixes #21945. - - *Ted Johansson* - -* Make it possible to pass `:to_table` when adding a foreign key through - `add_reference`. - - Fixes #21563. - - *Yves Senn* - -* No longer pass deprecated option `-i` to `pg_dump`. - - *Paul Sadauskas* - -* Concurrent `AR::Base#increment!` and `#decrement!` on the same record - are all reflected in the database rather than overwriting each other. - - *Bogdan Gusiev* - -* Avoid leaking the first relation we call `first` on, per model. - - Fixes #21921. - - *Matthew Draper*, *Jean Boussier* - -* Remove unused `pk_and_sequence_for` in `AbstractMysqlAdapter`. - - *Ryuta Kamizono* - -* Allow fixtures files to set the model class in the YAML file itself. - - To load the fixtures file `accounts.yml` as the `User` model, use: - - _fixture: - model_class: User - david: - name: David - - Fixes #9516. - - *Roque Pinel* - -* Don't require a database connection to load a class which uses acceptance - validations. +* Ensure hashes can be assigned to attributes created using `composed_of`. + Fixes #25210. *Sean Griffin* -* Correctly apply `unscope` when preloading through associations. - - *Jimmy Bourassa* - -* Fixed taking precision into count when assigning a value to timestamp attribute. - - Timestamp column can have less precision than ruby timestamp - In result in how big a fraction of a second can be stored in the - database. - - - m = Model.create! - m.created_at.usec == m.reload.created_at.usec # => false - # due to different precision in Time.now and database column - - If the precision is low enough, (mysql default is 0, so it is always low - enough by default) the value changes when model is reloaded from the - database. This patch fixes that issue ensuring that any timestamp - assigned as an attribute is converted to column precision under the - attribute. - - *Bogdan Gusiev* - -* Introduce `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). - - Also deprecate `SchemaCache#tables`, `SchemaCache#table_exists?` and - `SchemaCache#clear_table_cache!` in favor of their new data source - counterparts. - - *Yves Senn*, *Matthew Draper* - -* Add `ActiveRecord::Base.ignored_columns` to make some columns - invisible from Active Record. - - *Jean Boussier* - -* `ActiveRecord::Tasks::MySQLDatabaseTasks` fails if shellout to - mysql commands (like `mysqldump`) is not successful. - - *Steve Mitchell* - -* Ensure `select` quotes aliased attributes, even when using `from`. - - Fixes #21488. - - *Sean Griffin*, *@johanlunds* - -* MySQL: support `unsigned` numeric data types. - - Example: - - create_table :foos do |t| - t.unsigned_integer :quantity - t.unsigned_bigint :total - t.unsigned_float :percentage - t.unsigned_decimal :price, precision: 10, scale: 2 - end - - The `unsigned: true` option may be used for the primary key: - - create_table :foos, id: :bigint, unsigned: true do |t| - … - end +* Fix logging edge case where if an attribute was of the binary type and + was provided as a Hash. - *Ryuta Kamizono* + *Jon Moss* -* Add `#views` and `#view_exists?` methods on connection adapters. +* Handle JSON deserialization correctly if the column default from database + adapter returns `''` instead of `nil`. - *Ryuta Kamizono* + *Johannes Opper* -* Correctly dump composite primary key. +* Introduce ActiveRecord::TransactionSerializationError for catching + transaction serialization failures or deadlocks. - Example: - - create_table :barcodes, primary_key: ["region", "code"] do |t| - t.string :region - t.integer :code - end - - *Ryuta Kamizono* - -* Lookup the attribute name for `restrict_with_error` messages on the - model class that defines the association. - - *kuboon*, *Ronak Jangir* - -* Correct query for PostgreSQL 8.2 compatibility. - - *Ben Murphy*, *Matthew Draper* - -* `bin/rake db:migrate` uses - `ActiveRecord::Tasks::DatabaseTasks.migrations_paths` instead of - `Migrator.migrations_paths`. - - *Tobias Bielohlawek* - -* Support dropping indexes concurrently in PostgreSQL. - - See http://www.postgresql.org/docs/9.4/static/sql-dropindex.html for more - details. - - *Grey Baker* + *Erol Fornoles* -* Deprecate passing conditions to `ActiveRecord::Relation#delete_all` - and `ActiveRecord::Relation#destroy_all`. +* PostgreSQL: Fix db:structure:load silent failure on SQL error - *Wojciech Wnętrzak* - -* Instantiating an AR model with `ActionController::Parameters` now raises - an `ActiveModel::ForbiddenAttributesError` if the parameters include a - `type` field that has not been explicitly permitted. Previously, the - `type` field was simply ignored in the same situation. - - *Prem Sichanugrist* - -* PostgreSQL, `create_schema`, `drop_schema` and `rename_table` now quote - schema names. - - Fixes #21418. + The command line flag "-v ON_ERROR_STOP=1" should be used + when invoking psql to make sure errors are not suppressed. Example: - create_schema("my.schema") - # CREATE SCHEMA "my.schema"; - - *Yves Senn* - -* PostgreSQL, add `:if_exists` option to `#drop_schema`. This makes it - possible to drop a schema that might exist without raising an exception if - it doesn't. - - *Yves Senn* - -* Only try to nullify has_one target association if the record is persisted. - - Fixes #21223. - - *Agis Anastasopoulos* - -* Uniqueness validator raises descriptive error when running on a persisted - record without primary key. - - Fixes #21304. - - *Yves Senn* - -* Add a native JSON data type support in MySQL. - - Example: - - create_table :json_data_type do |t| - t.json :settings - end - - *Ryuta Kamizono* - -* Descriptive error message when fixtures contain a missing column. - - Fixes #21201. - - *Yves Senn* - -* `ActiveRecord::Tasks::PostgreSQLDatabaseTasks` fail if shellout to - postgresql commands (like `pg_dump`) is not successful. - - *Bryan Paxton*, *Nate Berkopec* - -* Add `ActiveRecord::Relation#in_batches` to work with records and relations - in batches. - - Available options are `of` (batch size), `load`, `start`, and `finish`. - - Examples: - - Person.in_batches.each_record(&:party_all_night!) - Person.in_batches.update_all(awesome: true) - Person.in_batches.delete_all - Person.in_batches.each do |relation| - relation.delete_all - sleep 10 # Throttles the delete queries - end - - Fixes #20933. - - *Sina Siadat* - -* Added methods for PostgreSQL geometric data types to use in migrations. - - Example: - - create_table :foo do |t| - t.line :foo_line - t.lseg :foo_lseg - t.box :foo_box - t.path :foo_path - t.polygon :foo_polygon - t.circle :foo_circle - end - - *Mehmet Emin İNAÇ* - -* Add `cache_key` to ActiveRecord::Relation. - - Example: - - @users = User.where("name like ?", "%Alberto%") - @users.cache_key - # => "/users/query-5942b155a43b139f2471b872ac54251f-3-20150714212107656125000" - - *Alberto Fernández-Capel* - -* Properly allow uniqueness validations on primary keys. - - Fixes #20966. - - *Sean Griffin*, *presskey* - -* Don't raise an error if an association failed to destroy when `destroy` was - called on the parent (as opposed to `destroy!`). - - Fixes #20991. - - *Sean Griffin* - -* `ActiveRecord::RecordNotFound` modified to store model name, primary_key and - id of the caller model. It allows the catcher of this exception to make - a better decision to what to do with it. - - Example: - - class SomeAbstractController < ActionController::Base - rescue_from ActiveRecord::RecordNotFound, with: :redirect_to_404 - - private def redirect_to_404(e) - return redirect_to(posts_url) if e.model == 'Post' - raise - end - end - - *Sameer Rahmani* - -* Deprecate the keys for association `restrict_dependent_destroy` errors in favor - of new key names. - - Previously `has_one` and `has_many` associations were using the - `one` and `many` keys respectively. Both of these keys have special - meaning in I18n (they are considered to be pluralizations) so by - renaming them to `has_one` and `has_many` we make the messages more explicit - and most importantly they don't clash with linguistical systems that need to - validate translation keys (and their pluralizations). - - The `:'restrict_dependent_destroy.one'` key should be replaced with - `:'restrict_dependent_destroy.has_one'`, and `:'restrict_dependent_destroy.many'` - with `:'restrict_dependent_destroy.has_many'`. - - *Roque Pinel*, *Christopher Dell* - -* Fix state being carried over from previous transaction. - - Considering the following example where `name` is a required attribute. - Before we had `new_record?` returning `true` for a persisted record: - - author = Author.create! name: 'foo' - author.name = nil - author.save # => false - author.new_record? # => true - - Fixes #20824. - - *Roque Pinel* - -* Correctly ignore `mark_for_destruction` when `autosave` isn't set to `true` - when validating associations. - - Fixes #20882. - - *Sean Griffin* - -* Fix a bug where counter_cache doesn't always work with polymorphic - relations. - - Fixes #16407. - - *Stefan Kanev*, *Sean Griffin* - -* Ensure that cyclic associations with autosave don't cause duplicate errors - to be added to the parent record. - - Fixes #20874. - - *Sean Griffin* - -* Ensure that `ActionController::Parameters` can still be passed to nested - attributes. - - Fixes #20922. - - *Sean Griffin* - -* Deprecate force association reload by passing a truthy argument to - association method. - - For collection association, you can call `#reload` on association proxy to - force a reload: - - @user.posts.reload # Instead of @user.posts(true) - - For singular association, you can call `#reload` on the parent object to - clear its association cache then call the association method: - - @user.reload.profile # Instead of @user.profile(true) - - Passing a truthy argument to force association to reload will be removed in - Rails 5.1. - - *Prem Sichanugrist* - -* Replaced `ActiveSupport::Concurrency::Latch` with `Concurrent::CountDownLatch` - from the concurrent-ruby gem. - - *Jerry D'Antonio* - -* Fix through associations using scopes having the scope merged multiple - times. - - Fixes #20721. - Fixes #20727. - - *Sean Griffin* - -* `ActiveRecord::Base.dump_schema_after_migration` applies migration tasks - other than `db:migrate`. (eg. `db:rollback`, `db:migrate:dup`, ...) - - Fixes #20743. - - *Yves Senn* - -* Add alternate syntax to make `change_column_default` reversible. - - User can pass in `:from` and `:to` to make `change_column_default` command - become reversible. - - Example: - - change_column_default :posts, :status, from: nil, to: "draft" - change_column_default :users, :authorized, from: true, to: false - - *Prem Sichanugrist* - -* Prevent error when using `force_reload: true` on an unassigned polymorphic - belongs_to association. - - Fixes #20426. - - *James Dabbs* - -* Correctly raise `ActiveRecord::AssociationTypeMismatch` when assigning - a wrong type to a namespaced association. - - Fixes #20545. - - *Diego Carrion* - -* `validates_absence_of` respects `marked_for_destruction?`. - - Fixes #20449. - - *Yves Senn* - -* Include the `Enumerable` module in `ActiveRecord::Relation` - - *Sean Griffin*, *bogdan* - -* Use `Enumerable#sum` in `ActiveRecord::Relation` if a block is given. - - *Sean Griffin* - -* Let `WITH` queries (Common Table Expressions) be explainable. - - *Vladimir Kochnev* - -* Make `remove_index :table, :column` reversible. - - *Yves Senn* - -* Fixed an error which would occur in dirty checking when calling - `update_attributes` from a getter. - - Fixes #20531. - - *Sean Griffin* - -* Make `remove_foreign_key` reversible. Any foreign key options must be - specified, similar to `remove_column`. - - *Aster Ryan* - -* Add `:_prefix` and `:_suffix` options to `enum` definition. - - Fixes #17511, #17415. - - *Igor Kapkov* - -* Correctly handle decimal arrays with defaults in the schema dumper. - - Fixes #20515. - - *Sean Griffin*, *jmondo* - -* Deprecate the PostgreSQL `:point` type in favor of a new one which will return - `Point` objects instead of an `Array` - - *Sean Griffin* - -* Ensure symbols passed to `ActiveRecord::Relation#select` are always treated - as columns. - - Fixes #20360. - - *Sean Griffin* - -* Do not set `sql_mode` if `strict: :default` is specified. - - # config/database.yml - production: - adapter: mysql2 - database: foo_prod - user: foo - strict: :default - - *Ryuta Kamizono* - -* Allow proc defaults to be passed to the attributes API. See documentation - for examples. - - *Sean Griffin*, *Kir Shatrov* - -* SQLite: `:collation` support for string and text columns. - - Example: - - create_table :foo do |t| - t.string :string_nocase, collation: 'NOCASE' - t.text :text_rtrim, collation: 'RTRIM' - end - - add_column :foo, :title, :string, collation: 'RTRIM' - - change_column :foo, :title, :string, collation: 'NOCASE' - - *Akshay Vishnoi* - -* Allow the use of symbols or strings to specify enum values in test - fixtures: - - awdr: - title: "Agile Web Development with Rails" - status: :proposed - - *George Claghorn* - -* Clear query cache when `ActiveRecord::Base#reload` is called. - - *Shane Hender, Pierre Nespo* - -* Include stored procedures and function on the MySQL structure dump. - - *Jonathan Worek* - -* Pass `:extend` option for `has_and_belongs_to_many` associations to the - underlying `has_many :through`. - - *Jaehyun Shin* - -* Deprecate `Relation#uniq` use `Relation#distinct` instead. - - See #9683. - - *Yves Senn* - -* Allow single table inheritance instantiation to work when storing - demodulized class names. - - *Alex Robbin* - -* Correctly pass MySQL options when using `structure_dump` or - `structure_load`. - - Specifically, it fixes an issue when using SSL authentication. - - *Alex Coomans* - -* Dump indexes in `create_table` instead of `add_index`. - - If the adapter supports indexes in `create_table`, generated SQL is - slightly more efficient. - - *Ryuta Kamizono* - -* Correctly dump `:options` on `create_table` for MySQL. - - *Ryuta Kamizono* - -* PostgreSQL: `:collation` support for string and text columns. - - Example: - - create_table :foos do |t| - t.string :string_en, collation: 'en_US.UTF-8' - t.text :text_ja, collation: 'ja_JP.UTF-8' - end - - *Ryuta Kamizono* - -* Remove `ActiveRecord::Serialization::XmlSerializer` from core. - - *Zachary Scott* - -* Make `unscope` aware of "less than" and "greater than" conditions. - - *TAKAHASHI Kazuaki* - -* `find_by` and `find_by!` raise `ArgumentError` when called without - arguments. - - *Kohei Suzuki* - -* Revert behavior of `db:schema:load` back to loading the full - environment. This ensures that initializers are run. - - Fixes #19545. - - *Yves Senn* - -* Fix missing index when using `timestamps` with the `index` option. - - The `index` option used with `timestamps` should be passed to both - `column` definitions for `created_at` and `updated_at` rather than just - the first. - - *Paul Mucur* - -* Rename `:class` to `:anonymous_class` in association options. - - Fixes #19659. - - *Andrew White* - -* Autosave existing records on a has many through association when the parent - is new. - - Fixes #19782. - - *Sean Griffin* - -* Fixed a bug where uniqueness validations would error on out of range values, - even if an validation should have prevented it from hitting the database. - - *Andrey Voronkov* - -* MySQL: `:charset` and `:collation` support for string and text columns. - - Example: - - create_table :foos do |t| - t.string :string_utf8_bin, charset: 'utf8', collation: 'utf8_bin' - t.text :text_ascii, charset: 'ascii' - end - - *Ryuta Kamizono* - -* Foreign key related methods in the migration DSL respect - `ActiveRecord::Base.pluralize_table_names = false`. - - Fixes #19643. - - *Mehmet Emin İNAÇ* - -* Reduce memory usage from loading types on PostgreSQL. - - Fixes #19578. - - *Sean Griffin* - -* Add `config.active_record.warn_on_records_fetched_greater_than` option. - - When set to an integer, a warning will be logged whenever a result set - larger than the specified size is returned by a query. - - Fixes #16463. - - *Jason Nochlin* - -* Ignore `.psqlrc` when loading database structure. - - *Jason Weathered* - -* Fix referencing wrong table aliases while joining tables of has many through - association (only when calling calculation methods). - - Fixes #19276. - - *pinglamb* - -* Correctly persist a serialized attribute that has been returned to - its default value by an in-place modification. - - Fixes #19467. - - *Matthew Draper* - -* Fix generating the schema file when using PostgreSQL `BigInt[]` data type. - Previously the `limit: 8` was not coming through, and this caused it to - become `Int[]` data type after rebuilding from the schema. - - Fixes #19420. - - *Jake Waller* - -* Reuse the `CollectionAssociation#reader` cache when the foreign key is - available prior to save. - - *Ben Woosley* - -* Add `config.active_record.dump_schemas` to fix `db:structure:dump` - when using schema_search_path and PostgreSQL extensions. - - Fixes #17157. - - *Ryan Wallace* - -* Renaming `use_transactional_fixtures` to `use_transactional_tests` for clarity. - - Fixes #18864. - - *Brandon Weiss* - -* Increase pg gem version requirement to `~> 0.18`. Earlier versions of the - pg gem are known to have problems with Ruby 2.2. - - *Matt Brictson* - -* Correctly dump `serial` and `bigserial`. - - *Ryuta Kamizono* - -* Fix default `format` value in `ActiveRecord::Tasks::DatabaseTasks#schema_file`. - - *James Cox* - -* Don't enroll records in the transaction if they don't have commit callbacks. - This was causing a memory leak when creating many records inside a transaction. - - Fixes #15549. - - *Will Bryant*, *Aaron Patterson* - -* Correctly create through records when created on a has many through - association when using `where`. - - Fixes #19073. - - *Sean Griffin* - -* Add `SchemaMigration.create_table` support for any unicode charsets with MySQL. - - *Ryuta Kamizono* - -* PostgreSQL no longer disables user triggers if system triggers can't be - disabled. Disabling user triggers does not fulfill what the method promises. - Rails currently requires superuser privileges for this method. - - If you absolutely rely on this behavior, consider patching - `disable_referential_integrity`. - - *Yves Senn* - -* Restore aborted transaction state when `disable_referential_integrity` fails - due to missing permissions. - - *Toby Ovod-Everett*, *Yves Senn* - -* In PostgreSQL, print a warning message if `disable_referential_integrity` - fails due to missing permissions. - - *Andrey Nering*, *Yves Senn* - -* Allow a `:limit` option for MySQL bigint primary key support. - - Example: - - create_table :foos, id: :primary_key, limit: 8 do |t| - end - - # or - - create_table :foos, id: false do |t| - t.primary_key :id, limit: 8 - end - - *Ryuta Kamizono* - -* `belongs_to` will now trigger a validation error by default if the association is not present. - You can turn this off on a per-association basis with `optional: true`. - (Note this new default only applies to new Rails apps that will be generated with - `config.active_record.belongs_to_required_by_default = true` in initializer.) - - *Josef Šimánek* - -* Fixed `ActiveRecord::Relation#becomes!` and `changed_attributes` issues for type - columns. - - Fixes #17139. - - *Miklos Fazekas* - -* Format the time string according to the precision of the time column. - - *Ryuta Kamizono* - -* Allow a `:precision` option for time type columns. - - *Ryuta Kamizono* - -* Add `ActiveRecord::Base.suppress` to prevent the receiver from being saved - during the given block. - - For example, here's a pattern of creating notifications when new comments - are posted. (The notification may in turn trigger an email, a push - notification, or just appear in the UI somewhere): - - class Comment < ActiveRecord::Base - belongs_to :commentable, polymorphic: true - after_create -> { Notification.create! comment: self, - recipients: commentable.recipients } - end - - That's what you want the bulk of the time. A new comment creates a new - Notification. There may be edge cases where you don't want that, like - when copying a commentable and its comments, in which case write a - concern with something like this: - - module Copyable - def copy_to(destination) - Notification.suppress do - # Copy logic that creates new comments that we do not want triggering - # notifications. - end - end - end - - *Michael Ryan* - -* `:time` option added for `#touch`. - - Fixes #18905. - - *Hyonjee Joo* - -* Deprecate passing of `start` value to `find_in_batches` and `find_each` - in favour of `begin_at` value. - - *Vipul A M* - -* Add `foreign_key_exists?` method. - - *Tõnis Simo* - -* Use SQL COUNT and LIMIT 1 queries for `none?` and `one?` methods - if no block or limit is given, instead of loading the entire - collection into memory. This applies to relations (e.g. `User.all`) - as well as associations (e.g. `account.users`) - - # Before: - - users.none? - # SELECT "users".* FROM "users" - - users.one? - # SELECT "users".* FROM "users" - - # After: - - users.none? - # SELECT 1 AS one FROM "users" LIMIT 1 - - users.one? - # SELECT COUNT(*) FROM "users" - - *Eugene Gilburg* - -* Have `enum` perform type casting consistently with the rest of Active - Record, such as `where`. - - *Sean Griffin* - -* `scoping` no longer pollutes the current scope of sibling classes when using - STI. - - Fixes #18806. - - Example: - - StiOne.none.scoping do - StiTwo.all - end - - - *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 `:finish` parameter that complements the `:start` - parameter to specify where to stop batch processing. - - *Vipul A M* - -* Fix a rounding problem for PostgreSQL timestamp columns. - - If a timestamp column has a precision specified, it needs to - format according to that. - - *Ryuta Kamizono* - -* Respect the database default charset for `schema_migrations` table. - - The charset of `version` column in `schema_migrations` table depends - on the database default charset and collation rather than the encoding - of the connection. - - *Ryuta Kamizono* - -* Raise `ArgumentError` when passing `nil` or `false` to `Relation#merge`. - - These are not valid values to merge in a relation, so it should warn users - early. - - *Rafael Mendonça França* - -* Use `SCHEMA` instead of `DB_STRUCTURE` for specifying a structure file. - - This makes the `db:structure` tasks consistent with `test:load_structure`. - - *Dieter Komendera* - -* Respect custom primary keys for associations when calling `Relation#where` - - Fixes #18813. - - *Sean Griffin* - -* Fix several edge cases which could result in a counter cache updating - twice or not updating at all for `has_many` and `has_many :through`. - - Fixes #10865. - - *Sean Griffin* - -* Foreign keys added by migrations were given random, generated names. This - meant a different `structure.sql` would be generated every time a developer - ran migrations on their machine. - - The generated part of foreign key names is now a hash of the table name and - column name, which is consistent every time you run the migration. - - *Chris Sinjakli* - -* Validation errors would be raised for parent records when an association - was saved when the parent had `validate: false`. It should not be the - responsibility of the model to validate an associated object unless the - object was created or modified by the parent. - - This fixes the issue by skipping validations if the parent record is - persisted, not changed, and not marked for destruction. - - Fixes #17621. - - *Eileen M. Uchitelle*, *Aaron Patterson* - -* Fix n+1 query problem when eager loading nil associations (fixes #18312) - - *Sammy Larbi* - -* Change the default error message from `can't be blank` to `must exist` for - the presence validator of the `:required` option on `belongs_to`/`has_one` - associations. - - *Henrik Nygren* - -* Fixed `ActiveRecord::Relation#group` method when an argument is an SQL - reserved keyword: - - Example: - - SplitTest.group(:key).count - Property.group(:value).count - - *Bogdan Gusiev* - -* Added the `#or` method on `ActiveRecord::Relation`, allowing use of the OR - operator to combine WHERE or HAVING clauses. - - Example: - - Post.where('id = 1').or(Post.where('id = 2')) - # => SELECT * FROM posts WHERE (id = 1) OR (id = 2) - - *Sean Griffin*, *Matthew Draper*, *Gael Muller*, *Olivier El Mekki* - -* Don't define autosave association callbacks twice from - `accepts_nested_attributes_for`. - - Fixes #18704. - - *Sean Griffin* - -* Integer types will no longer raise a `RangeError` when assigning an - attribute, but will instead raise when going to the database. - - Fixes several vague issues which were never reported directly. See the - commit message from the commit which added this line for some examples. - - *Sean Griffin* - -* Values which would error while being sent to the database (such as an - ASCII-8BIT string with invalid UTF-8 bytes on SQLite3), no longer error on - assignment. They will still error when sent to the database, but you are - given the ability to re-assign it to a valid value. - - Fixes #18580. - - *Sean Griffin* - -* Don't remove join dependencies in `Relation#exists?` - - Fixes #18632. - - *Sean Griffin* - -* Invalid values assigned to a JSON column are assumed to be `nil`. - - Fixes #18629. - - *Sean Griffin* - -* Add `ActiveRecord::Base#accessed_fields`, which can be used to quickly - discover which fields were read from a model when you are looking to only - select the data you need from the database. - - *Sean Griffin* - -* Introduce the `:if_exists` option for `drop_table`. - - Example: - - drop_table(:posts, if_exists: true) - - That would execute: - - DROP TABLE IF EXISTS posts - - If the table doesn't exist, `if_exists: false` (the default) raises an - exception whereas `if_exists: true` does nothing. - - *Cody Cutrer*, *Stefan Kanev*, *Ryuta Kamizono* - -* Don't run SQL if attribute value is not changed for update_attribute method. - - *Prathamesh Sonpatki* - -* `time` columns can now get affected by `time_zone_aware_attributes`. If you have - set `config.time_zone` to a value other than `'UTC'`, they will be treated - as in that time zone by default in Rails 5.1. If this is not the desired - behavior, you can set - - ActiveRecord::Base.time_zone_aware_types = [:datetime] - - A deprecation warning will be emitted if you have a `:time` column, and have - not explicitly opted out. - - Fixes #3145. - - *Sean Griffin* - -* Tests now run after_commit callbacks. You no longer have to declare - `uses_transaction ‘test name’` to test the results of an after_commit. - - after_commit callbacks run after committing a transaction whose parent - is not `joinable?`: un-nested transactions, transactions within test cases, - and transactions in `console --sandbox`. - - *arthurnn*, *Ravil Bayramgalin*, *Matthew Draper* - -* `nil` as a value for a binary column in a query no longer logs as - "<NULL binary data>", and instead logs as just "nil". - - *Sean Griffin* - -* `attribute_will_change!` will no longer cause non-persistable attributes to - be sent to the database. - - Fixes #18407. - - *Sean Griffin* - -* Remove support for the `protected_attributes` gem. - - *Carlos Antonio da Silva*, *Roberto Miranda* - -* Fix accessing of fixtures having non-string labels like Fixnum. - - *Prathamesh Sonpatki* - -* Remove deprecated support to preload instance-dependent associations. - - *Yves Senn* - -* 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 Active Record `before_` callback had the - side effect of halting the callback chain. - This is not recommended anymore and, depending on the value of the - `ActiveSupport.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. - - Fixes #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. - - Fixes #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. - - Fixes #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. + psql -v ON_ERROR_STOP=1 -q -f awesome-file.sql my-app-db - *Noam Gagliardi-Rabinovich* + Fixes #23818. -* `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. + *Ralin Chimev* - *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/RUNNING_UNIT_TESTS.rdoc b/activerecord/RUNNING_UNIT_TESTS.rdoc index a74fcf2df7..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: @@ -41,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 0564dca94a..012db35765 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -20,6 +20,8 @@ end desc 'Run mysql2, sqlite, and postgresql tests by default' task :default => :test +task :package + desc 'Run mysql2, sqlite, and postgresql tests' task :test do tasks = defined?(JRUBY_VERSION) ? diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 4405da2812..881dee13e4 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -13,7 +13,7 @@ Gem::Specification.new do |s| 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' 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 ab3846ae65..baa497dc98 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -137,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 3ff41ed81b..8bed5bca28 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -256,15 +256,16 @@ module ActiveRecord def writer_method(name, class_name, mapping, allow_nil, converter) define_method("#{name}=") do |part| klass = class_name.constantize - if part.is_a?(Hash) - raise ArgumentError unless part.size == part.keys.max - part = klass.new(*part.sort.map(&:last)) - end unless part.is_a?(klass) || converter.nil? || part.nil? part = converter.respond_to?(:call) ? converter.call(part) : klass.send(converter, part) end + if part.is_a?(Hash) + raise ArgumentError unless part.size == part.keys.max + part = klass.new(*part.sort.map(&:last)) + end + if part.nil? && allow_nil mapping.each { |key, _| self[key] = nil } @aggregation_cache[name] = nil 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 f6d8e8a342..3729e22e64 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -300,10 +300,10 @@ module ActiveRecord # # === A word of warning # - # Don't create associations that have the same name as instance methods of - # 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. + # Don't create associations that have the same name as {instance methods}[rdoc-ref:ActiveRecord::Core] of + # <tt>ActiveRecord::Base</tt>. Since the association adds a method with that name to + # its model, using an association with the same name as one provided by <tt>ActiveRecord::Base</tt> will override the method inherited through <tt>ActiveRecord::Base</tt> and will break things. + # For instance, +attributes+ and +connection+ would be bad choices for association names, because those names already exist in the list of <tt>ActiveRecord::Base</tt> instance methods. # # == Auto-generated methods # See also Instance Public methods below for more details. @@ -318,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 # ----------------------------------+-------+----------+---------- @@ -504,7 +504,7 @@ module ActiveRecord # # == Customizing the query # - # \Associations are built from <tt>Relation</tt>s, and you can use the Relation 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 @@ -1326,7 +1326,8 @@ module ActiveRecord # 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. @@ -1456,7 +1457,8 @@ module ActiveRecord # 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. @@ -1580,7 +1582,8 @@ 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. @@ -1593,6 +1596,8 @@ module ActiveRecord # 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 #has_one or #has_many association on the associated # object that is the inverse of this #belongs_to association. Does not work in @@ -1764,7 +1769,8 @@ module ActiveRecord # 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>. # [: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. diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index d64ab64c99..62e867a353 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -217,7 +217,8 @@ module ActiveRecord unless record.is_a?(reflection.klass) fresh_class = reflection.class_name.safe_constantize unless fresh_class && record.is_a?(fresh_class) - message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})" + 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 @@ -257,7 +258,7 @@ module ActiveRecord # Returns true if statement cache should be skipped on the association reader. def skip_statement_cache? - reflection.scope_chain.any?(&:any?) || + reflection.has_scope? || scope.eager_loading? || klass.scope_attributes? || reflection.source_reflection.active_record.default_scopes.any? diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 48437a1c9e..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 diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 41698c5360..24997370b2 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -61,6 +61,7 @@ module ActiveRecord 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 diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 346329c610..3121e70a04 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -33,6 +33,8 @@ module ActiveRecord::Associations::Builder # :nodoc: if (@_after_create_counter_called ||= false) @_after_create_counter_called = false + 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) diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index 56a8dc4e18..f25bd7ca9f 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -70,7 +70,11 @@ module ActiveRecord::Associations::Builder # :nodoc: 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 b888148841..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 @@ -76,6 +76,9 @@ module ActiveRecord::Associations::Builder # :nodoc: left_model.retrieve_connection end + def self.primary_key + false + end } join_model.name = "HABTM_#{association_name.to_s.camelize}" diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 473b80a658..0eaa0a4f36 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -10,9 +10,9 @@ module ActiveRecord # HasManyAssociation => has_many # HasManyThroughAssociation + ThroughAssociation => has_many :through # - # CollectionAssociation class provides common methods to the collections + # The CollectionAssociation class provides common methods to the collections # defined by +has_and_belongs_to_many+, +has_many+ or +has_many+ with - # +:through association+ option. + # the +:through association+ option. # # You need to be careful with assumptions regarding the target: The proxy # does not fetch records from the database until it needs them, but new @@ -72,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 @@ -133,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 @@ -235,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. @@ -269,7 +283,7 @@ module ActiveRecord _options = records.extract_options! dependent = _options[:dependent] || options[:dependent] - records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) } + records = find(records) if records.any? { |record| record.kind_of?(Integer) || record.kind_of?(String) } delete_or_destroy(records, dependent) end @@ -280,7 +294,7 @@ module ActiveRecord # +:dependent+ option. def destroy(*records) return if records.empty? - records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) } + records = find(records) if records.any? { |record| record.kind_of?(Integer) || record.kind_of?(String) } delete_or_destroy(records, :destroy) end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index fe693cfbb6..5d1e7ffb73 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -197,6 +197,16 @@ module ActiveRecord @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. @@ -587,7 +597,7 @@ module ActiveRecord # Pet.find(1) # # => ActiveRecord::RecordNotFound: Couldn't find Pet with 'id'=1 # - # You can pass +Fixnum+ or +String+ values, it finds the records + # You can pass +Integer+ or +String+ values, it finds the records # responding to the +id+ and executes delete on them. # # class Person < ActiveRecord::Base @@ -651,7 +661,7 @@ module ActiveRecord # # 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 + # You can pass +Integer+ or +String+ values, it finds the records # responding to the +id+ and then deletes them from the database. # # person.pets.size # => 3 @@ -705,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 # # => [ @@ -718,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, @@ -969,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. diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 0e4e951269..491152bbcb 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -92,8 +92,9 @@ module ActiveRecord # associations # => [:appointments] # joins # => [] # - def initialize(base, associations, joins) + def initialize(base, associations, joins, eager_loading: true) @alias_tracker = AliasTracker.create_with_joins(base.connection, base.table_name, joins, base.type_caster) + @eager_loading = eager_loading tree = self.class.make_tree associations @join_root = JoinBase.new base, build(tree, base) @join_root.children.each { |child| construct_tables! @join_root, child } @@ -228,7 +229,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) @@ -238,11 +239,12 @@ module ActiveRecord reflection.check_eager_loadable! if reflection.polymorphic? + next unless @eager_loading raise EagerLoadPolymorphicError.new(reflection) end JoinAssociation.new reflection, build(right, reflection.klass) - end + end.compact end def construct(ar_parent, parent, row, rs, seen, model_cache, aliases) 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 be65cf318c..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 @@ -75,7 +81,7 @@ module ActiveRecord column = klass.columns_hash[reflection.type.to_s] binds << Relation::QueryAttribute.new(column.name, value, klass.type_for_attribute(column.name)) - constraint = constraint.and table[reflection.type].eq(Arel::Nodes::BindParam.new) + 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 ecf6fb8643..e64af84e1a 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -184,6 +184,7 @@ module ActiveRecord 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 diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index e11a5cfb8a..3032bc786e 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -47,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 diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 6c83058202..b0203909ce 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -38,12 +38,7 @@ 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| @@ -53,13 +48,25 @@ module ActiveRecord 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?) diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index c7cc48ba16..f913f0852a 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -45,7 +45,7 @@ module ActiveRecord end def get_records - return scope.limit(1).to_a if skip_statement_cache? + 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/attribute.rb b/activerecord/lib/active_record/attribute.rb index 3c4c8f10ec..9530f134d0 100644 --- a/activerecord/lib/active_record/attribute.rb +++ b/activerecord/lib/active_record/attribute.rb @@ -77,7 +77,11 @@ module ActiveRecord end def with_type(type) - self.class.new(name, value_before_type_cast, type, original_attribute) + if changed_in_place? + with_value_from_user(value).with_type(type) + else + self.class.new(name, value_before_type_cast, type, original_attribute) + end end def type_cast(*) @@ -108,6 +112,22 @@ module ActiveRecord [self.class, name, value_before_type_cast, type].hash end + def init_with(coder) + @name = coder["name"] + @value_before_type_cast = coder["value_before_type_cast"] + @type = coder["type"] + @original_attribute = coder["original_attribute"] + @value = coder["value"] if coder.map.key?("value") + end + + def encode_with(coder) + coder["name"] = name + coder["value_before_type_cast"] = value_before_type_cast if value_before_type_cast + coder["type"] = type if type + coder["original_attribute"] = original_attribute if original_attribute + coder["value"] = value if defined?(@value) + end + protected attr_reader :original_attribute @@ -170,7 +190,7 @@ module ActiveRecord super(name, nil, Type::Value.new) end - def value + def type_cast(*) nil end @@ -185,6 +205,8 @@ module ActiveRecord end class Uninitialized < Attribute # :nodoc: + UNINITIALIZED_ORIGINAL_VALUE = Object.new + def initialize(name, type) super(name, nil, type) end @@ -195,12 +217,20 @@ module ActiveRecord end end + def original_value + UNINITIALIZED_ORIGINAL_VALUE + end + def value_for_database end def initialized? false end + + def with_type(type) + self.class.new(name, type) + end end private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue end diff --git a/activerecord/lib/active_record/attribute/user_provided_default.rb b/activerecord/lib/active_record/attribute/user_provided_default.rb index 6dbd92ce28..4580813364 100644 --- a/activerecord/lib/active_record/attribute/user_provided_default.rb +++ b/activerecord/lib/active_record/attribute/user_provided_default.rb @@ -4,20 +4,25 @@ 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 type_cast(value) - if value.is_a?(Proc) - super(value.call) + def value_before_type_cast + if user_provided_value.is_a?(Proc) + @memoized_value_before_type_cast ||= user_provided_value.call else - super + @user_provided_value end end def with_type(type) - self.class.new(name, value_before_type_cast, type, original_attribute) + 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 a6d81c82b4..b96d8e9352 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -29,14 +29,6 @@ module ActiveRecord assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty? end - # Tries to assign given value to given attribute. - # In case of an error, re-raises with the ActiveRecord constant. - def _assign_attribute(k, v) # :nodoc: - super - rescue ActiveModel::UnknownAttributeError - raise UnknownAttributeError.new(self, k) - end - # Assign any deferred nested attributes after the base attributes have been set. def assign_nested_parameter_attributes(pairs) pairs.each { |k, v| _assign_attribute(k, v) } @@ -46,7 +38,7 @@ module ActiveRecord # by calling new on the column type or aggregation type (through composed_of) object with these parameters. # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the - # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum and + # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Integer and # f for Float. If all the values for a given attribute are empty, the attribute will be set to +nil+. def assign_multiparameter_attributes(pairs) execute_callstack_for_multiparameter_attributes( diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 423a93964e..1fb5eb28cd 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -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 = Concurrent::Map.new - end - - def [](name) - @method_cache.compute_if_absent(name) do - safe_name = name.unpack('h*'.freeze).first - temp_method = "__temp__#{safe_name}" - ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name - @module.module_eval method_body(temp_method, safe_name), __FILE__, __LINE__ - @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 @@ -358,8 +334,6 @@ module ActiveRecord # # Note: +:id+ is always present. # - # Alias for the #read_attribute method. - # # class Person < ActiveRecord::Base # belongs_to :organization # end @@ -384,7 +358,7 @@ module ActiveRecord # person = Person.new # person[:age] = '22' # person[:age] # => 22 - # person[:age] # => Fixnum + # person[:age].class # => Integer def []=(attr_name, value) write_attribute(attr_name, value) end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 5197e21fa4..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,21 +24,6 @@ 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*'.freeze).first temp_method = "__temp__#{safe_name}" 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 061628725d..e160460286 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -20,7 +20,7 @@ module ActiveRecord nil end else - map(super) { |t| cast(t) } + map_avoiding_infinite_recursion(super) { |v| cast(v) } end end @@ -34,13 +34,23 @@ module ActiveRecord elsif value.is_a?(::Float) value else - map(value) { |v| convert_time_to_time_zone(v) } + 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 @@ -94,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 bbf2a51a0e..70c2d2f25d 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 @@ -39,7 +26,7 @@ module ActiveRecord end # Updates the attribute identified by <tt>attr_name</tt> with the - # specified +value+. Empty strings for fixnum and float columns are + # specified +value+. Empty strings for Integer and Float columns are # turned into +nil+. def write_attribute(attr_name, value) write_attribute_with_type_cast(attr_name, value, true) diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb index be581ac2a9..720d5f8b7c 100644 --- a/activerecord/lib/active_record/attribute_set.rb +++ b/activerecord/lib/active_record/attribute_set.rb @@ -1,7 +1,10 @@ require 'active_record/attribute_set/builder' +require 'active_record/attribute_set/yaml_encoder' module ActiveRecord class AttributeSet # :nodoc: + delegate :each_value, to: :attributes + def initialize(attributes) @attributes = attributes end diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb index 3bd7c7997b..24a255efc1 100644 --- a/activerecord/lib/active_record/attribute_set/builder.rb +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -22,7 +22,7 @@ module ActiveRecord end class LazyAttributeHash # :nodoc: - delegate :transform_values, :each_key, to: :materialize + delegate :transform_values, :each_key, :each_value, to: :materialize def initialize(types, values, additional_types) @types = types diff --git a/activerecord/lib/active_record/attribute_set/yaml_encoder.rb b/activerecord/lib/active_record/attribute_set/yaml_encoder.rb new file mode 100644 index 0000000000..6208048231 --- /dev/null +++ b/activerecord/lib/active_record/attribute_set/yaml_encoder.rb @@ -0,0 +1,39 @@ +module ActiveRecord + class AttributeSet + # Attempts to do more intelligent YAML dumping of an + # ActiveRecord::AttributeSet to reduce the size of the resulting string + class YAMLEncoder + def initialize(default_types) + @default_types = default_types + end + + def encode(attribute_set, coder) + coder['concise_attributes'] = attribute_set.each_value.map do |attr| + if attr.type.equal?(default_types[attr.name]) + attr.with_type(nil) + else + attr + end + end + end + + def decode(coder) + if coder['attributes'] + coder['attributes'] + else + attributes_hash = Hash[coder['concise_attributes'].map do |attr| + if attr.type.nil? + attr = attr.with_type(default_types[attr.name]) + end + [attr.name, attr] + end] + AttributeSet.new(attributes_hash) + end + end + + protected + + attr_reader :default_types + end + end +end diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index 5d0405c3be..519de271c3 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -67,12 +67,14 @@ 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 @@ -89,6 +91,7 @@ module ActiveRecord # # \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 @@ -119,7 +122,7 @@ module ActiveRecord # # class MoneyType < ActiveRecord::Type::Integer # def cast(value) - # if !value.kind_of(Numeric) && value.include?('$') + # if !value.kind_of?(Numeric) && value.include?('$') # price_in_dollars = value.gsub(/\$/, '').to_f # super(price_in_dollars * 100) # else @@ -131,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 @@ -154,7 +157,7 @@ module ActiveRecord # end # # class MoneyType < Type::Value - # def initialize(currency_converter) + # def initialize(currency_converter:) # @currency_converter = currency_converter # end # @@ -167,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")) diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index bac5a38a5d..06c7482bf9 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -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 # diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 4a31a1aa84..6a1a27ce41 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -13,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' @@ -132,9 +131,6 @@ module ActiveRecord #:nodoc: # end # end # - # You can alternatively use <tt>self[:attribute]=(value)</tt> and <tt>self[:attribute]</tt> - # or <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,7 +169,8 @@ module ActiveRecord #:nodoc: # 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 diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 854f9776a3..95de6937af 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -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 @@ -179,7 +179,7 @@ module ActiveRecord # # If the +before_validation+ callback throws +:abort+, the process will be # 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 a ActiveRecord::RecordInvalid exception. + # 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 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/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb index 5dcc98424a..8d41c0d799 100644 --- a/activerecord/lib/active_record/collection_cache_key.rb +++ b/activerecord/lib/active_record/collection_cache_key.rb @@ -16,7 +16,7 @@ module ActiveRecord query = collection .unscope(:select) - .select("COUNT(*) AS size", "MAX(#{column}) AS timestamp") + .select("COUNT(*) AS #{connection.quote_column_name("size")}", "MAX(#{column}) AS timestamp") .unscope(:order) result = connection.select_one(query) 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 ccd2899489..c341773be1 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -298,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 @@ -618,7 +618,7 @@ module ActiveRecord timeout_time = Time.now + (@checkout_timeout * 2) @available.with_a_bias_for(Thread.current) do - while true + loop do synchronize do return if collected_conns.size == @connections.size && @now_connecting == 0 remaining_timeout = timeout_time - Time.now @@ -778,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: # @@ -821,17 +820,16 @@ 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. + # 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 - @class_to_pool = Concurrent::Map.new(:initial_capacity => 2) do |h,k| - h[k] = Concurrent::Map.new - end end def connection_pool_list @@ -839,10 +837,12 @@ 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(config) + resolver = ConnectionSpecification::Resolver.new(Base.configurations) + spec = resolver.spec(config) + + remove_connection(spec.name) + owner_to_pool[spec.name] = ConnectionAdapters::ConnectionPool.new(spec) end # Returns true if there are any active connections among the connection @@ -873,18 +873,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 @@ -892,82 +892,43 @@ 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 end end - # Retrieving the connection pool happens a lot so we cache it in @class_to_pool. + # Retrieving the connection pool happens a lot, so we cache it in @owner_to_pool. # This makes retrieving the connection pool O(1) once the process is warm. # When a connection is established or removed, we invalidate the cache. - # - # Ideally we would use #fetch here, as class_to_pool[klass] may sometimes be nil. - # However, benchmarking (https://gist.github.com/jonleighton/3552829) showed that - # #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).tap do |pool| + establish_connection(ancestor_pool.spec.to_hash).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'] - - status, headers, body = @app.call(env) - proxy = ::Rack::BodyProxy.new(body) do - ActiveRecord::Base.clear_active_connections! unless testing - end - [status, headers, proxy] - 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_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 7a2a1a0e33..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,10 +27,10 @@ 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 sql = to_sql(arel, binds) - if arel.is_a?(String) + if !prepared_statements || (arel.is_a?(String) && preparable.nil?) preparable = false else preparable = visitor.preparable @@ -66,18 +66,23 @@ module ActiveRecord # Returns an array of arrays containing the field values. # Order is the same as that returned by +columns+. def select_rows(sql, name = nil, binds = []) + 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 = [], prepare: false) + raise NotImplementedError end # Executes insert +sql+ statement in the context of this connection using @@ -121,18 +126,21 @@ module ActiveRecord 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+ @@ -213,9 +221,7 @@ module ActiveRecord # * You are creating a nested (savepoint) transaction # # The 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. + # isolation level. def transaction(requires_new: nil, isolation: nil, joinable: true) if !requires_new && current_transaction.joinable? if isolation @@ -285,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 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 7e3760d34b..860ef17dca 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -82,7 +82,7 @@ module ActiveRecord # 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. @@ -146,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 @@ -166,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}'" @@ -181,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 0ba4d94e3c..6add697eeb 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -53,8 +53,8 @@ module ActiveRecord 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? - create_sql << "#{o.options}" + 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 @@ -82,6 +82,19 @@ module ActiveRecord "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? @@ -92,6 +105,7 @@ module ActiveRecord 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 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 690e0ba957..8dbafc5a4b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -3,14 +3,14 @@ module ActiveRecord # 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, :collation, :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 @@ -51,11 +51,12 @@ module ActiveRecord options[:primary_key] != default_primary_key end - def defined_for?(options_or_to_table = {}) - if options_or_to_table.is_a?(Hash) - options_or_to_table.all? {|key, value| options[key].to_s == value.to_s } + def defined_for?(to_table_ord = nil, to_table: nil, **options) + if to_table_ord + self.to_table == to_table_ord.to_s else - to_table == options_or_to_table.to_s + (to_table.nil? || to_table.to_s == self.to_table) && + options.all? { |k, v| self.options[k].to_s == v.to_s } end end @@ -182,6 +183,7 @@ module ActiveRecord end CODE end + alias_method :numeric, :decimal end # Represents the schema of an SQL table in an abstract way. This class @@ -206,17 +208,18 @@ module ActiveRecord include ColumnMethods attr_accessor :indexes - attr_reader :name, :temporary, :options, :as, :foreign_keys + attr_reader :name, :temporary, :options, :as, :foreign_keys, :comment - def initialize(name, temporary, options, as = nil) + def initialize(name, temporary = false, options = nil, as = nil, comment: nil) @columns_hash = {} @indexes = {} - @foreign_keys = {} + @foreign_keys = [] @primary_keys = nil @temporary = temporary @options = options @as = as @name = name + @comment = comment end def primary_keys(name = nil) # :nodoc: @@ -329,7 +332,10 @@ 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 @@ -372,6 +378,7 @@ module ActiveRecord 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 @@ -436,6 +443,7 @@ module ActiveRecord # t.bigint # t.float # t.decimal + # t.numeric # t.datetime # t.timestamp # t.time 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 a95109fdae..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,15 +7,16 @@ 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 an Adapter level basis to support other @@ -23,9 +24,6 @@ module ActiveRecord # PostgreSQL::ColumnDumper) def prepare_column_options(column) spec = {} - spec[:name] = column.name.inspect - spec[:type] = schema_type(column) - spec[:null] = 'false' unless column.null if limit = schema_limit(column) spec[:limit] = limit @@ -42,26 +40,38 @@ module ActiveRecord 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, :collation] + [: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 + limit = column.limit unless column.bigint? limit.inspect if limit && limit != native_database_types[column.type][:limit] 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 002f2ea8ce..eec0bc8518 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -18,6 +18,11 @@ module ActiveRecord 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('.', '_') @@ -92,7 +97,9 @@ module ActiveRecord # 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. # @@ -110,18 +117,25 @@ 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 @@ -245,8 +259,8 @@ 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 @@ -274,6 +288,14 @@ module ActiveRecord end end + 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 end @@ -320,12 +342,13 @@ 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 @@ -450,7 +473,7 @@ module ActiveRecord # The +type+ parameter is normally one of the migrations native types, # which is one of the following: # <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>, - # <tt>:integer</tt>, <tt>:bigint</tt>, <tt>:float</tt>, <tt>:decimal</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>. # @@ -468,9 +491,9 @@ module ActiveRecord # 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. + # Specifies the precision for the <tt>:decimal</tt> and <tt>:numeric</tt> columns. # * <tt>:scale</tt> - - # Specifies the scale for a <tt>:decimal</tt> column. + # 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 @@ -487,8 +510,6 @@ module ActiveRecord # 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]. @@ -691,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}" @@ -770,6 +791,7 @@ module ActiveRecord # 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>] @@ -789,6 +811,14 @@ module ActiveRecord # # 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) @@ -817,14 +847,19 @@ module ActiveRecord # # remove_reference(:products, :user, index: true, foreign_key: true) # - def remove_reference(table_name, ref_name, options = {}) - if options[:foreign_key] + def remove_reference(table_name, ref_name, foreign_key: false, polymorphic: false, **options) + if foreign_key reference_name = Base.pluralize_table_names ? ref_name.to_s.pluralize : ref_name - remove_foreign_key(table_name, reference_name) + if foreign_key.is_a?(Hash) + foreign_key_options = foreign_key + else + foreign_key_options = { to_table: reference_name } + end + remove_foreign_key(table_name, **foreign_key_options) end remove_column(table_name, "#{ref_name}_id") - remove_column(table_name, "#{ref_name}_type") if options[:polymorphic] + remove_column(table_name, "#{ref_name}_type") if polymorphic end alias :remove_belongs_to :remove_reference @@ -847,7 +882,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 # @@ -863,7 +898,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>] @@ -955,11 +990,23 @@ module ActiveRecord 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 - sql = "INSERT INTO #{sm_table} (version) VALUES " - sql << ActiveRecord::SchemaMigration.order('version').pluck(:version).map {|v| "('#{v}')" }.join(', ') - sql << ";\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. @@ -972,6 +1019,10 @@ module ActiveRecord 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 @@ -992,7 +1043,7 @@ module ActiveRecord if (duplicate = inserting.detect {|v| inserting.count(v) > 1}) raise "Duplicate migration #{duplicate}. Please renumber your migrations to resolve the conflict." end - execute "INSERT INTO #{sm_table} (version) VALUES #{inserting.map {|v| '(#{v})'}.join(', ') }" + execute insert_versions_sql(inserting) end end @@ -1040,9 +1091,9 @@ module ActiveRecord 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? @@ -1064,16 +1115,19 @@ module ActiveRecord Table.new(table_name, base) end - def add_index_options(table_name, column_name, options = {}) #:nodoc: - column_names = Array(column_name) + 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[:type].to_s if options.key?(:type) index_type ||= options[:unique] ? "UNIQUE" : "" index_name = options[:name].to_s if options.key?(:name) - index_name ||= index_name(table_name, column: column_names) - max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length + index_name ||= index_name(table_name, index_name_options(column_names)) if options.key?(:algorithm) algorithm = index_algorithms.fetch(options[:algorithm]) { @@ -1087,21 +1141,30 @@ module ActiveRecord index_options = options[:where] ? " WHERE #{options[:where]}" : "" end - 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 + validate_index_length!(table_name, index_name, options.fetch(:internal, false)) + if data_source_exists?(table_name) && index_name_exists?(table_name, index_name, false) 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 def add_index_sort_order(option_strings, column_names, options = {}) if options.is_a?(Hash) && order = options[:order] @@ -1118,6 +1181,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 @@ -1129,6 +1194,8 @@ module ActiveRecord end def index_name_for_remove(table_name, options = {}) + return options[:name] if can_remove_index_by_name?(options) + # if the adapter doesn't support the indexes call the best we can do # is return the default index name for the options provided return index_name(table_name, options) unless respond_to?(:indexes) @@ -1136,7 +1203,7 @@ module ActiveRecord checks = [] if options.is_a?(Hash) - checks << lambda { |i| i.name == options[:name].to_s } if options.has_key?(:name) + 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) @@ -1183,14 +1250,22 @@ module ActiveRecord end private - def create_table_definition(name, temporary = false, options = nil, as = nil) - TableDefinition.new(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) @@ -1199,8 +1274,10 @@ module ActiveRecord end end - def validate_index_length!(table_name, new_name) # :nodoc: - if new_name.length > allowed_index_name_length + def validate_index_length!(table_name, new_name, internal = false) # :nodoc: + max_index_length = internal ? index_name_length : allowed_index_name_length + + if new_name.length > max_index_length raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters" end end @@ -1212,6 +1289,10 @@ module ActiveRecord 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 14d04a6388..ca795cb1ad 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -167,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 @@ -183,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 @@ -209,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 d9b42d4283..d4b9e301bc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -1,5 +1,4 @@ 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' @@ -28,7 +27,6 @@ module ActiveRecord autoload_at 'active_record/connection_adapters/abstract/connection_pool' do autoload :ConnectionHandler - autoload :ConnectionManagement end autoload_under 'abstract' do @@ -69,6 +67,7 @@ module ActiveRecord include QueryCache include ActiveSupport::Callbacks include ColumnDumper + include Savepoints SIMPLE_INT = /\A\d+\z/ @@ -106,8 +105,15 @@ module ActiveRecord @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 @@ -143,8 +149,12 @@ module ActiveRecord end end + def arel_visitor # :nodoc: + Arel::Visitors::ToSql.new(self) + end + def valid_type?(type) - true + false end def schema_creation @@ -238,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 @@ -279,6 +294,21 @@ module ActiveRecord 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 @@ -380,12 +410,6 @@ module ActiveRecord @connection end - def create_savepoint(name = nil) - end - - def release_savepoint(name = nil) - end - def case_sensitive_comparison(table, attribute, column, value) if value.nil? table[attribute].eq(value) @@ -398,7 +422,7 @@ module ActiveRecord if can_perform_case_insensitive_comparison_for?(column) 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 @@ -407,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 @@ -422,8 +442,8 @@ module ActiveRecord end end - def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) - Column.new(name, default, sql_type_metadata, null, default_function, collation) + 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: @@ -434,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: 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 3e84786be0..718a6c5b91 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -1,5 +1,8 @@ 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' @@ -10,17 +13,21 @@ require 'active_support/core_ext/string/strip' module ActiveRecord module ConnectionAdapters class AbstractMysqlAdapter < AbstractAdapter + include MySQL::Quoting include MySQL::ColumnDumper - include Savepoints def update_table_definition(table_name, base) # :nodoc: MySQL::Table.new(table_name, base) end - def schema_creation + def schema_creation # :nodoc: MySQL::SchemaCreation.new(self) end + def arel_visitor # :nodoc: + Arel::Visitors::MySQL.new(self) + end + ## # :singleton-method: # By default, the Mysql2Adapter will consider all columns of type <tt>tinyint(1)</tt> @@ -31,25 +38,17 @@ module ActiveRecord 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 auto_increment PRIMARY KEY", string: { name: "varchar", limit: 255 }, - text: { name: "text" }, + text: { name: "text", limit: 65535 }, integer: { name: "int", limit: 4 }, float: { name: "float" }, decimal: { name: "decimal" }, datetime: { name: "datetime" }, time: { name: "time" }, date: { name: "date" }, - binary: { name: "blob" }, + binary: { name: "blob", limit: 65535 }, boolean: { name: "tinyint", limit: 1 }, json: { name: "json" }, } @@ -57,35 +56,38 @@ module ActiveRecord 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, config) - @quoted_column_names, @quoted_table_names = {}, {} - @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 - @visitor.extend(DetermineIfPreparableVisitor) - 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 + 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. def supports_migrations? true @@ -99,18 +101,20 @@ 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 >= '5.0.0' + true end def supports_explain? @@ -126,25 +130,27 @@ module ActiveRecord end def supports_views? - version >= '5.0.0' + true end def supports_datetime_with_precision? - version >= '5.6.4' + if mariadb? + version >= '5.3.0' + else + version >= '5.6.4' + end end - # 5.0.0 definitely supports it, possibly supported by earlier versions but - # not sure def supports_advisory_locks? - version >= '5.0.0' + true end def get_advisory_lock(lock_name, timeout = 0) # :nodoc: - select_value("SELECT GET_LOCK('#{lock_name}', #{timeout});").to_s == '1' + select_value("SELECT GET_LOCK(#{quote(lock_name)}, #{timeout})") == 1 end def release_advisory_lock(lock_name) # :nodoc: - select_value("SELECT RELEASE_LOCK('#{lock_name}')").to_s == '1' + select_value("SELECT RELEASE_LOCK(#{quote(lock_name)})") == 1 end def native_database_types @@ -163,8 +169,8 @@ module ActiveRecord raise NotImplementedError end - def new_column(field, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc: - MySQL::Column.new(field, default, sql_type_metadata, null, default_function, collation) + def new_column(*args) #:nodoc: + MySQL::Column.new(*args) end # Must return the MySQL error number from the exception, if the exception has an @@ -173,48 +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 - - def quoted_date(value) - if supports_datetime_with_precision? - super - else - super.sub(/\.\d{6}\z/, '') - end - end - # REFERENTIAL INTEGRITY ==================================== def disable_referential_integrity #:nodoc: @@ -228,6 +192,14 @@ module ActiveRecord end end + # CONNECTION MANAGEMENT ==================================== + + # Clears the prepared statements cache. + def clear_cache! + reload_type_map + @statements.clear + end + #-- # DATABASE STATEMENTS ====================================== #++ @@ -238,77 +210,7 @@ module ActiveRecord result = exec_query(sql, 'EXPLAIN', binds) elapsed = Time.now - start - ExplainPrettyPrinter.new.pp(result, elapsed) - end - - 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 - - def clear_cache! - super - reload_type_map + MySQL::ExplainPrettyPrinter.new.pp(result, elapsed) end # Executes the SQL statement in the context of this connection. @@ -376,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 @@ -387,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 @@ -445,8 +347,7 @@ module ActiveRecord def data_source_exists?(table_name) return false unless table_name.present? - schema, name = table_name.to_s.split('.', 2) - schema, name = @config[:database], schema unless name # A table was provided without a schema + 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)}" @@ -461,8 +362,7 @@ module ActiveRecord def view_exists?(view_name) # :nodoc: return false unless view_name.present? - schema, name = view_name.to_s.split('.', 2) - schema, name = @config[:database], schema unless name # A view was provided without a schema + schema, name = extract_schema_qualified_name(view_name) sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'VIEW'" sql << " AND table_schema = #{quote(schema)} AND table_name = #{quote(name)}" @@ -483,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] @@ -496,21 +396,28 @@ module ActiveRecord # 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| - type_metadata = fetch_type_metadata(field[:Type], field[:Extra]) - if type_metadata.type == :datetime && field[:Default] == "CURRENT_TIMESTAMP" - new_column(field[:Field], nil, type_metadata, field[:Null] == "YES", field[:Default], field[:Collation]) - else - new_column(field[:Field], field[:Default], type_metadata, field[:Null] == "YES", nil, field[:Collation]) - end + 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: @@ -593,11 +500,21 @@ module ActiveRecord end def add_index(table_name, column_name, options = {}) #:nodoc: - index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options) - execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}" + 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' @@ -605,8 +522,8 @@ 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 = create_table_info(table_name) @@ -632,7 +549,12 @@ module ActiveRecord 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') + 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. @@ -660,8 +582,7 @@ module ActiveRecord # SHOW VARIABLES LIKE 'name' def show_variable(name) - variables = select_all("select @@#{name} as 'Value'", 'SCHEMA') - variables.first['Value'] unless variables.empty? + select_value("SELECT @@#{name}", 'SCHEMA') rescue ActiveRecord::StatementInvalid nil end @@ -669,8 +590,7 @@ module ActiveRecord def primary_keys(table_name) # :nodoc: raise ArgumentError unless table_name.present? - schema, name = table_name.to_s.split('.', 2) - schema, name = @config[:database], schema unless name # A table was provided without a schema + schema, name = extract_schema_qualified_name(table_name) select_values(<<-SQL.strip_heredoc, 'SCHEMA') SELECT column_name @@ -683,20 +603,17 @@ module ActiveRecord end def case_sensitive_comparison(table, attribute, column, value) - if value.nil? || column.case_sensitive? - super - else + 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(Arel::Nodes::BindParam.new) - 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 @@ -746,7 +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.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' @@ -790,7 +707,7 @@ module ActiveRecord case length when Hash column_names.each {|name| option_strings[name] += "(#{length[name]})" if length.has_key?(name) && length[name].present?} - when Fixnum + when Integer column_names.each {|name| option_strings[name] += "(#{length})"} end end @@ -810,12 +727,22 @@ module ActiveRecord column_names.map {|name| quote_column_name(name) + option_strings[name]} end + # See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html + ER_DUP_ENTRY = 1062 + ER_NO_REFERENCED_ROW_2 = 1452 + ER_DATA_TOO_LONG = 1406 + ER_LOCK_DEADLOCK = 1213 + def translate_exception(exception, message) case error_number(exception) - when 1062 + when ER_DUP_ENTRY RecordNotUnique.new(message) - when 1452 + when ER_NO_REFERENCED_ROW_2 InvalidForeignKey.new(message) + when ER_DATA_TOO_LONG + ValueTooLong.new(message) + when ER_LOCK_DEADLOCK + TransactionSerializationError.new(message) else super end @@ -901,10 +828,6 @@ module ActiveRecord subselect.from subsubselect.as('__active_record_temp') end - def mariadb? - full_version =~ /mariadb/i - end - def supports_rename_index? mariadb? ? false : version >= '5.7.6' end @@ -917,7 +840,7 @@ module ActiveRecord # Increase timeout so the server doesn't disconnect us. wait_timeout = @config[:wait_timeout] - wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum) + wait_timeout = 2147483 unless wait_timeout.is_a?(Integer) variables['wait_timeout'] = self.class.type_cast_config_to_integer(wait_timeout) defaults = [':default', :default].to_set @@ -925,9 +848,19 @@ module ActiveRecord # Make MySQL reject illegal values rather than truncating or blanking them, see # 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') || defaults.include?(@config[:strict]) - 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.7/en/set-statement.html#id944430 @@ -949,7 +882,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: @@ -969,8 +908,14 @@ module ActiveRecord create_table_info_cache[table_name] ||= select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] end - def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: - MySQL::TableDefinition.new(name, temporary, options, as) + 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: @@ -980,7 +925,6 @@ module ActiveRecord when 3; 'mediumint' when nil, 4; 'int' when 5..8; 'bigint' - when 11; 'int(11)' # backward compatibility with Rails 2.0 else raise(ActiveRecordError, "No integer type has byte size #{limit}") end end @@ -1016,8 +960,8 @@ module ActiveRecord 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 @@ -1026,8 +970,8 @@ 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 diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 81de7c03fb..28f0c8686a 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -1,11 +1,9 @@ -require 'set' - module ActiveRecord # :stopdoc: module ConnectionAdapters # An abstract definition of a column in a table. class Column - attr_reader :name, :null, :sql_type_metadata, :default, :default_function, :collation + 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 @@ -15,14 +13,15 @@ module ActiveRecord # +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, collation = nil) + 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 - @table_name = nil + @comment = comment end def has_default? @@ -30,7 +29,7 @@ module ActiveRecord end def bigint? - /bigint/ === sql_type + /\Abigint\b/ === sql_type end # Returns the human name of the column name. @@ -54,7 +53,7 @@ module ActiveRecord protected def attributes_for_hash - [self.class, name, default, sql_type_metadata, null, default_function, collation] + [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 f633892dee..346916337e 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -3,16 +3,20 @@ 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) @config = original.config.dup end + def to_hash + @config.merge(name: @name) + end + # Expands a connection string into a hash. class ConnectionUrlResolver # :nodoc: @@ -33,7 +37,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 @@ -179,7 +183,12 @@ module ActiveRecord end adapter_method = "#{spec[:adapter]}_connection" - ConnectionSpecification.new(spec, adapter_method) + + unless ActiveRecord::Base.respond_to?(adapter_method) + raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter" + end + + ConnectionSpecification.new(spec.delete(:name) || "primary", spec, adapter_method) end private @@ -224,7 +233,7 @@ module ActiveRecord # def resolve_symbol_connection(spec) if config = configurations[spec.to_s] - resolve_connection(config) + resolve_connection(config).merge("name" => spec.to_s) else raise(AdapterNotSpecified, "'#{spec}' database is not configured. Available: #{configurations.keys.inspect}") 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 index 1e2c859af9..fd2dc2aee8 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb @@ -2,6 +2,9 @@ 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) @@ -22,6 +25,10 @@ module ActiveRecord 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 @@ -29,13 +36,15 @@ module ActiveRecord end def add_column_options!(sql, options) - if options[:charset] - sql << " CHARACTER SET #{options[:charset]}" + if charset = options[:charset] + sql << " CHARACTER SET #{charset}" end - if options[:collation] - sql << " COLLATE #{options[:collation]}" + + if collation = options[:collation] + sql << " COLLATE #{collation}" end - super + + add_sql_comment!(super, options[:comment]) end def add_column_position!(sql, options) @@ -44,12 +53,13 @@ module ActiveRecord 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 = @conn.add_index_options(table_name, column_name, options) - "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns}) " + 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 diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb index 9dee3172f4..2ba9657f24 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb @@ -3,18 +3,13 @@ module ActiveRecord module MySQL module ColumnDumper def column_spec_for_primary_key(column) - spec = {} if column.bigint? - spec[:id] = ':bigint' + spec = { id: :bigint.inspect } spec[:default] = schema_default(column) || 'nil' unless column.auto_increment? - spec[:unsigned] = 'true' if column.unsigned? - elsif column.auto_increment? - spec[:unsigned] = 'true' if column.unsigned? - return if spec.empty? else - spec[:id] = column.type.inspect - spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) + spec = super end + spec[:unsigned] = 'true' if column.unsigned? spec end @@ -30,24 +25,24 @@ module ActiveRecord private + def default_primary_key?(column) + super && column.auto_increment? + end + def schema_type(column) if column.sql_type == 'tinyblob' - 'blob' + :blob else super end end - def schema_limit(column) - super unless column.type == :boolean - end - def schema_precision(column) super unless /time/ === column.sql_type && column.precision == 0 end def schema_collation(column) - if column.collation && table_name = column.instance_variable_get(:@table_name) + 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] diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index c3c5b660fd..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.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: @@ -16,7 +18,7 @@ module ActiveRecord if config[:flags].kind_of? Array config[:flags].push "FOUND_ROWS".freeze else - config[:flags] |= Mysql2::Client::FOUND_ROWS + config[:flags] |= Mysql2::Client::FOUND_ROWS end end @@ -35,14 +37,28 @@ 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_json? - version >= '5.7.8' + !mariadb? && version >= '5.7.8' + end + + def supports_comments? + true + end + + def supports_comments_in_create? + true + end + + def supports_savepoints? + true end # HELPER METHODS =========================================== @@ -95,61 +111,6 @@ module ActiveRecord end end - #-- - # DATABASE STATEMENTS ====================================== - #++ - - # 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) - execute(to_sql(arel, binds), name).each(as: :hash) do |row| - @connection.next_result while @connection.more_results? - return row - end - 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 = []) - result = execute(sql, name) - @connection.next_result while @connection.more_results? - result.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 = [], prepare: false) - result = execute(sql, name) - @connection.next_result while @connection.more_results? - ActiveRecord::Result.new(result.fields, result.to_a) - end - - alias exec_without_stmt exec_query - - 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 diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb index bfa03fa136..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,6 @@ module ActiveRecord def serial? return unless default_function - table_name = @table_name || '(?<table_name>.+)' %r{\Anextval\('"?#{table_name}_#{name}_seq"?'::regclass\)\z} === default_function 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 6c15facf3b..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 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 + PostgreSQL::ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) end def select_value(arel, name = nil, binds = []) @@ -128,6 +91,8 @@ 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) @@ -153,7 +118,7 @@ module ActiveRecord alias :exec_update :exec_delete def sql_for_insert(sql, pk, id_value, sequence_name, binds) # :nodoc: - unless pk + 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 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/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/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 index 7427a25ad5..4da240edb2 100644 --- 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 @@ -14,6 +14,8 @@ module ActiveRecord def cast(value) case value when ::String + return if value.blank? + if value[0] == '(' && value[-1] == ')' value = value[1...-1] end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index c1c77a967e..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,8 @@ 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. @@ -41,8 +41,8 @@ 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. diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb index cc7721ddd8..a1e10fd364 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb @@ -3,16 +3,9 @@ module ActiveRecord module PostgreSQL module ColumnDumper 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] = schema_default(column) || 'nil' - else - spec[:id] = column.type.inspect - spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) + spec = super + if schema_type(column) == :uuid + spec[:default] ||= 'nil' end spec end @@ -31,13 +24,17 @@ module ActiveRecord private + def default_primary_key?(column) + schema_type(column) == :serial + end + def schema_type(column) return super unless column.serial? if column.bigint? - 'bigserial' + :bigserial else - 'serial' + :serial 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 67e727d8ed..f6860b9aba 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 @@ -172,7 +174,11 @@ module ActiveRecord 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 + 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 @@ -190,43 +196,61 @@ module ActiveRecord 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, collation| + 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, default_function, collation) + 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, collation = nil) # :nodoc: - PostgreSQLColumn.new(name, default, sql_type_metadata, null, default_function, collation) + 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. @@ -325,7 +349,7 @@ module ActiveRecord 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 @@ -340,7 +364,7 @@ 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 @@ -445,6 +469,7 @@ module ActiveRecord 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 def change_column(table_name, column_name, type, options = {}) #:nodoc: @@ -466,6 +491,7 @@ 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. @@ -494,6 +520,18 @@ 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) #:nodoc: clear_cache! @@ -502,8 +540,10 @@ 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 #{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, options = {}) #:nodoc: @@ -601,7 +641,7 @@ module ActiveRecord when 1, 2; 'smallint' when nil, 3, 4; 'integer' when 5..8; 'bigint' - else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.") + else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with scale 0 instead.") end else super(type, limit, precision, scale) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 2de6fbfaf0..ddfc560747 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -5,6 +5,7 @@ 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" @@ -15,8 +16,6 @@ require "active_record/connection_adapters/postgresql/type_metadata" require "active_record/connection_adapters/postgresql/utils" require "active_record/connection_adapters/statement_pool" -require 'ipaddr' - module ActiveRecord module ConnectionHandling # :nodoc: # Establishes a connection to the database that's used by all Active Record objects @@ -118,12 +117,15 @@ module ActiveRecord include PostgreSQL::SchemaStatements include PostgreSQL::DatabaseStatements include PostgreSQL::ColumnDumper - include Savepoints def schema_creation # :nodoc: PostgreSQL::SchemaCreation.new self end + def arel_visitor # :nodoc: + Arel::Visitors::PostgreSQL.new(self) + end + # Returns true, since this connection adapter supports prepared statement # caching. def supports_statement_cache? @@ -138,6 +140,10 @@ module ActiveRecord true end + def supports_expression_index? + true + end + def supports_transaction_isolation? true end @@ -158,6 +164,14 @@ module ActiveRecord postgresql_version >= 90200 end + def supports_comments? + true + end + + def supports_savepoints? + true + end + def index_algorithms { concurrently: 'CONCURRENTLY' } end @@ -194,14 +208,6 @@ module ActiveRecord def initialize(connection, logger, connection_parameters, config) 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 - @visitor.extend(DetermineIfPreparableVisitor) - else - @prepared_statements = false - end - @connection_parameters = connection_parameters # @local_tz is initialized as nil to avoid warnings when connect tries to use it @@ -211,10 +217,10 @@ module ActiveRecord connect 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 @@ -296,9 +302,8 @@ module ActiveRecord 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 @@ -398,8 +403,10 @@ module ActiveRecord protected # See http://www.postgresql.org/docs/current/static/errcodes-appendix.html + VALUE_LIMIT_VIOLATION = "22001" FOREIGN_KEY_VIOLATION = "23503" UNIQUE_VIOLATION = "23505" + SERIALIZATION_FAILURE = "40001" def translate_exception(exception, message) return exception unless exception.respond_to?(:result) @@ -409,6 +416,10 @@ module ActiveRecord RecordNotUnique.new(message) when FOREIGN_KEY_VIOLATION InvalidForeignKey.new(message) + when VALUE_LIMIT_VIOLATION + ValueTooLong.new(message) + when SERIALIZATION_FAILURE + TransactionSerializationError.new(message) else super end @@ -598,25 +609,41 @@ module ActiveRecord @connection.exec_prepared(stmt_key, type_casted_binds) end rescue ActiveRecord::StatementInvalid => e - pgerror = e.cause + raise unless is_cached_plan_failure?(e) - # 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 + # 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) @@ -645,12 +672,6 @@ 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") @@ -702,7 +723,7 @@ module ActiveRecord # 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 @@ -722,7 +743,8 @@ module ActiveRecord SELECT a.attname, format_type(a.atttypid, a.atttypmod), pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod, (SELECT c.collname FROM pg_collation c, pg_type t - WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation) + 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 @@ -736,8 +758,8 @@ module ActiveRecord $1.strip if $1 end - def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: - PostgreSQL::TableDefinition.new(name, temporary, options, as) + def create_table_definition(*args) # :nodoc: + PostgreSQL::TableDefinition.new(*args) end def can_perform_case_insensitive_comparison_for?(column) @@ -805,7 +827,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) 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 index fe1dcbd710..70c0d28830 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb @@ -3,6 +3,13 @@ module ActiveRecord 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]}\"" diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index d1893f35f5..eb2268157b 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -1,5 +1,7 @@ require 'active_record/connection_adapters/abstract_adapter' require 'active_record/connection_adapters/statement_pool' +require 'active_record/connection_adapters/sqlite3/explain_pretty_printer' +require 'active_record/connection_adapters/sqlite3/quoting' require 'active_record/connection_adapters/sqlite3/schema_creation' gem 'sqlite3', '~> 1.3.6' @@ -7,7 +9,6 @@ require 'sqlite3' module ActiveRecord module ConnectionHandling # :nodoc: - # sqlite3 adapter reuses sqlite_connection. def sqlite3_connection(config) # Require database. unless config[:database] @@ -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', @@ -77,21 +79,15 @@ module ActiveRecord SQLite3::SchemaCreation.new self end + def arel_visitor # :nodoc: + Arel::Visitors::SQLite.new(self) + end + def initialize(connection, logger, connection_options, config) super(connection, logger, config) @active = nil - @statements = StatementPool.new(self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) - - @visitor = Arel::Visitors::SQLite.new self - @quoted_column_names = {} - - if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) - @prepared_statements = true - @visitor.extend(DetermineIfPreparableVisitor) - else - @prepared_statements = false - end + @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) end def supports_ddl_transactions? @@ -133,6 +129,10 @@ module ActiveRecord true end + def supports_multi_insert? + sqlite_version >= '3.7.11' + end + def active? @active != false end @@ -154,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 @@ -174,65 +178,13 @@ 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 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) # :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 = [], prepare: false) @@ -280,10 +232,6 @@ module ActiveRecord log(sql, name) { @connection.execute(sql) } end - 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 @@ -351,7 +299,8 @@ module ActiveRecord 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 @@ -365,7 +314,7 @@ module ActiveRecord 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, nil, collation) + new_column(field['name'], field['dflt_value'], type_metadata, field['notnull'].to_i == 0, table_name, nil, collation) end end @@ -592,7 +541,7 @@ module ActiveRecord result = exec_query(sql, 'SCHEMA').first if result - # Splitting with left parantheses and picking up last will return all + # Splitting with left parentheses and picking up last will return all # columns separated with comma(,). columns_string = result["sql"].split('(').last diff --git a/activerecord/lib/active_record/connection_adapters/statement_pool.rb b/activerecord/lib/active_record/connection_adapters/statement_pool.rb index 57463dd749..9b0ed3e08b 100644 --- a/activerecord/lib/active_record/connection_adapters/statement_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/statement_pool.rb @@ -1,11 +1,13 @@ module ActiveRecord module ConnectionAdapters - class StatementPool + class StatementPool # :nodoc: include Enumerable - def initialize(max = 1000) + DEFAULT_STATEMENT_LIMIT = 1000 + + def initialize(statement_limit = nil) @cache = Hash.new { |h,pid| h[pid] = {} } - @max = max + @statement_limit = statement_limit || DEFAULT_STATEMENT_LIMIT end def each(&block) @@ -25,7 +27,7 @@ module ActiveRecord end def []=(sql, stmt) - while @max <= cache.size + while @statement_limit <= cache.size dealloc(cache.shift.last) end cache[sql] = stmt diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index a8b3d03ba5..086c678af5 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -44,17 +44,18 @@ module ActiveRecord # # The exceptions AdapterNotSpecified, AdapterNotFound and +ArgumentError+ # may be returned on an error. - def establish_connection(spec = nil) - spec ||= DEFAULT_ENV.call.to_sym - resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new configurations - spec = resolver.spec(spec) + def establish_connection(config = nil) + raise "Anonymous class is not allowed." unless name - unless respond_to?(spec.adapter_method) - raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter" - end + config ||= DEFAULT_ENV.call.to_sym + spec_name = self == Base ? "primary" : name + self.connection_specification_name = spec_name + + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(Base.configurations) + spec = resolver.resolve(config).symbolize_keys + spec[:name] = spec_name - remove_connection - connection_handler.establish_connection self, spec + connection_handler.establish_connection(spec) end class MergeAndResolveDefaultUrlConfig # :nodoc: @@ -87,6 +88,16 @@ 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 ||= Thread.current.object_id end @@ -106,20 +117,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 475a298467..de337b24d6 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 @@ -128,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: @@ -151,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 @@ -249,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 @@ -267,8 +280,9 @@ 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 @@ -324,7 +338,7 @@ module ActiveRecord # post.title # => 'hello world' def init_with(coder) coder = LegacyYamlAdapter.convert(self.class, coder) - @attributes = coder['attributes'] + @attributes = self.class.yaml_encoder.decode(coder) init_internals @@ -390,11 +404,9 @@ module ActiveRecord # Post.new.encode_with(coder) # coder # => {"attributes" => {"id" => nil, ... }} def encode_with(coder) - # FIXME: Remove this when we better serialize attributes - coder['raw_attributes'] = attributes_before_type_cast - coder['attributes'] = @attributes + self.class.yaml_encoder.encode(@attributes, coder) coder['new_record'] = new_record? - coder['active_record_yaml_version'] = 1 + coder['active_record_yaml_version'] = 2 end # Returns true if +comparison_object+ is the same exact object, or +comparison_object+ @@ -418,7 +430,7 @@ module ActiveRecord # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ] def hash if id - id.hash + [self.class, id].hash else super end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 903c63a7db..7be332fb97 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -152,7 +152,7 @@ module ActiveRecord 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 } diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index e5906b6756..38e4fbec8b 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -125,6 +125,10 @@ 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 {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#find] method) # does not match number of expected values supplied. @@ -139,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. @@ -275,4 +284,19 @@ module ActiveRecord # The mysql2 and postgresql adapters support setting the transaction isolation level. class TransactionIsolationError < ActiveRecordError end + + # TransactionSerializationError will be raised when a transaction is rolled + # back by the database due to a serialization failure or a deadlock. + # + # See the following: + # + # * http://www.postgresql.org/docs/current/static/transaction-iso.html + # * https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html#error_er_lock_deadlock + class TransactionSerializationError < 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/fixture_set/file.rb b/activerecord/lib/active_record/fixture_set/file.rb index f969556c50..e4a44244e2 100644 --- a/activerecord/lib/active_record/fixture_set/file.rb +++ b/activerecord/lib/active_record/fixture_set/file.rb @@ -52,9 +52,15 @@ module ActiveRecord end 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 ed1bbf5dcd..51bf12d0bf 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -66,7 +66,7 @@ module ActiveRecord # By default, +test_helper.rb+ will load all of your fixtures into your test # database, so this test will succeed. # - # The testing environment will automatically load the all fixtures into the database before each + # The testing environment will automatically load all the fixtures into the database before each # test. To ensure consistent data, the environment deletes the fixtures before running the load. # # In addition to being available in the database, the fixture's data may also be accessed by diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb index b4f2f66e1c..f33456a744 100644 --- a/activerecord/lib/active_record/gem_version.rb +++ b/activerecord/lib/active_record/gem_version.rb @@ -6,9 +6,9 @@ module ActiveRecord module VERSION MAJOR = 5 - MINOR = 0 + MINOR = 1 TINY = 0 - PRE = "beta1.1" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 3b6fb70d0d..899683ee4f 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -192,7 +192,7 @@ module ActiveRecord 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) diff --git a/activerecord/lib/active_record/internal_metadata.rb b/activerecord/lib/active_record/internal_metadata.rb index 10fee4dca2..17a5dc1d1b 100644 --- a/activerecord/lib/active_record/internal_metadata.rb +++ b/activerecord/lib/active_record/internal_metadata.rb @@ -5,10 +5,6 @@ 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: - # Keys in mysql are limited to 191 characters, due to this no adapter can - # use a longer key - KEY_LIMIT = 191 - class << self def primary_key "key" @@ -18,12 +14,12 @@ module ActiveRecord "#{table_name_prefix}#{ActiveRecord::Base.internal_metadata_table_name}#{table_name_suffix}" end - def index_name - "#{table_name_prefix}unique_#{ActiveRecord::Base.internal_metadata_table_name}#{table_name_suffix}" + 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) + find_or_initialize_by(key: key).update_attributes!(value: value) end def [](key) @@ -34,14 +30,23 @@ module ActiveRecord 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? - connection.create_table(table_name, id: false) do |t| - t.column :key, :string, null: false, limit: KEY_LIMIT - t.column :value, :string - t.index :key, unique: true, name: index_name + 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 diff --git a/activerecord/lib/active_record/legacy_yaml_adapter.rb b/activerecord/lib/active_record/legacy_yaml_adapter.rb index 89dee58423..c7683f68c7 100644 --- a/activerecord/lib/active_record/legacy_yaml_adapter.rb +++ b/activerecord/lib/active_record/legacy_yaml_adapter.rb @@ -4,7 +4,7 @@ module ActiveRecord return coder unless coder.is_a?(Psych::Coder) case coder["active_record_yaml_version"] - when 1 then coder + when 1, 2 then coder else if coder["attributes"].is_a?(AttributeSet) Rails420.convert(klass, coder) diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 2336d23a1c..67b8efac66 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -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/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index b63caa4473..8e32af1c49 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -22,7 +22,11 @@ module ActiveRecord def render_bind(attribute) value = if attribute.type.binary? && attribute.value - "<#{attribute.value.bytesize} bytes of binary data>" + if attribute.value.is_a?(Hash) + "<#{attribute.value_for_database.to_s.bytesize} bytes of binary data>" + else + "<#{attribute.value.bytesize} bytes of binary data>" + end else attribute.value_for_database end @@ -67,7 +71,7 @@ module ActiveRecord case sql when /\A\s*rollback/mi RED - when /\s*.*?select .*for update/mi, /\A\s*lock/mi + when /select .*for update/mi, /\A\s*lock/mi WHITE when /\A\s*select/i BLUE diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 4419a7b1e7..81fe053fe1 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -156,8 +156,8 @@ module ActiveRecord 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 = "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 @@ -166,13 +166,13 @@ module ActiveRecord 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 << "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("#{msg} RAILS_ENV=#{::Rails.env}") + super("#{msg} RAILS_ENV=#{::Rails.env}\n\n") else - super(msg) + super("#{msg}\n\n") end end end @@ -277,7 +277,7 @@ module ActiveRecord # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes # the column to a different type using the same parameters as add_column. # * <tt>change_column_default(table_name, column_name, default)</tt>: Sets a - # default value for +column_name+ definded by +default+ on +table_name+. + # 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 @@ -524,23 +524,17 @@ module ActiveRecord end def self.[](version) - version = version.to_s - name = "V#{version.tr('.', '_')}" - unless Compatibility.const_defined?(name) - versions = Compatibility.constants.grep(/\AV[0-9_]+\z/).map { |s| s.to_s.delete('V').tr('_', '.').inspect } - raise "Unknown migration version #{version.inspect}; expected one of #{versions.sort.join(', ')}" - end - Compatibility.const_get(name) + Compatibility.find(version) end def self.current_version - Rails.version.to_f + 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 diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb index 5d742b523b..74833f938f 100644 --- a/activerecord/lib/active_record/migration/compatibility.rb +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -1,7 +1,17 @@ module ActiveRecord class Migration module Compatibility # :nodoc: all - V5_0 = Current + 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 @@ -30,6 +40,19 @@ module ActiveRecord 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 @@ -44,7 +67,7 @@ module ActiveRecord def index_exists?(table_name, column_name, options = {}) column_names = Array(column_name).map(&:to_s) options[:name] = - if options.key?(:name).present? + if options[:name].present? options[:name].to_s else index_name(table_name, column: column_names) @@ -79,6 +102,9 @@ module ActiveRecord 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 @@ -89,7 +115,7 @@ module ActiveRecord module Legacy include FourTwoShared - def run(*) + def migrate(*) ActiveSupport::Deprecation.warn \ "Directly inheriting from ActiveRecord::Migration is deprecated. " \ "Please specify the Rails release the migration was written for:\n" \ diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 722d7b5fce..7996c32bbc 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -44,9 +44,9 @@ module ActiveRecord ## # :singleton-method: - # Accessor for the name of the internal metadata table. By default, the value is "active_record_internal_metadatas" + # 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 = "active_record_internal_metadatas" + self.internal_metadata_table_name = "ar_internal_metadata" ## # :singleton-method: @@ -231,6 +231,18 @@ 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.data_source_exists?(table_name) @@ -255,6 +267,10 @@ module ActiveRecord @attribute_types ||= Hash.new(Type::Value.new) end + def yaml_encoder # :nodoc: + @yaml_encoder ||= AttributeSet::YAMLEncoder.new(attribute_types) + end + # 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 @@ -363,6 +379,7 @@ module ActiveRecord @columns = nil @columns_hash = nil @attribute_names = nil + @yaml_encoder = nil direct_descendants.each do |descendant| descendant.send(:reload_schema_from_cache) end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 0d5a8e6f25..fe68869143 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -195,19 +195,27 @@ 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. # diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 0b500346bc..1ab4e0404f 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -1,7 +1,7 @@ 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 4d661735cc..afed5e5e85 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -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) @@ -106,7 +106,7 @@ module ActiveRecord # the existing record gets updated. # # By default, save always runs validations. If any of them fail the action - # is cancelled and #save returns +false+. However, if you supply + # 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. # @@ -133,7 +133,7 @@ module ActiveRecord # the existing record gets updated. # # By default, #save! always runs validations. If any of them fail - # ActiveRecord::RecordInvalid gets raised. However, if you supply + # 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. # @@ -270,7 +270,7 @@ module ActiveRecord alias update_attributes update # Updates its receiver just like #update but calls #save! instead - # of +save+, so an exception is raised if the record is invalid. + # 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. diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index dcb2bd3d84..07d863d15e 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -5,7 +5,7 @@ module ActiveRecord # Enable the query cache within the block if Active Record is configured. # If it's not, it will execute the given block. def cache(&block) - if ActiveRecord::Base.connected? + if connected? connection.cache(&block) else yield @@ -15,7 +15,7 @@ module ActiveRecord # Disable the query cache within the block if Active Record is configured. # If it's not, it will execute the given block. def uncached(&block) - if ActiveRecord::Base.connected? + if connected? connection.uncached(&block) else yield @@ -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 1f429cfd94..53ddd95bb0 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -1,7 +1,7 @@ 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 @@ -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 38916f7376..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 @@ -71,7 +65,6 @@ module ActiveRecord ActiveSupport.on_load(:active_record) do self.time_zone_aware_attributes = true self.default_timezone = :utc - self.time_zone_aware_types = ActiveRecord::Base.time_zone_aware_types end end @@ -153,11 +146,9 @@ 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_cache! ActiveRecord::Base.clear_reloadable_connections! @@ -166,6 +157,12 @@ end_warning 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/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 69a7838001..fc1b62ee96 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -22,7 +22,7 @@ db_namespace = namespace :db do end end - desc 'Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV, it defaults to creating the development and test databases.' + desc 'Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV 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 @@ -33,7 +33,7 @@ db_namespace = namespace :db do end end - desc 'Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV, it defaults to dropping the development and test databases.' + desc 'Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV 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 @@ -256,7 +256,7 @@ db_namespace = namespace :db do end desc 'Loads a schema.rb file into the database' - task :load => [:environment, :load_config] do + task :load => [:environment, :load_config, :check_protected_environments] do ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV['SCHEMA']) end @@ -278,7 +278,7 @@ db_namespace = namespace :db do 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 @@ -302,7 +302,7 @@ db_namespace = namespace :db do end desc "Recreates the databases from the structure.sql file" - task :load => [:environment, :load_config] do + task :load => [:environment, :load_config, :check_protected_environments] do ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['SCHEMA']) end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 320ced5afa..bf398b0d40 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -124,9 +124,24 @@ module ActiveRecord 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 @@ -228,18 +243,14 @@ module ActiveRecord 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. # @@ -418,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 @@ -438,6 +449,10 @@ module ActiveRecord scope ? [[scope]] : [[]] end + def has_scope? + scope + end + def has_inverse? inverse_name end @@ -483,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] @@ -513,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 @@ -522,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. @@ -622,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) @@ -658,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: @@ -685,6 +708,10 @@ module ActiveRecord @source_reflection_name = delegate_reflection.options[:source] end + def through_reflection? + true + end + def klass @klass ||= delegate_reflection.compute_class(class_name) end @@ -743,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 @@ -808,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 @@ -853,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" \ @@ -914,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 @@ -970,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 032b8d4c5d..7a1552856b 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -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,17 +94,21 @@ 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, Arel::Nodes::BindParam.new] + 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. # @@ -249,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. @@ -271,12 +279,7 @@ module ActiveRecord def empty? return @records.empty? if loaded? - if limit_value == 0 - true - else - c = count(:all) - c.respond_to?(:zero?) ? c.zero? : c.empty? - end + limit_value == 0 || !exists? end # Returns true if there are no records. @@ -294,13 +297,13 @@ module ActiveRecord # Returns true if there is exactly one record. def one? return super if block_given? - limit_value ? to_a.one? : size == 1 + limit_value ? records.one? : size == 1 end # Returns true if there is more than one record. def many? return super if block_given? - limit_value ? to_a.many? : size > 1 + limit_value ? records.many? : size > 1 end # Returns a cache key that can be used to identify the records fetched by @@ -373,9 +376,9 @@ module ActiveRecord stmt.table(table) if joins_values.any? - @klass.connection.join_to_update(stmt, arel, table[primary_key]) + @klass.connection.join_to_update(stmt, arel, arel_attribute(primary_key)) else - stmt.key = table[primary_key] + stmt.key = arel_attribute(primary_key) stmt.take(arel.limit) stmt.order(*arel.orders) stmt.wheres = arel.constraints @@ -414,13 +417,13 @@ module ActiveRecord 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` + Please pass the id of the object by calling `.id`. MSG end object = find(id) @@ -449,11 +452,11 @@ module ActiveRecord 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 + 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 @@ -519,7 +522,7 @@ module ActiveRecord 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 + To achieve the same use where(conditions).delete_all. MESSAGE where(conditions).delete_all else @@ -527,7 +530,7 @@ module ActiveRecord 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 @@ -583,7 +586,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 @@ -650,21 +653,21 @@ module ActiveRecord 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 @@ -672,7 +675,7 @@ 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(', ')}]>" @@ -681,14 +684,14 @@ module ActiveRecord protected def load_records(records) - @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 54587ae18e..3639625722 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -2,6 +2,8 @@ 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 Scoping::Named::ClassMethods.all method, for example) # is very inefficient since it will try to instantiate all the objects at once. @@ -31,6 +33,9 @@ module ActiveRecord # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value. + # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when + # 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 @@ -48,13 +53,13 @@ module ActiveRecord # # NOTE: You can't set the limit either, that's used to control # the batch sizes. - def find_each(start: nil, finish: nil, batch_size: 1000) + def find_each(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil) if block_given? - find_in_batches(start: start, finish: finish, 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, start: start, finish: finish, 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, start, finish).size end @@ -83,6 +88,9 @@ module ActiveRecord # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value. + # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when + # 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 @@ -100,16 +108,16 @@ module ActiveRecord # # NOTE: You can't set the limit either, that's used to control # the batch sizes. - def find_in_batches(start: nil, finish: nil, batch_size: 1000) + 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, start: start, finish: finish, batch_size: batch_size) do + 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 - in_batches(of: batch_size, start: start, finish: finish, load: true) do |batch| + 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 @@ -140,6 +148,8 @@ module ActiveRecord # * <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 @@ -171,14 +181,14 @@ module ActiveRecord # # 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) + 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 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") + if arel.orders.present? || arel.taken.present? + act_on_order_or_limit_ignored(error_on_ignore) end relation = relation.reorder(batch_order).limit(of) @@ -187,7 +197,7 @@ module ActiveRecord loop do if load - records = batch_relation.to_a + records = batch_relation.records ids = records.map(&:id) yielded_relation = self.where(primary_key => ids) yielded_relation.load_records(records) @@ -204,20 +214,30 @@ module ActiveRecord yield yielded_relation break if ids.length < of - batch_relation = relation.where(table[primary_key].gt(primary_key_offset)) + batch_relation = relation.where(arel_attribute(primary_key).gt(primary_key_offset)) end end private def apply_limits(relation, start, finish) - relation = relation.where(table[primary_key].gteq(start)) if start - relation = relation.where(table[primary_key].lteq(finish)) if 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 index c6e39814dd..333b3a63cf 100644 --- a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb +++ b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb @@ -35,14 +35,14 @@ module ActiveRecord 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.to_a.each { |record| yield record } + 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.in_batches.destroy_all('age < 10') + # 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| diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index f45844a9ea..d6d92b8607 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -37,7 +37,11 @@ module ActiveRecord # 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 @@ -89,7 +93,7 @@ module ActiveRecord # # There are two basic forms of output: # - # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float + # * Single aggregate value: The single value is type cast to Integer for COUNT, Float # for AVG, and the given column's type for everything else. # # * Grouped values: This returns an ordered hash of the values and groups them. It @@ -155,15 +159,7 @@ module ActiveRecord # 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 - end - - if loaded? && (column_names - @klass.column_names).empty? + if loaded? && (column_names.map(&:to_s) - @klass.attribute_names - @klass.attribute_aliases.keys).empty? return @records.pluck(*column_names) end @@ -172,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) diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index e4e5d63006..2484cb3264 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -1,4 +1,3 @@ -require 'set' require 'active_support/concern' module ActiveRecord @@ -36,8 +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. - delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, - :[], :&, :|, :+, :-, :sample, :shuffle, :reverse, :compact, 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 diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 3cbb12a09d..d255cad91b 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -42,10 +42,10 @@ 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 + # Person.find_or_create_by(name: 'Spartacus', rating: 4) # # returns the first item or creates it and returns it. # # ==== Alternatives for #find @@ -145,15 +145,21 @@ 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 ActiveRecord::RecordNotFound if no record @@ -242,6 +248,38 @@ module ActiveRecord find_nth! 41 end + # 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: # @@ -274,13 +312,13 @@ 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 return false if !conditions - relation = apply_join_dependency(self, construct_join_dependency) + relation = apply_join_dependency(self, construct_join_dependency(eager_loading: false)) return false if ActiveRecord::NullRelation === relation relation = relation.except(:select, :order).select(ONE_AS_ONE).limit(1) @@ -295,10 +333,12 @@ module ActiveRecord end connection.select_value(relation, "#{name} Exists", relation.bound_attributes) ? true : false + rescue RangeError + false 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 @@ -354,21 +394,13 @@ module ActiveRecord end end - def construct_join_dependency(joins = []) + def construct_join_dependency(joins = [], eager_loading: true) including = eager_load_values + includes_values - ActiveRecord::Associations::JoinDependency.new(@klass, including, joins) + ActiveRecord::Associations::JoinDependency.new(@klass, including, joins, eager_loading: eager_loading) end def construct_relation_for_association_calculations - from = arel.froms.first - if Arel::Table === from - 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 - # possible for `from` to be a list? - apply_join_dependency(self, construct_join_dependency(from)) - end + apply_join_dependency(self, construct_join_dependency(joins_values)) end def apply_join_dependency(relation, join_dependency) @@ -429,7 +461,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 @@ -468,7 +500,7 @@ module ActiveRecord def find_some_ordered(ids) ids = ids.slice(offset_value || 0, limit_value || ids.size) || [] - result = except(:limit, :offset).where(primary_key => ids).to_a + result = except(:limit, :offset).where(primary_key => ids).records if result.size == ids.size pk_type = @klass.type_for_attribute(primary_key) @@ -484,25 +516,24 @@ module ActiveRecord if loaded? @records.first else - @take ||= limit(1).to_a.first + @take ||= limit(1).records.first end end 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 - # 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 - else - offset = offset_index - end + offset ||= offset_index @offsets[offset + index] ||= find_nth_with_limit_and_offset(index, 1, offset: offset).first end end @@ -515,7 +546,7 @@ module ActiveRecord # 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 @@ -524,16 +555,22 @@ module ActiveRecord relation.limit(limit).to_a end - def find_last + def find_nth_from_last(index) if loaded? - @records.last + @records[-index] else - @last ||= - if limit_value - to_a.last - else - reverse_order.limit(1).to_a.first - end + 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 @@ -547,5 +584,9 @@ module ActiveRecord 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/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 0f88791d92..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' @@ -22,6 +23,7 @@ module ActiveRecord 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) @@ -40,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 @@ -137,9 +136,11 @@ module ActiveRecord end def convert_dot_notation_to_hash(attributes) - dot_notation = attributes.keys.select { |s| s.include?(".".freeze) } + dot_notation = attributes.select do |k, v| + k.include?(".".freeze) && !v.is_a?(Hash) + end - dot_notation.each do |key| + dot_notation.each_key do |key| table_name, column_name = key.split(".".freeze) value = attributes.delete(key) attributes[table_name] ||= {} 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 e81be63cd3..413cb9fd84 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,17 @@ module ActiveRecord class PredicateBuilder class AssociationQueryHandler # :nodoc: + def self.value_for(table, column, value) + associated_table = table.associated_table(column) + klass = if associated_table.polymorphic_association? && ::Array === value && value.first.is_a?(Base) + PolymorphicArrayValue + else + AssociationQueryValue + end + + klass.new(associated_table, value) + end + def initialize(predicate_builder) @predicate_builder = predicate_builder end 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/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_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 716b1e8505..8a87015e44 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -99,22 +99,28 @@ module ActiveRecord end def bound_attributes - result = from_clause.binds + arel.bind_values + where_clause.binds + having_clause.binds if limit_value && !string_containing_comma?(limit_value) - result << Attribute.with_cast_value( + limit_bind = Attribute.with_cast_value( "LIMIT".freeze, connection.sanitize_limit(limit_value), Type::Value.new, ) end if offset_value - result << Attribute.with_cast_value( + offset_bind = Attribute.with_cast_value( "OFFSET".freeze, offset_value.to_i, Type::Value.new, ) end - result + 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 @@ -652,9 +658,13 @@ module ActiveRecord # present). Neither relation may have a #limit, #offset, or #distinct set. # # Post.where("id = 1").or(Post.where("author_id = 3")) - # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR '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 @@ -998,12 +1008,6 @@ module ActiveRecord self.send(unscope_code, result) end - def association_for_table(table_name) - table_name = table_name.to_s - @klass._reflect_on_association(table_name) || - @klass._reflect_on_association(table_name.singularize) - end - def build_from opts = from_clause.value name = from_clause.name @@ -1093,8 +1097,8 @@ module ActiveRecord def arel_columns(columns) columns.map do |field| - if (Symbol === field || String === field) && columns_hash.key?(field.to_s) && !from_clause.value - arel_table[field] + 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 @@ -1104,14 +1108,23 @@ module ActiveRecord 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 @@ -1121,6 +1134,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?) @@ -1156,12 +1176,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 diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 67d7f83cb4..d5c18a2a4a 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -29,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 diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb index 2c2d6cfa47..89396b518c 100644 --- a/activerecord/lib/active_record/relation/where_clause.rb +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -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/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 2bfc5ff7ae..a9e1fd0dad 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -60,7 +60,7 @@ module ActiveRecord end # Accepts an array, or string of SQL conditions and sanitizes - # them into a valid SQL fragment for a ORDER clause. + # them into a valid SQL fragment for an ORDER clause. # # sanitize_sql_for_order(["field(id, ?)", [1,3,2]]) # # => "field(id, 1,3,2)" diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 65005bd44b..d769376d1a 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -50,10 +50,6 @@ module ActiveRecord def header(stream) define_params = @version ? "version: #{@version}" : "" - if stream.respond_to?(:external_encoding) && stream.external_encoding - stream.puts "# encoding: #{stream.external_encoding.name}" - end - stream.puts <<HEADER # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to @@ -104,10 +100,7 @@ HEADER end def table(table, stream) - columns = @connection.columns(table).map do |column| - column.instance_variable_set(:@table_name, table) - column - end + columns = @connection.columns(table) begin tbl = StringIO.new @@ -126,7 +119,7 @@ HEADER 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 @@ -141,6 +134,10 @@ HEADER 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 @@ -178,7 +175,7 @@ HEADER tbl.puts end - indexes(table, tbl) + indexes_in_create(table, tbl) tbl.puts " end" tbl.puts @@ -194,31 +191,47 @@ 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 = [ - "t.index #{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") + stream.puts + 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| diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index ee4c71f304..b6cb233e03 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -16,31 +16,22 @@ module ActiveRecord "#{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? 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.index :version, unique: true, name: index_name + t.string :version, version_options end 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/default.rb b/activerecord/lib/active_record/scoping/default.rb index f6b6768ce3..9eab59ac78 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -115,7 +115,8 @@ module ActiveRecord 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 diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 103569c84d..5395bd6076 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -151,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) @@ -169,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/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index f6b0efb88a..6c896ccea6 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -106,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/suppressor.rb b/activerecord/lib/active_record/suppressor.rb index b3644bf569..d9acb1a1dc 100644 --- a/activerecord/lib/active_record/suppressor.rb +++ b/activerecord/lib/active_record/suppressor.rb @@ -30,14 +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 - def create_or_update(*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 f9bb1cf5e0..a1326aa359 100644 --- a/activerecord/lib/active_record/table_metadata.rb +++ b/activerecord/lib/active_record/table_metadata.rb @@ -22,7 +22,11 @@ module ActiveRecord 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) @@ -40,7 +44,8 @@ module ActiveRecord def associated_table(table_name) return self if table_name == arel_table.name - association = klass._reflect_on_association(table_name) + association = klass._reflect_on_association(table_name) || klass._reflect_on_association(table_name.singularize) + if association && !association.polymorphic? association_klass = association.klass arel_table = association_klass.arel_table.alias(table_name) diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 8f52e9068a..e3e665e149 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -107,8 +107,9 @@ 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 $stderr.puts "Couldn't create database for #{configuration.inspect}" @@ -116,7 +117,11 @@ module ActiveRecord 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.to_hash) + end end def create_current(environment = env) @@ -129,11 +134,12 @@ 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 - $stderr.puts "Couldn't drop #{configuration['database']}" + $stderr.puts "Couldn't drop database '#{configuration['database']}'" raise end @@ -155,6 +161,7 @@ module ActiveRecord Migrator.migrate(migrations_paths, version) do |migration| scope.blank? || scope == migration.scope end + ActiveRecord::Base.clear_cache! ensure Migration.verbose = verbose_was end @@ -278,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 4cf32c2abe..b1a0ad0115 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -131,7 +131,7 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; 'sslca' => '--ssl-ca', 'sslcert' => '--ssl-cert', 'sslcapath' => '--ssl-capath', - 'sslcipher' => '--ssh-cipher', + 'sslcipher' => '--ssl-cipher', 'sslkey' => '--ssl-key' }.map { |opt, arg| "#{arg}=#{configuration[opt]}" if configuration[opt] }.compact diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index 8b4874044c..b19ab57ee4 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -2,6 +2,7 @@ module ActiveRecord module Tasks # :nodoc: class PostgreSQLDatabaseTasks # :nodoc: DEFAULT_ENCODING = ENV['CHARSET'] || 'utf8' + ON_ERROR_STOP_1 = 'ON_ERROR_STOP=1'.freeze delegate :connection, :establish_connection, :clear_active_connections!, to: ActiveRecord::Base @@ -67,7 +68,7 @@ module ActiveRecord def structure_load(filename) set_psql_env - args = [ '-q', '-f', filename, configuration['database'] ] + args = [ '-v', ON_ERROR_STOP_1, '-q', '-f', filename, configuration['database'] ] run_cmd('psql', args, 'loading' ) end diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index e210e94f00..4911d93dd9 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -61,7 +61,7 @@ module ActiveRecord 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/internal/abstract_json.rb b/activerecord/lib/active_record/type/internal/abstract_json.rb index 097d1bd363..513c938088 100644 --- a/activerecord/lib/active_record/type/internal/abstract_json.rb +++ b/activerecord/lib/active_record/type/internal/abstract_json.rb @@ -17,11 +17,7 @@ module ActiveRecord end def serialize(value) - if value.is_a?(::Array) || value.is_a?(::Hash) - ::ActiveSupport::JSON.encode(value) - else - value - end + ::ActiveSupport::JSON.encode(value) end def accessor diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index 4ff0740cfb..a3a5241780 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -32,7 +32,7 @@ module ActiveRecord def changed_in_place?(raw_old_value, value) return false if value.nil? - raw_new_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 @@ -52,6 +52,12 @@ module ActiveRecord 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/time.rb b/activerecord/lib/active_record/type/time.rb index 70988d84ff..7da49e43c7 100644 --- a/activerecord/lib/active_record/type/time.rb +++ b/activerecord/lib/active_record/type/time.rb @@ -2,6 +2,18 @@ module ActiveRecord module Type class Time < ActiveModel::Type::Time include Internal::Timezone + + class Value < DelegateClass(::Time) # :nodoc: + end + + def serialize(value) + case value = super + when ::Time + Value.new(value) + else + value + end + end end end end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 6677e6dc5f..ecaf04e39e 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -45,7 +45,7 @@ module ActiveRecord end # Attempts to save the record just like {ActiveRecord::Base#save}[rdoc-ref:Base#save] but - # will raise a ActiveRecord::RecordInvalid exception instead of returning +false+ if the record is not valid. + # 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 diff --git a/activerecord/lib/active_record/validations/absence.rb b/activerecord/lib/active_record/validations/absence.rb index 2e19e6dc5c..641d041f3d 100644 --- a/activerecord/lib/active_record/validations/absence.rb +++ b/activerecord/lib/active_record/validations/absence.rb @@ -2,7 +2,6 @@ module ActiveRecord module Validations class AbsenceValidator < ActiveModel::Validations::AbsenceValidator # :nodoc: def validate_each(record, attribute, association_or_value) - return unless should_validate?(record) if record.class._reflect_on_association(attribute) association_or_value = Array.wrap(association_or_value).reject(&:marked_for_destruction?) end diff --git a/activerecord/lib/active_record/validations/length.rb b/activerecord/lib/active_record/validations/length.rb index 69e048eef1..0e0cebce4a 100644 --- a/activerecord/lib/active_record/validations/length.rb +++ b/activerecord/lib/active_record/validations/length.rb @@ -2,23 +2,11 @@ module ActiveRecord module Validations class LengthValidator < ActiveModel::Validations::LengthValidator # :nodoc: def validate_each(record, attribute, association_or_value) - return unless should_validate?(record) || associations_are_dirty?(record) if association_or_value.respond_to?(:loaded?) && association_or_value.loaded? association_or_value = association_or_value.target.reject(&:marked_for_destruction?) end super end - - def associations_are_dirty?(record) - attributes.any? do |attribute| - value = record.read_attribute_for_validation(attribute) - if value.respond_to?(:loaded?) && value.loaded? - value.target.any?(&:marked_for_destruction?) - else - false - end - end - end end module ClassMethods diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index 7e85ed43ac..ad82ea66c4 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -2,7 +2,6 @@ module ActiveRecord module Validations class PresenceValidator < ActiveModel::Validations::PresenceValidator # :nodoc: def validate_each(record, attribute, association_or_value) - return unless should_validate?(record) if record.class._reflect_on_association(attribute) association_or_value = Array.wrap(association_or_value).reject(&:marked_for_destruction?) end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index a376e2a17f..ec9f498c40 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -11,15 +11,14 @@ module ActiveRecord end def validate_each(record, attribute, value) - return unless should_validate?(record) finder_class = find_finder_class_for(record) table = finder_class.arel_table value = map_enum_attribute(finder_class, attribute, value) relation = build_relation(finder_class, table, attribute, value) - if record.persisted? && finder_class.primary_key.to_s != attribute.to_s + if record.persisted? if finder_class.primary_key - relation = relation.where.not(finder_class.primary_key => record.id) + 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 @@ -57,21 +56,15 @@ 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 comparison = if !options[:case_sensitive] && !value.nil? # will use SQL LOWER function before comparison, unless it detects a case insensitive collation @@ -82,11 +75,9 @@ module ActiveRecord if value.nil? klass.unscoped.where(comparison) else - bind = Relation::QueryAttribute.new(attribute.to_s, value, Type::Value.new) + bind = Relation::QueryAttribute.new(attribute_name, value, cast_type) klass.unscoped.where(comparison, bind) end - rescue RangeError - klass.none end def scope_relation(record, table, relation) 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 107f107dc4..481c70201b 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb @@ -19,7 +19,11 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi 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 7395839fca..0d72913258 100644 --- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb +++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb @@ -21,11 +21,13 @@ module ActiveRecord end def create_model_file + generate_application_record template 'model.rb', File.join('app/models', class_path, "#{file_name}.rb") end def create_module_file return if regular_class_path.empty? + generate_application_record template 'module.rb', File.join('app/models', "#{class_path.join('/')}.rb") if behavior == :invoke end @@ -37,26 +39,29 @@ module ActiveRecord 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] || determine_default_parent_class + options[:parent] || 'ApplicationRecord' end - def determine_default_parent_class - application_record = nil - - in_root do - application_record = if mountable_engine? - File.exist?("app/models/#{namespaced_path}/application_record.rb") - else - File.exist?('app/models/application_record.rb') - end - end + def application_record_exist? + file_exist = nil + in_root { file_exist = File.exist?(application_record_file_name) } + file_exist + end - if application_record - "ApplicationRecord" + def application_record_file_name + @application_record_file_name ||= if mountable_engine? + "app/models/#{namespaced_path}/application_record.rb" else - "ActiveRecord::Base" + 'app/models/application_record.rb' 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/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 0ee147cdba..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,6 +24,12 @@ 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 = nil ActiveSupport::Deprecation.silence { tables = @connection.tables } @@ -79,6 +87,17 @@ 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 @@ -193,6 +212,14 @@ module ActiveRecord 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 def test_disable_referential_integrity diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index 99f97c7914..95d1f6b8a3 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -63,14 +63,14 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase 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" + 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" + 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 @@ -155,7 +155,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase 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/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb index 8575df9e43..739bb275ce 100644 --- a/activerecord/test/cases/adapters/mysql2/boolean_test.rb +++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb @@ -43,11 +43,16 @@ class Mysql2BooleanTest < ActiveRecord::Mysql2TestCase 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::Mysql2TestCase 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 963116f08a..9cb05119a2 100644 --- a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb +++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb @@ -51,4 +51,13 @@ class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase 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 index 668c07dacb..c8028b6b36 100644 --- a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb +++ b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb @@ -49,6 +49,6 @@ class Mysql2CharsetCollationTest < ActiveRecord::Mysql2TestCase test "schema dump includes collation" do output = dump_table_schema("charset_collations") assert_match %r{t.string\s+"string_ascii_bin",\s+collation: "ascii_bin"$}, output - assert_match %r{t.text\s+"text_ucs2_unicode_ci",\s+limit: 65535,\s+collation: "ucs2_unicode_ci"$}, output + assert_match %r{t.text\s+"text_ucs2_unicode_ci",\s+collation: "ucs2_unicode_ci"$}, output end end diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 575138eb2a..fe610ae951 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -69,40 +69,48 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase end 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_strict_mode_specified_default + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.merge(strict: :default)) + global_sql_mode = ActiveRecord::Base.connection.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.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| + 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| + 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_strict_mode_specified_default - run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.merge({strict: :default})) - global_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.sql_mode" - session_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode" - assert_equal global_sql_mode.rows, session_sql_mode.rows - end - end def test_mysql_set_session_variable run_without_connection do |orig_connection| @@ -112,14 +120,6 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase 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}})) @@ -144,7 +144,7 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase end def test_get_and_release_advisory_lock - lock_name = "test_lock_name" + 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" @@ -159,7 +159,7 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase end def test_release_non_existent_advisory_lock - lock_name = "fake_lock_name" + 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' @@ -168,6 +168,6 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase protected def test_lock_free(lock_name) - @connection.select_value("SELECT IS_FREE_LOCK('#{lock_name}');") == 1 + @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 bb89e893e0..35dbc76d1b 100644 --- a/activerecord/test/cases/adapters/mysql2/enum_test.rb +++ b/activerecord/test/cases/adapters/mysql2/enum_test.rb @@ -18,4 +18,9 @@ class Mysql2EnumTest < ActiveRecord::Mysql2TestCase 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 4fc7414b18..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::Mysql2TestCase - 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 index c8c933af5e..9c3fef1b59 100644 --- a/activerecord/test/cases/adapters/mysql2/json_test.rb +++ b/activerecord/test/cases/adapters/mysql2/json_test.rb @@ -161,12 +161,19 @@ class Mysql2JSONTest < ActiveRecord::Mysql2TestCase 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 end diff --git a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb index 4efd728754..61dd0828d0 100644 --- a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb @@ -1,10 +1,33 @@ 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", []) @@ -41,4 +64,10 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase 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/quoting_test.rb b/activerecord/test/cases/adapters/mysql2/quoting_test.rb deleted file mode 100644 index 2de7e1b526..0000000000 --- a/activerecord/test/cases/adapters/mysql2/quoting_test.rb +++ /dev/null @@ -1,21 +0,0 @@ -require "cases/helper" - -class Mysql2QuotingTest < ActiveRecord::Mysql2TestCase - setup do - @connection = ActiveRecord::Base.connection - end - - test 'quoted date precision for gte 5.6.4' do - @connection.stubs(:full_version).returns('5.6.4') - @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version) - t = Time.now.change(usec: 1) - assert_match(/\.000001\z/, @connection.quoted_date(t)) - end - - test 'quoted date precision for lt 5.6.4' do - @connection.stubs(:full_version).returns('5.6.3') - @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version) - t = Time.now.change(usec: 1) - assert_no_match(/\.000001\z/, @connection.quoted_date(t)) - end -end diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb index 9a9a4fed42..7c89fda582 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -33,11 +33,6 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase end end - def test_key_limit_max_matches_max - assert_equal ActiveRecord::InternalMetadata::KEY_LIMIT ,ActiveRecord::ConnectionAdapters::Mysql2Adapter::MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN - end - - private def with_encoding_utf8mb4 diff --git a/activerecord/test/cases/adapters/mysql2/transaction_test.rb b/activerecord/test/cases/adapters/mysql2/transaction_test.rb new file mode 100644 index 0000000000..0e37c70e5c --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/transaction_test.rb @@ -0,0 +1,62 @@ +require "cases/helper" +require 'support/connection_helper' + +module ActiveRecord + class Mysql2TransactionTest < ActiveRecord::Mysql2TestCase + self.use_transactional_tests = false + + class Sample < ActiveRecord::Base + self.table_name = 'samples' + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.clear_cache! + + @connection.transaction do + @connection.drop_table 'samples', if_exists: true + @connection.create_table('samples') do |t| + t.integer 'value' + end + end + + Sample.reset_column_information + end + + teardown do + @connection.drop_table 'samples', if_exists: true + end + + test "raises error when a serialization failure occurs" do + assert_raises(ActiveRecord::TransactionSerializationError) do + thread = Thread.new do + Sample.transaction isolation: :serializable do + Sample.delete_all + + 10.times do |i| + sleep 0.1 + + Sample.create value: i + end + end + end + + sleep 0.1 + + Sample.transaction isolation: :serializable do + Sample.delete_all + + 10.times do |i| + sleep 0.1 + + Sample.create value: i + end + + sleep 1 + end + + thread.join + end + end + end +end diff --git a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb index c95a64cc16..3df11ce11b 100644 --- a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb +++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb @@ -28,10 +28,10 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase 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(RangeError) do + assert_raise(ActiveModel::RangeError) do UnsignedType.create(unsigned_bigint: -10) end assert_raise(ActiveRecord::StatementInvalid) do @@ -58,7 +58,7 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase 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.integer\s+"unsigned_bigint",\s+limit: 8,\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 diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb index ed44bf7362..439e2ce6f7 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -28,7 +28,13 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase 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,17 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase 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 @@ -69,6 +76,11 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase 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 def method_missing(method_symbol, *arguments) ActiveRecord::Base.connection.send(method_symbol, *arguments) diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb index 6f72fa6e0f..cec6081aec 100644 --- a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb @@ -57,9 +57,11 @@ class PostgresqlBitStringTest < ActiveRecord::PostgreSQLTestCase 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 b6bb1929e6..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 ByteaDataType < ActiveRecord::Base self.table_name = 'bytea_data_type' end @@ -122,4 +125,10 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase 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/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index d559de3e28..f8403bfe1a 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -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], 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 + 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 diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb index 4d0fd640aa..29bf2c15ea 100644 --- a/activerecord/test/cases/adapters/postgresql/explain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb @@ -6,15 +6,15 @@ 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 + 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 + 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 %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 b2a805333c..b56c226763 100644 --- a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb +++ b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb @@ -24,9 +24,9 @@ class PostgresqlExtensionMigrationTest < ActiveRecord::PostgreSQLTestCase 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::PostgreSQLTestCase 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/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb index 9e250c2b7c..66f0a70394 100644 --- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -104,6 +104,13 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase 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), diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index b3b121b4fb..663de680b5 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -38,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']) @@ -178,12 +178,19 @@ 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 diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index f1995b243a..9832df7839 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -5,6 +5,7 @@ require 'support/connection_helper' module ActiveRecord module ConnectionAdapters class PostgreSQLAdapterTest < ActiveRecord::PostgreSQLTestCase + self.use_transactional_tests = false include DdlHelper include ConnectionHelper @@ -53,62 +54,12 @@ module ActiveRecord end end - def test_composite_primary_key - with_example_table 'id serial, number serial, PRIMARY KEY (id, number)' do - assert_nil @connection.primary_key('ex') - end - end - def test_primary_key_raises_error_if_table_not_found assert_raises(ActiveRecord::StatementInvalid) do @connection.primary_key('unobtainium') 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 - end - - def test_insert_sql_with_returning_disabled - connection = connection_without_insert_returning - id = connection.insert_sql("insert into postgresql_partitioned_table_parent (number) VALUES (1)") - expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first - assert_equal expect.to_i, id - end - def test_exec_insert_with_returning_disabled connection = connection_without_insert_returning result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id', 'postgresql_partitioned_table_parent_id_seq') @@ -245,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 @@ -292,33 +219,35 @@ module ActiveRecord 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, 'foo']], result.rows + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length + + 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 @@ -330,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", []) @@ -444,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) @@ -465,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/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb index a0afd922b2..285a92f60e 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb @@ -55,20 +55,22 @@ class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase 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 4aeca4d709..52ef07f654 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -166,22 +166,24 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase 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', [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_data_source_exists? @@ -323,7 +325,7 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase def test_dump_indexes_for_table_with_scheme_specified_in_name indexes = @connection.indexes("#{SCHEMA_NAME}.#{TABLE_NAME}") - assert_equal 4, indexes.size + assert_equal 5, indexes.size end def test_with_uppercase_index_name @@ -447,18 +449,22 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase 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 diff --git a/activerecord/test/cases/adapters/postgresql/serial_test.rb b/activerecord/test/cases/adapters/postgresql/serial_test.rb index 7d30db247b..8abe064bf1 100644 --- a/activerecord/test/cases/adapters/postgresql/serial_test.rb +++ b/activerecord/test/cases/adapters/postgresql/serial_test.rb @@ -10,6 +10,7 @@ class PostgresqlSerialTest < ActiveRecord::PostgreSQLTestCase @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,9 +25,21 @@ class PostgresqlSerialTest < ActiveRecord::PostgreSQLTestCase 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 @@ -39,6 +52,7 @@ class PostgresqlBigSerialTest < ActiveRecord::PostgreSQLTestCase @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::PostgreSQLTestCase 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/transaction_test.rb b/activerecord/test/cases/adapters/postgresql/transaction_test.rb new file mode 100644 index 0000000000..e76705a802 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/transaction_test.rb @@ -0,0 +1,72 @@ +require "cases/helper" +require 'support/connection_helper' + +module ActiveRecord + class PostgresqlTransactionTest < ActiveRecord::PostgreSQLTestCase + self.use_transactional_tests = false + + class Sample < ActiveRecord::Base + self.table_name = 'samples' + end + + setup do + @connection = ActiveRecord::Base.connection + + @connection.transaction do + @connection.drop_table 'samples', if_exists: true + @connection.create_table('samples') do |t| + t.integer 'value' + end + end + + Sample.reset_column_information + end + + teardown do + @connection.drop_table 'samples', if_exists: true + end + + test "raises error when a serialization failure occurs" do + with_warning_suppression do + assert_raises(ActiveRecord::TransactionSerializationError) do + thread = Thread.new do + Sample.transaction isolation: :serializable do + Sample.delete_all + + 10.times do |i| + sleep 0.1 + + Sample.create value: i + end + end + end + + sleep 0.1 + + Sample.transaction isolation: :serializable do + Sample.delete_all + + 10.times do |i| + sleep 0.1 + + Sample.create value: i + end + + sleep 1 + end + + thread.join + end + 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/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb index 77a99ca778..ea0f0b8fa5 100644 --- a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb +++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb @@ -18,7 +18,7 @@ class PostgresqlTypeLookupTest < ActiveRecord::PostgreSQLTestCase 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::PostgreSQLTestCase 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/sqlite3/explain_test.rb b/activerecord/test/cases/adapters/sqlite3/explain_test.rb index 2aec322582..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::SQLite3TestCase - 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 87a892db37..fe2425b845 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -3,93 +3,92 @@ require 'bigdecimal' require 'yaml' require 'securerandom' -module ActiveRecord - module ConnectionAdapters - class SQLite3Adapter - class QuotingTest < ActiveRecord::SQLite3TestCase - def setup - @conn = Base.sqlite3_connection :database => ':memory:', - :adapter => 'sqlite3', - :timeout => 100 - end - - def test_type_cast_binary_encoding_without_logger - @conn.extend(Module.new { def logger; end }) - binary = SecureRandom.hex - expected = binary.dup.encode!(Encoding::UTF_8) - assert_equal expected, @conn.type_cast(binary) - end - - def test_type_cast_symbol - assert_equal 'foo', @conn.type_cast(:foo) - end - - def test_type_cast_date - date = Date.today - expected = @conn.quoted_date(date) - assert_equal expected, @conn.type_cast(date) - end - - def test_type_cast_time - time = Time.now - expected = @conn.quoted_date(time) - assert_equal expected, @conn.type_cast(time) - end - - def test_type_cast_numeric - assert_equal 10, @conn.type_cast(10) - assert_equal 2.2, @conn.type_cast(2.2) - end - - def test_type_cast_nil - assert_equal nil, @conn.type_cast(nil) - end - - def test_type_cast_true - assert_equal 't', @conn.type_cast(true) - end - - def test_type_cast_false - assert_equal 'f', @conn.type_cast(false) - end - - def test_type_cast_bigdecimal - bd = BigDecimal.new '10.0' - assert_equal bd.to_f, @conn.type_cast(bd) - end - - def test_type_cast_unknown_should_raise_error - obj = Class.new.new - assert_raise(TypeError) { @conn.type_cast(obj) } - end - - def test_type_cast_object_which_responds_to_quoted_id - quoted_id_obj = Class.new { - def quoted_id - "'zomg'" - end - - def id - 10 - end - }.new - assert_equal 10, @conn.type_cast(quoted_id_obj) - - quoted_id_obj = Class.new { - def quoted_id - "'zomg'" - end - }.new - assert_raise(TypeError) { @conn.type_cast(quoted_id_obj) } - end - - def test_quoting_binary_strings - value = "hello".encode('ascii-8bit') - type = Type::String.new - - assert_equal "'hello'", @conn.quote(type.serialize(value)) - end +class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase + def setup + @conn = ActiveRecord::Base.connection + end + + def test_type_cast_binary_encoding_without_logger + @conn.extend(Module.new { def logger; end }) + binary = SecureRandom.hex + expected = binary.dup.encode!(Encoding::UTF_8) + assert_equal expected, @conn.type_cast(binary) + end + + def test_type_cast_symbol + assert_equal 'foo', @conn.type_cast(:foo) + end + + def test_type_cast_date + date = Date.today + expected = @conn.quoted_date(date) + assert_equal expected, @conn.type_cast(date) + end + + def test_type_cast_time + time = Time.now + expected = @conn.quoted_date(time) + assert_equal expected, @conn.type_cast(time) + end + + def test_type_cast_numeric + assert_equal 10, @conn.type_cast(10) + assert_equal 2.2, @conn.type_cast(2.2) + end + + def test_type_cast_nil + assert_equal nil, @conn.type_cast(nil) + end + + def test_type_cast_true + assert_equal 't', @conn.type_cast(true) + end + + def test_type_cast_false + assert_equal 'f', @conn.type_cast(false) + end + + def test_type_cast_bigdecimal + bd = BigDecimal.new '10.0' + assert_equal bd.to_f, @conn.type_cast(bd) + end + + def test_type_cast_unknown_should_raise_error + obj = Class.new.new + assert_raise(TypeError) { @conn.type_cast(obj) } + end + + def test_type_cast_object_which_responds_to_quoted_id + quoted_id_obj = Class.new { + def quoted_id + "'zomg'" + end + + def id + 10 end - end + }.new + assert_equal 10, @conn.type_cast(quoted_id_obj) + + quoted_id_obj = Class.new { + def quoted_id + "'zomg'" + end + }.new + assert_raise(TypeError) { @conn.type_cast(quoted_id_obj) } + end + + def test_quoting_binary_strings + value = "hello".encode('ascii-8bit') + type = ActiveRecord::Type::String.new + + 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 = ActiveRecord::Type::Time.new + + assert_equal "'2000-01-01 12:30:00.999999'", @conn.quote(type.serialize(value)) 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 038d9e2d0f..bbc9f978bf 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -213,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 @@ -239,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 @@ -400,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 diff --git a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb index 559b951109..24cc6875ab 100644 --- a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb @@ -1,24 +1,20 @@ require 'cases/helper' -module ActiveRecord::ConnectionAdapters - class SQLite3Adapter - class StatementPoolTest < ActiveRecord::SQLite3TestCase - if Process.respond_to?(:fork) - def test_cache_is_per_pid +class SQLite3StatementPoolTest < ActiveRecord::SQLite3TestCase + if Process.respond_to?(:fork) + def test_cache_is_per_pid - cache = StatementPool.new(10) - cache['foo'] = 'bar' - assert_equal 'bar', cache['foo'] + cache = ActiveRecord::ConnectionAdapters::SQLite3Adapter::StatementPool.new(10) + cache['foo'] = 'bar' + assert_equal 'bar', cache['foo'] - pid = fork { - lookup = cache['foo']; - exit!(!lookup) - } + pid = fork { + lookup = cache['foo']; + exit!(!lookup) + } - Process.waitpid pid - assert $?.success?, 'process should exit successfully' - end - end + Process.waitpid pid + assert $?.success?, 'process should exit successfully' end end end diff --git a/activerecord/test/cases/aggregations_test.rb b/activerecord/test/cases/aggregations_test.rb index 5536702f58..8a728902a8 100644 --- a/activerecord/test/cases/aggregations_test.rb +++ b/activerecord/test/cases/aggregations_test.rb @@ -138,6 +138,11 @@ class AggregationsTest < ActiveRecord::TestCase assert_equal 'Barnoit GUMBLEAU', customers(:barney).fullname.to_s assert_kind_of Fullname, customers(:barney).fullname end + + def test_assigning_hash_to_custom_converter + customers(:barney).fullname = { first: "Barney", last: "Stinson" } + assert_equal "Barney STINSON", customers(:barney).name + end end class OverridingAggregationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index 1f32c48b95..9c99689c1e 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -21,7 +21,7 @@ if ActiveRecord::Base.connection.supports_migrations? ActiveRecord::Migration.verbose = @original_verbose end - def test_has_has_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_equal "version", ActiveRecord::SchemaMigration.primary_key diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 4f99c57c3c..9dadd114a1 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -168,7 +168,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase e = assert_raise(ActiveRecord::AssociationTypeMismatch) { Admin::RegionalUser.new(region: 'wrong value') } - assert_match(/^Region\([^)]+\) expected, got String\([^)]+\)$/, e.message) + 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") @@ -700,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 @@ -726,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 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 874d53c51f..80d9a6083b 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -90,6 +90,10 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_no_queries { authors.map(&:post) } end + def test_calculate_with_string_in_from_and_eager_loading + assert_equal 10, Post.from("authors, posts").eager_load(:comments).where("posts.author_id = authors.id").count + end + def test_with_two_tables_in_from_without_getting_double_quoted posts = Post.select("posts.*").from("authors, posts").eager_load(:comments).where("posts.author_id = authors.id").order("posts.id").to_a assert_equal 2, posts.first.comments.size @@ -749,6 +753,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 @@ -1216,7 +1252,7 @@ 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 @@ -1309,6 +1345,7 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_nothing_raised do authors(:david).essays.includes(:writer).any? + authors(:david).essays.includes(:writer).exists? end end @@ -1360,6 +1397,18 @@ class EagerAssociationTest < ActiveRecord::TestCase 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 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 ccb062f991..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 @@ -146,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) @@ -827,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 @@ -925,7 +938,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_with_symbol_class_name - assert_nothing_raised NoMethodError do + assert_nothing_raised do DeveloperWithSymbolClassName.new 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 ad157582a4..7ec0dfce7a 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -408,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 @@ -1305,7 +1315,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 2, summit.client_of end - def test_deleting_by_fixnum_id + def test_deleting_by_integer_id david = Developer.find(1) assert_difference 'david.projects.count', -1 do @@ -1342,7 +1352,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 1, companies(:first_firm).clients_of_firm.reload.size end - def test_destroying_by_fixnum_id + def test_destroying_by_integer_id force_signal37_to_load_all_clients_of_firm assert_difference "Client.count", -1 do @@ -2271,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! 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 226ecf5447..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 @@ -884,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 @@ -1082,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 diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index c9d9e29f09..1574f373c2 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -659,4 +659,22 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_deprecated { firm.account(true) } end + + class SpecialBook < ActiveRecord::Base + self.table_name = 'books' + belongs_to :author, class_name: 'SpecialAuthor' + end + + class SpecialAuthor < ActiveRecord::Base + self.table_name = 'authors' + has_one :book, class_name: 'SpecialBook', foreign_key: 'author_id' + end + + def test_assocation_enum_works_properly + author = SpecialAuthor.create!(name: 'Test') + book = SpecialBook.create!(status: 'published') + author.book = book + + refute_equal 0, SpecialAuthor.joins(:book).where(books: { status: 'published' } ).count + end end diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 57d1c8feda..c9743e80d3 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -130,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 @@ -666,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 @@ -676,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 @@ -688,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 @@ -696,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 f6dddaf5b4..3047914b70 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -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 @@ -363,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) @@ -591,7 +598,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert_raise(ActiveRecord::AssociationTypeMismatch) { posts(:thinking).tags.delete(Object.new) } end - def test_deleting_by_fixnum_id_from_has_many_through + def test_deleting_by_integer_id_from_has_many_through post = posts(:thinking) assert_difference 'post.tags.count', -1 do @@ -740,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 index 4af791b758..eee135cfb8 100644 --- a/activerecord/test/cases/associations/left_outer_join_association_test.rb +++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb @@ -50,7 +50,7 @@ class LeftOuterJoinAssociationTest < ActiveRecord::TestCase 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.any? { |sql| /writer_type.*?=.*?(Author|\?|\$1|\:a1)/i =~ sql } assert queries.none? { |sql| /WHERE/i =~ sql } end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 94dfbc3346..1db52af59b 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -714,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] @@ -791,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_test.rb b/activerecord/test/cases/attribute_test.rb index a24a4fc6a4..b1b8639696 100644 --- a/activerecord/test/cases/attribute_test.rb +++ b/activerecord/test/cases/attribute_test.rb @@ -242,5 +242,12 @@ module ActiveRecord attribute.with_value_from_user(1) end end + + test "with_type preserves mutations" do + attribute = Attribute.from_database(:foo, "", Type::Value.new) + attribute.value << "1" + + assert_equal 1, attribute.with_type(Type::Integer.new).value + end end end diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index 2991ca8b76..7bcaa53aa2 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -38,7 +38,7 @@ module ActiveRecord data.reload assert_equal 2, data.overloaded_float - assert_kind_of Fixnum, OverloadedType.last.overloaded_float + assert_kind_of Integer, OverloadedType.last.overloaded_float assert_equal 2.0, UnoverloadedType.last.overloaded_float assert_kind_of Float, UnoverloadedType.last.overloaded_float end @@ -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 @@ -135,6 +144,17 @@ module ActiveRecord 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! diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 3608063b01..9e3266b7d6 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -749,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 @@ -809,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 diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index ecdf508e3e..00e4e50ea1 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -14,7 +14,6 @@ require 'models/auto_id' require 'models/boolean' require 'models/column_name' require 'models/subscriber' -require 'models/keyboard' require 'models/comment' require 'models/minimalistic' require 'models/warehouse_thing' @@ -25,7 +24,6 @@ require 'models/joke' require 'models/bird' require 'models/car' require 'models/bulb' -require 'rexml/document' require 'concurrent/atomic/count_down_latch' class FirstAbstractClass < ActiveRecord::Base @@ -804,7 +802,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 @@ -940,7 +938,7 @@ class BasicsTest < ActiveRecord::TestCase assert_kind_of Integer, m1.world_population assert_equal 6000000000, m1.world_population - assert_kind_of Fixnum, m1.my_house_population + assert_kind_of Integer, m1.my_house_population assert_equal 3, m1.my_house_population assert_kind_of BigDecimal, m1.bank_balance @@ -968,7 +966,7 @@ class BasicsTest < ActiveRecord::TestCase assert_kind_of Integer, m1.world_population assert_equal 6000000000, m1.world_population - assert_kind_of Fixnum, m1.my_house_population + assert_kind_of Integer, m1.my_house_population assert_equal 3, m1.my_house_population assert_kind_of BigDecimal, m1.bank_balance @@ -1504,6 +1502,10 @@ class BasicsTest < ActiveRecord::TestCase assert_not_equal Post.new.hash, Post.new.hash end + test "records of different classes have different hashes" do + assert_not_equal Post.new(id: 1).hash, Developer.new(id: 1).hash + end + test "resetting column information doesn't remove attribute methods" do topic = topics(:first) diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 3602ee7ba2..db71840658 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -108,7 +108,7 @@ class EachTest < ActiveRecord::TestCase end end - def test_find_in_batches_should_finish_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, finish: 5) do |batch| assert_kind_of Array, batch @@ -164,6 +164,42 @@ class EachTest < ActiveRecord::TestCase 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_overridden + # Set the config option which will be overridden + 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 special_posts_ids = SpecialPostWithDefaultScope.all.map(&:id).sort posts = [] @@ -316,7 +352,7 @@ class EachTest < ActiveRecord::TestCase end end - def test_in_batches_should_finish_the_end_option + 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 diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index cd9c76f1f0..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) diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index c922a8d1c2..cfae700159 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -124,7 +124,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_generate_valid_sql_with_joins_and_group - assert_nothing_raised ActiveRecord::StatementInvalid do + assert_nothing_raised do AuditLog.joins(:developer).group(:id).count end end @@ -482,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) @@ -742,7 +746,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_reference_correct_aliases_while_joining_tables_of_has_many_through_association - assert_nothing_raised ActiveRecord::StatementInvalid do + assert_nothing_raised do developer = Developer.create!(name: 'developer') developer.ratings.includes(comment: :post).where(posts: { id: 1 }).count end 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/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 580568c8ac..c7ca428ab7 100644 --- a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb +++ b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb @@ -37,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 9b1865e8bb..a019cc6490 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -4,43 +4,40 @@ 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) + @spec_name = "primary" + @pool = @handler.establish_connection(ActiveRecord::Base.configurations['arunit']) + 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.to_hash) + + 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) - end - def test_retrieve_connection_pool - assert_not_nil @handler.retrieve_connection_pool(@klass) + assert_not_nil @handler.retrieve_connection_pool(@spec_name) end - def test_retrieve_connection_pool_uses_superclass_when_no_subclass_connection - assert_not_nil @handler.retrieve_connection_pool(@subklass) - 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) - - @handler.remove_connection @subklass - assert_same @pool, @handler.retrieve_connection_pool(@subklass) + def test_retrieve_connection_pool_with_invalid_id + assert_nil @handler.retrieve_connection_pool("foo") end def test_connection_pools @@ -79,7 +76,7 @@ module ActiveRecord pid = fork { rd.close - pool = @handler.retrieve_connection_pool(@klass) + pool = @handler.retrieve_connection_pool(@spec_name) wr.write Marshal.dump pool.schema_cache.size wr.close exit! @@ -91,6 +88,36 @@ module ActiveRecord 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/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb index 9ee92a3cd2..f25b85e8a7 100644 --- a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb +++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb @@ -27,7 +27,7 @@ module ActiveRecord ENV['DATABASE_URL'] = "postgres://localhost/foo" config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } actual = resolve_spec(:default_env, config) - expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } + expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost", "name"=>"default_env" } assert_equal expected, actual end @@ -37,7 +37,7 @@ module ActiveRecord config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } actual = resolve_spec(:foo, config) - expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost" } + expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost","name"=>"foo" } assert_equal expected, actual end @@ -47,7 +47,7 @@ module ActiveRecord config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } actual = resolve_spec(:foo, config) - expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost" } + expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost","name"=>"foo" } assert_equal expected, actual end @@ -55,7 +55,7 @@ module ActiveRecord ENV['DATABASE_URL'] = "postgres://localhost/foo" config = { "production" => { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost" } } actual = resolve_spec(:production, config) - expected = { "adapter"=>"not_postgres", "database"=>"not_foo", "host"=>"localhost" } + expected = { "adapter"=>"not_postgres", "database"=>"not_foo", "host"=>"localhost", "name"=>"production" } assert_equal expected, actual end @@ -93,7 +93,7 @@ module ActiveRecord ENV['DATABASE_URL'] = "ibm-db://localhost/foo" config = { "default_env" => { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost" } } actual = resolve_spec(:default_env, config) - expected = { "adapter"=>"ibm_db", "database"=>"foo", "host"=>"localhost" } + expected = { "adapter"=>"ibm_db", "database"=>"foo", "host"=>"localhost", "name"=>"default_env" } assert_equal expected, actual end diff --git a/activerecord/test/cases/connection_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb index 7566863653..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 diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb index d43668e57c..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,7 +21,7 @@ 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 @@ -27,17 +29,12 @@ module ActiveRecord 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 = [] @@ -51,46 +48,41 @@ 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 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 "/path", response_body.to_path end @@ -98,9 +90,23 @@ module ActiveRecord test "doesn't mutate the original response" do original_response = [200, {}, 'hi'] app = lambda { |_| original_response } - ConnectionManagement.new(app).call(@env)[2] + 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 efa3e0455e..a45ee281c7 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -335,11 +335,10 @@ 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 diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb index 3c2f5d4219..b30a83d9ce 100644 --- a/activerecord/test/cases/connection_specification/resolver_test.rb +++ b/activerecord/test/cases/connection_specification/resolver_test.rb @@ -28,7 +28,8 @@ module ActiveRecord assert_equal({ "adapter" => "abstract", "host" => "foo", - "encoding" => "utf8" }, spec) + "encoding" => "utf8", + "name" => "production"}, spec) end def test_url_sub_key @@ -36,7 +37,8 @@ module ActiveRecord assert_equal({ "adapter" => "abstract", "host" => "foo", - "encoding" => "utf8" }, spec) + "encoding" => "utf8", + "name" => "production"}, spec) end def test_url_sub_key_merges_correctly @@ -46,7 +48,8 @@ module ActiveRecord "adapter" => "abstract", "host" => "foo", "encoding" => "utf8", - "pool" => "3" }, spec) + "pool" => "3", + "name" => "production"}, spec) end def test_url_host_no_db @@ -57,6 +60,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({ @@ -107,9 +116,19 @@ module ActiveRecord assert_equal({ "adapter" => "sqlite3", "database" => "foo", - "encoding" => "utf8" }, spec) + "encoding" => "utf8", + "name" => "production"}, 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 922cb59280..66b4c3f1ff 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -151,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 diff --git a/activerecord/test/cases/database_statements_test.rb b/activerecord/test/cases/database_statements_test.rb index ba085991e0..3169408ac0 100644 --- a/activerecord/test/cases/database_statements_test.rb +++ b/activerecord/test/cases/database_statements_test.rb @@ -13,6 +13,12 @@ class DatabaseStatementsTest < ActiveRecord::TestCase assert_not_nil return_the_inserted_id(method: :create) end + def test_insert_update_delete_sql_is_deprecated + assert_deprecated { @connection.insert_sql("INSERT INTO accounts (firm_id,credit_limit) VALUES (42,5000)") } + assert_deprecated { @connection.update_sql("UPDATE accounts SET credit_limit = 6000 WHERE firm_id = 42") } + assert_deprecated { @connection.delete_sql("DELETE FROM accounts WHERE firm_id = 42") } + end + private def return_the_inserted_id(method:) diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb index e996d142a2..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 diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index 69b0487dd8..067513e24c 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -201,8 +201,7 @@ if current_adapter?(: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 cd1967c373..f9794518c7 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -5,23 +5,6 @@ require 'models/parrot' require 'models/person' # For optimistic locking require 'models/aircraft' -class Pirate # Just reopening it, not defining it - attr_accessor :detected_changes_in_after_update # Boolean for if changes are detected - attr_accessor :changes_detected_in_after_update # Actual changes - - after_update :check_changes - -private - # after_save/update and the model itself - # can end up checking dirty status and acting on the results - def check_changes - if self.changed? - self.detected_changes_in_after_update = true - self.changes_detected_in_after_update = self.changes - end - end -end - class NumericData < ActiveRecord::Base self.table_name = 'numeric_data' end @@ -37,8 +20,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' @@ -734,6 +717,17 @@ class DirtyTest < ActiveRecord::TestCase assert_equal "arr", pirate.catchphrase end + test "attributes assigned but not selected are dirty" do + person = Person.select(:id).first + refute person.changed? + + person.first_name = "Sean" + assert person.changed? + + person.first_name = nil + assert person.changed? + end + private def with_partial_writes(klass, on = true) old = klass.partial_writes? diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 75a74c052d..6eaaa30cd0 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -43,7 +43,7 @@ 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 @@ -101,7 +101,7 @@ class FinderTest < ActiveRecord::TestCase def test_find_with_ids_where_and_limit # Please note that Topic 1 is the only not approved so - # if it were among the first 3 it would raise a ActiveRecord::RecordNotFound + # 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 @@ -173,11 +173,9 @@ class FinderTest < ActiveRecord::TestCase end end - def test_exists_fails_when_parameter_has_invalid_type - assert_raises(RangeError) do - assert_equal false, Topic.exists?(("9"*53).to_i) # number that's bigger than int - end + def test_exists_returns_false_when_parameter_has_invalid_type assert_equal false, Topic.exists?("foo") + assert_equal false, Topic.exists?(("9"*53).to_i) # number that's bigger than int end def test_exists_does_not_select_columns_without_alias @@ -486,6 +484,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! @@ -516,16 +574,44 @@ class FinderTest < ActiveRecord::TestCase 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 @@ -564,11 +650,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) } @@ -1058,7 +1149,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 diff --git a/activerecord/test/cases/fixture_set/file_test.rb b/activerecord/test/cases/fixture_set/file_test.rb index 242e7a9bec..e64b90507e 100644 --- a/activerecord/test/cases/fixture_set/file_test.rb +++ b/activerecord/test/cases/fixture_set/file_test.rb @@ -135,6 +135,12 @@ END 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 da934ab8fe..9455d4886c 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -857,7 +857,7 @@ class FoxyFixturesTest < ActiveRecord::TestCase assert_equal("X marks the spot!", pirates(:mark).catchphrase) end - def test_supports_label_interpolation_for_fixnum_label + def test_supports_label_interpolation_for_integer_label assert_equal("#1 pirate!", pirates(1).catchphrase) end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 95f8706d73..d2fdf03e9d 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -1,5 +1,3 @@ -require File.expand_path('../../../../load_paths', __FILE__) - require 'config' require 'active_support/testing/autorun' 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 7da6842047..e234b9a6a9 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -441,12 +441,7 @@ 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 diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index e030f6c588..aba854820b 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -151,6 +151,14 @@ module ActiveRecord end end + class RevertCustomForeignKeyTable < SilentMigration + def change + change_table(:horses) do |t| + t.references :owner, foreign_key: { to_table: :developers } + end + end + end + setup do @verbose_was, ActiveRecord::Migration.verbose = ActiveRecord::Migration.verbose, false end @@ -353,6 +361,13 @@ module ActiveRecord ActiveRecord::Base.table_name_prefix = ActiveRecord::Base.table_name_suffix = '' end + def test_migrations_can_handle_foreign_keys_to_specific_tables + migration = RevertCustomForeignKeyTable.new + InvertibleMigration.migrate(:up) + migration.migrate(:up) + migration.migrate(:down) + end + # MySQL 5.7 and Oracle do not allow to create duplicate indexes on the same columns unless current_adapter?(:Mysql2Adapter, :OracleAdapter) def test_migrate_revert_add_index_with_name diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 4fe76e563a..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 @@ -441,7 +447,7 @@ unless in_memory_db? def test_lock_sending_custom_lock_statement Person.transaction do person = Person.find(1) - assert_sql(/LIMIT \$\d 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 707a2d1da1..c97960a412 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -215,5 +215,11 @@ class LogSubscriberTest < ActiveRecord::TestCase wait assert_match(/<16 bytes of binary data>/, @logger.logged(:debug).join) end + + def test_binary_data_hash + Binary.create(data: { a: 1 }) + wait + assert_match(/<7 bytes of binary data>/, @logger.logged(:debug).join) + end end end diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index c7a1b81a75..29546525f3 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -154,7 +154,7 @@ module ActiveRecord assert_equal String, bob.first_name.class assert_equal String, bob.last_name.class assert_equal String, bob.bio.class - assert_equal Fixnum, bob.age.class + assert_kind_of Integer, bob.age assert_equal Time, bob.birthday.class if current_adapter?(:OracleAdapter) diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb index 6a9cdd9d29..60ca90464d 100644 --- a/activerecord/test/cases/migration/compatibility_test.rb +++ b/activerecord/test/cases/migration/compatibility_test.rb @@ -21,7 +21,7 @@ module ActiveRecord teardown do connection.drop_table :testings rescue nil ActiveRecord::Migration.verbose = @verbose_was - ActiveRecord::SchemaMigration.delete_all + ActiveRecord::SchemaMigration.delete_all rescue nil end def test_migration_doesnt_remove_named_index @@ -71,6 +71,48 @@ module ActiveRecord 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 0a7b57455c..920c472c73 100644 --- a/activerecord/test/cases/migration/create_join_table_test.rb +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -132,6 +132,13 @@ module ActiveRecord 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 diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb index b01415afb2..9e19eb9f73 100644 --- a/activerecord/test/cases/migration/references_foreign_key_test.rb +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -144,6 +144,52 @@ module ActiveRecord @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 diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb index b9ce6bbc55..70c64f3e71 100644 --- a/activerecord/test/cases/migration/references_statements_test.rb +++ b/activerecord/test/cases/migration/references_statements_test.rb @@ -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/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 f51e366b1d..36b6662820 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -69,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 @@ -176,7 +180,7 @@ class MigrationTest < ActiveRecord::TestCase # is_a?(Bignum) assert_kind_of Integer, b.world_population assert_equal 6000000000, b.world_population - assert_kind_of Fixnum, b.my_house_population + assert_kind_of Integer, b.my_house_population assert_equal 3, b.my_house_population assert_kind_of BigDecimal, b.bank_balance assert_equal BigDecimal("1586.43"), b.bank_balance @@ -192,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) @@ -202,7 +204,7 @@ class MigrationTest < ActiveRecord::TestCase assert_in_delta BigDecimal("2.71828182845905"), b.value_of_e, 0.00000000000001 else # - SQL standard is an integer - assert_kind_of Fixnum, b.value_of_e + assert_kind_of Integer, b.value_of_e assert_equal 2, b.value_of_e end @@ -357,14 +359,14 @@ class MigrationTest < ActiveRecord::TestCase def test_internal_metadata_table_name original_internal_metadata_table_name = ActiveRecord::Base.internal_metadata_table_name - assert_equal "active_record_internal_metadatas", ActiveRecord::InternalMetadata.table_name - ActiveRecord::Base.table_name_prefix = "prefix_" - ActiveRecord::Base.table_name_suffix = "_suffix" + 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 "prefix_active_record_internal_metadatas_suffix", ActiveRecord::InternalMetadata.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 "prefix_changed_suffix", ActiveRecord::InternalMetadata.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 @@ -426,6 +428,38 @@ class MigrationTest < ActiveRecord::TestCase ENV["RACK_ENV"] = original_rack_env end + def test_internal_metadata_stores_environment_when_other_data_exists + ActiveRecord::InternalMetadata.delete_all + ActiveRecord::InternalMetadata[:foo] = 'bar' + + current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + migrations_path = MIGRATIONS_ROOT + "/valid" + old_path = ActiveRecord::Migrator.migrations_paths + ActiveRecord::Migrator.migrations_paths = migrations_path + + current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + ActiveRecord::Migrator.up(migrations_path) + assert_equal current_env, ActiveRecord::InternalMetadata[:environment] + assert_equal 'bar', ActiveRecord::InternalMetadata[:foo] + ensure + ActiveRecord::Migrator.migrations_paths = old_path + 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 @@ -503,13 +537,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" @@ -517,12 +550,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) @@ -530,7 +562,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 @@ -573,8 +605,7 @@ class MigrationTest < ActiveRecord::TestCase end if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) - def test_out_of_range_limit_should_raise - Person.connection.drop_table :test_limits rescue nil + 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 @@ -582,16 +613,22 @@ 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 - Person.connection.drop_table :test_limits rescue nil + assert_match(/No text type has byte length #{0xfffffffff}/, e.message) + ensure + Person.connection.drop_table :test_text_limits, if_exists: true end end @@ -713,7 +750,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 @@ -725,11 +762,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 @@ -1091,4 +1128,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/modules_test.rb b/activerecord/test/cases/modules_test.rb index 7f31325f47..486bcc22df 100644 --- a/activerecord/test/cases/modules_test.rb +++ b/activerecord/test/cases/modules_test.rb @@ -63,7 +63,7 @@ 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 @@ -72,7 +72,7 @@ class ModulesTest < ActiveRecord::TestCase 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/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb index ae18573126..d05cb22740 100644 --- a/activerecord/test/cases/multiparameter_attributes_test.rb +++ b/activerecord/test/cases/multiparameter_attributes_test.rb @@ -11,15 +11,13 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase topic.attributes = attributes # note that extra #to_date call allows test to pass for Oracle, which # treats dates/times the same - assert_date_from_db Date.new(2004, 6, 24), topic.last_read.to_date + assert_equal Date.new(2004, 6, 24), topic.last_read.to_date end def test_multiparameter_attributes_on_date_with_empty_year attributes = { "last_read(1i)" => "", "last_read(2i)" => "6", "last_read(3i)" => "24" } topic = Topic.find(1) topic.attributes = attributes - # note that extra #to_date call allows test to pass for Oracle, which - # treats dates/times the same assert_nil topic.last_read end @@ -27,8 +25,6 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "", "last_read(3i)" => "24" } topic = Topic.find(1) topic.attributes = attributes - # note that extra #to_date call allows test to pass for Oracle, which - # treats dates/times the same assert_nil topic.last_read end @@ -36,8 +32,6 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "" } topic = Topic.find(1) topic.attributes = attributes - # note that extra #to_date call allows test to pass for Oracle, which - # treats dates/times the same assert_nil topic.last_read end @@ -45,8 +39,6 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase attributes = { "last_read(1i)" => "", "last_read(2i)" => "6", "last_read(3i)" => "" } topic = Topic.find(1) topic.attributes = attributes - # note that extra #to_date call allows test to pass for Oracle, which - # treats dates/times the same assert_nil topic.last_read end @@ -54,8 +46,6 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "", "last_read(3i)" => "" } topic = Topic.find(1) topic.attributes = attributes - # note that extra #to_date call allows test to pass for Oracle, which - # treats dates/times the same assert_nil topic.last_read end @@ -63,8 +53,6 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase attributes = { "last_read(1i)" => "", "last_read(2i)" => "", "last_read(3i)" => "24" } topic = Topic.find(1) topic.attributes = attributes - # note that extra #to_date call allows test to pass for Oracle, which - # treats dates/times the same assert_nil topic.last_read 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 6fbc6196cc..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 @@ -173,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 @@ -504,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 @@ -512,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 @@ -529,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 @@ -582,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', @@ -699,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 @@ -734,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") @@ -810,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 af15e63d9c..56092aaa0c 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -183,7 +183,7 @@ class PersistenceTest < ActiveRecord::TestCase 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! diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 7e18313c00..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 @@ -230,9 +228,10 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase 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 @@ -262,6 +261,13 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase 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 @@ -345,9 +351,9 @@ if current_adapter?(:PostgreSQLAdapter, :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 diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index d84653e4c9..d5c01315c1 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 @@ -170,7 +133,6 @@ class QueryCacheTest < ActiveRecord::TestCase def test_cache_is_flat Task.cache do - Topic.columns # don't count this query assert_queries(1) { Topic.find(1); Topic.find(1); } end @@ -181,13 +143,12 @@ class QueryCacheTest < ActiveRecord::TestCase def test_cache_does_not_wrap_string_results_in_arrays Task.cache do - # Oracle adapter returns count() as Fixnum or Float + # Oracle adapter returns count() as Integer 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, :PostgreSQLAdapter) # Future versions of the sqlite3 adapter will return numeric - assert_instance_of Fixnum, - Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") + assert_instance_of Fixnum, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") else assert_instance_of String, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") end @@ -213,6 +174,22 @@ class QueryCacheTest < ActiveRecord::TestCase ActiveRecord::Base.configurations = conf end + def test_cache_is_not_available_when_using_a_not_connected_connection + spec_name = Task.connection_specification_name + conf = ActiveRecord::Base.configurations['arunit'].merge('name' => 'test2') + ActiveRecord::Base.connection_handler.establish_connection(conf) + Task.connection_specification_name = "test2" + refute Task.connected? + + Task.cache do + Task.connection # warmup postgresql connection setup queries + assert_queries(2) { Task.find(1); Task.find(1) } + end + ensure + ActiveRecord::Base.connection_handler.remove_connection(Task.connection_specification_name) + Task.connection_specification_name = spec_name + end + def test_query_cache_doesnt_leak_cached_results_of_rolled_back_queries ActiveRecord::Base.connection.enable_query_cache! post = Post.first @@ -244,6 +221,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 diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index 6d91f96bf6..c01c82f4f5 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -102,9 +102,9 @@ module ActiveRecord assert_equal float.to_s, @quoter.quote(float, nil) end - def test_quote_fixnum - fixnum = 1 - assert_equal fixnum.to_s, @quoter.quote(fixnum, nil) + def test_quote_integer + integer = 1 + assert_equal integer.to_s, @quoter.quote(integer, nil) end def test_quote_bignum diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index d0f60a84b5..ffb2da7a26 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -26,6 +26,10 @@ module ActiveRecord def sanitize_sql_for_order(sql) sql end + + def arel_attribute(name, table) + table[name] + end end def relation diff --git a/activerecord/test/cases/relation/or_test.rb b/activerecord/test/cases/relation/or_test.rb index 28a0862f91..ce8c5ca489 100644 --- a/activerecord/test/cases/relation/or_test.rb +++ b/activerecord/test/cases/relation/or_test.rb @@ -82,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 index 62f0a7cc49..0e0e23b24b 100644 --- a/activerecord/test/cases/relation/record_fetch_warning_test.rb +++ b/activerecord/test/cases/relation/record_fetch_warning_test.rb @@ -1,28 +1,40 @@ require 'cases/helper' require 'models/post' +require 'active_record/relation/record_fetch_warning' module ActiveRecord class RecordFetchWarningTest < ActiveRecord::TestCase fixtures :posts - def test_warn_on_records_fetched_greater_than - original_logger = ActiveRecord::Base.logger - orginal_warn_on_records_fetched_greater_than = ActiveRecord::Base.warn_on_records_fetched_greater_than + 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 - log = StringIO.new - ActiveRecord::Base.logger = ActiveSupport::Logger.new(log) + 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 - require 'active_record/relation/record_fetch_warning' + Post.all.to_a - ActiveRecord::Base.warn_on_records_fetched_greater_than = 1 + 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_match(/Query fetched/, log.string) - ensure - ActiveRecord::Base.logger = original_logger - ActiveRecord::Base.warn_on_records_fetched_greater_than = orginal_warn_on_records_fetched_greater_than + 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 bc6378b90e..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])) diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 0638edacbd..5604124bb3 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -19,6 +19,7 @@ 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, @@ -223,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 @@ -324,6 +358,12 @@ class RelationTest < ActiveRecord::TestCase 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 @@ -1046,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 @@ -1239,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 @@ -1939,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/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 7b93d20e05..12f4196724 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -29,8 +29,22 @@ class SchemaDumperTest < ActiveRecord::TestCase ActiveRecord::SchemaMigration.delete_all end - def test_magic_comment - assert_match "# encoding: #{Encoding.default_external.name}", standard_dump + 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_schema_dump @@ -38,7 +52,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 "active_record_internal_metadatas"}, output + assert_no_match %r{create_table "ar_internal_metadata"}, output end def test_schema_dump_uses_force_cascade_on_create_table @@ -74,7 +88,7 @@ class SchemaDumperTest < ActiveRecord::TestCase next if column_set.empty? lengths = column_set.map do |column| - if match = column.match(/\bt\.\w+\s+"/) + if match = column.match(/\bt\.\w+\s+(?="\w+?")/) match[0].length end end.compact @@ -147,10 +161,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 @@ -159,7 +173,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 "active_record_internal_metadatas"}, output + assert_no_match %r{create_table "ar_internal_metadata"}, output end def test_schema_dump_with_regexp_ignored_table @@ -167,7 +181,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 "active_record_internal_metadatas"}, output + assert_no_match %r{create_table "ar_internal_metadata"}, output end def test_schema_dumps_index_columns_in_right_order @@ -204,12 +218,17 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{t\.boolean\s+"has_fun",.+default: false}, output end - 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 - end + def test_schema_dump_does_not_include_limit_for_text_field + output = standard_dump + assert_match %r{t\.text\s+"params"$}, output + end + + def test_schema_dump_does_not_include_limit_for_binary_field + output = standard_dump + assert_match %r{t\.binary\s+"data"$}, output + end + if current_adapter?(:Mysql2Adapter) def test_schema_dump_includes_length_for_mysql_binary_fields output = standard_dump assert_match %r{t\.binary\s+"var_binary",\s+limit: 255$}, output @@ -219,11 +238,11 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dump_includes_length_for_mysql_blob_and_text_fields output = standard_dump 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+"normal_blob"$}, output assert_match %r{t\.binary\s+"medium_blob",\s+limit: 16777215$}, output assert_match %r{t\.binary\s+"long_blob",\s+limit: 4294967295$}, output assert_match %r{t\.text\s+"tiny_text",\s+limit: 255$}, output - assert_match %r{t\.text\s+"normal_text",\s+limit: 65535$}, output + assert_match %r{t\.text\s+"normal_text"$}, output assert_match %r{t\.text\s+"medium_text",\s+limit: 16777215$}, output assert_match %r{t\.text\s+"long_text",\s+limit: 4294967295$}, output end @@ -248,12 +267,12 @@ class SchemaDumperTest < ActiveRecord::TestCase 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\.integer\s+"big_int_data_points\",\s+limit: 8,\s+array: true}, output + assert_match %r{t\.bigint\s+"big_int_data_points\",\s+array: true}, output end def test_schema_dump_allows_array_of_decimal_defaults @@ -261,6 +280,11 @@ class SchemaDumperTest < ActiveRecord::TestCase 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? def test_schema_dump_includes_extensions connection = ActiveRecord::Base.connection @@ -323,9 +347,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") @@ -345,7 +369,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 "active_record_internal_metadatas"}, 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 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 ad5ca70f36..dcd09b6973 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -4,6 +4,7 @@ require 'models/comment' require 'models/developer' require 'models/computer' require 'models/vehicle' +require 'models/cat' class DefaultScopingTest < ActiveRecord::TestCase fixtures :developers, :posts, :comments @@ -374,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 @@ -473,4 +486,15 @@ class DefaultScopingTest < ActiveRecord::TestCase 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 7a8eaeccb7..0e277ed235 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -301,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, @@ -440,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') @@ -525,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/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 6056156698..846be857d0 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -295,4 +295,37 @@ class SerializedAttributeTest < ActiveRecord::TestCase 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 72c5c16555..2f00241de2 100644 --- a/activerecord/test/cases/suppressor_test.rb +++ b/activerecord/test/cases/suppressor_test.rb @@ -46,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 49df6628eb..510bb088c8 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -8,6 +8,13 @@ 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 @@ -161,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 @@ -282,38 +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'] - ActiveRecord::Tasks::DatabaseTasks.migrations_paths = 'custom/path' ENV['VERBOSE'] = 'false' ENV['VERSION'] = '4' ActiveRecord::Migrator.expects(:migrate).with('custom/path', 4) ActiveRecord::Tasks::DatabaseTasks.migrate ensure - ActiveRecord::Tasks::DatabaseTasks.migrations_paths = nil 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 7c1da51990..70e406038f 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -1,4 +1,5 @@ require 'cases/helper' +require 'active_record/tasks/database_tasks' if current_adapter?(:Mysql2Adapter) 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_without_database @@ -48,14 +56,20 @@ 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 @@ -77,6 +91,13 @@ module ActiveRecord ActiveRecord::Base.stubs(:establish_connection). raises(@error). then.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_root_password_is_requested @@ -160,6 +181,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_mysql_database @@ -173,6 +201,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 MySQLPurgeTest < ActiveRecord::TestCase @@ -307,6 +341,5 @@ module ActiveRecord 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 ba53f340ae..99d73e91a4 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 @@ -63,14 +71,20 @@ module ActiveRecord assert_raises(Exception) { 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('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 @@ -261,18 +288,17 @@ module ActiveRecord def test_structure_load filename = "awesome-file.sql" - Kernel.expects(:system).with('psql', '-q', '-f', filename, @configuration['database']).returns(true) + Kernel.expects(:system).with('psql', '-v', 'ON_ERROR_STOP=1', '-q', '-f', filename, @configuration['database']).returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end def test_structure_load_accepts_path_with_spaces filename = "awesome file.sql" - Kernel.expects(:system).with('psql', '-q', '-f', filename, @configuration['database']).returns(true) + Kernel.expects(:system).with('psql', '-v', 'ON_ERROR_STOP=1', '-q', '-f', filename, @configuration['database']).returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end end - end end diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb index 0aea0c3b38..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 @@ -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 87299c0dab..c8adc21bbc 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -12,10 +12,6 @@ module ActiveRecord SQLCounter.clear_log end - def assert_date_from_db(expected, actual, message = nil) - assert_equal expected.to_s, actual.to_s, message - end - def capture_sql SQLCounter.clear_log yield diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb index 3b6e4dcc2b..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 diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 970f6bcf4a..937b84bccc 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -98,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 @@ -120,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 diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index 637f89196e..8a7f19293d 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -34,6 +34,7 @@ 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_create_commit { |record| record.do_after_commit(:create) } after_update_commit { |record| record.do_after_commit(: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/validations/absence_validation_test.rb b/activerecord/test/cases/validations/absence_validation_test.rb index dd43ee358c..c0b3750bcc 100644 --- a/activerecord/test/cases/validations/absence_validation_test.rb +++ b/activerecord/test/cases/validations/absence_validation_test.rb @@ -57,19 +57,17 @@ class AbsenceValidationTest < ActiveRecord::TestCase assert_nothing_raised { boy_klass.new(face: face_with_to_a).valid? } end - def test_does_not_validate_if_parent_record_is_validate_false + def test_validates_absence_of_virtual_attribute_on_model repair_validations(Interest) do - Interest.validates_absence_of(:topic) - interest = Interest.new(topic: Topic.new(title: "Math")) - interest.save!(validate: false) - assert interest.persisted? + Interest.send(:attr_accessor, :token) + Interest.validates_absence_of(:token) - man = Man.new(interest_ids: [interest.id]) - man.save! - - assert_equal man.interests.size, 1 + interest = Interest.create!(topic: 'Thought Leadering') assert interest.valid? - assert man.valid? + + interest.token = 'tl' + + assert interest.invalid? end end end diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb index 981239c4d6..5b5307489a 100644 --- a/activerecord/test/cases/validations/i18n_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_validation_test.rb @@ -41,14 +41,10 @@ class I18nValidationTest < ActiveRecord::TestCase [ "given custom message", {:message => "custom"}, {:message => "custom"}], [ "given if condition", {:if => lambda { true }}, {}], [ "given unless condition", {:unless => lambda { false }}, {}], - [ "given option that is not reserved", {:format => "jpg"}, {:format => "jpg" }] - # TODO Add :on case, but below doesn't work, because then the validation isn't run for some reason - # even when using .save instead .valid? - # [ "given on condition", {on: :save}, {}] + [ "given option that is not reserved", {:format => "jpg"}, {:format => "jpg" }], + [ "given on condition", {on: [:create, :update] }, {}] ] - # 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 @@ -59,8 +55,6 @@ class I18nValidationTest < ActiveRecord::TestCase 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 @@ -70,8 +64,6 @@ class I18nValidationTest < ActiveRecord::TestCase 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 c5d8f8895c..78263fd955 100644 --- a/activerecord/test/cases/validations/length_validation_test.rb +++ b/activerecord/test/cases/validations/length_validation_test.rb @@ -61,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 7502a55391..4b0a590adb 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -5,6 +5,7 @@ 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 @@ -48,6 +49,18 @@ class BigIntReverseTest < ActiveRecord::Base 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 @@ -336,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 @@ -412,23 +447,6 @@ 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) - - Reply.create!(content: "Topic Title") - - reply = Reply.new(content: "Topic Title") - reply.save!(validate: false) - assert reply.persisted? - - topic = Topic.new(reply_ids: [reply.id]) - topic.save! - - assert_equal topic.replies.size, 1 - assert reply.valid? - assert topic.valid? - end - def test_validate_uniqueness_of_custom_primary_key klass = Class.new(ActiveRecord::Base) do self.table_name = "keyboards" @@ -469,4 +487,46 @@ class UniquenessValidationTest < ActiveRecord::TestCase 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 d04f4f7ce7..85e33d2218 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -130,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 diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index 56909a8630..d1c9a00786 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -109,6 +109,16 @@ class YamlSerializationTest < ActiveRecord::TestCase assert_equal("Have a nice day", topic.content) end + def test_yaml_encoding_keeps_mutations + author = Author.first + author.name = "Sean" + dumped = YAML.load(YAML.dump(author)) + + assert_equal "Sean", dumped.name + assert_equal author.name_was, dumped.name_was + assert_equal author.changes, dumped.changes + end + private def yaml_fixture(file_name) 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/migrations/to_copy/1_people_have_hobbies.rb b/activerecord/test/migrations/to_copy/1_people_have_hobbies.rb index 607113b091..76734bcd7d 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::Current +class PeopleHaveHobbies < 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 d4cbddab50..7f883dbb45 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::Current +class PeopleHaveDescriptions < ActiveRecord::Migration::Current def self.up add_column "people", "description", :text end diff --git a/activerecord/test/migrations/to_copy2/2_create_comments.rb b/activerecord/test/migrations/to_copy2/2_create_comments.rb index 2e9f5ec6bc..d361847d4b 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::Current +class CreateComments < 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 8f81805fe1..1a863367dd 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::Current +class PeopleHaveHobbies < 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 607113b091..76734bcd7d 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::Current +class PeopleHaveHobbies < 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 d4cbddab50..7f883dbb45 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::Current +class PeopleHaveDescriptions < ActiveRecord::Migration::Current def self.up add_column "people", "description", :text end diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 0d90cbb110..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,6 +144,14 @@ class Author < ActiveRecord::Base has_many :posts_with_signature, ->(record) { where("posts.title LIKE ?", "%by #{record.name.downcase}%") }, class_name: "Post" + 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/car.rb b/activerecord/test/models/car.rb index 778c22b1f6..0f37e9a289 100644 --- a/activerecord/test/models/car.rb +++ b/activerecord/test/models/car.rb @@ -12,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/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/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/customer.rb b/activerecord/test/models/customer.rb index afe4b3d707..3338aaf7e1 100644 --- a/activerecord/test/models/customer.rb +++ b/activerecord/test/models/customer.rb @@ -64,7 +64,12 @@ class Fullname def self.parse(str) return nil unless str - new(*str.to_s.split) + + if str.is_a?(Hash) + new(str[:first], str[:last]) + else + new(*str.to_s.split) + end end def initialize(first, last = nil) 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/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/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/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/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index 701e6f45b3..5a49b38457 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -21,19 +21,19 @@ ActiveRecord::Schema.define do 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 @@ -62,7 +62,7 @@ SQL ActiveRecord::Base.connection.execute <<-SQL CREATE TABLE enum_tests ( - enum_column ENUM('text','blob','tiny','medium','long','unsigned') + enum_column ENUM('text','blob','tiny','medium','long','unsigned','bigint') ) SQL end diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index 3a5d73a0ed..24713f722a 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -106,4 +106,9 @@ _SQL 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 fcc4eee79c..628a59c2e3 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -1,10 +1,4 @@ ActiveRecord::Schema.define do - def except(adapter_names_to_exclude) - unless [adapter_names_to_exclude].flatten.include?(adapter_name) - yield - end - end - # ------------------------------------------------------------------- # # # # Please keep these create table statements in alphabetical order # @@ -202,12 +196,12 @@ 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 @@ -304,8 +298,8 @@ ActiveRecord::Schema.define do 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 @@ -422,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 @@ -616,6 +615,12 @@ ActiveRecord::Schema.define do 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 @@ -782,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 @@ -930,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 @@ -987,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 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/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 21c79949ca..d9f23c55ba 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,529 +1,64 @@ -* Match `HashWithIndifferentAccess#default`'s behaviour with `Hash#default`. +* Remove deprecated arguments in `assert_nothing_raised`. - *David Cornu* + *Rafel Mendonça França* -* Adds `:exception_object` key to `ActiveSupport::Notifications::Instrumenter` - payload when an exception is raised. +* `Date.to_s` doesn't produce too many spaces. For example, `to_s(:short)` + will now produce `01 Feb` instead of ` 1 Feb`. - Adds new key/value pair to payload when an exception is raised: - e.g. `:exception_object => #<RuntimeError: FAIL>`. + Fixes #25251. - *Ryan T. Hosford* + *Sean Griffin* -* Support extended grapheme clusters and UAX 29. +* Introduce Module#delegate_missing_to. - *Adam Roben* + When building a decorator, a common pattern emerges: -* Add petabyte and exabyte numeric conversion. - - *Akshay Vishnoi* - -## Rails 5.0.0.beta1 (December 18, 2015) ## - -* Add thread_m/cattr_accessor/reader/writer suite of methods for declaring class and module variables that live per-thread. - This makes it easy to declare per-thread globals that are encapsulated. Note: This is a sharp edge. A wild proliferation - of globals is A Bad Thing. But like other sharp tools, when it's right, it's right. - - Here's an example of a simple event tracking system where the object being tracked needs not pass a creator that it - doesn't need itself along: - - module Current - thread_mattr_accessor :account - thread_mattr_accessor :user - - def self.reset() self.account = self.user = nil end - end - - class ApplicationController < ActionController::Base - before_action :set_current - after_action { Current.reset } - - private - def set_current - Current.account = Account.find(params[:account_id]) - Current.user = Current.account.users.find(params[:user_id]) - end - end - - class MessagesController < ApplicationController - def create - @message = Message.create!(message_params) - end - end - - class Message < ApplicationRecord - has_many :events - after_create :track_created - - private - def track_created - events.create! origin: self, action: :create - end - end - - class Event < ApplicationRecord - belongs_to :creator, class_name: 'User' - before_validation { self.creator ||= Current.user } - end - - *DHH* - - -* Deprecated `Module#qualified_const_` in favour of the builtin Module#const_ - methods. - - *Genadi Samokovarov* - -* Deprecate passing string to define callback. - - *Yuichiro Kaneko* - -* `ActiveSupport::Cache::Store#namespaced_key`, - `ActiveSupport::Cache::MemCachedStore#escape_key`, and - `ActiveSupport::Cache::FileStore#key_file_path` - are deprecated and replaced with `normalize_key` that now calls `super`. - - `ActiveSupport::Cache::LocaleCache#set_cache_value` is deprecated and replaced with `write_cache_value`. - - *Michael Grosser* - -* Implements an evented file watcher to asynchronously detect changes in the - application source code, routes, locales, etc. - - This watcher is disabled by default, applications my enable it in the configuration: - - # config/environments/development.rb - config.file_watcher = ActiveSupport::EventedFileUpdateChecker - - This feature depends on the [listen](https://github.com/guard/listen) gem: - - group :development do - gem 'listen', '~> 3.0.5' - end - - *Puneet Agarwal* and *Xavier Noria* - -* Added `Time.days_in_year` to return the number of days in the given year, or the - current year if no argument is provided. - - *Jon Pascoe* - -* Updated `parameterize` to preserve the case of a string, optionally. - - Example: - - parameterize("Donald E. Knuth", separator: '_') # => "donald_e_knuth" - parameterize("Donald E. Knuth", preserve_case: true) # => "Donald-E-Knuth" - - *Swaathi Kakarla* - -* `HashWithIndifferentAccess.new` respects the default value or proc on objects - that respond to `#to_hash`. `.new_from_hash_copying_default` simply invokes `.new`. - All calls to `.new_from_hash_copying_default` are replaced with `.new`. - - *Gordon Chan* - -* Change Integer#year to return a Fixnum instead of a Float to improve - consistency. - - Integer#years returned a Float while the rest of the accompanying methods - (days, weeks, months, etc.) return a Fixnum. - - Before: - - 1.year # => 31557600.0 - - After: - - 1.year # => 31557600 - - *Konstantinos Rousis* - -* Handle invalid UTF-8 strings when HTML escaping. - - Use `ActiveSupport::Multibyte::Unicode.tidy_bytes` to handle invalid UTF-8 - strings in `ERB::Util.unwrapped_html_escape` and `ERB::Util.html_escape_once`. - Prevents user-entered input passed from a querystring into a form field from - causing invalid byte sequence errors. - - *Grey Baker* - -* Update `ActiveSupport::Multibyte::Chars#slice!` to return `nil` if the - arguments are out of bounds, to mirror the behavior of `String#slice!` - - *Gourav Tiwari* - -* Fix `number_to_human` so that 999999999 rounds to "1 Billion" instead of - "1000 Million". - - *Max Jacobson* - -* Fix `ActiveSupport::Deprecation#deprecate_methods` to report using the - current deprecator instance, where applicable. - - *Brandon Dunne* - -* `Cache#fetch` instrumentation marks whether it was a `:hit`. - - *Robin Clowers* - -* `assert_difference` and `assert_no_difference` now returns the result of the - yielded block. - - Example: - - post = assert_difference -> { Post.count }, 1 do - Post.create - end - - *Lucas Mazza* - -* Short-circuit `blank?` on date and time values since they are never blank. - - Fixes #21657. - - *Andrew White* - -* Replaced deprecated `ThreadSafe::Cache` with its successor `Concurrent::Map` now that - the thread_safe gem has been merged into concurrent-ruby. - - *Jerry D'Antonio* - -* Updated Unicode version to 8.0.0 - - *Anshul Sharma* - -* `number_to_currency` and `number_with_delimiter` now accept custom `delimiter_pattern` option - to handle placement of delimiter, to support currency formats like INR - - Example: - - number_to_currency(1230000, delimiter_pattern: /(\d+?)(?=(\d\d)+(\d)(?!\d))/, unit: '₹', format: "%u %n") - # => '₹ 12,30,000.00' - - *Vipul A M* - -* Deprecate `:prefix` option of `number_to_human_size` with no replacement. - - *Jean Boussier* - -* Fix `TimeWithZone#eql?` to properly handle `TimeWithZone` created from `DateTime`: - twz = DateTime.now.in_time_zone - twz.eql?(twz.dup) => true - - Fixes #14178. - - *Roque Pinel* - -* ActiveSupport::HashWithIndifferentAccess `select` and `reject` will now return - enumerator if called without block. - - Fixes #20095. - - *Bernard Potocki* - -* Removed `ActiveSupport::Concurrency::Latch`, superseded by `Concurrent::CountDownLatch` - from the concurrent-ruby gem. - - *Jerry D'Antonio* - -* Fix not calling `#default` on `HashWithIndifferentAccess#to_hash` when only - `default_proc` is set, which could raise. - - *Simon Eskildsen* - -* Fix setting `default_proc` on `HashWithIndifferentAccess#dup`. - - *Simon Eskildsen* - -* Fix a range of values for parameters of the Time#change. - - *Nikolay Kondratyev* - -* Add `Enumerable#pluck` to get the same values from arrays as from ActiveRecord - associations. - - Fixes #20339. - - *Kevin Deisz* + class Partition + def initialize(first_event) + @events = [ first_event ] + end -* Add a bang version to `ActiveSupport::OrderedOptions` get methods which will raise - an `KeyError` if the value is `.blank?`. + def people + if @events.first.detail.people.any? + @events.collect { |e| Array(e.detail.people) }.flatten.uniq + else + @events.collect(&:creator).uniq + end + end - Before: + private + def respond_to_missing?(name, include_private = false) + @events.respond_to?(name, include_private) + end - if (slack_url = Rails.application.secrets.slack_url).present? - # Do something worthwhile - else - # Raise as important secret password is not specified + def method_missing(method, *args, &block) + @events.send(method, *args, &block) + end end - After: - - slack_url = Rails.application.secrets.slack_url! + With `Module#delegate_missing_to`, the above is condensed to: - *Aditya Sanghi*, *Gaurish Sharma* + class Partition + delegate_missing_to :@events -* Remove deprecated `Class#superclass_delegating_accessor`. - Use `Class#class_attribute` instead. - - *Akshay Vishnoi* - -* Patch `Delegator` to work with `#try`. - - Fixes #5790. - - *Nate Smith* - -* Add `Integer#positive?` and `Integer#negative?` query methods - in the vein of `Fixnum#zero?`. - - This makes it nicer to do things like `bunch_of_numbers.select(&:positive?)`. - - *DHH* - -* Encoding `ActiveSupport::TimeWithZone` to YAML now preserves the timezone information. - - Fixes #9183. - - *Andrew White* - -* Added `ActiveSupport::TimeZone#strptime` to allow parsing times as if - from a given timezone. - - *Paul A Jungwirth* - -* `ActiveSupport::Callbacks#skip_callback` now raises an `ArgumentError` if - an unrecognized callback is removed. - - *Iain Beeston* - -* Added `ActiveSupport::ArrayInquirer` and `Array#inquiry`. - - Wrapping an array in an `ArrayInquirer` gives a friendlier way to check its - contents: - - variants = ActiveSupport::ArrayInquirer.new([:phone, :tablet]) - - variants.phone? # => true - variants.tablet? # => true - variants.desktop? # => false - - variants.any?(:phone, :tablet) # => true - variants.any?(:phone, :desktop) # => true - variants.any?(:desktop, :watch) # => false - - `Array#inquiry` is a shortcut for wrapping the receiving array in an - `ArrayInquirer`. - - *George Claghorn* - -* Deprecate `alias_method_chain` in favour of `Module#prepend` introduced in - Ruby 2.0. - - *Kir Shatrov* - -* Added `#without` on `Enumerable` and `Array` to return a copy of an - enumerable without the specified elements. - - *Todd Bealmear* - -* Fixed a problem where `String#truncate_words` would get stuck with a complex - string. - - *Henrik Nygren* - -* Fixed a roundtrip problem with `AS::SafeBuffer` where primitive-like strings - will be dumped as primitives: - - Before: - - YAML.load ActiveSupport::SafeBuffer.new("Hello").to_yaml # => "Hello" - YAML.load ActiveSupport::SafeBuffer.new("true").to_yaml # => true - YAML.load ActiveSupport::SafeBuffer.new("false").to_yaml # => false - YAML.load ActiveSupport::SafeBuffer.new("1").to_yaml # => 1 - YAML.load ActiveSupport::SafeBuffer.new("1.1").to_yaml # => 1.1 - - After: - - YAML.load ActiveSupport::SafeBuffer.new("Hello").to_yaml # => "Hello" - YAML.load ActiveSupport::SafeBuffer.new("true").to_yaml # => "true" - YAML.load ActiveSupport::SafeBuffer.new("false").to_yaml # => "false" - YAML.load ActiveSupport::SafeBuffer.new("1").to_yaml # => "1" - YAML.load ActiveSupport::SafeBuffer.new("1.1").to_yaml # => "1.1" - - *Godfrey Chan* - -* Enable `number_to_percentage` to keep the number's precision by allowing - `:precision` to be `nil`. - - *Jack Xu* - -* `config_accessor` became a private method, as with Ruby's `attr_accessor`. - - *Akira Matsuda* - -* `AS::Testing::TimeHelpers#travel_to` now changes `DateTime.now` as well as - `Time.now` and `Date.today`. - - *Yuki Nishijima* - -* Add `file_fixture` to `ActiveSupport::TestCase`. - It provides a simple mechanism to access sample files in your test cases. - - By default file fixtures are stored in `test/fixtures/files`. This can be - configured per test-case using the `file_fixture_path` class attribute. - - *Yves Senn* - -* Return value of yielded block in `File.atomic_write`. - - *Ian Ker-Seymer* - -* Duplicate frozen array when assigning it to a `HashWithIndifferentAccess` so - that it doesn't raise a `RuntimeError` when calling `map!` on it in `convert_value`. - - Fixes #18550. - - *Aditya Kapoor* - -* Add missing time zone definitions for Russian Federation and sync them - with `zone.tab` file from tzdata version 2014j (latest). - - *Andrey Novikov* - -* Add `SecureRandom.base58` for generation of random base58 strings. - - *Matthew Draper*, *Guillermo Iguaran* - -* Add `#prev_day` and `#next_day` counterparts to `#yesterday` and - `#tomorrow` for `Date`, `Time`, and `DateTime`. - - *George Claghorn* - -* Add `same_time` option to `#next_week` and `#prev_week` for `Date`, `Time`, - and `DateTime`. - - *George Claghorn* - -* Add `#on_weekend?`, `#next_weekday`, `#prev_weekday` methods to `Date`, - `Time`, and `DateTime`. - - `#on_weekend?` returns `true` if the receiving date/time falls on a Saturday - or Sunday. - - `#next_weekday` returns a new date/time representing the next day that does - not fall on a Saturday or Sunday. - - `#prev_weekday` returns a new date/time representing the previous day that - does not fall on a Saturday or Sunday. - - *George Claghorn* - -* Change the default test order from `:sorted` to `:random`. - - *Rafael Mendonça França* - -* 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, callbacks could only be halted by explicitly providing a - terminator and by having a callback match the conditions of the terminator. - -* Add `ActiveSupport.halt_callback_chains_on_return_false` - - Setting `ActiveSupport.halt_callback_chains_on_return_false` - to `true` will let an app support the deprecated way of halting Active Record, - and Active Model callback chains by returning `false`. - - Setting the value to `false` will tell the app to ignore any `false` value - returned by those callbacks, and only halt the chain upon `throw(:abort)`. - - 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*, *Roque Pinel* - -* 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 + def initialize(first_event) + @events = [ first_event ] end - rescue_from ErrorDispatcher do |error| - render status: error.status, json: { error: error.to_s } + def people + if @events.first.detail.people.any? + @events.collect { |e| Array(e.detail.people) }.flatten.uniq + else + @events.collect(&:creator).uniq + end end end - *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. + *Genadi Samokovarov*, *DHH* - 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. +* Rescuable: If a handler doesn't match the exception, check for handlers + matching the exception's cause. - *Logan Leger* + *Jeremy Daer* -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/Rakefile b/activesupport/Rakefile index 81c242d4b1..7b56e36abf 100644 --- a/activesupport/Rakefile +++ b/activesupport/Rakefile @@ -1,6 +1,9 @@ require 'rake/testtask' task :default => :test + +task :package + Rake::TestTask.new do |t| t.libs << 'test' t.pattern = 'test/**/*_test.rb' diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index 32e28c0212..c6ca969844 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -13,7 +13,7 @@ Gem::Specification.new do |s| 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,9 +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 'concurrent-ruby', '~> 1.0' - s.add_dependency 'method_source' + s.add_dependency 'concurrent-ruby', '~> 1.0', '>= 1.0.2' end diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 94fe893149..11569add37 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -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,10 +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 @@ -82,6 +86,14 @@ module ActiveSupport 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 index f59ddf5403..ea328f603e 100644 --- a/activesupport/lib/active_support/array_inquirer.rb +++ b/activesupport/lib/active_support/array_inquirer.rb @@ -9,8 +9,10 @@ module ActiveSupport # variants.desktop? # => false class ArrayInquirer < Array # Passes each element of +candidates+ collection to ArrayInquirer collection. - # The method returns true if at least one element is the same. If +candidates+ - # collection is not given, method returns true. + # 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]) # 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 610105f41c..179ca13b49 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -158,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 @@ -198,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. @@ -243,14 +250,14 @@ module ActiveSupport # sleep 60 # # Thread.new do - # val_1 = cache.fetch('foo', race_condition_ttl: 10) do + # val_1 = cache.fetch('foo', race_condition_ttl: 10.seconds) do # sleep 1 # 'new value 1' # end # end # # Thread.new do - # val_2 = cache.fetch('foo', race_condition_ttl: 10) do + # val_2 = cache.fetch('foo', race_condition_ttl: 10.seconds) do # 'new value 2' # end # end @@ -292,6 +299,8 @@ module ActiveSupport 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 @@ -323,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. @@ -333,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 = normalize_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 @@ -415,7 +422,7 @@ module ActiveSupport 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. # @@ -424,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. # @@ -433,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. # @@ -442,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. # @@ -451,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. @@ -462,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. @@ -481,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) @@ -509,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: @@ -529,7 +536,7 @@ 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 normalize_key(key, options) key = expanded_key(key) @@ -555,17 +562,6 @@ module ActiveSupport 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) @@ -588,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 @@ -611,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) @@ -630,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 @@ -665,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/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb index 174913365a..2ca4b51efa 100644 --- a/activesupport/lib/active_support/cache/mem_cache_store.rb +++ b/activesupport/lib/active_support/cache/mem_cache_store.rb @@ -96,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| [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 + 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 diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb index df38dbcf11..1c678dc2af 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -126,7 +126,7 @@ module ActiveSupport 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` + Please use `write_cache_value` instead. MESSAGE write_cache_value name, value, options end diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index e6baddf5db..557a4695a6 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -103,7 +103,7 @@ module ActiveSupport end # A hook invoked every time a before callback is halted. - # This can be overridden in AS::Callback implementors in order + # This can be overridden in ActiveSupport::Callbacks implementors in order # to provide better debugging/logging. def halted_callback_hook(filter) end @@ -571,15 +571,15 @@ 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 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(deprecated); or as an @@ -736,8 +736,13 @@ module ActiveSupport # # would call <tt>Audit#save</tt>. # - # NOTE: +method_name+ passed to `define_model_callbacks` must not end with + # ===== Notes + # + # +names+ passed to `define_callbacks` must not end with # `!`, `?` or `=`. + # + # Calling `define_callbacks` multiple times with the same +names+ will + # overwrite previous callbacks registered with `set_callback`. def define_callbacks(*names) options = names.extract_options! @@ -782,7 +787,7 @@ module ActiveSupport 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 the next release of Rails. + 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 diff --git a/activesupport/lib/active_support/concurrency/share_lock.rb b/activesupport/lib/active_support/concurrency/share_lock.rb index ca48164c54..89e63aefd4 100644 --- a/activesupport/lib/active_support/concurrency/share_lock.rb +++ b/activesupport/lib/active_support/concurrency/share_lock.rb @@ -6,12 +6,6 @@ module ActiveSupport # A share/exclusive lock, otherwise known as a read/write lock. # # https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock - #-- - # Note that a pending Exclusive lock attempt does not block incoming - # Share requests (i.e., we are "read-preferring"). That seems - # consistent with the behavior of "loose" upgrades, but may be the - # wrong choice otherwise: it nominally reduces the possibility of - # deadlock by risking starvation instead. class ShareLock include MonitorMixin @@ -48,17 +42,11 @@ module ActiveSupport def start_exclusive(purpose: nil, compatible: [], no_wait: false) synchronize do unless @exclusive_thread == Thread.current - if busy?(purpose) + if busy_for_exclusive?(purpose) return false if no_wait - loose_shares = @sharing.delete(Thread.current) - @waiting[Thread.current] = compatible if loose_shares - - begin - @cv.wait_while { busy?(purpose) } - ensure - @waiting.delete Thread.current - @sharing[Thread.current] = loose_shares if loose_shares + yield_shares(purpose: purpose, compatible: compatible, block_share: true) do + @cv.wait_while { busy_for_exclusive?(purpose) } end end @exclusive_thread = Thread.current @@ -71,13 +59,19 @@ module ActiveSupport # Relinquish the exclusive lock. Must only be called by the thread # that called start_exclusive (and currently holds the lock). - def stop_exclusive + 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 @@ -85,8 +79,16 @@ module ActiveSupport def start_sharing synchronize do - if @exclusive_thread && @exclusive_thread != Thread.current + 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 @@ -109,12 +111,12 @@ module ActiveSupport # the block. # # See +start_exclusive+ for other options. - def exclusive(purpose: nil, compatible: [], no_wait: false) + 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 + stop_exclusive(compatible: after_compatible) end end end @@ -129,14 +131,56 @@ module ActiveSupport 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?(purpose) - (@exclusive_thread && @exclusive_thread != Thread.current) || - @waiting.any? { |k, v| k != Thread.current && !v.include?(purpose) } || + 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/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/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/big_decimal/conversions.rb b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb index 22fc7ecf92..074e2eabf8 100644 --- a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb +++ b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb @@ -3,10 +3,8 @@ require 'bigdecimal/util' module ActiveSupport module BigDecimalWithDefaultFormat #:nodoc: - DEFAULT_STRING_FORMAT = 'F' - - def to_s(format = nil) - super(format || DEFAULT_STRING_FORMAT) + def to_s(format = 'F') + super(format) end end end diff --git a/activesupport/lib/active_support/core_ext/class/subclasses.rb b/activesupport/lib/active_support/core_ext/class/subclasses.rb index b0f9a8be34..1d8c33b43e 100644 --- a/activesupport/lib/active_support/core_ext/class/subclasses.rb +++ b/activesupport/lib/active_support/core_ext/class/subclasses.rb @@ -26,8 +26,6 @@ class Class # Returns an array with the direct children of +self+. # - # Integer.subclasses # => [Fixnum, Bignum] - # # class Foo; end # class Bar < Foo; end # class Baz < Bar; end diff --git a/activesupport/lib/active_support/core_ext/date/conversions.rb b/activesupport/lib/active_support/core_ext/date/conversions.rb index ed8bca77ac..6e3b4a89ce 100644 --- a/activesupport/lib/active_support/core_ext/date/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date/conversions.rb @@ -5,15 +5,15 @@ require 'active_support/core_ext/module/remove_method' class Date DATE_FORMATS = { - :short => '%e %b', - :long => '%B %e, %Y', + :short => '%d %b', + :long => '%B %d, %Y', :db => '%Y-%m-%d', :number => '%Y%m%d', :long_ordinal => lambda { |date| day_format = ActiveSupport::Inflector.ordinalize(date.day) date.strftime("%B #{day_format}, %Y") # => "April 25th, 2007" }, - :rfc822 => '%e %b %Y', + :rfc822 => '%d %b %Y', :iso8601 => lambda { |date| date.iso8601 } } @@ -80,6 +80,7 @@ class Date # # 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 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 e079af594d..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) @@ -287,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 d29a8db5cf..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 @@ -5,7 +5,7 @@ module DateAndTime # # Time.zone = 'Hawaii' # => 'Hawaii' # 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 + # 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,7 +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 - # 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 bcb228b09a..86177488c0 100644 --- a/activesupport/lib/active_support/core_ext/date_time.rb +++ b/activesupport/lib/active_support/core_ext/date_time.rb @@ -1,5 +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/calculations.rb b/activesupport/lib/active_support/core_ext/date_time/calculations.rb index 95617fb8c2..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 + 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/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 8a74ad4d66..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(:+) || 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 @@ -91,15 +94,36 @@ 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 + 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 + super end end end diff --git a/activesupport/lib/active_support/core_ext/hash/compact.rb b/activesupport/lib/active_support/core_ext/hash/compact.rb index 5dc9a05ec7..f072530e04 100644 --- a/activesupport/lib/active_support/core_ext/hash/compact.rb +++ b/activesupport/lib/active_support/core_ext/hash/compact.rb @@ -1,19 +1,22 @@ class Hash # Returns a hash with non +nil+ values. # - # hash = { a: true, b: false, c: nil} - # hash.compact # => { a: true, b: false} - # hash # => { a: true, b: false, c: nil} - # { c: nil }.compact # => {} + # hash = { a: true, b: false, c: nil } + # hash.compact # => { a: true, b: false } + # hash # => { a: true, b: false, c: nil } + # { c: nil }.compact # => {} + # { c: true }.compact # => { c: true } def compact self.select { |_, value| !value.nil? } end # Replaces current hash with non +nil+ values. + # Returns nil if no changes were made, otherwise returns the hash. # - # hash = { a: true, b: false, c: nil} - # hash.compact! # => { a: true, b: false} - # hash # => { a: true, b: false} + # hash = { a: true, b: false, c: nil } + # hash.compact! # => { a: true, b: false } + # hash # => { a: true, b: false } + # { c: true }.compact! # => nil def compact! self.reject! { |_, value| value.nil? } end diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb index 6741e732f0..2fc514cfce 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>. @@ -55,8 +55,7 @@ class Hash # # XML_TYPE_NAMES = { # "Symbol" => "symbol", - # "Fixnum" => "integer", - # "Bignum" => "integer", + # "Integer" => "integer", # "BigDecimal" => "decimal", # "Float" => "float", # "TrueClass" => "boolean", diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb index 8b2366c4b3..1bfa18aeee 100644 --- a/activesupport/lib/active_support/core_ext/hash/keys.rb +++ b/activesupport/lib/active_support/core_ext/hash/keys.rb @@ -11,7 +11,7 @@ class Hash # hash.transform_keys.with_index { |k, i| [k, i].join } # => {"name0"=>"Rob", "age1"=>"28"} def transform_keys return enum_for(:transform_keys) { size } unless block_given? - result = self.class.new + result = {} each_key do |key| result[yield(key)] = self[key] 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 8afc258df8..d0197af95f 100644 --- a/activesupport/lib/active_support/core_ext/kernel/reporting.rb +++ b/activesupport/lib/active_support/core_ext/kernel/reporting.rb @@ -1,4 +1,6 @@ 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/marshal.rb b/activesupport/lib/active_support/core_ext/marshal.rb index e333b26133..edfc8296fe 100644 --- a/activesupport/lib/active_support/core_ext/marshal.rb +++ b/activesupport/lib/active_support/core_ext/marshal.rb @@ -3,9 +3,12 @@ module ActiveSupport 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 + 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 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 76825862d7..567ac825e9 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -27,7 +27,7 @@ class Module # <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 diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb index 8a7e6776da..045668c207 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb @@ -25,7 +25,7 @@ class Module # end # # => NameError: invalid attribute name: 1_Badname # - # If you want to opt out the creation on the instance reader method, pass + # If you want to opt out of the creation of the instance reader method, pass # <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>. # # class Current @@ -47,7 +47,7 @@ class Module unless options[:instance_reader] == false || options[:instance_accessor] == false class_eval(<<-EOS, __FILE__, __LINE__ + 1) def #{sym} - Thread.current[:"attr_#{self.class.name}_#{sym}"] + Thread.current[:"attr_#{name}_#{sym}"] end EOS end @@ -65,7 +65,7 @@ class Module # Current.user = "DHH" # Thread.current[:attr_Current_user] # => "DHH" # - # If you want to opt out the instance writer method, pass + # If you want to opt out of the creation of the instance writer method, pass # <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>. # # class Current @@ -86,7 +86,7 @@ class Module unless options[:instance_writer] == false || options[:instance_accessor] == false class_eval(<<-EOS, __FILE__, __LINE__ + 1) def #{sym}=(obj) - Thread.current[:"attr_#{self.class.name}_#{sym}"] = obj + Thread.current[:"attr_#{name}_#{sym}"] = obj end EOS end diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index 0d46248582..3f6e8bd26c 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -148,15 +148,11 @@ class Module # Foo.new("Bar").name # raises NoMethodError: undefined method `name' # # 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 @@ -168,8 +164,8 @@ class Module '' end - file, line = caller(1, 1).first.split(':'.freeze, 2) - line = line.to_i + location = caller_locations(1, 1).first + file, line = location.path, location.lineno to = to.to_s to = "self.#{to}" if DELEGATION_RESERVED_METHOD_NAMES.include?(to) @@ -215,4 +211,68 @@ class Module module_eval(method_def, file, line) end end + + # When building decorators, a common pattern may emerge: + # + # class Partition + # def initialize(first_event) + # @events = [ first_event ] + # end + # + # def people + # if @events.first.detail.people.any? + # @events.collect { |e| Array(e.detail.people) }.flatten.uniq + # else + # @events.collect(&:creator).uniq + # end + # end + # + # private + # def respond_to_missing?(name, include_private = false) + # @events.respond_to?(name, include_private) + # end + # + # def method_missing(method, *args, &block) + # @events.send(method, *args, &block) + # end + # end + # + # With `Module#delegate_missing_to`, the above is condensed to: + # + # class Partition + # delegate_missing_to :@events + # + # def initialize(first_event) + # @events = [ first_event ] + # end + # + # def people + # if @events.first.detail.people.any? + # @events.collect { |e| Array(e.detail.people) }.flatten.uniq + # else + # @events.collect(&:creator).uniq + # end + # end + # end + # + # The target can be anything callable withing the object. E.g. instance + # variables, methods, constants ant the likes. + def delegate_missing_to(target) + target = target.to_s + target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target) + + module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def respond_to_missing?(name, include_private = false) + #{target}.respond_to?(name, include_private) + end + + def method_missing(method, *args, &block) + if #{target}.respond_to?(method) + #{target}.public_send(method, *args, &block) + else + super + end + end + RUBY + end end 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/numeric/conversions.rb b/activesupport/lib/active_support/core_ext/numeric/conversions.rb index 9d832897ed..6586a351f8 100644 --- a/activesupport/lib/active_support/core_ext/numeric/conversions.rb +++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb @@ -15,72 +15,72 @@ module ActiveSupport::NumericWithFormat # ==== 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.000 % + # 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 - # 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 + # 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" # @@ -134,6 +134,12 @@ module ActiveSupport::NumericWithFormat deprecate to_formatted_s: :to_s end -[Fixnum, Bignum, Float, BigDecimal].each do |klass| - klass.prepend(ActiveSupport::NumericWithFormat) +# Ruby 2.4+ unifies Fixnum & Bignum into Integer. +if Integer == Fixnum + Integer.prepend ActiveSupport::NumericWithFormat +else + Fixnum.prepend ActiveSupport::NumericWithFormat + Bignum.prepend ActiveSupport::NumericWithFormat end +Float.prepend ActiveSupport::NumericWithFormat +BigDecimal.prepend ActiveSupport::NumericWithFormat diff --git a/activesupport/lib/active_support/core_ext/numeric/time.rb b/activesupport/lib/active_support/core_ext/numeric/time.rb index 6c4a975495..c6ece22f8d 100644 --- a/activesupport/lib/active_support/core_ext/numeric/time.rb +++ b/activesupport/lib/active_support/core_ext/numeric/time.rb @@ -25,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 @@ -49,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 039c50a4a2..cb74bad73e 100644 --- a/activesupport/lib/active_support/core_ext/object/blank.rb +++ b/activesupport/lib/active_support/core_ext/object/blank.rb @@ -112,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 diff --git a/activesupport/lib/active_support/core_ext/object/duplicable.rb b/activesupport/lib/active_support/core_ext/object/duplicable.rb index befa5aee21..9bc5ee65ba 100644 --- a/activesupport/lib/active_support/core_ext/object/duplicable.rb +++ b/activesupport/lib/active_support/core_ext/object/duplicable.rb @@ -70,7 +70,7 @@ class Numeric # Numbers are not duplicable: # # 3.duplicable? # => false - # 3.dup # => TypeError: can't dup Fixnum + # 3.dup # => TypeError: can't dup Integer def duplicable? false end diff --git a/activesupport/lib/active_support/core_ext/object/json.rb b/activesupport/lib/active_support/core_ext/object/json.rb index 0db787010c..d49b2fbe54 100644 --- a/activesupport/lib/active_support/core_ext/object/json.rb +++ b/activesupport/lib/active_support/core_ext/object/json.rb @@ -197,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 8c16d95b62..3b6d9da216 100644 --- a/activesupport/lib/active_support/core_ext/object/try.rb +++ b/activesupport/lib/active_support/core_ext/object/try.rb @@ -99,7 +99,7 @@ class Object # # "a".try!(:upcase) # => "A" # nil.try!(:upcase) # => nil - # 123.try!(:upcase) # => NoMethodError: undefined method `upcase' for 123:Fixnum + # 123.try!(:upcase) # => NoMethodError: undefined method `upcase' for 123:Integer end class Delegator diff --git a/activesupport/lib/active_support/core_ext/string/access.rb b/activesupport/lib/active_support/core_ext/string/access.rb index ebd0dd3fc7..213a91aa7a 100644 --- a/activesupport/lib/active_support/core_ext/string/access.rb +++ b/activesupport/lib/active_support/core_ext/string/access.rb @@ -1,5 +1,5 @@ class String - # If you pass a single Fixnum, returns a substring of one character at that + # If you pass a single integer, returns a substring of one character at that # position. The first character of the string is at position 0, the next at # position 1, and so on. If a range is supplied, a substring containing # characters at offsets given by the range is returned. In both cases, if an diff --git a/activesupport/lib/active_support/core_ext/string/conversions.rb b/activesupport/lib/active_support/core_ext/string/conversions.rb index fd79a40e31..946976c5e9 100644 --- a/activesupport/lib/active_support/core_ext/string/conversions.rb +++ b/activesupport/lib/active_support/core_ext/string/conversions.rb @@ -18,7 +18,8 @@ class String # "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/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index cc71b8155d..7277f51076 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -222,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/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb index 6251f34daf..005ad93b08 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -171,7 +171,7 @@ module ActiveSupport #:nodoc: original_concat(value) end - def initialize(*) + def initialize(str = '') @html_safe = true super end @@ -250,7 +250,7 @@ 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/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 768c9a1b2c..e81b48ab26 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -73,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 @@ -220,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) 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/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index af18ff746f..57f6286de3 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -88,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 @@ -143,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| @@ -171,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 @@ -262,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 @@ -352,7 +343,6 @@ module ActiveSupport #:nodoc: end def clear - log_call Dependencies.unload_interlock do loaded.clear loading.clear @@ -361,7 +351,6 @@ module ActiveSupport #:nodoc: 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) @@ -377,8 +366,6 @@ module ActiveSupport #:nodoc: begin if load? - log "loading #{file_name}" - # Enable warnings if this file has not been loaded before and # warnings_on_first_load is set. load_args = ["#{file_name}.rb"] @@ -390,7 +377,6 @@ module ActiveSupport #:nodoc: enable_warnings { result = load_file(*load_args) } end else - log "requiring #{file_name}" result = require file_name end rescue Exception @@ -483,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 } @@ -494,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 @@ -508,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 @@ -673,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). @@ -738,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' @@ -760,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) @@ -770,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 index fbeb904684..f1865ca2f8 100644 --- a/activesupport/lib/active_support/dependencies/interlock.rb +++ b/activesupport/lib/active_support/dependencies/interlock.rb @@ -8,25 +8,23 @@ module ActiveSupport #:nodoc: end def loading - @lock.exclusive(purpose: :load, compatible: [:load]) do + @lock.exclusive(purpose: :load, compatible: [:load], after_compatible: [:load]) do yield end end def unloading - @lock.exclusive(purpose: :unload, compatible: [:load, :unload]) do + @lock.exclusive(purpose: :unload, compatible: [:load, :unload], after_compatible: [:load, :unload]) do yield end end - # Attempt to obtain an "unloading" (exclusive) lock. If possible, - # execute the supplied block while holding the lock. If there is - # concurrent activity, return immediately (without executing the - # block) instead of waiting. - def attempt_unloading - @lock.exclusive(purpose: :unload, compatible: [:load, :unload], no_wait: true) do - yield - end + 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 @@ -42,6 +40,12 @@ module ActiveSupport #:nodoc: 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 24545d766c..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.1', 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 0de891f1a2..35a9e5f8b8 100644 --- a/activesupport/lib/active_support/deprecation/behaviors.rb +++ b/activesupport/lib/active_support/deprecation/behaviors.rb @@ -2,7 +2,7 @@ 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. + # You would set <tt>:raise</tt>, as a behavior to raise errors and proactively report exceptions from deprecations. class DeprecationException < StandardError end @@ -11,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 }, diff --git a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb index 6f0ad445fc..0cb2d4d22e 100644 --- a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb @@ -80,7 +80,7 @@ module ActiveSupport # example.old_request.to_s # # => DEPRECATION WARNING: @request is deprecated! Call request.to_s instead of # @request.to_s - # (Bactrace information…) + # (Backtrace information…) # "special_request" # # example.request.to_s @@ -118,7 +118,7 @@ module ActiveSupport # # PLANETS.map { |planet| planet.capitalize } # # => DEPRECATION WARNING: PLANETS is deprecated! Use PLANETS_POST_2006 instead. - # (Bactrace information…) + # (Backtrace information…) # ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"] class DeprecatedConstantProxy < DeprecationProxy def initialize(old_const, new_const, deprecator = ActiveSupport::Deprecation.instance) diff --git a/activesupport/lib/active_support/deprecation/reporting.rb b/activesupport/lib/active_support/deprecation/reporting.rb index f89fc0fe14..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 @@ -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 @@ -81,17 +82,17 @@ module ActiveSupport def extract_callstack(callstack) return _extract_callstack(callstack) if callstack.first.is_a? String - rails_gem_root = File.expand_path("../../../../..", __FILE__) + "/" offending_line = callstack.find { |frame| - frame.absolute_path && !frame.absolute_path.start_with?(rails_gem_root) + 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 - rails_gem_root = File.expand_path("../../../../..", __FILE__) + "/" - offending_line = callstack.find { |line| !line.start_with?(rails_gem_root) } || callstack.first + offending_line = callstack.find { |line| !ignored_callstack(line) } || callstack.first + if offending_line if md = offending_line.match(/^(.+?):(\d+)(?::in `(.*?)')?/) md.captures @@ -100,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 c63b61e97a..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 @@ -117,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 @@ -130,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 @@ -139,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 index 315be85fb3..a2dcf31132 100644 --- a/activesupport/lib/active_support/evented_file_update_checker.rb +++ b/activesupport/lib/active_support/evented_file_update_checker.rb @@ -3,6 +3,33 @@ require 'pathname' require 'concurrent/atomic/atomic_boolean' module ActiveSupport + # Allows you to "listen" to changes in a file system. + # The evented file updater does not hit disk when checking for updates + # instead it uses platform specific file system events to trigger a change + # in state. + # + # The file checker takes an array of files to watch or a hash specifying directories + # and file extensions to watch. It also takes a block that is called when + # EventedFileUpdateChecker#execute is run or when EventedFileUpdateChecker#execute_if_updated + # is run and there have been changes to the file system. + # + # Note: Forking will cause the first call to `updated?` to return `true`. + # + # Example: + # + # checker = EventedFileUpdateChecker.new(["/tmp/foo"], -> { puts "changed" }) + # checker.updated? + # # => false + # checker.execute_if_updated + # # => nil + # + # FileUtils.touch("/tmp/foo") + # + # checker.updated? + # # => true + # checker.execute_if_updated + # # => "changed" + # class EventedFileUpdateChecker #:nodoc: all def initialize(files, dirs = {}, &block) @ph = PathHelper.new @@ -13,20 +40,35 @@ module ActiveSupport @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) + @block = block + @updated = Concurrent::AtomicBoolean.new(false) + @lcsp = @ph.longest_common_subpath(@dirs.keys) + @pid = Process.pid + @boot_mutex = Mutex.new - if (dtw = directories_to_watch).any? + 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 { require 'listen' } - Listen.to(*dtw, &method(:changed)).start + 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 end + boot! end def updated? + @boot_mutex.synchronize do + if @pid != Process.pid + boot! + @pid = Process.pid + @updated.make_true + end + end @updated.true? end @@ -37,12 +79,16 @@ module ActiveSupport def execute_if_updated if updated? + yield if block_given? execute true end end private + def boot! + Listen.to(*@dtw, &method(:changed)).start + end def changed(modified, added, removed) unless updated? @@ -79,16 +125,6 @@ module ActiveSupport end class PathHelper - using Module.new { - refine Pathname do - def ascendant_of?(other) - self != other && other.ascend do |ascendant| - break true if self == ascendant - end - end - end - } - def xpath(path) Pathname.new(path).expand_path end @@ -105,7 +141,7 @@ module ActiveSupport lcsp = Pathname.new(paths[0]) paths[1..-1].each do |path| - until lcsp.ascendant_of?(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 @@ -138,13 +174,21 @@ module ActiveSupport dir = dirs_sorted_by_nparts.shift dirs_sorted_by_nparts.reject! do |possible_descendant| - dir.ascendant_of?(possible_descendant) && descendants << 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 1fa9335080..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 @@ -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 3104d0e00f..74f2d8dd4b 100644 --- a/activesupport/lib/active_support/gem_version.rb +++ b/activesupport/lib/active_support/gem_version.rb @@ -6,9 +6,9 @@ module ActiveSupport module VERSION MAJOR = 5 - MINOR = 0 + MINOR = 1 TINY = 0 - PRE = "beta1.1" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index b878f31e75..03770a197c 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -69,9 +69,13 @@ module ActiveSupport end def default(*args) - key = args.first - args[0] = key.to_s if key.is_a?(Symbol) - super(*args) + arg_key = args.first + + if include?(key = convert_key(arg_key)) + self[key] + else + super + end end def self.new_from_hash_copying_default(hash) diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index 82aacf3b24..6cc7c90c12 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -64,8 +64,8 @@ module I18n 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. diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index f741c0bfb8..f94e12e14f 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -140,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. diff --git a/activesupport/lib/active_support/logger.rb b/activesupport/lib/active_support/logger.rb index 7626b28108..92b890dbb0 100644 --- a/activesupport/lib/active_support/logger.rb +++ b/activesupport/lib/active_support/logger.rb @@ -1,8 +1,10 @@ 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 @@ -48,6 +50,29 @@ 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 + + define_method(:silence) do |level = Logger::ERROR, &block| + if logger.respond_to?(:silence) + logger.silence(level) do + if respond_to?(:silence) + super(level, &block) + else + block.call(self) + end + end + else + if respond_to?(:silence) + super(level, &block) + else + block.call(self) + end + end + end end end diff --git a/activesupport/lib/active_support/logger_silence.rb b/activesupport/lib/active_support/logger_silence.rb index 125d81d973..3eb8098c77 100644 --- a/activesupport/lib/active_support/logger_silence.rb +++ b/activesupport/lib/active_support/logger_silence.rb @@ -7,39 +7,22 @@ module LoggerSilence included do cattr_accessor :silencer - attr_reader :local_levels self.silencer = true end - def after_initialize - @local_levels = Concurrent::Map.new(:initial_capacity => 2) - end - - def local_log_id - Thread.current.__id__ - end - - def level - local_levels[local_log_id] || super - end - # Silences the logger for the duration of the block. def silence(temporary_level = Logger::ERROR) if silencer begin - old_local_level = local_levels[local_log_id] - local_levels[local_log_id] = temporary_level + old_local_level = local_level + self.local_level = temporary_level yield self ensure - if old_local_level - local_levels[local_log_id] = old_local_level - else - local_levels.delete(local_log_id) - end + 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 2dde01c844..721efea789 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -97,7 +97,7 @@ module ActiveSupport end def new_cipher - OpenSSL::Cipher::Cipher.new(@cipher) + OpenSSL::Cipher.new(@cipher) end def verifier diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index 854029bf83..4c3deffe6e 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -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 diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index 64d9e71f37..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 @@ -78,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 @@ -118,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) # => 1000,000% - # number_to_percentage(1000, precision: nil) # => 1000% - # number_to_percentage('98a') # => 98a% - # number_to_percentage(100, format: '%n %') # => 100.000 % + # 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 @@ -149,19 +156,19 @@ module ActiveSupport # # ==== 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 + # # => "1,23,456.78" def number_to_delimited(number, options = {}) NumberToDelimitedConverter.convert(number, options) end @@ -190,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 @@ -237,17 +244,17 @@ module ActiveSupport # # ==== 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(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(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 7986eb50f0..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,7 +9,7 @@ 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 @@ -18,10 +20,6 @@ module ActiveSupport private - def is_negative?(number) - number.to_f.phase != 0 - end - def absolute_value(number) number.respond_to?(:abs) ? number.abs : number.sub(/\A-/, '') end 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 45ae8f1a93..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 @@ -12,7 +12,7 @@ module ActiveSupport private def parts - left, right = number.to_s.split('.') + left, right = number.to_s.split('.'.freeze) left.gsub!(delimiter_pattern) do |digit_to_delimit| "#{digit_to_delimit}#{options[:delimiter]}" 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_options.rb b/activesupport/lib/active_support/ordered_options.rb index 53a55bd986..501ba7fc76 100644 --- a/activesupport/lib/active_support/ordered_options.rb +++ b/activesupport/lib/active_support/ordered_options.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/object/blank' + module ActiveSupport # Usually key value pairs are handled something like this: # 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 fcf5553061..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]]] + 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/test_case.rb b/activesupport/lib/active_support/test_case.rb index d9a668c0ea..221e1171e7 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -66,12 +66,15 @@ module ActiveSupport alias :assert_not_respond_to :refute_respond_to alias :assert_not_same :refute_same - # Reveals the intention that the block should not raise any 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) + def assert_nothing_raised yield end end diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb index 29305e0082..ad83638572 100644 --- a/activesupport/lib/active_support/testing/assertions.rb +++ b/activesupport/lib/active_support/testing/assertions.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/object/blank' - module ActiveSupport module Testing module Assertions diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb index edf8b30a0a..7dd03ce65d 100644 --- a/activesupport/lib/active_support/testing/isolation.rb +++ b/activesupport/lib/active_support/testing/isolation.rb @@ -13,17 +13,6 @@ module ActiveSupport !ENV["NO_FORK"] && Process.respond_to?(:fork) end - @@class_setup_mutex = Mutex.new - - def _run_class_setup # class setup method should only happen in parent - @@class_setup_mutex.synchronize do - unless defined?(@@ran_class_setup) || ENV['ISOLATION_TEST'] - self.class.setup if self.class.respond_to?(:setup) - @@ran_class_setup = true - end - end - end - def run serialized = run_in_isolation do super diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 79cc748cf5..b1cec43124 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -1,6 +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 @@ -44,20 +45,21 @@ module ActiveSupport PRECISIONS = Hash.new { |h, n| h[n] = "%FT%T.%#{n}N".freeze } PRECISIONS[0] = '%FT%T'.freeze - include Comparable + 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 @@ -77,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 @@ -401,11 +402,6 @@ module ActiveSupport utc.to_r end - # Returns 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 @@ -454,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 @@ -482,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 7ca3592520..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 '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. @@ -185,6 +184,7 @@ module ActiveSupport UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.tr(':', '') @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 @@ -243,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 @@ -267,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. @@ -275,8 +285,7 @@ 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 diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb index df7b081993..99fc26549e 100644 --- a/activesupport/lib/active_support/xml_mini.rb +++ b/activesupport/lib/active_support/xml_mini.rb @@ -32,20 +32,25 @@ module ActiveSupport "binary" => "base64" } unless defined?(DEFAULT_ENCODINGS) - TYPE_NAMES = { - "Symbol" => "symbol", - "Fixnum" => "integer", - "Bignum" => "integer", - "BigDecimal" => "decimal", - "Float" => "float", - "TrueClass" => "boolean", - "FalseClass" => "boolean", - "Date" => "date", - "DateTime" => "dateTime", - "Time" => "dateTime", - "Array" => "array", - "Hash" => "hash" - } unless defined?(TYPE_NAMES) + unless defined?(TYPE_NAMES) + TYPE_NAMES = { + "Symbol" => "symbol", + "Integer" => "integer", + "BigDecimal" => "decimal", + "Float" => "float", + "TrueClass" => "boolean", + "FalseClass" => "boolean", + "Date" => "date", + "DateTime" => "dateTime", + "Time" => "dateTime", + "Array" => "array", + "Hash" => "hash" + } + + # No need to map these on Ruby 2.4+ + TYPE_NAMES["Fixnum"] = "integer" unless Fixnum == Integer + TYPE_NAMES["Bignum"] = "integer" unless Bignum == Integer + end FORMATTING = { "symbol" => Proc.new { |symbol| symbol.to_s }, diff --git a/activesupport/lib/active_support/xml_mini/rexml.rb b/activesupport/lib/active_support/xml_mini/rexml.rb index 924ed72345..95af5af2c0 100644 --- a/activesupport/lib/active_support/xml_mini/rexml.rb +++ b/activesupport/lib/active_support/xml_mini/rexml.rb @@ -20,11 +20,9 @@ module ActiveSupport data = StringIO.new(data || '') end - char = data.getc - if char.nil? + if data.eof? {} else - data.ungetc(char) silence_warnings { require 'rexml/document' } unless defined?(REXML::Document) doc = REXML::Document.new(data) diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb index c0e23e89f7..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 @@ -25,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 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/broadcast_logger_test.rb b/activesupport/test/broadcast_logger_test.rb index 6d4e3b74f7..f95b3e5ad3 100644 --- a/activesupport/test/broadcast_logger_test.rb +++ b/activesupport/test/broadcast_logger_test.rb @@ -3,75 +3,147 @@ require 'abstract_unit' module ActiveSupport class BroadcastLoggerTest < TestCase attr_reader :logger, :log1, :log2 - def setup + + setup do @log1 = FakeLogger.new @log2 = FakeLogger.new @log1.extend Logger.broadcast @log2 @logger = @log1 end - def test_debug - logger.debug "foo" - assert_equal 'foo', log1.adds.first[2] - assert_equal 'foo', log2.adds.first[2] + Logger::Severity.constants.each do |level_name| + method = level_name.downcase + level = Logger::Severity.const_get(level_name) + + test "##{method} adds the message to all loggers" do + logger.send(method, "msg") + + assert_equal [level, "msg", nil], log1.adds.first + assert_equal [level, "msg", nil], log2.adds.first + end end - def test_close + test "#close broadcasts to all loggers" do logger.close + assert log1.closed, 'should be closed' assert log2.closed, 'should be closed' end - def test_chevrons + test "#<< shovels the value into all loggers" do logger << "foo" + assert_equal %w{ foo }, log1.chevrons assert_equal %w{ foo }, log2.chevrons end - def test_level - assert_nil logger.level - logger.level = 10 - assert_equal 10, log1.level - assert_equal 10, log2.level + test "#level= assigns the level to all loggers" do + assert_equal ::Logger::DEBUG, logger.level + logger.level = ::Logger::FATAL + + assert_equal ::Logger::FATAL, log1.level + assert_equal ::Logger::FATAL, log2.level end - def test_progname + test "#progname= assigns to all the loggers" do assert_nil logger.progname - logger.progname = 10 - assert_equal 10, log1.progname - assert_equal 10, log2.progname + logger.progname = ::Logger::FATAL + + assert_equal ::Logger::FATAL, log1.progname + assert_equal ::Logger::FATAL, log2.progname end - def test_formatter + test "#formatter= assigns to all the loggers" do assert_nil logger.formatter - logger.formatter = 10 - assert_equal 10, log1.formatter - assert_equal 10, log2.formatter + logger.formatter = ::Logger::FATAL + + assert_equal ::Logger::FATAL, log1.formatter + assert_equal ::Logger::FATAL, log2.formatter + end + + test "#local_level= assigns the local_level to all loggers" do + assert_equal ::Logger::DEBUG, logger.local_level + logger.local_level = ::Logger::FATAL + + assert_equal ::Logger::FATAL, log1.local_level + assert_equal ::Logger::FATAL, log2.local_level + end + + test "#silence silences all loggers below the default level of ERROR" do + logger.silence do + logger.debug "test" + end + + assert_equal [], log1.adds + assert_equal [], log2.adds + end + + test "#silence does not silence at or above ERROR" do + logger.silence do + logger.error "from error" + logger.unknown "from unknown" + end + + assert_equal [[::Logger::ERROR, "from error", nil], [::Logger::UNKNOWN, "from unknown", nil]], log1.adds + assert_equal [[::Logger::ERROR, "from error", nil], [::Logger::UNKNOWN, "from unknown", nil]], log2.adds + end + + test "#silence allows you to override the silence level" do + logger.silence(::Logger::FATAL) do + logger.error "unseen" + logger.fatal "seen" + end + + assert_equal [[::Logger::FATAL, "seen", nil]], log1.adds + assert_equal [[::Logger::FATAL, "seen", nil]], log2.adds end class FakeLogger + include LoggerSilence + attr_reader :adds, :closed, :chevrons - attr_accessor :level, :progname, :formatter + attr_accessor :level, :progname, :formatter, :local_level def initialize - @adds = [] - @closed = false - @chevrons = [] - @level = nil - @progname = nil - @formatter = nil + @adds = [] + @closed = false + @chevrons = [] + @level = ::Logger::DEBUG + @local_level = ::Logger::DEBUG + @progname = nil + @formatter = nil + end + + def debug(message, &block) + add(::Logger::DEBUG, message, &block) + end + + def info(message, &block) + add(::Logger::INFO, message, &block) + end + + def warn(message, &block) + add(::Logger::WARN, message, &block) + end + + def error(message, &block) + add(::Logger::ERROR, message, &block) + end + + def fatal(message, &block) + add(::Logger::FATAL, message, &block) end - def debug msg, &block - add(:omg, nil, msg, &block) + def unknown(message, &block) + add(::Logger::UNKNOWN, message, &block) end def << x @chevrons << x end - def add(*args) - @adds << args + def add(message_level, message=nil, progname=nil, &block) + @adds << [message_level, message, progname] if message_level >= local_level end def close diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index 4a299429f3..ec7d028d7e 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -266,6 +266,20 @@ module CacheStoreBehavior 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 assert @cache.write('foo', {:a => "b"}) assert_equal({:a => "b"}, @cache.read('foo')) @@ -836,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 @@ -844,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 @@ -1151,15 +1165,6 @@ class CacheStoreLoggerTest < ActiveSupport::TestCase @cache.mute { @cache.fetch('foo') { 'bar' } } assert @buffer.string.blank? end - - def test_multi_read_loggin - @cache.write 'hello', 'goodbye' - @cache.write 'world', 'earth' - - @cache.read_multi('hello', 'world') - - assert_match "Caches multi read:\n- hello\n- world", @buffer.string - end end class CacheEntryTest < ActiveSupport::TestCase 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 507e13f968..de36e2026d 100644 --- a/activesupport/test/core_ext/array/conversions_test.rb +++ b/activesupport/test/core_ext/array/conversions_test.rb @@ -101,10 +101,10 @@ class ToXmlTest < ActiveSupport::TestCase end def test_to_xml_with_non_hash_elements - xml = [1, 2, 3].to_xml(skip_instruct: true, indent: 0) + xml = %w[1 2 3].to_xml(skip_instruct: true, indent: 0) - assert_equal '<fixnums type="array"><fixnum', xml.first(29) - assert xml.include?(%(<fixnum type="integer">2</fixnum>)), xml + assert_equal '<strings type="array"><string', xml.first(29) + assert xml.include?(%(<string>2</string>)), xml end def test_to_xml_with_non_hash_different_type_elements diff --git a/activesupport/test/core_ext/array/grouping_test.rb b/activesupport/test/core_ext/array/grouping_test.rb index 2eb0f05141..0682241f0b 100644 --- a/activesupport/test/core_ext/array/grouping_test.rb +++ b/activesupport/test/core_ext/array/grouping_test.rb @@ -3,11 +3,12 @@ require 'active_support/core_ext/array' class GroupingTest < ActiveSupport::TestCase def setup - Fixnum.send :private, :/ # test we avoid Integer#/ (redefined by mathn) + # In Ruby < 2.4, test we avoid Integer#/ (redefined by mathn) + Fixnum.send :private, :/ unless Fixnum == Integer end def teardown - Fixnum.send :public, :/ + Fixnum.send :public, :/ unless Fixnum == Integer end def test_in_groups_of_with_perfect_fit @@ -123,4 +124,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/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 0fc3f765f5..a7219eee31 100644 --- a/activesupport/test/core_ext/date_ext_test.rb +++ b/activesupport/test/core_ext/date_ext_test.rb @@ -30,6 +30,17 @@ class DateExtCalculationsTest < ActiveSupport::TestCase assert_equal "2005-02-21", date.to_s(:iso8601) end + def test_to_s_with_single_digit_day + date = Date.new(2005, 2, 1) + assert_equal "2005-02-01", date.to_s + assert_equal "01 Feb", date.to_s(:short) + assert_equal "February 01, 2005", date.to_s(:long) + assert_equal "February 1st, 2005", date.to_s(:long_ordinal) + assert_equal "2005-02-01", date.to_s(:db) + assert_equal "01 Feb 2005", date.to_s(:rfc822) + assert_equal "2005-02-01", date.to_s(:iso8601) + end + def test_readable_inspect assert_equal "Mon, 21 Feb 2005", Date.new(2005, 2, 21).readable_inspect assert_equal Date.new(2005, 2, 21).readable_inspect, Date.new(2005, 2, 21).inspect @@ -49,6 +60,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 @@ -280,6 +295,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) diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb index 6fe38c45ec..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 @@ -306,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 @@ -350,6 +379,24 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase 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 @@ -370,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 9e97acaffb..502e2811fa 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -12,7 +12,7 @@ class DurationTest < ActiveSupport::TestCase assert d.is_a?(ActiveSupport::Duration) assert_kind_of ActiveSupport::Duration, d assert_kind_of Numeric, d - assert_kind_of Fixnum, d + assert_kind_of Integer, d assert !d.is_a?(Hash) k = Class.new @@ -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('') @@ -222,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 f09b7d8850..99c3236c35 100644 --- a/activesupport/test/core_ext/enumerable_test.rb +++ b/activesupport/test/core_ext/enumerable_test.rb @@ -10,22 +10,27 @@ class SummablePayment < Payment 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 @@ -38,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 @@ -55,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 @@ -68,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 diff --git a/activesupport/test/core_ext/hash/transform_keys_test.rb b/activesupport/test/core_ext/hash/transform_keys_test.rb index 99af274614..962d3a30b6 100644 --- a/activesupport/test/core_ext/hash/transform_keys_test.rb +++ b/activesupport/test/core_ext/hash/transform_keys_test.rb @@ -43,4 +43,20 @@ class TransformKeysTest < ActiveSupport::TestCase 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_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index 1b66f784e4..e8099baa35 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -36,12 +36,12 @@ class HashExtTest < ActiveSupport::TestCase def setup @strings = { 'a' => 1, 'b' => 2 } @nested_strings = { 'a' => { 'b' => { 'c' => 3 } } } - @symbols = { :a => 1, :b => 2 } + @symbols = { :a => 1, :b => 2 } @nested_symbols = { :a => { :b => { :c => 3 } } } - @mixed = { :a => 1, 'b' => 2 } - @nested_mixed = { 'a' => { :b => { 'c' => 3 } } } - @fixnums = { 0 => 1, 1 => 2 } - @nested_fixnums = { 0 => { 1 => { 2 => 3} } } + @mixed = { :a => 1, 'b' => 2 } + @nested_mixed = { 'a' => { :b => { 'c' => 3 } } } + @integers = { 0 => 1, 1 => 2 } + @nested_integers = { 0 => { 1 => { 2 => 3} } } @illegal_symbols = { [] => 3 } @nested_illegal_symbols = { [] => { [] => 3} } @upcase_strings = { 'A' => 1, 'B' => 2 } @@ -196,14 +196,14 @@ class HashExtTest < ActiveSupport::TestCase assert_equal @nested_illegal_symbols, @nested_illegal_symbols.deep_dup.deep_symbolize_keys! end - def test_symbolize_keys_preserves_fixnum_keys - assert_equal @fixnums, @fixnums.symbolize_keys - assert_equal @fixnums, @fixnums.dup.symbolize_keys! + def test_symbolize_keys_preserves_integer_keys + assert_equal @integers, @integers.symbolize_keys + assert_equal @integers, @integers.dup.symbolize_keys! end - def test_deep_symbolize_keys_preserves_fixnum_keys - assert_equal @nested_fixnums, @nested_fixnums.deep_symbolize_keys - assert_equal @nested_fixnums, @nested_fixnums.deep_dup.deep_symbolize_keys! + def test_deep_symbolize_keys_preserves_integer_keys + assert_equal @nested_integers, @nested_integers.deep_symbolize_keys + assert_equal @nested_integers, @nested_integers.deep_dup.deep_symbolize_keys! end def test_stringify_keys @@ -299,14 +299,14 @@ class HashExtTest < ActiveSupport::TestCase assert_raise(NoMethodError) { @nested_illegal_symbols.with_indifferent_access.deep_dup.deep_symbolize_keys! } end - def test_symbolize_keys_preserves_fixnum_keys_for_hash_with_indifferent_access - assert_equal @fixnums, @fixnums.with_indifferent_access.symbolize_keys - assert_raise(NoMethodError) { @fixnums.with_indifferent_access.dup.symbolize_keys! } + def test_symbolize_keys_preserves_integer_keys_for_hash_with_indifferent_access + assert_equal @integers, @integers.with_indifferent_access.symbolize_keys + assert_raise(NoMethodError) { @integers.with_indifferent_access.dup.symbolize_keys! } end - def test_deep_symbolize_keys_preserves_fixnum_keys_for_hash_with_indifferent_access - assert_equal @nested_fixnums, @nested_fixnums.with_indifferent_access.deep_symbolize_keys - assert_raise(NoMethodError) { @nested_fixnums.with_indifferent_access.deep_dup.deep_symbolize_keys! } + def test_deep_symbolize_keys_preserves_integer_keys_for_hash_with_indifferent_access + assert_equal @nested_integers, @nested_integers.with_indifferent_access.deep_symbolize_keys + assert_raise(NoMethodError) { @nested_integers.with_indifferent_access.deep_dup.deep_symbolize_keys! } end def test_stringify_keys_for_hash_with_indifferent_access @@ -702,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 ]) @@ -993,6 +999,10 @@ class HashExtTest < ActiveSupport::TestCase h = hash_with_only_nil_values.dup assert_equal({}, h.compact) assert_equal(hash_with_only_nil_values, h) + + h = @symbols.dup + assert_equal(@symbols, h.compact) + assert_equal(@symbols, h) end def test_compact! @@ -1006,6 +1016,10 @@ class HashExtTest < ActiveSupport::TestCase h = hash_with_only_nil_values.dup assert_equal({}, h.compact!) assert_equal({}, h) + + h = @symbols.dup + assert_equal(nil, h.compact!) + assert_equal(@symbols, h) end def test_new_with_to_hash_conversion @@ -1028,7 +1042,7 @@ class HashExtTest < ActiveSupport::TestCase assert_equal 3, new_hash[2] new_hash.default = 2 - assert_equal 2, new_hash[:non_existant] + assert_equal 2, new_hash[:non_existent] end def test_to_hash_with_raising_default_proc diff --git a/activesupport/test/core_ext/marshal_test.rb b/activesupport/test/core_ext/marshal_test.rb index 825df439a5..380f64c6fd 100644 --- a/activesupport/test/core_ext/marshal_test.rb +++ b/activesupport/test/core_ext/marshal_test.rb @@ -29,7 +29,12 @@ class MarshalTest < ActiveSupport::TestCase ActiveSupport::Dependencies.clear with_autoloading_fixtures do - assert_kind_of EM, Marshal.load(dumped) + object = nil + assert_nothing_raised do + object = Marshal.load(dumped) + end + + assert_kind_of EM, object end end @@ -43,7 +48,12 @@ 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 @@ -64,6 +74,17 @@ class MarshalTest < ActiveSupport::TestCase 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 @@ -96,7 +117,7 @@ class MarshalTest < ActiveSupport::TestCase Marshal.load(dumped) end - assert_nothing_raised("EM failed to load while we expect only SomeClass to fail loading") do + assert_nothing_raised do EM.new end @@ -117,7 +138,12 @@ class MarshalTest < ActiveSupport::TestCase ActiveSupport::Dependencies.clear with_autoloading_fixtures do - assert_kind_of EM, Marshal.load(f) + object = nil + assert_nothing_raised do + object = Marshal.load(f) + end + + assert_kind_of EM, object end end 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 index 65fadc5c20..a9fd878b80 100644 --- a/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb +++ b/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb @@ -106,4 +106,10 @@ class ModuleAttributeAccessorPerThreadTest < ActiveSupport::TestCase 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_test.rb b/activesupport/test/core_ext/module_test.rb index 0ed66f8c37..566f29b470 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -40,6 +40,12 @@ class Someone < Struct.new(:name, :place) FAILED_DELEGATE_LINE_2 = __LINE__ + 1 delegate :bar, :to => :place, :allow_nil => true + + private + + def private_name + "Private" + end end Invoice = Struct.new(:client) do @@ -83,6 +89,20 @@ Product = Struct.new(:name) do end end +DecoratedTester = Struct.new(:client) do + delegate_missing_to :client +end + +class DecoratedReserved + delegate_missing_to :case + + attr_reader :case + + def initialize(kase) + @case = kase + end +end + class Block def hello? true @@ -173,6 +193,21 @@ class ModuleTest < ActiveSupport::TestCase end end + def test_delegation_target_when_prefix_is_true + assert_nothing_raised do + Name.send :delegate, :go, to: :you, prefix: true + end + assert_nothing_raised do + Name.send :delegate, :go, to: :_you, prefix: true + end + assert_raise(ArgumentError) do + Name.send :delegate, :go, to: :You, prefix: true + end + assert_raise(ArgumentError) do + Name.send :delegate, :go, to: :@you, prefix: true + end + end + def test_delegation_prefix invoice = Invoice.new(@david) assert_equal invoice.client_name, "David" @@ -316,6 +351,30 @@ class ModuleTest < ActiveSupport::TestCase assert has_block.hello? end + def test_delegate_to_missing_with_method + assert_equal "David", DecoratedTester.new(@david).name + end + + def test_delegate_to_missing_with_reserved_methods + assert_equal "David", DecoratedReserved.new(@david).name + end + + def test_delegate_to_missing_does_not_delegate_to_private_methods + e = assert_raises(NoMethodError) do + DecoratedReserved.new(@david).private_name + end + + assert_match(/undefined method `private_name' for/, e.message) + end + + def test_delegate_to_missing_does_not_delegate_to_fake_methods + e = assert_raises(NoMethodError) do + DecoratedReserved.new(@david).my_fake_method + end + + assert_match(/undefined method `my_fake_method' for/, e.message) + end + def test_parent assert_equal Yz::Zy, Yz::Zy::Cd.parent assert_equal Yz, Yz::Zy.parent @@ -328,7 +387,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 diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb index 5654aeb4f8..69c30a8a9e 100644 --- a/activesupport/test/core_ext/numeric_ext_test.rb +++ b/activesupport/test/core_ext/numeric_ext_test.rb @@ -387,16 +387,9 @@ class NumericExtFormattingTest < ActiveSupport::TestCase end def test_to_s__injected_on_proper_types - assert_equal Fixnum, 1230.class assert_equal '1.23 Thousand', 1230.to_s(:human) - - assert_equal Float, Float(1230).class assert_equal '1.23 Thousand', Float(1230).to_s(:human) - - assert_equal Bignum, (100**10).class assert_equal '100000 Quadrillion', (100**10).to_s(:human) - - assert_equal BigDecimal, BigDecimal("1000010").class assert_equal '1 Million', BigDecimal("1000010").to_s(:human) end diff --git a/activesupport/test/core_ext/object/deep_dup_test.rb b/activesupport/test/core_ext/object/deep_dup_test.rb index 791b5e7172..aa839201ea 100644 --- a/activesupport/test/core_ext/object/deep_dup_test.rb +++ b/activesupport/test/core_ext/object/deep_dup_test.rb @@ -51,7 +51,7 @@ class DeepDupTest < ActiveSupport::TestCase end def test_deep_dup_with_hash_class_key - hash = { Fixnum => 1 } + hash = { Integer => 1 } dup = hash.deep_dup assert_equal 1, dup.keys.length 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 index 02ab17fb64..2cbb1d590f 100644 --- a/activesupport/test/core_ext/object/json_gem_encoding_test.rb +++ b/activesupport/test/core_ext/object/json_gem_encoding_test.rb @@ -10,7 +10,7 @@ require 'json/encoding_test_cases' # The AS::JSON encoder requires the BigDecimal core_ext, which, unfortunately, # changes the BigDecimal#to_s output, and consequently the JSON gem output. So -# we need to require this unfront to ensure we don't get a false failure, but +# 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' diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index 2e69816364..d68a77680b 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) @@ -332,7 +345,7 @@ class StringInflectionsTest < ActiveSupport::TestCase end class StringAccessTest < ActiveSupport::TestCase - test "#at with Fixnum, returns a substring of one character at that position" do + test "#at with Integer, returns a substring of one character at that position" do assert_equal "h", "hello".at(0) end @@ -345,19 +358,19 @@ class StringAccessTest < ActiveSupport::TestCase assert_equal nil, "hello".at(/nonexisting/) end - test "#from with positive Fixnum, returns substring from the given position to the end" do + test "#from with positive Integer, returns substring from the given position to the end" do assert_equal "llo", "hello".from(2) end - test "#from with negative Fixnum, position is counted from the end" do + test "#from with negative Integer, position is counted from the end" do assert_equal "lo", "hello".from(-2) end - test "#to with positive Fixnum, substring from the beginning to the given position" do + test "#to with positive Integer, substring from the beginning to the given position" do assert_equal "hel", "hello".to(2) end - test "#to with negative Fixnum, position is counted from the end" do + test "#to with negative Integer, position is counted from the end" do assert_equal "hell", "hello".to(-2) end @@ -371,14 +384,14 @@ class StringAccessTest < ActiveSupport::TestCase assert_equal 'x', 'x'.first end - test "#first with Fixnum, returns a substring from the beginning to position" do + test "#first with Integer, returns a substring from the beginning to position" do assert_equal "he", "hello".first(2) assert_equal "", "hello".first(0) assert_equal "hello", "hello".first(10) assert_equal 'x', 'x'.first(4) end - test "#first with Fixnum >= string length still returns a new string" do + test "#first with Integer >= string length still returns a new string" do string = "hello" different_string = string.first(5) assert_not_same different_string, string @@ -389,14 +402,14 @@ class StringAccessTest < ActiveSupport::TestCase assert_equal 'x', 'x'.last end - test "#last with Fixnum, returns a substring from the end to position" do + test "#last with Integer, returns a substring from the end to position" do assert_equal "llo", "hello".last(3) assert_equal "hello", "hello".last(10) assert_equal "", "hello".last(0) assert_equal 'x', 'x'.last(4) end - test "#last with Fixnum >= string length still returns a new string" do + test "#last with Integer >= string length still returns a new string" do string = "hello" different_string = string.last(5) assert_not_same different_string, string @@ -444,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 @@ -661,7 +682,7 @@ class OutputSafetyTest < ActiveSupport::TestCase assert_equal @string, @string.html_safe end - test "A fixnum is safe by default" do + test "An integer is safe by default" do assert 5.html_safe? end @@ -792,7 +813,7 @@ class OutputSafetyTest < ActiveSupport::TestCase assert_equal ["<p>", "<b>", "<h1>"], @other_string end - test "Concatting a fixnum to safe always yields safe" do + test "Concatting an integer to safe always yields safe" do string = @string.html_safe string = string.concat(13) assert_equal "hello".concat(13), string diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb index e45df63fd5..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 @@ -392,7 +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(ArgumentError) { Time.new(2015, 5, 9, 10, 00, 00, '+03:00').change(nsec: 999999999) } + assert_nothing_raised { Time.new(2015, 5, 9, 10, 00, 00, '+03:00').change(nsec: 999999999) } end def test_utc_change diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index 7acada011d..d90714acdb 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -11,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 @@ -47,6 +50,8 @@ 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? diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index 757e600646..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 @@ -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 @@ -1047,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_test.rb b/activesupport/test/deprecation_test.rb index 58a0a3964d..dbde3d2e15 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 @@ -143,7 +143,7 @@ class DeprecationTest < ActiveSupport::TestCase stderr_output = capture(:stderr) { assert_nil behavior.call('Some error!', ['call stack!']) } - assert stderr_output.blank? + assert stderr_output.empty? end def test_deprecated_instance_variable_proxy @@ -341,7 +341,7 @@ class DeprecationTest < ActiveSupport::TestCase end def test_default_deprecation_horizon_should_always_bigger_than_current_rails_version - assert ActiveSupport::Deprecation.new.deprecation_horizon > ActiveSupport::VERSION::STRING + assert_operator ActiveSupport::Deprecation.new.deprecation_horizon, :>, ActiveSupport::VERSION::STRING end def test_default_gem_name diff --git a/activesupport/test/evented_file_update_checker_test.rb b/activesupport/test/evented_file_update_checker_test.rb index bc3f77bd54..2cb2d8167f 100644 --- a/activesupport/test/evented_file_update_checker_test.rb +++ b/activesupport/test/evented_file_update_checker_test.rb @@ -11,7 +11,7 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase end def new_checker(files = [], dirs = {}, &block) - ActiveSupport::EventedFileUpdateChecker.new(files, dirs, &block).tap do + ActiveSupport::EventedFileUpdateChecker.new(files, dirs, &block).tap do |c| wait end end @@ -34,6 +34,48 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase super wait end + + test 'notifies forked processes' do + FileUtils.touch(tmpfiles) + + checker = new_checker(tmpfiles) { } + assert !checker.updated? + + # Pipes used for flow controll across fork. + boot_reader, boot_writer = IO.pipe + touch_reader, touch_writer = IO.pipe + + pid = fork do + assert checker.updated? + + # Clear previous check value. + checker.execute + assert !checker.updated? + + # Fork is booted, ready for file to be touched + # notify parent process. + boot_writer.write("booted") + + # Wait for parent process to signal that file + # has been touched. + IO.select([touch_reader]) + + assert checker.updated? + end + + assert pid + + # Wait for fork to be booted before touching files. + IO.select([boot_reader]) + touch(tmpfiles) + + # Notify fork that files have been touched. + touch_writer.write("touched") + + assert checker.updated? + + Process.wait(pid) + end end class EventedFileUpdateCheckerPathHelperTest < ActiveSupport::TestCase 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 index 9c07e38fe5..40ae0c7617 100644 --- a/activesupport/test/file_update_checker_shared_tests.rb +++ b/activesupport/test/file_update_checker_shared_tests.rb @@ -5,19 +5,21 @@ module FileUpdateCheckerSharedTests include FileUtils def tmpdir - @tmpdir ||= Dir.mktmpdir(nil, __dir__) + @tmpdir end def tmpfile(name) - "#{tmpdir}/#{name}" + File.join(tmpdir, name) end def tmpfiles @tmpfiles ||= %w(foo.rb bar.rb baz.rb).map { |f| tmpfile(f) } end - def teardown - FileUtils.rm_rf(@tmpdir) if defined? @tmpdir + 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 @@ -134,6 +136,22 @@ module FileUpdateCheckerSharedTests 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 @@ -209,7 +227,7 @@ module FileUpdateCheckerSharedTests assert !checker.execute_if_updated assert_equal 0, i - touch("#{subdir}/nested.rb") + touch(File.join(subdir, "nested.rb")) assert checker.execute_if_updated assert_equal 1, i @@ -229,12 +247,12 @@ module FileUpdateCheckerSharedTests assert_equal 0, i # subdir does not look for Ruby files, but its parent tmpdir does. - touch("#{subdir}/nested.rb") + touch(File.join(subdir, "nested.rb")) assert checker.execute_if_updated assert_equal 1, i - touch("#{subdir}/nested.txt") + touch(File.join(subdir, "nested.txt")) assert checker.execute_if_updated assert_equal 2, i diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb index 14fe97a986..c7dc1dadb6 100644 --- a/activesupport/test/inflector_test_cases.rb +++ b/activesupport/test/inflector_test_cases.rb @@ -270,7 +270,8 @@ module InflectorTestCases "maybe you'll be there" => "Maybe You'll Be There", "¿por qué?" => '¿Por Qué?', "Fred’s" => "Fred’s", - "Fred`s" => "Fred`s" + "Fred`s" => "Fred`s", + ActiveSupport::SafeBuffer.new("confirmation num") => "Confirmation Num" } OrdinalNumbers = { diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb index 9f4b62fd8b..5fc2e16336 100644 --- a/activesupport/test/json/encoding_test.rb +++ b/activesupport/test/json/encoding_test.rb @@ -422,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/logger_test.rb b/activesupport/test/logger_test.rb index 317e09b7f2..dfc5f3fdf4 100644 --- a/activesupport/test/logger_test.rb +++ b/activesupport/test/logger_test.rb @@ -141,6 +141,52 @@ class LoggerTest < ActiveSupport::TestCase assert @output.string.include?("THIS IS HERE") end + def test_logger_silencing_works_for_broadcast + another_output = StringIO.new + another_logger = ActiveSupport::Logger.new(another_output) + + @logger.extend ActiveSupport::Logger.broadcast(another_logger) + + @logger.debug "CORRECT DEBUG" + @logger.silence do |logger| + assert_kind_of ActiveSupport::Logger, logger + @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 ActiveSupport::Logger.broadcast(another_logger) + + @logger.debug "CORRECT DEBUG" + @logger.silence do |logger| + assert_kind_of ActiveSupport::Logger, logger + @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) diff --git a/activesupport/test/multibyte_conformance_test.rb b/activesupport/test/multibyte_conformance_test.rb index 5df8f32e46..9fca47a985 100644 --- a/activesupport/test/multibyte_conformance_test.rb +++ b/activesupport/test/multibyte_conformance_test.rb @@ -28,7 +28,7 @@ class MultibyteConformanceTest < ActiveSupport::TestCase 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) diff --git a/activesupport/test/multibyte_grapheme_break_conformance_test.rb b/activesupport/test/multibyte_grapheme_break_conformance_test.rb index 229f24990e..6e2f02abed 100644 --- a/activesupport/test/multibyte_grapheme_break_conformance_test.rb +++ b/activesupport/test/multibyte_grapheme_break_conformance_test.rb @@ -27,7 +27,7 @@ class MultibyteGraphemeBreakConformanceTest < ActiveSupport::TestCase TEST_DATA_URL = "http://www.unicode.org/Public/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}/ucd/auxiliary" TEST_DATA_FILE = '/GraphemeBreakTest.txt' - CACHE_DIR = File.join(Dir.tmpdir, 'cache') + CACHE_DIR = "#{Dir.tmpdir}/cache/unicode_conformance" def setup FileUtils.mkdir_p(CACHE_DIR) diff --git a/activesupport/test/multibyte_normalization_conformance_test.rb b/activesupport/test/multibyte_normalization_conformance_test.rb index 8bc91ef708..0d31c9520f 100644 --- a/activesupport/test/multibyte_normalization_conformance_test.rb +++ b/activesupport/test/multibyte_normalization_conformance_test.rb @@ -30,7 +30,7 @@ class MultibyteNormalizationConformanceTest < ActiveSupport::TestCase 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" def setup FileUtils.mkdir_p(CACHE_DIR) diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb index b3464462c8..074c872efc 100644 --- a/activesupport/test/number_helper_test.rb +++ b/activesupport/test/number_helper_test.rb @@ -57,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 @@ -74,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 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 index 465a657308..acefa185a8 100644 --- a/activesupport/test/share_lock_test.rb +++ b/activesupport/test/share_lock_test.rb @@ -114,14 +114,17 @@ class ShareLockTest < ActiveSupport::TestCase [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 @@ -183,11 +186,14 @@ class ShareLockTest < ActiveSupport::TestCase 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 @@ -209,6 +215,245 @@ class ShareLockTest < ActiveSupport::TestCase 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 diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index 00d40c4497..a15d5c6a0e 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -395,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) @@ -491,6 +502,11 @@ class TimeZoneTest < ActiveSupport::TestCase 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) 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/rexml_engine_test.rb b/activesupport/test/xml_mini/rexml_engine_test.rb index f0067ca656..6e9ce7ac11 100644 --- a/activesupport/test/xml_mini/rexml_engine_test.rb +++ b/activesupport/test/xml_mini/rexml_engine_test.rb @@ -22,14 +22,24 @@ class REXMLEngineTest < ActiveSupport::TestCase morning </root> eoxml - assert_equal_rexml(io) + hash = ActiveSupport::XmlMini.parse(io) + assert hash.has_key?('root') + assert hash['root'].has_key?('products') + assert_match "good", hash['root']['__content__'] + products = hash['root']['products'] + assert products.has_key?("__content__") + assert_match 'hello everyone', products['__content__'] + end + + def test_parse_from_empty_string + ActiveSupport::XmlMini.backend = 'REXML' + assert_equal({}, ActiveSupport::XmlMini.parse("")) + end + + def test_parse_from_frozen_string + ActiveSupport::XmlMini.backend = 'REXML' + xml_string = "<root></root>".freeze + assert_equal({"root" => {}}, ActiveSupport::XmlMini.parse(xml_string)) end - private - def assert_equal_rexml(xml) - parsed_xml = ActiveSupport::XmlMini.parse(xml) - xml.rewind if xml.respond_to?(:rewind) - hash = ActiveSupport::XmlMini.with_backend('REXML') { ActiveSupport::XmlMini.parse(xml) } - assert_equal(hash, parsed_xml) - end end diff --git a/ci/travis.rb b/ci/travis.rb index e9a3626b9a..b8891d6889 100755 --- a/ci/travis.rb +++ b/ci/travis.rb @@ -120,9 +120,9 @@ class Build def env if activesupport? && !isolated? - # There is a known issue with the listen tests that casuses 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. + # 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 solution + # is to only run them in isolation to avoid randomly failing our test suite. { 'LISTEN' => '0' } else {} @@ -148,6 +148,7 @@ ENV['GEM'].split(',').each do |gem| next if ENV['TRAVIS_PULL_REQUEST'] && ENV['TRAVIS_PULL_REQUEST'] != 'false' && isolated next if gem == 'railties' && isolated next if gem == 'ac' && isolated + next if gem == 'ac:integration' && isolated next if gem == 'aj:integration' && isolated next if gem == 'guides' && isolated @@ -157,20 +158,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 aae405d5ac..2730d2dfea 100644 --- a/guides/CHANGELOG.md +++ b/guides/CHANGELOG.md @@ -1,23 +1,2 @@ -## Rails 5.0.0.beta1 (December 18, 2015) ## -* Add code of conduct to contributing guide - - *Jon Moss* - -* New section in Configuring: Configuring Active Job - - *Eliot Sykes* - -* New section in Active Record Association Basics: Single Table Inheritance - - *Andrey Nering* - -* New section in Active Record Querying: Understanding The Method Chaining - - *Andrey Nering* - -* New section in Configuring: Search Engines Indexing - - *Andrey Nering* - -Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/guides/CHANGELOG.md) for previous changes. +Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/guides/CHANGELOG.md) for previous changes. 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 4d0cb417b7..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/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 58ba708a39..604cf4be41 100644 --- a/guides/bug_report_templates/action_controller_gem.rb +++ b/guides/bug_report_templates/action_controller_gem.rb @@ -8,7 +8,7 @@ end gemfile(true) do source 'https://rubygems.org' # Activate the gem you are reporting the issue against. - gem 'rails', '4.2.0' + gem 'rails', '5.0.0.rc1' end require 'rack/test' diff --git a/guides/bug_report_templates/action_controller_master.rb b/guides/bug_report_templates/action_controller_master.rb index 3f24aa3b4d..8322707495 100644 --- a/guides/bug_report_templates/action_controller_master.rb +++ b/guides/bug_report_templates/action_controller_master.rb @@ -8,11 +8,6 @@ end gemfile(true) do source 'https://rubygems.org' gem 'rails', github: 'rails/rails' - gem 'arel', github: 'rails/arel' - gem 'rack', github: 'rack/rack' - gem 'sprockets', github: 'rails/sprockets' - gem 'sprockets-rails', github: 'rails/sprockets-rails' - gem 'sass-rails', github: 'rails/sass-rails' end require 'action_controller/railtie' diff --git a/guides/bug_report_templates/active_record_gem.rb b/guides/bug_report_templates/active_record_gem.rb index 09d6e7b331..d4ede4e2c1 100644 --- a/guides/bug_report_templates/active_record_gem.rb +++ b/guides/bug_report_templates/active_record_gem.rb @@ -8,7 +8,7 @@ end gemfile(true) do source 'https://rubygems.org' # Activate the gem you are reporting the issue against. - gem 'activerecord', '4.2.0' + gem 'activerecord', '5.0.0.rc1' gem 'sqlite3' end diff --git a/guides/bug_report_templates/active_record_master.rb b/guides/bug_report_templates/active_record_master.rb index 5b742a9093..6fd401bd50 100644 --- a/guides/bug_report_templates/active_record_master.rb +++ b/guides/bug_report_templates/active_record_master.rb @@ -8,11 +8,6 @@ end gemfile(true) do source 'https://rubygems.org' gem 'rails', github: 'rails/rails' - gem 'arel', github: 'rails/arel' - gem 'rack', github: 'rack/rack' - gem 'sprockets', github: 'rails/sprockets' - gem 'sprockets-rails', github: 'rails/sprockets-rails' - gem 'sass-rails', github: 'rails/sass-rails' gem 'sqlite3' end @@ -25,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 index a4fe51156d..bb8af3be18 100644 --- a/guides/bug_report_templates/generic_gem.rb +++ b/guides/bug_report_templates/generic_gem.rb @@ -8,7 +8,7 @@ end gemfile(true) do source 'https://rubygems.org' # Activate the gem you are reporting the issue against. - gem 'activesupport', '4.2.0' + gem 'activesupport', '5.0.0.rc1' end require 'active_support/core_ext/object/blank' diff --git a/guides/bug_report_templates/generic_master.rb b/guides/bug_report_templates/generic_master.rb index 0a8048cc48..70cf931f34 100644 --- a/guides/bug_report_templates/generic_master.rb +++ b/guides/bug_report_templates/generic_master.rb @@ -8,20 +8,12 @@ end gemfile(true) do source 'https://rubygems.org' gem 'rails', github: 'rails/rails' - gem 'arel', github: 'rails/arel' - gem 'rack', github: 'rack/rack' - gem 'sprockets', github: 'rails/sprockets' - gem 'sprockets-rails', github: 'rails/sprockets-rails' - gem 'sass-rails', github: 'rails/sass-rails' end require 'active_support' require 'active_support/core_ext/object/blank' require 'minitest/autorun' -# Ensure backward compatibility with Minitest 4 -Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test) - class BugTest < Minitest::Test def test_stuff assert "zomg".present? 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 696493a3cf..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,7 +86,7 @@ $ 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://bundler.io/) @@ -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 327495704a..feee0f9920 100644 --- a/guides/source/3_1_release_notes.md +++ b/guides/source/3_1_release_notes.md @@ -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/4_0_release_notes.md b/guides/source/4_0_release_notes.md index b9444510ea..4615cf18e6 100644 --- a/guides/source/4_0_release_notes.md +++ b/guides/source/4_0_release_notes.md @@ -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 8a59007420..a30bfc458a 100644 --- a/guides/source/4_2_release_notes.md +++ b/guides/source/4_2_release_notes.md @@ -405,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 @@ -871,7 +871,7 @@ Please refer to the [Changelog][active-support] for detailed changes. `module Foo; extend ActiveSupport::Concern; end` boilerplate. ([Commit](https://github.com/rails/rails/commit/b16c36e688970df2f96f793a759365b248b582ad)) -* New [guide](constant_autoloading_and_reloading.html) about constant autoloading and reloading. +* New [guide](autoloading_and_reloading_constants.html) about constant autoloading and reloading. Credits ------- diff --git a/guides/source/5_0_release_notes.md b/guides/source/5_0_release_notes.md index 2650384df3..8bd2074e82 100644 --- a/guides/source/5_0_release_notes.md +++ b/guides/source/5_0_release_notes.md @@ -37,9 +37,20 @@ Major Features -------------- ### Action Cable -[Pull Request](https://github.com/rails/rails/pull/22586) -ToDo... +Action Cable is a new framework in Rails 5. It seamlessly integrates +[WebSockets](https://en.wikipedia.org/wiki/WebSocket) with the rest of your +Rails application. + +Action Cable 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. + +See the [Active Cable Overview](action_cable_overview.html) guide for more +information. ### Rails API [Pull Request](https://github.com/rails/rails/pull/19832) @@ -48,7 +59,74 @@ ToDo... ### Active Record attributes API -ToDo... +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) @@ -94,6 +172,10 @@ Please refer to the [Changelog][railties] for detailed changes. * 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`. @@ -120,6 +202,32 @@ Please refer to the [Changelog][railties] for detailed changes. ([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)) + + +* Added `--skip-action-mailer` to skip Action Mailer while generating new app. + ([Pull Request](https://github.com/rails/rails/pull/18288)) + +* Removed `tmp/sessions` directory and the clear rake task associated with it. + ([Pull Request](https://github.com/rails/rails/pull/18314)) + +* Changed `_form.html.erb` generated by scaffold generator to use local variables. + ([Pull Request](https://github.com/rails/rails/pull/13434)) Action Pack ----------- @@ -206,6 +314,12 @@ Please refer to the [Changelog][action-pack] for detailed changes. `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)) + +* Deprecated `:controller` and `:action` path parameters. + ([Pull Request](https://github.com/rails/rails/pull/23980)) + ### Notable changes * Added `ActionController::Renderer` to render arbitrary templates @@ -249,10 +363,37 @@ Please refer to the [Changelog][action-pack] for detailed changes. * Changed the `protect_from_forgery` prepend default to `false`. ([commit](https://github.com/rails/rails/commit/39794037817703575c35a75f1961b01b83791191)) -* `ActionController::TestCase` will be moved to it's own gem in Rails 5.1. Use +* `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)) + +* Discarded flash messages get removed before storing into session. + ([Pull Request](https://github.com/rails/rails/pull/18721)) + Action View ------------- @@ -272,17 +413,21 @@ Please refer to the [Changelog][action-view] for detailed changes. 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 automatically caches and fetches multiple partials. - ([Pull Request](https://github.com/rails/rails/pull/18948)) - -* Allow defining explicit collection caching using a `# Template Collection: ...` - directive inside templates. - ([Pull Request](https://github.com/rails/rails/pull/20781)) +* 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)) @@ -291,6 +436,8 @@ Please refer to the [Changelog][action-view] for detailed changes. button on submit to prevent double submits. ([Pull Request](https://github.com/rails/rails/pull/21135)) +* Partial template name no longer has to be a valid Ruby identifier. + ([commit](https://github.com/rails/rails/commit/da9038e)) Action Mailer ------------- @@ -321,6 +468,11 @@ Please refer to the [Changelog][action-mailer] for detailed changes. 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 ------------- @@ -378,6 +530,12 @@ Please refer to the [Changelog][active-record] for detailed changes. * 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)) + +* Removed support for `activerecord-deprecated_finders` gem. + ([commit](https://github.com/rails/rails/commit/78dab2a8569408658542e462a957ea5a35aa4679)) + ### Deprecations * Deprecated passing a class as a value in a query. Users should pass strings @@ -390,10 +548,6 @@ Please refer to the [Changelog][active-record] for detailed changes. * Deprecated `ActiveRecord::Base.errors_in_transactional_callbacks=`. ([commit](https://github.com/rails/rails/commit/07d3d402341e81ada0214f2cb2be1da69eadfe72)) -* Deprecated passing of `start` value to `find_in_batches` and `find_each` - in favour of `begin_at` value. - ([Pull Request](https://github.com/rails/rails/pull/18961)) - * Deprecated `Relation#uniq` use `Relation#distinct` instead. ([commit](https://github.com/rails/rails/commit/adfab2dcf4003ca564d78d4425566dd2d9cd8b4f)) @@ -435,6 +589,14 @@ Please refer to the [Changelog][active-record] for detailed changes. `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. @@ -443,19 +605,13 @@ Please refer to the [Changelog][active-record] for detailed changes. * 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 `:_prefix`/`:_suffix` option to `enum` definition. + ([Pull Request](https://github.com/rails/rails/pull/19813), + [Pull Request](https://github.com/rails/rails/pull/20999)) * Added `#cache_key` to `ActiveRecord::Relation`. ([Pull Request](https://github.com/rails/rails/pull/20884)) -* Added `ActiveRecord::Relation#outer_joins`. - ([Pull Request](https://github.com/rails/rails/pull/12071)) - -* Require `belongs_to` by default. - ([Pull Request](https://github.com/rails/rails/pull/18937)) - Deprecate - `required` option in favor of `optional` for `belongs_to` - * Changed the default `null` value for `timestamps` to `false`. ([commit](https://github.com/rails/rails/commit/a939506f297b667291480f26fa32a373a18ae06a)) @@ -484,7 +640,8 @@ Please refer to the [Changelog][active-record] for detailed changes. * `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`. + 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 @@ -536,6 +693,31 @@ Please refer to the [Changelog][active-record] for detailed changes. 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)) + +* Added ability to call `ActionRecord::Relation#update` on relation objects + which will run validations on callbacks on all objects in the relation. + ([Pull Request](https://github.com/rails/rails/pull/11898)) + +* Added `:touch` option to the `save` method so that records can be saved without + updating timestamps. + ([Pull Request](https://github.com/rails/rails/pull/18225)) + +* Added expression indexes and operator classes support for PostgreSQL. + ([commit](https://github.com/rails/rails/commit/edc2b7718725016e988089b5fb6d6fb9d6e16882)) + +* Added `:index_errors` option to add indexes to errors of nested attributes. + ([Pull Request](https://github.com/rails/rails/pull/19686)) Active Model ------------ @@ -552,6 +734,9 @@ Please refer to the [Changelog][active-model] for detailed changes. [activemodel-serializers-xml](https://github.com/rails/activemodel-serializers-xml) gem. ([Pull Request](https://github.com/rails/rails/pull/21161)) +* Removed `ActionController::ModelNaming` module. + ([Pull Request](https://github.com/rails/rails/pull/18194)) + ### Deprecations * Deprecated returning `false` as a way to halt Active Model and @@ -595,23 +780,32 @@ Please refer to the [Changelog][active-job] for detailed changes. ### Notable changes -* `ActiveJob::Base.deserialize` delegates to the job class. this allows jobs +* `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`, and `que` to report the job id back to - `ActiveJob::Base` as `provider_job_id`. +* 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)) + [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 -------------- @@ -649,6 +843,9 @@ Please refer to the [Changelog][active-support] for detailed changes. * Removed deprecated `ThreadSafe::Cache`. Use `Concurrent::Map` instead. ([Pull Request](https://github.com/rails/rails/pull/21679)) +* Removed `Object#itself` as it is implemented in Ruby 2.2. + ([Pull Request](https://github.com/rails/rails/pull/18244)) + ### Deprecations * Deprecated `MissingSourceFile` in favor of `LoadError`. @@ -680,6 +877,13 @@ Please refer to the [Changelog][active-support] for detailed changes. 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 @@ -699,7 +903,7 @@ Please refer to the [Changelog][active-support] for detailed changes. * Changed the default test order from `:sorted` to `:random`. ([commit](https://github.com/rails/rails/commit/5f777e4b5ee2e3e8e6fd0e2a208ec2a4d25a960d)) -* Added `#on_weekend?`, `#next_weekday`, `#prev_weekday` methods to `Date`, +* Added `#on_weekend?`, `#on_weekday?`, `#next_weekday`, `#prev_weekday` methods to `Date`, `Time`, and `DateTime`. ([Pull Request](https://github.com/rails/rails/pull/18335)) @@ -730,7 +934,7 @@ Please refer to the [Changelog][active-support] for detailed changes. ([commit](https://github.com/rails/rails/commit/a5e507fa0b8180c3d97458a9b86c195e9857d8f6)) * Added `Integer#positive?` and `Integer#negative?` query methods - in the vein of `Fixnum#zero?`. + in the vein of `Integer#zero?`. ([commit](https://github.com/rails/rails/commit/e54277a45da3c86fecdfa930663d7692fd083daa)) * Added a bang version to `ActiveSupport::OrderedOptions` get methods which will raise @@ -749,6 +953,23 @@ Please refer to the [Changelog][active-support] for detailed changes. 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)) + +* Added ability to `TaggedLogging` to allow loggers to be instantiated multiple + times so that they don't share tags with each other. + ([Pull Request](https://github.com/rails/rails/pull/9065)) Credits ------- @@ -762,6 +983,7 @@ framework it is. Kudos to all of them. [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 diff --git a/guides/source/action_cable_overview.md b/guides/source/action_cable_overview.md new file mode 100644 index 0000000000..16aa9438a2 --- /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 unauthorized). + +### 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 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 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 9ef2c1a441..58362fea58 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -21,7 +21,7 @@ After reading this guide, you will know: What Does a Controller Do? -------------------------- -Action Controller is the C in MVC. After routing has determined which controller to use for a request, the controller is responsible for making sense of the request and producing the appropriate output. Luckily, Action Controller does most of the groundwork for you and uses smart conventions to make this as straightforward as possible. +Action Controller is the C in MVC. After the router has determined which controller to use for a request, the controller is responsible for making sense of the request and producing the appropriate output. Luckily, Action Controller does most of the groundwork for you and uses smart conventions to make this as straightforward as possible. For most conventional [RESTful](http://en.wikipedia.org/wiki/Representational_state_transfer) applications, the controller will receive the request (this is invisible to you as the developer), fetch or save data from a model and use a view to create HTML output. If your controller needs to do things a little differently, that's not a problem, this is just the most common way for a controller to work. @@ -145,7 +145,7 @@ So for example, if you are sending this JSON content: Your controller will receive `params[:company]` as `{ "name" => "acme", "address" => "123 Carrot Street" }`. -Also, if you've turned on `config.wrap_parameters` in your initializer or called `wrap_parameters` in your controller, you can safely omit the root element in the JSON parameter. In this case, the parameters will be cloned and wrapped with a key chosen based on your controller's name. So the above JSON POST can be written as: +Also, if you've turned on `config.wrap_parameters` in your initializer or called `wrap_parameters` in your controller, you can safely omit the root element in the JSON parameter. In this case, the parameters will be cloned and wrapped with a key chosen based on your controller's name. So the above JSON request can be written as: ```json { "name": "acme", "address": "123 Carrot Street" } @@ -199,11 +199,12 @@ practice to help prevent accidentally allowing users to update sensitive model attributes. In addition, parameters can be marked as required and will flow through a -predefined raise/rescue flow to end up as a 400 Bad Request. +predefined raise/rescue flow that will result in a 400 Bad Request being +returned if not all required parameters are passed in. ```ruby class PeopleController < ActionController::Base - # This will raise an ActiveModel::ForbiddenAttributes exception + # This will raise an ActiveModel::ForbiddenAttributesError exception # because it's using mass assignment without an explicit permit # step. def create @@ -213,8 +214,8 @@ class PeopleController < ActionController::Base # This will pass with flying colors as long as there's a person key # in the parameters, otherwise it'll raise a # ActionController::ParameterMissing exception, which will get - # caught by ActionController::Base and turned into that 400 Bad - # Request reply. + # caught by ActionController::Base and turned into a 400 Bad + # Request error. def update person = current_account.people.find(params[:id]) person.update!(person_params) @@ -700,7 +701,7 @@ 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 @@ -814,7 +815,7 @@ In every controller there are two accessor methods pointing to the request and t ### The `request` Object -The request object contains a lot of useful information about the request coming in from the client. To get a full list of the available methods, refer to the [API documentation](http://api.rubyonrails.org/classes/ActionDispatch/Request.html). Among the properties that you can access on this object are: +The request object contains a lot of useful information about the request coming in from the client. To get a full list of the available methods, refer to the [Rails API documentation](http://api.rubyonrails.org/classes/ActionDispatch/Request.html) and [Rack Documentation](http://www.rubydoc.info/github/rack/rack/Rack/Request). Among the properties that you can access on this object are: | Property of `request` | Purpose | | ----------------------------------------- | -------------------------------------------------------------------------------- | @@ -836,7 +837,7 @@ Rails collects all of the parameters sent along with the request in the `params` ### The `response` Object -The response object is not usually used directly, but is built up during the execution of the action and rendering of the data that is being sent back to the user, but sometimes - like in an after filter - it can be useful to access the response directly. Some of these accessor methods also have setters, allowing you to change their values. +The response object is not usually used directly, but is built up during the execution of the action and rendering of the data that is being sent back to the user, but sometimes - like in an after filter - it can be useful to access the response directly. Some of these accessor methods also have setters, allowing you to change their values. To get a full list of the available methods, refer to the [Rails API documentation](http://api.rubyonrails.org/classes/ActionDispatch/Response.html) and [Rack Documentation](http://www.rubydoc.info/github/rack/rack/Rack/Response). | Property of `response` | Purpose | | ---------------------- | --------------------------------------------------------------------------------------------------- | @@ -1088,6 +1089,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. diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index cd2c13e8c1..7359438025 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -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 } @@ -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 @@ -714,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` (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>| +|`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' or 'peer') or directly the constant (`OpenSSL::SSL::VERIFY_NONE` or `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.| diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md index 543937f8e5..f68abbae3c 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? -------------------- -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. +In Rails, web requests are handled by [Action Controller](action_controller_overview.html) and Action View. Typically, Action Controller is 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. @@ -173,11 +173,11 @@ would produce: ```json { "name": "Alex", - "email: "alex@example.com" + "email": "alex@example.com" } ``` -See the [Jbuilder documention](https://github.com/rails/jbuilder#jbuilder) for +See the [Jbuilder documentation](https://github.com/rails/jbuilder#jbuilder) for more examples and information. #### Template Caching @@ -260,7 +260,7 @@ 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: @@ -442,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 @@ -493,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 @@ -530,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 @@ -599,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 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 `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 %> @@ -1247,7 +1247,7 @@ file_field_tag 'attachment' #### form_tag -Starts a form tag that points the action to a 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 %> @@ -1419,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 @@ -1524,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 76c13f0ea9..c9f70dc87b 100644 --- a/guides/source/active_job_basics.md +++ b/guides/source/active_job_basics.md @@ -62,12 +62,12 @@ $ bin/rails generate job guests_cleanup --queue urgent ``` If you don't want to use a generator, you could create your own file inside of -`app/jobs`, just make sure that it inherits from `ActiveJob::Base`. +`app/jobs`, just make sure that it inherits from `ApplicationJob`. Here's what a job looks like: ```ruby -class GuestsCleanupJob < ActiveJob::Base +class GuestsCleanupJob < ApplicationJob queue_as :default def perform(*guests) @@ -109,10 +109,12 @@ That's it! Job Execution ------------- -For enqueuing and executing jobs you need to set up a queuing backend, that is to -say you need to decide for a 3rd-party queuing library that Rails should use. -Rails itself does not provide a sophisticated queuing system and just executes the -job immediately if no adapter is set. +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 @@ -136,6 +138,18 @@ module YourApp end ``` +You can also configure your backend on a per job basis. + +```ruby +class GuestsCleanupJob < ApplicationJob + 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 @@ -157,7 +171,7 @@ Most of the adapters support multiple queues. With Active Job you can schedule the job to run on a specific queue: ```ruby -class GuestsCleanupJob < ActiveJob::Base +class GuestsCleanupJob < ApplicationJob queue_as :low_priority #.... end @@ -175,7 +189,7 @@ module YourApp end # app/jobs/guests_cleanup_job.rb -class GuestsCleanupJob < ActiveJob::Base +class GuestsCleanupJob < ApplicationJob queue_as :low_priority #.... end @@ -198,7 +212,7 @@ module YourApp end # app/jobs/guests_cleanup_job.rb -class GuestsCleanupJob < ActiveJob::Base +class GuestsCleanupJob < ApplicationJob queue_as :low_priority #.... end @@ -220,7 +234,7 @@ block will be executed in the job context (so you can access `self.arguments`) and you must return the queue name: ```ruby -class ProcessVideoJob < ActiveJob::Base +class ProcessVideoJob < ApplicationJob queue_as do video = self.arguments.first if video.owner.premium? @@ -260,7 +274,7 @@ trigger logic during the life cycle of a job. ### Usage ```ruby -class GuestsCleanupJob < ActiveJob::Base +class GuestsCleanupJob < ApplicationJob queue_as :default before_enqueue do |job| @@ -317,7 +331,7 @@ Active Record objects to your job instead of class/id pairs, which you then have to manually deserialize. Before, jobs would look like this: ```ruby -class TrashableCleanupJob < ActiveJob::Base +class TrashableCleanupJob < ApplicationJob def perform(trashable_class, trashable_id, depth) trashable = trashable_class.constantize.find(trashable_id) trashable.cleanup(depth) @@ -328,7 +342,7 @@ end Now you can simply do: ```ruby -class TrashableCleanupJob < ActiveJob::Base +class TrashableCleanupJob < ApplicationJob def perform(trashable, depth) trashable.cleanup(depth) end @@ -346,7 +360,7 @@ Active Job provides a way to catch exceptions raised during the execution of the job: ```ruby -class GuestsCleanupJob < ActiveJob::Base +class GuestsCleanupJob < ApplicationJob queue_as :default rescue_from(ActiveRecord::RecordNotFound) do |exception| diff --git a/guides/source/active_model_basics.md b/guides/source/active_model_basics.md index c05e20aceb..e834aeadb1 100644 --- a/guides/source/active_model_basics.md +++ b/guides/source/active_model_basics.md @@ -13,7 +13,7 @@ After reading this guide, you will know: * How an Active Record model behaves. * How Callbacks and validations work. * How serializers work. -* The Rails internationalization (i18n) framework. +* How Active Model integrates with the Rails internationalization (i18n) framework. -------------------------------------------------------------------------------- @@ -319,7 +319,7 @@ person.serializable_hash # => {"name"=>"Bob"} #### ActiveModel::Serializers -Rails provides a `ActiveModel::Serializers::JSON` serializer. +Rails provides an `ActiveModel::Serializers::JSON` serializer. This module automatically include the `ActiveModel::Serialization`. ##### ActiveModel::Serializers::JSON diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md index fba89f9d13..d9e9466a33 100644 --- a/guides/source/active_record_basics.md +++ b/guides/source/active_record_basics.md @@ -361,7 +361,7 @@ class CreatePublications < ActiveRecord::Migration[5.0] t.string :publisher_type t.boolean :single_issue - t.timestamps null: false + t.timestamps end add_index :publications, :publication_type_id end diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index fb5d2065d3..a7975c7772 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -399,7 +399,7 @@ By using the `after_commit` callback we can account for this case. ```ruby class PictureFile < ApplicationRecord - after_commit :delete_picture_file_from_disk, on: [:destroy] + after_commit :delete_picture_file_from_disk, on: :destroy def delete_picture_file_from_disk if File.exist?(filepath) @@ -409,7 +409,7 @@ class PictureFile < ApplicationRecord end ``` -NOTE: the `:on` option specifies when a callback will be fired. If you +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 diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md index 83f4b951ee..f914122242 100644 --- a/guides/source/active_record_migrations.md +++ b/guides/source/active_record_migrations.md @@ -41,7 +41,7 @@ class CreateProducts < ActiveRecord::Migration[5.0] t.string :name t.text :description - t.timestamps null: false + t.timestamps end end 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: @@ -287,7 +288,7 @@ class CreateProducts < ActiveRecord::Migration[5.0] t.string :name t.text :description - t.timestamps null: false + t.timestamps end end end @@ -353,7 +354,14 @@ 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 @@ -454,6 +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. +* `comment` Adds a comment for the column. Some adapters may support additional options; see the adapter specific API docs for further information. @@ -847,7 +856,7 @@ class CreateProducts < ActiveRecord::Migration[5.0] create_table :products do |t| t.string :name t.text :description - t.timestamps null: false + t.timestamps end end @@ -883,7 +892,7 @@ 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 `rails db:migrate`. You must rollback the migration (for example with `bin/rails db:rollback`), edit your migration and then run @@ -933,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: @@ -970,7 +979,7 @@ 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` rails task) into `db/structure.sql`. For example, for PostgreSQL, the `pg_dump` -utility is used. For MySQL, this file will contain the output of +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 diff --git a/guides/source/active_record_postgresql.md b/guides/source/active_record_postgresql.md index 68c6a77882..d7e35490ef 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 @@ -435,7 +435,7 @@ create_table :documents do |t| t.string 'body' end -execute "CREATE INDEX documents_idx ON documents USING gin(to_tsvector('english', title || ' ' || body));" +add_index :documents, "to_tsvector('english', title || ' ' || body)", using: :gin, name: 'documents_idx' # app/models/document.rb class Document < ApplicationRecord @@ -503,9 +503,9 @@ second = Article.create! title: "Brace yourself", status: "draft", published_at: 1.month.ago -Article.count # => 1 -first.archive! Article.count # => 2 +first.archive! +Article.count # => 1 ``` NOTE: This application only cares about non-archived `Articles`. A view also diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index 784be91845..928ab43b3b 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -11,7 +11,7 @@ After reading this guide, you will know: * How to specify the order, retrieved attributes, grouping, and other properties of the found records. * How to use eager loading to reduce the number of database queries needed for data retrieval. * How to use dynamic finder methods. -* How to use method chaining to use multiple ActiveRecord methods together. +* 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. @@ -50,7 +50,7 @@ class Role < ApplicationRecord 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 ------------------------------------ @@ -153,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: @@ -192,10 +192,10 @@ You can pass in a numerical argument to the `first` method to return up to that ```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: @@ -243,10 +243,10 @@ You can pass in a numerical argument to the `last` method to return up to that n ```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: @@ -472,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 @@ -517,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 -------- @@ -1085,6 +1095,8 @@ SELECT categories.* FROM categories INNER JOIN tags ON tags.article_id = articles.id ``` +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) provide a special syntax for specifying conditions for the joined tables: @@ -1109,7 +1121,7 @@ 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') +Author.left_outer_joins(:posts).distinct.select('authors.*, COUNT(posts.*) AS posts_count').group('authors.id') ``` Which produces: @@ -1294,6 +1306,28 @@ 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 @@ -1589,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 @@ -1621,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. @@ -1636,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` @@ -1891,7 +1925,7 @@ 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 that of the corresponding database shell. So, the same query running with the @@ -1951,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 @@ -1962,4 +1996,6 @@ following pointers may be helpful: * 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 dd7adf09a2..2737237c1a 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -149,7 +149,7 @@ false` as an argument. This technique should be used with caution. ### `valid?` and `invalid?` -Before saving an ActiveRecord object, Rails runs 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 @@ -278,12 +278,6 @@ form was submitted. This is typically used when the user needs to agree to your application's terms of service, confirm that some text is read, or any similar concept. -This validation is very specific to web applications and this -'acceptance' does not need to be recorded anywhere in your database. If you -don't have a field for it, the helper will just create a virtual attribute. If -the field does exist in your database, the `accept` option must be set to -`true` or else the validation will not run. - ```ruby class Person < ApplicationRecord validates :terms_of_service, acceptance: true @@ -292,16 +286,31 @@ 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. -It can receive an `:accept` option, which determines the value that will be -considered acceptance. It defaults to "1" and can be easily changed. +```ruby +class Person < ApplicationRecord + validates :terms_of_service, acceptance: true, message: 'must be abided' +end +``` + +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 < 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 @@ -383,7 +392,8 @@ The `exclusion` helper has an option `:in` that receives the set of values that will not be accepted for the validated attributes. The `:in` option has an alias called `:within` that you can use for the same purpose, if you'd like to. This example uses the `:message` option to show how you can include the -attribute's value. +attribute's value. For full options to the message argument please see the +[message documentation](#message). The default error message is _"is reserved"_. @@ -418,7 +428,8 @@ end The `inclusion` helper has an option `:in` that receives the set of values that will be accepted. The `:in` option has an alias called `:within` that you can use for the same purpose, if you'd like to. The previous example uses the -`:message` option to show how you can include the attribute's value. +`:message` option to show how you can include the attribute's value. For full +options please see the [message documentation](#message). The default error message for this helper is _"is not included in the list"_. @@ -505,6 +516,8 @@ constraints to acceptable values: * `: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 @@ -757,6 +770,9 @@ class Coffee < ApplicationRecord end ``` +For full options to the message argument please see the +[message documentation](#message). + ### `:allow_blank` The `:allow_blank` option is similar to the `:allow_nil` option. This option @@ -781,9 +797,10 @@ 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. +validation fails. This replacement is done using the I18n gem, and the +placeholders must match exactly, no spaces are allowed. -A `Proc` `:message` value is given two arguments: a message key for i18n, and +A `Proc` `:message` value is given two arguments: the object being validated, and a hash with `:model`, `:attribute`, and `:value` key-value pairs. ```ruby @@ -799,10 +816,10 @@ class Person < ApplicationRecord # Proc validates :username, uniqueness: { - # key = "activerecord.errors.models.person.attributes.username.taken" + # object = person object being validated # data = { model: "Person", attribute: "Username", value: <username> } - message: ->(key, data) do - "#{data[:value]} taken! Try again #{Time.zone.tomorrow}" + message: ->(object, data) do + "Hey #{object.name}!, #{data[:value]} is taken already! Try again #{Time.zone.tomorrow}" end } end @@ -830,6 +847,25 @@ class Person < ApplicationRecord 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 ------------------ @@ -1196,9 +1232,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)"] ``` diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index 0aca6db9b6..565c87c4b5 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -252,7 +252,7 @@ Note that `try` will swallow no-method errors, returning nil instead. If you wan ```ruby @number.try(:nest) # => nil -@number.try!(:nest) # NoMethodError: undefined method `nest' for 1:Fixnum +@number.try!(:nest) # NoMethodError: undefined method `nest' for 1:Integer ``` NOTE: Defined in `active_support/core_ext/object/try.rb`. @@ -632,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 @@ -709,87 +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 -bare constant names. Active Support extends this API to be able to pass -relative qualified constant names. - -The new methods are `qualified_const_defined?`, `qualified_const_get`, and -`qualified_const_set`. Their arguments are assumed to be qualified constant -names relative to their receiver: - -```ruby -Object.qualified_const_defined?("Math::PI") # => true -Object.qualified_const_get("Math::PI") # => 3.141592653589793 -Object.qualified_const_set("Math::Phi", 1.618034) # => 1.618034 -``` - -Arguments may be bare constant names: - -```ruby -Math.qualified_const_get("E") # => 2.718281828459045 -``` - -These methods are analogous to their built-in counterparts. In particular, -`qualified_constant_defined?` accepts an optional second argument to be -able to say whether you want the predicate to look in the ancestors. -This flag is taken into account for each constant in the expression while -walking down the path. - -For example, given - -```ruby -module M - X = 1 -end - -module N - class C - include M - end -end -``` - -`qualified_const_defined?` behaves this way: - -```ruby -N.qualified_const_defined?("C::X", false) # => false -N.qualified_const_defined?("C::X", true) # => true -N.qualified_const_defined?("C::X") # => true -``` - -As the last example implies, the second argument defaults to true, -as in `const_defined?`. - -For coherence with the built-in methods only relative paths are accepted. -Absolute qualified constant names like `::Math::PI` raise `NameError`. - -NOTE: Defined in `active_support/core_ext/module/qualified_const.rb`. - ### Reachable A named module is reachable if it is stored in its corresponding constant. It means you can reach the module object via the constant. @@ -1686,19 +1603,6 @@ Given a string with a qualified constant reference expression, `deconstantize` r "Admin::Hotel::ReservationUtils".deconstantize # => "Admin::Hotel" ``` -Active Support for example uses this method in `Module#qualified_const_set`: - -```ruby -def qualified_const_set(path, value) - QualifiedConstUtils.raise_if_absolute(path) - - const_name = path.demodulize - mod_name = path.deconstantize - mod = mod_name.empty? ? self : qualified_const_get(mod_name) - mod.const_set(const_name, value) -end -``` - NOTE: Defined in `active_support/core_ext/string/inflections.rb`. #### `parameterize` @@ -1767,7 +1671,7 @@ NOTE: Defined in `active_support/core_ext/string/inflections.rb`. The method `constantize` resolves the constant reference expression in its receiver: ```ruby -"Fixnum".constantize # => Fixnum +"Integer".constantize # => Integer module M X = 1 @@ -2240,7 +2144,7 @@ Similarly, `from` returns the tail from the element at the passed index to the e [].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" @@ -2374,7 +2278,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": @@ -2636,8 +2540,7 @@ To do so, the method loops over the pairs and builds nodes that depend on the _v ```ruby XML_TYPE_NAMES = { "Symbol" => "symbol", - "Fixnum" => "integer", - "Bignum" => "integer", + "Integer" => "integer", "BigDecimal" => "decimal", "Float" => "float", "TrueClass" => "boolean", @@ -3078,7 +2981,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`. @@ -3467,6 +3370,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: @@ -3653,6 +3558,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 0fd0112c9f..03af3cf819 100644 --- a/guides/source/active_support_instrumentation.md +++ b/guides/source/active_support_instrumentation.md @@ -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", diff --git a/guides/source/api_app.md b/guides/source/api_app.md index 86baa9ee84..f373d313cc 100644 --- a/guides/source/api_app.md +++ b/guides/source/api_app.md @@ -8,13 +8,13 @@ In this guide you will learn: * What Rails provides for API-only applications * How to configure Rails to start without any browser features -* How to decide which middlewares you will want to include +* How to decide which middleware you will want to include * How to decide which modules to use in your controller -------------------------------------------------------------------------------- -What is an API app? -------------------- +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. @@ -28,15 +28,14 @@ applications. For example, Twitter uses its [public API](https://dev.twitter.com) in its web application, which is built as a static site that consumes JSON resources. -Instead of using Rails to generate dynamic HTML that will communicate with the -server through forms and links, many developers are treating their web application -as just another client, delivered as static HTML, CSS and JavaScript consuming -a simple JSON API. +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 **or** a client-side framework. +API client, including client-side frameworks. -Why use Rails for JSON APIs? +Why Use Rails for JSON APIs? ---------------------------- The first question a lot of people have when thinking about building a JSON API @@ -44,11 +43,11 @@ using Rails is: "isn't using Rails to spit out some JSON overkill? Shouldn't I just use something like Sinatra?". For very simple APIs, this may be true. However, even in very HTML-heavy -applications, most of an application's logic is actually outside of the view +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 us to get up and running quickly without having to make a lot of trivial +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 @@ -75,18 +74,16 @@ Handled at the middleware layer: 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`), +- Conditional GETs: Rails handles conditional `GET` (`ETag` and `Last-Modified`) processing request headers and returning the correct response headers and status code. All you need to do is use the [`stale?`](http://api.rubyonrails.org/classes/ActionController/ConditionalGet.html#method-i-stale-3F) check in your controller, and Rails will handle all of the HTTP details for you. -- Caching: If you use `dirty?` with public cache control, Rails will automatically - cache your responses. You can easily configure the cache store. - HEAD requests: Rails will transparently convert `HEAD` requests into `GET` ones, and return just the headers on the way out. This makes `HEAD` work reliably in all Rails APIs. -While you could obviously build these up in terms of existing Rack middlewares, +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". @@ -97,28 +94,28 @@ Handled at the Action Pack layer: 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/) + 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 +- Basic, Digest, and Token Authentication: Rails comes with out-of-the-box support for three kinds of HTTP authentication. -- Instrumentation: Rails has an instrumentation API that will trigger registered +- 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: This may be passé for advanced Rails users, but it can be nice to - generate a resource and get your model, controller, test stubs, and routes - created for you in a single command. +- 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). + 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 @@ -126,7 +123,7 @@ 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". +to be most of it. The Basic Configuration ----------------------- @@ -135,6 +132,8 @@ 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 @@ -143,16 +142,18 @@ $ rails new my_api --api This will do three main things for you: -- Configure your application to start with a more limited set of middlewares +- 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 middlewares, this will leave out any Action + `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. @@ -163,14 +164,23 @@ class definition: config.api_only = true ``` -Optionally, in `config/environments/development.rb` add the following line -to render error responses using the API format (JSON by default) when it -is a local request: +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 @@ -185,35 +195,36 @@ class ApplicationController < ActionController::API end ``` -Choosing Middlewares +Choosing Middleware -------------------- -An API application comes with the following middlewares by default: +An API application comes with the following middleware by default: - `Rack::Sendfile` - `ActionDispatch::Static` -- `Rack::Lock` +- `ActionDispatch::Executor` - `ActiveSupport::Cache::Strategy::LocalCache::Middleware` +- `Rack::Runtime` - `ActionDispatch::RequestId` - `Rails::Rack::Logger` -- `Rack::Runtime` - `ActionDispatch::ShowExceptions` - `ActionDispatch::DebugExceptions` - `ActionDispatch::RemoteIp` - `ActionDispatch::Reloader` - `ActionDispatch::Callbacks` +- `ActiveRecord::Migration::CheckPending` - `Rack::Head` - `Rack::ConditionalGet` - `Rack::ETag` -See the [internal middlewares](rails_on_rack.html#internal-middleware-stack) +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 middlewares. In -general, these middlewares are agnostic to the type of application you are +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 middlewares in your application via: +You can get a list of all middleware in your application via: ```bash $ rails middleware @@ -262,9 +273,6 @@ subsequent inbound requests for the same URL. Think of it as page caching using HTTP semantics. -NOTE: This middleware is always outside of the `Rack::Lock` mutex, even in -single-threaded applications. - ### Using Rack::Sendfile When you use the `send_file` method inside a Rails controller, it sets the @@ -282,7 +290,7 @@ You can learn more about how to use `Rack::Sendfile` with popular front-ends in [the Rack::Sendfile documentation](http://rubydoc.info/github/rack/rack/master/Rack/Sendfile). -Here are some values for popular servers, once they are configured, to support +Here are some values for this header for some popular servers, once these servers are configured to support accelerated file sending: ```ruby @@ -296,9 +304,6 @@ config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" Make sure to configure your server to support these options following the instructions in the `Rack::Sendfile` documentation. -NOTE: The `Rack::Sendfile` middleware is always outside of the `Rack::Lock` -mutex, even in single-threaded applications. - ### Using ActionDispatch::Request `ActionDispatch::Request#params` will take parameters from the client in the JSON @@ -327,26 +332,26 @@ will be: { :person => { :firstName => "Yehuda", :lastName => "Katz" } } ``` -### Other Middlewares +### Other Middleware -Rails ships with a number of other middlewares that you might want to use in an +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 +- For session management * `ActionDispatch::Session::CacheStore` * `ActionDispatch::Session::CookieStore` * `ActionDispatch::Session::MemCacheStore` -Any of these middlewares can be added via: +Any of these middleware can be added via: ```ruby config.middleware.use Rack::MethodOverride ``` -### Removing Middlewares +### 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: @@ -355,7 +360,7 @@ middleware set, you can remove it with: config.middleware.delete ::Rack::Sendfile ``` -Keep in mind that removing these middlewares will remove support for certain +Keep in mind that removing these middleware will remove support for certain features in Action Controller. Choosing Controller Modules @@ -364,24 +369,24 @@ Choosing Controller Modules An API application (using `ActionController::API`) comes with the following controller modules by default: -- `ActionController::UrlFor`: Makes `url_for` and friends available. +- `ActionController::UrlFor`: Makes `url_for` and similar helpers available. - `ActionController::Redirecting`: Support for `redirect_to`. -- `ActionController::Rendering`: Basic support for rendering. +- `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 isn't 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 friends. +- `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)). -- `ActionController::Rescue`: Support for `rescue_from`. -- `ActionController::BasicImplicitRender`: Makes sure to return an empty response - if there's not an explicit one. -- `ActionController::StrongParameters`: Support for parameters white-listing in - combination with Active Model mass assignment. -- `ActionController::ParamsWrapper`: Wraps the parameters hash into a nested hash - so you don't have to specify root elements sending POST requests for instance. + guide](active_support_instrumentation.html#action-controller) for +more information regarding this). +- `ActionController::ParamsWrapper`: Wraps the parameters hash into a nested hash, + so that you don't have to specify root elements sending POST requests for instance. Other plugins may add additional modules. You can get a list of all modules included into `ActionController::API` in the rails console: @@ -389,6 +394,13 @@ 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 @@ -403,10 +415,10 @@ Some common modules you might want to add: and translation methods. - `ActionController::HttpAuthentication::Basic` (or `Digest` or `Token`): Support for basic, digest or token HTTP authentication. -- `AbstractController::Layouts`: Support for layouts when rendering. +- `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 +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 cd208c2e13..5b34330936 100644 --- a/guides/source/api_documentation_guidelines.md +++ b/guides/source/api_documentation_guidelines.md @@ -120,7 +120,7 @@ On the other hand, big chunks of structured documentation may have a separate "E The results of expressions follow them and are introduced by "# => ", vertically aligned: ```ruby -# For checking if a fixnum is even or odd. +# For checking if an integer is even or odd. # # 1.even? # => false # 1.odd? # => true diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 5bdaf600ad..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 your 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 @@ -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 @@ -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. @@ -667,7 +666,7 @@ anymore, delete these options from the `javascript_include_tag` and `stylesheet_link_tag`. The fingerprinting behavior is controlled by the `config.assets.digest` -initialization option (which defaults to `true` for production and development). +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 @@ -786,7 +785,6 @@ location ~ ^/assets/ { add_header Cache-Control public; add_header ETag ""; - break; } ``` @@ -898,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' @@ -1107,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 @@ -1177,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: @@ -1287,7 +1286,7 @@ config.assets.js_compressor = :uglifier # 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 @@ -1295,7 +1294,7 @@ config.assets.digest = true # 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`. diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index accce5a904..3993fdb1dd 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -105,13 +105,13 @@ class CreateBooks < ActiveRecord::Migration[5.0] def change create_table :authors do |t| t.string :name - t.timestamps null: false + t.timestamps end create_table :books do |t| t.belongs_to :author, index: true t.datetime :published_at - t.timestamps null: false + t.timestamps end end end @@ -136,13 +136,13 @@ 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 @@ -180,13 +180,13 @@ class CreateAuthors < ActiveRecord::Migration[5.0] def change create_table :authors do |t| t.string :name - t.timestamps null: false + t.timestamps end create_table :books do |t| t.belongs_to :author, index: true t.datetime :published_at - t.timestamps null: false + t.timestamps end end end @@ -222,19 +222,19 @@ 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 @@ -308,19 +308,19 @@ 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 @@ -349,12 +349,12 @@ 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| @@ -388,13 +388,13 @@ 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 @@ -472,7 +472,7 @@ class CreatePictures < ActiveRecord::Migration[5.0] 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] @@ -488,7 +488,7 @@ class CreatePictures < ActiveRecord::Migration[5.0] create_table :pictures do |t| t.string :name t.references :imageable, polymorphic: true, index: true - t.timestamps null: false + t.timestamps end end end @@ -518,7 +518,7 @@ 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 @@ -545,12 +545,12 @@ 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 author.books # retrieves books from the database author.books.size # uses the cached copy of books -author.books(true).empty? # discards the cached copy of books +author.books.reload.empty? # discards the cached copy of books # and goes back to the database ``` @@ -713,7 +713,7 @@ By default, Active Record doesn't know about the connection between these associ ```ruby a = Author.first -b = c.books.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 @@ -726,7 +726,7 @@ class Author < ApplicationRecord has_many :books, inverse_of: :author end -class book < ApplicationRecord +class Book < ApplicationRecord belongs_to :author, inverse_of: :books end ``` @@ -734,8 +734,8 @@ end With these changes, Active Record will only load one copy of the author object, preventing inconsistencies and making your application more efficient: ```ruby -a = author.first -b = c.books.first +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 @@ -932,15 +932,13 @@ 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_all`, when the object is destroyed, all its associated objects will be -deleted directly from the database without calling their `destroy` method. -* `:nullify`, causes the foreign key to be set to `NULL`. Callbacks are not executed. -* `:restrict_with_exception`, causes an exception to be raised if there is an associated record -* `:restrict_with_error`, causes an error to be added to the owner if there is an associated object +* `: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. @@ -1479,7 +1477,7 @@ WARNING: Objects will _always_ be removed from the database, ignoring the `:depe ##### `collection=(objects)` -The `collection=` method makes the collection contain only the supplied objects, by adding and deleting as appropriate. +The `collection=` method makes the collection contain only the supplied objects, by adding and deleting as appropriate. The changes are persisted to the database. ##### `collection_singular_ids` @@ -1491,7 +1489,7 @@ The `collection_singular_ids` method returns an array of the ids of the objects ##### `collection_singular_ids=(ids)` -The `collection_singular_ids=` method makes the collection contain only the objects identified by the supplied primary key values, by adding and deleting as appropriate. +The `collection_singular_ids=` method makes the collection contain only the objects identified by the supplied primary key values, by adding and deleting as appropriate. The changes are persisted to the database. ##### `collection.clear` @@ -2008,7 +2006,7 @@ The `collection.destroy` method removes one or more objects from the collection ##### `collection=(objects)` -The `collection=` method makes the collection contain only the supplied objects, by adding and deleting as appropriate. +The `collection=` method makes the collection contain only the supplied objects, by adding and deleting as appropriate. The changes are persisted to the database. ##### `collection_singular_ids` @@ -2020,7 +2018,7 @@ The `collection_singular_ids` method returns an array of the ids of the objects ##### `collection_singular_ids=(ids)` -The `collection_singular_ids=` method makes the collection contain only the objects identified by the supplied primary key values, by adding and deleting as appropriate. +The `collection_singular_ids=` method makes the collection contain only the objects identified by the supplied primary key values, by adding and deleting as appropriate. The changes are persisted to the database. ##### `collection.clear` diff --git a/guides/source/autoloading_and_reloading_constants.md b/guides/source/autoloading_and_reloading_constants.md index de0fa2fdc0..61657023e7 100644 --- a/guides/source/autoloading_and_reloading_constants.md +++ b/guides/source/autoloading_and_reloading_constants.md @@ -449,9 +449,10 @@ Alright, Rails has a collection of directories similar to `$LOAD_PATH` in which to look up `post.rb`. That collection is called `autoload_paths` and by default it contains: -* All subdirectories of `app` in the application and engines. For example, - `app/controllers`. They do not need to be the default ones, any custom - directories like `app/workers` belong automatically to `autoload_paths`. +* All subdirectories of `app` in the application and engines present at boot + time. For example, `app/controllers`. They do not need to be the default + ones, any custom directories like `app/workers` belong automatically to + `autoload_paths`. * Any existing second level directories called `app/*/concerns` in the application and engines. @@ -524,7 +525,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`. @@ -624,7 +625,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 diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md index 3a1a1ccfe6..6c734c1d78 100644 --- a/guides/source/caching_with_rails.md +++ b/guides/source/caching_with_rails.md @@ -100,8 +100,8 @@ 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 +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. @@ -119,25 +119,16 @@ If you want to cache a fragment under certain conditions, you can use The `render` helper can also cache individual templates rendered for a collection. It can even one up the previous example with `each` by reading all cache -templates at once instead of one by one. This is done automatically if the template -rendered by the collection includes a `cache` call. Take a collection that renders -a `products/_product.html.erb` partial for each element: - -```ruby -render products -``` - -If `products/_product.html.erb` starts with a `cache` call like so: +templates at once instead of one by one. This is done by passing `cached: true` when rendering the collection: ```html+erb -<% cache product do %> - <%= product.name %> -<% end %> +<%= render partial: 'products/product', collection: @products, cached: true %> ``` -All the cached templates from previous renders will be fetched at once with much -greater speed. There's more info on how to make your templates [eligible for -collection caching](http://api.rubyonrails.org/classes/ActionView/Template/Handlers/ERB.html#method-i-resource_cache_call_pattern). +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. + ### Russian Doll Caching @@ -267,7 +258,7 @@ comment format anywhere in the template, like: 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 +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 @@ -521,6 +512,40 @@ class ProductsController < ApplicationController 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 ---------- diff --git a/guides/source/command_line.md b/guides/source/command_line.md index f33e729de0..f766403228 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -65,11 +65,12 @@ $ bin/rails server => 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 -=> Ctrl-C to shutdown server -Puma 2.15.3 starting... -* Min threads: 0, max threads: 16 +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. @@ -325,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`. @@ -432,7 +433,7 @@ 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, 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, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, Rack::Head, Rack::ConditionalGet, Rack::ETag Application root /home/foobar/commandsapp Environment development Database adapter sqlite3 @@ -618,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 8a21d4062a..b3d3b2c681 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -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,9 +88,9 @@ application. Accepts a valid week day symbol (e.g. `:monday`). end ``` -* `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. @@ -98,15 +98,17 @@ 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` 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.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 served over HTTPS by using the `ActionDispatch::SSL` middleware. This can be configured by setting `config.ssl_options` - see the [ActionDispatch::SSL documentation](http://edgeapi.rubyonrails.org/classes/ActionDispatch/SSL.html) for details. +* `config.force_ssl` forces all requests to be served over HTTPS by using the `ActionDispatch::SSL` middleware, and sets `config.action_mailer.default_url_options` to be `{ protocol: 'https' }`. This can be configured by setting `config.ssl_options` - see the [ActionDispatch::SSL documentation](http://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`, @@ -114,15 +116,29 @@ defaults to `:debug` for all environments. The available log levels are: `:debug * `config.log_tags` accepts a list of: methods that the `request` object responds to, a `Proc` that accepts the `request` object, or something that responds to `to_s`. This makes it easy to tag log lines with debug information like subdomain and request id - both very helpful in debugging multi-user production applications. -* `config.logger` accepts a logger conforming to the interface of Log4r or the default Ruby `Logger` class. Defaults to an instance of `ActiveSupport::Logger`. +* `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. + + ```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.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.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: @@ -137,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. @@ -155,15 +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.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 @@ -191,6 +205,7 @@ The full set of methods that can be used in this block are as follows: * `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`. +* `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`. @@ -200,7 +215,7 @@ Every Rails application comes with a standard set of middleware which it uses in * `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"`. -* `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::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. @@ -209,8 +224,6 @@ Every Rails application comes with a standard set of middleware which it uses in * `ActionDispatch::RemoteIp` checks for IP spoofing attacks and gets valid `client_ip` from request headers. Configurable with the `config.action_dispatch.ip_spoofing_check`, and `config.action_dispatch.trusted_proxies` options. * `Rack::Sendfile` intercepts responses whose body is being served from a file and replaces it with a server specific X-Sendfile header. Configurable with `config.action_dispatch.x_sendfile_header`. * `ActionDispatch::Callbacks` runs the prepare callbacks before serving the request. -* `ActiveRecord::ConnectionAdapters::ConnectionManagement` cleans active connections after each request, unless the `rack.test` key in the request environment is set to `true`. -* `ActiveRecord::QueryCache` caches all SELECT queries generated in a request. If any INSERT or UPDATE takes place then the cache is cleaned. * `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. @@ -229,6 +242,12 @@ This will put the `Magical::Unicorns` middleware on the end of the stack. You ca config.middleware.insert_before Rack::Head, Magical::Unicorns ``` +Or you can insert a middleware to exact position by using indexes. For example, if you want to insert `Magical::Unicorns` middleware on top of the stack, you can do it, like so: + +```ruby +config.middleware.insert_before 0, Magical::Unicorns +``` + There's also `insert_after` which will insert a middleware after another: ```ruby @@ -259,6 +278,26 @@ All these configuration options are delegated to the `I18n` library. * `config.i18n.load_path` sets the path Rails uses to look for locale files. Defaults to `config/locales/*.{yml,rb}`. +* `config.i18n.fallbacks` sets fallback behavior for missing translations. Here are 3 usage examples for this option: + + * You can set the option to `true` for using default locale as fallback, like so: + + ```ruby + config.i18n.fallbacks = true + ``` + + * Or you can set an array of locales as fallback, like so: + + ```ruby + config.i18n.fallbacks = [:tr, :en] + ``` + + * Or you can set different fallbacks for locales individually. For example, if you want to use `:tr` for `:az` and `:de`, `:en` for `:da` as fallbacks, you can do it, like so: + + ```ruby + config.i18n.fallbacks = { az: :tr, da: [:de, :en] } + ``` + ### Configuring Active Record `config.active_record` includes a variety of configuration options: @@ -266,8 +305,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. @@ -275,15 +314,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.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.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`. @@ -291,13 +332,13 @@ 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, @@ -315,11 +356,11 @@ All these configuration options are delegated to the `I18n` library. * `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. + Defaults to `false`. The MySQL adapter adds one additional configuration option: -* `ActiveRecord::ConnectionAdapters::Mysql2Adapter.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: @@ -331,11 +372,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 the caching features provided by the Action Controller component 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.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. @@ -373,6 +414,10 @@ The schema dumper adds one additional configuration option: * `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'`. @@ -387,7 +432,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: @@ -442,7 +487,7 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. * `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 + 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 @@ -461,9 +506,9 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. 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. + submit_tag should automatically disable on click, this defaults to `true`. -* `config.action_view.debug_missing_translation` determins whether to wrap the missing translations key in a `<span>` tag or not. 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 @@ -478,16 +523,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` or `:peer` -- or the constant directly `OpenSSL::SSL::VERIFY_NONE` or `OpenSSL::SSL::VERIFY_PEER`, respectively. + * `: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](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: @@ -533,6 +581,9 @@ There are a number of settings available on `config.action_mailer`: * `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: @@ -547,7 +598,7 @@ There are a few configuration options available in Active Support: * `config.active_support.time_precision` sets the precision of JSON encoded time values. Defaults to `3`. -* `ActiveSupport.halt_callback_chains_on_return_false` specifies whether Active Record and Active Model callback chains can be halted by returning `false` in a 'before' callback. When set to `false`, callback chains are halted only when explicitly done so with `throw(:abort)`. When set to `true`, callback chains are halted when a callback returns false (the previous behavior before Rails 5) and a deprecation warning is given. Defaults to `true` during the deprecation period. New Rails 5 apps generate an initializer file called `callback_terminator.rb` which sets the value to `false`. This file is *not* added when running `rake rails: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.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`. @@ -563,7 +614,7 @@ There are a few configuration options available in Active Support: `config.active_job` provides the following configuration options: -* `config.active_job.queue_adapter` sets the adapter for the queueing backend. The default adapter is `:inline` which will perform jobs immediately. For an up-to-date list of built-in adapters see the [ActiveJob::QueueAdapters API documentation](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html). +* `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 @@ -612,6 +663,17 @@ There are a few configuration options available in Active Support: * `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 Just about every Rails application will interact with a database. You can connect to the database by setting an environment variable `ENV['DATABASE_URL']` or by using a configuration file called `config/database.yml`. @@ -753,11 +815,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: @@ -770,7 +832,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 @@ -812,9 +874,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: @@ -867,7 +929,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 @@ -992,98 +1054,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. -* `active_job.logger` Sets `ActiveJob::Base.logger` - if it's not already set - +* `active_job.logger`: Sets `ActiveJob::Base.logger` - if it's not already set - to `Rails.logger`. -* `active_job.set_configs` Sets up Active Job by using the settings in `config.active_job` by `send`'ing the method names as setters to `ActiveJob::Base` and passing the values through. +* `active_job.set_configs`: Sets up Active Job by using the settings in `config.active_job` by `send`'ing the method names as setters to `ActiveJob::Base` and passing the values through. -* `action_mailer.logger` Sets `ActionMailer::Base.logger` - if it's not already set - to `Rails.logger`. +* `action_mailer.logger`: Sets `ActionMailer::Base.logger` - if it's not already set - to `Rails.logger`. -* `action_mailer.set_configs` Sets up Action Mailer by using the settings in `config.action_mailer` by `send`'ing the method names as setters to `ActionMailer::Base` and passing the values through. +* `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. -* `action_mailer.compile_config_methods` Initializes methods for the config settings specified so that they are quicker to access. +* `action_mailer.compile_config_methods`: Initializes methods for the config settings specified so that they are quicker to access. -* `set_load_path` This initializer runs before `bootstrap_hook`. Adds paths specified by `config.load_paths` and all autoload paths to `$LOAD_PATH`. +* `set_load_path`: This initializer runs before `bootstrap_hook`. Adds paths specified by `config.load_paths` and all autoload paths to `$LOAD_PATH`. -* `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`. +* `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_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. +* `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. -* `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. +* `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. -* `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_view_paths`: Adds the directory `app/views` from the application, railties and engines to the lookup path for view files for the application. -* `load_environment_config` Loads the `config/environments` file for the current environment. +* `load_environment_config`: Loads the `config/environments` file for the current environment. -* `prepend_helpers_path` Adds the directory `app/helpers` from the application, railties and engines to the lookup path for helpers for the application. +* `prepend_helpers_path`: Adds the directory `app/helpers` from the application, railties and engines to the lookup path for helpers for the application. -* `load_config_initializers` Loads all Ruby files from `config/initializers` in the application, railties and engines. The files in this directory can be used to hold configuration settings that should be made after all of the frameworks are loaded. +* `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. -* `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. +* `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_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_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. -* `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. +* `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. -* `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_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_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. +* `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. -* `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. +* `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. -* `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`. +* `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`. -* `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. +* `finisher_hook`: Provides a hook for after the initialization of process of the application is complete, as well as running all the `config.after_initialize` blocks for the application, railties and engines. -* `set_routes_reloader` Configures Action Dispatch to reload the routes file using `ActionDispatch::Callbacks.to_prepare`. +* `set_routes_reloader`: 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. +* `disable_dependency_loading`: Disables the automatic dependency loading if the `config.eager_load` is set to `true`. Database pooling ---------------- @@ -1188,7 +1262,7 @@ 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: +`false`: ```ruby group :development do diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index cbc304c87f..ba8d085f79 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -25,7 +25,7 @@ Reporting an Issue Ruby on Rails uses [GitHub Issue Tracking](https://github.com/rails/rails/issues) to track issues (primarily bugs and contributions of new code). If you've found a bug in Ruby on Rails, this is the place to start. You'll need to create a (free) GitHub account in order to submit an issue, to comment on them or to create pull requests. -NOTE: Bugs in the most recent released version of Ruby on Rails are likely to get the most attention. Also, the Rails core team is always interested in feedback from those who can take the time to test _edge Rails_ (the code for the version of Rails that is currently under development). Later in this guide you'll find out how to get edge Rails for testing. +NOTE: Bugs in the most recent released version of Ruby on Rails are likely to get the most attention. Also, the Rails core team is always interested in feedback from those who can take the time to test _edge Rails_ (the code for the version of Rails that is currently under development). Later in this guide, you'll find out how to get edge Rails for testing. ### Creating a Bug Report @@ -58,7 +58,7 @@ WARNING: Please do not report security vulnerabilities with public GitHub issue Please don't put "feature request" items into GitHub Issues. If there's a new feature that you want to see added to Ruby on Rails, you'll need to write the code yourself - or convince someone else to partner with you to write the code. -Later in this guide you'll find detailed instructions for proposing a patch to +Later in this guide, you'll find detailed instructions for proposing a patch to Ruby on Rails. If you enter a wish list item in GitHub Issues with no code, you can expect it to be marked "invalid" as soon as it's reviewed. @@ -105,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 ``` @@ -130,11 +130,11 @@ learn about Ruby on Rails, and the API, which serves as a reference. You can help improve the Rails guides by making them more coherent, consistent or readable, adding missing information, correcting factual errors, fixing typos, or bringing them up to date with the latest edge Rails. -You can either open a pull request to [Rails](http://github.com/rails/rails) or -ask the [Rails core team](http://rubyonrails.org/core) for commit access on +You can either open a pull request to [Rails](https://github.com/rails/rails) or +ask the [Rails core team](http://rubyonrails.org/community/#core) for commit access on docrails if you contribute regularly. Please do not open pull requests in docrails, if you'd like to get feedback on your -change, ask for it in [Rails](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. @@ -159,7 +159,7 @@ If you want to translate the Rails guides in your own language, follows these st * 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* direcotry then run (eg. for it-IT): +To generate the guides in HTML format cd into the *guides* directory then run (eg. for it-IT): ```bash $ bundle install @@ -174,8 +174,8 @@ 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**: [http://github.com/apohllo/docrails/tree/master](http://github.com/apohllo/docrails/tree/master) -* **French** : [http://github.com/railsfrance/docrails](http://github.com/railsfrance/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) @@ -189,7 +189,7 @@ Contributing to the Rails Code ### Setting Up a Development Environment -To move on from submitting bugs to helping resolve existing issues or contributing your own code to Ruby on Rails, you _must_ be able to run its test suite. In this section of the guide you'll learn how to setup the tests on your own computer. +To move on from submitting bugs to helping resolve existing issues or contributing your own code to Ruby on Rails, you _must_ be able to run its test suite. In this section of the guide, you'll learn how to setup the tests on your own computer. #### The Easy Way @@ -204,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: @@ -299,9 +299,9 @@ Please see the benchmark/ips [README](https://github.com/evanphx/benchmark-ips/b ### Running Tests It is not customary in Rails to run the full test suite before pushing -changes. The railties test suite in particular takes a long time, and even -more if the source code is mounted in `/vagrant` as happens in the recommended -workflow with the [rails-dev-box](https://github.com/rails/rails-dev-box). +changes. The railties test suite in particular takes a long time, and takes an +especially long time if the source code is mounted in `/vagrant` as happens in +the recommended workflow with the [rails-dev-box](https://github.com/rails/rails-dev-box). As a compromise, test what your code obviously affects, and if the change is not in railties, run the whole test suite of the affected component. If all @@ -367,7 +367,7 @@ 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: @@ -506,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: @@ -520,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: @@ -662,7 +662,7 @@ Changes that are merged into master are intended for the next major release of R For simple fixes, the easiest way to backport your changes is to [extract a diff from your changes in master and apply them to the target branch](http://ariejan.net/2009/10/26/how-to-create-and-apply-a-patch-with-git). -First make sure your changes are the only difference between your current branch and master: +First, make sure your changes are the only difference between your current branch and master: ```bash $ git log master..HEAD diff --git a/guides/source/credits.html.erb b/guides/source/credits.html.erb index 1d995581fa..511d76041b 100644 --- a/guides/source/credits.html.erb +++ b/guides/source/credits.html.erb @@ -64,7 +64,7 @@ Oscar Del Ben is a software engineer at <a href="http://www.wildfireapp.com/">Wi <% end %> <%= author('Pratik Naik', 'lifo') do %> - Pratik Naik is a Ruby on Rails developer at <a href="https://basecamp.com/">Basecamp</a> and also a member of the <a href="http://rubyonrails.org/core">Rails core team</a>. He maintains a blog at <a href="http://m.onkey.org">has_many :bugs, :through => :rails</a> and has a semi-active <a href="http://twitter.com/lifo">twitter account</a>. + Pratik Naik is a Ruby on Rails developer at <a href="https://basecamp.com/">Basecamp</a> and maintains a blog at <a href="http://m.onkey.org">has_many :bugs, :through => :rails</a>. He also has a semi-active <a href="http://twitter.com/lifo">twitter account</a>. <% end %> <%= author('Emilio Tagua', 'miloops') do %> diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md index 35ad6eb705..f0d0f9753a 100644 --- a/guides/source/debugging_rails_applications.md +++ b/guides/source/debugging_rails_applications.md @@ -109,18 +109,18 @@ It can also be useful to save information to log files at runtime. Rails maintai 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 `environment.rb` or any other environment file, for example: +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. @@ -255,7 +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 into the underlying Rails code. +use this guide to learn how to move from the code you have written into the +underlying Rails code. ### Setup @@ -314,13 +315,12 @@ For example: => 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 -=> Ctrl-C to shutdown server -Puma 2.15.3 starting... -* Min threads: 0, max threads: 16 +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 @@ -336,7 +336,6 @@ Processing by ArticlesController#index as HTML 10: respond_to do |format| 11: format.html # index.html.erb 12: format.json { render json: @articles } - (byebug) ``` @@ -346,11 +345,45 @@ by asking the debugger for help. Type: `help` ``` (byebug) help - h[elp][ <cmd>[ <subcmd>]] + 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 - help -- prints this help. - help <cmd> -- prints help on command <cmd>. - help <cmd> <subcmd> -- prints help on <cmd>'s subcommand <subcmd>. +(byebug) ``` To see the previous ten lines you should type `list-` (or `l-`). @@ -369,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 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=` +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= @@ -390,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) ``` @@ -412,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 @@ -460,11 +490,11 @@ 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 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 when you are debugging concurrent threads and need to verify that there are no race conditions in your code. @@ -491,9 +521,9 @@ current context: 12: format.json { render json: @articles } (byebug) instance_variables -[:@_action_has_layout, :@_routes, :@_headers, :@_status, :@_request, - :@_response, :@_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 @@ -503,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 @@ -522,29 +553,35 @@ And then ask again for the instance_variables: ``` (byebug) instance_variables -[:@_action_has_layout, :@_routes, :@_headers, :@_status, :@_request, - :@_response, :@_prefixes, :@_lookup_context, :@_action_name, - :@_response_body, :@marked_for_same_origin_verification, :@_config, - :@articles] +[:@_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 will start an irb session 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 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 @@ -562,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. ``` @@ -580,7 +618,7 @@ tracking the values of a variable while the execution goes on. ``` 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 +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 @@ -590,32 +628,23 @@ 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, 6] in /PathToProject/app/models/article.rb 1: class Article < ApplicationRecord - 2: - 3: def self.find_recent(limit = 10) - 4: byebug -=> 5: where('created_at > ?', 1.week.ago).limit(limit) - 6: end - 7: - 8: end + 2: def self.find_recent(limit = 10) + 3: byebug +=> 4: where('created_at > ?', 1.week.ago).limit(limit) + 5: end + 6: end (byebug) ``` @@ -627,11 +656,7 @@ method. ``` (byebug) next - -Next advances to the next line (line 6: `end`), which returns to the next line -of the caller method: - -[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 @@ -652,23 +677,24 @@ 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. +TIP: You can also use `step n` or `next n` to move forward `n` steps at once. + ### Breakpoints A breakpoint makes your application stop whenever a certain point in the program @@ -677,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 @@ -702,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. @@ -727,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 @@ -745,24 +771,22 @@ 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 @@ -776,21 +800,43 @@ will be stopped and you will have to start it again. `byebug` has a few available options to tweak its behavior: -* `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. +``` +(byebug) help set + + set <setting> <value> -You can see the full list by using `help set`. Use `help set _subcommand_` to -learn about a particular `set` command. + 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. + + You can see these environment settings with the "show" 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 ``` diff --git a/guides/source/development_dependencies_install.md b/guides/source/development_dependencies_install.md index 7beb8f72a9..cc24e6f666 100644 --- a/guides/source/development_dependencies_install.md +++ b/guides/source/development_dependencies_install.md @@ -30,7 +30,6 @@ Ruby on Rails uses Git for source code control. The [Git homepage](http://git-sc * [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. * [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. diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml index 4473eba478..a06a53b250 100644 --- a/guides/source/documents.yaml +++ b/guides/source/documents.yaml @@ -116,7 +116,7 @@ name: The Rails Initialization Process work_in_progress: true url: initialization.html - description: This guide explains the internals of the Rails initialization process as of Rails 4. + description: This guide explains the internals of the Rails initialization process. - name: Autoloading and Reloading Constants url: autoloading_and_reloading_constants.html @@ -135,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 @@ -187,6 +195,11 @@ url: upgrading_ruby_on_rails.html description: This guide helps in upgrading applications to latest Ruby on Rails versions. - + name: Ruby on Rails 5.0 Release Notes + url: 5_0_release_notes.html + description: Release notes for Rails 5.0. + work_in_progress: true + - name: Ruby on Rails 4.2 Release Notes url: 4_2_release_notes.html description: Release notes for Rails 4.2. diff --git a/guides/source/engines.md b/guides/source/engines.md index 697938434c..f9a37e45ac 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -11,9 +11,9 @@ After reading this guide, you will know: * What makes an engine. * How to generate an engine. -* Building features for the engine. -* Hooking the engine into an application. -* Overriding engine functionality in the application. +* How to build features for the engine. +* How to hook the engine into an application. +* How to override engine functionality in the application. -------------------------------------------------------------------------------- @@ -25,7 +25,7 @@ their host applications. A Rails application is actually just a "supercharged" engine, with the `Rails::Application` class inheriting a lot of its behavior from `Rails::Engine`. -Therefore, engines and applications can be thought of almost the same thing, +Therefore, engines and applications can be thought of as almost the same thing, just with subtle differences, as you'll see throughout this guide. Engines and applications also share a common structure. @@ -402,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: @@ -461,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 @@ -799,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. @@ -807,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 @@ -1034,6 +1034,8 @@ typical `GET` to a controller in a controller's functional test like this: ```ruby module Blorgh class FooControllerTest < ActionDispatch::IntegrationTest + include Engine.routes.url_helpers + def test_index get foos_url ... @@ -1050,6 +1052,8 @@ in your setup code: ```ruby module Blorgh class FooControllerTest < ActionDispatch::IntegrationTest + include Engine.routes.url_helpers + setup do @routes = Engine.routes end @@ -1205,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 diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md index 2a289dd33a..048fe190e8 100644 --- a/guides/source/form_helpers.md +++ b/guides/source/form_helpers.md @@ -174,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) %> @@ -195,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" /> @@ -317,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: diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index 9677ab1583..65fdd7ca0d 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -93,7 +93,7 @@ current version of Ruby installed: ```bash $ ruby -v -ruby 2.2.2p95 +ruby 2.3.1p112 ``` TIP: A number of tools exist to help you quickly install Ruby and Ruby @@ -164,7 +164,7 @@ of the files and folders that Rails created by default: | File/Folder | Purpose | | ----------- | ------- | -|app/|Contains the controllers, models, views, helpers, mailers and assets for your application. You'll focus on this folder for the remainder of this guide.| +|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.| @@ -223,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 @@ -245,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. @@ -264,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 @@ -298,33 +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. - # See how all your routes lay out with "bin/rails routes". - # - # 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 @@ -348,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 @@ -387,7 +391,7 @@ 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 @@ -403,7 +407,7 @@ a controller called `ArticlesController`. You can do this by running this command: ```bash -$ bin/rails generate controller articles +$ bin/rails generate controller Articles ``` If you open up the newly generated `app/controllers/articles_controller.rb` @@ -471,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 @@ -625,7 +629,7 @@ end 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. @@ -635,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 @@ -653,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. @@ -686,7 +690,7 @@ class CreateArticles < ActiveRecord::Migration[5.0] t.string :title t.text :text - t.timestamps null: false + t.timestamps end end end @@ -767,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. @@ -1540,7 +1544,7 @@ 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. +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. @@ -1554,9 +1558,9 @@ class CreateComments < ActiveRecord::Migration[5.0] create_table :comments do |t| t.string :commenter t.text :body - t.references :article, index: true, foreign_key: true + t.references :article, foreign_key: true - t.timestamps null: false + t.timestamps end end end diff --git a/guides/source/i18n.md b/guides/source/i18n.md index 5bbd4048b9..f3802a142f 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 -------------------------------------------------------------------------------- @@ -109,12 +109,11 @@ The **translations load path** (`I18n.load_path`) is an array of paths to files 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 `config/application.rb` file has instructions on how to add locales from another directory and how to set a different default locale. +You can change the default locale as well as configure the translations load paths in `config/application.rb` as follows: ```ruby -# 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 + config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] + config.i18n.default_locale = :de ``` 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`: @@ -1113,7 +1112,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 @@ -1130,10 +1129,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 6232ef4c57..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: @@ -157,7 +157,7 @@ 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 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 @@ -356,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 diff --git a/guides/source/layout.html.erb b/guides/source/layout.html.erb index 1f81ea4694..9abb863da6 100644 --- a/guides/source/layout.html.erb +++ b/guides/source/layout.html.erb @@ -24,7 +24,17 @@ <% end %> <div id="topNav"> <div class="wrapper"> - <strong class="more-info-label">←<a href="http://rubyonrails.org/">Back to rubyonrails.org:</a> </strong> + <strong class="more-info-label">More at <a href="http://rubyonrails.org/">rubyonrails.org:</a> </strong> + <span class="red-button more-info-button"> + More Ruby on Rails + </span> + <ul class="more-info-links s-hidden"> + <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> <div id="header"> @@ -101,7 +111,7 @@ <%= link_to 'open an issue', 'https://github.com/rails/rails/issues' %>. </p> <p>And last but not least, any kind of discussion regarding Ruby on Rails - documentation is very welcome in the <%= link_to 'rubyonrails-docs mailing list', 'http://groups.google.com/group/rubyonrails-docs' %>. + documentation is very welcome in the <%= link_to 'rubyonrails-docs mailing list', 'https://groups.google.com/forum/#!forum/rubyonrails-docs' %>. </p> </div> </div> diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md index d55e1007ee..2722789c49 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -149,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 @@ -238,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: When using `html:` option, HTML entities will be escaped if the string is not marked as HTML safe by using `html_safe` method. +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 @@ -555,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/` @@ -700,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 diff --git a/guides/source/rails_application_templates.md b/guides/source/rails_application_templates.md index 5a46baff2d..3e99ee7021 100644 --- a/guides/source/rails_application_templates.md +++ b/guides/source/rails_application_templates.md @@ -15,18 +15,18 @@ After reading this guide, you will know: Usage ----- -To apply a template, you need to provide the Rails generator with the location of the template you wish to apply using the -m option. This can either be a path to a file or a URL. +To apply a template, you need to provide the Rails generator with the location of the template you wish to apply using the `-m` option. This can either be a path to a file or a URL. ```bash $ 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 `app:template` Rake task to apply templates to an existing Rails application. The location of the template needs to be passed in via the LOCATION environment variable. Again, this can either be path to a file or a URL. ```bash -$ bin/rails rails:template LOCATION=~/template.rb -$ bin/rails 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 @@ -175,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 rake tasks in the Rails application. Let's say you want to migrate the database: +Runs the supplied task in the Rails application. Let's say you want to migrate the database: ```ruby -rake "db:migrate" +rails_command "db:migrate" ``` -You can also run rake tasks with a different Rails environment: +You can also run tasks with a different Rails environment: ```ruby -rake "db:migrate", env: 'production' +rails_command "db:migrate", env: 'production' +``` + +You can also run tasks as a super-user: + +```ruby +rails_command "log:clear", sudo: true ``` ### route(routing_code) @@ -226,7 +232,7 @@ CODE 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 934693252e..8148f70c31 100644 --- a/guides/source/rails_on_rack.md +++ b/guides/source/rails_on_rack.md @@ -93,7 +93,7 @@ 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/rails middleware @@ -104,20 +104,19 @@ For a freshly generated Rails application, this might produce something like: ```ruby use Rack::Sendfile use ActionDispatch::Static -use Rack::Lock -use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x000000029a0838> +use ActionDispatch::Executor +use ActiveSupport::Cache::Strategy::LocalCache::Middleware use Rack::Runtime use Rack::MethodOverride use ActionDispatch::RequestId use Rails::Rack::Logger use ActionDispatch::ShowExceptions +use WebConsole::Middleware use ActionDispatch::DebugExceptions use ActionDispatch::RemoteIp use ActionDispatch::Reloader use ActionDispatch::Callbacks use ActiveRecord::Migration::CheckPending -use ActiveRecord::ConnectionAdapters::ConnectionManagement -use ActiveRecord::QueryCache use ActionDispatch::Cookies use ActionDispatch::Session::CookieStore use ActionDispatch::Flash @@ -149,9 +148,9 @@ You can add a new middleware to the middleware stack using any of the following # Push Rack::BounceFavicon at the bottom config.middleware.use Rack::BounceFavicon -# Add Lifo::Cache after ActiveRecord::QueryCache. +# Add Lifo::Cache after ActionDispatch::Executor. # Pass { page_cache: false } argument to Lifo::Cache. -config.middleware.insert_after ActiveRecord::QueryCache, Lifo::Cache, page_cache: false +config.middleware.insert_after ActionDispatch::Executor, Lifo::Cache, page_cache: false ``` #### Swapping a Middleware @@ -171,10 +170,10 @@ 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 @@ -219,6 +218,10 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol * 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. @@ -263,14 +266,6 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol * Checks pending migrations and raises `ActiveRecord::PendingMigrationError` if any migrations are pending. -**`ActiveRecord::ConnectionAdapters::ConnectionManagement`** - -* Cleans active connections after each request, unless the `rack.test` key in the request environment is set to `true`. - -**`ActiveRecord::QueryCache`** - -* Enables the Active Record query cache. - **`ActionDispatch::Cookies`** * Sets cookies for the request. diff --git a/guides/source/routing.md b/guides/source/routing.md index 5a745b10cd..756e0fefd7 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -9,16 +9,16 @@ After reading this guide, you will know: * 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 declare route parameters, which are passed onto controller actions. * How to automatically create paths and URLs using route helpers. -* Advanced techniques such as constraints and Rack endpoints. +* Advanced techniques such as creating constraints and mounting Rack endpoints. -------------------------------------------------------------------------------- The Purpose of the Rails Router ------------------------------- -The Rails router recognizes URLs and dispatches them to a controller's action. It can also generate paths and URLs, avoiding the need to hardcode strings in your views. +The Rails router recognizes URLs and dispatches them to a controller's action, or to a Rack application. It can also generate paths and URLs, avoiding the need to hardcode strings in your views. ### Connecting URLs to Code @@ -812,10 +812,10 @@ 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. @@ -1136,10 +1136,21 @@ For example, here's a small section of the `rails routes` output for a RESTful r 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. -```bash -$ CONTROLLER=users bin/rails routes +``` +$ 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. + +``` +$ 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 `rails routes` is much more readable if you widen your terminal window until the output lines don't wrap. diff --git a/guides/source/security.md b/guides/source/security.md index 1d0e87d831..ca985134e6 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,7 +89,7 @@ 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_. @@ -102,7 +102,7 @@ Thus the session becomes a more secure place to store data. The encryption is done using a server-side secret key `secrets.secret_key_base` stored in `config/secrets.yml`. -That means the security of this storage depends on this secret (and on the digest algorithm, which defaults to SHA1, for compatibility). So _don't use a trivial secret, i.e. a word from a dictionary, or one which is shorter than 30 characters, use `rake secret` instead_. +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.: @@ -137,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._ ![Session fixation](images/session_fixation.png) -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. @@ -160,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. @@ -168,7 +168,7 @@ 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 < ApplicationRecord @@ -196,11 +196,11 @@ This attack method works by including malicious code or a link in a page that ac ![](images/csrf.png) -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: +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. +* 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. @@ -381,7 +381,7 @@ Refer to the Injection section for countermeasures against XSS. It is _recommend **CSRF** Cross-Site Request Forgery (CSRF), also known as Cross-Site Reference Forgery (XSRF), is a gigantic attack method, it allows the attacker to do everything the administrator or Intranet user may do. As you have already seen above how CSRF works, here are a few examples of what attackers can do in the Intranet or admin interface. -A real-world example is a [router reconfiguration by CSRF](http://www.h-online.com/security/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 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. If the victim was logged into Google Adsense, the administration interface for Google advertisement campaigns, an attacker could change the credentials of the victim.
@@ -453,7 +453,7 @@ 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 @@ -466,7 +466,7 @@ The problem with CAPTCHAs is that they have a negative impact on the user experi Most bots are really dumb. They crawl the web and put their spam into every form's field they can find. Negative CAPTCHAs take advantage of that and include a "honeypot" field in the form which will be hidden from the human user by CSS or JavaScript. -Note that negative CAPTCHAs are only effective against dumb bots and won't suffice to protect critical applications from targeted bots. Still, the negative and positive CAPTCHAs can be combined to increase the performance, e.g., if the "honeypot" field is not empty (bot detected), you won't need to verify the positive CAPTCHA, which would require a HTTPS request to Google ReCaptcha before computing the response. +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: @@ -494,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._ @@ -565,7 +567,7 @@ This is alright for some web applications, but certainly not if the user is not Depending on your web application, there will be many more parameters the user can tamper with. As a rule of thumb, _no user input data is secure, until proven otherwise, and every parameter from the user is potentially manipulated_. -Don't be fooled by security by obfuscation and JavaScript security. The Web Developer Toolbar for Mozilla Firefox lets you review and change every form's hidden fields. _JavaScript can be used to validate user input data, but certainly not to prevent attackers from sending malicious requests with unexpected values_. The Live Http Headers plugin for Mozilla Firefox logs every request and may repeat and change them. That is an easy way to bypass any JavaScript validations. And there are even client-side proxies that allow you to intercept any request and response from and to the Internet. +Don't be fooled by security by obfuscation and JavaScript security. Developer tools let you review and change every form's hidden fields. _JavaScript can be used to validate user input data, but certainly not to prevent attackers from sending malicious requests with unexpected values_. The Firebug addon for Mozilla Firefox logs every request and may repeat and change them. That is an easy way to bypass any JavaScript validations. And there are even client-side proxies that allow you to intercept any request and response from and to the Internet. Injection --------- @@ -675,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_. @@ -720,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** flag to cookies, so that document.cookie may not be read by JavaScript. Http only cookies can be used from IE v6.SP1, Firefox v2.0.0.5 and Opera 9.5. Safari is still considering, it ignores the option. But other, older browsers (such as WebTV and IE 5.5 on Mac) can actually cause the page to fail to load. Be warned that cookies [will still be visible using Ajax](https://www.owasp.org/index.php/HTTPOnly#Browsers_Supporting_HttpOnly), though. +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 @@ -787,7 +787,7 @@ 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. diff --git a/guides/source/testing.md b/guides/source/testing.md index b5e49a41f4..050bdda9e3 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -265,7 +265,7 @@ By now you've caught a glimpse of some of the assertions that are available. Ass 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. +specify to make your test failure messages clearer. | Assertion | Purpose | | ---------------------------------------------------------------- | ------- | @@ -287,7 +287,7 @@ specify to make your test failure messages clearer. | `assert_not_in_delta( expected, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are not within `delta` of each other.| | `assert_throws( symbol, [msg] ) { block }` | Ensures that the given block throws the symbol.| | `assert_raises( exception1, exception2, ... ) { block }` | Ensures that the given block raises one of the given exceptions.| -| `assert_nothing_raised( exception1, exception2, ... ) { block }` | Ensures that the given block doesn't raise one of the given exceptions.| +| `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.| @@ -316,12 +316,12 @@ 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)` | 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`.| +| [`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. @@ -329,11 +329,11 @@ You'll see the usage of some of these assertions in the next chapter. 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` -* `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. @@ -415,6 +415,8 @@ You can find comprehensive documentation in the [Fixtures API documentation](htt _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 @@ -493,7 +495,8 @@ users(:david) users(:david).id # one can also access methods available on the User class -email(david.partner.email, david.location_tonight) +david = users(:david) +david.call(david.partner) ``` To get multiple fixtures at once, you can pass in a list of fixture names. For example: @@ -518,7 +521,7 @@ 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`. +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 @@ -589,7 +592,7 @@ class BlogFlowTest < ActionDispatch::IntegrationTest 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. +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. @@ -638,9 +641,9 @@ We were able to successfully test a very small workflow for visiting our blog an 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: @@ -650,8 +653,7 @@ 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? -The easiest way to see functional tests in action is to generate a controller -scaffold: +The easiest way to see functional tests in action is to generate a controller using the scaffold generator: ```bash $ bin/rails generate scaffold_controller article title:string body:text @@ -664,7 +666,7 @@ create test/controllers/articles_controller_test.rb ``` This will generate the controller code and tests for an `Article` resource. -You can take look at the file `articles_controller_test.rb` in the `test/controllers` directory. +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: @@ -677,7 +679,7 @@ create test/controllers/articles_controller_test.rb ... ``` -Let me take you through one such test, `test_should_get_index` from the file `articles_controller_test.rb`. +Let's take a look at one such test, `test_should_get_index` from the file `articles_controller_test.rb`. ```ruby # articles_controller_test.rb @@ -693,7 +695,7 @@ 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 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 route (i.e. `articles_url`). @@ -705,7 +707,7 @@ 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: @@ -753,7 +755,7 @@ 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" do @@ -797,7 +799,7 @@ 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 articles_url # simulate the request with custom header @@ -808,7 +810,7 @@ post article_url # simulate the request with custom env variable ### Testing `flash` notices -If you remember from earlier one of the Three 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. @@ -829,7 +831,7 @@ end If we run our test now, we should see a failure: ```bash -$ bin/rails 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: @@ -867,7 +869,7 @@ end Now if we run our tests, we should see it pass: ```bash -$ bin/rails 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: @@ -893,7 +895,7 @@ test "should show article" do 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? @@ -913,14 +915,19 @@ We can also add a test for updating an existing Article. ```ruby test "should update article" do article = articles(:one) + 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' @@ -952,8 +959,12 @@ class ArticlesControllerTest < ActionDispatch::IntegrationTest end test "should update article" do - patch article_url(@article), params: { article: { title: "updated" } } + 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 ``` @@ -966,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) @@ -999,6 +1010,8 @@ Testing Routes Like everything else in your Rails application, you can test your routes. +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 @@ -1045,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 @@ -1068,27 +1081,31 @@ end 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 - test "should return the user name" do - # ... + test "should return the user's full name" do + user = users(:david) + + assert_dom_equal %{<a href="/user/#{user.id}">David Heinemeier Hansson</a>}, link_to_user(user) end end ``` @@ -1123,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 @@ -1175,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 @@ -1204,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 @@ -1252,10 +1269,12 @@ class ProductTest < ActiveJob::TestCase end ``` -Testing Time-Dependent Code ---------------------------- +Additional Testing Resources +---------------------------- + +### Testing Time-Dependent Code -Rails provides inbuilt helper methods that enable you to assert that your time-sensitve code works as expected. +Rails provides built-in helper methods that enable you to assert that your time-sensitive code works as expected. Here is an example using the [`travel_to`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html#method-i-travel_to) helper: @@ -1270,5 +1289,5 @@ end assert_equal Date.new(2004, 10, 24), user.activation_date # The change was visible only inside the `travel_to` block. ``` -Please see [`ActiveSupport::TimeHelpers` API Documentation](http://api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html) +Please see [`ActiveSupport::Testing::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 202e5b5cb9..82080c4def 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -16,6 +16,21 @@ 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: @@ -27,15 +42,15 @@ Rails generally stays close to the latest released Ruby version when it's releas 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 @@ -55,7 +70,8 @@ Upgrading from Rails 4.2 to Rails 5.0 ### Ruby 2.2.2+ -ToDo... +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 @@ -168,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 @@ -865,7 +881,7 @@ Rails 4.0 no longer supports loading plugins from `vendor/plugins`. You must rep * Rails 4.0 has removed the identity map from Active Record, due to [some inconsistencies with associations](https://github.com/rails/rails/commit/302c912bf6bcd0fa200d964ec2dc4a44abe328a6). If you have manually enabled it in your application, you will have to remove the following config that has no effect anymore: `config.active_record.identity_map`. -* The `delete` method in collection associations can now receive `Fixnum` or `String` arguments as record ids, besides records, pretty much like the `destroy` method does. Previously it raised `ActiveRecord::AssociationTypeMismatch` for such arguments. From Rails 4.0 on `delete` automatically tries to find the records matching the given ids before deleting them. +* The `delete` method in collection associations can now receive `Integer` or `String` arguments as record ids, besides records, pretty much like the `destroy` method does. Previously it raised `ActiveRecord::AssociationTypeMismatch` for such arguments. From Rails 4.0 on `delete` automatically tries to find the records matching the given ids before deleting them. * In Rails 4.0 when a column or a table is renamed the related indexes are also renamed. If you have migrations which rename the indexes, they are no longer needed. diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md index 48fc6bc9c0..c1dfcab6f3 100644 --- a/guides/source/working_with_javascript_in_rails.md +++ b/guides/source/working_with_javascript_in_rails.md @@ -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 @@ -328,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" } @@ -350,8 +350,8 @@ $("<%= 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 @@ -364,14 +364,14 @@ 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 @@ -389,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/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 d4e78b6aa1..8f9cc181a0 100644 --- a/rails.gemspec +++ b/rails.gemspec @@ -14,7 +14,7 @@ Gem::Specification.new do |s| 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'] diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 2c363c55da..b8f1cda329 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,451 +1,5 @@ -* Bring back `TEST=` env for `rake test` task. +* Added a shared section to `config/secrets.yml` that will be loaded for all environments. - *Yves Senn* + *DHH* -* Specify log file names or all logs to clear `rake log:clear` - - Specify which logs to clear when using the `rake log:clear` task, e.g. `rake log:clear LOGS=test,staging` - - Clear all logs from log/*.log e.g. `rake log:clear LOGS=all` - - By default `rake log:clear` clears standard environment log files i.e. 'development,test,production' - - *Pramod Shinde* - -* Fix using `add_source` with a block after using `gem` in a custom generator. - - *Will Fisher* - -## Rails 5.0.0.beta1 (December 18, 2015) ## - -* Newly generated plugins get a `README.md` in Markdown. - - *Yuji Yaginuma* - -* The generated config file for the development environment includes a new - config line, commented out, showing how to enable the evented file watcher. - - *Xavier Noria* - -* `config.debug_exception_response_format` configures the format used - in responses when errors occur in development mode. - - Set `config.debug_exception_response_format` to render an HTML page with - debug info (using the value `:default`) or render debug info preserving - the response format (using the value `:api`). - - *Jorge Bejar* - -* Fix setting exit status code for rake test tasks. The exit status code - was not set when tests were fired with `rake`. Now, it is being set and it matches - behavior of running tests via `rails` command (`rails test`), so no matter if - `rake test` or `rails test` command is used the exit code will be set. - - *Arkadiusz Fal* - -* Add Command infrastructure to replace rake. - - Also move `rake dev:cache` to new infrastructure. You'll need to use - `rails dev:cache` to toggle development caching from now on. - - *Chuck Callebs* - -* Allow use of minitest-rails gem with Rails test runner. - - Fixes #22455. - - *Chris Kottom* - -* Add `bin/test` script to rails plugin. - - `bin/test` can use the same API as `bin/rails test`. - - *Yuji Yaginuma* - -* Make `static_index` part of the `config.public_file_server` config and - call it `public_file_server.index_name`. - - *Yuki Nishijima* - -* Deprecate `serve_static_files` in favor of `public_file_server.enabled`. - - Unifies the static asset options under `public_file_server`. - - To upgrade, replace occurrences of: - - ``` - config.serve_static_files = # false or true - ``` - - in your environment files, with: - - ``` - config.public_file_server.enabled = # false or true - ``` - - *Kasper Timm Hansen* - -* Deprecate `config.static_cache_control` in favor of - `config.public_file_server.headers`. - - To upgrade, replace occurrences of: - - ``` - config.static_cache_control = 'public, max-age=60' - ``` - - in your environment files, with: - - ``` - config.public_file_server.headers = { - 'Cache-Control' => 'public, max-age=60' - } - ``` - - `config.public_file_server.headers` can set arbitrary headers, sent along when - a response is delivered. - - *Yuki Nishijima* - -* Route generator should be idempotent - running generators several times no longer require you to cleanup routes.rb - - *Thiago Pinto* - -* Allow passing an environment to `config_for`. - - *Simon Eskildsen* - -* Allow rake:stats to account for rake tasks in lib/tasks - - *Kevin Deisz* - -* Added javascript to update the URL on mailer previews with the currently - selected email format. Reloading the page now keeps you on your selected - format rather than going back to the default html version. - - *James Kerr* - -* Add fail fast to `bin/rails test` - - Adding `--fail-fast` or `-f` when running tests will interrupt the run on - the first failure: - - ``` - # Running: - - ................................................S......E - - ArgumentError: Wups! Bet you didn't expect this! - test/models/bunny_test.rb:19:in `block in <class:BunnyTest>' - - bin/rails test test/models/bunny_test.rb:18 - - ....................................F - - This failed - - bin/rails test test/models/bunny_test.rb:14 - - Interrupted. Exiting... - - - Finished in 0.051427s, 1808.3872 runs/s, 1769.4972 assertions/s. - - ``` - - Note that any unexpected errors don't abort the run. - - *Kasper Timm Hansen* - -* Add inline output to `bin/rails test` - - Any failures or errors (and skips if running in verbose mode) are output - during a test run: - - ``` - # Running: - - .....S..........................................F - - This failed - - bin/rails test test/models/bunny_test.rb:14 - - .................................E - - ArgumentError: Wups! Bet you didn't expect this! - test/models/bunny_test.rb:19:in `block in <class:BunnyTest>' - - bin/rails test test/models/bunny_test.rb:18 - - .................... - - Finished in 0.069708s, 1477.6019 runs/s, 1448.9106 assertions/s. - ``` - - Output can be deferred to after a run with the `--defer-output` option. - - *Kasper Timm Hansen* - -* Fix displaying mailer previews on non local requests when config - `action_mailer.show_previews` is set - - *Wojciech Wnętrzak* - -* `rails server` will now honour the `PORT` environment variable - - *David Cornu* - -* Plugins generated using `rails plugin new` are now generated with the - version number set to 0.1.0. - - *Daniel Morris* - -* `I18n.load_path` is now reloaded under development so there's no need to - restart the server to make new locale files available. Also, I18n will no - longer raise for deleted locale files. - - *Kir Shatrov* - -* Add `bin/update` script to update development environment automatically. - - *Mehmet Emin İNAÇ* - -* Fix STATS_DIRECTORIES already defined warning when running rake from within - the top level directory of an engine that has a test app. - - Fixes #20510. - - *Ersin Akinci* - -* Make enabling or disabling caching in development mode possible with - rake dev:cache. - - Running rake dev:cache will create or remove tmp/caching-dev.txt. When this - file exists config.action_controller.perform_caching will be set to true in - config/environments/development.rb. - - Additionally, a server can be started with either --dev-caching or - --no-dev-caching included to toggle caching on startup. - - *Jussi Mertanen*, *Chuck Callebs* - -* Add a `--api` option in order to generate plugins that can be added - inside an API application. - - *Robin Dupret* - -* Fix `NoMethodError` when generating a scaffold inside a full engine. - - *Yuji Yaginuma* - -* Adding support for passing a block to the `add_source` action of a custom generator - - *Mike Dalton*, *Hirofumi Wakasugi* - -* `assert_file` understands paths with special characters - (eg. `v0.1.4~alpha+nightly`). - - *Diego Carrion* - -* Remove ContentLength middleware from the defaults. If you want it, just - add it as a middleware in your config. - - *Egg McMuffin* - -* Make it possible to customize the executable inside rerun snippets. - - *Yves Senn* - -* Add support for API only apps. - Middleware stack was slimmed down and it has only the needed - middleware for API apps & generators generates the right files, - folders and configurations. - - *Santiago Pastorino*, *Jorge Bejar* - -* Make generated scaffold functional tests work inside engines. - - *Yuji Yaginuma* - -* Generate a `.keep` file in the `tmp` folder by default as many scripts - assume the existence of this folder and most would fail if it is absent. - - See #20299. - - *Yoong Kang Lim*, *Sunny Juneja* - -* `config.static_index` configures directory `index.html` filename - - Set `config.static_index` to serve a static directory index file not named - `index`. E.g. to serve `main.html` instead of `index.html` for directory - requests, set `config.static_index` to `"main"`. - - *Eliot Sykes* - -* `bin/setup` uses built-in rake tasks (`log:clear`, `tmp:clear`). - - *Mohnish Thallavajhula* - -* Fix mailer previews with attachments by using the mail gem's own API to - locate the first part of the correct mime type. - - Fixes #14435. - - *Andrew White* - -* Remove sqlite support from `rails dbconsole`. - - *Andrew White* - -* Rename `railties/bin` to `railties/exe` to match the new Bundler executables - convention. - - *Islam Wazery* - -* Print `bundle install` output in `rails new` as soon as it's available. - - Running `rails new` will now print the output of `bundle install` as - it is available, instead of waiting until all gems finish installing. - - *Max Holder* - -* Respect `pluralize_table_names` when generating fixture file. - - Fixes #19519. - - *Yuji Yaginuma* - -* Add a new-line to the end of route method generated code. - - We need to add a `\n`, because we cannot have two routes - in the same line. - - *arthurnn* - -* Add `rake initializers`. - - This task prints out all defined initializers in the order they are invoked - by Rails. This is helpful for debugging issues related to the initialization - process. - - *Naoto Kaneko* - -* Created rake restart task. Restarts your Rails app by touching the - `tmp/restart.txt`. - - See #18876. - - *Hyonjee Joo* - -* Add `config/initializers/active_record_belongs_to_required_by_default.rb`. - - Newly generated Rails apps have a new initializer called - `active_record_belongs_to_required_by_default.rb` which sets the value of - the configuration option `config.active_record.belongs_to_required_by_default` - to `true` when ActiveRecord is not skipped. - - As a result, new Rails apps require `belongs_to` association on model - to be valid. - - This initializer is *not* added when running `rake rails:update`, so - old apps ported to Rails 5 will work without any change. - - *Josef Šimánek* - -* `delete` operations in configurations are run last in order to eliminate - 'No such middleware' errors when `insert_before` or `insert_after` are added - after the `delete` operation for the middleware being deleted. - - Fixes #16433. - - *Guo Xiang Tan* - -* Newly generated applications get a `README.md` in Markdown. - - *Xavier Noria* - -* Remove the documentation tasks `doc:app`, `doc:rails`, and `doc:guides`. - - *Xavier Noria* - -* Force generated routes to be inserted into `config/routes.rb`. - - *Andrew White* - -* Don't remove all line endings from `config/routes.rb` when revoking scaffold. - - Fixes #15913. - - *Andrew White* - -* Rename `--skip-test-unit` option to `--skip-test` in app generator - - *Melanie Gilman* - -* Add the `method_source` gem to the default Gemfile for apps. - - *Sean Griffin* - -* Drop old test locations from `rake stats`: - - - test/functional - - test/unit - - *Ravil Bayramgalin* - -* Update `rake stats` to correctly count declarative tests - as methods in `_test.rb` files. - - *Ravil Bayramgalin* - -* Remove deprecated `test:all` and `test:all:db` tasks. - - *Rafael Mendonça França* - -* 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 - `ActiveSupport.halt_callback_chains_on_return_false` to `false`. - - As a result, new Rails apps do not halt Active Record and Active Model - 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/RDOC_MAIN.rdoc b/railties/RDOC_MAIN.rdoc index ce024563c4..ef9bbf3d7e 100644 --- a/railties/RDOC_MAIN.rdoc +++ b/railties/RDOC_MAIN.rdoc @@ -35,23 +35,23 @@ can read more about Action Pack in its {README}[link:files/actionpack/README_rdo 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 <tt>--help</tt> or <tt>-h</tt> for options. 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: diff --git a/railties/Rakefile b/railties/Rakefile index cf130a5f14..db23bbabaa 100644 --- a/railties/Rakefile +++ b/railties/Rakefile @@ -2,6 +2,8 @@ require 'rake/testtask' task :default => :test +task :package + desc "Run all unit tests" task :test => 'test:isolated' diff --git a/railties/lib/rails/all.rb b/railties/lib/rails/all.rb index 11f4d5c4bc..1a7f7855f1 100644 --- a/railties/lib/rails/all.rb +++ b/railties/lib/rails/all.rb @@ -1,4 +1,4 @@ -require "rails" +require 'rails' %w( active_record/railtie @@ -11,7 +11,7 @@ require "rails" sprockets/railtie ).each do |railtie| begin - require "#{railtie}" + require railtie rescue LoadError end end diff --git a/railties/lib/rails/app_loader.rb b/railties/lib/rails/app_loader.rb index a9fe21824e..af004d85bf 100644 --- a/railties/lib/rails/app_loader.rb +++ b/railties/lib/rails/app_loader.rb @@ -16,7 +16,7 @@ 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. diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index cac31e1eed..c383de3e06 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -1,4 +1,3 @@ -require 'fileutils' require 'yaml' require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/object/blank' @@ -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 @@ -214,7 +216,7 @@ 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 @@ -243,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, @@ -259,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 @@ -383,11 +385,16 @@ module Rails def secrets @secrets ||= begin secrets = ActiveSupport::OrderedOptions.new - yaml = config.paths["config/secrets"].first + yaml = config.paths["config/secrets"].first + if File.exist?(yaml) require "erb" - all_secrets = YAML.load(ERB.new(IO.read(yaml)).result) || {} - env_secrets = all_secrets[Rails.env] + + all_secrets = YAML.load(ERB.new(IO.read(yaml)).result) || {} + shared_secrets = all_secrets['shared'] + env_secrets = all_secrets[Rails.env] + + secrets.merge!(shared_secrets.symbolize_keys) if shared_secrets secrets.merge!(env_secrets.symbolize_keys) if env_secrets end diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb index 9baf8aa742..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 diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 65cff1561a..f415a20833 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -58,7 +58,7 @@ module Rails def static_cache_control=(value) ActiveSupport::Deprecation.warn <<-eow.strip_heredoc - `static_cache_control` is deprecated and will be removed in Rails 5.1. + `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. @@ -69,8 +69,8 @@ module Rails def serve_static_files ActiveSupport::Deprecation.warn <<-eow.strip_heredoc - `serve_static_files` is deprecated and will be removed in Rails 5.1. - Please use `public_file_server.enabled` instead. + `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 @@ -78,8 +78,8 @@ module Rails def serve_static_files=(value) ActiveSupport::Deprecation.warn <<-eow.strip_heredoc - `serve_static_files` is deprecated and will be removed in Rails 5.1. - Please use `public_file_server.enabled = #{value}` instead. + `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 diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb index 4f1cc0703d..381e548730 100644 --- a/railties/lib/rails/application/default_middleware_stack.rb +++ b/railties/lib/rails/application/default_middleware_stack.rb @@ -34,22 +34,10 @@ module Rails # handling: presumably their code is not threadsafe middleware.use ::Rack::Lock - - elsif config.allow_concurrency == :unsafe - # Do nothing, even if we know this is dangerous. This is the - # historical behaviour for true. - - else - # Default concurrency setting: enabled, but safe - - unless config.cache_classes && config.eager_load - # Without cache_classes + eager_load, the load interlock - # is required for proper operation - - middleware.use ::ActionDispatch::LoadInterlock - end end + middleware.use ::ActionDispatch::Executor, app.executor + middleware.use ::Rack::Runtime middleware.use ::Rack::MethodOverride unless config.api_only middleware.use ::ActionDispatch::RequestId @@ -61,7 +49,7 @@ module Rails 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 @@ -83,10 +71,6 @@ module Rails private - def reload_dependencies? - config.reload_classes_only_on_change != true || app.reloaders.map(&:updated?).any? - 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 404e3c3e23..4e24640510 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 behavior 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,27 @@ 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::Dependencies.interlock.attempt_unloading do - ActiveSupport::DescendantsTracker.clear - ActiveSupport::Dependencies.clear + 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 @@ -99,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/code_statistics.rb b/railties/lib/rails/code_statistics.rb index 0997414482..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 @@ -67,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/command.rb b/railties/lib/rails/command.rb deleted file mode 100644 index f7753cbb83..0000000000 --- a/railties/lib/rails/command.rb +++ /dev/null @@ -1,70 +0,0 @@ -require 'rails/commands/commands_tasks' - -module Rails - class Command #:nodoc: - attr_reader :argv - - def initialize(argv = []) - @argv = argv - - @option_parser = build_option_parser - @options = {} - end - - def self.run(task_name, argv) - command_name = command_name_for(task_name) - - if command = command_for(command_name) - command.new(argv).run(command_name) - else - Rails::CommandsTasks.new(argv).run_command!(task_name) - end - end - - def run(command_name) - parse_options_for(command_name) - @option_parser.parse! @argv - - public_send(command_name) - end - - def self.options_for(command_name, &options_to_parse) - @@command_options[command_name] = options_to_parse - end - - def self.set_banner(command_name, banner) - options_for(command_name) { |opts, _| opts.banner = banner } - end - - private - @@commands = [] - @@command_options = {} - - def parse_options_for(command_name) - @@command_options.fetch(command_name, proc {}).call(@option_parser, @options) - end - - def build_option_parser - OptionParser.new do |opts| - opts.on('-h', '--help', 'Show this help.') do - puts opts - exit - end - end - end - - def self.inherited(command) - @@commands << command - end - - def self.command_name_for(task_name) - task_name.gsub(':', '_').to_sym - end - - def self.command_for(command_name) - @@commands.find do |command| - command.public_instance_methods.include?(command_name) - end - end - end -end diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb index 7627fcf5a0..5a66b78a92 100644 --- a/railties/lib/rails/commands.rb +++ b/railties/lib/rails/commands.rb @@ -13,7 +13,6 @@ aliases = { command = ARGV.shift command = aliases[command] || command -require 'rails/command' -require 'rails/commands/dev_cache' +require 'rails/commands/commands_tasks' -Rails::Command.run(command, ARGV) +Rails::CommandsTasks.new(ARGV).run_command!(command) diff --git a/railties/lib/rails/commands/dev_cache.rb b/railties/lib/rails/commands/dev_cache.rb deleted file mode 100644 index ec96e8f630..0000000000 --- a/railties/lib/rails/commands/dev_cache.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'rails/command' - -module Rails - module Commands - # This is a wrapper around the Rails dev:cache command - class DevCache < Command - set_banner :dev_cache, 'Toggle development mode caching on/off' - def dev_cache - if File.exist? 'tmp/caching-dev.txt' - File.delete 'tmp/caching-dev.txt' - puts 'Development mode is no longer being cached.' - else - FileUtils.touch 'tmp/caching-dev.txt' - puts 'Development mode is now being cached.' - end - - FileUtils.touch 'tmp/restart.txt' - end - end - end -end diff --git a/railties/lib/rails/commands/runner.rb b/railties/lib/rails/commands/runner.rb index babb197ba1..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,17 +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 begin - eval(code_or_file, binding, __FILE__, __LINE__) - rescue SyntaxError,NameError => err + 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 '#{$0} -h' for help." + $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 27cbaf360a..7418dff18b 100644 --- a/railties/lib/rails/commands/server.rb +++ b/railties/lib/rails/commands/server.rb @@ -2,6 +2,7 @@ require 'fileutils' require 'optparse' require 'action_dispatch' require 'rails' +require 'rails/dev_caching' module Rails class Server < ::Rack::Server @@ -92,20 +93,17 @@ module Rails DoNotReverseLookup: true, environment: (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development").dup, daemonize: false, - caching: false, - pid: Options::DEFAULT_PID_PATH + caching: nil, + pid: Options::DEFAULT_PID_PATH, + restart_cmd: restart_command }) end private def setup_dev_caching - return unless options[:environment] == "development" - - if options[:caching] == false - delete_cache_file - elsif options[:caching] - create_cache_file + if options[:environment] == "development" + Rails::DevCaching.enable_by_argument(options[:caching]) end end @@ -114,16 +112,6 @@ module Rails 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_cache_file - FileUtils.touch("tmp/caching-dev.txt") - end - - def delete_cache_file - FileUtils.rm("tmp/caching-dev.txt") if File.exist?("tmp/caching-dev.txt") end def create_tmp_directories @@ -143,5 +131,9 @@ module Rails 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/console/app.rb b/railties/lib/rails/console/app.rb index ac5836a588..9ad77e0a80 100644 --- a/railties/lib/rails/console/app.rb +++ b/railties/lib/rails/console/app.rb @@ -29,8 +29,7 @@ module Rails # 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/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 5757d235d2..9701409755 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -219,7 +219,7 @@ module Rails # The next thing that changes in isolated engines is the behavior of routes. # Normally, when you namespace your controllers, you also need to namespace # the related routes. With an isolated engine, the engine's namespace is - # automatically applied, so you don't need to specify it explicity in your + # automatically applied, so you don't need to specify it explicitly in your # routes: # # MyEngine::Engine.routes.draw do diff --git a/railties/lib/rails/engine/commands.rb b/railties/lib/rails/engine/commands.rb index a6d87b78e4..7bbd9ef744 100644 --- a/railties/lib/rails/engine/commands.rb +++ b/railties/lib/rails/engine/commands.rb @@ -1,3 +1,5 @@ +require 'rails/engine/commands_tasks' + ARGV << '--help' if ARGV.empty? aliases = { @@ -9,35 +11,4 @@ aliases = { command = ARGV.shift command = aliases[command] || command -require ENGINE_PATH -engine = ::Rails::Engine.find(ENGINE_ROOT) - -case command -when 'generate', 'destroy', 'test' - 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") - 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). - 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 8cadbc3ddd..294d07446f 100644 --- a/railties/lib/rails/engine/configuration.rb +++ b/railties/lib/rails/engine/configuration.rb @@ -39,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 f4d5b660dc..9c49e0655a 100644 --- a/railties/lib/rails/gem_version.rb +++ b/railties/lib/rails/gem_version.rb @@ -6,9 +6,9 @@ module Rails module VERSION MAJOR = 5 - MINOR = 0 + MINOR = 1 TINY = 0 - PRE = "beta1.1" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index e3d79521e7..330bd7ec5d 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -105,7 +105,7 @@ module Rails # Configure generators for API only applications. It basically hides # everything that is usually browser related, such as assets and session - # migration generators, and completely disable views, helpers and assets + # 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" @@ -116,6 +116,10 @@ module Rails helper: false, template_engine: nil ) + + if ARGV.first == 'mailer' + options[:rails].merge!(template_engine: :erb) + end end # Remove the color from output. diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb index 9ca731347a..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 @@ -207,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 @@ -270,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 d664b07652..6c5b55466d 100644 --- a/railties/lib/rails/generators/actions/create_migration.rb +++ b/railties/lib/rails/generators/actions/create_migration.rb @@ -1,3 +1,4 @@ +require 'fileutils' require 'thor/actions' module Rails diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index c629459d95..f0a3289563 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) @@ -51,6 +52,9 @@ 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' @@ -60,6 +64,9 @@ module Rails class_option :skip_spring, type: :boolean, default: false, desc: "Don't install Spring application preloader" + 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' @@ -113,6 +120,7 @@ module Rails def gemfile_entries [rails_gemfile_entry, database_gemfile_entry, + webserver_gemfile_entry, assets_gemfile_entry, javascript_gemfile_entry, jbuilder_gemfile_entry, @@ -171,6 +179,12 @@ module Rails "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, :skip_action_cable).none? end @@ -282,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', @@ -294,20 +308,15 @@ module Rails def jbuilder_gemfile_entry comment = 'Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder' - GemfileEntry.new 'jbuilder', '~> 2.0', comment, {}, options[:api] + GemfileEntry.new 'jbuilder', '~> 2.5', 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] @@ -315,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 @@ -342,10 +351,9 @@ module Rails def cable_gemfile_entry return [] if options[:skip_action_cable] - comment = 'Action Cable dependencies for the Redis adapter' + comment = 'Use Redis adapter to run Action Cable in production' gems = [] - gems << GemfileEntry.new("em-hiredis", '~> 0.3.0', comment) - gems << GemfileEntry.new("redis", '~> 3.0', comment) + gems << GemfileEntry.new("redis", '~> 3.0', comment, {}, true) gems end @@ -381,6 +389,14 @@ module Rails !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 bundle_command('install') if bundle_install? end diff --git a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb index bc249aa5e5..97f3657070 100644 --- a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb +++ b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb @@ -9,6 +9,13 @@ 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', class_path, filename_with_extensions('mailer', format)) + template filename_with_extensions(:layout, format), layout_path + end + end + actions.each do |action| @action = action @@ -26,7 +33,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.tt b/railties/lib/rails/generators/erb/mailer/templates/layout.html.erb.tt new file mode 100644 index 0000000000..55f3675d49 --- /dev/null +++ b/railties/lib/rails/generators/erb/mailer/templates/layout.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.tt b/railties/lib/rails/generators/erb/mailer/templates/layout.text.erb.tt new file mode 100644 index 0000000000..6363733e6e --- /dev/null +++ b/railties/lib/rails/generators/erb/mailer/templates/layout.text.erb.tt @@ -0,0 +1 @@ +<%%= yield %> diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb index efbf51ddfb..ee076eb711 100644 --- a/railties/lib/rails/generators/named_base.rb +++ b/railties/lib/rails/generators/named_base.rb @@ -26,6 +26,10 @@ module Rails super end end + + def js_template(source, destination) + template(source + '.js', destination + '.js') + end end protected diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index a4758857f2..448dce06af 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -79,6 +79,8 @@ module Rails 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" @@ -88,21 +90,23 @@ module Rails def config_when_updating cookie_serializer_config_exist = File.exist?('config/initializers/cookies_serializer.rb') - callback_terminator_config_exist = File.exist?('config/initializers/callback_terminator.rb') - active_record_belongs_to_required_by_default_config_exist = File.exist?('config/initializers/active_record_belongs_to_required_by_default.rb') + action_cable_config_exist = File.exist?('config/cable.yml') + rack_cors_config_exist = File.exist?('config/initializers/cors.rb') config - unless callback_terminator_config_exist - remove_file 'config/initializers/callback_terminator.rb' - end + gsub_file 'config/environments/development.rb', /^(\s+)config\.file_watcher/, '\1# config.file_watcher' unless cookie_serializer_config_exist - gsub_file 'config/initializers/cookies_serializer.rb', /json/, 'marshal' + gsub_file 'config/initializers/cookies_serializer.rb', /json(?!,)/, 'marshal' + end + + unless action_cable_config_exist + template 'config/cable.yml' end - unless active_record_belongs_to_required_by_default_config_exist - remove_file 'config/initializers/active_record_belongs_to_required_by_default.rb' + unless rack_cors_config_exist + remove_file 'config/initializers/cors.rb' end end @@ -275,9 +279,20 @@ module Rails end end - def delete_app_views_if_api_option + def delete_application_layout_file_if_api_option if options[:api] - remove_dir 'app/views' + remove_file 'app/views/layouts/application.html.erb' + end + end + + def delete_public_files_if_api_option + if options[:api] + remove_file 'public/404.html' + remove_file 'public/422.html' + remove_file 'public/500.html' + remove_file 'public/apple-touch-icon-precomposed.png' + remove_file 'public/apple-touch-icon.png' + remove_file 'public/favicon.ico' end end @@ -307,18 +322,11 @@ module Rails end end - def delete_active_record_initializers_skipping_active_record - if options[:skip_active_record] - remove_file 'config/initializers/active_record_belongs_to_required_by_default.rb' - end - end - def delete_action_cable_files_skipping_action_cable if options[:skip_action_cable] remove_file 'config/cable.yml' - remove_file 'app/assets/javascripts/cable.coffee' + remove_file 'app/assets/javascripts/cable.js' remove_dir 'app/channels' - gsub_file 'app/views/layouts/application.html.erb', /action_cable_meta_tag/, '' unless options[:api] end end @@ -326,8 +334,6 @@ module Rails 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 diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index da5af4eefc..86143ca1f1 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -12,9 +12,6 @@ source 'https://rubygems.org' <% end -%> <% end -%> -# Use Puma as the app server -gem 'puma' - # Use ActiveModel has_secure_password # gem 'bcrypt', '~> 3.1.7' @@ -29,21 +26,27 @@ gem 'puma' <% 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 group :development do <%- unless options.api? -%> - # Access an IRB console on exception pages or by using <%%= console %> in views + # 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' <%- else -%> - gem 'web-console', '~> 3.0' + gem 'web-console' <%- end -%> <%- 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 <% 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/javascripts/cable.coffee b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.coffee deleted file mode 100644 index 07934d026f..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.coffee +++ /dev/null @@ -1,11 +0,0 @@ -# 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. -# -# Turn on the cable connection by removing the comments after the require statements (and ensure it's also on in config/routes.rb). -# -#= require action_cable -#= require_self -#= require_tree ./channels -# -# @App ||= {} -# App.cable = ActionCable.createConsumer() 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/channels/application_cable/channel.rb b/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/channel.rb index d56fa30f4d..d672697283 100644 --- 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 @@ -1,4 +1,3 @@ -# 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 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 index b4f41389ad..0ff5442f47 100644 --- 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 @@ -1,4 +1,3 @@ -# 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 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 f726fd6305..413354186d 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,7 +1,5 @@ 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/views/layouts/application.html.erb.tt b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt index 68b5c051b2..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 @@ -3,16 +3,13 @@ <head> <title><%= camelized %></title> <%%= csrf_meta_tags %> - <%- unless options[:skip_action_cable] -%> - <%%= action_cable_meta_tag %> - <%- end -%> <%- 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 %> + <%%= 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' %> 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 df88bfd3bc..acae810c1a 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/setup +++ b/railties/lib/rails/generators/rails/app/templates/bin/setup @@ -15,7 +15,7 @@ chdir APP_ROOT do puts '== Installing dependencies ==' system! 'gem install bundler --conservative' - system('bundle check') or system!('bundle install') + system('bundle check') || system!('bundle install') # puts "\n== Copying sample files ==" # unless File.exist?('config/database.yml') diff --git a/railties/lib/rails/generators/rails/app/templates/bin/update b/railties/lib/rails/generators/rails/app/templates/bin/update index c6ed3ae64b..770a605fed 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/update +++ b/railties/lib/rails/generators/rails/app/templates/bin/update @@ -15,7 +15,7 @@ chdir APP_ROOT do puts '== Installing dependencies ==' system! 'gem install bundler --conservative' - system 'bundle check' or system! 'bundle install' + system('bundle check') || system!('bundle install') puts "\n== Updating database ==" system! 'bin/rails db:migrate' diff --git a/railties/lib/rails/generators/rails/app/templates/config.ru b/railties/lib/rails/generators/rails/app/templates/config.ru new file mode 100644 index 0000000000..f7ba0b527b --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config.ru @@ -0,0 +1,5 @@ +# This file is used by Rack-based servers to start the application. + +require_relative 'config/environment' + +run Rails.application diff --git a/railties/lib/rails/generators/rails/app/templates/config.ru.tt b/railties/lib/rails/generators/rails/app/templates/config.ru.tt deleted file mode 100644 index 70556fcc99..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/config.ru.tt +++ /dev/null @@ -1,11 +0,0 @@ -# This file is used by Rack-based servers to start the application. - -require ::File.expand_path('../config/environment', __FILE__) -<%- unless options[:skip_action_cable] -%> - -# Action Cable uses EventMachine which requires that all classes are loaded in advance -Rails.application.eager_load! -require 'action_cable/process/logging' -<%- end -%> - -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 cb83364360..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' 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 index 004adb7b3c..0bbde6f74f 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/cable.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/cable.yml @@ -1,12 +1,9 @@ -# Action Cable uses Redis by default to administer connections, channels, and sending/receiving messages over the WebSocket. -production: - adapter: redis - url: redis://localhost:6379/1 - development: - adapter: redis - url: redis://localhost:6379/2 + adapter: async test: + adapter: async + +production: adapter: redis - url: redis://localhost:6379/3 + url: redis://localhost:6379/1 diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml index 34fc0e3465..917b52e535 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml @@ -8,6 +8,7 @@ # default: &default adapter: frontbase + pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> host: localhost username: <%= app_name %> password: '' diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml index 187ff01bac..d40117a27f 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml @@ -34,6 +34,7 @@ # default: &default adapter: ibm_db + pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: db2inst1 password: #schema: db2inst1 diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml index db0a429753..563be77710 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml @@ -38,6 +38,7 @@ default: &default adapter: jdbc + pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: <%= app_name %> password: driver: 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 5ca549a8c8..a2b2a64ba6 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 @@ -11,6 +11,7 @@ # default: &default adapter: mysql + pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: host: localhost 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..70df04079d 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' @@ -6,6 +6,7 @@ default: &default adapter: postgresql encoding: unicode + pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> development: <<: *default diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml index 28c36eb82f..371415e6a8 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml @@ -6,6 +6,7 @@ # default: &default adapter: sqlite3 + pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> development: <<: *default 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 119c2fe2c3..d987cf303b 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 @@ -12,7 +12,7 @@ default: &default adapter: mysql2 encoding: utf8 - pool: 5 + pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: <% if mysql_socket -%> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml index 9aedcc15cb..d2499ea4fb 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml @@ -18,6 +18,7 @@ # default: &default adapter: oracle + pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: <%= app_name %> password: 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/databases/sqlite3.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml index 1c1a37ca8d..9510568124 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml @@ -6,7 +6,7 @@ # default: &default adapter: sqlite3 - pool: 5 + pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> timeout: 5000 development: diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml index 30b0df34a8..c223d6bc62 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml @@ -25,6 +25,7 @@ default: &default adapter: sqlserver encoding: utf8 + pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> reconnect: false username: <%= app_name %> password: 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 fd41372d9c..f3ccf95045 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 @@ -15,18 +15,22 @@ Rails.application.configure do # 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. @@ -43,14 +47,8 @@ Rails.application.configure do # 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 + # Suppress logger output for asset requests. + config.assets.quiet = true <%- end -%> # Raises error for missing translations @@ -58,5 +56,5 @@ Rails.application.configure do # Use an evented file watcher to asynchronously detect changes in source code, # routes, locales, etc. This feature depends on the listen gem. - # config.file_watcher = ActiveSupport::EventedFileUpdateChecker + <%= '# ' 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 82509f5ef5..363af05459 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 @@ -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 -%> @@ -41,7 +37,8 @@ Rails.application.configure do # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX <%- unless options[:skip_action_cable] -%> - # Action Cable endpoint configuration + # Mount Action Cable outside main process or domain + # config.action_cable.mount_path = nil # config.action_cable.url = 'wss://example.com/cable' # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] <%- end -%> @@ -56,10 +53,6 @@ Rails.application.configure do # Prepend all log lines with the following tags. config.log_tags = [ :request_id ] - # Use a different logger for distributed setups. - # require 'syslog/logger' - # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') - # Use a different cache store in production. # config.cache_store = :mem_cache_store @@ -67,6 +60,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. @@ -82,6 +76,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 8133917591..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 @@ -28,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 @@ -35,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 deleted file mode 100644 index f613b40f80..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/active_record_belongs_to_required_by_default.rb +++ /dev/null @@ -1,6 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# 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/callback_terminator.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb deleted file mode 100644 index 649e82280e..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb +++ /dev/null @@ -1,6 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# 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/new_framework_defaults.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults.rb.tt new file mode 100644 index 0000000000..991963b65e --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults.rb.tt @@ -0,0 +1,34 @@ +# Be sure to restart your server when you modify this file. +# +# This file contains migration options to ease your Rails 5.0 upgrade. +# +<%- if options[:update] -%> +# Once upgraded flip defaults one by one to migrate to the new default. +# +<%- end -%> +# Read the Rails 5.0 release notes for more info on each option. +<%- unless options[:api] -%> + +# Enable per-form CSRF tokens. Previous versions had false. +Rails.application.config.action_controller.per_form_csrf_tokens = <%= options[:update] ? false : true %> + +# Enable origin-checking CSRF mitigation. Previous versions had false. +Rails.application.config.action_controller.forgery_protection_origin_check = <%= options[:update] ? false : true %> +<%- end -%> + +# Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. +# Previous versions had false. +ActiveSupport.to_time_preserves_timezone = <%= options[:update] ? false : true %> +<%- unless options[:skip_active_record] -%> + +# Require `belongs_to` associations by default. Previous versions had false. +Rails.application.config.active_record.belongs_to_required_by_default = <%= options[:update] ? false : true %> +<%- end -%> + +# Do not halt callback chains when a callback returns false. Previous versions had true. +ActiveSupport.halt_callback_chains_on_return_false = <%= options[:update] ? true : false %> +<%- unless options[:update] -%> + +# Configure SSL options to enable HSTS with subdomains. Previous versions had false. +Rails.application.config.ssl_options = { hsts: { subdomains: true } } +<%- end -%> 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 deleted file mode 100644 index 1f569dedfd..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/per_form_csrf_tokens.rb +++ /dev/null @@ -1,4 +0,0 @@ -# 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 deleted file mode 100644 index 3eab78a885..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/request_forgery_protection.rb +++ /dev/null @@ -1,4 +0,0 @@ -# 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/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 8293c8a483..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,6 +1,3 @@ Rails.application.routes.draw do # 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 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 cdea2fd060..8e995a5df1 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/secrets.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/secrets.yml @@ -10,6 +10,13 @@ # Make sure the secrets in this file are kept private # if you're sharing your code publicly. +# Shared secrets are available across all environments. + +shared: + api_key: 123 + +# Environmental secrets are only available for that specific environment. + development: secret_key_base: <%= app_secret %> @@ -18,5 +25,6 @@ test: # Do not keep production secrets in the repository, # instead read values from the environment. + production: secret_key_base: <%%= ENV["SECRET_KEY_BASE"] %> 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/public/apple-touch-icon-precomposed.png b/railties/lib/rails/generators/rails/app/templates/public/apple-touch-icon-precomposed.png new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ 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 0a4c509a31..6c583e5811 100644 --- a/railties/lib/rails/generators/rails/controller/controller_generator.rb +++ b/railties/lib/rails/generators/rails/controller/controller_generator.rb @@ -19,8 +19,7 @@ module Rails end end - hook_for :template_engine, :test_framework - hook_for :helper, :assets, hide: true + hook_for :template_engine, :test_framework, :helper, :assets private diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb index 0c7a73a54e..56efd35a95 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -90,6 +90,7 @@ task default: :test 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 @@ -259,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 '-' @@ -281,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 diff --git a/railties/lib/rails/generators/rails/plugin/templates/Rakefile b/railties/lib/rails/generators/rails/plugin/templates/Rakefile index f1943644e4..383d2fb2d1 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/Rakefile +++ b/railties/lib/rails/generators/rails/plugin/templates/Rakefile @@ -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 7fe4e5034d..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::#{api? ? "API" : "Base"} + #{ api? ? '# ' : '' }protect_from_forgery with: :exception 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/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/scaffold/templates/scaffold.css b/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css index 79f8b7f96f..cd4f3de38d 100644 --- a/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css +++ b/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css @@ -1,13 +1,13 @@ body { background-color: #fff; color: #333; + margin: 33px; } body, p, ol, ul, td { font-family: verdana, arial, helvetica, sans-serif; font-size: 13px; line-height: 18px; - margin: 33px; } pre { @@ -34,9 +34,7 @@ th { } td { - padding-bottom: 7px; - padding-left: 5px; - padding-right: 5px; + padding: 0 5px 7px; } div.field, @@ -57,8 +55,7 @@ div.actions { #error_explanation { width: 450px; border: 2px solid red; - padding: 7px; - padding-bottom: 0; + padding: 7px 7px 0; margin-bottom: 20px; background-color: #f0f0f0; } @@ -68,8 +65,7 @@ div.actions { font-weight: bold; padding: 5px 5px 5px 15px; font-size: 12px; - margin: -7px; - margin-bottom: 0; + margin: -7px -7px 0; background-color: #c00; color: #fff; } 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/templates/fixtures.yml b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml index 2656767eb4..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 -%> 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 index 0d18478043..c469c188e6 100644 --- 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 @@ -11,31 +11,31 @@ class <%= controller_class_name %>ControllerTest < ActionDispatch::IntegrationTe end test "should get index" do - get <%= index_helper %>_url + get <%= index_helper %>_url, as: :json 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} }" %> } + post <%= index_helper %>_url, params: { <%= "#{singular_table_name}: { #{attributes_hash} }" %> }, as: :json end assert_response 201 end test "should show <%= singular_table_name %>" do - get <%= show_helper %> + get <%= show_helper %>, as: :json assert_response :success end test "should update <%= singular_table_name %>" do - patch <%= show_helper %>, params: { <%= "#{singular_table_name}: { #{attributes_hash} }" %> } + patch <%= show_helper %>, params: { <%= "#{singular_table_name}: { #{attributes_hash} }" %> }, as: :json assert_response 200 end test "should destroy <%= singular_table_name %>" do assert_difference('<%= class_name %>.count', -1) do - delete <%= show_helper %> + delete <%= show_helper %>, as: :json end assert_response 204 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 0e6bef12fc..c33375b7b4 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 @@ -25,7 +25,7 @@ class <%= controller_class_name %>ControllerTest < ActionDispatch::IntegrationTe post <%= index_helper %>_url, params: { <%= "#{singular_table_name}: { #{attributes_hash} }" %> } end - assert_redirected_to <%= singular_table_name %>_path(<%= class_name %>.last) + assert_redirected_to <%= singular_table_name %>_url(<%= class_name %>.last) end test "should show <%= singular_table_name %>" do @@ -40,7 +40,7 @@ class <%= controller_class_name %>ControllerTest < ActionDispatch::IntegrationTe test "should update <%= singular_table_name %>" do patch <%= show_helper %>, params: { <%= "#{singular_table_name}: { #{attributes_hash} }" %> } - assert_redirected_to <%= singular_table_name %>_path(<%= "@#{singular_table_name}" %>) + assert_redirected_to <%= singular_table_name %>_url(<%= "@#{singular_table_name}" %>) end test "should destroy <%= singular_table_name %>" do @@ -48,7 +48,7 @@ class <%= controller_class_name %>ControllerTest < ActionDispatch::IntegrationTe delete <%= show_helper %> end - assert_redirected_to <%= index_helper %>_path + assert_redirected_to <%= index_helper %>_url end end <% end -%> diff --git a/railties/lib/rails/rack/logger.rb b/railties/lib/rails/rack/logger.rb index 12676b18bc..b63d3a58d2 100644 --- a/railties/lib/rails/rack/logger.rb +++ b/railties/lib/rails/rack/logger.rb @@ -30,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) } @@ -78,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/tasks.rb b/railties/lib/rails/tasks.rb index d60eaf6f4f..d3e33584d7 100644 --- a/railties/lib/rails/tasks.rb +++ b/railties/lib/rails/tasks.rb @@ -3,6 +3,7 @@ require 'rake' # Load Rails Rakefile extensions %w( annotations + dev framework initializers log 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 c51524f8f6..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 diff --git a/railties/lib/rails/tasks/framework.rake b/railties/lib/rails/tasks/framework.rake index 904b9d9ad6..51d9daaaa9 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,12 +26,12 @@ 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 @@ -45,8 +47,9 @@ namespace :rails do @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 + gen = Rails::Generators::AppGenerator.new ["rails"], + { api: !!Rails.application.config.api_only, update: true }, + destination_root: Rails.root File.exist?(Rails.root.join("config", "application.rb")) ? gen.send(:app_const) : gen.send(:valid_const?) gen @@ -66,3 +69,15 @@ namespace :rails do end end end + +namespace :rails do + %i(update template templates:copy update:configs update:bin).each do |task_name| + task "#{task_name}" do + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Running #{task_name} with the rails: namespace is deprecated in favor of app: namespace. + Run bin/rails app:#{task_name} instead. + MSG + Rake.application.invoke_task("app:#{task_name}") + end + end +end diff --git a/railties/lib/rails/tasks/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 f36c86d81b..3f98cbe51f 100644 --- a/railties/lib/rails/tasks/restart.rake +++ b/railties/lib/rails/tasks/restart.rake @@ -1,5 +1,8 @@ -desc "Restart app by touching tmp/restart.txt" +desc 'Restart app by touching tmp/restart.txt' task :restart do - FileUtils.mkdir_p('tmp') - 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 a919d36939..3e40d3b037 100644 --- a/railties/lib/rails/tasks/statistics.rake +++ b/railties/lib/rails/tasks/statistics.rake @@ -7,6 +7,7 @@ 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), 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/welcome/index.html.erb b/railties/lib/rails/templates/rails/welcome/index.html.erb index acf04af416..5cdb7e6a20 100644 --- a/railties/lib/rails/templates/rails/welcome/index.html.erb +++ b/railties/lib/rails/templates/rails/welcome/index.html.erb @@ -1,268 +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,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); - 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,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" /> + + <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_unit/line_filtering.rb b/railties/lib/rails/test_unit/line_filtering.rb index dab4d3631d..dd9732bb12 100644 --- a/railties/lib/rails/test_unit/line_filtering.rb +++ b/railties/lib/rails/test_unit/line_filtering.rb @@ -13,9 +13,12 @@ module Rails end class CompositeFilter # :nodoc: + attr_reader :named_filter + def initialize(runnable, filter, patterns) @runnable = runnable - @filters = [ derive_regexp(filter), *derive_line_filters(patterns) ].compact + @named_filter = derive_named_filter(filter) + @filters = [ @named_filter, *derive_line_filters(patterns) ].compact end # Minitest uses === to find matching filters. @@ -24,9 +27,14 @@ module Rails end private - def derive_regexp(filter) - # Regexp filtering copied from Minitest. - filter =~ %r%/(.*)/% ? Regexp.new($1) : filter + 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) diff --git a/railties/lib/rails/test_unit/minitest_plugin.rb b/railties/lib/rails/test_unit/minitest_plugin.rb index 29a3d991b8..076ab536be 100644 --- a/railties/lib/rails/test_unit/minitest_plugin.rb +++ b/railties/lib/rails/test_unit/minitest_plugin.rb @@ -1,6 +1,7 @@ require "active_support/core_ext/module/attribute_accessors" require "rails/test_unit/reporter" require "rails/test_unit/test_requirer" +require 'shellwords' module Minitest class SuppressedSummaryReporter < SummaryReporter @@ -42,7 +43,7 @@ module Minitest end opts.on("-f", "--fail-fast", - "Abort test run on first failure") do + "Abort test run on first failure or error") do options[:fail_fast] = true end @@ -53,27 +54,26 @@ module Minitest options[:color] = true options[:output_inline] = true - options[:patterns] = opts.order! + 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 + 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" - unless run_with_autorun - patterns = defined?(@rake_patterns) ? @rake_patterns : options[:patterns] - ::Rails::TestRequirer.require_files(patterns) - end + ::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. @@ -81,14 +81,18 @@ module Minitest end # Replace progress reporter for colors. - self.reporter.reporters.delete_if { |reporter| reporter.kind_of?(SummaryReporter) || reporter.kind_of?(ProgressReporter) } - self.reporter << SuppressedSummaryReporter.new(options[:io], options) - self.reporter << ::Rails::TestUnitReporter.new(options[:io], options) + 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 +# 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 << 'rails' +Minitest.extensions.unshift 'rails' diff --git a/railties/lib/rails/test_unit/reporter.rb b/railties/lib/rails/test_unit/reporter.rb index 73b8d7d27b..4086d5b731 100644 --- a/railties/lib/rails/test_unit/reporter.rb +++ b/railties/lib/rails/test_unit/reporter.rb @@ -18,13 +18,13 @@ module Rails if output_inline? && result.failure && (!result.skipped? || options[:verbose]) io.puts io.puts - io.puts format_failures(result).map { |line| color_output(line, by: result) } + io.puts color_output(result, by: result) io.puts io.puts format_rerun_snippet(result) io.puts end - if fail_fast? && result.failure && !result.error? && !result.skipped? + if fail_fast? && result.failure && !result.skipped? raise Interrupt end end @@ -66,21 +66,9 @@ module Rails "%s#%s = %.2f s = %s" % [result.class, result.name, result.time, result.result_code] end - def format_failures(result) - result.failures.map do |failure| - "#{failure.result_label}:\n#{result.class}##{result.name}:\n#{failure.message}\n" - end - end - def format_rerun_snippet(result) - # Try to extract path to assertion from backtrace. - if result.location =~ /\[(.*)\]\z/ - assertion_path = $1 - else - assertion_path = result.method(result.name).source_location.join(':') - end - - "#{self.executable} #{relative_path_for(assertion_path)}" + location, line = result.method(result.name).source_location + "#{self.executable} #{relative_path_for(location)}:#{line}" end def app_root diff --git a/railties/railties.gemspec b/railties/railties.gemspec index a06336f698..fa417816eb 100644 --- a/railties/railties.gemspec +++ b/railties/railties.gemspec @@ -13,7 +13,7 @@ Gem::Specification.new do |s| 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', 'RDOC_MAIN.rdoc', 'exe/**/*', 'lib/**/{*,.[a-z]*}'] s.require_path = 'lib' 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/application/assets_test.rb b/railties/test/application/assets_test.rb index 2670fad618..9e8531b482 100644 --- a/railties/test/application/assets_test.rb +++ b/railties/test/application/assets_test.rb @@ -186,6 +186,26 @@ module ApplicationTests 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_manifest.js" ]' add_to_env_config 'production', 'config.assets.precompile += [ "another_manifest.js" ]' @@ -287,7 +307,7 @@ module ApplicationTests 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", "p { background-image: url(<%= asset_path('rails.png') %>) }" @@ -324,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 diff --git a/railties/test/application/bin_setup_test.rb b/railties/test/application/bin_setup_test.rb index 8c3ab65c51..ba700df1d6 100644 --- a/railties/test/application/bin_setup_test.rb +++ b/railties/test/application/bin_setup_test.rb @@ -28,7 +28,7 @@ module ApplicationTests assert_not File.exist?("tmp/restart.txt") `bin/setup 2>&1` assert_equal 0, File.size("log/test.log") - assert_equal '["articles", "schema_migrations", "active_record_internal_metadatas"]', list_tables.call + assert_equal '["articles", "schema_migrations", "ar_internal_metadata"]', list_tables.call assert File.exist?("tmp/restart.txt") end end @@ -43,6 +43,8 @@ module ApplicationTests The Gemfile's dependencies are satisfied == Preparing database == +Created database 'db/development.sqlite3' +Created database 'db/test.sqlite3' == Removing old logs and tempfiles == diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 7bcfc86d03..7ec25aeca1 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -348,6 +348,17 @@ module ApplicationTests end end + 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 @@ -544,6 +555,31 @@ module ApplicationTests assert_equal 'myamazonsecretaccesskey', app.secrets.aws_secret_access_key end + test "shared secrets saved in config/secrets.yml are loaded in app secrets" do + app_file 'config/secrets.yml', <<-YAML + shared: + api_key: 3b7cd727 + YAML + + app 'development' + + assert_equal '3b7cd727', app.secrets.api_key + end + + test "shared secrets will yield to environment specific secrets" do + app_file 'config/secrets.yml', <<-YAML + shared: + api_key: 3b7cd727 + + development: + api_key: abc12345 + YAML + + app 'development' + + assert_equal 'abc12345', app.secrets.api_key + end + test "blank config/secrets.yml does not crash the loading process" do app_file 'config/secrets.yml', <<-YAML YAML @@ -675,7 +711,7 @@ module ApplicationTests private - def form_authenticity_token(*args); token; end # stub the authenticy token + def form_authenticity_token(*args); token; end # stub the authenticity token end RUBY @@ -988,7 +1024,7 @@ module ApplicationTests 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 @@ -1326,7 +1362,7 @@ module ApplicationTests assert_equal 'custom key', Rails.application.config.my_custom_config['key'] end - test "config_for use the Pathname object if it is provided" do + test "config_for uses the Pathname object if it is provided" do app_file 'config/custom.yml', <<-RUBY development: key: 'custom key' @@ -1444,7 +1480,7 @@ module ApplicationTests assert Rails.configuration.api_only end - test "debug_exception_response_format is :api by default if only_api is enabled" do + test "debug_exception_response_format is :api by default if api_only is enabled" do add_to_config <<-RUBY config.api_only = true RUBY @@ -1453,7 +1489,7 @@ module ApplicationTests assert_equal :api, Rails.configuration.debug_exception_response_format end - test "debug_exception_response_format can be override" do + test "debug_exception_response_format can be overridden" do add_to_config <<-RUBY config.api_only = true RUBY diff --git a/railties/test/application/console_test.rb b/railties/test/application/console_test.rb index 7bf123d12b..ea68e63f8f 100644 --- a/railties/test/application/console_test.rb +++ b/railties/test/application/console_test.rb @@ -52,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 @@ -81,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 84cc6e120b..644af0e737 100644 --- a/railties/test/application/generators_test.rb +++ b/railties/test/application/generators_test.rb @@ -160,5 +160,15 @@ module ApplicationTests 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 4c06b6324c..44209a52f7 100644 --- a/railties/test/application/initializers/frameworks_test.rb +++ b/railties/test/application/initializers/frameworks_test.rb @@ -16,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 diff --git a/railties/test/application/initializers/i18n_test.rb b/railties/test/application/initializers/i18n_test.rb index ab7f29b0f2..0f9bb41053 100644 --- a/railties/test/application/initializers/i18n_test.rb +++ b/railties/test/application/initializers/i18n_test.rb @@ -245,7 +245,7 @@ fr: 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] @@ -257,6 +257,12 @@ fr: 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 40abaf860d..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 diff --git a/railties/test/application/middleware/exceptions_test.rb b/railties/test/application/middleware/exceptions_test.rb index 7b4babb13b..639b01b562 100644 --- a/railties/test/application/middleware/exceptions_test.rb +++ b/railties/test/application/middleware/exceptions_test.rb @@ -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/session_test.rb b/railties/test/application/middleware/session_test.rb index f847e80471..85e7761727 100644 --- a/railties/test/application/middleware/session_test.rb +++ b/railties/test/application/middleware/session_test.rb @@ -345,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_test.rb b/railties/test/application/middleware_test.rb index 1434522cce..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", - "ActionDispatch::LoadInterlock", + "ActionDispatch::Executor", "ActiveSupport::Cache::Strategy::LocalCache", "Rack::Runtime", "Rack::MethodOverride", @@ -38,8 +38,6 @@ module ApplicationTests "ActionDispatch::Reloader", "ActionDispatch::Callbacks", "ActiveRecord::Migration::CheckPending", - "ActiveRecord::ConnectionAdapters::ConnectionManagement", - "ActiveRecord::QueryCache", "ActionDispatch::Cookies", "ActionDispatch::Session::CookieStore", "ActionDispatch::Flash", @@ -57,7 +55,7 @@ module ApplicationTests assert_equal [ "Rack::Sendfile", "ActionDispatch::Static", - "ActionDispatch::LoadInterlock", + "ActionDispatch::Executor", "ActiveSupport::Cache::Strategy::LocalCache", "Rack::Runtime", "ActionDispatch::RequestId", @@ -67,8 +65,6 @@ module ApplicationTests "ActionDispatch::RemoteIp", "ActionDispatch::Reloader", "ActionDispatch::Callbacks", - "ActiveRecord::ConnectionAdapters::ConnectionManagement", - "ActiveRecord::QueryCache", "Rack::Head", "Rack::ConditionalGet", "Rack::ETag" @@ -114,23 +110,12 @@ 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 interlock if cache_classes is set but eager_load is not" do - add_to_config "config.cache_classes = true" - boot! - assert_not_includes middleware, "Rack::Lock" - assert_includes middleware, "ActionDispatch::LoadInterlock" - end - - test "includes interlock if cache_classes is off" do - add_to_config "config.cache_classes = false" + test "includes executor" do boot! - assert_not_includes middleware, "Rack::Lock" - assert_includes middleware, "ActionDispatch::LoadInterlock" + assert_includes middleware, "ActionDispatch::Executor" end test "does not include lock if cache_classes is set and so is eager_load" do @@ -138,21 +123,18 @@ module ApplicationTests add_to_config "config.eager_load = true" boot! assert_not_includes middleware, "Rack::Lock" - assert_not_includes middleware, "ActionDispatch::LoadInterlock" end test "does not include lock if allow_concurrency is set to :unsafe" do add_to_config "config.allow_concurrency = :unsafe" boot! assert_not_includes middleware, "Rack::Lock" - assert_not_includes middleware, "ActionDispatch::LoadInterlock" end test "includes lock if allow_concurrency is disabled" do add_to_config "config.allow_concurrency = false" boot! assert_includes middleware, "Rack::Lock" - assert_not_includes middleware, "ActionDispatch::LoadInterlock" end test "removes static asset server if public_file_server.enabled is disabled" do @@ -253,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/per_request_digest_cache_test.rb b/railties/test/application/per_request_digest_cache_test.rb index 3198e12662..dfe3fc9354 100644 --- a/railties/test/application/per_request_digest_cache_test.rb +++ b/railties/test/application/per_request_digest_cache_test.rb @@ -29,6 +29,8 @@ class PerRequestDigestCacheTest < ActiveSupport::TestCase 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 @@ -50,12 +52,13 @@ class PerRequestDigestCacheTest < ActiveSupport::TestCase get '/customers' assert_equal 200, last_response.status - assert_equal [ '8ba099b7749542fe765ff34a6824d548' ], ActionView::Digestor.cache.values + 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::Digestor.cache, :clear) do + assert_called(ActionView::LookupContext::DetailsKey, :clear) do get '/customers' assert_equal 200, last_response.status end diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb index c000a70382..cee9db5535 100644 --- a/railties/test/application/rake/dbs_test.rb +++ b/railties/test/application/rake/dbs_test.rb @@ -29,11 +29,11 @@ module ApplicationTests def db_create_and_drop(expected_database) Dir.chdir(app_path) do output = `bin/rails db:create` - assert_empty output + assert_match(/Created database/, output) assert File.exist?(expected_database) assert_equal expected_database, ActiveRecord::Base.connection_config[:database] output = `bin/rails db:drop` - assert_empty output + assert_match(/Dropped database/, output) assert !File.exist?(expected_database) end end @@ -222,14 +222,14 @@ module ApplicationTests assert_equal '["posts"]', list_tables[] `bin/rails db:schema:load` - assert_equal '["posts", "comments", "schema_migrations", "active_record_internal_metadatas"]', list_tables[] + 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/rails db:structure:load` - assert_equal '["posts", "comments", "schema_migrations", "active_record_internal_metadatas", "users"]', list_tables[] + assert_equal '["posts", "comments", "schema_migrations", "ar_internal_metadata", "users"]', list_tables[] end end 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/restart_test.rb b/railties/test/application/rake/restart_test.rb index 4cae199e6b..30f662a9be 100644 --- a/railties/test/application/rake/restart_test.rb +++ b/railties/test/application/rake/restart_test.rb @@ -34,6 +34,15 @@ module ApplicationTests 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 b979ad64d1..badb9ecdd6 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -24,7 +24,7 @@ module ApplicationTests assert $task_loaded end - def test_the_test_rake_task_is_protected_when_previous_migration_was_production + 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; @@ -118,11 +118,11 @@ module ApplicationTests end def test_code_statistics_sanity - assert_match "Code LOC: 14 Test LOC: 0 Code to Test Ratio: 1:0.0", - Dir.chdir(app_path){ `bin/rails 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' @@ -133,7 +133,7 @@ module ApplicationTests 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' @@ -141,12 +141,79 @@ module ApplicationTests end RUBY - ENV['CONTROLLER'] = 'cart' - output = Dir.chdir(app_path){ `bin/rails 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_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 + + 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_rake_routes_displays_message_when_no_routes_are_defined + def test_rails_routes_displays_message_when_no_routes_are_defined app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do end @@ -161,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 @@ -209,7 +287,7 @@ module ApplicationTests RAILS_ENV=test bin/rails db:migrate test` end - assert_match(/7 runs, 12 assertions, 0 failures, 0 errors/, output) + assert_match(/7 runs, 9 assertions, 0 failures, 0 errors/, output) assert_no_match(/Errors running/, output) end @@ -232,16 +310,15 @@ module ApplicationTests 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_scaffold_with_references_columns_tests_pass_by_default output = Dir.chdir(app_path) do - `bin/rails generate scaffold LineItems product:references cart:belongs_to; + `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, 12 assertions, 0 failures, 0 errors/, output) + assert_match(/7 runs, 9 assertions, 0 failures, 0 errors/, output) assert_no_match(/Errors running/, output) end @@ -303,7 +380,7 @@ module ApplicationTests def test_copy_templates Dir.chdir(app_path) do - `bin/rails 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 @@ -318,7 +395,7 @@ module ApplicationTests app_file "template.rb", "" output = Dir.chdir(app_path) do - `bin/rails rails:template LOCATION=template.rb` + `bin/rails app:template LOCATION=template.rb` end assert_match(/Hello, World!/, output) diff --git a/railties/test/application/routing_test.rb b/railties/test/application/routing_test.rb index 0777714d35..e51f32aaed 100644 --- a/railties/test/application/routing_test.rb +++ b/railties/test/application/routing_test.rb @@ -42,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 diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb index a7eb0feb11..08759ab5a4 100644 --- a/railties/test/application/test_runner_test.rb +++ b/railties/test/application/test_runner_test.rb @@ -363,7 +363,7 @@ module ApplicationTests end RUBY - run_test_command('test/models/account_test.rb:4:9 test/models/post_test:4:9').tap do |output| + 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 @@ -382,6 +382,54 @@ module ApplicationTests 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 @@ -419,7 +467,7 @@ module ApplicationTests 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:\nwups!\n\nbin/rails test test/models/post_test.rb:6\n\n\n\n} + 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 @@ -454,6 +502,40 @@ module ApplicationTests 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 def run_test_command(arguments = 'test/unit/test_test.rb') Dir.chdir(app_path) { `bin/rails t #{arguments}` } diff --git a/railties/test/commands/dev_cache_test.rb b/railties/test/commands/dev_cache_test.rb deleted file mode 100644 index 1b7a72e7fc..0000000000 --- a/railties/test/commands/dev_cache_test.rb +++ /dev/null @@ -1,32 +0,0 @@ -require_relative '../isolation/abstract_unit' - -module CommandsTests - class DevCacheTest < 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(%r{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(%r{Development mode is no longer being cached}, output) - end - end - end -end diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb index 0c49bd9c53..38a1605d1f 100644 --- a/railties/test/commands/server_test.rb +++ b/railties/test/commands/server_test.rb @@ -54,7 +54,8 @@ class Rails::ServerTest < ActiveSupport::TestCase def test_caching_without_option args = [] options = Rails::Server::Options.new.parse!(args) - assert_equal nil, options[:caching] + merged_options = Rails::Server.new.default_options.merge(options) + assert_equal nil, merged_options[:caching] end def test_caching_with_option @@ -117,4 +118,18 @@ class Rails::ServerTest < ActiveSupport::TestCase 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 3300850604..3b2b3c37d0 100644 --- a/railties/test/generators/actions_test.rb +++ b/railties/test/generators/actions_test.rb @@ -201,7 +201,7 @@ class ActionsTest < Rails::Generators::TestCase end end - def test_rake_should_run_rake_command_with_default_env + 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' @@ -209,13 +209,13 @@ class ActionsTest < Rails::Generators::TestCase end end - def test_rake_with_env_option_should_run_rake_command_in_env + 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_rails_env_variable_should_run_rake_command_in_env + 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' @@ -223,7 +223,7 @@ class ActionsTest < Rails::Generators::TestCase end end - def test_env_option_should_win_over_rails_env_variable_when_running_rake + 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' @@ -231,7 +231,7 @@ class ActionsTest < Rails::Generators::TestCase end end - def test_rake_with_sudo_option_should_run_rake_command_with_sudo + 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 @@ -239,6 +239,44 @@ class ActionsTest < Rails::Generators::TestCase 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 + + 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 assert_called_with(generator, :run, ['capify .', verbose: false]) do action :capify! diff --git a/railties/test/generators/api_app_generator_test.rb b/railties/test/generators/api_app_generator_test.rb index d9eb7770f3..92779452e1 100644 --- a/railties/test/generators/api_app_generator_test.rb +++ b/railties/test/generators/api_app_generator_test.rb @@ -52,6 +52,25 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase 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 + + def test_generator_skips_per_form_csrf_token_and_origin_check_configs_for_api_apps + run_generator + + assert_file "config/initializers/new_framework_defaults.rb" do |initializer_content| + assert_no_match(/per_form_csrf_tokens/, initializer_content) + assert_no_match(/forgery_protection_origin_check/, initializer_content) + end + end + private def default_files @@ -63,6 +82,8 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase app/controllers app/mailers app/models + app/views/layouts/mailer.html.erb + app/views/layouts/mailer.text.erb config/environments config/initializers config/locales @@ -84,15 +105,19 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase def skipped_files %w(app/assets app/helpers - app/views + 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) + tmp/cache/assets + public/404.html + public/422.html + public/500.html + public/apple-touch-icon-precomposed.png + public/apple-touch-icon.png + public/favicon.ico) end end diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 136bdd1694..058308aa13 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -27,6 +27,8 @@ DEFAULT_APP_FILES = %w( config/initializers config/locales config/cable.yml + config/puma.rb + config/spring.rb db lib lib/tasks @@ -63,8 +65,8 @@ 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 @@ -124,7 +126,7 @@ class AppGeneratorTest < Rails::Generators::TestCase # make sure we are in correct dir FileUtils.cd(app_moved_root) - generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, + generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_moved_root, shell: @shell generator.send(:app_const) quietly { generator.send(:update_config_files) } @@ -139,7 +141,7 @@ class AppGeneratorTest < Rails::Generators::TestCase run_generator [app_root] stub_rails_application(app_root) do - generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell + 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/ @@ -163,80 +165,84 @@ class AppGeneratorTest < Rails::Generators::TestCase run_generator [app_root] stub_rails_application(app_root) do - generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell + 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 + 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/callback_terminator.rb") + FileUtils.rm("#{app_root}/config/initializers/cookies_serializer.rb") stub_rails_application(app_root) do - generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell + 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" + assert_file("#{app_root}/config/initializers/cookies_serializer.rb", + /Valid options are :json, :marshal, and :hybrid\.\nRails\.application\.config\.action_dispatch\.cookies_serializer = :marshal/) end end - def test_rails_update_does_not_remove_callback_terminator_initializer_if_already_present + def test_rails_update_dont_set_file_watcher app_root = File.join(destination_root, 'myapp') run_generator [app_root] - FileUtils.touch("#{app_root}/config/initializers/callback_terminator.rb") - stub_rails_application(app_root) do - generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell + 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" + assert_file "#{app_root}/config/environments/development.rb" do |content| + assert_match(/# config.file_watcher/, content) + end end end - def test_rails_update_set_the_cookie_serializer_to_marchal_if_it_is_not_already_configured + def test_rails_update_does_not_create_new_framework_defaults_by_default app_root = File.join(destination_root, 'myapp') run_generator [app_root] - FileUtils.rm("#{app_root}/config/initializers/cookies_serializer.rb") + FileUtils.rm("#{app_root}/config/initializers/new_framework_defaults.rb") stub_rails_application(app_root) do - generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell + generator = Rails::Generators::AppGenerator.new ["rails"], { update: true }, destination_root: app_root, shell: @shell generator.send(:app_const) quietly { generator.send(:update_config_files) } - assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :marshal/) + + assert_file "#{app_root}/config/initializers/new_framework_defaults.rb" do |content| + assert_match(/ActiveSupport\.halt_callback_chains_on_return_false = true/, content) + assert_match(/Rails\.application\.config.active_record\.belongs_to_required_by_default = false/, content) + assert_no_match(/Rails\.application\.config\.ssl_options/, content) + end end end - def test_rails_update_does_not_create_active_record_belongs_to_required_by_default + def test_rails_update_does_not_create_rack_cors app_root = File.join(destination_root, 'myapp') run_generator [app_root] - FileUtils.rm("#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb") - stub_rails_application(app_root) do - generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell + 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" + assert_no_file "#{app_root}/config/initializers/cors.rb" end end - def test_rails_update_does_not_remove_active_record_belongs_to_required_by_default_if_already_present + 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/active_record_belongs_to_required_by_default.rb") + FileUtils.touch("#{app_root}/config/initializers/cors.rb") stub_rails_application(app_root) do - generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell + 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" + assert_file "#{app_root}/config/initializers/cors.rb" end end @@ -337,15 +343,31 @@ 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) end + + assert_file "config/initializers/new_framework_defaults.rb" do |initializer_content| + assert_no_match(/belongs_to_required_by_default/, initializer_content) + end end def test_generator_if_skip_action_mailer_is_given @@ -376,9 +398,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) @@ -394,33 +417,16 @@ class AppGeneratorTest < Rails::Generators::TestCase 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.coffee" - assert_no_file "app/channels" - assert_file "app/views/layouts/application.html.erb" do |content| - assert_no_match(/action_cable_meta_tag/, content) - end - assert_file "Gemfile" do |content| - assert_no_match(/em-hiredis/, content) - assert_no_match(/redis/, content) - end - end - - def test_generator_if_skip_action_cable_is_given_for_an_api_app - run_generator [destination_root, "--skip-action-cable", "--api"] - assert_file "config/application.rb", /#\s+require\s+["']action_cable\/engine["']/ - assert_no_file "config/cable.yml" - assert_no_file "app/assets/javascripts/cable.coffee" + assert_no_file "app/assets/javascripts/cable.js" assert_no_file "app/channels" assert_file "Gemfile" do |content| - assert_no_match(/em-hiredis/, content) assert_no_match(/redis/, content) end end def test_action_cable_redis_gems run_generator - assert_gem 'em-hiredis' - assert_gem 'redis' + assert_file "Gemfile", /^# gem 'redis'/ end def test_inclusion_of_javascript_runtime @@ -483,6 +489,31 @@ 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"])) @@ -553,7 +584,7 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "Gemfile" do |content| assert_match(/gem 'web-console',\s+github: 'rails\/web-console'/, content) - assert_no_match(/gem 'web-console', '~> 3.0'/, content) + assert_no_match(/\Agem 'web-console'\z/, content) end end @@ -562,7 +593,7 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "Gemfile" do |content| assert_match(/gem 'web-console',\s+github: 'rails\/web-console'/, content) - assert_no_match(/gem 'web-console', '~> 3.0'/, content) + assert_no_match(/\Agem 'web-console'\z/, content) end end @@ -592,7 +623,7 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_spring_no_fork jruby_skip "spring doesn't run on JRuby" - assert_called_with(Process, :respond_to?, [:fork], returns: false) do + assert_called_with(Process, :respond_to?, [[:fork], [:fork]], returns: false) do run_generator assert_file "Gemfile" do |content| @@ -604,6 +635,7 @@ class AppGeneratorTest < Rails::Generators::TestCase 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 @@ -703,9 +735,8 @@ class AppGeneratorTest < Rails::Generators::TestCase end sequence = ['install', 'exec spring binstub --all', 'echo ran after_bundle'] - ensure_bundler_first = -> command do @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 @@ -717,6 +748,8 @@ class AppGeneratorTest < Rails::Generators::TestCase end end end + + assert_equal 3, @sequence_step end protected @@ -739,4 +772,23 @@ class AppGeneratorTest < Rails::Generators::TestCase 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 index c1f0c03fbf..e3edde681f 100644 --- a/railties/test/generators/channel_generator_test.rb +++ b/railties/test/generators/channel_generator_test.rb @@ -5,6 +5,18 @@ 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'] @@ -12,8 +24,24 @@ class ChannelGeneratorTest < Rails::Generators::TestCase 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) + assert_file "app/assets/javascripts/channels/chat.js" do |channel| + assert_match(/App.chat = App.cable.subscriptions.create\("ChatChannel/, channel) + end + end + + def test_channel_with_multiple_actions_is_created + run_generator ['chat', 'speak', 'mute'] + + assert_file "app/channels/chat_channel.rb" do |channel| + assert_match(/class ChatChannel < ApplicationCable::Channel/, channel) + assert_match(/def speak/, channel) + assert_match(/def mute/, channel) + end + + assert_file "app/assets/javascripts/channels/chat.js" do |channel| + assert_match(/App.chat = App.cable.subscriptions.create\("ChatChannel/, channel) + assert_match(/,\n\n speak/, channel) + assert_match(/,\n\n mute: function\(\) \{\n return this\.perform\('mute'\);\n \}\n\}\);/, channel) end end @@ -24,6 +52,26 @@ class ChannelGeneratorTest < Rails::Generators::TestCase assert_match(/class ChatChannel < ApplicationCable::Channel/, channel) end - assert_no_file "app/assets/javascripts/channels/chat.coffee" + assert_no_file "app/assets/javascripts/channels/chat.js" + 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.js" + + 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/job_generator_test.rb b/railties/test/generators/job_generator_test.rb index 7fd8f2062f..dbff0ab704 100644 --- a/railties/test/generators/job_generator_test.rb +++ b/railties/test/generators/job_generator_test.rb @@ -26,4 +26,11 @@ class JobGeneratorTest < Rails::Generators::TestCase 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 f8d9ccacb4..6a4951840d 100644 --- a/railties/test/generators/mailer_generator_test.rb +++ b/railties/test/generators/mailer_generator_test.rb @@ -12,6 +12,12 @@ class MailerGeneratorTest < Rails::Generators::TestCase assert_no_match(/default from: "from@example.com"/, mailer) assert_no_match(/layout :mailer_notifier/, mailer) end + + assert_file 'app/mailers/application_mailer.rb' do |mailer| + assert_match(/class ApplicationMailer < ActionMailer::Base/, mailer) + assert_match(/default from: 'from@example.com'/, mailer) + assert_match(/layout 'mailer'/, mailer) + end end def test_mailer_with_i18n_helper @@ -78,6 +84,10 @@ 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 @@ -91,6 +101,10 @@ 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{<body>\n <%= yield %>\n </body>}, view) + end end def test_invokes_default_template_engine_even_with_no_action diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb index 814f4c050e..6b30c40476 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) @@ -26,7 +34,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_invokes_default_orm run_generator - assert_file "app/models/account.rb", /class Account < ActiveRecord::Base/ + assert_file "app/models/account.rb", /class Account < ApplicationRecord/ end def test_model_with_parent_option @@ -48,7 +56,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_plural_names_are_singularized content = run_generator ["accounts".freeze] - assert_file "app/models/account.rb", /class Account < ActiveRecord::Base/ + assert_file "app/models/account.rb", /class Account < ApplicationRecord/ assert_file "test/models/account_test.rb", /class AccountTest/ assert_match(/\[WARNING\] The model name 'accounts' was recognized as a plural, using the singular 'account' instead\. Override with --force-plural or setup custom inflection rules for this noun before running the generator\./, content) end @@ -63,7 +71,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase assert_file "app/models/admin.rb", /module Admin/ assert_file "app/models/admin.rb", /def self\.table_name_prefix/ assert_file "app/models/admin.rb", /'admin_'/ - assert_file "app/models/admin/account.rb", /class Admin::Account < ActiveRecord::Base/ + assert_file "app/models/admin/account.rb", /class Admin::Account < ApplicationRecord/ end def test_migration @@ -298,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 @@ -378,7 +386,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase run_generator ["account", "supplier:references{required}"] expected_file = <<-FILE.strip_heredoc - class Account < ActiveRecord::Base + class Account < ApplicationRecord belongs_to :supplier, required: true end FILE @@ -389,7 +397,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase run_generator ["account", "supplier:references{required,polymorphic}"] expected_file = <<-FILE.strip_heredoc - class Account < ActiveRecord::Base + class Account < ApplicationRecord belongs_to :supplier, polymorphic: true, required: true end FILE @@ -400,7 +408,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase run_generator ["account", "supplier:references{polymorphic.required}"] expected_file = <<-FILE.strip_heredoc - class Account < ActiveRecord::Base + class Account < ApplicationRecord belongs_to :supplier, polymorphic: true, required: true end FILE @@ -451,7 +459,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_token_option_adds_has_secure_token run_generator ["user", "token:token", "auth_token:token"] expected_file = <<-FILE.strip_heredoc - class User < ActiveRecord::Base + class User < ApplicationRecord has_secure_token has_secure_token :auth_token end diff --git a/railties/test/generators/namespaced_generators_test.rb b/railties/test/generators/namespaced_generators_test.rb index d76759a7d1..902c340321 100644 --- a/railties/test/generators/namespaced_generators_test.rb +++ b/railties/test/generators/namespaced_generators_test.rb @@ -91,7 +91,7 @@ class NamespacedModelGeneratorTest < NamespacedGeneratorTestCase def test_adds_namespace_to_model run_generator - assert_file "app/models/test_app/account.rb", /module TestApp/, / class Account < ActiveRecord::Base/ + assert_file "app/models/test_app/account.rb", /module TestApp/, / class Account < ApplicationRecord/ end def test_model_with_namespace @@ -99,7 +99,7 @@ class NamespacedModelGeneratorTest < NamespacedGeneratorTestCase assert_file "app/models/test_app/admin.rb", /module TestApp/, /module Admin/ assert_file "app/models/test_app/admin.rb", /def self\.table_name_prefix/ assert_file "app/models/test_app/admin.rb", /'test_app_admin_'/ - assert_file "app/models/test_app/admin/account.rb", /module TestApp/, /class Admin::Account < ActiveRecord::Base/ + assert_file "app/models/test_app/admin/account.rb", /module TestApp/, /class Admin::Account < ApplicationRecord/ end def test_migration @@ -201,7 +201,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase run_generator # Model - assert_file "app/models/test_app/product_line.rb", /module TestApp\n class ProductLine < ActiveRecord::Base/ + assert_file "app/models/test_app/product_line.rb", /module TestApp\n class ProductLine < ApplicationRecord/ assert_file "test/models/test_app/product_line_test.rb", /module TestApp\n class ProductLineTest < ActiveSupport::TestCase/ assert_file "test/fixtures/test_app/product_lines.yml" assert_migration "db/migrate/create_test_app_product_lines.rb" @@ -268,7 +268,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase # 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 "app/models/test_app/admin/role.rb", /module TestApp\n class Admin::Role < ApplicationRecord/ 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" @@ -336,7 +336,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase # Model assert_file "app/models/test_app/admin/user/special.rb", /module TestApp\n module Admin/ - assert_file "app/models/test_app/admin/user/special/role.rb", /module TestApp\n class Admin::User::Special::Role < ActiveRecord::Base/ + assert_file "app/models/test_app/admin/user/special/role.rb", /module TestApp\n class Admin::User::Special::Role < ApplicationRecord/ assert_file "test/models/test_app/admin/user/special/role_test.rb", /module TestApp\n class Admin::User::Special::RoleTest < ActiveSupport::TestCase/ assert_file "test/fixtures/test_app/admin/user/special/roles.yml" assert_migration "db/migrate/create_test_app_admin_user_special_roles.rb" @@ -402,7 +402,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase # 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 "app/models/test_app/admin/role.rb", /module TestApp\n class Admin::Role < ApplicationRecord/ 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" diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index 874bda17b7..5dd4cce28a 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -240,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 @@ -335,7 +335,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase 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/ @@ -357,7 +357,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase 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/ @@ -379,7 +379,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase 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/ @@ -444,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"]) @@ -634,6 +642,89 @@ class PluginGeneratorTest < Rails::Generators::TestCase 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_mailer_layouts_when_does_not_exist_in_mountable_engine + run_generator [destination_root, '--mountable'] + capture(:stdout) do + `#{destination_root}/bin/rails g mailer User` + end + + assert_file "#{destination_root}/app/views/layouts/bukkits/mailer.text.erb" do |view| + assert_match(/<%= yield %>/, view) + end + + assert_file "#{destination_root}/app/views/layouts/bukkits/mailer.html.erb" do |view| + assert_match(%r{<body>\n <%= yield %>\n </body>}, view) + 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_runner_test.rb b/railties/test/generators/plugin_test_runner_test.rb index f492cd49ef..ef6359fece 100644 --- a/railties/test/generators/plugin_test_runner_test.rb +++ b/railties/test/generators/plugin_test_runner_test.rb @@ -63,7 +63,7 @@ class PluginTestRunnerTest < ActiveSupport::TestCase create_test_file 'post', pass: false output = run_test_command('test/post_test.rb') - expect = %r{Running:\n\nPostTest\nF\n\nFailure:\nPostTest#test_truth:\nwups!\n\nbin/test (/private)?#{plugin_path}/test/post_test.rb:6} + 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 diff --git a/railties/test/generators/resource_generator_test.rb b/railties/test/generators/resource_generator_test.rb index addaf83bc8..53dcfc4024 100644 --- a/railties/test/generators/resource_generator_test.rb +++ b/railties/test/generators/resource_generator_test.rb @@ -60,14 +60,14 @@ class ResourceGeneratorTest < Rails::Generators::TestCase def test_plural_names_are_singularized content = run_generator ["accounts".freeze] - assert_file "app/models/account.rb", /class Account < ActiveRecord::Base/ + assert_file "app/models/account.rb", /class Account < ApplicationRecord/ assert_file "test/models/account_test.rb", /class AccountTest/ assert_match(/\[WARNING\] The model name 'accounts' was recognized as a plural, using the singular 'account' instead\. Override with --force-plural or setup custom inflection rules for this noun before running the generator\./, content) end def test_plural_names_can_be_forced content = run_generator ["accounts", "--force-plural"] - assert_file "app/models/accounts.rb", /class Accounts < ActiveRecord::Base/ + assert_file "app/models/accounts.rb", /class Accounts < ApplicationRecord/ assert_file "test/models/accounts_test.rb", /class AccountsTest/ assert_no_match(/\[WARNING\]/, content) end diff --git a/railties/test/generators/scaffold_controller_generator_test.rb b/railties/test/generators/scaffold_controller_generator_test.rb index c37e289f4b..736ff0b41f 100644 --- a/railties/test/generators/scaffold_controller_generator_test.rb +++ b/railties/test/generators/scaffold_controller_generator_test.rb @@ -238,8 +238,8 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase 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_match(/post users_url, params: \{ user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \} \}, as: :json/, 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 \} \}, as: :json/, content) assert_no_match(/assert_redirected_to/, content) end end diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb index 6f7a83cae0..bd69906b9d 100644 --- a/railties/test/generators/scaffold_generator_test.rb +++ b/railties/test/generators/scaffold_generator_test.rb @@ -11,7 +11,7 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase run_generator # Model - assert_file "app/models/product_line.rb", /class ProductLine < ActiveRecord::Base/ + assert_file "app/models/product_line.rb", /class ProductLine < ApplicationRecord/ 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/ @@ -91,7 +91,7 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase 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 "app/models/product_line.rb", /class ProductLine < ApplicationRecord/ 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/ @@ -205,7 +205,7 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase # Model assert_file "app/models/admin.rb", /module Admin/ - assert_file "app/models/admin/role.rb", /class Admin::Role < ActiveRecord::Base/ + assert_file "app/models/admin/role.rb", /class Admin::Role < ApplicationRecord/ assert_file "test/models/admin/role_test.rb", /class Admin::RoleTest < ActiveSupport::TestCase/ assert_file "test/fixtures/admin/roles.yml" assert_migration "db/migrate/create_admin_roles.rb" @@ -486,9 +486,9 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase Dir.chdir(engine_path) do quietly do `bin/rails g scaffold User name:string age:integer; - bundle exec rake db:migrate` + bin/rails db:migrate` end - assert_match(/8 runs, 13 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) + assert_match(/8 runs, 10 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) end end @@ -500,9 +500,9 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase Dir.chdir(engine_path) do quietly do `bin/rails g scaffold User name:string age:integer; - bundle exec rake db:migrate` + bin/rails db:migrate` end - assert_match(/8 runs, 13 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) + assert_match(/8 runs, 10 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) end end @@ -514,7 +514,7 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase Dir.chdir(engine_path) do quietly do `bin/rails g scaffold User name:string age:integer; - bundle exec rake db:migrate` + bin/rails db:migrate` end assert_match(/6 runs, 8 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) end @@ -528,7 +528,7 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase Dir.chdir(engine_path) do quietly do `bin/rails g scaffold User name:string age:integer; - bundle exec rake db:migrate` + bin/rails db:migrate` end assert_match(/6 runs, 8 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) end diff --git a/railties/test/generators/test_runner_in_engine_test.rb b/railties/test/generators/test_runner_in_engine_test.rb index 69906b962b..d37e261fbb 100644 --- a/railties/test/generators/test_runner_in_engine_test.rb +++ b/railties/test/generators/test_runner_in_engine_test.rb @@ -17,7 +17,7 @@ class TestRunnerInEngineTest < ActiveSupport::TestCase create_test_file 'post', pass: false output = run_test_command('test/post_test.rb') - expect = %r{Running:\n\nPostTest\nF\n\nFailure:\nPostTest#test_truth:\nwups!\n\nbin/rails test test/post_test.rb:6} + expect = %r{Running:\n\nPostTest\nF\n\nFailure:\nPostTest#test_truth \[[^\]]+test/post_test.rb:6\]:\nwups!\n\nbin/rails test test/post_test.rb:4} assert_match expect, output end diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index df3c2ca66d..e427614dfa 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -18,6 +18,7 @@ RAILS_FRAMEWORK_ROOT = File.expand_path("#{File.dirname(__FILE__)}/../../..") # These files do not require any others and are needed # to run the tests +require "active_support/core_ext/object/blank" require "active_support/testing/isolation" require "active_support/core_ext/kernel/reporting" require 'tmpdir' @@ -74,10 +75,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) @@ -109,7 +112,7 @@ module TestHelpers # Delete the initializers unless requested unless options[:initializers] - Dir["#{app_path}/config/initializers/*.rb"].each do |initializer| + Dir["#{app_path}/config/initializers/**/*.rb"].each do |initializer| File.delete(initializer) end end @@ -122,7 +125,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 @@ -270,10 +273,17 @@ 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, mode = 'w') @@ -302,10 +312,6 @@ module TestHelpers end def boot_rails - # FIXME: shush Sass warning spam, not relevant to testing Railties - Kernel.silence_warnings do - require File.expand_path('../../../../load_paths', __FILE__) - end end end end @@ -317,7 +323,6 @@ class ActiveSupport::TestCase include ActiveSupport::Testing::Stream self.test_order = :sorted - end # Create a scope and build a fixture rails app @@ -328,12 +333,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/exe/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/rails_info_controller_test.rb b/railties/test/rails_info_controller_test.rb index c51503c2b7..2e10d63599 100644 --- a/railties/test/rails_info_controller_test.rb +++ b/railties/test/rails_info_controller_test.rb @@ -78,4 +78,10 @@ class InfoControllerTest < ActionController::TestCase get :routes, params: { path: 'rails/info/routes.html' } assert fuzzy_count.call == 0, 'should match optional parts of route literally' end + + test "internal routes do not have a default params[:internal] value" do + get :properties + assert_response :success + assert_nil @controller.params[:internal] + end end diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index 4a47ab32b4..b6fdd33898 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -481,7 +481,7 @@ YAML end RUBY - add_to_config "config.middleware.use \"Bukkits\"" + add_to_config "config.middleware.use Bukkits" boot_rails end diff --git a/railties/test/railties/generators_test.rb b/railties/test/railties/generators_test.rb index 5f4171d44b..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/exe/rails #{cmd}` + `#{Gem.ruby} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails #{cmd}` end def build_engine(is_mountable=false) diff --git a/railties/test/test_unit/reporter_test.rb b/railties/test/test_unit/reporter_test.rb index 2d08d4ec30..0d64b48550 100644 --- a/railties/test/test_unit/reporter_test.rb +++ b/railties/test/test_unit/reporter_test.rb @@ -62,7 +62,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase @reporter.record(failed_test) @reporter.report - expect = %r{\AF\n\nFailure:\nTestUnitReporterTest::ExampleTest#woot:\nboo\n\nbin/rails test test/test_unit/reporter_test.rb:\d+\n\n\z} + expect = %r{\AF\n\nFailure:\nTestUnitReporterTest::ExampleTest#woot \[[^\]]+\]:\nboo\n\nbin/rails test test/test_unit/reporter_test.rb:\d+\n\n\z} assert_match expect, @output.string end @@ -79,7 +79,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase verbose.record(skipped_test) verbose.report - expect = %r{\ATestUnitReporterTest::ExampleTest#woot = 10\.00 s = S\n\n\nSkipped:\nTestUnitReporterTest::ExampleTest#woot:\nskipchurches, misstemples\n\nbin/rails test test/test_unit/reporter_test.rb:\d+\n\n\z} + expect = %r{\ATestUnitReporterTest::ExampleTest#woot = 10\.00 s = S\n\n\nSkipped:\nTestUnitReporterTest::ExampleTest#woot \[[^\]]+\]:\nskipchurches, misstemples\n\nbin/rails test test/test_unit/reporter_test.rb:\d+\n\n\z} assert_match expect, @output.string end @@ -104,11 +104,22 @@ class TestUnitReporterTest < ActiveSupport::TestCase end end - test "fail fast does not interrupt run errors or skips" do + test "fail fast interrupts run on error" do fail_fast = Rails::TestUnitReporter.new @output, fail_fast: true + interrupt_raised = false - fail_fast.record(errored_test) - assert_no_match 'Failed tests:', @output.string + # 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 diff --git a/tasks/release.rb b/tasks/release.rb index c7704aa865..4a1ed04478 100644 --- a/tasks/release.rb +++ b/tasks/release.rb @@ -43,22 +43,54 @@ 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 :push => :build do sh "gem push #{gem}" + sh "npm publish" if File.exist?("#{framework}/package.json") end end end @@ -133,7 +165,7 @@ 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 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/test.rb b/tools/test.rb index 70f295b554..62e0faa3db 100644 --- a/tools/test.rb +++ b/tools/test.rb @@ -1,5 +1,8 @@ $: << File.expand_path("test", COMPONENT_ROOT) -require File.expand_path("../../load_paths", __FILE__) + +require 'bundler' +Bundler.setup + require "rails/test_unit/minitest_plugin" module Rails diff --git a/version.rb b/version.rb index f4d5b660dc..9c49e0655a 100644 --- a/version.rb +++ b/version.rb @@ -6,9 +6,9 @@ module Rails module VERSION MAJOR = 5 - MINOR = 0 + MINOR = 1 TINY = 0 - PRE = "beta1.1" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end |