diff options
422 files changed, 3303 insertions, 1517 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml index e4568d9d8b..7d4ec1c54f 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -23,7 +23,7 @@ checks: engines: rubocop: enabled: true - channel: rubocop-0-52 + channel: rubocop-0-54 ratings: paths: diff --git a/.github/issue_template.md b/.github/issue_template.md index 2d071d4a71..2ff6a271db 100644 --- a/.github/issue_template.md +++ b/.github/issue_template.md @@ -1,7 +1,7 @@ ### 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)) +here](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#creating-a-bug-report)) ### Expected behavior Tell us what should happen diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 214d63740c..a36687ec99 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -16,6 +16,6 @@ CHANGELOG files by reviewers, please add the CHANGELOG entry at the top of the f 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) +here](http://edgeguides.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 index 3c765d5b1d..954ab3b1cb 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,3 +1,5 @@ +require: './ci/custom_cops/lib/custom_cops' + AllCops: TargetRubyVersion: 2.4 # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop @@ -7,6 +9,17 @@ AllCops: - '**/templates/**/*' - '**/vendor/**/*' - 'actionpack/lib/action_dispatch/journey/parser.rb' + - 'railties/test/fixtures/tmp/**/*' + +# Prefer assert_not_x over refute_x +CustomCops/RefuteNot: + Include: + - '**/test/**/*' + +# Prefer assert_not over assert ! +CustomCops/AssertNot: + Include: + - '**/test/**/*' # Prefer &&/|| over and/or. Style/AndOr: @@ -29,6 +42,13 @@ Layout/CommentIndentation: Layout/ElseAlignment: Enabled: true +# Align `end` with the matching keyword or starting expression except for +# assignments, where it should be aligned with the LHS. +Layout/EndAlignment: + Enabled: true + EnforcedStyleAlignWith: variable + AutoCorrect: true + Layout/EmptyLineAfterMagicComment: Enabled: true @@ -138,13 +158,6 @@ Layout/TrailingWhitespace: Style/UnneededPercentQ: Enabled: true -# Align `end` with the matching keyword or starting expression except for -# assignments, where it should be aligned with the LHS. -Lint/EndAlignment: - Enabled: true - EnforcedStyleAlignWith: variable - AutoCorrect: 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 1c67c1475c..0fdea1367c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -61,21 +61,21 @@ env: - "GEM=ac:integration" rvm: - - 2.4.3 - - 2.5.0 + - 2.4.4 + - 2.5.1 - ruby-head matrix: include: - - rvm: 2.5.0 + - rvm: 2.5.1 env: "GEM=av:ujs" - - rvm: 2.4.3 + - rvm: 2.4.4 env: "GEM=aj:integration" services: - memcached - redis-server - rabbitmq - - rvm: 2.5.0 + - rvm: 2.5.1 env: "GEM=aj:integration" services: - memcached @@ -87,15 +87,15 @@ matrix: - memcached - redis-server - rabbitmq - - rvm: 2.5.0 + - rvm: 2.5.1 env: - "GEM=ar:mysql2 MYSQL=mariadb" addons: mariadb: 10.2 - - rvm: 2.5.0 + - rvm: 2.5.1 env: - "GEM=ar:sqlite3_mem" - - rvm: 2.5.0 + - rvm: 2.5.1 env: - "GEM=ar:postgresql POSTGRES=9.2" addons: diff --git a/Brewfile b/Brewfile new file mode 100644 index 0000000000..4ac325e80a --- /dev/null +++ b/Brewfile @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +tap "homebrew/core" +tap "homebrew/bundle" +tap "homebrew/services" +tap "caskroom/cask" +brew "ffmpeg" +brew "memcached" +brew "mysql" +brew "postgresql" +brew "redis" +brew "yarn" +cask "xquartz" +brew "mupdf" +brew "poppler" diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 078d5f1219..ecd56b87d6 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -8,5 +8,5 @@ http://rubyonrails.org/conduct/ For a history of updates, see the page history here: -https://github.com/rails/rails.github.com/commits/master/conduct/index.html +https://github.com/rails/homepage/commits/master/conduct.html @@ -9,11 +9,9 @@ gemspec # We need a newish Rake since Active Job sets its test tasks' descriptions. gem "rake", ">= 11.1" -# 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", require: false +gem "mocha" -gem "capybara", ">= 2.15", "< 4.0" +gem "capybara", ">= 2.15" gem "rack-cache", "~> 1.2" gem "coffee-rails" @@ -45,7 +43,7 @@ group :doc do end # Active Support. -gem "dalli", "< 2.7.7" +gem "dalli" gem "listen", ">= 3.0.5", "< 3.2", require: false gem "libxml-ruby", platforms: :ruby gem "connection_pool", require: false diff --git a/Gemfile.lock b/Gemfile.lock index 19de9bc22b..a0f823b6fc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -156,21 +156,21 @@ GEM childprocess faraday selenium-webdriver - bootsnap (1.1.2) + bootsnap (1.2.1) msgpack (~> 1.0) - bootsnap (1.1.2-java) + bootsnap (1.2.1-java) msgpack (~> 1.0) builder (3.2.3) bunny (2.6.6) amq-protocol (>= 2.1.0) byebug (9.0.6) - capybara (2.15.1) + capybara (3.0.1) addressable mini_mime (>= 0.1.3) - nokogiri (>= 1.3.3) - rack (>= 1.0.0) - rack-test (>= 0.5.4) - xpath (~> 2.0) + nokogiri (~> 1.8) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + xpath (~> 3.0) childprocess (0.7.1) ffi (~> 1.0, >= 1.0.11) chromedriver-helper (1.1.0) @@ -190,7 +190,7 @@ GEM crass (1.0.3) curses (1.0.2) daemons (1.2.4) - dalli (2.7.6) + dalli (2.7.8) dante (0.2.0) declarative (0.0.10) declarative-option (0.1.0) @@ -300,7 +300,7 @@ GEM mime-types-data (3.2016.0521) mimemagic (0.3.2) mini_magick (4.8.0) - mini_mime (0.1.4) + mini_mime (1.0.0) mini_portile2 (2.3.0) minitest (5.11.3) minitest-bisect (1.4.0) @@ -308,13 +308,13 @@ GEM path_expander (~> 1.0) minitest-server (1.0.5) minitest (~> 5.0) - mocha (1.3.0) + mocha (1.5.0) metaclass (~> 0.0.1) mono_logger (1.1.0) - msgpack (1.1.0) - msgpack (1.1.0-java) - msgpack (1.1.0-x64-mingw32) - msgpack (1.1.0-x86-mingw32) + msgpack (1.2.4) + msgpack (1.2.4-java) + msgpack (1.2.4-x64-mingw32) + msgpack (1.2.4-x86-mingw32) multi_json (1.12.2) multipart-post (2.0.0) mustache (1.0.5) @@ -333,7 +333,7 @@ GEM mini_portile2 (~> 2.3.0) os (0.9.6) parallel (1.12.1) - parser (2.5.0.2) + parser (2.5.1.0) ast (~> 2.4.0) path_expander (1.0.2) pg (1.0.0) @@ -341,7 +341,7 @@ GEM pg (1.0.0-x86-mingw32) powerpack (0.1.1) psych (3.0.2) - public_suffix (3.0.1) + public_suffix (3.0.2) puma (3.9.1) puma (3.9.1-java) que (0.14.0) @@ -354,7 +354,7 @@ GEM rack (>= 0.4) rack-protection (2.0.1) rack - rack-test (0.8.0) + rack-test (1.0.0) rack (>= 1.0, < 3) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) @@ -385,9 +385,9 @@ GEM resque (~> 1.26) rufus-scheduler (~> 3.2) retriable (3.1.1) - rubocop (0.52.1) + rubocop (0.54.0) parallel (~> 1.10) - parser (>= 2.4.0.2, < 3.0) + parser (>= 2.5) powerpack (~> 0.1) rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) @@ -460,9 +460,9 @@ GEM thread_safe (0.3.6) thread_safe (0.3.6-java) tilt (2.0.8) - turbolinks (5.0.1) - turbolinks-source (~> 5) - turbolinks-source (5.0.3) + turbolinks (5.1.1) + turbolinks-source (~> 5.1) + turbolinks-source (5.1.0) tzinfo (1.2.3) thread_safe (~> 0.1) tzinfo-data (1.2017.2) @@ -470,7 +470,7 @@ GEM uber (0.1.0) uglifier (3.2.0) execjs (>= 0.3.0, < 3) - unicode-display_width (1.3.0) + unicode-display_width (1.3.2) useragent (0.16.8) vegas (0.1.11) rack (>= 1.0.0) @@ -484,8 +484,8 @@ GEM websocket-driver (0.6.5-java) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) - xpath (2.1.0) - nokogiri (~> 1.3) + xpath (3.0.0) + nokogiri (~> 1.8) PLATFORMS java @@ -506,11 +506,11 @@ DEPENDENCIES blade-sauce_labs_plugin bootsnap (>= 1.1.0) byebug - capybara (>= 2.15, < 4.0) + capybara (>= 2.15) chromedriver-helper coffee-rails connection_pool - dalli (< 2.7.7) + dalli delayed_job delayed_job_active_record google-cloud-storage (~> 1.8) @@ -1,48 +1,55 @@ # Welcome to Rails +## What's Rails + Rails is a web-application framework that includes everything needed to create database-backed web applications according to the [Model-View-Controller (MVC)](http://en.wikipedia.org/wiki/Model-view-controller) pattern. Understanding the MVC pattern is key to understanding Rails. MVC divides your -application into three layers, each with a specific responsibility. +application into three layers: Model, View, and Controller, each with a specific responsibility. + +## Model layer -The _Model layer_ represents your domain model (such as Account, Product, -Person, Post, etc.) and encapsulates the business logic that is specific to +The _**Model layer**_ represents the domain model (such as Account, Product, +Person, Post, etc.) and encapsulates the business logic specific to your application. In Rails, database-backed model classes are derived from -`ActiveRecord::Base`. Active Record allows you to present the data from +`ActiveRecord::Base`. [Active Record](activerecord/README.rdoc) allows you to present the data from database rows as objects and embellish these data objects with business logic -methods. You can read more about Active Record in its [README](activerecord/README.rdoc). +methods. Although most Rails models are backed by a database, models can also be ordinary Ruby classes, or Ruby classes that implement a set of interfaces as provided by -the Active Model module. You can read more about Active Model in its [README](activemodel/README.rdoc). +the [Active Model](activemodel/README.rdoc) module. + +## Controller layer -The _Controller layer_ is responsible for handling incoming HTTP requests and +The _**Controller layer**_ is responsible for handling incoming HTTP requests and providing a suitable response. Usually this means returning HTML, but Rails controllers can also generate XML, JSON, PDFs, mobile-specific views, and more. Controllers load and manipulate models, and render view templates in order to generate the appropriate HTTP response. In Rails, incoming requests are routed by Action Dispatch to an appropriate controller, and controller classes are derived from `ActionController::Base`. Action Dispatch and Action Controller -are bundled together in Action Pack. You can read more about Action Pack in its -[README](actionpack/README.rdoc). +are bundled together in [Action Pack](actionpack/README.rdoc). -The _View layer_ is composed of "templates" that are responsible for providing +## View layer + +The _**View layer**_ is composed of "templates" that are responsible for providing appropriate representations of your application's resources. Templates can come in a variety of formats, but most view templates are HTML with embedded Ruby code (ERB files). Views are typically rendered to generate a controller response, -or to generate the body of an email. In Rails, View generation is handled by Action View. -You can read more about Action View in its [README](actionview/README.rdoc). +or to generate the body of an email. In Rails, View generation is handled by [Action View](actionview/README.rdoc). + +## Frameworks and libraries -Active Record, Active Model, Action Pack, and Action View can each be used independently outside Rails. -In addition to that, Rails also comes with Action Mailer ([README](actionmailer/README.rdoc)), a library -to generate and send emails; Active Job ([README](activejob/README.md)), a +[Active Record](activerecord/README.rdoc), [Active Model](activemodel/README.rdoc), [Action Pack](actionpack/README.rdoc), and [Action View](actionview/README.rdoc) can each be used independently outside Rails. +In addition to that, Rails also comes with [Action Mailer](actionmailer/README.rdoc), a library +to generate and send emails; [Active Job](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 -integrate WebSockets with a Rails application; -Active Storage ([README](activestorage/README.md)), a library to attach cloud +backends; [Action Cable](actioncable/README.md), a framework to +integrate WebSockets with a Rails application; [Active Storage](activestorage/README.md), a library to attach cloud and local files to Rails applications; -and Active Support ([README](activesupport/README.rdoc)), a collection +and [Active Support](activesupport/README.rdoc), a collection of utility classes and standard library extensions that are useful for Rails, and may also be used independently outside Rails. @@ -65,7 +72,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: +4. Go to `http://localhost:3000` and you'll see: "Yay! You’re on Rails!" 5. Follow the guidelines to start developing your application. You may find diff --git a/RELEASING_RAILS.md b/RELEASING_RAILS.md index c61fe7eb13..60434451fe 100644 --- a/RELEASING_RAILS.md +++ b/RELEASING_RAILS.md @@ -14,14 +14,14 @@ Today is mostly coordination tasks. Here are the things you must do today: Do not release with a Red CI. You can find the CI status here: ``` -http://travis-ci.org/rails/rails +https://travis-ci.org/rails/rails ``` ### Is Sam Ruby happy? If not, make him happy. 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)) +([Agile Web Development with Rails](https://pragprog.com/book/rails51/agile-web-development-with-rails-51)) all work. These are valuable system tests for Rails. You can check the status of these tests here: diff --git a/actioncable/lib/action_cable/channel/broadcasting.rb b/actioncable/lib/action_cable/channel/broadcasting.rb index acc791817b..9a96720f4a 100644 --- a/actioncable/lib/action_cable/channel/broadcasting.rb +++ b/actioncable/lib/action_cable/channel/broadcasting.rb @@ -9,7 +9,7 @@ module ActionCable delegate :broadcasting_for, to: :class - class_methods do + module ClassMethods # Broadcast a hash to a unique broadcasting for this <tt>model</tt> in this channel. def broadcast_to(model, message) ActionCable.server.broadcast(broadcasting_for([ channel_name, model ]), message) diff --git a/actioncable/lib/action_cable/channel/callbacks.rb b/actioncable/lib/action_cable/channel/callbacks.rb index 4223c0d996..e4cb19b26a 100644 --- a/actioncable/lib/action_cable/channel/callbacks.rb +++ b/actioncable/lib/action_cable/channel/callbacks.rb @@ -13,7 +13,7 @@ module ActionCable define_callbacks :unsubscribe end - class_methods do + module ClassMethods def before_subscribe(*methods, &block) set_callback(:subscribe, :before, *methods, &block) end diff --git a/actioncable/lib/action_cable/channel/naming.rb b/actioncable/lib/action_cable/channel/naming.rb index 03a5dcd3a0..9c324a2a53 100644 --- a/actioncable/lib/action_cable/channel/naming.rb +++ b/actioncable/lib/action_cable/channel/naming.rb @@ -5,7 +5,7 @@ module ActionCable module Naming extend ActiveSupport::Concern - class_methods do + module ClassMethods # Returns the name of the channel, underscored, without the <tt>Channel</tt> ending. # If the channel is in a namespace, then the namespaces are represented by single # colon separators in the channel name. diff --git a/actioncable/lib/action_cable/connection/identification.rb b/actioncable/lib/action_cable/connection/identification.rb index 4b5f9ca115..cc544685dd 100644 --- a/actioncable/lib/action_cable/connection/identification.rb +++ b/actioncable/lib/action_cable/connection/identification.rb @@ -11,7 +11,7 @@ module ActionCable class_attribute :identifiers, default: Set.new end - class_methods do + module ClassMethods # 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. # diff --git a/actioncable/lib/rails/generators/channel/channel_generator.rb b/actioncable/lib/rails/generators/channel/channel_generator.rb index c3528370c6..427eef1f55 100644 --- a/actioncable/lib/rails/generators/channel/channel_generator.rb +++ b/actioncable/lib/rails/generators/channel/channel_generator.rb @@ -27,7 +27,7 @@ module Rails private def file_name - @_file_name ||= super.gsub(/_channel/i, "") + @_file_name ||= super.sub(/_channel\z/i, "") end # FIXME: Change these files to symlinks once RubyGems 2.5.0 is required. diff --git a/actioncable/test/channel/stream_test.rb b/actioncable/test/channel/stream_test.rb index e9e8849637..df9d44d8dd 100644 --- a/actioncable/test/channel/stream_test.rb +++ b/actioncable/test/channel/stream_test.rb @@ -200,7 +200,7 @@ module ActionCable::StreamTests end def receive(connection, command:, identifiers:, channel: "ActionCable::StreamTests::ChatChannel") - identifier = JSON.generate(channel: channel, **identifiers) + identifier = JSON.generate(identifiers.merge(channel: channel)) connection.dispatch_websocket_message JSON.generate(command: command, identifier: identifier) wait_for_async end diff --git a/actioncable/test/connection/base_test.rb b/actioncable/test/connection/base_test.rb index c69d4d0b36..ac10b3d4d2 100644 --- a/actioncable/test/connection/base_test.rb +++ b/actioncable/test/connection/base_test.rb @@ -83,7 +83,7 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase connection.subscriptions.expects(:unsubscribe_from_all) connection.send :handle_close - assert ! connection.connected + assert_not connection.connected assert_equal [], @server.connections end end diff --git a/actioncable/test/test_helper.rb b/actioncable/test/test_helper.rb index 2a4611fb37..2f186b7193 100644 --- a/actioncable/test/test_helper.rb +++ b/actioncable/test/test_helper.rb @@ -4,7 +4,7 @@ require "action_cable" require "active_support/testing/autorun" require "puma" -require "mocha/setup" +require "mocha/minitest" require "rack/mock" begin diff --git a/actionmailer/lib/action_mailer/test_helper.rb b/actionmailer/lib/action_mailer/test_helper.rb index e9ddef3b94..6906472660 100644 --- a/actionmailer/lib/action_mailer/test_helper.rb +++ b/actionmailer/lib/action_mailer/test_helper.rb @@ -115,11 +115,11 @@ module ActionMailer # end # end # - # If `args` is provided as a Hash, a parameterized email is matched. + # If +args+ is provided as a Hash, a parameterized email is matched. # # def test_parameterized_email # assert_enqueued_email_with ContactMailer, :welcome, - # args: {email: 'user@example.com} do + # args: {email: 'user@example.com'} do # ContactMailer.with(email: 'user@example.com').welcome.deliver_later # end # end diff --git a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb index 97eac30db1..c37a74c762 100644 --- a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb +++ b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb @@ -23,7 +23,7 @@ module Rails private def file_name # :doc: - @_file_name ||= super.gsub(/_mailer/i, "") + @_file_name ||= super.sub(/_mailer\z/i, "") end def application_mailer_file_name diff --git a/actionmailer/test/caching_test.rb b/actionmailer/test/caching_test.rb index 574840c30e..22f310f39f 100644 --- a/actionmailer/test/caching_test.rb +++ b/actionmailer/test/caching_test.rb @@ -40,14 +40,14 @@ class FragmentCachingTest < BaseCachingTest def test_fragment_exist_with_caching_enabled @store.write("views/name", "value") assert @mailer.fragment_exist?("name") - assert !@mailer.fragment_exist?("other_name") + assert_not @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") + assert_not @mailer.fragment_exist?("name") + assert_not @mailer.fragment_exist?("other_name") end def test_write_fragment_with_caching_enabled @@ -90,7 +90,7 @@ class FragmentCachingTest < BaseCachingTest buffer = "generated till now -> ".html_safe buffer << view_context.send(:fragment_for, "expensive") { fragment_computed = true } - assert !fragment_computed + assert_not fragment_computed assert_equal "generated till now -> fragment content", buffer end diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 61451dd673..a13d9e1078 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,41 @@ +* Introduce a new error page to when the implict render page is accessed in the browser. + + Now instead of showing an error page that with exception and backtraces we now show only + one informative page. + + *Vinicius Stock* + +* Introduce ActionDispatch::DebugExceptions.register_interceptor + + Exception aware plugin authors can use the newly introduced + `.register_interceptor` method to get the processed exception, instead of + monkey patching DebugExceptions. + + ActionDispatch::DebugExceptions.register_interceptor do |request, exception| + HypoteticalPlugin.capture_exception(request, exception) + end + + *Genadi Samokovarov* + +* Output only one Content-Security-Policy nonce header value per request. + + Fixes #32597. + + *Andrey Novikov*, *Andrew White* + +* Move default headers configuration into their own module that can be included in controllers. + + *Kevin Deisz* + +* Add method `dig` to `session`. + + *claudiob*, *Takumi Shotoku* + +* Controller level `force_ssl` has been deprecated in favor of + `config.force_ssl`. + + *Derek Prior* + * Rails 6 requires Ruby 2.4.1 or newer. *Jeremy Daer* diff --git a/actionpack/Rakefile b/actionpack/Rakefile index 4dd7c59ce8..e99eb1723a 100644 --- a/actionpack/Rakefile +++ b/actionpack/Rakefile @@ -28,7 +28,7 @@ namespace :test do end task :lines do - load File.expand_path("..", __dir__) + "/tools/line_statistics" + load File.expand_path("../tools/line_statistics", __dir__) files = FileList["lib/**/*.rb"] CodeTools::LineStatistics.new(files).print_loc end diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index f43784f9f2..29d61c3ceb 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -25,6 +25,7 @@ module ActionController autoload :ContentSecurityPolicy autoload :Cookies autoload :DataStreaming + autoload :DefaultHeaders autoload :EtagWithTemplateDigest autoload :EtagWithFlash autoload :Flash diff --git a/actionpack/lib/action_controller/api.rb b/actionpack/lib/action_controller/api.rb index b192e496de..93ffff1bd6 100644 --- a/actionpack/lib/action_controller/api.rb +++ b/actionpack/lib/action_controller/api.rb @@ -122,6 +122,7 @@ module ActionController ForceSSL, DataStreaming, + DefaultHeaders, # Before callbacks should also be executed as early as possible, so # also include them at the bottom. diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 204a3d400c..3378d6db0f 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -232,6 +232,7 @@ module ActionController HttpAuthentication::Basic::ControllerMethods, HttpAuthentication::Digest::ControllerMethods, HttpAuthentication::Token::ControllerMethods, + DefaultHeaders, # Before callbacks should also be executed as early as possible, so # also include them at the bottom. @@ -264,12 +265,6 @@ module ActionController PROTECTED_IVARS end - def self.make_response!(request) - ActionDispatch::Response.create.tap do |res| - res.request = request - end - end - ActiveSupport.run_load_hooks(:action_controller_base, self) ActiveSupport.run_load_hooks(:action_controller, self) end diff --git a/actionpack/lib/action_controller/metal/default_headers.rb b/actionpack/lib/action_controller/metal/default_headers.rb new file mode 100644 index 0000000000..eef0602fcd --- /dev/null +++ b/actionpack/lib/action_controller/metal/default_headers.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module ActionController + # Allows configuring default headers that will be automatically merged into + # each response. + module DefaultHeaders + extend ActiveSupport::Concern + + module ClassMethods + def make_response!(request) + ActionDispatch::Response.create.tap do |res| + res.request = request + end + end + end + end +end diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb index a65857d6ef..ce9eb209fe 100644 --- a/actionpack/lib/action_controller/metal/exceptions.rb +++ b/actionpack/lib/action_controller/metal/exceptions.rb @@ -22,7 +22,7 @@ module ActionController end end - class ActionController::UrlGenerationError < ActionControllerError #:nodoc: + class UrlGenerationError < ActionControllerError #:nodoc: end class MethodNotAllowed < ActionControllerError #:nodoc: @@ -50,4 +50,7 @@ module ActionController class UnknownFormat < ActionControllerError #:nodoc: end + + class MissingExactTemplate < UnknownFormat #:nodoc: + end end diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index 7de500d119..8d53a30e93 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -4,18 +4,10 @@ require "active_support/core_ext/hash/except" require "active_support/core_ext/hash/slice" module ActionController - # This module provides a method which will redirect the browser to use the secured HTTPS - # protocol. This will ensure that users' sensitive information will be - # 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 is transferred via HTTPS, and will - # prevent the user from getting their session hijacked when accessing the - # site over unsecured HTTP protocol. - module ForceSSL + # This module is deprecated in favor of +config.force_ssl+ in your environment + # config file. This will ensure all communication to non-whitelisted endpoints + # served by your application occurs over HTTPS. + module ForceSSL # :nodoc: extend ActiveSupport::Concern include AbstractController::Callbacks @@ -23,45 +15,17 @@ module ActionController URL_OPTIONS = [:protocol, :host, :domain, :subdomain, :port, :path] REDIRECT_OPTIONS = [:status, :flash, :alert, :notice] - module ClassMethods - # Force the request to this particular controller or specified actions to be - # through the HTTPS protocol. - # - # If you need to disable this for any reason (e.g. development) then you can use - # an +:if+ or +:unless+ condition. - # - # class AccountsController < ApplicationController - # force_ssl if: :ssl_configured? - # - # def ssl_configured? - # !Rails.env.development? - # end - # end - # - # ==== URL Options - # You can pass any of the following options to affect the redirect URL - # * <tt>host</tt> - Redirect to a different host name - # * <tt>subdomain</tt> - Redirect to a different subdomain - # * <tt>domain</tt> - Redirect to a different domain - # * <tt>port</tt> - Redirect to a non-standard port - # * <tt>path</tt> - Redirect to a different path - # - # ==== Redirect Options - # You can pass any of the following options to affect the redirect status and response - # * <tt>status</tt> - Redirect with a custom status (default is 301 Moved Permanently) - # * <tt>flash</tt> - Set a flash message when redirecting - # * <tt>alert</tt> - Set an alert message when redirecting - # * <tt>notice</tt> - Set a notice message when redirecting - # - # ==== Action Options - # You can pass any of the following options to affect the before_action callback - # * <tt>only</tt> - The callback should be run only for this action - # * <tt>except</tt> - The callback should be run for all actions except this action - # * <tt>if</tt> - A symbol naming an instance method or a proc; the - # callback will be called only when it returns a true value. - # * <tt>unless</tt> - A symbol naming an instance method or a proc; the - # callback will be called only when it returns a false value. + module ClassMethods # :nodoc: def force_ssl(options = {}) + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + Controller-level `force_ssl` is deprecated and will be removed from + Rails 6.1. Please enable `config.force_ssl` in your environment + configuration to enable the ActionDispatch::SSL middleware to more + fully enforce that your application communicate over HTTPS. If needed, + you can use `config.ssl_options` to exempt matching endpoints from + being redirected to HTTPS. + MESSAGE + action_options = options.slice(*ACTION_OPTIONS) redirect_options = options.except(*ACTION_OPTIONS) before_action(action_options) do @@ -70,11 +34,6 @@ module ActionController end end - # Redirect the existing request to use the HTTPS protocol. - # - # ==== Parameters - # * <tt>host_or_options</tt> - Either a host name or any of the URL and - # redirect options available to the <tt>force_ssl</tt> method. def force_ssl_redirect(host_or_options = nil) unless request.ssl? options = { diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb index ac0c127cdc..d3bb58f48b 100644 --- a/actionpack/lib/action_controller/metal/implicit_render.rb +++ b/actionpack/lib/action_controller/metal/implicit_render.rb @@ -41,18 +41,8 @@ module ActionController 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 + message = "#{self.class.name}\##{action_name} is missing a template for request formats: #{request.formats.map(&:to_s).join(',')}" + raise ActionController::MissingExactTemplate, message else logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger super diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 94092de96c..fc9cf8aaff 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -417,7 +417,7 @@ module ActionController #:nodoc: NULL_ORIGIN_MESSAGE = <<~MSG The browser returned a 'null' origin for a request with origin-based forgery protection turned on. This usually - means you have the 'no-referrer' Referrer-Policy header enabled, or that you the request came from a site that + means you have the 'no-referrer' Referrer-Policy header enabled, or that the request came from a site that refused to give its origin. This makes it impossible for Rails to verify the source of the requests. Likely the best solution is to change your referrer policy to something less strict like same-origin or strict-same-origin. If you cannot change the referrer policy, you can disable origin checking with the diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 75ca282804..5a06bf86e3 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -374,7 +374,7 @@ module ActionController # Person.new(params) # => #<Person id: nil, name: "Francesco"> def permit! each_pair do |key, value| - Array.wrap(value).each do |v| + Array.wrap(value).flatten.each do |v| v.permit! if v.respond_to? :permit! end end @@ -590,7 +590,8 @@ module ActionController # params2 = ActionController::Parameters.new(foo: [10, 11, 12]) # params2.dig(:foo, 1) # => 11 def dig(*keys) - convert_value_to_parameters(@parameters.dig(*keys)) + convert_hashes_to_parameters(keys.first, @parameters[keys.first]) + @parameters.dig(*keys) end # Returns a new <tt>ActionController::Parameters</tt> instance that diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 798d142755..8f2a7e2b5f 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -460,10 +460,6 @@ module ActionController def process(action, method: "GET", params: {}, session: nil, body: nil, flash: {}, format: nil, xhr: false, as: nil) check_required_ivars - if body - @request.set_header "RAW_POST_DATA", body - end - http_method = method.to_s.upcase @html_document = nil @@ -478,6 +474,10 @@ module ActionController @response.request = @request @controller.recycle! + if body + @request.set_header "RAW_POST_DATA", body + end + @request.set_header "REQUEST_METHOD", http_method if as @@ -604,6 +604,7 @@ module ActionController env.delete "action_dispatch.request.query_parameters" env.delete "action_dispatch.request.request_parameters" env["rack.input"] = StringIO.new + env.delete "RAW_POST_DATA" env end diff --git a/actionpack/lib/action_dispatch/http/content_security_policy.rb b/actionpack/lib/action_dispatch/http/content_security_policy.rb index a3407c9698..17e72b46ff 100644 --- a/actionpack/lib/action_dispatch/http/content_security_policy.rb +++ b/actionpack/lib/action_dispatch/http/content_security_policy.rb @@ -21,13 +21,8 @@ module ActionDispatch #:nodoc: return response if policy_present?(headers) if policy = request.content_security_policy - if policy.directives["script-src"] - if nonce = request.content_security_policy_nonce - policy.directives["script-src"] << "'nonce-#{nonce}'" - end - end - - headers[header_name(request)] = policy.build(request.controller_instance) + nonce = request.content_security_policy_nonce + headers[header_name(request)] = policy.build(request.controller_instance, nonce) end response @@ -113,7 +108,9 @@ module ActionDispatch #:nodoc: blob: "blob:", filesystem: "filesystem:", report_sample: "'report-sample'", - strict_dynamic: "'strict-dynamic'" + strict_dynamic: "'strict-dynamic'", + ws: "ws:", + wss: "wss:" }.freeze DIRECTIVES = { @@ -134,7 +131,9 @@ module ActionDispatch #:nodoc: worker_src: "worker-src" }.freeze - private_constant :MAPPINGS, :DIRECTIVES + NONCE_DIRECTIVES = %w[script-src].freeze + + private_constant :MAPPINGS, :DIRECTIVES, :NONCE_DIRECTIVES attr_reader :directives @@ -203,8 +202,8 @@ module ActionDispatch #:nodoc: end end - def build(context = nil) - build_directives(context).compact.join("; ") + def build(context = nil, nonce = nil) + build_directives(context, nonce).compact.join("; ") end private @@ -227,10 +226,14 @@ module ActionDispatch #:nodoc: end end - def build_directives(context) + def build_directives(context, nonce) @directives.map do |directive, sources| if sources.is_a?(Array) - "#{directive} #{build_directive(sources, context).join(' ')}" + if nonce && nonce_directive?(directive) + "#{directive} #{build_directive(sources, context).join(' ')} 'nonce-#{nonce}'" + else + "#{directive} #{build_directive(sources, context).join(' ')}" + end elsif sources directive else @@ -259,5 +262,9 @@ module ActionDispatch #:nodoc: raise RuntimeError, "Unexpected content security policy source: #{source.inspect}" end end + + def nonce_directive?(directive) + NONCE_DIRECTIVES.include?(directive) + end end end diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index ec86b8bc47..ec012ad02d 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -9,8 +9,8 @@ module ActionDispatch # 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, where the value or the key can be replaced using - # String#replace or similar method. + # sub-hashes are passed to it, where the value or the key can be replaced using + # String#replace or similar methods. # # env["action_dispatch.parameter_filter"] = [:password] # => replaces the value to all keys matching /password/i with "[FILTERED]" diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index 511306eb0e..33edad8bd9 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -50,10 +50,18 @@ module ActionDispatch end end - def initialize(app, routes_app = nil, response_format = :default) + cattr_reader :interceptors, instance_accessor: false, default: [] + + def self.register_interceptor(object = nil, &block) + interceptor = object || block + interceptors << interceptor + end + + def initialize(app, routes_app = nil, response_format = :default, interceptors = self.class.interceptors) @app = app @routes_app = routes_app @response_format = response_format + @interceptors = interceptors end def call(env) @@ -67,12 +75,26 @@ module ActionDispatch response rescue Exception => exception + invoke_interceptors(request, exception) raise exception unless request.show_exceptions? render_exception(request, exception) end private + def invoke_interceptors(request, exception) + backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner") + wrapper = ExceptionWrapper.new(backtrace_cleaner, exception) + + @interceptors.each do |interceptor| + begin + interceptor.call(request, exception) + rescue Exception + log_error(request, wrapper) + end + end + end + def render_exception(request, exception) backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner") wrapper = ExceptionWrapper.new(backtrace_cleaner, exception) diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index d1b4508378..f05c69137b 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -12,6 +12,7 @@ module ActionDispatch "ActionController::UnknownHttpMethod" => :method_not_allowed, "ActionController::NotImplemented" => :not_implemented, "ActionController::UnknownFormat" => :not_acceptable, + "ActionController::MissingExactTemplate" => :not_acceptable, "ActionController::InvalidAuthenticityToken" => :unprocessable_entity, "ActionController::InvalidCrossOriginRequest" => :unprocessable_entity, "ActionDispatch::Http::Parameters::ParseError" => :bad_request, @@ -22,11 +23,12 @@ module ActionDispatch ) cattr_accessor :rescue_templates, default: Hash.new("diagnostics").merge!( - "ActionView::MissingTemplate" => "missing_template", - "ActionController::RoutingError" => "routing_error", - "AbstractController::ActionNotFound" => "unknown_action", - "ActiveRecord::StatementInvalid" => "invalid_statement", - "ActionView::Template::Error" => "template_error" + "ActionView::MissingTemplate" => "missing_template", + "ActionController::RoutingError" => "routing_error", + "AbstractController::ActionNotFound" => "unknown_action", + "ActiveRecord::StatementInvalid" => "invalid_statement", + "ActionView::Template::Error" => "template_error", + "ActionController::MissingExactTemplate" => "missing_exact_template", ) attr_reader :backtrace_cleaner, :exception, :line_number, :file diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index 3e11846778..fd05eec172 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -73,7 +73,7 @@ module ActionDispatch end end - def reset_session # :nodoc + def reset_session # :nodoc: super self.flash = nil end diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index acd999444a..8130bfe2e7 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -69,7 +69,7 @@ module ActionDispatch headers["Vary"] = "Accept-Encoding" if gzip_path - return [status, headers, body] + [status, headers, body] ensure request.path_info = path end diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb new file mode 100644 index 0000000000..3621ea81de --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb @@ -0,0 +1,19 @@ +<header> + <h1>No template for interactive request</h1> +</header> + +<div id="container"> + <h3><%= h @exception.message %></h3> + + <p class="summary"> + <strong>NOTE!</strong><br> + Unless told otherwise, Rails expects an action to render a template with the same name,<br> + contained in a folder named after its controller. + + If this controller is an API responding with 204 (No Content), <br> + which does not require a template, + then this error will occur when trying to access it via browser,<br> + since we expect an HTML template + to be rendered for such requests. If that's the case, carry on. + </p> +</div> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb new file mode 100644 index 0000000000..fcdbe6069d --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb @@ -0,0 +1,3 @@ +Missing exact template + +<%= @exception.message %> diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb index 000847e193..bc5e0670e0 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -93,6 +93,14 @@ module ActionDispatch @delegate[key.to_s] end + # Returns the nested value specified by the sequence of keys, returning + # +nil+ if any intermediate step is +nil+. + def dig(*keys) + load_for_read! + keys = keys.map.with_index { |key, i| i.zero? ? key.to_s : key } + @delegate.dig(*keys) + end + # Returns true if the session has the given key or false. def has_key?(key) load_for_read! diff --git a/actionpack/lib/action_dispatch/routing/endpoint.rb b/actionpack/lib/action_dispatch/routing/endpoint.rb index 24dced1efd..28bb20d688 100644 --- a/actionpack/lib/action_dispatch/routing/endpoint.rb +++ b/actionpack/lib/action_dispatch/routing/endpoint.rb @@ -3,12 +3,15 @@ module ActionDispatch module Routing class Endpoint # :nodoc: - def dispatcher?; false; end - def redirect?; false; end - def engine?; rack_app.respond_to?(:routes); end - def matches?(req); true; end - def app; self; end - def rack_app; app; end + def dispatcher?; false; end + def redirect?; false; end + def matches?(req); true; end + def app; self; end + def rack_app; app; end + + def engine? + rack_app.is_a?(Class) && rack_app < Rails::Engine + end end end end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index a29a5a04ef..1134279a7f 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -35,7 +35,7 @@ module ActionDispatch if @raise_on_name_error raise else - return [404, { "X-Cascade" => "pass" }, []] + [404, { "X-Cascade" => "pass" }, []] end end diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index 865d10f886..1a31c7dbb8 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -204,7 +204,7 @@ module ActionDispatch # end # # This maintains the context of the original caller on - # whether to return a path or full url, e.g: + # whether to return a path or full URL, e.g: # # threadable_path(threadable) # => "/buckets/1" # threadable_url(threadable) # => "http://example.com/buckets/1" diff --git a/actionpack/lib/action_dispatch/system_test_case.rb b/actionpack/lib/action_dispatch/system_test_case.rb index d06ff0c804..c74c0ccced 100644 --- a/actionpack/lib/action_dispatch/system_test_case.rb +++ b/actionpack/lib/action_dispatch/system_test_case.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -gem "capybara", ">= 2.15", "< 4.0" +gem "capybara", ">= 2.15" require "capybara/dsl" require "capybara/minitest" diff --git a/actionpack/lib/action_dispatch/system_testing/browser.rb b/actionpack/lib/action_dispatch/system_testing/browser.rb index 10e6888ab3..1b0bce6b9e 100644 --- a/actionpack/lib/action_dispatch/system_testing/browser.rb +++ b/actionpack/lib/action_dispatch/system_testing/browser.rb @@ -33,7 +33,7 @@ module ActionDispatch def headless_chrome_browser_options options = Selenium::WebDriver::Chrome::Options.new options.args << "--headless" - options.args << "--disable-gpu" + options.args << "--disable-gpu" if Gem.win_platform? options end diff --git a/actionpack/test/abstract/callbacks_test.rb b/actionpack/test/abstract/callbacks_test.rb index fdc09bd951..4512ea27b3 100644 --- a/actionpack/test/abstract/callbacks_test.rb +++ b/actionpack/test/abstract/callbacks_test.rb @@ -154,7 +154,7 @@ module AbstractController test "when :except is specified, an after action is not triggered on that action" do @controller.process(:index) - assert !@controller.instance_variable_defined?("@authenticated") + assert_not @controller.instance_variable_defined?("@authenticated") end end @@ -198,7 +198,7 @@ module AbstractController test "when :except is specified with an array, an after action is not triggered on that action" do @controller.process(:index) - assert !@controller.instance_variable_defined?("@authenticated") + assert_not @controller.instance_variable_defined?("@authenticated") end end diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index 504c77b8ef..763df3a776 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -290,13 +290,13 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase def test_template_objects_exist process :assign_this - assert !@controller.instance_variable_defined?(:"@hi") + assert_not @controller.instance_variable_defined?(:"@hi") assert @controller.instance_variable_get(:"@howdy") end def test_template_objects_missing process :nothing - assert !@controller.instance_variable_defined?(:@howdy) + assert_not @controller.instance_variable_defined?(:@howdy) end def test_empty_flash @@ -366,7 +366,7 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase process :redirect_external assert_predicate @response, :redirect? assert_match(/rubyonrails/, @response.redirect_url) - assert !/perloffrails/.match(@response.redirect_url) + assert_no_match(/perloffrails/, @response.redirect_url) end def test_redirection diff --git a/actionpack/test/controller/api/force_ssl_test.rb b/actionpack/test/controller/api/force_ssl_test.rb index 07459c3753..8191578eb0 100644 --- a/actionpack/test/controller/api/force_ssl_test.rb +++ b/actionpack/test/controller/api/force_ssl_test.rb @@ -3,7 +3,9 @@ require "abstract_unit" class ForceSSLApiController < ActionController::API - force_ssl + ActiveSupport::Deprecation.silence do + force_ssl + end def one; end def two diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index 8b596083d5..6fe036dd15 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -94,14 +94,14 @@ class FragmentCachingTest < ActionController::TestCase def test_fragment_exist_with_caching_enabled @store.write("views/name", "value") assert @controller.fragment_exist?("name") - assert !@controller.fragment_exist?("other_name") + assert_not @controller.fragment_exist?("other_name") end def test_fragment_exist_with_caching_disabled @controller.perform_caching = false @store.write("views/name", "value") - assert !@controller.fragment_exist?("name") - assert !@controller.fragment_exist?("other_name") + assert_not @controller.fragment_exist?("name") + assert_not @controller.fragment_exist?("other_name") end def test_write_fragment_with_caching_enabled @@ -144,7 +144,7 @@ class FragmentCachingTest < ActionController::TestCase buffer = "generated till now -> ".html_safe buffer << view_context.send(:fragment_for, "expensive") { fragment_computed = true } - assert !fragment_computed + assert_not fragment_computed assert_equal "generated till now -> fragment content", buffer end @@ -173,6 +173,9 @@ class FunctionalCachingController < CachingController end end + def xml_fragment_cached_with_html_partial + end + def formatted_fragment_cached respond_to do |format| format.html @@ -308,6 +311,11 @@ CACHED @store.read("views/functional_caching/formatted_fragment_cached_with_variant:#{template_digest("functional_caching/formatted_fragment_cached_with_variant")}/fragment") end + def test_fragment_caching_with_html_partials_in_xml + get :xml_fragment_cached_with_html_partial, format: "*/*" + assert_response :success + end + private def template_digest(name) ActionView::Digestor.digest(name: name, finder: @controller.lookup_context) diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb index 2b16a555bb..425a6e25cc 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -787,7 +787,7 @@ class FilterTest < ActionController::TestCase assert_equal %w( ensure_login find_user ), @controller.instance_variable_get(:@ran_filter) test_process(ConditionalSkippingController, "login") - assert !@controller.instance_variable_defined?("@ran_after_action") + assert_not @controller.instance_variable_defined?("@ran_after_action") test_process(ConditionalSkippingController, "change_password") assert_equal %w( clean_up ), @controller.instance_variable_get("@ran_after_action") end diff --git a/actionpack/test/controller/flash_hash_test.rb b/actionpack/test/controller/flash_hash_test.rb index 6c3ac26de1..e3ec5bb7fc 100644 --- a/actionpack/test/controller/flash_hash_test.rb +++ b/actionpack/test/controller/flash_hash_test.rb @@ -44,7 +44,7 @@ module ActionDispatch @hash["foo"] = "bar" @hash.delete "foo" - assert !@hash.key?("foo") + assert_not @hash.key?("foo") assert_nil @hash["foo"] end @@ -53,7 +53,7 @@ module ActionDispatch assert_equal({ "foo" => "bar" }, @hash.to_hash) @hash.to_hash["zomg"] = "aaron" - assert !@hash.key?("zomg") + assert_not @hash.key?("zomg") assert_equal({ "foo" => "bar" }, @hash.to_hash) end diff --git a/actionpack/test/controller/force_ssl_test.rb b/actionpack/test/controller/force_ssl_test.rb index 84ac1fda3c..7f59f6acaf 100644 --- a/actionpack/test/controller/force_ssl_test.rb +++ b/actionpack/test/controller/force_ssl_test.rb @@ -13,19 +13,23 @@ class ForceSSLController < ActionController::Base end class ForceSSLControllerLevel < ForceSSLController - force_ssl + ActiveSupport::Deprecation.silence do + force_ssl + end end class ForceSSLCustomOptions < ForceSSLController - force_ssl host: "secure.example.com", only: :redirect_host - force_ssl port: 8443, only: :redirect_port - force_ssl subdomain: "secure", only: :redirect_subdomain - force_ssl domain: "secure.com", only: :redirect_domain - force_ssl path: "/foo", only: :redirect_path - force_ssl status: :found, only: :redirect_status - force_ssl flash: { message: "Foo, Bar!" }, only: :redirect_flash - force_ssl alert: "Foo, Bar!", only: :redirect_alert - force_ssl notice: "Foo, Bar!", only: :redirect_notice + ActiveSupport::Deprecation.silence do + force_ssl host: "secure.example.com", only: :redirect_host + force_ssl port: 8443, only: :redirect_port + force_ssl subdomain: "secure", only: :redirect_subdomain + force_ssl domain: "secure.com", only: :redirect_domain + force_ssl path: "/foo", only: :redirect_path + force_ssl status: :found, only: :redirect_status + force_ssl flash: { message: "Foo, Bar!" }, only: :redirect_flash + force_ssl alert: "Foo, Bar!", only: :redirect_alert + force_ssl notice: "Foo, Bar!", only: :redirect_notice + end def force_ssl_action render plain: action_name @@ -55,15 +59,21 @@ class ForceSSLCustomOptions < ForceSSLController end class ForceSSLOnlyAction < ForceSSLController - force_ssl only: :cheeseburger + ActiveSupport::Deprecation.silence do + force_ssl only: :cheeseburger + end end class ForceSSLExceptAction < ForceSSLController - force_ssl except: :banana + ActiveSupport::Deprecation.silence do + force_ssl except: :banana + end end class ForceSSLIfCondition < ForceSSLController - force_ssl if: :use_force_ssl? + ActiveSupport::Deprecation.silence do + force_ssl if: :use_force_ssl? + end def use_force_ssl? action_name == "cheeseburger" @@ -71,7 +81,9 @@ class ForceSSLIfCondition < ForceSSLController end class ForceSSLFlash < ForceSSLController - force_ssl except: [:banana, :set_flash, :use_flash] + ActiveSupport::Deprecation.silence do + force_ssl except: [:banana, :set_flash, :use_flash] + end def set_flash flash["that"] = "hello" diff --git a/actionpack/test/controller/http_digest_authentication_test.rb b/actionpack/test/controller/http_digest_authentication_test.rb index 560157dc61..3f211cd60d 100644 --- a/actionpack/test/controller/http_digest_authentication_test.rb +++ b/actionpack/test/controller/http_digest_authentication_test.rb @@ -202,7 +202,7 @@ class HttpDigestAuthenticationTest < ActionController::TestCase test "validate_digest_response should fail with nil returning password_procedure" do @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: nil, password: nil) - assert !ActionController::HttpAuthentication::Digest.validate_digest_response(@request, "SuperSecret") { nil } + assert_not ActionController::HttpAuthentication::Digest.validate_digest_response(@request, "SuperSecret") { nil } end test "authentication request with request-uri ending in '/'" do diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index a685f5868e..9cdf04b886 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -135,7 +135,7 @@ class IntegrationTestTest < ActiveSupport::TestCase session1 = @test.open_session { |sess| } session2 = @test.open_session # implicit session - assert !session1.equal?(session2) + assert_not session1.equal?(session2) end # RSpec mixes Matchers (which has a #method_missing) into @@ -345,7 +345,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest follow_redirect! assert_response :ok - refute_same previous_html_document, html_document + assert_not_same previous_html_document, html_document end end @@ -375,7 +375,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest a = open_session b = open_session - refute_same(a.integration_session, b.integration_session) + assert_not_same(a.integration_session, b.integration_session) end def test_get_with_query_string diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb index f9ffd5f54c..771eccb29b 100644 --- a/actionpack/test/controller/mime/respond_to_test.rb +++ b/actionpack/test/controller/mime/respond_to_test.rb @@ -658,13 +658,13 @@ class RespondToControllerTest < ActionController::TestCase end def test_variant_without_implicit_rendering_from_browser - assert_raises(ActionController::UnknownFormat) do + assert_raises(ActionController::MissingExactTemplate) 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 + assert_raises(ActionController::MissingExactTemplate) do get :variant_without_implicit_template_rendering end end diff --git a/actionpack/test/controller/parameters/accessors_test.rb b/actionpack/test/controller/parameters/accessors_test.rb index 07a897a103..674b2c6266 100644 --- a/actionpack/test/controller/parameters/accessors_test.rb +++ b/actionpack/test/controller/parameters/accessors_test.rb @@ -284,4 +284,12 @@ class ParametersAccessorsTest < ActiveSupport::TestCase value.is_a?(ActionController::Parameters) end end + + test "mutating #dig return value mutates underlying parameters" do + @params.dig(:person, :name)[:first] = "Bill" + assert_equal "Bill", @params.dig(:person, :name, :first) + + @params.dig(:person, :addresses)[0] = { city: "Boston", state: "Massachusetts" } + assert_equal "Boston", @params.dig(:person, :addresses, 0, :city) + end end diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb index 295f3a03ef..34b9ac0ab8 100644 --- a/actionpack/test/controller/parameters/parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -136,7 +136,7 @@ class ParametersPermitTest < ActiveSupport::TestCase test "key: it is not assigned if not present in params" do params = ActionController::Parameters.new(name: "Joe") permitted = params.permit(:id) - assert !permitted.has_key?(:id) + assert_not permitted.has_key?(:id) end test "key to empty array: empty arrays pass" do @@ -309,7 +309,7 @@ class ParametersPermitTest < ActiveSupport::TestCase merged_params = @params.reverse_merge(default_params) assert_equal "1234", merged_params[:id] - refute_predicate merged_params[:person], :empty? + assert_not_predicate merged_params[:person], :empty? end test "#with_defaults is an alias of reverse_merge" do @@ -317,11 +317,11 @@ class ParametersPermitTest < ActiveSupport::TestCase merged_params = @params.with_defaults(default_params) assert_equal "1234", merged_params[:id] - refute_predicate merged_params[:person], :empty? + assert_not_predicate merged_params[:person], :empty? end test "not permitted is sticky beyond reverse_merge" do - refute_predicate @params.reverse_merge(a: "b"), :permitted? + assert_not_predicate @params.reverse_merge(a: "b"), :permitted? end test "permitted is sticky beyond reverse_merge" do @@ -334,7 +334,7 @@ class ParametersPermitTest < ActiveSupport::TestCase @params.reverse_merge!(default_params) assert_equal "1234", @params[:id] - refute_predicate @params[:person], :empty? + assert_not_predicate @params[:person], :empty? end test "#with_defaults! is an alias of reverse_merge!" do @@ -342,7 +342,7 @@ class ParametersPermitTest < ActiveSupport::TestCase @params.with_defaults!(default_params) assert_equal "1234", @params[:id] - refute_predicate @params[:person], :empty? + assert_not_predicate @params[:person], :empty? end test "modifying the parameters" do @@ -353,12 +353,15 @@ class ParametersPermitTest < ActiveSupport::TestCase assert_equal "Jonas", @params[:person][:family][:brother] end - test "permit is recursive" do + test "permit! is recursive" do + @params[:nested_array] = [[{ x: 2, y: 3 }, { x: 21, y: 42 }]] @params.permit! assert_predicate @params, :permitted? assert_predicate @params[:person], :permitted? assert_predicate @params[:person][:name], :permitted? assert_predicate @params[:person][:addresses][0], :permitted? + assert_predicate @params[:nested_array][0][0], :permitted? + assert_predicate @params[:nested_array][0][1], :permitted? end test "permitted takes a default value when Parameters.permit_all_parameters is set" do diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index fc21543049..24c5761e41 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -650,7 +650,7 @@ class ImplicitRenderTest < ActionController::TestCase tests ImplicitRenderTestController def test_implicit_no_content_response_as_browser - assert_raises(ActionController::UnknownFormat) do + assert_raises(ActionController::MissingExactTemplate) do get :empty_action end end diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index 7d4850294d..e66c409786 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -681,6 +681,14 @@ XML assert_equal "baz", @request.filtered_parameters[:foo] end + def test_raw_post_reset_between_post_requests + post :no_op, params: { foo: "bar" } + assert_equal "foo=bar", @request.raw_post + + post :no_op, params: { foo: "baz" } + assert_equal "foo=baz", @request.raw_post + end + def test_path_is_kept_after_the_request get :test_params, params: { id: "foo" } assert_equal "/test_case_test/test/test_params/foo", @request.path @@ -740,6 +748,14 @@ XML assert_equal "application/json", @response.body end + def test_request_format_kwarg_doesnt_mutate_params + params = { foo: "bar" }.freeze + + assert_nothing_raised do + get :test_format, format: "json", params: params + end + end + def test_should_have_knowledge_of_client_side_cookie_state_even_if_they_are_not_set cookies["foo"] = "bar" get :no_op diff --git a/actionpack/test/dispatch/content_security_policy_test.rb b/actionpack/test/dispatch/content_security_policy_test.rb index f133aae865..c4c7f53903 100644 --- a/actionpack/test/dispatch/content_security_policy_test.rb +++ b/actionpack/test/dispatch/content_security_policy_test.rb @@ -51,6 +51,12 @@ class ContentSecurityPolicyTest < ActiveSupport::TestCase @policy.script_src :strict_dynamic assert_equal "script-src 'strict-dynamic'", @policy.build + @policy.script_src :ws + assert_equal "script-src ws:", @policy.build + + @policy.script_src :wss + assert_equal "script-src wss:", @policy.build + @policy.script_src :none, :report_sample assert_equal "script-src 'none' 'report-sample'", @policy.build end @@ -194,7 +200,7 @@ class ContentSecurityPolicyTest < ActiveSupport::TestCase end def test_dynamic_directives - request = Struct.new(:host).new("www.example.com") + request = ActionDispatch::Request.new("HTTP_HOST" => "www.example.com") controller = Struct.new(:request).new(request) @policy.script_src -> { request.host } @@ -203,7 +209,9 @@ class ContentSecurityPolicyTest < ActiveSupport::TestCase def test_mixed_static_and_dynamic_directives @policy.script_src :self, -> { "foo.com" }, "bar.com" - assert_equal "script-src 'self' foo.com bar.com", @policy.build(Object.new) + request = ActionDispatch::Request.new({}) + controller = Struct.new(:request).new(request) + assert_equal "script-src 'self' foo.com bar.com", @policy.build(controller) end def test_invalid_directive_source @@ -235,6 +243,73 @@ class ContentSecurityPolicyTest < ActiveSupport::TestCase end end +class DefaultContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest + class PolicyController < ActionController::Base + def index + head :ok + end + end + + ROUTES = ActionDispatch::Routing::RouteSet.new + ROUTES.draw do + scope module: "default_content_security_policy_integration_test" do + get "/", to: "policy#index" + end + end + + POLICY = ActionDispatch::ContentSecurityPolicy.new do |p| + p.default_src :self + p.script_src :https + end + + class PolicyConfigMiddleware + def initialize(app) + @app = app + end + + def call(env) + env["action_dispatch.content_security_policy"] = POLICY + env["action_dispatch.content_security_policy_nonce_generator"] = proc { "iyhD0Yc0W+c=" } + env["action_dispatch.content_security_policy_report_only"] = false + env["action_dispatch.show_exceptions"] = false + + @app.call(env) + end + end + + APP = build_app(ROUTES) do |middleware| + middleware.use PolicyConfigMiddleware + middleware.use ActionDispatch::ContentSecurityPolicy::Middleware + end + + def app + APP + end + + def test_adds_nonce_to_script_src_content_security_policy_only_once + get "/" + get "/" + assert_policy "default-src 'self'; script-src https: 'nonce-iyhD0Yc0W+c='" + end + + private + + def assert_policy(expected, report_only: false) + assert_response :success + + if report_only + expected_header = "Content-Security-Policy-Report-Only" + unexpected_header = "Content-Security-Policy" + else + expected_header = "Content-Security-Policy" + unexpected_header = "Content-Security-Policy-Report-Only" + end + + assert_nil response.headers[unexpected_header] + assert_equal expected, response.headers[expected_header] + end +end + class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest class PolicyController < ActionController::Base content_security_policy only: :inline do |p| diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 94cff10fe4..aba778fad6 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -65,8 +65,8 @@ class CookieJarTest < ActiveSupport::TestCase end def test_key_methods - assert !request.cookie_jar.key?(:foo) - assert !request.cookie_jar.has_key?("foo") + assert_not request.cookie_jar.key?(:foo) + assert_not request.cookie_jar.has_key?("foo") request.cookie_jar[:foo] = :bar assert request.cookie_jar.key?(:foo) diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb index 60acba0616..045567ff83 100644 --- a/actionpack/test/dispatch/debug_exceptions_test.rb +++ b/actionpack/test/dispatch/debug_exceptions_test.rb @@ -3,6 +3,8 @@ require "abstract_unit" class DebugExceptionsTest < ActionDispatch::IntegrationTest + InterceptedErrorInstance = StandardError.new + class Boomer attr_accessor :closed @@ -36,6 +38,8 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest raise RuntimeError when %r{/method_not_allowed} raise ActionController::MethodNotAllowed + when %r{/intercepted_error} + raise InterceptedErrorInstance when %r{/unknown_http_method} raise ActionController::UnknownHttpMethod when %r{/not_implemented} @@ -76,9 +80,13 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest end end + Interceptor = proc { |request, exception| request.set_header("int", exception) } + BadInterceptor = proc { |request, exception| raise "bad" } RoutesApp = Struct.new(:routes).new(SharedTestRoutes) ProductionApp = ActionDispatch::DebugExceptions.new(Boomer.new(false), RoutesApp) DevelopmentApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp) + InterceptedApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :default, [Interceptor]) + BadInterceptedApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :default, [BadInterceptor]) test "skip diagnosis if not showing detailed exceptions" do @app = ProductionApp @@ -499,4 +507,20 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest end end end + + test "invoke interceptors before rendering" do + @app = InterceptedApp + get "/intercepted_error", headers: { "action_dispatch.show_exceptions" => true } + + assert_equal InterceptedErrorInstance, request.get_header("int") + end + + test "bad interceptors doesn't debug exceptions" do + @app = BadInterceptedApp + + get "/puke", headers: { "action_dispatch.show_exceptions" => true } + + assert_response 500 + assert_match(/puke/, body) + end end diff --git a/actionpack/test/dispatch/executor_test.rb b/actionpack/test/dispatch/executor_test.rb index 8eb6450385..5b8be39b6d 100644 --- a/actionpack/test/dispatch/executor_test.rb +++ b/actionpack/test/dispatch/executor_test.rb @@ -81,7 +81,7 @@ class ExecutorTest < ActiveSupport::TestCase running = false body.close - assert !running + assert_not running end def test_complete_callbacks_are_called_on_close @@ -89,7 +89,7 @@ class ExecutorTest < ActiveSupport::TestCase executor.to_complete { completed = true } body = call_and_return_body - assert !completed + assert_not completed body.close assert completed @@ -116,7 +116,7 @@ class ExecutorTest < ActiveSupport::TestCase call_and_return_body.close assert result - assert !defined?(@in_shared_context) # it's not in the test itself + assert_not defined?(@in_shared_context) # it's not in the test itself end private diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb index 6167ea46df..fa264417e1 100644 --- a/actionpack/test/dispatch/mime_type_test.rb +++ b/actionpack/test/dispatch/mime_type_test.rb @@ -180,8 +180,8 @@ class MimeTypeTest < ActiveSupport::TestCase assert Mime[:js] =~ "text/javascript" assert Mime[:js] =~ "application/javascript" assert Mime[:js] !~ "text/html" - assert !(Mime[:js] !~ "text/javascript") - assert !(Mime[:js] !~ "application/javascript") + assert_not (Mime[:js] !~ "text/javascript") + assert_not (Mime[:js] !~ "application/javascript") assert Mime[:html] =~ "application/xhtml+xml" end end diff --git a/actionpack/test/dispatch/reloader_test.rb b/actionpack/test/dispatch/reloader_test.rb index e529229fae..edc4cd62a3 100644 --- a/actionpack/test/dispatch/reloader_test.rb +++ b/actionpack/test/dispatch/reloader_test.rb @@ -115,7 +115,7 @@ class ReloaderTest < ActiveSupport::TestCase reloader.to_complete { completed = true } body = call_and_return_body - assert !completed + assert_not completed body.close assert completed @@ -129,7 +129,7 @@ class ReloaderTest < ActiveSupport::TestCase prepared = false body.close - assert !prepared + assert_not prepared end def test_complete_callbacks_are_called_on_exceptions diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb index bf5a74e694..74da2fe7d3 100644 --- a/actionpack/test/dispatch/request/session_test.rb +++ b/actionpack/test/dispatch/request/session_test.rb @@ -118,6 +118,18 @@ module ActionDispatch end end + def test_dig + session = Session.create(store, req, {}) + session["one"] = { "two" => "3" } + + assert_equal "3", session.dig("one", "two") + assert_equal "3", session.dig(:one, "two") + + assert_nil session.dig("three", "two") + assert_nil session.dig("one", "three") + assert_nil session.dig("one", :two) + end + private def store Class.new { diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 4c8d528507..6d87314e97 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -191,7 +191,7 @@ class ResponseTest < ActiveSupport::TestCase test "does not include Status header" do @response.status = "200 OK" _, headers, _ = @response.to_a - assert !headers.has_key?("Status") + assert_not headers.has_key?("Status") end test "response code" do diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index fe314e26b1..dd6adcbfd1 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -3166,7 +3166,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end - assert !respond_to?(:routes_no_collision_path) + assert_not respond_to?(:routes_no_collision_path) end def test_controller_name_with_leading_slash_raise_error diff --git a/actionpack/test/dispatch/system_testing/server_test.rb b/actionpack/test/dispatch/system_testing/server_test.rb index 95e411faf4..740e90a4da 100644 --- a/actionpack/test/dispatch/system_testing/server_test.rb +++ b/actionpack/test/dispatch/system_testing/server_test.rb @@ -17,7 +17,7 @@ class ServerTest < ActiveSupport::TestCase test "server is changed from `default` to `puma`" do Capybara.server = :default ActionDispatch::SystemTesting::Server.new.run - refute_equal Capybara.server, Capybara.servers[:default] + assert_not_equal Capybara.server, Capybara.servers[:default] end test "server is not changed to `puma` when is different than default" do diff --git a/actionpack/test/fixtures/functional_caching/_formatted_partial.html.erb b/actionpack/test/fixtures/functional_caching/_formatted_partial.html.erb new file mode 100644 index 0000000000..aad73c0d6b --- /dev/null +++ b/actionpack/test/fixtures/functional_caching/_formatted_partial.html.erb @@ -0,0 +1 @@ +<p>Hello!</p> diff --git a/actionpack/test/fixtures/functional_caching/xml_fragment_cached_with_html_partial.xml.builder b/actionpack/test/fixtures/functional_caching/xml_fragment_cached_with_html_partial.xml.builder new file mode 100644 index 0000000000..2bdda3af18 --- /dev/null +++ b/actionpack/test/fixtures/functional_caching/xml_fragment_cached_with_html_partial.xml.builder @@ -0,0 +1,5 @@ +cache do + xml.title "Hello!" +end + +xml.body cdata_section(render("formatted_partial")) diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index f44f03f40d..2c1ca12043 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,37 @@ +* Fix JavaScript views rendering does not work with Firefox when using + Content Security Policy. + + Fixes #32577. + + *Yuji Yaginuma* + +* Add the `nonce: true` option for `javascript_include_tag` helper to + support automatic nonce generation for Content Security Policy. + Works the same way as `javascript_tag nonce: true` does. + + *Yaroslav Markin* + +* Remove `ActionView::Helpers::RecordTagHelper`. + + *Yoshiyuki Hirano* + +* Disable `ActionView::Template` finalizers in test environment. + + Template finalization can be expensive in large view test suites. + Add a configuration option, + `action_view.finalize_compiled_template_methods`, and turn it off in + the test environment. + + *Simon Coffey* + +* Extract the `confirm` call in its own, overridable method in `rails_ujs`. + Example : + Rails.confirm = function(message, element) { + return (my_bootstrap_modal_confirm(message)); + } + + *Mathieu Mahé* + * Enable select tag helper to mark `prompt` option as `selected` and/or `disabled` for `required` field. Example: diff --git a/actionview/Rakefile b/actionview/Rakefile index 8650f541b0..bdfd96c141 100644 --- a/actionview/Rakefile +++ b/actionview/Rakefile @@ -29,6 +29,7 @@ namespace :test do t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end + desc "Run tests for rails-ujs" task :ujs do begin Dir.mkdir("log") @@ -56,7 +57,7 @@ namespace :test do end namespace :integration do - desc "ActiveRecord Integration Tests" + # Active Record Integration Tests Rake::TestTask.new(:active_record) do |t| t.libs << "test" t.test_files = Dir.glob("test/activerecord/*_test.rb") @@ -65,7 +66,7 @@ namespace :test do t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end - desc "ActionPack Integration Tests" + # Action Pack Integration Tests Rake::TestTask.new(:action_pack) do |t| t.libs << "test" t.test_files = Dir.glob("test/actionpack/**/*_test.rb") @@ -130,7 +131,7 @@ namespace :assets do end task :lines do - load File.join(File.expand_path("..", __dir__), "/tools/line_statistics") + load File.expand_path("../tools/line_statistics", __dir__) files = FileList["lib/**/*.rb"] CodeTools::LineStatistics.new(files).print_loc end diff --git a/actionview/app/assets/javascripts/rails-ujs/features/confirm.coffee b/actionview/app/assets/javascripts/rails-ujs/features/confirm.coffee index 72b5aaa218..0738ffcdc9 100644 --- a/actionview/app/assets/javascripts/rails-ujs/features/confirm.coffee +++ b/actionview/app/assets/javascripts/rails-ujs/features/confirm.coffee @@ -5,6 +5,10 @@ Rails.handleConfirm = (e) -> stopEverything(e) unless allowAction(this) +# Default confirm dialog, may be overridden with custom confirm dialog in Rails.confirm +Rails.confirm = (message, element) -> + confirm(message) + # For 'data-confirm' attribute: # - Fires `confirm` event # - Shows the confirmation dialog @@ -20,7 +24,7 @@ allowAction = (element) -> answer = false if fire(element, 'confirm') - try answer = confirm(message) + try answer = Rails.confirm(message, element) callback = fire(element, 'confirm:complete', [answer]) answer and callback diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee index 2a8f5659e3..019bda635a 100644 --- a/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee +++ b/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee @@ -66,10 +66,10 @@ processResponse = (response, type) -> try response = JSON.parse(response) else if type.match(/\b(?:java|ecma)script\b/) script = document.createElement('script') - script.nonce = cspNonce() + script.setAttribute('nonce', cspNonce()) script.text = response document.head.appendChild(script).parentNode.removeChild(script) - else if type.match(/\b(xml|html|svg)\b/) + else if type.match(/\bxml\b/) parser = new DOMParser() type = type.replace(/;.+/, '') # remove something like ';charset=utf-8' try response = parser.parseFromString(response, type) diff --git a/actionview/lib/action_view/context.rb b/actionview/lib/action_view/context.rb index 665a9e3171..3c605c3ee3 100644 --- a/actionview/lib/action_view/context.rb +++ b/actionview/lib/action_view/context.rb @@ -10,10 +10,11 @@ module ActionView # Action View contexts are supplied to Action Controller to render a template. # The default Action View context is ActionView::Base. # - # In order to work with ActionController, a Context must just include this module. - # The initialization of the variables used by the context (@output_buffer, @view_flow, - # and @virtual_path) is responsibility of the object that includes this module - # (although you can call _prepare_context defined below). + # In order to work with Action Controller, a Context must just include this + # module. The initialization of the variables used by the context + # (@output_buffer, @view_flow, and @virtual_path) is responsibility of the + # object that includes this module (although you can call _prepare_context + # defined below). module Context include CompiledTemplates attr_accessor :output_buffer, :view_flow diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index dbd7a4ee11..3832293251 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -45,9 +45,8 @@ module ActionView # Create a dependency tree for template named +name+. def tree(name, finder, partial = false, seen = {}) logical_name = name.gsub(%r|/_|, "/") - finder.formats = [finder.rendered_format] if finder.rendered_format - if template = finder.disable_cache { finder.find_all(logical_name, [], partial, []).first } + if template = find_template(finder, logical_name, [], partial, []) finder.rendered_format ||= template.formats.first if node = seen[template.identifier] # handle cycles in the tree @@ -69,6 +68,22 @@ module ActionView seen[name] ||= Missing.new(name, logical_name, nil) end end + + private + def find_template(finder, *args) + name = args.first + prefixes = args[1] || [] + partial = args[2] || false + keys = args[3] || [] + options = args[4] || {} + finder.disable_cache do + if format = finder.rendered_format + finder.find_all(name, prefixes, partial, keys, options.merge(formats: [format])).first || finder.find_all(name, prefixes, partial, keys, options).first + else + finder.find_all(name, prefixes, partial, keys, options).first + end + end + end end class Node diff --git a/actionview/lib/action_view/helpers.rb b/actionview/lib/action_view/helpers.rb index 8cc8013718..0d77f74171 100644 --- a/actionview/lib/action_view/helpers.rb +++ b/actionview/lib/action_view/helpers.rb @@ -23,7 +23,6 @@ module ActionView #:nodoc: autoload :JavaScriptHelper, "action_view/helpers/javascript_helper" autoload :NumberHelper autoload :OutputSafetyHelper - autoload :RecordTagHelper autoload :RenderingHelper autoload :SanitizeHelper autoload :TagHelper @@ -57,7 +56,6 @@ module ActionView #:nodoc: include JavaScriptHelper include NumberHelper include OutputSafetyHelper - include RecordTagHelper include RenderingHelper include SanitizeHelper include TagHelper diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index 76b1c3fb6e..257080d902 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -55,6 +55,8 @@ module ActionView # that path. # * <tt>:skip_pipeline</tt> - This option is used to bypass the asset pipeline # when it is set to true. + # * <tt>:nonce<tt> - When set to true, adds an automatic nonce value if + # you have Content Security Policy enabled. # # ==== Examples # @@ -79,6 +81,9 @@ module ActionView # # javascript_include_tag "http://www.example.com/xmlhr.js" # # => <script src="http://www.example.com/xmlhr.js"></script> + # + # javascript_include_tag "http://www.example.com/xmlhr.js", nonce: true + # # => <script src="http://www.example.com/xmlhr.js" nonce="..."></script> def javascript_include_tag(*sources) options = sources.extract_options!.stringify_keys path_options = options.extract!("protocol", "extname", "host", "skip_pipeline").symbolize_keys @@ -90,6 +95,9 @@ module ActionView tag_options = { "src" => href }.merge!(options) + if tag_options["nonce"] == true + tag_options["nonce"] = content_security_policy_nonce + end content_tag("script".freeze, "", tag_options) }.join("\n").html_safe @@ -105,7 +113,7 @@ module ActionView # to "screen", so you must explicitly set it to "all" for the stylesheet(s) to # apply to all media types. # - # If the server supports Early Hints header links for these assets will be + # If the server supports Early Hints header links for these assets will be # automatically pushed. # # stylesheet_link_tag "style" diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb index 4c45f122fe..620e1e9f21 100644 --- a/actionview/lib/action_view/helpers/date_helper.rb +++ b/actionview/lib/action_view/helpers/date_helper.rb @@ -302,15 +302,15 @@ module ActionView # time_select("article", "start_time", include_seconds: true) # # # You can set the <tt>:minute_step</tt> to 15 which will give you: 00, 15, 30, and 45. - # time_select 'game', 'game_time', {minute_step: 15} + # time_select 'game', 'game_time', { minute_step: 15 } # # # Creates a time select tag with a custom prompt. Use <tt>prompt: true</tt> for generic prompts. - # time_select("article", "written_on", prompt: {hour: 'Choose hour', minute: 'Choose minute', second: 'Choose seconds'}) - # time_select("article", "written_on", prompt: {hour: true}) # generic prompt for hours + # time_select("article", "written_on", prompt: { hour: 'Choose hour', minute: 'Choose minute', second: 'Choose seconds' }) + # time_select("article", "written_on", prompt: { hour: true }) # generic prompt for hours # time_select("article", "written_on", prompt: true) # generic prompts for all # # # You can set :ampm option to true which will show the hours as: 12 PM, 01 AM .. 11 PM. - # time_select 'game', 'game_time', {ampm: true} + # time_select 'game', 'game_time', { ampm: true } # # The selects are prepared for multi-parameter assignment to an Active Record object. # @@ -346,8 +346,8 @@ module ActionView # datetime_select("article", "written_on", discard_type: true) # # # Generates a datetime select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts. - # datetime_select("article", "written_on", prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'}) - # datetime_select("article", "written_on", prompt: {hour: true}) # generic prompt for hours + # datetime_select("article", "written_on", prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' }) + # datetime_select("article", "written_on", prompt: { hour: true }) # generic prompt for hours # datetime_select("article", "written_on", prompt: true) # generic prompts for all # # The selects are prepared for multi-parameter assignment to an Active Record object. @@ -397,8 +397,8 @@ module ActionView # select_datetime(my_date_time, prefix: 'payday') # # # Generates a datetime select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts. - # select_datetime(my_date_time, prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'}) - # select_datetime(my_date_time, prompt: {hour: true}) # generic prompt for hours + # select_datetime(my_date_time, prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' }) + # select_datetime(my_date_time, prompt: { hour: true }) # generic prompt for hours # select_datetime(my_date_time, prompt: true) # generic prompts for all def select_datetime(datetime = Time.current, options = {}, html_options = {}) DateTimeSelector.new(datetime, options, html_options).select_datetime @@ -436,8 +436,8 @@ module ActionView # select_date(my_date, prefix: 'payday') # # # Generates a date select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts. - # select_date(my_date, prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'}) - # select_date(my_date, prompt: {hour: true}) # generic prompt for hours + # select_date(my_date, prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' }) + # select_date(my_date, prompt: { hour: true }) # generic prompt for hours # select_date(my_date, prompt: true) # generic prompts for all def select_date(date = Date.current, options = {}, html_options = {}) DateTimeSelector.new(date, options, html_options).select_date @@ -476,8 +476,8 @@ module ActionView # select_time(my_time, start_hour: 2, end_hour: 14) # # # Generates a time select with a custom prompt. Use <tt>:prompt</tt> to true for generic prompts. - # select_time(my_time, prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'}) - # select_time(my_time, prompt: {hour: true}) # generic prompt for hours + # select_time(my_time, prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' }) + # select_time(my_time, prompt: { hour: true }) # generic prompt for hours # select_time(my_time, prompt: true) # generic prompts for all def select_time(datetime = Time.current, options = {}, html_options = {}) DateTimeSelector.new(datetime, options, html_options).select_time @@ -681,9 +681,8 @@ module ActionView options = args.extract_options! format = options.delete(:format) || :long content = args.first || I18n.l(date_or_time, format: format) - datetime = date_or_time.acts_like?(:time) ? date_or_time.xmlschema : date_or_time.iso8601 - content_tag("time".freeze, content, options.reverse_merge(datetime: datetime), &block) + content_tag("time".freeze, content, options.reverse_merge(datetime: date_or_time.iso8601), &block) end private diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index afd49286e6..2d5c5684c1 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -1970,7 +1970,7 @@ module ActionView convert_to_legacy_options(options) - fields_for(scope || model, model, **options, &block) + fields_for(scope || model, model, options, &block) end # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb index fe5e0b693e..d02f641867 100644 --- a/actionview/lib/action_view/helpers/form_options_helper.rb +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -16,7 +16,7 @@ module ActionView # # * <tt>:include_blank</tt> - set to true or a prompt string if the first option element of the select element is a blank. Useful if there is not a default value required for the select element. # - # select("post", "category", Post::CATEGORIES, {include_blank: true}) + # select("post", "category", Post::CATEGORIES, { include_blank: true }) # # could become: # @@ -30,7 +30,7 @@ module ActionView # # Example with <tt>@post.person_id => 2</tt>: # - # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: 'None'}) + # select("post", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { include_blank: 'None' }) # # could become: # @@ -43,7 +43,7 @@ module ActionView # # * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this prepends an option with a generic prompt -- "Please select" -- or the given prompt string. # - # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {prompt: 'Select Person'}) + # select("post", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { prompt: 'Select Person' }) # # could become: # @@ -69,7 +69,7 @@ module ActionView # # * <tt>:disabled</tt> - can be a single value or an array of values that will be disabled options in the final output. # - # select("post", "category", Post::CATEGORIES, {disabled: 'restricted'}) + # select("post", "category", Post::CATEGORIES, { disabled: 'restricted' }) # # could become: # @@ -82,7 +82,7 @@ module ActionView # # When used with the <tt>collection_select</tt> helper, <tt>:disabled</tt> can also be a Proc that identifies those options that should be disabled. # - # collection_select(:post, :category_id, Category.all, :id, :name, {disabled: -> (category) { category.archived? }}) + # collection_select(:post, :category_id, Category.all, :id, :name, { disabled: -> (category) { category.archived? } }) # # If the categories "2008 stuff" and "Christmas" return true when the method <tt>archived?</tt> is called, this would return: # <select name="post[category_id]" id="post_category_id"> @@ -107,7 +107,7 @@ module ActionView # # For example: # - # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, { include_blank: true }) + # select("post", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { include_blank: true }) # # would become: # @@ -323,12 +323,12 @@ module ActionView # # You can optionally provide HTML attributes as the last element of the array. # - # options_for_select([ "Denmark", ["USA", {class: 'bold'}], "Sweden" ], ["USA", "Sweden"]) + # options_for_select([ "Denmark", ["USA", { class: 'bold' }], "Sweden" ], ["USA", "Sweden"]) # # => <option value="Denmark">Denmark</option> # # => <option value="USA" class="bold" selected="selected">USA</option> # # => <option value="Sweden" selected="selected">Sweden</option> # - # options_for_select([["Dollar", "$", {class: "bold"}], ["Kroner", "DKK", {onclick: "alert('HI');"}]]) + # options_for_select([["Dollar", "$", { class: "bold" }], ["Kroner", "DKK", { onclick: "alert('HI');" }]]) # # => <option value="$" class="bold">Dollar</option> # # => <option value="DKK" onclick="alert('HI');">Kroner</option> # diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index 54f82e058e..ba09738beb 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -389,8 +389,8 @@ module ActionView # * Any other key creates standard HTML options for the tag. # # ==== Examples - # radio_button_tag 'gender', 'male' - # # => <input id="gender_male" name="gender" type="radio" value="male" /> + # radio_button_tag 'favorite_color', 'maroon' + # # => <input id="favorite_color_maroon" name="favorite_color" type="radio" value="maroon" /> # # radio_button_tag 'receive_updates', 'no', true # # => <input checked="checked" id="receive_updates_no" name="receive_updates" type="radio" value="no" /> diff --git a/actionview/lib/action_view/helpers/record_tag_helper.rb b/actionview/lib/action_view/helpers/record_tag_helper.rb deleted file mode 100644 index a6953ee905..0000000000 --- a/actionview/lib/action_view/helpers/record_tag_helper.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module ActionView - module Helpers #:nodoc: - module RecordTagHelper - def div_for(*) # :nodoc: - raise NoMethodError, "The `div_for` method has been removed from " \ - "Rails. To continue using it, add the `record_tag_helper` gem to " \ - "your Gemfile:\n" \ - " gem 'record_tag_helper', '~> 1.0'\n" \ - "Consult the Rails upgrade guide for details." - end - - def content_tag_for(*) # :nodoc: - raise NoMethodError, "The `content_tag_for` method has been removed from " \ - "Rails. To continue using it, add the `record_tag_helper` gem to " \ - "your Gemfile:\n" \ - " gem 'record_tag_helper', '~> 1.0'\n" \ - "Consult the Rails upgrade guide for details." - end - end - end -end diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb index a6cec3f69c..b73c4be1ee 100644 --- a/actionview/lib/action_view/helpers/tag_helper.rb +++ b/actionview/lib/action_view/helpers/tag_helper.rb @@ -227,10 +227,10 @@ module ActionView # tag("img", src: "open & shut.png") # # => <img src="open & shut.png" /> # - # tag("img", {src: "open & shut.png"}, false, false) + # tag("img", { src: "open & shut.png" }, false, false) # # => <img src="open & shut.png" /> # - # tag("div", data: {name: 'Stephen', city_state: %w(Chicago IL)}) + # tag("div", data: { name: 'Stephen', city_state: %w(Chicago IL) }) # # => <div data-name="Stephen" data-city-state="["Chicago","IL"]" /> def tag(name = nil, options = nil, open = false, escape = true) if name.nil? diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb index 80cb73d683..db44fdbfee 100644 --- a/actionview/lib/action_view/helpers/translation_helper.rb +++ b/actionview/lib/action_view/helpers/translation_helper.rb @@ -60,7 +60,11 @@ module ActionView def translate(key, options = {}) options = options.dup has_default = options.has_key?(:default) - remaining_defaults = Array(options.delete(:default)).compact + if has_default + remaining_defaults = Array(options.delete(:default)).compact + else + remaining_defaults = [] + end if has_default && !remaining_defaults.first.kind_of?(Symbol) options[:default] = remaining_defaults diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index 889562c478..cae62f2312 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -636,7 +636,7 @@ module ActionView # to_form_params(name: 'David', nationality: 'Danish') # # => [{name: :name, value: 'David'}, {name: 'nationality', value: 'Danish'}] # - # to_form_params(country: {name: 'Denmark'}) + # to_form_params(country: { name: 'Denmark' }) # # => [{name: 'country[name]', value: 'Denmark'}] # # to_form_params(countries: ['Denmark', 'Sweden']}) diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb index 12bdc642d4..12d06bf376 100644 --- a/actionview/lib/action_view/railtie.rb +++ b/actionview/lib/action_view/railtie.rb @@ -10,6 +10,7 @@ module ActionView config.action_view.embed_authenticity_token_in_remote_forms = nil config.action_view.debug_missing_translation = true config.action_view.default_enforce_utf8 = nil + config.action_view.finalize_compiled_template_methods = true config.eager_load_namespaces << ActionView @@ -45,6 +46,13 @@ module ActionView end end + initializer "action_view.finalize_compiled_template_methods" do |app| + ActiveSupport.on_load(:action_view) do + ActionView::Template.finalize_compiled_template_methods = + app.config.action_view.delete(:finalize_compiled_template_methods) + end + end + initializer "action_view.logger" do ActiveSupport.on_load(:action_view) { self.logger ||= Rails.logger } end diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb index 0c4bb73acb..ee1cd61f12 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -9,6 +9,8 @@ module ActionView class Template extend ActiveSupport::Autoload + mattr_accessor :finalize_compiled_template_methods, default: true + # === Encodings in ActionView::Template # # ActionView::Template is one of a few sources of potential @@ -307,7 +309,9 @@ module ActionView end mod.module_eval(source, identifier, 0) - ObjectSpace.define_finalizer(self, Finalizer[method_name, mod]) + if finalize_compiled_template_methods + ObjectSpace.define_finalizer(self, Finalizer[method_name, mod]) + end end def handle_render_error(view, e) diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb index 6d98eacfb8..e68f03d1f4 100644 --- a/actionview/test/template/asset_tag_helper_test.rb +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -29,6 +29,10 @@ class AssetTagHelperTest < ActionView::TestCase "http://www.example.com" end + def content_security_policy_nonce + "iyhD0Yc0W+c=" + end + AssetPathToTag = { %(asset_path("")) => %(), %(asset_path(" ")) => %(), @@ -421,6 +425,10 @@ class AssetTagHelperTest < ActionView::TestCase assert_dom_equal %(<script src="//assets.example.com/javascripts/prototype.js"></script>), javascript_include_tag("prototype") end + def test_javascript_include_tag_nonce + assert_dom_equal %(<script src="/javascripts/bank.js" nonce="iyhD0Yc0W+c="></script>), javascript_include_tag("bank", nonce: true) + end + def test_stylesheet_path StylePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end diff --git a/actionview/test/template/capture_helper_test.rb b/actionview/test/template/capture_helper_test.rb index 31c280a91c..131e49327e 100644 --- a/actionview/test/template/capture_helper_test.rb +++ b/actionview/test/template/capture_helper_test.rb @@ -49,21 +49,21 @@ class CaptureHelperTest < ActionView::TestCase end def test_content_for_with_multiple_calls - assert ! content_for?(:title) + assert_not content_for?(:title) content_for :title, "foo" content_for :title, "bar" assert_equal "foobar", content_for(:title) end def test_content_for_with_multiple_calls_and_flush - assert ! content_for?(:title) + assert_not content_for?(:title) content_for :title, "foo" content_for :title, "bar", flush: true assert_equal "bar", content_for(:title) end def test_content_for_with_block - assert ! content_for?(:title) + assert_not content_for?(:title) content_for :title do output_buffer << "foo" output_buffer << "bar" @@ -73,7 +73,7 @@ class CaptureHelperTest < ActionView::TestCase end def test_content_for_with_block_and_multiple_calls_with_flush - assert ! content_for?(:title) + assert_not content_for?(:title) content_for :title do "foo" end @@ -84,7 +84,7 @@ class CaptureHelperTest < ActionView::TestCase end def test_content_for_with_block_and_multiple_calls_with_flush_nil_content - assert ! content_for?(:title) + assert_not content_for?(:title) content_for :title do "foo" end @@ -95,7 +95,7 @@ class CaptureHelperTest < ActionView::TestCase end def test_content_for_with_block_and_multiple_calls_without_flush - assert ! content_for?(:title) + assert_not content_for?(:title) content_for :title do "foo" end @@ -106,7 +106,7 @@ class CaptureHelperTest < ActionView::TestCase end def test_content_for_with_whitespace_block - assert ! content_for?(:title) + assert_not content_for?(:title) content_for :title, "foo" content_for :title do output_buffer << " \n " @@ -117,7 +117,7 @@ class CaptureHelperTest < ActionView::TestCase end def test_content_for_with_whitespace_block_and_flush - assert ! content_for?(:title) + assert_not content_for?(:title) content_for :title, "foo" content_for :title, flush: true do output_buffer << " \n " @@ -128,7 +128,7 @@ class CaptureHelperTest < ActionView::TestCase end def test_content_for_returns_nil_when_writing - assert ! content_for?(:title) + assert_not content_for?(:title) assert_nil content_for(:title, "foo") assert_nil content_for(:title) { output_buffer << "bar"; nil } assert_nil content_for(:title) { output_buffer << " \n "; nil } @@ -144,14 +144,14 @@ class CaptureHelperTest < ActionView::TestCase end def test_content_for_question_mark - assert ! content_for?(:title) + assert_not content_for?(:title) content_for :title, "title" assert content_for?(:title) - assert ! content_for?(:something_else) + assert_not content_for?(:something_else) end def test_content_for_should_be_html_safe_after_flush_empty - assert ! content_for?(:title) + assert_not content_for?(:title) content_for :title do content_tag(:p, "title") end @@ -164,7 +164,7 @@ class CaptureHelperTest < ActionView::TestCase end def test_provide - assert !content_for?(:title) + assert_not content_for?(:title) provide :title, "hi" assert content_for?(:title) assert_equal "hi", content_for(:title) diff --git a/actionview/test/template/lookup_context_test.rb b/actionview/test/template/lookup_context_test.rb index beee76f711..38469cbe3d 100644 --- a/actionview/test/template/lookup_context_test.rb +++ b/actionview/test/template/lookup_context_test.rb @@ -195,7 +195,7 @@ class LookupContextTest < ActiveSupport::TestCase assert @lookup_context.cache template = @lookup_context.disable_cache do - assert !@lookup_context.cache + assert_not @lookup_context.cache @lookup_context.find("foo", %w(test), true) end assert @lookup_context.cache diff --git a/actionview/test/template/record_tag_helper_test.rb b/actionview/test/template/record_tag_helper_test.rb deleted file mode 100644 index 7bbbfccdd0..0000000000 --- a/actionview/test/template/record_tag_helper_test.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -require "abstract_unit" - -class RecordTagPost - extend ActiveModel::Naming - - attr_accessor :id, :body - - def initialize - @id = 45 - @body = "What a wonderful world!" - - yield self if block_given? - end -end - -class RecordTagHelperTest < ActionView::TestCase - tests ActionView::Helpers::RecordTagHelper - - def setup - super - @post = RecordTagPost.new - end - - def test_content_tag_for - assert_raises(NoMethodError) { content_tag_for(:li, @post) } - end - - def test_div_for - assert_raises(NoMethodError) { div_for(@post, class: "special") } - end -end diff --git a/actionview/test/template/test_case_test.rb b/actionview/test/template/test_case_test.rb index 05e5f21ce4..d98fd4f9a2 100644 --- a/actionview/test/template/test_case_test.rb +++ b/actionview/test/template/test_case_test.rb @@ -192,7 +192,7 @@ module ActionView helper HelperThatInvokesProtectAgainstForgery test "protect_from_forgery? in any helpers returns false" do - assert !view.help_me + assert_not view.help_me end end diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index 8bccda481b..08cb5dfea7 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -508,16 +508,16 @@ class UrlHelperTest < ActiveSupport::TestCase def test_current_page_considering_params @request = request_for_url("/?order=desc&page=1") - assert !current_page?(url_hash, check_parameters: true) - assert !current_page?(url_hash.merge(check_parameters: true)) - assert !current_page?(ActionController::Parameters.new(url_hash.merge(check_parameters: true)).permit!) - assert !current_page?("http://www.example.com/", check_parameters: true) + assert_not current_page?(url_hash, check_parameters: true) + assert_not current_page?(url_hash.merge(check_parameters: true)) + assert_not current_page?(ActionController::Parameters.new(url_hash.merge(check_parameters: true)).permit!) + assert_not current_page?("http://www.example.com/", check_parameters: true) end def test_current_page_considering_params_when_options_does_not_respond_to_to_hash @request = request_for_url("/?order=desc&page=1") - assert !current_page?(:back, check_parameters: false) + assert_not current_page?(:back, check_parameters: false) end def test_current_page_with_params_that_match @@ -562,7 +562,7 @@ class UrlHelperTest < ActiveSupport::TestCase def test_current_page_with_not_get_verb @request = request_for_url("/events", method: :post) - assert !current_page?("/events") + assert_not current_page?("/events") end def test_link_unless_current diff --git a/actionview/test/ujs/public/test/call-remote-callbacks.js b/actionview/test/ujs/public/test/call-remote-callbacks.js index 48763f6301..9c0c8cfb4b 100644 --- a/actionview/test/ujs/public/test/call-remote-callbacks.js +++ b/actionview/test/ujs/public/test/call-remote-callbacks.js @@ -75,9 +75,9 @@ asyncTest('setting data("with-credentials",true) with "ajax:before" uses new set asyncTest('stopping the "ajax:beforeSend" event aborts the request', 1, function() { submit(function(form) { - form.bindNative('ajax:beforeSend', function() { + form.bindNative('ajax:beforeSend', function(e) { ok(true, 'aborting request in ajax:beforeSend') - return false + e.preventDefault() }) form.unbind('ajax:send').bindNative('ajax:send', function() { ok(false, 'ajax:send should not run') @@ -148,8 +148,8 @@ function skipIt() { .bind('iframe:loading', function() { ok(false, 'form should not get submitted') }) - .bindNative('ajax:aborted:file', function() { - return false + .bindNative('ajax:aborted:file', function(e) { + e.preventDefault() }) .triggerNative('submit') @@ -162,9 +162,9 @@ function skipIt() { } asyncTest('"ajax:beforeSend" can be observed and stopped with event delegation', 1, function() { - $(document).delegate('form[data-remote]', 'ajax:beforeSend', function() { + $(document).delegate('form[data-remote]', 'ajax:beforeSend', function(e) { ok(true, 'ajax:beforeSend observed with event delegation') - return false + e.preventDefault() }) submit(function(form) { diff --git a/actionview/test/ujs/public/test/call-remote.js b/actionview/test/ujs/public/test/call-remote.js index 5932195363..778dc1b09a 100644 --- a/actionview/test/ujs/public/test/call-remote.js +++ b/actionview/test/ujs/public/test/call-remote.js @@ -128,6 +128,17 @@ asyncTest('execution of JS code does not modify current DOM', 1, function() { }) }) +asyncTest('HTML content should be plain-text', 1, function() { + buildForm({ method: 'post', 'data-type': 'html' }) + + $('form').append('<input type="text" name="content_type" value="text/html">') + $('form').append('<input type="text" name="content" value="<p>hello</p>">') + + submit(function(e, data, status, xhr) { + ok(data === '<p>hello</p>', 'returned data should be a plain-text string') + }) +}) + asyncTest('XML document should be parsed', 1, function() { buildForm({ method: 'post', 'data-type': 'html' }) @@ -199,7 +210,7 @@ asyncTest('allow empty form "action"', 1, function() { buildForm({ action: '' }) $('#qunit-fixture').find('form') - .bindNative('ajax:beforeSend', function(e, xhr, settings) { + .bindNative('ajax:beforeSend', function(evt, xhr, settings) { // Get current location (the same way jQuery does) try { currentLocation = location.href @@ -218,7 +229,7 @@ asyncTest('allow empty form "action"', 1, function() { // Prevent the request from actually getting sent to the current page and // causing an error. - return false + evt.preventDefault() }) .triggerNative('submit') @@ -246,7 +257,7 @@ asyncTest('intelligently guesses crossDomain behavior when target URL has a diff equal(settings.crossDomain, true, 'crossDomain should be set to true') // prevent request from actually getting sent off-domain - return false + evt.preventDefault() }) .triggerNative('submit') @@ -265,7 +276,7 @@ asyncTest('intelligently guesses crossDomain behavior when target URL consists o equal(settings.crossDomain, false, 'crossDomain should be set to false') // prevent request from actually getting sent off-domain - return false + evt.preventDefault() }) .triggerNative('submit') diff --git a/actionview/test/ujs/public/test/data-confirm.js b/actionview/test/ujs/public/test/data-confirm.js index d1ea82ea7e..1bd57b69ad 100644 --- a/actionview/test/ujs/public/test/data-confirm.js +++ b/actionview/test/ujs/public/test/data-confirm.js @@ -173,9 +173,9 @@ asyncTest('binding to confirm event of a link and returning false', 1, function( } $('a[data-confirm]') - .bindNative('confirm', function() { + .bindNative('confirm', function(e) { App.assertCallbackInvoked('confirm') - return false + e.preventDefault() }) .bindNative('confirm:complete', function() { App.assertCallbackNotInvoked('confirm:complete') @@ -194,9 +194,9 @@ asyncTest('binding to confirm event of a button and returning false', 1, functio } $('button[data-confirm]') - .bindNative('confirm', function() { + .bindNative('confirm', function(e) { App.assertCallbackInvoked('confirm') - return false + e.preventDefault() }) .bindNative('confirm:complete', function() { App.assertCallbackNotInvoked('confirm:complete') @@ -216,9 +216,9 @@ asyncTest('binding to confirm:complete event of a link and returning false', 2, } $('a[data-confirm]') - .bindNative('confirm:complete', function() { + .bindNative('confirm:complete', function(e) { App.assertCallbackInvoked('confirm:complete') - return false + e.preventDefault() }) .bindNative('ajax:beforeSend', function() { App.assertCallbackNotInvoked('ajax:beforeSend') @@ -238,9 +238,9 @@ asyncTest('binding to confirm:complete event of a button and returning false', 2 } $('button[data-confirm]') - .bindNative('confirm:complete', function() { + .bindNative('confirm:complete', function(e) { App.assertCallbackInvoked('confirm:complete') - return false + e.preventDefault() }) .bindNative('ajax:beforeSend', function() { App.assertCallbackNotInvoked('ajax:beforeSend') @@ -314,3 +314,29 @@ asyncTest('clicking on the children of a disabled button should not trigger a co start() }, 50) }) + +asyncTest('clicking on a link with data-confirm attribute with custom confirm handler. Confirm yes.', 7, function() { + var message, element + // redefine confirm function so we can make sure it's not called + window.confirm = function(msg) { + ok(false, 'confirm dialog should not be called') + } + // custom auto-confirm: + Rails.confirm = function(msg, elem) { message = msg; element = elem; return true } + + $('a[data-confirm]') + .bindNative('confirm:complete', function(e, data) { + App.assertCallbackInvoked('confirm:complete') + ok(data == true, 'confirm:complete passes in confirm answer (true)') + }) + .bindNative('ajax:success', function(e, data, status, xhr) { + App.assertCallbackInvoked('ajax:success') + App.assertRequestPath(data, '/echo') + App.assertGetRequest(data) + + equal(message, 'Are you absolutely sure?') + equal(element, $('a[data-confirm]').get(0)) + start() + }) + .triggerNative('click') +}) diff --git a/actionview/test/ujs/public/test/data-disable-with.js b/actionview/test/ujs/public/test/data-disable-with.js index b29cbbc867..645ad494c3 100644 --- a/actionview/test/ujs/public/test/data-disable-with.js +++ b/actionview/test/ujs/public/test/data-disable-with.js @@ -132,7 +132,8 @@ test('form input[type=submit][data-disable-with] re-enables when `pageshow` even }) asyncTest('form[data-remote] input[type=submit][data-disable-with] is replaced in ajax callback', 2, function() { - var form = $('form:not([data-remote])').attr('data-remote', 'true'), origFormContents = form.html() + var form = $('#qunit-fixture form:not([data-remote])').attr('data-remote', 'true'), + origFormContents = form.html() form.bindNative('ajax:success', function() { form.html(origFormContents) @@ -146,7 +147,8 @@ asyncTest('form[data-remote] input[type=submit][data-disable-with] is replaced i }) asyncTest('form[data-remote] input[data-disable-with] is replaced with disabled field in ajax callback', 2, function() { - var form = $('form:not([data-remote])').attr('data-remote', 'true'), input = form.find('input[type=submit]'), + var form = $('#qunit-fixture form:not([data-remote])').attr('data-remote', 'true'), + input = form.find('input[type=submit]'), newDisabledInput = input.clone().attr('disabled', 'disabled') form.bindNative('ajax:success', function() { @@ -238,9 +240,9 @@ asyncTest('a[data-remote][data-disable-with] re-enables when `ajax:before` event App.checkEnabledState(link, 'Click me') link - .bindNative('ajax:before', function() { + .bindNative('ajax:before', function(e) { App.checkDisabledState(link, 'clicking...') - return false + e.preventDefault() }) .triggerNative('click') @@ -256,9 +258,9 @@ asyncTest('a[data-remote][data-disable-with] re-enables when `ajax:beforeSend` e App.checkEnabledState(link, 'Click me') link - .bindNative('ajax:beforeSend', function() { + .bindNative('ajax:beforeSend', function(e) { App.checkDisabledState(link, 'clicking...') - return false + e.preventDefault() }) .triggerNative('click') @@ -293,8 +295,9 @@ asyncTest('form[data-remote] input|button|textarea[data-disable-with] does not d submit = $('<input type="submit" data-disable-with="submitting ..." name="submit2" value="Submit" />').appendTo(form) form - .bindNative('ajax:beforeSend', function() { - return false + .bindNative('ajax:beforeSend', function(e) { + e.preventDefault() + e.stopPropagation() }) .triggerNative('submit') @@ -343,9 +346,9 @@ asyncTest('button[data-remote][data-disable-with] re-enables when `ajax:before` App.checkEnabledState(button, 'Click me') button - .bindNative('ajax:before', function() { + .bindNative('ajax:before', function(e) { App.checkDisabledState(button, 'clicking...') - return false + e.preventDefault() }) .triggerNative('click') @@ -361,9 +364,9 @@ asyncTest('button[data-remote][data-disable-with] re-enables when `ajax:beforeSe App.checkEnabledState(button, 'Click me') button - .bindNative('ajax:beforeSend', function() { + .bindNative('ajax:beforeSend', function(e) { App.checkDisabledState(button, 'clicking...') - return false + e.preventDefault() }) .triggerNative('click') diff --git a/actionview/test/ujs/public/test/data-disable.js b/actionview/test/ujs/public/test/data-disable.js index ccc38cf9ae..e9919764b6 100644 --- a/actionview/test/ujs/public/test/data-disable.js +++ b/actionview/test/ujs/public/test/data-disable.js @@ -91,7 +91,7 @@ asyncTest('form input[type=submit][data-disable] disables', 6, function() { }) asyncTest('form[data-remote] input[type=submit][data-disable] is replaced in ajax callback', 2, function() { - var form = $('form:not([data-remote])').attr('data-remote', 'true'), origFormContents = form.html() + var form = $('#qunit-fixture form:not([data-remote])').attr('data-remote', 'true'), origFormContents = form.html() form.bindNative('ajax:success', function() { form.html(origFormContents) @@ -105,7 +105,7 @@ asyncTest('form[data-remote] input[type=submit][data-disable] is replaced in aja }) asyncTest('form[data-remote] input[data-disable] is replaced with disabled field in ajax callback', 2, function() { - var form = $('form:not([data-remote])').attr('data-remote', 'true'), input = form.find('input[type=submit]'), + var form = $('#qunit-fixture form:not([data-remote])').attr('data-remote', 'true'), input = form.find('input[type=submit]'), newDisabledInput = input.clone().attr('disabled', 'disabled') form.bindNative('ajax:success', function() { @@ -168,9 +168,9 @@ asyncTest('a[data-remote][data-disable] re-enables when `ajax:before` event is c App.checkEnabledState(link, 'Click me') link - .bindNative('ajax:before', function() { + .bindNative('ajax:before', function(e) { App.checkDisabledState(link, 'Click me') - return false + e.preventDefault() }) .triggerNative('click') @@ -186,9 +186,9 @@ asyncTest('a[data-remote][data-disable] re-enables when `ajax:beforeSend` event App.checkEnabledState(link, 'Click me') link - .bindNative('ajax:beforeSend', function() { + .bindNative('ajax:beforeSend', function(e) { App.checkDisabledState(link, 'Click me') - return false + e.preventDefault() }) .triggerNative('click') @@ -223,8 +223,9 @@ asyncTest('form[data-remote] input|button|textarea[data-disable] does not disabl submit = $('<input type="submit" data-disable="submitting ..." name="submit2" value="Submit" />').appendTo(form) form - .bindNative('ajax:beforeSend', function() { - return false + .bindNative('ajax:beforeSend', function(e) { + e.preventDefault() + e.stopPropagation() }) .triggerNative('submit') @@ -273,9 +274,9 @@ asyncTest('button[data-remote][data-disable] re-enables when `ajax:before` event App.checkEnabledState(button, 'Click me') button - .bindNative('ajax:before', function() { + .bindNative('ajax:before', function(e) { App.checkDisabledState(button, 'Click me') - return false + e.preventDefault() }) .triggerNative('click') @@ -291,9 +292,9 @@ asyncTest('button[data-remote][data-disable] re-enables when `ajax:beforeSend` e App.checkEnabledState(button, 'Click me') button - .bindNative('ajax:beforeSend', function() { + .bindNative('ajax:beforeSend', function(e) { App.checkDisabledState(button, 'Click me') - return false + e.preventDefault() }) .triggerNative('click') diff --git a/actionview/test/ujs/public/test/data-remote.js b/actionview/test/ujs/public/test/data-remote.js index cbbd4e6c92..3503c2cff3 100644 --- a/actionview/test/ujs/public/test/data-remote.js +++ b/actionview/test/ujs/public/test/data-remote.js @@ -272,9 +272,10 @@ asyncTest('returning false in form\'s submit bindings in non-submit-bubbling bro form .append($('<input type="submit" />')) - .bindNative('submit', function() { + .bindNative('submit', function(e) { ok(true, 'binding handler is called') - return false + e.preventDefault() + e.stopPropagation() }) .bindNative('ajax:beforeSend', function() { ok(false, 'form should not be submitted') @@ -296,8 +297,8 @@ asyncTest('clicking on a link with falsy "data-remote" attribute does not fire a .bindNative('ajax:beforeSend', function() { ok(false, 'ajax should not be triggered') }) - .bindNative('click', function() { - return false + .bindNative('click', function(e) { + e.preventDefault() }) .triggerNative('click') @@ -314,8 +315,8 @@ asyncTest('ctrl-clicking on a link with falsy "data-remote" attribute does not f .bindNative('ajax:beforeSend', function() { ok(false, 'ajax should not be triggered') }) - .bindNative('click', function() { - return false + .bindNative('click', function(e) { + e.preventDefault() }) .triggerNative('click', { metaKey: true }) @@ -333,8 +334,8 @@ asyncTest('clicking on a button with falsy "data-remote" attribute', 0, function .bindNative('ajax:beforeSend', function() { ok(false, 'ajax should not be triggered') }) - .bindNative('click', function() { - return false + .bindNative('click', function(e) { + e.preventDefault() }) .triggerNative('click') @@ -347,8 +348,8 @@ asyncTest('submitting a form with falsy "data-remote" attribute', 0, function() .bindNative('ajax:beforeSend', function() { ok(false, 'ajax should not be triggered') }) - .bindNative('submit', function() { - return false + .bindNative('submit', function(e) { + e.preventDefault() }) .triggerNative('submit') @@ -429,7 +430,7 @@ asyncTest('changing a select option without "data-url" attribute still fires aja ajaxLocation = settings.url.replace(settings.data, '').replace(/&$/, '').replace(/\?$/, '') equal(ajaxLocation, currentLocation, 'URL should be current page by default') - return false + e.preventDefault() }) .val('optionValue2') .triggerNative('change') diff --git a/actionview/test/ujs/public/test/override.js b/actionview/test/ujs/public/test/override.js index 299c7018cc..d73276ee4f 100644 --- a/actionview/test/ujs/public/test/override.js +++ b/actionview/test/ujs/public/test/override.js @@ -25,7 +25,7 @@ asyncTest('the getter for an element\'s href is overridable', 1, function() { $('#qunit-fixture a') .bindNative('ajax:beforeSend', function(e, xhr, options) { equal('/data/href', options.url) - return false + e.preventDefault() }) .triggerNative('click') start() @@ -35,7 +35,7 @@ asyncTest('the getter for an element\'s href works normally if not overridden', $('#qunit-fixture a') .bindNative('ajax:beforeSend', function(e, xhr, options) { equal(location.protocol + '//' + location.host + '/real/href', options.url) - return false + e.preventDefault() }) .triggerNative('click') start() diff --git a/actionview/test/ujs/public/test/settings.js b/actionview/test/ujs/public/test/settings.js index 299c71bb00..b1ce3b8c64 100644 --- a/actionview/test/ujs/public/test/settings.js +++ b/actionview/test/ujs/public/test/settings.js @@ -103,14 +103,16 @@ $.fn.extend({ bindNative: function(event, handler) { if (!handler) return this - this.bind(event, function(e) { + var el = this[0] + el.addEventListener(event, function(e) { var args = [] - if (e.originalEvent.detail) { - args = e.originalEvent.detail.slice() + if (e.detail) { + args = e.detail.slice() } args.unshift(e) - return handler.apply(this, args) - }) + return handler.apply(el, args) + }, false) + return this } }) diff --git a/activejob/lib/active_job/serializers/object_serializer.rb b/activejob/lib/active_job/serializers/object_serializer.rb index 1dfd1e44be..6d280969be 100644 --- a/activejob/lib/active_job/serializers/object_serializer.rb +++ b/activejob/lib/active_job/serializers/object_serializer.rb @@ -38,7 +38,7 @@ module ActiveJob { Arguments::OBJECT_SERIALIZER_KEY => self.class.name }.merge!(hash) end - # Deserilizes an argument form a JSON primiteve type. + # Deserializes an argument from a JSON primitive type. def deserialize(_argument) raise NotImplementedError end diff --git a/activejob/lib/rails/generators/job/job_generator.rb b/activejob/lib/rails/generators/job/job_generator.rb index 69b4fe7d26..03346a7f12 100644 --- a/activejob/lib/rails/generators/job/job_generator.rb +++ b/activejob/lib/rails/generators/job/job_generator.rb @@ -28,6 +28,10 @@ module Rails # :nodoc: end private + def file_name + @_file_name ||= super.sub(/_job\z/i, "") + end + def application_job_file_name @application_job_file_name ||= if mountable_engine? "app/jobs/#{namespaced_path}/application_job.rb" diff --git a/activemodel/lib/active_model/attribute_mutation_tracker.rb b/activemodel/lib/active_model/attribute_mutation_tracker.rb index 493be5bb88..6abf37bd44 100644 --- a/activemodel/lib/active_model/attribute_mutation_tracker.rb +++ b/activemodel/lib/active_model/attribute_mutation_tracker.rb @@ -11,6 +11,10 @@ module ActiveModel @forced_changes = Set.new end + def changed_attribute_names + attr_names.select { |attr_name| changed?(attr_name) } + end + def changed_values attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| if changed?(attr_name) @@ -23,7 +27,7 @@ module ActiveModel attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| change = change_to_attribute(attr_name) if change - result[attr_name] = change + result.merge!(attr_name => change) end end end @@ -76,6 +80,10 @@ module ActiveModel class NullMutationTracker # :nodoc: include Singleton + def changed_attribute_names(*) + [] + end + def changed_values(*) {} end diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index 3104e7e329..9c12dc14c5 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -19,7 +19,7 @@ module ActiveModel # particular enumerable object. # # class Person < ActiveRecord::Base - # validates_inclusion_of :gender, in: %w( m f ) + # validates_inclusion_of :role, in: %w( admin contributor ) # validates_inclusion_of :age, in: 0..99 # validates_inclusion_of :format, in: %w( jpg gif png ), message: "extension %{value} is not included in the list" # validates_inclusion_of :states, in: ->(person) { STATES[person.country] } diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index e28e7e9219..88cca318ef 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -63,7 +63,7 @@ module ActiveModel # and strings in shortcut form. # # validates :email, format: /@/ - # validates :gender, inclusion: %w(male female) + # validates :role, inclusion: %(admin contributor) # validates :password, length: 6..20 # # When using shortcut form, ranges and arrays are passed to your diff --git a/activemodel/test/cases/attributes_dirty_test.rb b/activemodel/test/cases/attributes_dirty_test.rb index c991176389..f9693a23cd 100644 --- a/activemodel/test/cases/attributes_dirty_test.rb +++ b/activemodel/test/cases/attributes_dirty_test.rb @@ -39,7 +39,7 @@ class AttributesDirtyTest < ActiveModel::TestCase end test "changes to attribute values" do - assert !@model.changes["name"] + assert_not @model.changes["name"] @model.name = "John" assert_equal [nil, "John"], @model.changes["name"] end diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index f769eb0da1..b120e68027 100644 --- a/activemodel/test/cases/dirty_test.rb +++ b/activemodel/test/cases/dirty_test.rb @@ -78,7 +78,7 @@ class DirtyTest < ActiveModel::TestCase end test "changes to attribute values" do - assert !@model.changes["name"] + assert_not @model.changes["name"] @model.name = "John" assert_equal [nil, "John"], @model.changes["name"] end diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index cb6a8c43d5..6ff3be1308 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -207,26 +207,26 @@ class ErrorsTest < ActiveModel::TestCase test "added? returns false when no errors are present" do person = Person.new - assert !person.errors.added?(:name) + assert_not person.errors.added?(:name) end test "added? returns false when checking a nonexisting error and other errors are present for the given attribute" do person = Person.new person.errors.add(:name, "is invalid") - assert !person.errors.added?(:name, "cannot be blank") + assert_not person.errors.added?(:name, "cannot be blank") end test "added? returns false when checking for an error, but not providing message arguments" do person = Person.new person.errors.add(:name, "cannot be blank") - assert !person.errors.added?(:name) + assert_not person.errors.added?(:name) end test "added? returns false when checking for an error by symbol and a different error with same message is present" do I18n.backend.store_translations("en", errors: { attributes: { name: { wrong: "is wrong", used: "is wrong" } } }) person = Person.new person.errors.add(:name, :wrong) - assert !person.errors.added?(:name, :used) + assert_not person.errors.added?(:name, :used) end test "size calculates the number of error messages" do diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb index d19e81a119..912cb6d5ab 100644 --- a/activemodel/test/cases/secure_password_test.rb +++ b/activemodel/test/cases/secure_password_test.rb @@ -187,7 +187,7 @@ class SecurePasswordTest < ActiveModel::TestCase test "authenticate" do @user.password = "secret" - assert !@user.authenticate("wrong") + assert_not @user.authenticate("wrong") assert @user.authenticate("secret") end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 647c96d4b1..0d8fa48235 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,23 @@ +* Add support to preload associations of polymorphic associations when not all the records have the requested associations. + + *Dana Sherson* + +* Add `touch_all` method to `ActiveRecord::Relation`. + + Example: + + Person.where(name: "David").touch_all(time: Time.new(2020, 5, 16, 0, 0, 0)) + + *fatkodima*, *duggiefresh* + +* Add `ActiveRecord::Base.base_class?` predicate. + + *Bogdan Gusiev* + +* Add custom prefix option to ActiveRecord::Store.store_accessor. + + *Tan Huynh* + * Rails 6 requires Ruby 2.4.1 or newer. *Jeremy Daer* diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 591c451da5..170c95b827 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -41,9 +41,11 @@ namespace :test do end end -desc "Build MySQL and PostgreSQL test databases" namespace :db do + desc "Build MySQL and PostgreSQL test databases" task create: ["db:mysql:build", "db:postgresql:build"] + + desc "Drop MySQL and PostgreSQL test databases" task drop: ["db:mysql:drop", "db:postgresql:drop"] end diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index d43378c64f..d198466dbf 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -40,6 +40,7 @@ module ActiveRecord autoload :Core autoload :ConnectionHandling autoload :CounterCache + autoload :DatabaseConfigurations autoload :DynamicMatchers autoload :Enum autoload :InternalMetadata diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index b1cad0d0a4..0e68e49182 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -241,7 +241,7 @@ module ActiveRecord association end - def association_cached?(name) # :nodoc + def association_cached?(name) # :nodoc: @association_cache.key?(name) end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 443ccaaa72..671c4c56df 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -214,7 +214,7 @@ module ActiveRecord target.size elsif !association_scope.group_values.empty? load_target.size - elsif !association_scope.distinct_value && target.is_a?(Array) + elsif !association_scope.distinct_value && !target.empty? unsaved_records = target.select(&:new_record?) unsaved_records.size + count_records else @@ -234,7 +234,7 @@ module ActiveRecord if loaded? size.zero? else - @target.blank? && !scope.exists? + target.empty? && !scope.exists? end end diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index 491282adf7..019bf0729f 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -28,7 +28,11 @@ module ActiveRecord end if through_record - through_record.update(attributes) + if through_record.new_record? + through_record.assign_attributes(attributes) + else + through_record.update(attributes) + end elsif owner.new_record? || !save through_proxy.build(attributes) else diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 1ea0aeac3a..5c2ac5b374 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -98,26 +98,30 @@ module ActiveRecord private # Loads all the given data into +records+ for the +association+. - def preloaders_on(association, records, scope) + def preloaders_on(association, records, scope, polymorphic_parent = false) case association when Hash - preloaders_for_hash(association, records, scope) + preloaders_for_hash(association, records, scope, polymorphic_parent) when Symbol - preloaders_for_one(association, records, scope) + preloaders_for_one(association, records, scope, polymorphic_parent) when String - preloaders_for_one(association.to_sym, records, scope) + preloaders_for_one(association.to_sym, records, scope, polymorphic_parent) else raise ArgumentError, "#{association.inspect} was not recognized for preload" end end - def preloaders_for_hash(association, records, scope) + def preloaders_for_hash(association, records, scope, polymorphic_parent) association.flat_map { |parent, child| - loaders = preloaders_for_one parent, records, scope + loaders = preloaders_for_one parent, records, scope, polymorphic_parent recs = loaders.flat_map(&:preloaded_records).uniq + + reflection = records.first.class._reflect_on_association(parent) + polymorphic_parent = reflection && reflection.options[:polymorphic] + loaders.concat Array.wrap(child).flat_map { |assoc| - preloaders_on assoc, recs, scope + preloaders_on assoc, recs, scope, polymorphic_parent } loaders } @@ -135,8 +139,8 @@ module ActiveRecord # Additionally, polymorphic belongs_to associations can have multiple associated # classes, depending on the polymorphic_type field. So we group by the classes as # well. - def preloaders_for_one(association, records, scope) - grouped_records(association, records).flat_map do |reflection, klasses| + def preloaders_for_one(association, records, scope, polymorphic_parent) + grouped_records(association, records, polymorphic_parent).flat_map do |reflection, klasses| klasses.map do |rhs_klass, rs| loader = preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope) loader.run self @@ -145,10 +149,11 @@ module ActiveRecord end end - def grouped_records(association, records) + def grouped_records(association, records, polymorphic_parent) h = {} records.each do |record| next unless record + next if polymorphic_parent && !record.class._reflect_on_association(association) assoc = record.association(association) next unless assoc.klass klasses = h[assoc.reflection] ||= {} diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 0f38d6bbda..d6f7359055 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -42,11 +42,11 @@ module ActiveRecord def associate_records_to_owner(owner, records) association = owner.association(reflection.name) + association.loaded! if reflection.collection? - association.loaded! association.target.concat(records) else - association.target = records.first + association.target = records.first unless records.empty? end end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 7db9bbe46b..83b5a5e698 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -59,7 +59,7 @@ module ActiveRecord # attribute methods. generated_attribute_methods.synchronize do return false if @attribute_methods_generated - superclass.define_attribute_methods unless self == base_class + superclass.define_attribute_methods unless base_class? super(attribute_names) @attribute_methods_generated = true end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 3de6fe566d..7224f970e0 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -114,7 +114,7 @@ module ActiveRecord # Alias for +changed+ def changed_attribute_names_to_save - changes_to_save.keys + mutations_from_database.changed_attribute_names end # Alias for +changed_attributes+ diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 2907547634..9b267bb7c0 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -83,7 +83,7 @@ module ActiveRecord end def reset_primary_key #:nodoc: - if self == base_class + if base_class? self.primary_key = get_primary_key(base_class.name) else self.primary_key = base_class.primary_key diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 14f700b6a9..0f7bcba564 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -69,7 +69,7 @@ module ActiveRecord if defined?(JRUBY_VERSION) # This form is significantly faster on JRuby, and this is one of our biggest hotspots. # https://github.com/jruby/jruby/pull/2562 - def _read_attribute(attr_name, &block) # :nodoc + def _read_attribute(attr_name, &block) # :nodoc: @attributes.fetch_value(attr_name.to_s, &block) end else diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index cc99401390..7ab9160265 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -290,6 +290,7 @@ module ActiveRecord #:nodoc: extend CollectionCacheKey include Core + include DatabaseConfigurations include Persistence include ReadonlyAttributes include ModelSchema diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 9dbdf845bd..fd6819d08f 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -75,21 +75,7 @@ module ActiveRecord # end # # Now, when <tt>Topic#destroy</tt> is run only +destroy_author+ is called. When <tt>Reply#destroy</tt> is - # run, both +destroy_author+ and +destroy_readers+ are called. Contrast this to the following situation - # where the +before_destroy+ method is overridden: - # - # class Topic < ActiveRecord::Base - # def before_destroy() destroy_author end - # end - # - # class Reply < Topic - # def before_destroy() destroy_readers end - # end - # - # In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+. - # So, use the callback macros when you want to ensure that a certain callback is called for the entire - # hierarchy, and use the regular overwritable methods when you want to leave it up to each descendant - # to decide whether they want to call +super+ and trigger the inherited callbacks. + # run, both +destroy_author+ and +destroy_readers+ are called. # # *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the # callbacks before specifying the associations. Otherwise, you might trigger the loading of a 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 08f3e15a4b..41553cfa83 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -385,7 +385,7 @@ module ActiveRecord end end - def empty_insert_statement_value + def empty_insert_statement_value(primary_key = nil) "DEFAULT VALUES" 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 584a86da21..6a498b353c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -101,6 +101,10 @@ module ActiveRecord end alias validated? validate? + def export_name_on_schema_dump? + name !~ ActiveRecord::SchemaDumper.fk_ignore_pattern + end + def defined_for?(to_table_ord = nil, to_table: nil, **options) if to_table_ord self.to_table == to_table_ord.to_s 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 e2147b7fcf..ac73337aef 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -305,7 +305,8 @@ module ActiveRecord yield td if block_given? if options[:force] - drop_table(table_name, **options, if_exists: true) + drop_opts = { if_exists: true }.merge(**options) + drop_table(table_name, drop_opts) end result = execute schema_creation.accept td @@ -908,7 +909,7 @@ module ActiveRecord foreign_key_options = { to_table: reference_name } end foreign_key_options[:column] ||= "#{ref_name}_id" - remove_foreign_key(table_name, **foreign_key_options) + remove_foreign_key(table_name, foreign_key_options) end remove_column(table_name, "#{ref_name}_id") diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index d9ac8db6a8..0ce3796829 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -75,7 +75,6 @@ module ActiveRecord class Transaction #:nodoc: attr_reader :connection, :state, :records, :savepoint_name - attr_writer :joinable def initialize(connection, options, run_commit_callbacks: false) @connection = connection 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 fbedddd7f9..477b09944f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -223,7 +223,7 @@ module ActiveRecord end end - def empty_insert_statement_value + def empty_insert_statement_value(primary_key = nil) "VALUES ()" end diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb new file mode 100644 index 0000000000..ffeed45030 --- /dev/null +++ b/activerecord/lib/active_record/database_configurations.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module ActiveRecord + module DatabaseConfigurations # :nodoc: + class DatabaseConfig + attr_reader :env_name, :spec_name, :config + + def initialize(env_name, spec_name, config) + @env_name = env_name + @spec_name = spec_name + @config = config + end + end + + # Selects the config for the specified environment and specification name + # + # For example if passed :development, and :animals it will select the database + # under the :development and :animals configuration level + def self.config_for_env_and_spec(environment, specification_name, configs = ActiveRecord::Base.configurations) # :nodoc: + configs_for(environment, configs).find do |db_config| + db_config.spec_name == specification_name + end + end + + # Collects the configs for the environment passed in. + # + # If a block is given returns the specification name and configuration + # otherwise returns an array of DatabaseConfig structs for the environment. + def self.configs_for(env, configs = ActiveRecord::Base.configurations, &blk) # :nodoc: + env_with_configs = db_configs(configs).select do |db_config| + db_config.env_name == env + end + + if block_given? + env_with_configs.each do |env_with_config| + yield env_with_config.spec_name, env_with_config.config + end + else + env_with_configs + end + end + + # Given an env, spec and config creates DatabaseConfig structs with + # each attribute set. + def self.walk_configs(env_name, spec_name, config) # :nodoc: + if config["database"] || config["url"] || config["adapter"] + DatabaseConfig.new(env_name, spec_name, config) + else + config.each_pair.map do |sub_spec_name, sub_config| + walk_configs(env_name, sub_spec_name, sub_config) + end + end + end + + # Walks all the configs passed in and returns an array + # of DatabaseConfig structs for each configuration. + def self.db_configs(configs = ActiveRecord::Base.configurations) # :nodoc: + configs.each_pair.flat_map do |env_name, config| + walk_configs(env_name, "primary", config) + end + end + end +end diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 208ba95c52..6891c575c7 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -55,7 +55,7 @@ module ActiveRecord if has_attribute?(inheritance_column) subclass = subclass_from_attributes(attributes) - if subclass.nil? && base_class == self + if subclass.nil? && base_class? subclass = subclass_from_attributes(column_defaults) end end @@ -104,6 +104,12 @@ module ActiveRecord end end + # Returns whether the class is a base class. + # See #base_class for more information. + def base_class? + base_class == self + end + # Set this to +true+ if this is an abstract class (see # <tt>abstract_class?</tt>). # If you are using inheritance with Active Record and don't want a class diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index bb85c47e06..5d1d15c94d 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -62,7 +62,7 @@ module ActiveRecord # the locked record. def lock!(lock = true) if persisted? - if changed? + if has_changes_to_save? raise(<<-MSG.squish) Locking a record with unpersisted changes is not supported. Use `save` to persist the changes, or `reload` to discard them diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 9234029c22..013c3765b2 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -125,7 +125,7 @@ module ActiveRecord ] end - RAILS_GEM_ROOT = File.expand_path("../../../..", __FILE__) + "/" + RAILS_GEM_ROOT = File.expand_path("../../..", __dir__) + "/" def ignored_callstack(path) path.start_with?(RAILS_GEM_ROOT) || diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 663b3c590a..025201c20b 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "benchmark" require "set" require "zlib" require "active_support/core_ext/module/attribute_accessors" diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index b04dc04899..694ff85fa1 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -276,7 +276,7 @@ module ActiveRecord end def sequence_name - if base_class == self + if base_class? @sequence_name ||= reset_sequence_name else (@sequence_name ||= nil) || base_class.sequence_name @@ -501,8 +501,7 @@ module ActiveRecord # Computes and returns a table name according to default conventions. def compute_table_name - base = base_class - if self == base + if base_class? # Nested classes are prefixed with singular parent table name. if parent < Base && !parent.abstract_class? contained = parent.table_name @@ -513,7 +512,7 @@ module ActiveRecord "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{full_table_name_suffix}" else # STI subclasses always use their superclass' table. - base.table_name + base_class.table_name end end end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 7721e6b691..c2393c1fc8 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -178,7 +178,7 @@ module ActiveRecord end if values.empty? - im = arel_table.compile_insert(connection.empty_insert_statement_value) + im = arel_table.compile_insert(connection.empty_insert_statement_value(primary_key)) im.into arel_table else im = arel_table.compile_insert(_substitute_values(values)) diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index c8e340712d..28194c7c46 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -26,19 +26,12 @@ module ActiveRecord end def self.run - ActiveRecord::Base.connection_handler.connection_pool_list.map do |pool| - caching_was_enabled = pool.query_cache_enabled - - pool.enable_query_cache! - - [pool, caching_was_enabled] - end + ActiveRecord::Base.connection_handler.connection_pool_list. + reject { |p| p.query_cache_enabled }.each { |p| p.enable_query_cache! } end - def self.complete(caching_pools) - caching_pools.each do |pool, caching_was_enabled| - pool.disable_query_cache! unless caching_was_enabled - end + def self.complete(pools) + pools.each { |pool| pool.disable_query_cache! } ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool| pool.release_connection if pool.active_connection? && !pool.connection.transaction_open? diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 662a8bc720..24449e8df3 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -22,6 +22,14 @@ db_namespace = namespace :db do task all: :load_config do ActiveRecord::Tasks::DatabaseTasks.create_all end + + ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| + desc "Create #{spec_name} database for current environment" + task spec_name => :load_config do + db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + ActiveRecord::Tasks::DatabaseTasks.create(db_config.config) + end + 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 or when RAILS_ENV is development, it defaults to creating the development and test databases." @@ -33,6 +41,14 @@ db_namespace = namespace :db do task all: [:load_config, :check_protected_environments] do ActiveRecord::Tasks::DatabaseTasks.drop_all end + + ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| + desc "Drop #{spec_name} database for current environment" + task spec_name => [:load_config, :check_protected_environments] do + db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + ActiveRecord::Tasks::DatabaseTasks.drop(db_config.config) + end + 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 or when RAILS_ENV is development, it defaults to dropping the development and test databases." @@ -57,7 +73,10 @@ db_namespace = namespace :db do desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." task migrate: :load_config do - ActiveRecord::Tasks::DatabaseTasks.migrate + ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| + ActiveRecord::Base.establish_connection(config) + ActiveRecord::Tasks::DatabaseTasks.migrate + end db_namespace["_dump"].invoke end @@ -77,6 +96,15 @@ db_namespace = namespace :db do end namespace :migrate do + ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| + desc "Migrate #{spec_name} database for current environment" + task spec_name => :load_config do + db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + ActiveRecord::Base.establish_connection(db_config.config) + ActiveRecord::Tasks::DatabaseTasks.migrate + end + end + # desc 'Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x).' task redo: :load_config do raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty? @@ -246,10 +274,15 @@ db_namespace = namespace :db do desc "Creates a db/schema.rb file that is portable against any DB supported by Active Record" task dump: :load_config do require "active_record/schema_dumper" - filename = ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema.rb") - File.open(filename, "w:utf-8") do |file| - ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) + + ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :ruby) + File.open(filename, "w:utf-8") do |file| + ActiveRecord::Base.establish_connection(config) + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) + end end + db_namespace["schema:dump"].reenable end @@ -276,22 +309,24 @@ db_namespace = namespace :db do rm_f filename, verbose: false end end - end namespace :structure do desc "Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql" task dump: :load_config do - filename = ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql") - current_config = ActiveRecord::Tasks::DatabaseTasks.current_config - ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename) - - if ActiveRecord::SchemaMigration.table_exists? - File.open(filename, "a") do |f| - f.puts ActiveRecord::Base.connection.dump_schema_information - f.print "\n" + ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| + ActiveRecord::Base.establish_connection(config) + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :sql) + ActiveRecord::Tasks::DatabaseTasks.structure_dump(config, filename) + + if ActiveRecord::SchemaMigration.table_exists? + File.open(filename, "a") do |f| + f.puts ActiveRecord::Base.connection.dump_schema_information + f.print "\n" + end end end + db_namespace["structure:dump"].reenable end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 34e643b2de..c055b97061 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -8,8 +8,8 @@ module ActiveRecord :extending, :unscope] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering, - :reverse_order, :distinct, :create_with, :skip_query_cache, - :skip_preloading] + :reverse_order, :distinct, :create_with, :skip_query_cache] + CLAUSE_METHODS = [:where, :having, :from] INVALID_METHODS_FOR_DELETE_ALL = [:distinct, :group, :having] @@ -19,6 +19,7 @@ module ActiveRecord include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation attr_reader :table, :klass, :loaded, :predicate_builder + attr_accessor :skip_preloading_value alias :model :klass alias :loaded? :loaded alias :locked? :lock_value @@ -30,6 +31,7 @@ module ActiveRecord @offsets = {} @loaded = false @predicate_builder = predicate_builder + @delegate_to_klass = false end def initialize_copy(other) @@ -313,6 +315,13 @@ module ActiveRecord klass.current_scope = previous end + def _exec_scope(*args, &block) # :nodoc: + @delegate_to_klass = true + instance_exec(*args, &block) || self + ensure + @delegate_to_klass = false + end + # Updates all records in the current relation with details given. This method constructs a single SQL UPDATE # statement and sends it straight to the database. It does not instantiate the involved models and it does not # trigger Active Record callbacks or validations. However, values passed to #update_all will still go through @@ -360,6 +369,43 @@ module ActiveRecord @klass.connection.update stmt, "#{@klass} Update All" end + # Touches all records in the current relation without instantiating records first with the updated_at/on attributes + # set to the current time or the time specified. + # This method can be passed attribute names and an optional time argument. + # If attribute names are passed, they are updated along with updated_at/on attributes. + # If no time argument is passed, the current time is used as default. + # + # === Examples + # + # # Touch all records + # Person.all.touch_all + # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670'" + # + # # Touch multiple records with a custom attribute + # Person.all.touch_all(:created_at) + # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670', \"created_at\" = '2018-01-04 22:55:23.132670'" + # + # # Touch multiple records with a specified time + # Person.all.touch_all(time: Time.new(2020, 5, 16, 0, 0, 0)) + # # => "UPDATE \"people\" SET \"updated_at\" = '2020-05-16 00:00:00'" + # + # # Touch records with scope + # Person.where(name: 'David').touch_all + # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670' WHERE \"people\".\"name\" = 'David'" + def touch_all(*names, time: nil) + attributes = Array(names) + klass.timestamp_attributes_for_update_in_model + time ||= klass.current_time_from_proper_timezone + updates = {} + attributes.each { |column| updates[column] = time } + + if klass.locking_enabled? + quoted_locking_column = connection.quote_column_name(klass.locking_column) + updates = sanitize_sql_for_assignment(updates) + ", #{quoted_locking_column} = COALESCE(#{quoted_locking_column}, 0) + 1" + end + + update_all(updates) + end + # Destroys the records by instantiating each # record and calling its {#destroy}[rdoc-ref:Persistence#destroy] method. # Each object's callbacks are executed (including <tt>:dependent</tt> association options). @@ -446,6 +492,7 @@ module ActiveRecord end def reset + @delegate_to_klass = false @to_sql = @arel = @loaded = @should_eager_load = nil @records = [].freeze @offsets = {} @@ -547,7 +594,7 @@ module ActiveRecord ActiveRecord::Associations::AliasTracker.create(connection, table.name, joins) end - def preload_associations(records) + def preload_associations(records) # :nodoc: preload = preload_values preload += includes_values unless eager_loading? preloader = nil diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 4b16b49cdf..488f71cdde 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -82,6 +82,11 @@ module ActiveRecord if @klass.respond_to?(method) self.class.delegate_to_scoped_klass(method) scoping { @klass.public_send(method, *args, &block) } + elsif @delegate_to_klass && @klass.respond_to?(method, true) + ActiveSupport::Deprecation.warn \ + "Delegating missing #{method} method to #{@klass}. " \ + "Accessibility of private/protected class methods in :scope is deprecated and will be removed in Rails 6.0." + @klass.send(method, *args, &block) elsif arel.respond_to?(method) ActiveSupport::Deprecation.warn \ "Delegating #{method} to arel is deprecated and will be removed in Rails 6.0." diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 4e60863e52..a180b0f0d3 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -894,8 +894,8 @@ module ActiveRecord self end - def skip_query_cache! # :nodoc: - self.skip_query_cache_value = true + def skip_query_cache!(value = true) # :nodoc: + self.skip_query_cache_value = value self end diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 562e04194c..b092399657 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -10,6 +10,7 @@ module ActiveRecord def spawn #:nodoc: clone end + alias :all :spawn # Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an ActiveRecord::Relation. # Returns an array representing the intersection of the resulting records with <tt>other</tt>, if <tt>other</tt> is an array. diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index b8d848b999..9974c28445 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -17,6 +17,12 @@ module ActiveRecord # Only strings are accepted if ActiveRecord::Base.schema_format == :sql. cattr_accessor :ignore_tables, default: [] + ## + # :singleton-method: + # Specify a custom regular expression matching foreign keys which name + # should not be dumped to db/schema.rb. + cattr_accessor :fk_ignore_pattern, default: /^fk_rails_[0-9a-f]{10}$/ + class << self def dump(connection = ActiveRecord::Base.connection, stream = STDOUT, config = ActiveRecord::Base) connection.create_schema_dumper(generate_options(config)).dump(stream) @@ -210,7 +216,7 @@ HEADER parts << "primary_key: #{foreign_key.primary_key.inspect}" end - if foreign_key.name !~ /^fk_rails_[0-9a-f]{10}$/ + if foreign_key.export_name_on_schema_dump? parts << "name: #{foreign_key.name.inspect}" end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 752655aa05..a784001587 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -183,7 +183,7 @@ module ActiveRecord if body.respond_to?(:to_proc) singleton_class.send(:define_method, name) do |*args| scope = all - scope = scope.instance_exec(*args, &body) || scope + scope = scope._exec_scope(*args, &body) scope = scope.extending(extension) if extension scope end diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 3290675338..8d628359c3 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -31,10 +31,14 @@ module ActiveRecord # # class User < ActiveRecord::Base # store :settings, accessors: [ :color, :homepage ], coder: JSON + # store :parent, accessors: [ :name ], coder: JSON, prefix: true + # store :spouse, accessors: [ :name ], coder: JSON, prefix: :partner # end # - # u = User.new(color: 'black', homepage: '37signals.com') + # u = User.new(color: 'black', homepage: '37signals.com', parent_name: 'Mary', partner_name: 'Lily') # u.color # Accessor stored attribute + # u.parent_name # Accessor stored attribute with prefix + # u.partner_name # Accessor stored attribute with custom prefix # u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor # # # There is no difference between strings and symbols for accessing custom attributes @@ -44,6 +48,7 @@ module ActiveRecord # # Add additional accessors to an existing store through store_accessor # class SuperUser < User # store_accessor :settings, :privileges, :servants + # store_accessor :parent, :birthday, prefix: true # end # # The stored attribute names can be retrieved using {.stored_attributes}[rdoc-ref:rdoc-ref:ClassMethods#stored_attributes]. @@ -81,19 +86,29 @@ module ActiveRecord module ClassMethods def store(store_attribute, options = {}) serialize store_attribute, IndifferentCoder.new(store_attribute, options[:coder]) - store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors + store_accessor(store_attribute, options[:accessors], prefix: options[:prefix]) if options.has_key? :accessors end - def store_accessor(store_attribute, *keys) + def store_accessor(store_attribute, *keys, prefix: nil) keys = keys.flatten + accessor_prefix = + case prefix + when String, Symbol + "#{prefix}_" + when TrueClass + "#{store_attribute}_" + else + "" + end + _store_accessors_module.module_eval do keys.each do |key| - define_method("#{key}=") do |value| + define_method("#{accessor_prefix}#{key}=") do |value| write_store_attribute(store_attribute, key, value) end - define_method(key) do + define_method("#{accessor_prefix}#{key}") do read_store_attribute(store_attribute, key) end end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index af1bbc7e93..521375954b 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -134,6 +134,18 @@ module ActiveRecord end end + def for_each + databases = Rails.application.config.load_database_yaml + database_configs = ActiveRecord::DatabaseConfigurations.configs_for(Rails.env, databases) + + # if this is a single database application we don't want tasks for each primary database + return if database_configs.count == 1 + + database_configs.each do |db_config| + yield db_config.spec_name + end + end + def create_current(environment = env) each_current_configuration(environment) { |configuration| create configuration @@ -233,8 +245,8 @@ module ActiveRecord class_for_adapter(configuration["adapter"]).new(*arguments).structure_load(filename, structure_load_flags) end - def load_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil, environment = env) # :nodoc: - file ||= schema_file(format) + def load_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil, environment = env, spec_name = "primary") # :nodoc: + file ||= dump_filename(spec_name, format) check_schema_file(file) ActiveRecord::Base.establish_connection(configuration) @@ -252,17 +264,31 @@ module ActiveRecord end def schema_file(format = ActiveRecord::Base.schema_format) + File.join(db_dir, schema_file_type(format)) + end + + def schema_file_type(format = ActiveRecord::Base.schema_format) case format when :ruby - File.join(db_dir, "schema.rb") + "schema.rb" when :sql - File.join(db_dir, "structure.sql") + "structure.sql" end end + def dump_filename(namespace, format = ActiveRecord::Base.schema_format) + filename = if namespace == "primary" + schema_file_type(format) + else + "#{namespace}_#{schema_file_type(format)}" + end + + ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, filename) + end + def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env) - each_current_configuration(environment) { |configuration, configuration_environment| - load_schema configuration, format, file, configuration_environment + each_current_configuration(environment) { |configuration, spec_name, env| + load_schema(configuration, format, file, env, spec_name) } ActiveRecord::Base.establish_connection(environment.to_sym) end @@ -312,10 +338,10 @@ module ActiveRecord environments = [environment] environments << "test" if environment == "development" - ActiveRecord::Base.configurations.slice(*environments).each do |configuration_environment, configuration| - next unless configuration["database"] - - yield configuration, configuration_environment + environments.each do |env| + ActiveRecord::DatabaseConfigurations.configs_for(env) do |spec_name, configuration| + yield configuration, spec_name, env + end end end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 5da3759e5a..e47f06bf3a 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -52,16 +52,20 @@ module ActiveRecord clear_timestamp_attributes end - class_methods do + module ClassMethods # :nodoc: + def timestamp_attributes_for_update_in_model + timestamp_attributes_for_update.select { |c| column_names.include?(c) } + end + + def current_time_from_proper_timezone + default_timezone == :utc ? Time.now.utc : Time.now + end + private def timestamp_attributes_for_create_in_model timestamp_attributes_for_create.select { |c| column_names.include?(c) } end - def timestamp_attributes_for_update_in_model - timestamp_attributes_for_update.select { |c| column_names.include?(c) } - end - def all_timestamp_attributes_in_model timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model end @@ -73,10 +77,6 @@ module ActiveRecord def timestamp_attributes_for_update ["updated_at", "updated_on"] end - - def current_time_from_proper_timezone - default_timezone == :utc ? Time.now.utc : Time.now - end end private @@ -116,7 +116,7 @@ module ActiveRecord end def timestamp_attributes_for_update_in_model - self.class.send(:timestamp_attributes_for_update_in_model) + self.class.timestamp_attributes_for_update_in_model end def all_timestamp_attributes_in_model @@ -124,7 +124,7 @@ module ActiveRecord end def current_time_from_proper_timezone - self.class.send(:current_time_from_proper_timezone) + self.class.current_time_from_proper_timezone end def max_updated_column_timestamp(timestamp_names = timestamp_attributes_for_update_in_model) diff --git a/activerecord/lib/active_record/translation.rb b/activerecord/lib/active_record/translation.rb index 3cf70eafb8..82661a328a 100644 --- a/activerecord/lib/active_record/translation.rb +++ b/activerecord/lib/active_record/translation.rb @@ -10,7 +10,7 @@ module ActiveRecord classes = [klass] return classes if klass == ActiveRecord::Base - while klass != klass.base_class + while !klass.base_class? classes << klass = klass.superclass end classes diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 4c41a68407..02c7e47583 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -84,7 +84,7 @@ module ActiveRecord indexes = @connection.indexes("accounts") assert_equal "accounts", indexes.first.table assert_equal idx_name, indexes.first.name - assert !indexes.first.unique + assert_not indexes.first.unique assert_equal ["firm_id"], indexes.first.columns ensure @connection.remove_index(:accounts, name: idx_name) rescue nil diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index 9ae2c42368..976c5dde58 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -148,8 +148,8 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase t.timestamps null: true end ActiveRecord::Base.connection.remove_timestamps :delete_me, null: true - assert !column_present?("delete_me", "updated_at", "datetime") - assert !column_present?("delete_me", "created_at", "datetime") + assert_not column_present?("delete_me", "updated_at", "datetime") + assert_not column_present?("delete_me", "created_at", "datetime") ensure ActiveRecord::Base.connection.drop_table :delete_me rescue nil end diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index 140d7cbcae..f05dcac7dd 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -116,8 +116,8 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase end end - assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null - assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + assert_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null + assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null end def test_timestamps_without_null_set_null_to_false_on_change_table @@ -129,8 +129,8 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase end end - assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null - assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + assert_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null + assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null end def test_timestamps_without_null_set_null_to_false_on_add_timestamps @@ -139,7 +139,7 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase add_timestamps :has_timestamps, default: Time.now end - assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null - assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + assert_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null + assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index f18c6177ac..f46be8734b 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -1218,6 +1218,7 @@ class EagerAssociationTest < ActiveRecord::TestCase client = assert_queries(2) { Client.preload(:firm).find(c.id) } assert_no_queries { assert_nil client.firm } + assert_equal c.client_of, client.client_of end def test_preloading_empty_belongs_to_polymorphic @@ -1225,6 +1226,7 @@ class EagerAssociationTest < ActiveRecord::TestCase tagging = assert_queries(2) { Tagging.preload(:taggable).find(t.id) } assert_no_queries { assert_nil tagging.taggable } + assert_equal t.taggable_id, tagging.taggable_id end def test_preloading_through_empty_belongs_to @@ -1513,6 +1515,35 @@ class EagerAssociationTest < ActiveRecord::TestCase Author.preload(:readonly_comments).first! end + test "preloading through a polymorphic association doesn't require the association to exist" do + sponsors = [] + assert_queries 5 do + sponsors = Sponsor.where(sponsorable_id: 1).preload(sponsorable: [:post, :membership]).to_a + end + # check the preload worked + assert_queries 0 do + sponsors.map(&:sponsorable).map { |s| s.respond_to?(:posts) ? s.post.author : s.membership } + end + end + + test "preloading a regular association through a polymorphic association doesn't require the association to exist on all types" do + sponsors = [] + assert_queries 6 do + sponsors = Sponsor.where(sponsorable_id: 1).preload(sponsorable: [{ post: :first_comment }, :membership]).to_a + end + # check the preload worked + assert_queries 0 do + sponsors.map(&:sponsorable).map { |s| s.respond_to?(:posts) ? s.post.author : s.membership } + end + end + + test "preloading a regular association with a typo through a polymorphic association still raises" do + # this test contains an intentional typo of first -> fist + assert_raises(ActiveRecord::AssociationNotFoundError) do + Sponsor.where(sponsorable_id: 1).preload(sponsorable: [{ post: :fist_comment }, :membership]).to_a + end + end + private def find_all_ordered(klass, include = nil) klass.order("#{klass.table_name}.#{klass.primary_key}").includes(include).to_a 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 5f771fe85f..5d9735d98a 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 @@ -569,7 +569,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase developer = Developer.create name: "Bryan", salary: 50_000 assert_not_predicate project.developers, :loaded? - assert ! project.developers.include?(developer) + assert_not project.developers.include?(developer) end def test_find_with_merged_options diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 00821f2319..33fe5ccabc 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -497,8 +497,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase person = Person.new person.first_name = "Naruto" person.references << Reference.new - person.id = 10 - person.references person.save! assert_equal 1, person.references.update_all(favourite: true) end @@ -507,8 +505,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase person = Person.new person.first_name = "Sasuke" person.references << Reference.new - person.id = 10 - person.references person.save! assert_predicate person.references, :exists? end @@ -987,6 +983,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_not_empty company.contracts end + def test_collection_size_with_dirty_target + post = posts(:thinking) + assert_equal [], post.reader_ids + assert_equal 0, post.readers.size + post.readers.reset + post.readers.build + assert_equal [], post.reader_ids + assert_equal 1, post.readers.size + end + def test_collection_size_twice_for_regressions post = posts(:thinking) assert_equal 0, post.readers.size @@ -2013,7 +2019,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_calling_none_on_loaded_association_should_not_use_query firm = companies(:first_firm) firm.clients.load # force load - assert_no_queries { assert ! firm.clients.none? } + assert_no_queries { assert_not firm.clients.none? } end def test_calling_none_should_defer_to_collection_if_using_a_block @@ -2048,7 +2054,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_calling_one_on_loaded_association_should_not_use_query firm = companies(:first_firm) firm.clients.load # force load - assert_no_queries { assert ! firm.clients.one? } + assert_no_queries { assert_not firm.clients.one? } end def test_calling_one_should_defer_to_collection_if_using_a_block 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 3b3d4037b9..0facc286da 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -866,7 +866,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase author = authors(:mary) category = author.named_categories.create(name: "Primary") author.named_categories.delete(category) - assert !Categorization.exists?(author_id: author.id, named_category_name: category.name) + assert_not Categorization.exists?(author_id: author.id, named_category_name: category.name) assert_empty author.named_categories.reload end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 602fe52701..d7e898a1c0 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -452,7 +452,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal new_ship, pirate.ship assert_predicate new_ship, :new_record? assert_nil orig_ship.pirate_id - assert !orig_ship.changed? # check it was saved + assert_not orig_ship.changed? # check it was saved end def test_creation_failure_with_dependent_option @@ -678,7 +678,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase book = SpecialBook.create!(status: "published") author.book = book - refute_equal 0, SpecialAuthor.joins(:book).where(books: { status: "published" }).count + assert_not_equal 0, SpecialAuthor.joins(:book).where(books: { status: "published" }).count end def test_association_enum_works_properly_with_nested_join diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index 9964f084ac..0309663943 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -64,6 +64,24 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase assert_equal clubs(:moustache_club), new_member.club end + def test_building_multiple_associations_builds_through_record + member_type = MemberType.create! + member = Member.create! + member_detail_with_one_association = MemberDetail.new(member_type: member_type) + assert_predicate member_detail_with_one_association.member, :new_record? + member_detail_with_two_associations = MemberDetail.new(member_type: member_type, admittable: member) + assert_predicate member_detail_with_two_associations.member, :new_record? + end + + def test_creating_multiple_associations_creates_through_record + member_type = MemberType.create! + member = Member.create! + member_detail_with_one_association = MemberDetail.create!(member_type: member_type) + assert_not_predicate member_detail_with_one_association.member, :new_record? + member_detail_with_two_associations = MemberDetail.create!(member_type: member_type, admittable: member) + assert_not_predicate member_detail_with_two_associations.member, :new_record? + end + def test_creating_association_sets_both_parent_ids_for_new member = Member.new(name: "Sean Griffin") club = Club.new(name: "Da Club") diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index f19a9f5f7a..9d1c73c33b 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -369,7 +369,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase 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 + assert_not_equal [], tags(:general).tagged_posts end def test_eager_has_many_polymorphic_with_source_type @@ -732,7 +732,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase category = Category.create!(name: "Not Associated") assert_not_predicate david.categories, :loaded? - assert ! david.categories.include?(category) + assert_not david.categories.include?(category) end def test_has_many_through_goes_through_all_sti_classes diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index 0170a6e98d..54512068ee 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -12,7 +12,7 @@ module ActiveRecord def setup @klass = Class.new(Class.new { def self.initialize_generated_modules; end }) do def self.superclass; Base; end - def self.base_class; self; end + def self.base_class?; true; end def self.decorate_matching_attribute_types(*); end include ActiveRecord::DefineCallbacks diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index dc6638d45d..1a37ad963f 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -63,8 +63,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase t.author_name = "" assert t.attribute_present?("title") assert t.attribute_present?("written_on") - assert !t.attribute_present?("content") - assert !t.attribute_present?("author_name") + assert_not t.attribute_present?("content") + assert_not t.attribute_present?("author_name") end test "attribute_present with booleans" do @@ -77,7 +77,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert b2.attribute_present?(:value) b3 = Boolean.new - assert !b3.attribute_present?(:value) + assert_not b3.attribute_present?(:value) b4 = Boolean.new b4.value = false @@ -827,7 +827,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase self.table_name = "computers" end - assert !klass.instance_method_already_implemented?(:system) + assert_not klass.instance_method_already_implemented?(:system) computer = klass.new assert_nil computer.system end @@ -841,8 +841,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase self.table_name = "computers" end - assert !klass.instance_method_already_implemented?(:system) - assert !subklass.instance_method_already_implemented?(:system) + assert_not klass.instance_method_already_implemented?(:system) + assert_not subklass.instance_method_already_implemented?(:system) computer = subklass.new assert_nil computer.system end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index b8243d148a..7915599f72 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -116,7 +116,7 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas assert_not_predicate firm.account, :valid? assert_not_predicate firm, :valid? - assert !firm.save + assert_not firm.save assert_equal ["is invalid"], firm.errors["account"] end @@ -237,7 +237,7 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test log.developer = Developer.new assert_not_predicate log.developer, :valid? assert_not_predicate log, :valid? - assert !log.save + assert_not log.save assert_equal ["is invalid"], log.errors["developer"] end @@ -499,10 +499,10 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_invalid_adding firm = Firm.find(1) - assert !(firm.clients_of_firm << c = Client.new) + assert_not (firm.clients_of_firm << c = Client.new) assert_not_predicate c, :persisted? assert_not_predicate firm, :valid? - assert !firm.save + assert_not firm.save assert_not_predicate c, :persisted? end @@ -512,7 +512,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa assert_not_predicate c, :persisted? assert_not_predicate c, :valid? assert_not_predicate new_firm, :valid? - assert !new_firm.save + assert_not new_firm.save assert_not_predicate c, :persisted? assert_not_predicate new_firm, :persisted? end @@ -550,7 +550,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa assert_not_predicate new_client, :persisted? assert_not_predicate new_client, :valid? assert_equal new_client, companies(:first_firm).clients_of_firm.last - assert !companies(:first_firm).save + assert_not companies(:first_firm).save assert_not_predicate new_client, :persisted? assert_equal 2, companies(:first_firm).clients_of_firm.reload.size end @@ -795,7 +795,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase @ship.pirate.catchphrase = "Changed Catchphrase" @ship.name_will_change! - assert_raise(RuntimeError) { assert !@pirate.save } + assert_raise(RuntimeError) { assert_not @pirate.save } assert_not_nil @pirate.reload.ship end @@ -855,7 +855,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase @ship.pirate.catchphrase = "Changed Catchphrase" - assert_raise(RuntimeError) { assert !@ship.save } + assert_raise(RuntimeError) { assert_not @ship.save } assert_not_nil @ship.reload.pirate end @@ -871,7 +871,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase def test_should_destroy_has_many_as_part_of_the_save_transaction_if_they_were_marked_for_destruction 2.times { |i| @pirate.birds.create!(name: "birds_#{i}") } - assert !@pirate.birds.any?(&:marked_for_destruction?) + assert_not @pirate.birds.any?(&:marked_for_destruction?) @pirate.birds.each(&:mark_for_destruction) klass = @pirate.birds.first.class @@ -937,7 +937,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase end end - assert_raise(RuntimeError) { assert !@pirate.save } + assert_raise(RuntimeError) { assert_not @pirate.save } assert_equal before, @pirate.reload.birds end @@ -1003,7 +1003,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase def test_should_destroy_habtm_as_part_of_the_save_transaction_if_they_were_marked_for_destruction 2.times { |i| @pirate.parrots.create!(name: "parrots_#{i}") } - assert !@pirate.parrots.any?(&:marked_for_destruction?) + assert_not @pirate.parrots.any?(&:marked_for_destruction?) @pirate.parrots.each(&:mark_for_destruction) assert_no_difference "Parrot.count" do @@ -1065,7 +1065,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase end end - assert_raise(RuntimeError) { assert !@pirate.save } + assert_raise(RuntimeError) { assert_not @pirate.save } assert_equal before, @pirate.reload.parrots end @@ -1213,7 +1213,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase assert_no_difference "Pirate.count" do assert_no_difference "Ship.count" do - assert !pirate.save + assert_not pirate.save end end end @@ -1232,7 +1232,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase end end - assert_raise(RuntimeError) { assert !@pirate.save } + assert_raise(RuntimeError) { assert_not @pirate.save } assert_equal before, [@pirate.reload.catchphrase, @pirate.ship.name] end @@ -1337,7 +1337,7 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase assert_no_difference "Ship.count" do assert_no_difference "Pirate.count" do - assert !ship.save + assert_not ship.save end end end @@ -1356,7 +1356,7 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase end end - assert_raise(RuntimeError) { assert !@ship.save } + assert_raise(RuntimeError) { assert_not @ship.save } assert_equal before, [@ship.pirate.reload.catchphrase, @ship.reload.name] end @@ -1480,7 +1480,7 @@ module AutosaveAssociationOnACollectionAssociationTests @child_1.name = "Changed" @child_1.cancel_save_from_callback = true - assert !@pirate.save + assert_not @pirate.save assert_equal "Don' botharrr talkin' like one, savvy?", @pirate.reload.catchphrase assert_equal "Posideons Killer", @child_1.reload.name @@ -1490,7 +1490,7 @@ module AutosaveAssociationOnACollectionAssociationTests assert_no_difference "Pirate.count" do assert_no_difference "#{new_child.class.name}.count" do - assert !new_pirate.save + assert_not new_pirate.save end end end @@ -1510,7 +1510,7 @@ module AutosaveAssociationOnACollectionAssociationTests end end - assert_raise(RuntimeError) { assert !@pirate.save } + assert_raise(RuntimeError) { assert_not @pirate.save } assert_equal before, [@pirate.reload.catchphrase, *@pirate.send(@association_name).map(&:name)] end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 7dfb05a6a5..fd008ca8e3 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -307,7 +307,7 @@ class BasicsTest < ActiveRecord::TestCase assert_equal "Dude", cbs[0].name assert_equal "Bob", cbs[1].name assert cbs[0].frickinawesome - assert !cbs[1].frickinawesome + assert_not cbs[1].frickinawesome end def test_load @@ -856,11 +856,11 @@ class BasicsTest < ActiveRecord::TestCase def test_clone_of_new_object_marks_as_dirty_only_changed_attributes developer = Developer.new name: "Bjorn" assert developer.name_changed? # obviously - assert !developer.salary_changed? # attribute has non-nil default value, so treated as not changed + assert_not developer.salary_changed? # attribute has non-nil default value, so treated as not changed cloned_developer = developer.clone assert_predicate cloned_developer, :name_changed? - assert !cloned_developer.salary_changed? # ... and cloned instance should behave same + assert_not cloned_developer.salary_changed? # ... and cloned instance should behave same end def test_dup_of_saved_object_marks_attributes_as_dirty @@ -875,12 +875,12 @@ class BasicsTest < ActiveRecord::TestCase def test_dup_of_saved_object_marks_as_dirty_only_changed_attributes developer = Developer.create! name: "Bjorn" - assert !developer.name_changed? # both attributes of saved object should be treated as not changed + assert_not developer.name_changed? # both attributes of saved object should be treated as not changed assert_not_predicate developer, :salary_changed? cloned_developer = developer.dup assert cloned_developer.name_changed? # ... but on cloned object should be - assert !cloned_developer.salary_changed? # ... BUT salary has non-nil default which should be treated as not changed on cloned instance + assert_not cloned_developer.salary_changed? # ... BUT salary has non-nil default which should be treated as not changed on cloned instance end def test_bignum @@ -1497,7 +1497,7 @@ class BasicsTest < ActiveRecord::TestCase end test "column names are quoted when using #from clause and model has ignored columns" do - refute_empty Developer.ignored_columns + assert_not_empty Developer.ignored_columns query = Developer.from("developers").to_sql quoted_id = "#{Developer.quoted_table_name}.#{Developer.quoted_primary_key}" diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 3b283a3aa6..b9ba51c730 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -385,9 +385,9 @@ class CallbacksTest < ActiveRecord::TestCase end def assert_save_callbacks_not_called(someone) - assert !someone.after_save_called - assert !someone.after_create_called - assert !someone.after_update_called + assert_not someone.after_save_called + assert_not someone.after_create_called + assert_not someone.after_update_called end private :assert_save_callbacks_not_called @@ -395,27 +395,27 @@ class CallbacksTest < ActiveRecord::TestCase someone = CallbackHaltedDeveloper.new someone.cancel_before_create = true assert_predicate someone, :valid? - assert !someone.save + assert_not someone.save assert_save_callbacks_not_called(someone) end def test_before_save_throwing_abort david = DeveloperWithCanceledCallbacks.find(1) assert_predicate david, :valid? - assert !david.save + assert_not david.save exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! } assert_equal david, exc.record david = DeveloperWithCanceledCallbacks.find(1) david.salary = 10_000_000 assert_not_predicate david, :valid? - assert !david.save + assert_not david.save assert_raise(ActiveRecord::RecordInvalid) { david.save! } someone = CallbackHaltedDeveloper.find(1) someone.cancel_before_save = true assert_predicate someone, :valid? - assert !someone.save + assert_not someone.save assert_save_callbacks_not_called(someone) end @@ -423,22 +423,22 @@ class CallbacksTest < ActiveRecord::TestCase someone = CallbackHaltedDeveloper.find(1) someone.cancel_before_update = true assert_predicate someone, :valid? - assert !someone.save + assert_not someone.save assert_save_callbacks_not_called(someone) end def test_before_destroy_throwing_abort david = DeveloperWithCanceledCallbacks.find(1) - assert !david.destroy + assert_not david.destroy exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! } assert_equal david, exc.record assert_not_nil ImmutableDeveloper.find_by_id(1) someone = CallbackHaltedDeveloper.find(1) someone.cancel_before_destroy = true - assert !someone.destroy + assert_not someone.destroy assert_raise(ActiveRecord::RecordNotDestroyed) { someone.destroy! } - assert !someone.after_destroy_called + assert_not someone.after_destroy_called end def test_callback_throwing_abort @@ -467,12 +467,12 @@ class CallbacksTest < ActiveRecord::TestCase def test_inheritance_of_callbacks parent = ParentDeveloper.new - assert !parent.after_save_called + assert_not parent.after_save_called parent.save assert parent.after_save_called child = ChildDeveloper.new - assert !child.after_save_called + assert_not child.after_save_called child.save assert child.after_save_called end diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index c06a4e2c52..b8e623f17b 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -89,11 +89,12 @@ module ActiveRecord ActiveRecord::Base.establish_connection - assert_equal "db/primary.sqlite3", ActiveRecord::Base.connection.pool.spec.config[:database] + assert_match "db/primary.sqlite3", ActiveRecord::Base.connection.pool.spec.config[:database] ensure ActiveRecord::Base.configurations = @prev_configs ENV["RAILS_ENV"] = previous_env ActiveRecord::Base.establish_connection(:arunit) + FileUtils.rm_rf "db" end def test_establish_connection_using_2_level_config_defaults_to_default_env_primary_db @@ -111,11 +112,12 @@ module ActiveRecord ActiveRecord::Base.establish_connection - assert_equal "db/primary.sqlite3", ActiveRecord::Base.connection.pool.spec.config[:database] + assert_match "db/primary.sqlite3", ActiveRecord::Base.connection.pool.spec.config[:database] ensure ActiveRecord::Base.configurations = @prev_configs ENV["RAILS_ENV"] = previous_env ActiveRecord::Base.establish_connection(:arunit) + FileUtils.rm_rf "db" end end @@ -326,7 +328,7 @@ module ActiveRecord pool = klass2.establish_connection(ActiveRecord::Base.connection_pool.spec.config) assert_same klass2.connection, pool.connection - refute_same klass2.connection, ActiveRecord::Base.connection + assert_not_same klass2.connection, ActiveRecord::Base.connection klass2.remove_connection @@ -345,7 +347,7 @@ module ActiveRecord 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 + assert_not_nil ActiveRecord::Base.connection assert_same klass2.connection, ActiveRecord::Base.connection end end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 2f1434b2bc..83cc2aa319 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -366,7 +366,7 @@ class DirtyTest < ActiveRecord::TestCase def test_changed_attributes_should_be_preserved_if_save_failure pirate = Pirate.new pirate.parrot_id = 1 - assert !pirate.save + assert_not pirate.save check_pirate_after_save_failure(pirate) pirate = Pirate.new @@ -473,6 +473,14 @@ class DirtyTest < ActiveRecord::TestCase end end + def test_changes_to_save_should_not_mutate_array_of_hashes + topic = Topic.new(author_name: "Bill", content: [{ a: "a" }]) + + topic.changes_to_save + + assert_equal [{ a: "a" }], topic.content + end + def test_previous_changes # original values should be in previous_changes pirate = Pirate.new @@ -488,7 +496,7 @@ class DirtyTest < ActiveRecord::TestCase assert_not_nil pirate.previous_changes["updated_on"][1] assert_nil pirate.previous_changes["created_on"][0] assert_not_nil pirate.previous_changes["created_on"][1] - assert !pirate.previous_changes.key?("parrot_id") + assert_not pirate.previous_changes.key?("parrot_id") # original values should be in previous_changes pirate = Pirate.new @@ -502,7 +510,7 @@ class DirtyTest < ActiveRecord::TestCase assert_equal [nil, pirate.id], pirate.previous_changes["id"] assert_includes pirate.previous_changes, "updated_on" assert_includes pirate.previous_changes, "created_on" - assert !pirate.previous_changes.key?("parrot_id") + assert_not pirate.previous_changes.key?("parrot_id") pirate.catchphrase = "Yar!!" pirate.reload @@ -519,8 +527,8 @@ class DirtyTest < ActiveRecord::TestCase assert_equal ["arrr", "Me Maties!"], pirate.previous_changes["catchphrase"] assert_not_nil pirate.previous_changes["updated_on"][0] assert_not_nil pirate.previous_changes["updated_on"][1] - assert !pirate.previous_changes.key?("parrot_id") - assert !pirate.previous_changes.key?("created_on") + assert_not pirate.previous_changes.key?("parrot_id") + assert_not pirate.previous_changes.key?("created_on") pirate = Pirate.find_by_catchphrase("Me Maties!") @@ -533,8 +541,8 @@ class DirtyTest < ActiveRecord::TestCase assert_equal ["Me Maties!", "Thar She Blows!"], pirate.previous_changes["catchphrase"] assert_not_nil pirate.previous_changes["updated_on"][0] assert_not_nil pirate.previous_changes["updated_on"][1] - assert !pirate.previous_changes.key?("parrot_id") - assert !pirate.previous_changes.key?("created_on") + assert_not pirate.previous_changes.key?("parrot_id") + assert_not pirate.previous_changes.key?("created_on") travel(1.second) @@ -545,8 +553,8 @@ class DirtyTest < ActiveRecord::TestCase assert_equal ["Thar She Blows!", "Ahoy!"], pirate.previous_changes["catchphrase"] assert_not_nil pirate.previous_changes["updated_on"][0] assert_not_nil pirate.previous_changes["updated_on"][1] - assert !pirate.previous_changes.key?("parrot_id") - assert !pirate.previous_changes.key?("created_on") + assert_not pirate.previous_changes.key?("parrot_id") + assert_not pirate.previous_changes.key?("created_on") travel(1.second) @@ -557,8 +565,8 @@ class DirtyTest < ActiveRecord::TestCase assert_equal ["Ahoy!", "Ninjas suck!"], pirate.previous_changes["catchphrase"] assert_not_nil pirate.previous_changes["updated_on"][0] assert_not_nil pirate.previous_changes["updated_on"][1] - assert !pirate.previous_changes.key?("parrot_id") - assert !pirate.previous_changes.key?("created_on") + assert_not pirate.previous_changes.key?("parrot_id") + assert_not pirate.previous_changes.key?("created_on") ensure travel_back end diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb index 9e33c3110c..387a0b1fdd 100644 --- a/activerecord/test/cases/dup_test.rb +++ b/activerecord/test/cases/dup_test.rb @@ -171,7 +171,7 @@ module ActiveRecord end end - assert !movie.persisted? + assert_not movie.persisted? end end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index d85910d9c6..04150f4d57 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -727,8 +727,8 @@ class FinderTest < ActiveRecord::TestCase assert_raise(ActiveModel::MissingAttributeError) { topic.title? } assert_nil topic.read_attribute("title") assert_equal "David", topic.author_name - assert !topic.attribute_present?("title") - assert !topic.attribute_present?(:title) + assert_not topic.attribute_present?("title") + assert_not topic.attribute_present?(:title) assert topic.attribute_present?("author_name") assert_respond_to topic, "author_name" end diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 184b750161..a4fa3c285b 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -592,10 +592,10 @@ class FixturesWithoutInstantiationTest < ActiveRecord::TestCase fixtures :topics, :developers, :accounts def test_without_complete_instantiation - assert !defined?(@first) - assert !defined?(@topics) - assert !defined?(@developers) - assert !defined?(@accounts) + assert_not defined?(@first) + assert_not defined?(@topics) + assert_not defined?(@developers) + assert_not defined?(@accounts) end def test_fixtures_from_root_yml_without_instantiation @@ -1082,13 +1082,13 @@ class FoxyFixturesTest < ActiveRecord::TestCase def test_supports_inline_habtm assert(parrots(:george).treasures.include?(treasures(:diamond))) assert(parrots(:george).treasures.include?(treasures(:sapphire))) - assert(!parrots(:george).treasures.include?(treasures(:ruby))) + assert_not(parrots(:george).treasures.include?(treasures(:ruby))) end def test_supports_inline_habtm_with_specified_id assert(parrots(:polly).treasures.include?(treasures(:ruby))) assert(parrots(:polly).treasures.include?(treasures(:sapphire))) - assert(!parrots(:polly).treasures.include?(treasures(:diamond))) + assert_not(parrots(:polly).treasures.include?(treasures(:diamond))) end def test_supports_yaml_arrays diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 6ea02ac191..66f11fe5bd 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -184,4 +184,4 @@ module InTimeZone end end -require "mocha/setup" # FIXME: stop using mocha +require "mocha/minitest" # FIXME: stop using mocha diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index e3ca79af99..7a5c06b894 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -174,17 +174,26 @@ class InheritanceTest < ActiveRecord::TestCase def test_inheritance_base_class assert_equal Post, Post.base_class + assert_predicate Post, :base_class? assert_equal Post, SpecialPost.base_class + assert_not_predicate SpecialPost, :base_class? assert_equal Post, StiPost.base_class + assert_not_predicate StiPost, :base_class? assert_equal Post, SubStiPost.base_class + assert_not_predicate SubStiPost, :base_class? assert_equal SubAbstractStiPost, SubAbstractStiPost.base_class + assert_predicate SubAbstractStiPost, :base_class? end def test_abstract_inheritance_base_class assert_equal LoosePerson, LoosePerson.base_class + assert_predicate LoosePerson, :base_class? assert_equal LooseDescendant, LooseDescendant.base_class + assert_predicate LooseDescendant, :base_class? assert_equal TightPerson, TightPerson.base_class + assert_predicate TightPerson, :base_class? assert_equal TightPerson, TightDescendant.base_class + assert_not_predicate TightDescendant, :base_class? end def test_base_class_activerecord_error diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index ebe0b0aa87..9c85543b9b 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -215,7 +215,7 @@ module ActiveRecord migration = InvertibleMigration.new migration.migrate :up migration.migrate :down - assert !migration.connection.table_exists?("horses") + assert_not migration.connection.table_exists?("horses") end def test_migrate_revert @@ -223,11 +223,11 @@ module ActiveRecord revert = InvertibleRevertMigration.new migration.migrate :up revert.migrate :up - assert !migration.connection.table_exists?("horses") + assert_not migration.connection.table_exists?("horses") revert.migrate :down assert migration.connection.table_exists?("horses") migration.migrate :down - assert !migration.connection.table_exists?("horses") + assert_not migration.connection.table_exists?("horses") end def test_migrate_revert_by_part @@ -241,12 +241,12 @@ module ActiveRecord } migration.migrate :up assert_equal [:both, :up], received - assert !migration.connection.table_exists?("horses") + assert_not migration.connection.table_exists?("horses") assert migration.connection.table_exists?("new_horses") migration.migrate :down assert_equal [:both, :up, :both, :down], received assert migration.connection.table_exists?("horses") - assert !migration.connection.table_exists?("new_horses") + assert_not migration.connection.table_exists?("new_horses") end def test_migrate_revert_whole_migration @@ -255,11 +255,11 @@ module ActiveRecord revert = RevertWholeMigration.new(klass) migration.migrate :up revert.migrate :up - assert !migration.connection.table_exists?("horses") + assert_not migration.connection.table_exists?("horses") revert.migrate :down assert migration.connection.table_exists?("horses") migration.migrate :down - assert !migration.connection.table_exists?("horses") + assert_not migration.connection.table_exists?("horses") end end @@ -268,7 +268,7 @@ module ActiveRecord revert.migrate :down assert revert.connection.table_exists?("horses") revert.migrate :up - assert !revert.connection.table_exists?("horses") + assert_not revert.connection.table_exists?("horses") end def test_migrate_revert_change_column_default @@ -402,7 +402,7 @@ module ActiveRecord UpOnlyMigration.new.migrate(:down) # should be no error connection = ActiveRecord::Base.connection - assert !connection.column_exists?(:horses, :oldie) + assert_not connection.column_exists?(:horses, :oldie) Horse.reset_column_information end end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 9d04750712..8513edb0ab 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -15,6 +15,7 @@ require "models/bulb" require "models/engine" require "models/wheel" require "models/treasure" +require "models/frog" class LockWithoutDefault < ActiveRecord::Base; end @@ -653,6 +654,16 @@ unless in_memory_db? end end + def test_locking_in_after_save_callback + assert_nothing_raised do + frog = ::Frog.create(name: "Old Frog") + frog.name = "New Frog" + assert_not_deprecated do + frog.save! + end + end + end + def test_with_lock_commits_transaction person = Person.find 1 person.with_lock do diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index 1494027182..f4d16cb093 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -205,8 +205,8 @@ module ActiveRecord created_at_column = created_columns.detect { |c| c.name == "created_at" } updated_at_column = created_columns.detect { |c| c.name == "updated_at" } - assert !created_at_column.null - assert !updated_at_column.null + assert_not created_at_column.null + assert_not updated_at_column.null end def test_create_table_with_timestamps_should_create_datetime_columns_with_options @@ -408,7 +408,7 @@ module ActiveRecord end connection.change_table :testings do |t| assert t.column_exists?(:foo) - assert !(t.column_exists?(:bar)) + assert_not (t.column_exists?(:bar)) 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 83fb4f9385..a1e5fb1115 100644 --- a/activerecord/test/cases/migration/create_join_table_test.rb +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -95,42 +95,42 @@ module ActiveRecord connection.create_join_table :artists, :musics connection.drop_join_table :artists, :musics - assert !connection.table_exists?("artists_musics") + assert_not connection.table_exists?("artists_musics") end def test_drop_join_table_with_strings connection.create_join_table :artists, :musics connection.drop_join_table "artists", "musics" - assert !connection.table_exists?("artists_musics") + assert_not connection.table_exists?("artists_musics") end def test_drop_join_table_with_the_proper_order connection.create_join_table :videos, :musics connection.drop_join_table :videos, :musics - assert !connection.table_exists?("musics_videos") + assert_not connection.table_exists?("musics_videos") end def test_drop_join_table_with_the_table_name connection.create_join_table :artists, :musics, table_name: :catalog connection.drop_join_table :artists, :musics, table_name: :catalog - assert !connection.table_exists?("catalog") + assert_not connection.table_exists?("catalog") end def test_drop_join_table_with_the_table_name_as_string connection.create_join_table :artists, :musics, table_name: "catalog" connection.drop_join_table :artists, :musics, table_name: "catalog" - assert !connection.table_exists?("catalog") + assert_not connection.table_exists?("catalog") end def test_drop_join_table_with_column_options connection.create_join_table :artists, :musics, column_options: { null: true } connection.drop_join_table :artists, :musics, column_options: { null: true } - assert !connection.table_exists?("artists_musics") + assert_not connection.table_exists?("artists_musics") end def test_create_and_drop_join_table_with_common_prefix diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index de37215e80..50f5696ad1 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -306,6 +306,17 @@ if ActiveRecord::Base.connection.supports_foreign_keys? assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id", name: "fk_name"$}, output end + def test_schema_dumping_with_custom_fk_ignore_pattern + original_pattern = ActiveRecord::SchemaDumper.fk_ignore_pattern + ActiveRecord::SchemaDumper.fk_ignore_pattern = /^ignored_/ + @connection.add_foreign_key :astronauts, :rockets, name: :ignored_fk_astronauts_rockets + + output = dump_table_schema "astronauts" + assert_match %r{\s+add_foreign_key "astronauts", "rockets"$}, output + + ActiveRecord::SchemaDumper.fk_ignore_pattern = original_pattern + end + def test_schema_dumping_on_delete_and_on_update_options @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify, on_update: :cascade diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index b25c6d84bc..f9b2dc0c73 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -99,7 +99,7 @@ module ActiveRecord connection.add_index :testings, :foo assert connection.index_exists?(:testings, :foo) - assert !connection.index_exists?(:testings, :bar) + assert_not connection.index_exists?(:testings, :bar) end def test_index_exists_on_multiple_columns @@ -131,7 +131,7 @@ module ActiveRecord assert connection.index_exists?(:testings, :foo) assert connection.index_exists?(:testings, :foo, name: "custom_index_name") - assert !connection.index_exists?(:testings, :foo, name: "other_index_name") + assert_not connection.index_exists?(:testings, :foo, name: "other_index_name") end def test_remove_named_index @@ -139,7 +139,7 @@ module ActiveRecord assert connection.index_exists?(:testings, :foo) connection.remove_index :testings, :foo - assert !connection.index_exists?(:testings, :foo) + assert_not connection.index_exists?(:testings, :foo) end def test_add_index_attribute_length_limit @@ -203,7 +203,7 @@ module ActiveRecord assert connection.index_exists?("testings", "last_name") connection.remove_index("testings", "last_name") - assert !connection.index_exists?("testings", "last_name") + assert_not connection.index_exists?("testings", "last_name") end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index f77a275544..1fa9a3c34a 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -404,7 +404,7 @@ class MigrationTest < ActiveRecord::TestCase ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "foofoo" new_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call - refute_equal current_env, new_env + assert_not_equal current_env, new_env sleep 1 # mysql by default does not store fractional seconds in the database migrator.up @@ -548,7 +548,7 @@ class MigrationTest < ActiveRecord::TestCase end assert Person.connection.column_exists?(:something, :foo) assert_nothing_raised { Person.connection.remove_column :something, :foo, :bar } - assert !Person.connection.column_exists?(:something, :foo) + assert_not Person.connection.column_exists?(:something, :foo) assert Person.connection.column_exists?(:something, :name) assert Person.connection.column_exists?(:something, :number) ensure @@ -822,7 +822,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? end end - [:qualification, :experience].each { |c| assert ! column(c) } + [:qualification, :experience].each { |c| assert_not column(c) } assert column(:qualification_experience) end @@ -852,7 +852,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? name_age_index = index(:index_delete_me_on_name_and_age) assert_equal ["name", "age"].sort, name_age_index.columns.sort - assert ! name_age_index.unique + assert_not name_age_index.unique assert index(:awesome_username_index).unique end @@ -880,7 +880,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? end end - assert ! index(:index_delete_me_on_name) + assert_not index(:index_delete_me_on_name) new_name_index = index(:new_name_index) assert new_name_index.unique @@ -892,7 +892,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? t.date :birthdate end - assert ! column(:name).default + assert_not column(:name).default assert_equal :date, column(:birthdate).type classname = ActiveRecord::Base.connection.class.name[/[^:]*$/] diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb index a24b173cf5..6f3903eed4 100644 --- a/activerecord/test/cases/multiparameter_attributes_test.rb +++ b/activerecord/test/cases/multiparameter_attributes_test.rb @@ -394,6 +394,6 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase "written_on(4i)" => "13", "written_on(5i)" => "55", ) - refute_predicate topic, :written_on_came_from_user? + assert_not_predicate topic, :written_on_came_from_user? end end diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 1ed3a61bbb..32af90caef 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -83,7 +83,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase def test_a_model_should_respond_to_underscore_destroy_and_return_if_it_is_marked_for_destruction ship = Ship.create!(name: "Nights Dirty Lightning") - assert !ship._destroy + assert_not ship._destroy ship.mark_for_destruction assert ship._destroy end @@ -835,7 +835,7 @@ module NestedAttributesOnACollectionAssociationTests man = Man.create(name: "John") interest = man.interests.create(topic: "bar", zine_id: 0) assert interest.save - assert !man.update(interests_attributes: { id: interest.id, zine_id: "foo" }) + assert_not man.update(interests_attributes: { id: interest.id, zine_id: "foo" }) end end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index e4a65a48ca..e0c5725944 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -48,7 +48,7 @@ class PersistenceTest < ActiveRecord::TestCase end if test_update_with_order_succeeds.call("id DESC") - assert !test_update_with_order_succeeds.call("id ASC") # test that this wasn't a fluke and using an incorrect order results in an exception + assert_not test_update_with_order_succeeds.call("id ASC") # test that this wasn't a fluke and using an incorrect order results in an exception else # test that we're failing because the current Arel's engine doesn't support UPDATE ORDER BY queries is using subselects instead assert_sql(/\AUPDATE .+ \(SELECT .* ORDER BY id DESC\)\z/i) do diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index d635a47c0e..393f363e37 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -441,8 +441,9 @@ class QueryCacheTest < ActiveRecord::TestCase assert_not ActiveRecord::Base.connection_handler.active_connections? # sanity check middleware { - assert ActiveRecord::Base.connection.query_cache_enabled, "QueryCache did not get lazily enabled" + assert_predicate ActiveRecord::Base.connection, :query_cache_enabled }.call({}) + assert_not_predicate ActiveRecord::Base.connection, :query_cache_enabled end end @@ -517,19 +518,19 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase def test_find assert_called(Task.connection, :clear_query_cache) do - assert !Task.connection.query_cache_enabled + assert_not Task.connection.query_cache_enabled Task.cache do assert Task.connection.query_cache_enabled Task.find(1) Task.uncached do - assert !Task.connection.query_cache_enabled + assert_not Task.connection.query_cache_enabled Task.find(1) end assert Task.connection.query_cache_enabled end - assert !Task.connection.query_cache_enabled + assert_not Task.connection.query_cache_enabled end end diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb index 383e43ed55..059fa76132 100644 --- a/activerecord/test/cases/readonly_test.rb +++ b/activerecord/test/cases/readonly_test.rb @@ -23,7 +23,7 @@ class ReadOnlyTest < ActiveRecord::TestCase assert_nothing_raised do dev.name = "Luscious forbidden fruit." - assert !dev.save + assert_not dev.save dev.name = "Forbidden." end @@ -38,8 +38,8 @@ class ReadOnlyTest < ActiveRecord::TestCase end def test_find_with_readonly_option - Developer.all.each { |d| assert !d.readonly? } - Developer.readonly(false).each { |d| assert !d.readonly? } + Developer.all.each { |d| assert_not d.readonly? } + Developer.readonly(false).each { |d| assert_not d.readonly? } Developer.readonly(true).each { |d| assert d.readonly? } Developer.readonly.each { |d| assert d.readonly? } end @@ -55,14 +55,14 @@ class ReadOnlyTest < ActiveRecord::TestCase def test_has_many_find_readonly post = Post.find(1) assert_not_empty post.comments - assert !post.comments.any?(&:readonly?) - assert !post.comments.to_a.any?(&:readonly?) + assert_not post.comments.any?(&:readonly?) + assert_not post.comments.to_a.any?(&:readonly?) assert post.comments.readonly(true).all?(&:readonly?) end def test_has_many_with_through_is_not_implicitly_marked_readonly assert people = Post.find(1).people - assert !people.any?(&:readonly?) + assert_not people.any?(&:readonly?) end def test_has_many_with_through_is_not_implicitly_marked_readonly_while_finding_by_id diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb index 61cb0f130d..b034fe3e3b 100644 --- a/activerecord/test/cases/reaper_test.rb +++ b/activerecord/test/cases/reaper_test.rb @@ -36,15 +36,15 @@ module ActiveRecord # A reaper with nil time should never reap connections def test_nil_time fp = FakePool.new - assert !fp.reaped + assert_not fp.reaped reaper = ConnectionPool::Reaper.new(fp, nil) reaper.run - assert !fp.reaped + assert_not fp.reaped end def test_some_time fp = FakePool.new - assert !fp.reaped + assert_not fp.reaped reaper = ConnectionPool::Reaper.new(fp, 0.0001) reaper.run diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index ed19192ad9..abadafbad4 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -75,7 +75,7 @@ class ReflectionTest < ActiveRecord::TestCase def test_column_null_not_null subscriber = Subscriber.first assert subscriber.column_for_attribute("name").null - assert !subscriber.column_for_attribute("nick").null + assert_not subscriber.column_for_attribute("nick").null end def test_human_name_for_column diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index 074ce9454f..f53ef1fe35 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -78,6 +78,10 @@ class RelationMergingTest < ActiveRecord::TestCase assert_equal 1, comments.count end + def test_relation_merging_with_skip_query_cache + assert_equal Post.all.merge(Post.all.skip_query_cache!).skip_query_cache_value, true + end + def test_relation_merging_with_association assert_queries(2) do # one for loading post, and another one merged query post = Post.where(body: "Such a lovely day").first diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index d6351bfe88..f82ecd4449 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -59,7 +59,7 @@ module ActiveRecord assert_equal [], relation.extending_values end - (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with, :skip_query_cache, :skip_preloading]).each do |method| + (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with, :skip_query_cache]).each do |method| test "##{method}!" do assert relation.public_send("#{method}!", :foo).equal?(relation) assert_equal :foo, relation.public_send("#{method}_value") diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 4e75371147..0f446e06aa 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -315,6 +315,14 @@ module ActiveRecord assert_equal "type cast from database", UpdateAllTestModel.first.body end + def test_skip_preloading_after_arel_has_been_generated + assert_nothing_raised do + relation = Comment.all + relation.arel + relation.skip_preloading! + end + end + private def skip_if_sqlite3_version_includes_quoting_bug diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index cf65789b97..952d2dd5d9 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -9,6 +9,7 @@ require "models/comment" require "models/author" require "models/entrant" require "models/developer" +require "models/person" require "models/computer" require "models/reply" require "models/company" @@ -25,7 +26,7 @@ require "models/edge" require "models/subscriber" class RelationTest < ActiveRecord::TestCase - fixtures :authors, :author_addresses, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :categories_posts, :posts, :comments, :tags, :taggings, :cars, :minivans + fixtures :authors, :author_addresses, :topics, :entrants, :developers, :people, :companies, :developers_projects, :accounts, :categories, :categorizations, :categories_posts, :posts, :comments, :tags, :taggings, :cars, :minivans class TopicWithCallbacks < ActiveRecord::Base self.table_name = :topics @@ -511,7 +512,7 @@ class RelationTest < ActiveRecord::TestCase end def test_find_with_readonly_option - Developer.all.each { |d| assert !d.readonly? } + Developer.all.each { |d| assert_not d.readonly? } Developer.all.readonly.each { |d| assert d.readonly? } end @@ -1097,7 +1098,7 @@ class RelationTest < ActiveRecord::TestCase assert_not_predicate posts.where(id: nil), :any? assert posts.any? { |p| p.id > 0 } - assert ! posts.any? { |p| p.id <= 0 } + assert_not posts.any? { |p| p.id <= 0 } end assert_predicate posts, :loaded? @@ -1109,7 +1110,7 @@ class RelationTest < ActiveRecord::TestCase assert_queries(2) do assert posts.many? # Uses COUNT() assert posts.many? { |p| p.id > 0 } - assert ! posts.many? { |p| p.id < 2 } + assert_not posts.many? { |p| p.id < 2 } end assert_predicate posts, :loaded? @@ -1125,14 +1126,14 @@ class RelationTest < ActiveRecord::TestCase def test_none? posts = Post.all assert_queries(1) do - assert ! posts.none? # Uses COUNT() + assert_not posts.none? # Uses COUNT() end assert_not_predicate posts, :loaded? assert_queries(1) do assert posts.none? { |p| p.id < 0 } - assert ! posts.none? { |p| p.id == 1 } + assert_not posts.none? { |p| p.id == 1 } end assert_predicate posts, :loaded? @@ -1141,13 +1142,13 @@ class RelationTest < ActiveRecord::TestCase def test_one posts = Post.all assert_queries(1) do - assert ! posts.one? # Uses COUNT() + assert_not posts.one? # Uses COUNT() end assert_not_predicate posts, :loaded? assert_queries(1) do - assert ! posts.one? { |p| p.id < 3 } + assert_not posts.one? { |p| p.id < 3 } assert posts.one? { |p| p.id == 1 } end @@ -1525,6 +1526,50 @@ class RelationTest < ActiveRecord::TestCase assert_equal posts(:welcome), comments(:greetings).post end + def test_touch_all_updates_records_timestamps + david = developers(:david) + david_previously_updated_at = david.updated_at + jamis = developers(:jamis) + jamis_previously_updated_at = jamis.updated_at + Developer.where(name: "David").touch_all + + assert_not_equal david_previously_updated_at, david.reload.updated_at + assert_equal jamis_previously_updated_at, jamis.reload.updated_at + end + + def test_touch_all_with_custom_timestamp + developer = developers(:david) + previously_created_at = developer.created_at + previously_updated_at = developer.updated_at + Developer.where(name: "David").touch_all(:created_at) + developer = developer.reload + + assert_not_equal previously_created_at, developer.created_at + assert_not_equal previously_updated_at, developer.updated_at + end + + def test_touch_all_with_given_time + developer = developers(:david) + previously_created_at = developer.created_at + previously_updated_at = developer.updated_at + new_time = Time.utc(2015, 2, 16, 4, 54, 0) + Developer.where(name: "David").touch_all(:created_at, time: new_time) + developer = developer.reload + + assert_not_equal previously_created_at, developer.created_at + assert_not_equal previously_updated_at, developer.updated_at + assert_equal new_time, developer.created_at + assert_equal new_time, developer.updated_at + end + + def test_touch_all_updates_locking_column + person = people(:david) + + assert_difference -> { person.reload.lock_version }, +1 do + Person.where(first_name: "David").touch_all + end + end + def test_update_on_relation topic1 = TopicWithCallbacks.create! title: "arel", author_name: nil topic2 = TopicWithCallbacks.create! title: "activerecord", author_name: nil @@ -1696,7 +1741,7 @@ class RelationTest < ActiveRecord::TestCase # checking if there are topics is used before you actually display them, # thus it shouldn't invoke an extra count query. assert_no_queries { assert topics.present? } - assert_no_queries { assert !topics.blank? } + assert_no_queries { assert_not topics.blank? } # shows count of topics and loops after loading the query should not trigger extra queries either. assert_no_queries { topics.size } diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 50d766a99e..31bdf3f357 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -469,7 +469,7 @@ class SchemaDumperTest < ActiveRecord::TestCase output = ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream).string assert_match %r{create_table "omg_cats"}, output - refute_match %r{create_table "cats"}, output + assert_no_match %r{create_table "cats"}, output ensure migration.migrate(:down) ActiveRecord::Base.table_name_prefix = original_table_name_prefix diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index 0804de1fb3..e3a34aa50d 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -193,7 +193,7 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_order_to_unscope_reordering scope = DeveloperOrderedBySalary.order("salary DESC, name ASC").reverse_order.unscope(:order) - assert !/order/i.match?(scope.to_sql) + assert_no_match(/order/i, scope.to_sql) end def test_unscope_reverse_order diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index ea71a5ce28..6cb252edaf 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -86,7 +86,7 @@ class NamedScopingTest < ActiveRecord::TestCase def test_scopes_are_composable assert_equal((approved = Topic.all.merge!(where: { approved: true }).to_a), Topic.approved) assert_equal((replied = Topic.all.merge!(where: "replies_count > 0").to_a), Topic.replied) - assert !(approved == replied) + assert_not (approved == replied) assert_not_empty (approved & replied) assert_equal approved & replied, Topic.approved.replied @@ -303,6 +303,13 @@ class NamedScopingTest < ActiveRecord::TestCase assert_equal "lifo", topic.author_name end + def test_deprecated_delegating_private_method + assert_deprecated do + scope = Topic.all.by_private_lifo + assert_not scope.instance_variable_get(:@delegate_to_klass) + end + end + def test_reserved_scope_names klass = Class.new(ActiveRecord::Base) do self.table_name = "topics" diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index 5c86bc892d..f18f1ed981 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -105,7 +105,7 @@ class RelationScopingTest < ActiveRecord::TestCase Developer.select("id, name").scoping do developer = Developer.where("name = 'David'").first assert_equal "David", developer.name - assert !developer.has_attribute?(:salary) + assert_not developer.has_attribute?(:salary) end end diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index 2d829ad4ba..932780bfef 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -67,8 +67,8 @@ class SerializationTest < ActiveRecord::TestCase klazz.include_root_in_json = false assert ActiveRecord::Base.include_root_in_json - assert !klazz.include_root_in_json - assert !klazz.new.include_root_in_json + assert_not klazz.include_root_in_json + assert_not klazz.new.include_root_in_json ensure ActiveRecord::Base.include_root_in_json = original_root_in_json end diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb index ad6cd198e2..e3c12f68fd 100644 --- a/activerecord/test/cases/statement_cache_test.rb +++ b/activerecord/test/cases/statement_cache_test.rb @@ -102,7 +102,7 @@ module ActiveRecord Book.find_by(name: "my other book") end - refute_equal book, other_book + assert_not_equal book, other_book end def test_find_by_does_not_use_statement_cache_if_table_name_is_changed diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index a30d13632a..3bd480cfbd 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -8,7 +8,12 @@ class StoreTest < ActiveRecord::TestCase fixtures :'admin/users' setup do - @john = Admin::User.create!(name: "John Doe", color: "black", remember_login: true, height: "tall", is_a_good_guy: true) + @john = Admin::User.create!( + name: "John Doe", color: "black", remember_login: true, + height: "tall", is_a_good_guy: true, + parent_name: "Quinn", partner_name: "Dallas", + partner_birthday: "1997-11-1" + ) end test "reading store attributes through accessors" do @@ -24,6 +29,21 @@ class StoreTest < ActiveRecord::TestCase assert_equal "37signals.com", @john.homepage end + test "reading store attributes through accessors with prefix" do + assert_equal "Quinn", @john.parent_name + assert_nil @john.parent_birthday + assert_equal "Dallas", @john.partner_name + assert_equal "1997-11-1", @john.partner_birthday + end + + test "writing store attributes through accessors with prefix" do + @john.partner_name = "River" + @john.partner_birthday = "1999-2-11" + + assert_equal "River", @john.partner_name + assert_equal "1999-2-11", @john.partner_birthday + end + test "accessing attributes not exposed by accessors" do @john.settings[:icecream] = "graeters" @john.save diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index 21226352ff..48d1fc7eb0 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -179,7 +179,7 @@ module ActiveRecord @configurations = { "development" => { "database" => "dev-db" }, "test" => { "database" => "test-db" }, - "production" => { "database" => "prod-db" } + "production" => { "url" => "prod-db-url" } } ActiveRecord::Base.stubs(:configurations).returns(@configurations) @@ -188,7 +188,89 @@ module ActiveRecord def test_creates_current_environment_database ActiveRecord::Tasks::DatabaseTasks.expects(:create). - with("database" => "prod-db") + with("database" => "test-db") + + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("test") + ) + end + + def test_creates_current_environment_database_with_url + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("url" => "prod-db-url") + + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("production") + ) + end + + def test_creates_test_and_development_databases_when_env_was_not_specified + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "dev-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "test-db") + + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + end + + 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") + 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_environments + ActiveRecord::Tasks::DatabaseTasks.stubs(:create).returns true + + ActiveRecord::Base.expects(:establish_connection).with(:development) + + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end + + class DatabaseTasksCreateCurrentThreeTierTest < ActiveRecord::TestCase + def setup + @configurations = { + "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } }, + "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } }, + "production" => { "primary" => { "url" => "prod-db-url" }, "secondary" => { "url" => "secondary-prod-db-url" } } + } + + ActiveRecord::Base.stubs(:configurations).returns(@configurations) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_creates_current_environment_database + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "test-db") + + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "secondary-test-db") + + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("test") + ) + end + + def test_creates_current_environment_database_with_url + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("url" => "prod-db-url") + + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("url" => "secondary-prod-db-url") ActiveRecord::Tasks::DatabaseTasks.create_current( ActiveSupport::StringInquirer.new("production") @@ -199,7 +281,11 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.expects(:create). with("database" => "dev-db") ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "secondary-dev-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:create). with("database" => "test-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "secondary-test-db") ActiveRecord::Tasks::DatabaseTasks.create_current( ActiveSupport::StringInquirer.new("development") @@ -212,7 +298,11 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.expects(:create). with("database" => "dev-db") ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "secondary-dev-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:create). with("database" => "test-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "secondary-test-db") ActiveRecord::Tasks::DatabaseTasks.create_current( ActiveSupport::StringInquirer.new("development") @@ -221,7 +311,7 @@ module ActiveRecord ENV["RAILS_ENV"] = old_env end - def test_establishes_connection_for_the_given_environment + def test_establishes_connection_for_the_given_environments_config ActiveRecord::Tasks::DatabaseTasks.stubs(:create).returns true ActiveRecord::Base.expects(:establish_connection).with(:development) @@ -305,7 +395,7 @@ module ActiveRecord @configurations = { "development" => { "database" => "dev-db" }, "test" => { "database" => "test-db" }, - "production" => { "database" => "prod-db" } + "production" => { "url" => "prod-db-url" } } ActiveRecord::Base.stubs(:configurations).returns(@configurations) @@ -313,7 +403,16 @@ module ActiveRecord def test_drops_current_environment_database ActiveRecord::Tasks::DatabaseTasks.expects(:drop). - with("database" => "prod-db") + with("database" => "test-db") + + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("test") + ) + end + + def test_drops_current_environment_database_with_url + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("url" => "prod-db-url") ActiveRecord::Tasks::DatabaseTasks.drop_current( ActiveSupport::StringInquirer.new("production") @@ -347,6 +446,76 @@ module ActiveRecord end end + class DatabaseTasksDropCurrentThreeTierTest < ActiveRecord::TestCase + def setup + @configurations = { + "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } }, + "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } }, + "production" => { "primary" => { "url" => "prod-db-url" }, "secondary" => { "url" => "secondary-prod-db-url" } } + } + + ActiveRecord::Base.stubs(:configurations).returns(@configurations) + end + + def test_drops_current_environment_database + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "test-db") + + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "secondary-test-db") + + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("test") + ) + end + + def test_drops_current_environment_database_with_url + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("url" => "prod-db-url") + + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("url" => "secondary-prod-db-url") + + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("production") + ) + end + + def test_drops_test_and_development_databases_when_env_was_not_specified + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "dev-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "secondary-dev-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "test-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "secondary-test-db") + + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("development") + ) + end + + 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") + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "secondary-dev-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "test-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "secondary-test-db") + + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("development") + ) + ensure + ENV["RAILS_ENV"] = old_env + end + end + if current_adapter?(:SQLite3Adapter) && !in_memory_db? class DatabaseTasksMigrateTest < ActiveRecord::TestCase self.use_transactional_tests = false diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index c70286d52a..3fd38b4b60 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -159,7 +159,7 @@ class TransactionTest < ActiveRecord::TestCase def @first.before_save_for_transaction raise ActiveRecord::Rollback end - assert !@first.approved + assert_not @first.approved Topic.transaction do @first.approved = true @@ -194,7 +194,7 @@ class TransactionTest < ActiveRecord::TestCase posts_count = author.posts.size assert posts_count > 0 status = author.update(name: nil, post_ids: []) - assert !status + assert_not status assert_equal posts_count, author.posts.reload.size end @@ -212,7 +212,7 @@ class TransactionTest < ActiveRecord::TestCase add_cancelling_before_destroy_with_db_side_effect_to_topic @first nbooks_before_destroy = Book.count status = @first.destroy - assert !status + assert_not status @first.reload assert_equal nbooks_before_destroy, Book.count end @@ -224,7 +224,7 @@ class TransactionTest < ActiveRecord::TestCase original_author_name = @first.author_name @first.author_name += "_this_should_not_end_up_in_the_db" status = @first.save - assert !status + assert_not status assert_equal original_author_name, @first.reload.author_name assert_equal nbooks_before_save, Book.count end @@ -323,8 +323,8 @@ class TransactionTest < ActiveRecord::TestCase raise ActiveRecord::Rollback end - refute_predicate topic_one, :persisted? - refute_predicate topic_two, :persisted? + assert_not_predicate topic_one, :persisted? + assert_not_predicate topic_two, :persisted? end def test_nested_transaction_without_new_transaction_applies_parent_state_on_rollback @@ -344,8 +344,8 @@ class TransactionTest < ActiveRecord::TestCase raise ActiveRecord::Rollback end - refute_predicate topic_one, :persisted? - refute_predicate topic_two, :persisted? + assert_not_predicate topic_one, :persisted? + assert_not_predicate topic_two, :persisted? end def test_double_nested_transaction_applies_parent_state_on_rollback @@ -371,9 +371,9 @@ class TransactionTest < ActiveRecord::TestCase raise ActiveRecord::Rollback end - refute_predicate topic_one, :persisted? - refute_predicate topic_two, :persisted? - refute_predicate topic_three, :persisted? + assert_not_predicate topic_one, :persisted? + assert_not_predicate topic_two, :persisted? + assert_not_predicate topic_three, :persisted? end def test_manually_rolling_back_a_transaction diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb index 62cd89041a..1fbcdc271b 100644 --- a/activerecord/test/cases/validations/length_validation_test.rb +++ b/activerecord/test/cases/validations/length_validation_test.rb @@ -17,7 +17,7 @@ class LengthValidationTest < ActiveRecord::TestCase def test_validates_size_of_association assert_nothing_raised { @owner.validates_size_of :pets, minimum: 1 } o = @owner.new("name" => "nopets") - assert !o.save + assert_not o.save assert_predicate o.errors[:pets], :any? o.pets.build("name" => "apet") assert_predicate o, :valid? @@ -26,21 +26,21 @@ class LengthValidationTest < ActiveRecord::TestCase def test_validates_size_of_association_using_within assert_nothing_raised { @owner.validates_size_of :pets, within: 1..2 } o = @owner.new("name" => "nopets") - assert !o.save + assert_not o.save assert_predicate o.errors[:pets], :any? o.pets.build("name" => "apet") assert_predicate o, :valid? 2.times { o.pets.build("name" => "apet") } - assert !o.save + assert_not o.save assert_predicate o.errors[:pets], :any? end def test_validates_size_of_association_utf8 @owner.validates_size_of :pets, minimum: 1 o = @owner.new("name" => "あいうえおかきくけこ") - assert !o.save + assert_not o.save assert_predicate o.errors[:pets], :any? o.pets.build("name" => "あいうえおかきくけこ") assert_predicate o, :valid? diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index 6c83bbd15c..a33877f43a 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -39,7 +39,7 @@ class ValidationsTest < ActiveRecord::TestCase def test_valid_using_special_context r = WrongReply.new(title: "Valid title") - assert !r.valid?(:special_case) + assert_not r.valid?(:special_case) assert_equal "Invalid", r.errors[:author_name].join r.author_name = "secret" @@ -125,7 +125,7 @@ class ValidationsTest < ActiveRecord::TestCase def test_save_without_validation reply = WrongReply.new - assert !reply.save + assert_not reply.save assert reply.save(validate: false) end diff --git a/activerecord/test/fixtures/sponsors.yml b/activerecord/test/fixtures/sponsors.yml index 2da541c539..02ddb8dd38 100644 --- a/activerecord/test/fixtures/sponsors.yml +++ b/activerecord/test/fixtures/sponsors.yml @@ -10,3 +10,6 @@ crazy_club_sponsor_for_groucho: sponsor_club: crazy_club sponsorable_id: 3 sponsorable_type: Member +sponsor_for_author_david: + sponsorable_id: 1 + sponsorable_type: Author diff --git a/activerecord/test/models/admin/user.rb b/activerecord/test/models/admin/user.rb index abb5cb28e7..3f55364510 100644 --- a/activerecord/test/models/admin/user.rb +++ b/activerecord/test/models/admin/user.rb @@ -19,6 +19,9 @@ class Admin::User < ActiveRecord::Base store :params, accessors: [ :token ], coder: YAML store :settings, accessors: [ :color, :homepage ] store_accessor :settings, :favorite_food + store :parent, accessors: [:birthday, :name], prefix: true + store :spouse, accessors: [:birthday], prefix: :partner + store_accessor :spouse, :name, prefix: :partner store :preferences, accessors: [ :remember_login ] store :json_data, accessors: [ :height, :weight ], coder: Coder.new store :json_data_empty, accessors: [ :is_a_good_guy ], coder: Coder.new diff --git a/activerecord/test/models/frog.rb b/activerecord/test/models/frog.rb new file mode 100644 index 0000000000..73601aacdd --- /dev/null +++ b/activerecord/test/models/frog.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class Frog < ActiveRecord::Base + after_save do + with_lock do + end + end +end diff --git a/activerecord/test/models/member_detail.rb b/activerecord/test/models/member_detail.rb index 87f7aab9a2..e121a849d0 100644 --- a/activerecord/test/models/member_detail.rb +++ b/activerecord/test/models/member_detail.rb @@ -5,6 +5,7 @@ class MemberDetail < ActiveRecord::Base belongs_to :organization has_one :member_type, through: :member has_one :membership, through: :member + has_one :admittable, through: :member, source_type: "Member" has_many :organization_member_details, through: :organization, source: :member_details end diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 8cd4dc352a..fa50eeb6a4 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -12,9 +12,17 @@ class Topic < ActiveRecord::Base scope :scope_with_lambda, lambda { all } + scope :by_private_lifo, -> { where(author_name: private_lifo) } scope :by_lifo, -> { where(author_name: "lifo") } scope :replied, -> { where "replies_count > 0" } + class << self + private + def private_lifo + "lifo" + end + end + scope "approved_as_string", -> { where(approved: true) } scope :anonymous_extension, -> {} do def one diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index ca86100bc5..92ad25ef76 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -21,6 +21,8 @@ ActiveRecord::Schema.define do create_table :admin_users, force: true do |t| t.string :name t.string :settings, null: true, limit: 1024 + t.string :parent, null: true, limit: 1024 + t.string :spouse, null: true, limit: 1024 # MySQL does not allow default values for blobs. Fake it out with a # big varchar below. t.string :preferences, null: true, default: "", limit: 1024 @@ -345,6 +347,10 @@ ActiveRecord::Schema.define do t.string :token end + create_table :frogs, force: true do |t| + t.string :name + end + create_table :funny_jokes, force: true do |t| t.string :name end @@ -480,7 +486,8 @@ ActiveRecord::Schema.define do create_table :members, force: true do |t| t.string :name - t.integer :member_type_id + t.references :member_type, index: false + t.references :admittable, polymorphic: true, index: false end create_table :member_details, force: true do |t| diff --git a/activestorage/Rakefile b/activestorage/Rakefile index 2aa4d2a76f..7dc69e04ea 100644 --- a/activestorage/Rakefile +++ b/activestorage/Rakefile @@ -8,6 +8,7 @@ Rake::TestTask.new do |test| test.libs << "app/controllers" test.libs << "test" test.test_files = FileList["test/**/*_test.rb"] + test.verbose = true test.warning = false end diff --git a/activestorage/app/assets/javascripts/activestorage.js b/activestorage/app/assets/javascripts/activestorage.js index 269fc52e64..3e1fbc7b50 100644 --- a/activestorage/app/assets/javascripts/activestorage.js +++ b/activestorage/app/assets/javascripts/activestorage.js @@ -1 +1 @@ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ActiveStorage=e():t.ActiveStorage=e()}(this,function(){return function(t){function e(n){if(r[n])return r[n].exports;var i=r[n]={i:n,l:!1,exports:{}};return t[n].call(i.exports,i,i.exports,e),i.l=!0,i.exports}var r={};return e.m=t,e.c=r,e.d=function(t,r,n){e.o(t,r)||Object.defineProperty(t,r,{configurable:!1,enumerable:!0,get:n})},e.n=function(t){var r=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(r,"a",r),r},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=2)}([function(t,e,r){"use strict";function n(t){var e=a(document.head,'meta[name="'+t+'"]');if(e)return e.getAttribute("content")}function i(t,e){return"string"==typeof t&&(e=t,t=document),o(t.querySelectorAll(e))}function a(t,e){return"string"==typeof t&&(e=t,t=document),t.querySelector(e)}function u(t,e){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},n=t.disabled,i=r.bubbles,a=r.cancelable,u=r.detail,o=document.createEvent("Event");o.initEvent(e,i||!0,a||!0),o.detail=u||{};try{t.disabled=!1,t.dispatchEvent(o)}finally{t.disabled=n}return o}function o(t){return Array.isArray(t)?t:Array.from?Array.from(t):[].slice.call(t)}e.d=n,e.c=i,e.b=a,e.a=u,e.e=o},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){if(t&&"function"==typeof t[e]){for(var r=arguments.length,n=Array(r>2?r-2:0),i=2;i<r;i++)n[i-2]=arguments[i];return t[e].apply(t,n)}}r.d(e,"a",function(){return c});var a=r(6),u=r(8),o=r(9),s=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),f=0,c=function(){function t(e,r,i){n(this,t),this.id=++f,this.file=e,this.url=r,this.delegate=i}return s(t,[{key:"create",value:function(t){var e=this;a.a.create(this.file,function(r,n){if(r)return void t(r);var a=new u.a(e.file,n,e.url);i(e.delegate,"directUploadWillCreateBlobWithXHR",a.xhr),a.create(function(r){if(r)t(r);else{var n=new o.a(a);i(e.delegate,"directUploadWillStoreFileWithXHR",n.xhr),n.create(function(e){e?t(e):t(null,a.toJSON())})}})})}}]),t}()},function(t,e,r){"use strict";function n(){window.ActiveStorage&&Object(i.a)()}Object.defineProperty(e,"__esModule",{value:!0});var i=r(3),a=r(1);r.d(e,"start",function(){return i.a}),r.d(e,"DirectUpload",function(){return a.a}),setTimeout(n,1)},function(t,e,r){"use strict";function n(){d||(d=!0,document.addEventListener("submit",i),document.addEventListener("ajax:before",a))}function i(t){u(t)}function a(t){"FORM"==t.target.tagName&&u(t)}function u(t){var e=t.target;if(e.hasAttribute(l))return void t.preventDefault();var r=new c.a(e),n=r.inputs;n.length&&(t.preventDefault(),e.setAttribute(l,""),n.forEach(s),r.start(function(t){e.removeAttribute(l),t?n.forEach(f):o(e)}))}function o(t){var e=Object(h.b)(t,"input[type=submit]");if(e){var r=e,n=r.disabled;e.disabled=!1,e.focus(),e.click(),e.disabled=n}else e=document.createElement("input"),e.type="submit",e.style="display:none",t.appendChild(e),e.click(),t.removeChild(e)}function s(t){t.disabled=!0}function f(t){t.disabled=!1}e.a=n;var c=r(4),h=r(0),l="data-direct-uploads-processing",d=!1},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return s});var i=r(5),a=r(0),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o="input[type=file][data-direct-upload-url]:not([disabled])",s=function(){function t(e){n(this,t),this.form=e,this.inputs=Object(a.c)(e,o).filter(function(t){return t.files.length})}return u(t,[{key:"start",value:function(t){var e=this,r=this.createDirectUploadControllers();this.dispatch("start"),function n(){var i=r.shift();i?i.start(function(r){r?(t(r),e.dispatch("end")):n()}):(t(),e.dispatch("end"))}()}},{key:"createDirectUploadControllers",value:function(){var t=[];return this.inputs.forEach(function(e){Object(a.e)(e.files).forEach(function(r){var n=new i.a(e,r);t.push(n)})}),t}},{key:"dispatch",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return Object(a.a)(this.form,"direct-uploads:"+t,{detail:e})}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return o});var i=r(1),a=r(0),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o=function(){function t(e,r){n(this,t),this.input=e,this.file=r,this.directUpload=new i.a(this.file,this.url,this),this.dispatch("initialize")}return u(t,[{key:"start",value:function(t){var e=this,r=document.createElement("input");r.type="hidden",r.name=this.input.name,this.input.insertAdjacentElement("beforebegin",r),this.dispatch("start"),this.directUpload.create(function(n,i){n?(r.parentNode.removeChild(r),e.dispatchError(n)):r.value=i.signed_id,e.dispatch("end"),t(n)})}},{key:"uploadRequestDidProgress",value:function(t){var e=t.loaded/t.total*100;e&&this.dispatch("progress",{progress:e})}},{key:"dispatch",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.file=this.file,e.id=this.directUpload.id,Object(a.a)(this.input,"direct-upload:"+t,{detail:e})}},{key:"dispatchError",value:function(t){this.dispatch("error",{error:t}).defaultPrevented||alert(t)}},{key:"directUploadWillCreateBlobWithXHR",value:function(t){this.dispatch("before-blob-request",{xhr:t})}},{key:"directUploadWillStoreFileWithXHR",value:function(t){var e=this;this.dispatch("before-storage-request",{xhr:t}),t.upload.addEventListener("progress",function(t){return e.uploadRequestDidProgress(t)})}},{key:"url",get:function(){return this.input.getAttribute("data-direct-upload-url")}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return s});var i=r(7),a=r.n(i),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o=File.prototype.slice||File.prototype.mozSlice||File.prototype.webkitSlice,s=function(){function t(e){n(this,t),this.file=e,this.chunkSize=2097152,this.chunkCount=Math.ceil(this.file.size/this.chunkSize),this.chunkIndex=0}return u(t,null,[{key:"create",value:function(e,r){new t(e).create(r)}}]),u(t,[{key:"create",value:function(t){var e=this;this.callback=t,this.md5Buffer=new a.a.ArrayBuffer,this.fileReader=new FileReader,this.fileReader.addEventListener("load",function(t){return e.fileReaderDidLoad(t)}),this.fileReader.addEventListener("error",function(t){return e.fileReaderDidError(t)}),this.readNextChunk()}},{key:"fileReaderDidLoad",value:function(t){if(this.md5Buffer.append(t.target.result),!this.readNextChunk()){var e=this.md5Buffer.end(!0),r=btoa(e);this.callback(null,r)}}},{key:"fileReaderDidError",value:function(t){this.callback("Error reading "+this.file.name)}},{key:"readNextChunk",value:function(){if(this.chunkIndex<this.chunkCount){var t=this.chunkIndex*this.chunkSize,e=Math.min(t+this.chunkSize,this.file.size),r=o.call(this.file,t,e);return this.fileReader.readAsArrayBuffer(r),this.chunkIndex++,!0}return!1}}]),t}()},function(t,e,r){!function(e){t.exports=e()}(function(t){"use strict";function e(t,e){var r=t[0],n=t[1],i=t[2],a=t[3];r+=(n&i|~n&a)+e[0]-680876936|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[1]-389564586|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[2]+606105819|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[3]-1044525330|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[4]-176418897|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[5]+1200080426|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[6]-1473231341|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[7]-45705983|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[8]+1770035416|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[9]-1958414417|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[10]-42063|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[11]-1990404162|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[12]+1804603682|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[13]-40341101|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[14]-1502002290|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[15]+1236535329|0,n=(n<<22|n>>>10)+i|0,r+=(n&a|i&~a)+e[1]-165796510|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[6]-1069501632|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[11]+643717713|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[0]-373897302|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[5]-701558691|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[10]+38016083|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[15]-660478335|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[4]-405537848|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[9]+568446438|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[14]-1019803690|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[3]-187363961|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[8]+1163531501|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[13]-1444681467|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[2]-51403784|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[7]+1735328473|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[12]-1926607734|0,n=(n<<20|n>>>12)+i|0,r+=(n^i^a)+e[5]-378558|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[8]-2022574463|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[11]+1839030562|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[14]-35309556|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[1]-1530992060|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[4]+1272893353|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[7]-155497632|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[10]-1094730640|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[13]+681279174|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[0]-358537222|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[3]-722521979|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[6]+76029189|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[9]-640364487|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[12]-421815835|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[15]+530742520|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[2]-995338651|0,n=(n<<23|n>>>9)+i|0,r+=(i^(n|~a))+e[0]-198630844|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[7]+1126891415|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[14]-1416354905|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[5]-57434055|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[12]+1700485571|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[3]-1894986606|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[10]-1051523|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[1]-2054922799|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[8]+1873313359|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[15]-30611744|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[6]-1560198380|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[13]+1309151649|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[4]-145523070|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[11]-1120210379|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[2]+718787259|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[9]-343485551|0,n=(n<<21|n>>>11)+i|0,t[0]=r+t[0]|0,t[1]=n+t[1]|0,t[2]=i+t[2]|0,t[3]=a+t[3]|0}function r(t){var e,r=[];for(e=0;e<64;e+=4)r[e>>2]=t.charCodeAt(e)+(t.charCodeAt(e+1)<<8)+(t.charCodeAt(e+2)<<16)+(t.charCodeAt(e+3)<<24);return r}function n(t){var e,r=[];for(e=0;e<64;e+=4)r[e>>2]=t[e]+(t[e+1]<<8)+(t[e+2]<<16)+(t[e+3]<<24);return r}function i(t){var n,i,a,u,o,s,f=t.length,c=[1732584193,-271733879,-1732584194,271733878];for(n=64;n<=f;n+=64)e(c,r(t.substring(n-64,n)));for(t=t.substring(n-64),i=t.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],n=0;n<i;n+=1)a[n>>2]|=t.charCodeAt(n)<<(n%4<<3);if(a[n>>2]|=128<<(n%4<<3),n>55)for(e(c,a),n=0;n<16;n+=1)a[n]=0;return u=8*f,u=u.toString(16).match(/(.*?)(.{0,8})$/),o=parseInt(u[2],16),s=parseInt(u[1],16)||0,a[14]=o,a[15]=s,e(c,a),c}function a(t){var r,i,a,u,o,s,f=t.length,c=[1732584193,-271733879,-1732584194,271733878];for(r=64;r<=f;r+=64)e(c,n(t.subarray(r-64,r)));for(t=r-64<f?t.subarray(r-64):new Uint8Array(0),i=t.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],r=0;r<i;r+=1)a[r>>2]|=t[r]<<(r%4<<3);if(a[r>>2]|=128<<(r%4<<3),r>55)for(e(c,a),r=0;r<16;r+=1)a[r]=0;return u=8*f,u=u.toString(16).match(/(.*?)(.{0,8})$/),o=parseInt(u[2],16),s=parseInt(u[1],16)||0,a[14]=o,a[15]=s,e(c,a),c}function u(t){var e,r="";for(e=0;e<4;e+=1)r+=p[t>>8*e+4&15]+p[t>>8*e&15];return r}function o(t){var e;for(e=0;e<t.length;e+=1)t[e]=u(t[e]);return t.join("")}function s(t){return/[\u0080-\uFFFF]/.test(t)&&(t=unescape(encodeURIComponent(t))),t}function f(t,e){var r,n=t.length,i=new ArrayBuffer(n),a=new Uint8Array(i);for(r=0;r<n;r+=1)a[r]=t.charCodeAt(r);return e?a:i}function c(t){return String.fromCharCode.apply(null,new Uint8Array(t))}function h(t,e,r){var n=new Uint8Array(t.byteLength+e.byteLength);return n.set(new Uint8Array(t)),n.set(new Uint8Array(e),t.byteLength),r?n:n.buffer}function l(t){var e,r=[],n=t.length;for(e=0;e<n-1;e+=2)r.push(parseInt(t.substr(e,2),16));return String.fromCharCode.apply(String,r)}function d(){this.reset()}var p=["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"];return"5d41402abc4b2a76b9719d911017c592"!==o(i("hello"))&&function(t,e){var r=(65535&t)+(65535&e);return(t>>16)+(e>>16)+(r>>16)<<16|65535&r},"undefined"==typeof ArrayBuffer||ArrayBuffer.prototype.slice||function(){function e(t,e){return t=0|t||0,t<0?Math.max(t+e,0):Math.min(t,e)}ArrayBuffer.prototype.slice=function(r,n){var i,a,u,o,s=this.byteLength,f=e(r,s),c=s;return n!==t&&(c=e(n,s)),f>c?new ArrayBuffer(0):(i=c-f,a=new ArrayBuffer(i),u=new Uint8Array(a),o=new Uint8Array(this,f,i),u.set(o),a)}}(),d.prototype.append=function(t){return this.appendBinary(s(t)),this},d.prototype.appendBinary=function(t){this._buff+=t,this._length+=t.length;var n,i=this._buff.length;for(n=64;n<=i;n+=64)e(this._hash,r(this._buff.substring(n-64,n)));return this._buff=this._buff.substring(n-64),this},d.prototype.end=function(t){var e,r,n=this._buff,i=n.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(e=0;e<i;e+=1)a[e>>2]|=n.charCodeAt(e)<<(e%4<<3);return this._finish(a,i),r=o(this._hash),t&&(r=l(r)),this.reset(),r},d.prototype.reset=function(){return this._buff="",this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},d.prototype.getState=function(){return{buff:this._buff,length:this._length,hash:this._hash}},d.prototype.setState=function(t){return this._buff=t.buff,this._length=t.length,this._hash=t.hash,this},d.prototype.destroy=function(){delete this._hash,delete this._buff,delete this._length},d.prototype._finish=function(t,r){var n,i,a,u=r;if(t[u>>2]|=128<<(u%4<<3),u>55)for(e(this._hash,t),u=0;u<16;u+=1)t[u]=0;n=8*this._length,n=n.toString(16).match(/(.*?)(.{0,8})$/),i=parseInt(n[2],16),a=parseInt(n[1],16)||0,t[14]=i,t[15]=a,e(this._hash,t)},d.hash=function(t,e){return d.hashBinary(s(t),e)},d.hashBinary=function(t,e){var r=i(t),n=o(r);return e?l(n):n},d.ArrayBuffer=function(){this.reset()},d.ArrayBuffer.prototype.append=function(t){var r,i=h(this._buff.buffer,t,!0),a=i.length;for(this._length+=t.byteLength,r=64;r<=a;r+=64)e(this._hash,n(i.subarray(r-64,r)));return this._buff=r-64<a?new Uint8Array(i.buffer.slice(r-64)):new Uint8Array(0),this},d.ArrayBuffer.prototype.end=function(t){var e,r,n=this._buff,i=n.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(e=0;e<i;e+=1)a[e>>2]|=n[e]<<(e%4<<3);return this._finish(a,i),r=o(this._hash),t&&(r=l(r)),this.reset(),r},d.ArrayBuffer.prototype.reset=function(){return this._buff=new Uint8Array(0),this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},d.ArrayBuffer.prototype.getState=function(){var t=d.prototype.getState.call(this);return t.buff=c(t.buff),t},d.ArrayBuffer.prototype.setState=function(t){return t.buff=f(t.buff,!0),d.prototype.setState.call(this,t)},d.ArrayBuffer.prototype.destroy=d.prototype.destroy,d.ArrayBuffer.prototype._finish=d.prototype._finish,d.ArrayBuffer.hash=function(t,e){var r=a(new Uint8Array(t)),n=o(r);return e?l(n):n},d})},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return u});var i=r(0),a=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),u=function(){function t(e,r,a){var u=this;n(this,t),this.file=e,this.attributes={filename:e.name,content_type:e.type,byte_size:e.size,checksum:r},this.xhr=new XMLHttpRequest,this.xhr.open("POST",a,!0),this.xhr.responseType="json",this.xhr.setRequestHeader("Content-Type","application/json"),this.xhr.setRequestHeader("Accept","application/json"),this.xhr.setRequestHeader("X-Requested-With","XMLHttpRequest"),this.xhr.setRequestHeader("X-CSRF-Token",Object(i.d)("csrf-token")),this.xhr.addEventListener("load",function(t){return u.requestDidLoad(t)}),this.xhr.addEventListener("error",function(t){return u.requestDidError(t)})}return a(t,[{key:"create",value:function(t){this.callback=t,this.xhr.send(JSON.stringify({blob:this.attributes}))}},{key:"requestDidLoad",value:function(t){if(this.status>=200&&this.status<300){var e=this.response,r=e.direct_upload;delete e.direct_upload,this.attributes=e,this.directUploadData=r,this.callback(null,this.toJSON())}else this.requestDidError(t)}},{key:"requestDidError",value:function(t){this.callback('Error creating Blob for "'+this.file.name+'". Status: '+this.status)}},{key:"toJSON",value:function(){var t={};for(var e in this.attributes)t[e]=this.attributes[e];return t}},{key:"status",get:function(){return this.xhr.status}},{key:"response",get:function(){var t=this.xhr,e=t.responseType,r=t.response;return"json"==e?r:JSON.parse(r)}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return a});var i=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),a=function(){function t(e){var r=this;n(this,t),this.blob=e,this.file=e.file;var i=e.directUploadData,a=i.url,u=i.headers;this.xhr=new XMLHttpRequest,this.xhr.open("PUT",a,!0),this.xhr.responseType="text";for(var o in u)this.xhr.setRequestHeader(o,u[o]);this.xhr.addEventListener("load",function(t){return r.requestDidLoad(t)}),this.xhr.addEventListener("error",function(t){return r.requestDidError(t)})}return i(t,[{key:"create",value:function(t){this.callback=t,this.xhr.send(this.file.slice())}},{key:"requestDidLoad",value:function(t){var e=this.xhr,r=e.status,n=e.response;r>=200&&r<300?this.callback(null,n):this.requestDidError(t)}},{key:"requestDidError",value:function(t){this.callback('Error storing "'+this.file.name+'". Status: '+this.xhr.status)}}]),t}()}])});
\ No newline at end of file +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ActiveStorage=e():t.ActiveStorage=e()}(this,function(){return function(t){function e(n){if(r[n])return r[n].exports;var i=r[n]={i:n,l:!1,exports:{}};return t[n].call(i.exports,i,i.exports,e),i.l=!0,i.exports}var r={};return e.m=t,e.c=r,e.d=function(t,r,n){e.o(t,r)||Object.defineProperty(t,r,{configurable:!1,enumerable:!0,get:n})},e.n=function(t){var r=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(r,"a",r),r},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=2)}([function(t,e,r){"use strict";function n(t){var e=a(document.head,'meta[name="'+t+'"]');if(e)return e.getAttribute("content")}function i(t,e){return"string"==typeof t&&(e=t,t=document),o(t.querySelectorAll(e))}function a(t,e){return"string"==typeof t&&(e=t,t=document),t.querySelector(e)}function u(t,e){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},n=t.disabled,i=r.bubbles,a=r.cancelable,u=r.detail,o=document.createEvent("Event");o.initEvent(e,i||!0,a||!0),o.detail=u||{};try{t.disabled=!1,t.dispatchEvent(o)}finally{t.disabled=n}return o}function o(t){return Array.isArray(t)?t:Array.from?Array.from(t):[].slice.call(t)}e.d=n,e.c=i,e.b=a,e.a=u,e.e=o},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){if(t&&"function"==typeof t[e]){for(var r=arguments.length,n=Array(r>2?r-2:0),i=2;i<r;i++)n[i-2]=arguments[i];return t[e].apply(t,n)}}r.d(e,"a",function(){return c});var a=r(6),u=r(8),o=r(9),s=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),f=0,c=function(){function t(e,r,i){n(this,t),this.id=++f,this.file=e,this.url=r,this.delegate=i}return s(t,[{key:"create",value:function(t){var e=this;a.a.create(this.file,function(r,n){if(r)return void t(r);var a=new u.a(e.file,n,e.url);i(e.delegate,"directUploadWillCreateBlobWithXHR",a.xhr),a.create(function(r){if(r)t(r);else{var n=new o.a(a);i(e.delegate,"directUploadWillStoreFileWithXHR",n.xhr),n.create(function(e){e?t(e):t(null,a.toJSON())})}})})}}]),t}()},function(t,e,r){"use strict";function n(){window.ActiveStorage&&Object(i.a)()}Object.defineProperty(e,"__esModule",{value:!0});var i=r(3),a=r(1);r.d(e,"start",function(){return i.a}),r.d(e,"DirectUpload",function(){return a.a}),setTimeout(n,1)},function(t,e,r){"use strict";function n(){d||(d=!0,document.addEventListener("submit",i),document.addEventListener("ajax:before",a))}function i(t){u(t)}function a(t){"FORM"==t.target.tagName&&u(t)}function u(t){var e=t.target;if(e.hasAttribute(l))return void t.preventDefault();var r=new c.a(e),n=r.inputs;n.length&&(t.preventDefault(),e.setAttribute(l,""),n.forEach(s),r.start(function(t){e.removeAttribute(l),t?n.forEach(f):o(e)}))}function o(t){var e=Object(h.b)(t,"input[type=submit]");if(e){var r=e,n=r.disabled;e.disabled=!1,e.focus(),e.click(),e.disabled=n}else e=document.createElement("input"),e.type="submit",e.style.display="none",t.appendChild(e),e.click(),t.removeChild(e)}function s(t){t.disabled=!0}function f(t){t.disabled=!1}e.a=n;var c=r(4),h=r(0),l="data-direct-uploads-processing",d=!1},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return s});var i=r(5),a=r(0),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o="input[type=file][data-direct-upload-url]:not([disabled])",s=function(){function t(e){n(this,t),this.form=e,this.inputs=Object(a.c)(e,o).filter(function(t){return t.files.length})}return u(t,[{key:"start",value:function(t){var e=this,r=this.createDirectUploadControllers();this.dispatch("start"),function n(){var i=r.shift();i?i.start(function(r){r?(t(r),e.dispatch("end")):n()}):(t(),e.dispatch("end"))}()}},{key:"createDirectUploadControllers",value:function(){var t=[];return this.inputs.forEach(function(e){Object(a.e)(e.files).forEach(function(r){var n=new i.a(e,r);t.push(n)})}),t}},{key:"dispatch",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return Object(a.a)(this.form,"direct-uploads:"+t,{detail:e})}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return o});var i=r(1),a=r(0),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o=function(){function t(e,r){n(this,t),this.input=e,this.file=r,this.directUpload=new i.a(this.file,this.url,this),this.dispatch("initialize")}return u(t,[{key:"start",value:function(t){var e=this,r=document.createElement("input");r.type="hidden",r.name=this.input.name,this.input.insertAdjacentElement("beforebegin",r),this.dispatch("start"),this.directUpload.create(function(n,i){n?(r.parentNode.removeChild(r),e.dispatchError(n)):r.value=i.signed_id,e.dispatch("end"),t(n)})}},{key:"uploadRequestDidProgress",value:function(t){var e=t.loaded/t.total*100;e&&this.dispatch("progress",{progress:e})}},{key:"dispatch",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.file=this.file,e.id=this.directUpload.id,Object(a.a)(this.input,"direct-upload:"+t,{detail:e})}},{key:"dispatchError",value:function(t){this.dispatch("error",{error:t}).defaultPrevented||alert(t)}},{key:"directUploadWillCreateBlobWithXHR",value:function(t){this.dispatch("before-blob-request",{xhr:t})}},{key:"directUploadWillStoreFileWithXHR",value:function(t){var e=this;this.dispatch("before-storage-request",{xhr:t}),t.upload.addEventListener("progress",function(t){return e.uploadRequestDidProgress(t)})}},{key:"url",get:function(){return this.input.getAttribute("data-direct-upload-url")}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return s});var i=r(7),a=r.n(i),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o=File.prototype.slice||File.prototype.mozSlice||File.prototype.webkitSlice,s=function(){function t(e){n(this,t),this.file=e,this.chunkSize=2097152,this.chunkCount=Math.ceil(this.file.size/this.chunkSize),this.chunkIndex=0}return u(t,null,[{key:"create",value:function(e,r){new t(e).create(r)}}]),u(t,[{key:"create",value:function(t){var e=this;this.callback=t,this.md5Buffer=new a.a.ArrayBuffer,this.fileReader=new FileReader,this.fileReader.addEventListener("load",function(t){return e.fileReaderDidLoad(t)}),this.fileReader.addEventListener("error",function(t){return e.fileReaderDidError(t)}),this.readNextChunk()}},{key:"fileReaderDidLoad",value:function(t){if(this.md5Buffer.append(t.target.result),!this.readNextChunk()){var e=this.md5Buffer.end(!0),r=btoa(e);this.callback(null,r)}}},{key:"fileReaderDidError",value:function(t){this.callback("Error reading "+this.file.name)}},{key:"readNextChunk",value:function(){if(this.chunkIndex<this.chunkCount){var t=this.chunkIndex*this.chunkSize,e=Math.min(t+this.chunkSize,this.file.size),r=o.call(this.file,t,e);return this.fileReader.readAsArrayBuffer(r),this.chunkIndex++,!0}return!1}}]),t}()},function(t,e,r){!function(e){t.exports=e()}(function(t){"use strict";function e(t,e){var r=t[0],n=t[1],i=t[2],a=t[3];r+=(n&i|~n&a)+e[0]-680876936|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[1]-389564586|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[2]+606105819|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[3]-1044525330|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[4]-176418897|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[5]+1200080426|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[6]-1473231341|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[7]-45705983|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[8]+1770035416|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[9]-1958414417|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[10]-42063|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[11]-1990404162|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[12]+1804603682|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[13]-40341101|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[14]-1502002290|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[15]+1236535329|0,n=(n<<22|n>>>10)+i|0,r+=(n&a|i&~a)+e[1]-165796510|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[6]-1069501632|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[11]+643717713|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[0]-373897302|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[5]-701558691|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[10]+38016083|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[15]-660478335|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[4]-405537848|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[9]+568446438|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[14]-1019803690|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[3]-187363961|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[8]+1163531501|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[13]-1444681467|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[2]-51403784|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[7]+1735328473|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[12]-1926607734|0,n=(n<<20|n>>>12)+i|0,r+=(n^i^a)+e[5]-378558|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[8]-2022574463|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[11]+1839030562|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[14]-35309556|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[1]-1530992060|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[4]+1272893353|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[7]-155497632|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[10]-1094730640|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[13]+681279174|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[0]-358537222|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[3]-722521979|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[6]+76029189|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[9]-640364487|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[12]-421815835|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[15]+530742520|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[2]-995338651|0,n=(n<<23|n>>>9)+i|0,r+=(i^(n|~a))+e[0]-198630844|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[7]+1126891415|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[14]-1416354905|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[5]-57434055|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[12]+1700485571|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[3]-1894986606|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[10]-1051523|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[1]-2054922799|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[8]+1873313359|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[15]-30611744|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[6]-1560198380|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[13]+1309151649|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[4]-145523070|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[11]-1120210379|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[2]+718787259|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[9]-343485551|0,n=(n<<21|n>>>11)+i|0,t[0]=r+t[0]|0,t[1]=n+t[1]|0,t[2]=i+t[2]|0,t[3]=a+t[3]|0}function r(t){var e,r=[];for(e=0;e<64;e+=4)r[e>>2]=t.charCodeAt(e)+(t.charCodeAt(e+1)<<8)+(t.charCodeAt(e+2)<<16)+(t.charCodeAt(e+3)<<24);return r}function n(t){var e,r=[];for(e=0;e<64;e+=4)r[e>>2]=t[e]+(t[e+1]<<8)+(t[e+2]<<16)+(t[e+3]<<24);return r}function i(t){var n,i,a,u,o,s,f=t.length,c=[1732584193,-271733879,-1732584194,271733878];for(n=64;n<=f;n+=64)e(c,r(t.substring(n-64,n)));for(t=t.substring(n-64),i=t.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],n=0;n<i;n+=1)a[n>>2]|=t.charCodeAt(n)<<(n%4<<3);if(a[n>>2]|=128<<(n%4<<3),n>55)for(e(c,a),n=0;n<16;n+=1)a[n]=0;return u=8*f,u=u.toString(16).match(/(.*?)(.{0,8})$/),o=parseInt(u[2],16),s=parseInt(u[1],16)||0,a[14]=o,a[15]=s,e(c,a),c}function a(t){var r,i,a,u,o,s,f=t.length,c=[1732584193,-271733879,-1732584194,271733878];for(r=64;r<=f;r+=64)e(c,n(t.subarray(r-64,r)));for(t=r-64<f?t.subarray(r-64):new Uint8Array(0),i=t.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],r=0;r<i;r+=1)a[r>>2]|=t[r]<<(r%4<<3);if(a[r>>2]|=128<<(r%4<<3),r>55)for(e(c,a),r=0;r<16;r+=1)a[r]=0;return u=8*f,u=u.toString(16).match(/(.*?)(.{0,8})$/),o=parseInt(u[2],16),s=parseInt(u[1],16)||0,a[14]=o,a[15]=s,e(c,a),c}function u(t){var e,r="";for(e=0;e<4;e+=1)r+=p[t>>8*e+4&15]+p[t>>8*e&15];return r}function o(t){var e;for(e=0;e<t.length;e+=1)t[e]=u(t[e]);return t.join("")}function s(t){return/[\u0080-\uFFFF]/.test(t)&&(t=unescape(encodeURIComponent(t))),t}function f(t,e){var r,n=t.length,i=new ArrayBuffer(n),a=new Uint8Array(i);for(r=0;r<n;r+=1)a[r]=t.charCodeAt(r);return e?a:i}function c(t){return String.fromCharCode.apply(null,new Uint8Array(t))}function h(t,e,r){var n=new Uint8Array(t.byteLength+e.byteLength);return n.set(new Uint8Array(t)),n.set(new Uint8Array(e),t.byteLength),r?n:n.buffer}function l(t){var e,r=[],n=t.length;for(e=0;e<n-1;e+=2)r.push(parseInt(t.substr(e,2),16));return String.fromCharCode.apply(String,r)}function d(){this.reset()}var p=["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"];return"5d41402abc4b2a76b9719d911017c592"!==o(i("hello"))&&function(t,e){var r=(65535&t)+(65535&e);return(t>>16)+(e>>16)+(r>>16)<<16|65535&r},"undefined"==typeof ArrayBuffer||ArrayBuffer.prototype.slice||function(){function e(t,e){return t=0|t||0,t<0?Math.max(t+e,0):Math.min(t,e)}ArrayBuffer.prototype.slice=function(r,n){var i,a,u,o,s=this.byteLength,f=e(r,s),c=s;return n!==t&&(c=e(n,s)),f>c?new ArrayBuffer(0):(i=c-f,a=new ArrayBuffer(i),u=new Uint8Array(a),o=new Uint8Array(this,f,i),u.set(o),a)}}(),d.prototype.append=function(t){return this.appendBinary(s(t)),this},d.prototype.appendBinary=function(t){this._buff+=t,this._length+=t.length;var n,i=this._buff.length;for(n=64;n<=i;n+=64)e(this._hash,r(this._buff.substring(n-64,n)));return this._buff=this._buff.substring(n-64),this},d.prototype.end=function(t){var e,r,n=this._buff,i=n.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(e=0;e<i;e+=1)a[e>>2]|=n.charCodeAt(e)<<(e%4<<3);return this._finish(a,i),r=o(this._hash),t&&(r=l(r)),this.reset(),r},d.prototype.reset=function(){return this._buff="",this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},d.prototype.getState=function(){return{buff:this._buff,length:this._length,hash:this._hash}},d.prototype.setState=function(t){return this._buff=t.buff,this._length=t.length,this._hash=t.hash,this},d.prototype.destroy=function(){delete this._hash,delete this._buff,delete this._length},d.prototype._finish=function(t,r){var n,i,a,u=r;if(t[u>>2]|=128<<(u%4<<3),u>55)for(e(this._hash,t),u=0;u<16;u+=1)t[u]=0;n=8*this._length,n=n.toString(16).match(/(.*?)(.{0,8})$/),i=parseInt(n[2],16),a=parseInt(n[1],16)||0,t[14]=i,t[15]=a,e(this._hash,t)},d.hash=function(t,e){return d.hashBinary(s(t),e)},d.hashBinary=function(t,e){var r=i(t),n=o(r);return e?l(n):n},d.ArrayBuffer=function(){this.reset()},d.ArrayBuffer.prototype.append=function(t){var r,i=h(this._buff.buffer,t,!0),a=i.length;for(this._length+=t.byteLength,r=64;r<=a;r+=64)e(this._hash,n(i.subarray(r-64,r)));return this._buff=r-64<a?new Uint8Array(i.buffer.slice(r-64)):new Uint8Array(0),this},d.ArrayBuffer.prototype.end=function(t){var e,r,n=this._buff,i=n.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(e=0;e<i;e+=1)a[e>>2]|=n[e]<<(e%4<<3);return this._finish(a,i),r=o(this._hash),t&&(r=l(r)),this.reset(),r},d.ArrayBuffer.prototype.reset=function(){return this._buff=new Uint8Array(0),this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},d.ArrayBuffer.prototype.getState=function(){var t=d.prototype.getState.call(this);return t.buff=c(t.buff),t},d.ArrayBuffer.prototype.setState=function(t){return t.buff=f(t.buff,!0),d.prototype.setState.call(this,t)},d.ArrayBuffer.prototype.destroy=d.prototype.destroy,d.ArrayBuffer.prototype._finish=d.prototype._finish,d.ArrayBuffer.hash=function(t,e){var r=a(new Uint8Array(t)),n=o(r);return e?l(n):n},d})},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return u});var i=r(0),a=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),u=function(){function t(e,r,a){var u=this;n(this,t),this.file=e,this.attributes={filename:e.name,content_type:e.type,byte_size:e.size,checksum:r},this.xhr=new XMLHttpRequest,this.xhr.open("POST",a,!0),this.xhr.responseType="json",this.xhr.setRequestHeader("Content-Type","application/json"),this.xhr.setRequestHeader("Accept","application/json"),this.xhr.setRequestHeader("X-Requested-With","XMLHttpRequest"),this.xhr.setRequestHeader("X-CSRF-Token",Object(i.d)("csrf-token")),this.xhr.addEventListener("load",function(t){return u.requestDidLoad(t)}),this.xhr.addEventListener("error",function(t){return u.requestDidError(t)})}return a(t,[{key:"create",value:function(t){this.callback=t,this.xhr.send(JSON.stringify({blob:this.attributes}))}},{key:"requestDidLoad",value:function(t){if(this.status>=200&&this.status<300){var e=this.response,r=e.direct_upload;delete e.direct_upload,this.attributes=e,this.directUploadData=r,this.callback(null,this.toJSON())}else this.requestDidError(t)}},{key:"requestDidError",value:function(t){this.callback('Error creating Blob for "'+this.file.name+'". Status: '+this.status)}},{key:"toJSON",value:function(){var t={};for(var e in this.attributes)t[e]=this.attributes[e];return t}},{key:"status",get:function(){return this.xhr.status}},{key:"response",get:function(){var t=this.xhr,e=t.responseType,r=t.response;return"json"==e?r:JSON.parse(r)}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return a});var i=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),a=function(){function t(e){var r=this;n(this,t),this.blob=e,this.file=e.file;var i=e.directUploadData,a=i.url,u=i.headers;this.xhr=new XMLHttpRequest,this.xhr.open("PUT",a,!0),this.xhr.responseType="text";for(var o in u)this.xhr.setRequestHeader(o,u[o]);this.xhr.addEventListener("load",function(t){return r.requestDidLoad(t)}),this.xhr.addEventListener("error",function(t){return r.requestDidError(t)})}return i(t,[{key:"create",value:function(t){this.callback=t,this.xhr.send(this.file.slice())}},{key:"requestDidLoad",value:function(t){var e=this.xhr,r=e.status,n=e.response;r>=200&&r<300?this.callback(null,n):this.requestDidError(t)}},{key:"requestDidError",value:function(t){this.callback('Error storing "'+this.file.name+'". Status: '+this.xhr.status)}}]),t}()}])}); diff --git a/activestorage/app/controllers/active_storage/base_controller.rb b/activestorage/app/controllers/active_storage/base_controller.rb new file mode 100644 index 0000000000..59312ac8df --- /dev/null +++ b/activestorage/app/controllers/active_storage/base_controller.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# The base controller for all ActiveStorage controllers. +class ActiveStorage::BaseController < ActionController::Base + protect_from_forgery with: :exception + + before_action do + ActiveStorage::Current.host = request.base_url + end +end diff --git a/activestorage/app/controllers/active_storage/blobs_controller.rb b/activestorage/app/controllers/active_storage/blobs_controller.rb index fa44131048..92e54c386d 100644 --- a/activestorage/app/controllers/active_storage/blobs_controller.rb +++ b/activestorage/app/controllers/active_storage/blobs_controller.rb @@ -4,7 +4,7 @@ # Note: These URLs are publicly accessible. If you need to enforce access protection beyond the # security-through-obscurity factor of the signed blob references, you'll need to implement your own # authenticated redirection controller. -class ActiveStorage::BlobsController < ActionController::Base +class ActiveStorage::BlobsController < ActiveStorage::BaseController include ActiveStorage::SetBlob def show diff --git a/activestorage/app/controllers/active_storage/direct_uploads_controller.rb b/activestorage/app/controllers/active_storage/direct_uploads_controller.rb index 205d173648..78b43fc94c 100644 --- a/activestorage/app/controllers/active_storage/direct_uploads_controller.rb +++ b/activestorage/app/controllers/active_storage/direct_uploads_controller.rb @@ -3,7 +3,7 @@ # Creates a new blob on the server side in anticipation of a direct-to-service upload from the client side. # When the client-side upload is completed, the signed_blob_id can be submitted as part of the form to reference # the blob that was created up front. -class ActiveStorage::DirectUploadsController < ActionController::Base +class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController def create blob = ActiveStorage::Blob.create_before_direct_upload!(blob_args) render json: direct_upload_json(blob) @@ -15,7 +15,7 @@ class ActiveStorage::DirectUploadsController < ActionController::Base end def direct_upload_json(blob) - blob.as_json(methods: :signed_id).merge(direct_upload: { + blob.as_json(root: false, methods: :signed_id).merge(direct_upload: { url: blob.service_url_for_direct_upload, headers: blob.service_headers_for_direct_upload }) diff --git a/activestorage/app/controllers/active_storage/disk_controller.rb b/activestorage/app/controllers/active_storage/disk_controller.rb index a7e10c0696..7bc5eb3fdb 100644 --- a/activestorage/app/controllers/active_storage/disk_controller.rb +++ b/activestorage/app/controllers/active_storage/disk_controller.rb @@ -4,8 +4,8 @@ # This means using expiring, signed URLs that are meant for immediate access, not permanent linking. # Always go through the BlobsController, or your own authenticated controller, rather than directly # to the service url. -class ActiveStorage::DiskController < ActionController::Base - skip_forgery_protection if default_protect_from_forgery +class ActiveStorage::DiskController < ActiveStorage::BaseController + skip_forgery_protection def show if key = decode_verified_key diff --git a/activestorage/app/controllers/active_storage/representations_controller.rb b/activestorage/app/controllers/active_storage/representations_controller.rb index e0e944dc9c..ce9286db7d 100644 --- a/activestorage/app/controllers/active_storage/representations_controller.rb +++ b/activestorage/app/controllers/active_storage/representations_controller.rb @@ -4,7 +4,7 @@ # Note: These URLs are publicly accessible. If you need to enforce access protection beyond the # security-through-obscurity factor of the signed blob and variation reference, you'll need to implement your own # authenticated redirection controller. -class ActiveStorage::RepresentationsController < ActionController::Base +class ActiveStorage::RepresentationsController < ActiveStorage::BaseController include ActiveStorage::SetBlob def show diff --git a/activestorage/app/javascript/activestorage/ujs.js b/activestorage/app/javascript/activestorage/ujs.js index 1dda02936f..08c535470d 100644 --- a/activestorage/app/javascript/activestorage/ujs.js +++ b/activestorage/app/javascript/activestorage/ujs.js @@ -59,7 +59,7 @@ function submitForm(form) { } else { button = document.createElement("input") button.type = "submit" - button.style = "display:none" + button.style.display = "none" form.appendChild(button) button.click() form.removeChild(button) diff --git a/activestorage/app/models/active_storage/attachment.rb b/activestorage/app/models/active_storage/attachment.rb index 19f48c57d6..c59877a9a5 100644 --- a/activestorage/app/models/active_storage/attachment.rb +++ b/activestorage/app/models/active_storage/attachment.rb @@ -14,7 +14,7 @@ class ActiveStorage::Attachment < ActiveRecord::Base delegate_missing_to :blob - after_create_commit :identify_blob, :analyze_blob_later + after_create_commit :analyze_blob_later, :identify_blob # Synchronously purges the blob (deletes it from the configured service) and destroys the attachment. def purge diff --git a/activestorage/app/models/active_storage/blob/representable.rb b/activestorage/app/models/active_storage/blob/representable.rb index 88fc25b7ae..fea62e62de 100644 --- a/activestorage/app/models/active_storage/blob/representable.rb +++ b/activestorage/app/models/active_storage/blob/representable.rb @@ -20,7 +20,7 @@ module ActiveStorage::Blob::Representable # # <%= image_tag Current.user.avatar.variant(resize: "100x100") %> # - # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::VariantsController + # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController # can then produce on-demand. # # Raises ActiveStorage::InvariableError if ImageMagick cannot transform the blob. To determine whether a blob is diff --git a/activestorage/app/models/active_storage/current.rb b/activestorage/app/models/active_storage/current.rb new file mode 100644 index 0000000000..7e431d8462 --- /dev/null +++ b/activestorage/app/models/active_storage/current.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ActiveStorage::Current < ActiveSupport::CurrentAttributes #:nodoc: + attribute :host +end diff --git a/activestorage/app/models/active_storage/variant.rb b/activestorage/app/models/active_storage/variant.rb index a95a4bfd7c..d84208419c 100644 --- a/activestorage/app/models/active_storage/variant.rb +++ b/activestorage/app/models/active_storage/variant.rb @@ -13,14 +13,14 @@ require "active_storage/downloading" # into memory. The larger the image, the more memory is used. Because of this process, you also want to be # considerate about when the variant is actually processed. You shouldn't be processing variants inline in a # template, for example. Delay the processing to an on-demand controller, like the one provided in -# ActiveStorage::VariantsController. +# ActiveStorage::RepresentationsController. # # To refer to such a delayed on-demand variant, simply link to the variant through the resolved route provided # by Active Storage like so: # # <%= image_tag Current.user.avatar.variant(resize: "100x100") %> # -# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::VariantsController +# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController # can then produce on-demand. # # When you do want to actually produce the variant needed, call +processed+. This will check that the variant @@ -65,7 +65,7 @@ class ActiveStorage::Variant # it allows permanent URLs that redirect to the +service_url+ to be cached in the view. # # Use <tt>url_for(variant)</tt> (or the implied form, like +link_to variant+ or +redirect_to variant+) to get the stable URL - # for a variant that points to the ActiveStorage::VariantsController, which in turn will use this +service_call+ method + # for a variant that points to the ActiveStorage::RepresentationsController, which in turn will use this +service_call+ method # for its redirection. def service_url(expires_in: service.url_expires_in, disposition: :inline) service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type diff --git a/activestorage/lib/active_storage/service/disk_service.rb b/activestorage/lib/active_storage/service/disk_service.rb index 75b66081c3..5b652fe74e 100644 --- a/activestorage/lib/active_storage/service/disk_service.rb +++ b/activestorage/lib/active_storage/service/disk_service.rb @@ -78,8 +78,9 @@ module ActiveStorage verified_key_with_expiration = ActiveStorage.verifier.generate(key, expires_in: expires_in, purpose: :blob_key) generated_url = - url_helpers.rails_disk_service_path( + url_helpers.rails_disk_service_url( verified_key_with_expiration, + host: current_host, filename: filename, disposition: content_disposition_with(type: disposition, filename: filename), content_type: content_type @@ -104,7 +105,7 @@ module ActiveStorage purpose: :blob_token } ) - generated_url = url_helpers.update_rails_disk_service_path(verified_token_with_expiration) + generated_url = url_helpers.update_rails_disk_service_url(verified_token_with_expiration, host: current_host) payload[:url] = generated_url @@ -129,7 +130,6 @@ module ActiveStorage path_for(key).tap { |path| FileUtils.mkdir_p File.dirname(path) } end - def ensure_integrity_of(key, checksum) unless Digest::MD5.file(path_for(key)).base64digest == checksum delete key @@ -137,9 +137,12 @@ module ActiveStorage end end - def url_helpers @url_helpers ||= Rails.application.routes.url_helpers end + + def current_host + ActiveStorage::Current.host + end end end diff --git a/activestorage/test/controllers/direct_uploads_controller_test.rb b/activestorage/test/controllers/direct_uploads_controller_test.rb index 88d85e12ab..1b16da17d9 100644 --- a/activestorage/test/controllers/direct_uploads_controller_test.rb +++ b/activestorage/test/controllers/direct_uploads_controller_test.rb @@ -121,4 +121,27 @@ class ActiveStorage::DiskDirectUploadsControllerTest < ActionDispatch::Integrati assert_equal({ "Content-Type" => "text/plain" }, details["direct_upload"]["headers"]) end end + + test "creating new direct upload does not include root in json" do + checksum = Digest::MD5.base64digest("Hello") + + set_include_root_in_json(true) do + post rails_direct_uploads_url, params: { blob: { + filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain" } } + end + + @response.parsed_body.tap do |details| + assert_nil details["blob"] + assert_not_nil details["id"] + end + end + + private + def set_include_root_in_json(value) + original = ActiveRecord::Base.include_root_in_json + ActiveRecord::Base.include_root_in_json = value + yield + ensure + ActiveRecord::Base.include_root_in_json = original + end end diff --git a/activestorage/test/models/attachments_test.rb b/activestorage/test/models/attachments_test.rb index 29b83eb841..ce83ec27d2 100644 --- a/activestorage/test/models/attachments_test.rb +++ b/activestorage/test/models/attachments_test.rb @@ -136,9 +136,7 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase end test "identify newly-attached, directly-uploaded blob" do - # Simulate a direct upload. - blob = create_blob_before_direct_upload(filename: "racecar.jpg", content_type: "application/octet-stream", byte_size: 1124062, checksum: "7GjDDNEQb4mzMzsW+MS0JQ==") - ActiveStorage::Blob.service.upload(blob.key, file_fixture("racecar.jpg").open) + blob = directly_upload_file_blob(content_type: "application/octet-stream") @user.avatar.attach(blob) @@ -146,6 +144,18 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase assert_predicate @user.avatar, :identified? end + test "identify and analyze newly-attached, directly-uploaded blob" do + blob = directly_upload_file_blob(content_type: "application/octet-stream") + + perform_enqueued_jobs do + @user.avatar.attach blob + end + + assert_equal true, @user.avatar.reload.metadata[:identified] + assert_equal 4104, @user.avatar.metadata[:width] + assert_equal 2736, @user.avatar.metadata[:height] + end + test "identify newly-attached blob only once" do blob = create_file_blob assert_predicate blob, :identified? diff --git a/activestorage/test/models/blob_test.rb b/activestorage/test/models/blob_test.rb index 202d0fb093..fead17d33a 100644 --- a/activestorage/test/models/blob_test.rb +++ b/activestorage/test/models/blob_test.rb @@ -140,6 +140,6 @@ class ActiveStorage::BlobTest < ActiveSupport::TestCase def expected_url_for(blob, disposition: :inline, filename: nil) filename ||= blob.filename query_string = { content_type: blob.content_type, disposition: "#{disposition}; #{filename.parameters}" }.to_param - "/rails/active_storage/disk/#{ActiveStorage.verifier.generate(blob.key, expires_in: 5.minutes, purpose: :blob_key)}/#{filename}?#{query_string}" + "https://example.com/rails/active_storage/disk/#{ActiveStorage.verifier.generate(blob.key, expires_in: 5.minutes, purpose: :blob_key)}/#{filename}?#{query_string}" end end diff --git a/activestorage/test/service/disk_service_test.rb b/activestorage/test/service/disk_service_test.rb index 4a6361b920..d7142de458 100644 --- a/activestorage/test/service/disk_service_test.rb +++ b/activestorage/test/service/disk_service_test.rb @@ -8,7 +8,7 @@ class ActiveStorage::Service::DiskServiceTest < ActiveSupport::TestCase include ActiveStorage::Service::SharedServiceTests test "url generation" do - assert_match(/rails\/active_storage\/disk\/.*\/avatar\.png\?content_type=image%2Fpng&disposition=inline/, + assert_match(/^https:\/\/example.com\/rails\/active_storage\/disk\/.*\/avatar\.png\?content_type=image%2Fpng&disposition=inline/, @service.url(FIXTURE_KEY, expires_in: 5.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png")) end end diff --git a/activestorage/test/test_helper.rb b/activestorage/test/test_helper.rb index 2a8e153303..028874f374 100644 --- a/activestorage/test/test_helper.rb +++ b/activestorage/test/test_helper.rb @@ -41,6 +41,14 @@ ActiveStorage.verifier = ActiveSupport::MessageVerifier.new("Testing") class ActiveSupport::TestCase self.file_fixture_path = File.expand_path("fixtures/files", __dir__) + setup do + ActiveStorage::Current.host = "https://example.com" + end + + teardown do + ActiveStorage::Current.reset + end + private def create_blob(data: "Hello world!", filename: "hello.txt", content_type: "text/plain") ActiveStorage::Blob.create_after_upload! io: StringIO.new(data), filename: filename, content_type: content_type @@ -54,6 +62,16 @@ class ActiveSupport::TestCase ActiveStorage::Blob.create_before_direct_upload! filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type end + def directly_upload_file_blob(filename: "racecar.jpg", content_type: "image/jpeg") + file = file_fixture(filename) + byte_size = file.size + checksum = Digest::MD5.file(file).base64digest + + create_blob_before_direct_upload(filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type).tap do |blob| + ActiveStorage::Blob.service.upload(blob.key, file.open) + end + end + def read_image(blob_or_variant) MiniMagick::Image.open blob_or_variant.service.send(:path_for, blob_or_variant.key) end diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 4cc15a3384..62c0f612a8 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,20 @@ +* Fix bug where `ActiveSupport::Timezone.all` would fail when tzinfo data for + any timezone defined in `ActiveSupport::TimeZone::MAPPING` is missing. + + *Dominik Sander* + +* Redis cache store: `delete_matched` no longer blocks the Redis server. + (Switches from evaled Lua to a batched SCAN + DEL loop.) + + *Gleb Mazovetskiy* + +* Fix bug where `ActiveSupport::Cache` will massively inflate the storage + size when compression is enabled (which is true by default). This patch + does not attempt to repair existing data: please manually flush the cache + to clear out the problematic entries. + + *Godfrey Chan* + * Fix bug where `URI.unscape` would fail with mixed Unicode/escaped character input: URI.unescape("\xe3\x83\x90") # => "バ" @@ -6,6 +23,39 @@ *Ashe Connor*, *Aaron Patterson* +* Add `before?` and `after?` methods to `Date`, `DateTime`, + `Time`, and `TimeWithZone`. + + *Nick Holden* + +* `ActiveSupport::Inflector#ordinal` and `ActiveSupport::Inflector#ordinalize` now support + translations through I18n. + + # locale/fr.rb + + { + fr: { + number: { + nth: { + ordinals: lambda do |_key, number:, **_options| + if number.to_i.abs == 1 + 'er' + else + 'e' + end + end, + + ordinalized: lambda do |_key, number:, **_options| + "#{number}#{ActiveSupport::Inflector.ordinal(number)}" + end + } + } + } + } + + + *Christian Blais* + * Add `:private` option to ActiveSupport's `Module#delegate` in order to delegate methods as private: diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 6967c164ab..d769e2c8ea 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -333,8 +333,9 @@ module ActiveSupport # the cache with the given key, then that data is returned. Otherwise, # +nil+ is returned. # - # Note, if data was written with the <tt>:expires_in<tt> or <tt>:version</tt> options, - # both of these conditions are applied before the data is returned. + # Note, if data was written with the <tt>:expires_in</tt> or + # <tt>:version</tt> options, both of these conditions are applied before + # the data is returned. # # Options are passed to the underlying cache implementation. def read(name, options = nil) @@ -712,17 +713,14 @@ module ActiveSupport DEFAULT_COMPRESS_LIMIT = 1.kilobyte # Creates a new cache entry for the specified value. Options supported are - # +:compress+, +:compress_threshold+, and +:expires_in+. - def initialize(value, options = {}) - @value = value - if should_compress?(options) - compress! - end - - @version = options[:version] + # +:compress+, +:compress_threshold+, +:version+ and +:expires_in+. + def initialize(value, compress: true, compress_threshold: DEFAULT_COMPRESS_LIMIT, version: nil, expires_in: nil, **) + @value = value + @version = version @created_at = Time.now.to_f - @expires_in = options[:expires_in] - @expires_in = @expires_in.to_f if @expires_in + @expires_in = expires_in && expires_in.to_f + + compress!(compress_threshold) if compress end def value @@ -754,17 +752,13 @@ module ActiveSupport # Returns the size of the cached value. This could be less than # <tt>value.size</tt> if the data is compressed. def size - if defined?(@s) - @s + case value + when NilClass + 0 + when String + @value.bytesize else - case value - when NilClass - 0 - when String - @value.bytesize - else - @s = Marshal.dump(@value).bytesize - end + @s ||= Marshal.dump(@value).bytesize end end @@ -781,31 +775,35 @@ module ActiveSupport end private - def should_compress?(options) - if @value && options.fetch(:compress, true) - compress_threshold = options.fetch(:compress_threshold, DEFAULT_COMPRESS_LIMIT) - serialized_value_size = (@value.is_a?(String) ? @value : marshaled_value).bytesize + def compress!(compress_threshold) + case @value + when nil, true, false, Numeric + uncompressed_size = 0 + when String + uncompressed_size = @value.bytesize + else + serialized = Marshal.dump(@value) + uncompressed_size = serialized.bytesize + end + + if uncompressed_size >= compress_threshold + serialized ||= Marshal.dump(@value) + compressed = Zlib::Deflate.deflate(serialized) - serialized_value_size >= compress_threshold + if compressed.bytesize < uncompressed_size + @value = compressed + @compressed = true + end end end def compressed? - defined?(@compressed) ? @compressed : false - end - - def compress! - @value = Zlib::Deflate.deflate(marshaled_value) - @compressed = true + defined?(@compressed) end def uncompress(value) Marshal.load(Zlib::Inflate.inflate(value)) end - - def marshaled_value - @marshaled_value ||= Marshal.dump(@value) - end end end end diff --git a/activesupport/lib/active_support/cache/redis_cache_store.rb b/activesupport/lib/active_support/cache/redis_cache_store.rb index a1cb6db25d..11c574258f 100644 --- a/activesupport/lib/active_support/cache/redis_cache_store.rb +++ b/activesupport/lib/active_support/cache/redis_cache_store.rb @@ -62,8 +62,9 @@ module ActiveSupport end end - DELETE_GLOB_LUA = "for i, name in ipairs(redis.call('KEYS', ARGV[1])) do redis.call('DEL', name); end" - private_constant :DELETE_GLOB_LUA + # The maximum number of entries to receive per SCAN call. + SCAN_BATCH_SIZE = 1000 + private_constant :SCAN_BATCH_SIZE # Support raw values in the local cache strategy. module LocalCacheWithRaw # :nodoc: @@ -231,12 +232,18 @@ module ActiveSupport # Failsafe: Raises errors. def delete_matched(matcher, options = nil) instrument :delete_matched, matcher do - case matcher - when String - redis.with { |c| c.eval DELETE_GLOB_LUA, [], [namespace_key(matcher, options)] } - else + unless String === matcher raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}" end + redis.with do |c| + pattern = namespace_key(matcher, options) + cursor = "0" + # Fetch keys in batches using SCAN to avoid blocking the Redis server. + begin + cursor, keys = c.scan(cursor, match: pattern, count: SCAN_BATCH_SIZE) + c.del(*keys) unless keys.empty? + end until cursor == "0" + end end end @@ -286,7 +293,7 @@ module ActiveSupport # Failsafe: Raises errors. def clear(options = nil) failsafe :clear do - if namespace = merged_options(options)[namespace] + if namespace = merged_options(options)[:namespace] delete_matched "*", namespace: namespace else redis.with { |c| c.flushdb } diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index 9a3728d986..a1b841ec3d 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -809,7 +809,9 @@ module ActiveSupport names.each do |name| name = name.to_sym - set_callbacks name, CallbackChain.new(name, options) + ([self] + ActiveSupport::DescendantsTracker.descendants(self)).each do |target| + target.set_callbacks name, CallbackChain.new(name, options) + end module_eval <<-RUBY, __FILE__, __LINE__ + 1 def _run_#{name}_callbacks(&block) 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 f6cb1a384c..de13f00e60 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 @@ -60,6 +60,16 @@ module DateAndTime !WEEKEND_DAYS.include?(wday) end + # Returns true if the date/time before <tt>date_or_time</tt>. + def before?(date_or_time) + self < date_or_time + end + + # Returns true if the date/time after <tt>date_or_time</tt>. + def after?(date_or_time) + self > date_or_time + end + # Returns a new date/time the specified number of days ago. def days_ago(days) advance(days: -days) diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index f01d01e6aa..edde4f46b9 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -6,7 +6,7 @@ module Enumerable # We can't use Refinements here because Refinements with Module which will be prepended # doesn't work well https://bugs.ruby-lang.org/issues/13446 - alias :_original_sum_with_required_identity :sum + alias :_original_sum_with_required_identity :sum # :nodoc: private :_original_sum_with_required_identity # Calculates a sum from the elements. 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 580baffa2b..01fee0fb74 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -163,10 +163,10 @@ class Module # parent class. Similarly if parent class changes the value then that would # change the value of subclasses too. # - # class Male < Person + # class Citizen < Person # end # - # Male.new.hair_colors << :blue + # Citizen.new.hair_colors << :blue # Person.new.hair_colors # => [:brown, :black, :blonde, :red, :blue] # # To opt out of the instance writer method, pass <tt>instance_writer: false</tt>. diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index ec3497173f..7f42f44efb 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -22,8 +22,9 @@ class Module # ==== Options # * <tt>:to</tt> - Specifies the target object # * <tt>:prefix</tt> - Prefixes the new method with the target name or a custom prefix - # * <tt>:allow_nil</tt> - if set to true, prevents a +Module::DelegationError+ + # * <tt>:allow_nil</tt> - If set to true, prevents a +Module::DelegationError+ # from being raised + # * <tt>:private</tt> - If set to true, changes method visibility to private # # The macro receives one or more method names (specified as symbols or # strings) and the name of the target object via the <tt>:to</tt> option diff --git a/activesupport/lib/active_support/deprecation/reporting.rb b/activesupport/lib/active_support/deprecation/reporting.rb index 2c004f4c9e..7075b5b869 100644 --- a/activesupport/lib/active_support/deprecation/reporting.rb +++ b/activesupport/lib/active_support/deprecation/reporting.rb @@ -104,7 +104,7 @@ module ActiveSupport end end - RAILS_GEM_ROOT = File.expand_path("../../../..", __dir__) + RAILS_GEM_ROOT = File.expand_path("../../../..", __dir__) + "/" def ignored_callstack(path) path.start_with?(RAILS_GEM_ROOT) || path.start_with?(RbConfig::CONFIG["rubylibdir"]) diff --git a/activesupport/lib/active_support/encrypted_configuration.rb b/activesupport/lib/active_support/encrypted_configuration.rb index dab953d5d5..3c6da10548 100644 --- a/activesupport/lib/active_support/encrypted_configuration.rb +++ b/activesupport/lib/active_support/encrypted_configuration.rb @@ -38,10 +38,6 @@ module ActiveSupport @options ||= ActiveSupport::InheritableOptions.new(config) end - def serialize(config) - config.present? ? YAML.dump(config) : "" - end - def deserialize(config) config.present? ? YAML.load(config, content_path) : {} end diff --git a/activesupport/lib/active_support/encrypted_file.rb b/activesupport/lib/active_support/encrypted_file.rb index 671b6b6a69..c66f1b557e 100644 --- a/activesupport/lib/active_support/encrypted_file.rb +++ b/activesupport/lib/active_support/encrypted_file.rb @@ -57,7 +57,7 @@ module ActiveSupport private def writing(contents) - tmp_file = "#{content_path.basename}.#{Process.pid}" + tmp_file = "#{Process.pid}.#{content_path.basename.to_s.chomp('.enc')}" tmp_path = Pathname.new File.join(Dir.tmpdir, tmp_file) tmp_path.binwrite contents diff --git a/activesupport/lib/active_support/i18n.rb b/activesupport/lib/active_support/i18n.rb index d60b3eff30..39dab1cc71 100644 --- a/activesupport/lib/active_support/i18n.rb +++ b/activesupport/lib/active_support/i18n.rb @@ -13,3 +13,4 @@ require "active_support/lazy_load_hooks" ActiveSupport.run_load_hooks(:i18n) I18n.load_path << File.expand_path("locale/en.yml", __dir__) +I18n.load_path << File.expand_path("locale/en.rb", __dir__) diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 7e782e2a93..339b93b8da 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -341,18 +341,7 @@ module ActiveSupport # ordinal(-11) # => "th" # ordinal(-1021) # => "st" def ordinal(number) - abs_number = number.to_i.abs - - if (11..13).include?(abs_number % 100) - "th" - else - case abs_number % 10 - when 1; "st" - when 2; "nd" - when 3; "rd" - else "th" - end - end + I18n.translate("number.nth.ordinals", number: number) end # Turns a number into an ordinal string used to denote the position in an @@ -365,7 +354,7 @@ module ActiveSupport # ordinalize(-11) # => "-11th" # ordinalize(-1021) # => "-1021st" def ordinalize(number) - "#{number}#{ordinal(number)}" + I18n.translate("number.nth.ordinalized", number: number) end private diff --git a/activesupport/lib/active_support/locale/en.rb b/activesupport/lib/active_support/locale/en.rb new file mode 100644 index 0000000000..26c2280c95 --- /dev/null +++ b/activesupport/lib/active_support/locale/en.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +{ + en: { + number: { + nth: { + ordinals: lambda do |_key, number:, **_options| + abs_number = number.to_i.abs + + if (11..13).cover?(abs_number % 100) + "th" + else + case abs_number % 10 + when 1 then "st" + when 2 then "nd" + when 3 then "rd" + else "th" + end + end + end, + + ordinalized: lambda do |_key, number:, **_options| + "#{number}#{ActiveSupport::Inflector.ordinal(number)}" + end + } + } + } +} diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index 5236c776dd..8b73270894 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -3,6 +3,7 @@ require "openssl" require "base64" require "active_support/core_ext/array/extract_options" +require "active_support/core_ext/module/attribute_accessors" require "active_support/message_verifier" require "active_support/messages/metadata" diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index a698b4e61e..4e42db4500 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -43,23 +43,23 @@ module ActiveSupport # Parallelizes the test suite. # - # Takes a `workers` argument that controls how many times the process + # Takes a +workers+ argument that controls how many times the process # is forked. For each process a new database will be created suffixed # with the worker number. # # test-database-0 # test-database-1 # - # If `ENV["PARALLEL_WORKERS"]` is set the workers argument will be ignored + # If <tt>ENV["PARALLEL_WORKERS"]</tt> is set the workers argument will be ignored # and the environment variable will be used instead. This is useful for CI # environments, or other environments where you may need more workers than # you do for local testing. # - # If the number of workers is set to `1` or fewer, the tests will not be + # If the number of workers is set to +1+ or fewer, the tests will not be # parallelized. # # The default parallelization method is to fork processes. If you'd like to - # use threads instead you can pass `with: :threads` to the `parallelize` + # use threads instead you can pass <tt>with: :threads</tt> to the +parallelize+ # method. Note the threaded parallelization does not create multiple # database and will not work with system tests at this time. # diff --git a/activesupport/lib/active_support/testing/setup_and_teardown.rb b/activesupport/lib/active_support/testing/setup_and_teardown.rb index 1dbf3c5da0..35236f1401 100644 --- a/activesupport/lib/active_support/testing/setup_and_teardown.rb +++ b/activesupport/lib/active_support/testing/setup_and_teardown.rb @@ -44,8 +44,15 @@ module ActiveSupport end def after_teardown # :nodoc: - run_callbacks :teardown + begin + run_callbacks :teardown + rescue => e + error = e + end + super + ensure + raise error if error end end end diff --git a/activesupport/lib/active_support/testing/stream.rb b/activesupport/lib/active_support/testing/stream.rb index d070a1793d..127cfe1e12 100644 --- a/activesupport/lib/active_support/testing/stream.rb +++ b/activesupport/lib/active_support/testing/stream.rb @@ -33,7 +33,7 @@ module ActiveSupport yield stream_io.rewind - return captured_stream.read + captured_stream.read ensure captured_stream.close captured_stream.unlink diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 20650ce714..7e71318404 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -225,6 +225,8 @@ module ActiveSupport def <=>(other) utc <=> other end + alias_method :before?, :< + alias_method :after?, :> # Returns true if the current object's time is within the specified # +min+ and +max+ time. diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index 9dfaddb825..5f709c5fd9 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -279,7 +279,8 @@ module ActiveSupport def zones_map @zones_map ||= MAPPING.each_with_object({}) do |(name, _), zones| - zones[name] = self[name] + timezone = self[name] + zones[name] = timezone if timezone end end end diff --git a/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb b/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb index 6f59ce48d2..ed8eba8fc2 100644 --- a/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb +++ b/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb @@ -7,9 +7,9 @@ module CacheDeleteMatchedBehavior @cache.write("foo/bar", "baz") @cache.write("fu/baz", "bar") @cache.delete_matched(/oo/) - assert !@cache.exist?("foo") + assert_not @cache.exist?("foo") assert @cache.exist?("fu") - assert !@cache.exist?("foo/bar") + assert_not @cache.exist?("foo/bar") assert @cache.exist?("fu/baz") end end diff --git a/activesupport/test/cache/behaviors/cache_store_behavior.rb b/activesupport/test/cache/behaviors/cache_store_behavior.rb index efb57d34a2..f9153ffe2a 100644 --- a/activesupport/test/cache/behaviors/cache_store_behavior.rb +++ b/activesupport/test/cache/behaviors/cache_store_behavior.rb @@ -33,7 +33,7 @@ module CacheStoreBehavior cache_miss = false assert_equal 3, @cache.fetch("foo") { |key| cache_miss = true; key.length } - assert !cache_miss + assert_not cache_miss end def test_fetch_with_forced_cache_miss @@ -141,29 +141,111 @@ module CacheStoreBehavior end end - def test_read_and_write_compressed_small_data - @cache.write("foo", "bar", compress: true) - assert_equal "bar", @cache.read("foo") + # Use strings that are guarenteed to compress well, so we can easily tell if + # the compression kicked in or not. + SMALL_STRING = "0" * 100 + LARGE_STRING = "0" * 2.kilobytes + + SMALL_OBJECT = { data: SMALL_STRING } + LARGE_OBJECT = { data: LARGE_STRING } + + def test_nil_with_default_compression_settings + assert_uncompressed(nil) end - def test_read_and_write_compressed_large_data - @cache.write("foo", "bar", compress: true, compress_threshold: 2) - assert_equal "bar", @cache.read("foo") + def test_nil_with_compress_true + assert_uncompressed(nil, compress: true) end - def test_read_and_write_compressed_nil - @cache.write("foo", nil, compress: true) - assert_nil @cache.read("foo") + def test_nil_with_compress_false + assert_uncompressed(nil, compress: false) end - def test_read_and_write_uncompressed_small_data - @cache.write("foo", "bar", compress: false) - assert_equal "bar", @cache.read("foo") + def test_nil_with_compress_low_compress_threshold + assert_uncompressed(nil, compress: true, compress_threshold: 1) end - def test_read_and_write_uncompressed_nil - @cache.write("foo", nil, compress: false) - assert_nil @cache.read("foo") + def test_small_string_with_default_compression_settings + assert_uncompressed(SMALL_STRING) + end + + def test_small_string_with_compress_true + assert_uncompressed(SMALL_STRING, compress: true) + end + + def test_small_string_with_compress_false + assert_uncompressed(SMALL_STRING, compress: false) + end + + def test_small_string_with_low_compress_threshold + assert_compressed(SMALL_STRING, compress: true, compress_threshold: 1) + end + + def test_small_object_with_default_compression_settings + assert_uncompressed(SMALL_OBJECT) + end + + def test_small_object_with_compress_true + assert_uncompressed(SMALL_OBJECT, compress: true) + end + + def test_small_object_with_compress_false + assert_uncompressed(SMALL_OBJECT, compress: false) + end + + def test_small_object_with_low_compress_threshold + assert_compressed(SMALL_OBJECT, compress: true, compress_threshold: 1) + end + + def test_large_string_with_default_compression_settings + assert_compressed(LARGE_STRING) + end + + def test_large_string_with_compress_true + assert_compressed(LARGE_STRING, compress: true) + end + + def test_large_string_with_compress_false + assert_uncompressed(LARGE_STRING, compress: false) + end + + def test_large_string_with_high_compress_threshold + assert_uncompressed(LARGE_STRING, compress: true, compress_threshold: 1.megabyte) + end + + def test_large_object_with_default_compression_settings + assert_compressed(LARGE_OBJECT) + end + + def test_large_object_with_compress_true + assert_compressed(LARGE_OBJECT, compress: true) + end + + def test_large_object_with_compress_false + assert_uncompressed(LARGE_OBJECT, compress: false) + end + + def test_large_object_with_high_compress_threshold + assert_uncompressed(LARGE_OBJECT, compress: true, compress_threshold: 1.megabyte) + end + + def test_incompressable_data + assert_uncompressed(nil, compress: true, compress_threshold: 1) + assert_uncompressed(true, compress: true, compress_threshold: 1) + assert_uncompressed(false, compress: true, compress_threshold: 1) + assert_uncompressed(0, compress: true, compress_threshold: 1) + assert_uncompressed(1.2345, compress: true, compress_threshold: 1) + assert_uncompressed("", compress: true, compress_threshold: 1) + + incompressible = nil + + # generate an incompressible string + loop do + incompressible = SecureRandom.random_bytes(1.kilobyte) + break if incompressible.bytesize < Zlib::Deflate.deflate(incompressible).bytesize + end + + assert_uncompressed(incompressible, compress: true, compress_threshold: 1) end def test_cache_key @@ -226,7 +308,7 @@ module CacheStoreBehavior @cache.write("foo", "bar") assert @cache.exist?("foo") assert @cache.delete("foo") - assert !@cache.exist?("foo") + assert_not @cache.exist?("foo") end def test_original_store_objects_should_not_be_immutable @@ -359,4 +441,41 @@ module CacheStoreBehavior ensure ActiveSupport::Notifications.unsubscribe "cache_read.active_support" end + + private + + def assert_compressed(value, **options) + assert_compression(true, value, **options) + end + + def assert_uncompressed(value, **options) + assert_compression(false, value, **options) + end + + def assert_compression(should_compress, value, **options) + freeze_time do + @cache.write("actual", value, options) + @cache.write("uncompressed", value, options.merge(compress: false)) + end + + if value.nil? + assert_nil @cache.read("actual") + assert_nil @cache.read("uncompressed") + else + assert_equal value, @cache.read("actual") + assert_equal value, @cache.read("uncompressed") + end + + actual_entry = @cache.send(:read_entry, @cache.send(:normalize_key, "actual", {}), {}) + uncompressed_entry = @cache.send(:read_entry, @cache.send(:normalize_key, "uncompressed", {}), {}) + + actual_size = Marshal.dump(actual_entry).bytesize + uncompressed_size = Marshal.dump(uncompressed_entry).bytesize + + if should_compress + assert_operator actual_size, :<, uncompressed_size, "value should be compressed" + else + assert_equal uncompressed_size, actual_size, "value should not be compressed" + end + end end diff --git a/activesupport/test/cache/behaviors/cache_store_version_behavior.rb b/activesupport/test/cache/behaviors/cache_store_version_behavior.rb index 62c0bb4f6f..805f061839 100644 --- a/activesupport/test/cache/behaviors/cache_store_version_behavior.rb +++ b/activesupport/test/cache/behaviors/cache_store_version_behavior.rb @@ -30,7 +30,7 @@ module CacheStoreVersionBehavior def test_exist_with_wrong_version_should_be_false @cache.write("foo", "bar", version: 1) - assert !@cache.exist?("foo", version: 2) + assert_not @cache.exist?("foo", version: 2) end def test_reading_and_writing_with_model_supporting_cache_version diff --git a/activesupport/test/cache/behaviors/failure_safety_behavior.rb b/activesupport/test/cache/behaviors/failure_safety_behavior.rb index 53bda4f942..43b67d81db 100644 --- a/activesupport/test/cache/behaviors/failure_safety_behavior.rb +++ b/activesupport/test/cache/behaviors/failure_safety_behavior.rb @@ -63,7 +63,7 @@ module FailureSafetyBehavior @cache.write("foo", "bar") emulating_unavailability do |cache| - assert !cache.exist?("foo") + assert_not cache.exist?("foo") end end diff --git a/activesupport/test/cache/cache_entry_test.rb b/activesupport/test/cache/cache_entry_test.rb index 80ff7ad564..d7baaa5c72 100644 --- a/activesupport/test/cache/cache_entry_test.rb +++ b/activesupport/test/cache/cache_entry_test.rb @@ -13,25 +13,4 @@ class CacheEntryTest < ActiveSupport::TestCase assert entry.expired?, "entry is expired" end end - - def test_compressed_values - value = "value" * 100 - entry = ActiveSupport::Cache::Entry.new(value, compress: true, compress_threshold: 1) - assert_equal value, entry.value - assert(value.bytesize > entry.size, "value is compressed") - end - - def test_compressed_by_default - value = "value" * 100 - entry = ActiveSupport::Cache::Entry.new(value, compress_threshold: 1) - assert_equal value, entry.value - assert(value.bytesize > entry.size, "value is compressed") - end - - def test_uncompressed_values - value = "value" * 100 - entry = ActiveSupport::Cache::Entry.new(value, compress: false) - assert_equal value, entry.value - assert_equal value.bytesize, entry.size - end end diff --git a/activesupport/test/cache/cache_store_namespace_test.rb b/activesupport/test/cache/cache_store_namespace_test.rb index b52a61c500..dfdb3262f2 100644 --- a/activesupport/test/cache/cache_store_namespace_test.rb +++ b/activesupport/test/cache/cache_store_namespace_test.rb @@ -25,7 +25,7 @@ class CacheStoreNamespaceTest < ActiveSupport::TestCase cache.write("foo", "bar") cache.write("fu", "baz") cache.delete_matched(/^fo/) - assert !cache.exist?("foo") + assert_not cache.exist?("foo") assert cache.exist?("fu") end @@ -34,7 +34,7 @@ class CacheStoreNamespaceTest < ActiveSupport::TestCase cache.write("foo", "bar") cache.write("fu", "baz") cache.delete_matched(/OO/i) - assert !cache.exist?("foo") + assert_not cache.exist?("foo") assert cache.exist?("fu") end end diff --git a/activesupport/test/cache/stores/file_store_test.rb b/activesupport/test/cache/stores/file_store_test.rb index c3c35a7bcc..f6855bb308 100644 --- a/activesupport/test/cache/stores/file_store_test.rb +++ b/activesupport/test/cache/stores/file_store_test.rb @@ -68,7 +68,9 @@ class FileStoreTest < ActiveSupport::TestCase def test_filename_max_size key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}" path = @cache.send(:normalize_key, key, {}) - Dir::Tmpname.create(path) do |tmpname, n, opts| + basename = File.basename(path) + dirname = File.dirname(path) + Dir::Tmpname.create(basename, Dir.tmpdir + dirname) do |tmpname, n, opts| assert File.basename(tmpname + ".lock").length <= 255, "Temp filename too long: #{File.basename(tmpname + '.lock').length}" end end diff --git a/activesupport/test/cache/stores/memory_store_test.rb b/activesupport/test/cache/stores/memory_store_test.rb index 72fafc187b..340fb517cb 100644 --- a/activesupport/test/cache/stores/memory_store_test.rb +++ b/activesupport/test/cache/stores/memory_store_test.rb @@ -6,8 +6,7 @@ require_relative "../behaviors" class MemoryStoreTest < ActiveSupport::TestCase def setup - @record_size = ActiveSupport::Cache.lookup_store(:memory_store).send(:cached_size, 1, ActiveSupport::Cache::Entry.new("aaaaaaaaaa")) - @cache = ActiveSupport::Cache.lookup_store(:memory_store, expires_in: 60, size: @record_size * 10 + 1) + @cache = ActiveSupport::Cache.lookup_store(:memory_store, expires_in: 60) end include CacheStoreBehavior @@ -15,6 +14,13 @@ class MemoryStoreTest < ActiveSupport::TestCase include CacheDeleteMatchedBehavior include CacheIncrementDecrementBehavior include CacheInstrumentationBehavior +end + +class MemoryStorePruningTest < ActiveSupport::TestCase + def setup + @record_size = ActiveSupport::Cache.lookup_store(:memory_store).send(:cached_size, 1, ActiveSupport::Cache::Entry.new("aaaaaaaaaa")) + @cache = ActiveSupport::Cache.lookup_store(:memory_store, expires_in: 60, size: @record_size * 10 + 1) + end def test_prune_size @cache.write(1, "aaaaaaaaaa") && sleep(0.001) @@ -98,7 +104,7 @@ class MemoryStoreTest < ActiveSupport::TestCase assert @cache.exist?(4) assert @cache.exist?(3) assert @cache.exist?(2) - assert !@cache.exist?(1) + assert_not @cache.exist?(1) end def test_write_with_unless_exist diff --git a/activesupport/test/cache/stores/redis_cache_store_test.rb b/activesupport/test/cache/stores/redis_cache_store_test.rb index dda96b68fb..24c4c5c481 100644 --- a/activesupport/test/cache/stores/redis_cache_store_test.rb +++ b/activesupport/test/cache/stores/redis_cache_store_test.rb @@ -211,7 +211,7 @@ module ActiveSupport::Cache::RedisCacheStoreTests @cache.write("foo", "bar") @cache.write("fu", "baz") @cache.delete_matched("foo*") - assert !@cache.exist?("foo") + assert_not @cache.exist?("foo") assert @cache.exist?("fu") end @@ -221,4 +221,22 @@ module ActiveSupport::Cache::RedisCacheStoreTests end end end + + class ClearTest < StoreTest + test "clear all cache key" do + @cache.write("foo", "bar") + @cache.write("fu", "baz") + @cache.clear + assert_not @cache.exist?("foo") + assert_not @cache.exist?("fu") + end + + test "only clear namespace cache key" do + @cache.write("foo", "bar") + @cache.redis.set("fu", "baz") + @cache.clear + assert_not @cache.exist?("foo") + assert @cache.redis.exists("fu") + end + end end diff --git a/activesupport/test/callback_inheritance_test.rb b/activesupport/test/callback_inheritance_test.rb index 015e17deb9..5633b6e2b8 100644 --- a/activesupport/test/callback_inheritance_test.rb +++ b/activesupport/test/callback_inheritance_test.rb @@ -176,3 +176,13 @@ class DynamicInheritedCallbacks < ActiveSupport::TestCase assert_equal 1, child.count end end + +class DynamicDefinedCallbacks < ActiveSupport::TestCase + def test_callbacks_should_be_performed_once_in_child_class_after_dynamic_define + GrandParent.define_callbacks(:foo) + GrandParent.set_callback(:foo, :before, :before1) + parent = Parent.new("foo") + parent.run_callbacks(:foo) + assert_equal %w(before1), parent.log + end +end diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb index 6c7643ed72..5c9a3b29e7 100644 --- a/activesupport/test/callbacks_test.rb +++ b/activesupport/test/callbacks_test.rb @@ -829,7 +829,7 @@ module CallbacksTest def test_block_never_called_if_terminated obj = CallbackTerminator.new obj.save - assert !obj.saved + assert_not obj.saved end end @@ -857,7 +857,7 @@ module CallbacksTest def test_block_never_called_if_abort_is_thrown obj = CallbackDefaultTerminator.new obj.save - assert !obj.saved + assert_not obj.saved end end diff --git a/activesupport/test/class_cache_test.rb b/activesupport/test/class_cache_test.rb index 8cfcaedafd..1ef1939b4b 100644 --- a/activesupport/test/class_cache_test.rb +++ b/activesupport/test/class_cache_test.rb @@ -68,7 +68,7 @@ module ActiveSupport def test_new_rejects_strings @cache.store ClassCacheTest.name - assert !@cache.key?(ClassCacheTest.name) + assert_not @cache.key?(ClassCacheTest.name) end def test_store_returns_self diff --git a/activesupport/test/core_ext/date_and_time_behavior.rb b/activesupport/test/core_ext/date_and_time_behavior.rb index 1422f135a8..b77ea22701 100644 --- a/activesupport/test/core_ext/date_and_time_behavior.rb +++ b/activesupport/test/core_ext/date_and_time_behavior.rb @@ -385,6 +385,18 @@ module DateAndTimeBehavior assert_predicate date_time_init(2015, 1, 5, 15, 15, 10), :on_weekday? end + def test_before + assert_equal false, date_time_init(2017, 3, 6, 12, 0, 0).before?(date_time_init(2017, 3, 5, 12, 0, 0)) + assert_equal false, date_time_init(2017, 3, 6, 12, 0, 0).before?(date_time_init(2017, 3, 6, 12, 0, 0)) + assert_equal true, date_time_init(2017, 3, 6, 12, 0, 0).before?(date_time_init(2017, 3, 7, 12, 0, 0)) + end + + def test_after + assert_equal true, date_time_init(2017, 3, 6, 12, 0, 0).after?(date_time_init(2017, 3, 5, 12, 0, 0)) + assert_equal false, date_time_init(2017, 3, 6, 12, 0, 0).after?(date_time_init(2017, 3, 6, 12, 0, 0)) + assert_equal false, date_time_init(2017, 3, 6, 12, 0, 0).after?(date_time_init(2017, 3, 7, 12, 0, 0)) + end + def with_bw_default(bw = :monday) old_bw = Date.beginning_of_week Date.beginning_of_week = bw diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb index 8f6befe809..240ae3bde0 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -16,30 +16,30 @@ class DurationTest < ActiveSupport::TestCase assert_kind_of ActiveSupport::Duration, d assert_kind_of Numeric, d assert_kind_of Integer, d - assert !d.is_a?(Hash) + assert_not d.is_a?(Hash) k = Class.new class << k; undef_method :== end - assert !d.is_a?(k) + assert_not d.is_a?(k) end def test_instance_of assert 1.minute.instance_of?(Integer) assert 2.days.instance_of?(ActiveSupport::Duration) - assert !3.second.instance_of?(Numeric) + assert_not 3.second.instance_of?(Numeric) end def test_threequals assert ActiveSupport::Duration === 1.day - assert !(ActiveSupport::Duration === 1.day.to_i) - assert !(ActiveSupport::Duration === "foo") + assert_not (ActiveSupport::Duration === 1.day.to_i) + assert_not (ActiveSupport::Duration === "foo") end def test_equals assert 1.day == 1.day assert 1.day == 1.day.to_i assert 1.day.to_i == 1.day - assert !(1.day == "foo") + assert_not (1.day == "foo") end def test_to_s @@ -53,11 +53,11 @@ class DurationTest < ActiveSupport::TestCase assert 1.minute.eql?(1.minute) assert 1.minute.eql?(60.seconds) assert 2.days.eql?(48.hours) - assert !1.second.eql?(1) - assert !1.eql?(1.second) + assert_not 1.second.eql?(1) + assert_not 1.eql?(1.second) assert 1.minute.eql?(180.seconds - 2.minutes) - assert !1.minute.eql?(60) - assert !1.minute.eql?("foo") + assert_not 1.minute.eql?(60) + assert_not 1.minute.eql?("foo") end def test_inspect diff --git a/activesupport/test/core_ext/file_test.rb b/activesupport/test/core_ext/file_test.rb index 23e3c277cc..9c97700e5d 100644 --- a/activesupport/test/core_ext/file_test.rb +++ b/activesupport/test/core_ext/file_test.rb @@ -8,7 +8,7 @@ class AtomicWriteTest < ActiveSupport::TestCase contents = "Atomic Text" File.atomic_write(file_name, Dir.pwd) do |file| file.write(contents) - assert !File.exist?(file_name) + assert_not File.exist?(file_name) end assert File.exist?(file_name) assert_equal contents, File.read(file_name) @@ -22,7 +22,7 @@ class AtomicWriteTest < ActiveSupport::TestCase raise "something bad" end rescue - assert !File.exist?(file_name) + assert_not File.exist?(file_name) end def test_atomic_write_preserves_file_permissions @@ -50,7 +50,7 @@ class AtomicWriteTest < ActiveSupport::TestCase contents = "Atomic Text" File.atomic_write(file_name, Dir.pwd) do |file| file.write(contents) - assert !File.exist?(file_name) + assert_not File.exist?(file_name) end assert File.exist?(file_name) assert_equal File.probe_stat_in(Dir.pwd).mode, file_mode diff --git a/activesupport/test/core_ext/integer_ext_test.rb b/activesupport/test/core_ext/integer_ext_test.rb index 14169b084d..5691dc5341 100644 --- a/activesupport/test/core_ext/integer_ext_test.rb +++ b/activesupport/test/core_ext/integer_ext_test.rb @@ -8,14 +8,14 @@ class IntegerExtTest < ActiveSupport::TestCase def test_multiple_of [ -7, 0, 7, 14 ].each { |i| assert i.multiple_of?(7) } - [ -7, 7, 14 ].each { |i| assert ! i.multiple_of?(6) } + [ -7, 7, 14 ].each { |i| assert_not i.multiple_of?(6) } # test the 0 edge case assert 0.multiple_of?(0) - assert !5.multiple_of?(0) + assert_not 5.multiple_of?(0) # test with a prime - [2, 3, 5, 7].each { |i| assert !PRIME.multiple_of?(i) } + [2, 3, 5, 7].each { |i| assert_not PRIME.multiple_of?(i) } end def test_ordinalize diff --git a/activesupport/test/core_ext/module/attr_internal_test.rb b/activesupport/test/core_ext/module/attr_internal_test.rb index c2a28eced4..9a65f75497 100644 --- a/activesupport/test/core_ext/module/attr_internal_test.rb +++ b/activesupport/test/core_ext/module/attr_internal_test.rb @@ -12,7 +12,7 @@ class AttrInternalTest < ActiveSupport::TestCase def test_reader assert_nothing_raised { @target.attr_internal_reader :foo } - assert !@instance.instance_variable_defined?("@_foo") + assert_not @instance.instance_variable_defined?("@_foo") assert_raise(NoMethodError) { @instance.foo = 1 } @instance.instance_variable_set("@_foo", 1) @@ -22,7 +22,7 @@ class AttrInternalTest < ActiveSupport::TestCase def test_writer assert_nothing_raised { @target.attr_internal_writer :foo } - assert !@instance.instance_variable_defined?("@_foo") + assert_not @instance.instance_variable_defined?("@_foo") assert_nothing_raised { assert_equal 1, @instance.foo = 1 } assert_equal 1, @instance.instance_variable_get("@_foo") @@ -32,7 +32,7 @@ class AttrInternalTest < ActiveSupport::TestCase def test_accessor assert_nothing_raised { @target.attr_internal :foo } - assert !@instance.instance_variable_defined?("@_foo") + assert_not @instance.instance_variable_defined?("@_foo") assert_nothing_raised { assert_equal 1, @instance.foo = 1 } assert_equal 1, @instance.instance_variable_get("@_foo") @@ -44,10 +44,10 @@ class AttrInternalTest < ActiveSupport::TestCase assert_nothing_raised { Module.attr_internal_naming_format = "@abc%sdef" } @target.attr_internal :foo - assert !@instance.instance_variable_defined?("@_foo") - assert !@instance.instance_variable_defined?("@abcfoodef") + assert_not @instance.instance_variable_defined?("@_foo") + assert_not @instance.instance_variable_defined?("@abcfoodef") assert_nothing_raised { @instance.foo = 1 } - assert !@instance.instance_variable_defined?("@_foo") + assert_not @instance.instance_variable_defined?("@_foo") assert @instance.instance_variable_defined?("@abcfoodef") ensure Module.attr_internal_naming_format = "@_%s" diff --git a/activesupport/test/core_ext/module/concerning_test.rb b/activesupport/test/core_ext/module/concerning_test.rb index 969434766f..374114c11b 100644 --- a/activesupport/test/core_ext/module/concerning_test.rb +++ b/activesupport/test/core_ext/module/concerning_test.rb @@ -21,7 +21,7 @@ class ModuleConcernTest < ActiveSupport::TestCase # Declares a concern but doesn't include it assert klass.const_defined?(:Baz, false) - assert !ModuleConcernTest.const_defined?(:Baz) + assert_not ModuleConcernTest.const_defined?(:Baz) assert_kind_of ActiveSupport::Concern, klass::Baz assert_not_includes klass.ancestors, klass::Baz, klass.ancestors.inspect diff --git a/activesupport/test/core_ext/name_error_test.rb b/activesupport/test/core_ext/name_error_test.rb index d1dace3713..5c6c12ffc7 100644 --- a/activesupport/test/core_ext/name_error_test.rb +++ b/activesupport/test/core_ext/name_error_test.rb @@ -17,7 +17,7 @@ class NameErrorTest < ActiveSupport::TestCase exc = assert_raise NameError do some_method_that_does_not_exist end - assert !exc.missing_name?(:Foo) + assert_not exc.missing_name?(:Foo) assert_nil exc.missing_name end end diff --git a/activesupport/test/core_ext/object/acts_like_test.rb b/activesupport/test/core_ext/object/acts_like_test.rb index 9f7b81f7fc..31241caf0a 100644 --- a/activesupport/test/core_ext/object/acts_like_test.rb +++ b/activesupport/test/core_ext/object/acts_like_test.rb @@ -17,19 +17,19 @@ class ObjectTests < ActiveSupport::TestCase dt = DateTime.new duck = DuckTime.new - assert !object.acts_like?(:time) - assert !object.acts_like?(:date) + assert_not object.acts_like?(:time) + assert_not object.acts_like?(:date) assert time.acts_like?(:time) - assert !time.acts_like?(:date) + assert_not time.acts_like?(:date) - assert !date.acts_like?(:time) + assert_not date.acts_like?(:time) assert date.acts_like?(:date) assert dt.acts_like?(:time) assert dt.acts_like?(:date) assert duck.acts_like?(:time) - assert !duck.acts_like?(:date) + assert_not duck.acts_like?(:date) end end diff --git a/activesupport/test/core_ext/object/deep_dup_test.rb b/activesupport/test/core_ext/object/deep_dup_test.rb index 2486592441..1fb26ebac7 100644 --- a/activesupport/test/core_ext/object/deep_dup_test.rb +++ b/activesupport/test/core_ext/object/deep_dup_test.rb @@ -47,7 +47,7 @@ class DeepDupTest < ActiveSupport::TestCase object = Object.new dup = object.deep_dup dup.instance_variable_set(:@a, 1) - assert !object.instance_variable_defined?(:@a) + assert_not object.instance_variable_defined?(:@a) assert dup.instance_variable_defined?(:@a) end diff --git a/activesupport/test/core_ext/object/inclusion_test.rb b/activesupport/test/core_ext/object/inclusion_test.rb index 52c21f2e8e..8cbb4f848f 100644 --- a/activesupport/test/core_ext/object/inclusion_test.rb +++ b/activesupport/test/core_ext/object/inclusion_test.rb @@ -6,30 +6,30 @@ require "active_support/core_ext/object/inclusion" class InTest < ActiveSupport::TestCase def test_in_array assert 1.in?([1, 2]) - assert !3.in?([1, 2]) + assert_not 3.in?([1, 2]) end def test_in_hash h = { "a" => 100, "b" => 200 } assert "a".in?(h) - assert !"z".in?(h) + assert_not "z".in?(h) end def test_in_string assert "lo".in?("hello") - assert !"ol".in?("hello") + assert_not "ol".in?("hello") assert ?h.in?("hello") end def test_in_range assert 25.in?(1..50) - assert !75.in?(1..50) + assert_not 75.in?(1..50) end def test_in_set s = Set.new([1, 2]) assert 1.in?(s) - assert !3.in?(s) + assert_not 3.in?(s) end module A @@ -45,8 +45,8 @@ class InTest < ActiveSupport::TestCase def test_in_module assert A.in?(B) assert A.in?(C) - assert !A.in?(A) - assert !A.in?(D) + assert_not A.in?(A) + assert_not A.in?(D) end def test_no_method_catching diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb index 903c173e59..7c7a78f461 100644 --- a/activesupport/test/core_ext/range_ext_test.rb +++ b/activesupport/test/core_ext/range_ext_test.rb @@ -37,7 +37,7 @@ class RangeTest < ActiveSupport::TestCase end def test_overlaps_last_exclusive - assert !(1...5).overlaps?(5..10) + assert_not (1...5).overlaps?(5..10) end def test_overlaps_first_inclusive @@ -45,7 +45,7 @@ class RangeTest < ActiveSupport::TestCase end def test_overlaps_first_exclusive - assert !(5..10).overlaps?(1...5) + assert_not (5..10).overlaps?(1...5) end def test_should_include_identical_inclusive @@ -102,7 +102,7 @@ class RangeTest < ActiveSupport::TestCase def test_no_overlaps_on_time time_range_1 = Time.utc(2005, 12, 10, 15, 30)..Time.utc(2005, 12, 10, 17, 30) time_range_2 = Time.utc(2005, 12, 10, 17, 31)..Time.utc(2005, 12, 10, 18, 00) - assert !time_range_1.overlaps?(time_range_2) + assert_not time_range_1.overlaps?(time_range_2) end def test_each_on_time_with_zone diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index ccaa5dc786..b8de16cc5e 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -237,11 +237,11 @@ class StringInflectionsTest < ActiveSupport::TestCase s = "hello" assert s.starts_with?("h") assert s.starts_with?("hel") - assert !s.starts_with?("el") + assert_not s.starts_with?("el") assert s.ends_with?("o") assert s.ends_with?("lo") - assert !s.ends_with?("el") + assert_not s.ends_with?("el") end def test_string_squish diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index 2ea5f0921c..e650209268 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -289,6 +289,20 @@ class TimeWithZoneTest < ActiveSupport::TestCase end end + def test_before + twz = ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 0, 0), @time_zone) + assert_equal false, twz.before?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 11, 59, 59), @time_zone)) + assert_equal false, twz.before?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 0, 0), @time_zone)) + assert_equal true, twz.before?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 00, 1), @time_zone)) + end + + def test_after + twz = ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 0, 0), @time_zone) + assert_equal true, twz.after?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 11, 59, 59), @time_zone)) + assert_equal false, twz.after?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 0, 0), @time_zone)) + assert_equal false, twz.after?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 00, 1), @time_zone)) + end + def test_eql? assert_equal true, @twz.eql?(@twz.dup) assert_equal true, @twz.eql?(Time.utc(2000)) diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index c0c4c4cac5..a4fbab7b55 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -223,7 +223,7 @@ class DependenciesTest < ActiveSupport::TestCase Timeout.timeout(0.1) do # Remove the constant, as if Rails development middleware is reloading changed files: ActiveSupport::Dependencies.remove_unloadable_constants! - refute defined?(AnotherConstant::ReloadError) + assert_not defined?(AnotherConstant::ReloadError) end # Change the file, so that it is **correct** this time: @@ -231,7 +231,7 @@ class DependenciesTest < ActiveSupport::TestCase # Again: Remove the constant, as if Rails development middleware is reloading changed files: ActiveSupport::Dependencies.remove_unloadable_constants! - refute defined?(AnotherConstant::ReloadError) + assert_not defined?(AnotherConstant::ReloadError) # Now, reload the _fixed_ constant: assert ConstantReloadError @@ -535,9 +535,9 @@ class DependenciesTest < ActiveSupport::TestCase def test_qualified_const_defined_should_not_call_const_missing ModuleWithMissing.missing_count = 0 - assert ! ActiveSupport::Dependencies.qualified_const_defined?("ModuleWithMissing::A") + assert_not ActiveSupport::Dependencies.qualified_const_defined?("ModuleWithMissing::A") assert_equal 0, ModuleWithMissing.missing_count - assert ! ActiveSupport::Dependencies.qualified_const_defined?("ModuleWithMissing::A::B") + assert_not ActiveSupport::Dependencies.qualified_const_defined?("ModuleWithMissing::A::B") assert_equal 0, ModuleWithMissing.missing_count end @@ -547,13 +547,13 @@ class DependenciesTest < ActiveSupport::TestCase def test_autoloaded? with_autoloading_fixtures do - assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder") - assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass") + assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder") + assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass") assert ActiveSupport::Dependencies.autoloaded?(ModuleFolder) assert ActiveSupport::Dependencies.autoloaded?("ModuleFolder") - assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass") + assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass") assert ActiveSupport::Dependencies.autoloaded?(ModuleFolder::NestedClass) @@ -564,11 +564,11 @@ class DependenciesTest < ActiveSupport::TestCase assert ActiveSupport::Dependencies.autoloaded?(:ModuleFolder) # Anonymous modules aren't autoloaded. - assert !ActiveSupport::Dependencies.autoloaded?(Module.new) + assert_not ActiveSupport::Dependencies.autoloaded?(Module.new) nil_name = Module.new def nil_name.name() nil end - assert !ActiveSupport::Dependencies.autoloaded?(nil_name) + assert_not ActiveSupport::Dependencies.autoloaded?(nil_name) end ensure remove_constants(:ModuleFolder) @@ -778,7 +778,7 @@ class DependenciesTest < ActiveSupport::TestCase M.unloadable ActiveSupport::Dependencies.clear - assert ! defined?(M) + assert_not defined?(M) Object.const_set :M, Module.new ActiveSupport::Dependencies.clear @@ -809,7 +809,7 @@ class DependenciesTest < ActiveSupport::TestCase assert_called(C, :before_remove_const, times: 1) do assert_respond_to C, :before_remove_const ActiveSupport::Dependencies.clear - assert !defined?(C) + assert_not defined?(C) end ensure remove_constants(:C) @@ -1023,7 +1023,7 @@ class DependenciesTest < ActiveSupport::TestCase assert !defined?(::RaisesNameError), "::RaisesNameError is defined but it should have failed!" end - assert !defined?(::RaisesNameError) + assert_not defined?(::RaisesNameError) 2.times do assert_raise(NameError) { ::RaisesNameError } assert !defined?(::RaisesNameError), "::RaisesNameError is defined but it should have failed!" diff --git a/activesupport/test/deprecation/proxy_wrappers_test.rb b/activesupport/test/deprecation/proxy_wrappers_test.rb index 2f866775f6..9e26052fb4 100644 --- a/activesupport/test/deprecation/proxy_wrappers_test.rb +++ b/activesupport/test/deprecation/proxy_wrappers_test.rb @@ -9,16 +9,16 @@ class ProxyWrappersTest < ActiveSupport::TestCase def test_deprecated_object_proxy_doesnt_wrap_falsy_objects proxy = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(nil, "message") - assert !proxy + assert_not proxy end def test_deprecated_instance_variable_proxy_doesnt_wrap_falsy_objects proxy = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(nil, :waffles) - assert !proxy + assert_not proxy end def test_deprecated_constant_proxy_doesnt_wrap_falsy_objects proxy = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(Waffles, NewWaffles) - assert !proxy + assert_not proxy end end diff --git a/activesupport/test/descendants_tracker_without_autoloading_test.rb b/activesupport/test/descendants_tracker_without_autoloading_test.rb index f5c6a3045d..c65f69cba3 100644 --- a/activesupport/test/descendants_tracker_without_autoloading_test.rb +++ b/activesupport/test/descendants_tracker_without_autoloading_test.rb @@ -13,7 +13,7 @@ class DescendantsTrackerWithoutAutoloadingTest < ActiveSupport::TestCase parent_instance = Parent.new parent_instance.singleton_class.descendants ActiveSupport::DescendantsTracker.clear - assert !ActiveSupport::DescendantsTracker.class_variable_get(:@@direct_descendants).key?(parent_instance.singleton_class) + assert_not ActiveSupport::DescendantsTracker.class_variable_get(:@@direct_descendants).key?(parent_instance.singleton_class) end end end diff --git a/activesupport/test/file_update_checker_shared_tests.rb b/activesupport/test/file_update_checker_shared_tests.rb index daf7f9d139..72683816b3 100644 --- a/activesupport/test/file_update_checker_shared_tests.rb +++ b/activesupport/test/file_update_checker_shared_tests.rb @@ -30,7 +30,7 @@ module FileUpdateCheckerSharedTests checker = new_checker { i += 1 } - assert !checker.execute_if_updated + assert_not checker.execute_if_updated assert_equal 0, i end @@ -41,7 +41,7 @@ module FileUpdateCheckerSharedTests checker = new_checker(tmpfiles) { i += 1 } - assert !checker.execute_if_updated + assert_not checker.execute_if_updated assert_equal 0, i end @@ -212,7 +212,7 @@ module FileUpdateCheckerSharedTests touch(tmpfile("foo.rb")) wait - assert !checker.execute_if_updated + assert_not checker.execute_if_updated assert_equal 0, i end @@ -238,7 +238,7 @@ module FileUpdateCheckerSharedTests mkdir(subdir) wait - assert !checker.execute_if_updated + assert_not checker.execute_if_updated assert_equal 0, i touch(File.join(subdir, "nested.rb")) @@ -259,7 +259,7 @@ module FileUpdateCheckerSharedTests touch(tmpfile("new.txt")) wait - assert !checker.execute_if_updated + assert_not checker.execute_if_updated assert_equal 0, i # subdir does not look for Ruby files, but its parent tmpdir does. diff --git a/activesupport/test/hash_with_indifferent_access_test.rb b/activesupport/test/hash_with_indifferent_access_test.rb index b06250baf8..a20c428bf8 100644 --- a/activesupport/test/hash_with_indifferent_access_test.rb +++ b/activesupport/test/hash_with_indifferent_access_test.rb @@ -280,7 +280,7 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase replaced = hash.replace(b: 12) assert hash.key?("b") - assert !hash.key?(:a) + assert_not hash.key?(:a) assert_equal 12, hash[:b] assert_same hash, replaced end @@ -292,7 +292,7 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase replaced = hash.replace(HashByConversion.new(b: 12)) assert hash.key?("b") - assert !hash.key?(:a) + assert_not hash.key?(:a) assert_equal 12, hash[:b] assert_same hash, replaced end diff --git a/activesupport/test/message_verifier_test.rb b/activesupport/test/message_verifier_test.rb index 05d5c1cbc3..0fa53695e0 100644 --- a/activesupport/test/message_verifier_test.rb +++ b/activesupport/test/message_verifier_test.rb @@ -25,12 +25,12 @@ class MessageVerifierTest < ActiveSupport::TestCase def test_valid_message data, hash = @verifier.generate(@data).split("--") - assert !@verifier.valid_message?(nil) - assert !@verifier.valid_message?("") - assert !@verifier.valid_message?("\xff") # invalid encoding - assert !@verifier.valid_message?("#{data.reverse}--#{hash}") - assert !@verifier.valid_message?("#{data}--#{hash.reverse}") - assert !@verifier.valid_message?("purejunk") + assert_not @verifier.valid_message?(nil) + assert_not @verifier.valid_message?("") + assert_not @verifier.valid_message?("\xff") # invalid encoding + assert_not @verifier.valid_message?("#{data.reverse}--#{hash}") + assert_not @verifier.valid_message?("#{data}--#{hash.reverse}") + assert_not @verifier.valid_message?("purejunk") end def test_simple_round_tripping @@ -40,7 +40,7 @@ class MessageVerifierTest < ActiveSupport::TestCase end def test_verified_returns_false_on_invalid_message - assert !@verifier.verified("purejunk") + assert_not @verifier.verified("purejunk") end def test_verify_exception_on_invalid_message diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index 560b86b1a3..061446c782 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -75,7 +75,7 @@ class MultibyteCharsTest < ActiveSupport::TestCase def test_consumes_utf8_strings assert @proxy_class.consumes?(UNICODE_STRING) assert @proxy_class.consumes?(ASCII_STRING) - assert !@proxy_class.consumes?(BYTE_STRING) + assert_not @proxy_class.consumes?(BYTE_STRING) end def test_concatenation_should_return_a_proxy_class_instance @@ -148,7 +148,7 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase def test_identity assert_equal @chars, @chars assert @chars.eql?(@chars) - assert !@chars.eql?(UNICODE_STRING) + assert_not @chars.eql?(UNICODE_STRING) end def test_string_methods_are_chainable diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb index 28a02aafc8..d035f993f7 100644 --- a/activesupport/test/notifications_test.rb +++ b/activesupport/test/notifications_test.rb @@ -270,9 +270,9 @@ module Notifications parent.children << child assert parent.parent_of?(child) - assert !child.parent_of?(parent) - assert !parent.parent_of?(not_child) - assert !not_child.parent_of?(parent) + assert_not child.parent_of?(parent) + assert_not parent.parent_of?(not_child) + assert_not not_child.parent_of?(parent) end private diff --git a/activesupport/test/ordered_options_test.rb b/activesupport/test/ordered_options_test.rb index cba5b8a8de..90394fee0a 100644 --- a/activesupport/test/ordered_options_test.rb +++ b/activesupport/test/ordered_options_test.rb @@ -15,7 +15,7 @@ class OrderedOptionsTest < ActiveSupport::TestCase a[:allow_concurrency] = false assert_equal 1, a.size - assert !a[:allow_concurrency] + assert_not a[:allow_concurrency] a["else_where"] = 56 assert_equal 2, a.size @@ -47,7 +47,7 @@ class OrderedOptionsTest < ActiveSupport::TestCase a.allow_concurrency = false assert_equal 1, a.size - assert !a.allow_concurrency + assert_not a.allow_concurrency a.else_where = 56 assert_equal 2, a.size diff --git a/activesupport/test/reloader_test.rb b/activesupport/test/reloader_test.rb index 3e4229eaf7..976917c1a1 100644 --- a/activesupport/test/reloader_test.rb +++ b/activesupport/test/reloader_test.rb @@ -8,18 +8,18 @@ class ReloaderTest < ActiveSupport::TestCase reloader.to_prepare { prepared = true } reloader.to_complete { completed = true } - assert !prepared - assert !completed + assert_not prepared + assert_not completed reloader.prepare! assert prepared - assert !completed + assert_not completed prepared = false reloader.wrap do assert prepared prepared = false end - assert !prepared + assert_not prepared end def test_prepend_prepare_callback @@ -42,7 +42,7 @@ class ReloaderTest < ActiveSupport::TestCase invoked = false r.to_run { invoked = true } r.wrap {} - assert !invoked + assert_not invoked end def test_full_reload_sequence diff --git a/activesupport/test/security_utils_test.rb b/activesupport/test/security_utils_test.rb index 0a607594a2..fff9cc2a8d 100644 --- a/activesupport/test/security_utils_test.rb +++ b/activesupport/test/security_utils_test.rb @@ -11,7 +11,7 @@ class SecurityUtilsTest < ActiveSupport::TestCase def test_fixed_length_secure_compare_should_perform_string_comparison assert ActiveSupport::SecurityUtils.fixed_length_secure_compare("a", "a") - assert !ActiveSupport::SecurityUtils.fixed_length_secure_compare("a", "b") + assert_not ActiveSupport::SecurityUtils.fixed_length_secure_compare("a", "b") end def test_fixed_length_secure_compare_raise_on_length_mismatch diff --git a/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb index eced622137..8a1ecb6b33 100644 --- a/activesupport/test/test_case_test.rb +++ b/activesupport/test/test_case_test.rb @@ -2,7 +2,7 @@ require "abstract_unit" -class AssertDifferenceTest < ActiveSupport::TestCase +class AssertionsTest < ActiveSupport::TestCase def setup @object = Class.new do attr_accessor :num diff --git a/activesupport/test/testing/after_teardown_test.rb b/activesupport/test/testing/after_teardown_test.rb new file mode 100644 index 0000000000..68c368909c --- /dev/null +++ b/activesupport/test/testing/after_teardown_test.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module OtherAfterTeardown + def after_teardown + @witness = true + end +end + +class AfterTeardownTest < Minitest::Test + include OtherAfterTeardown + include ActiveSupport::Testing::SetupAndTeardown + + attr_writer :witness + + MyError = Class.new(StandardError) + + teardown do + raise MyError, "Test raises an error, all after_teardown should still get called" + end + + def after_teardown + assert_raises MyError do + super + end + + assert_equal true, @witness + end + + def test_teardown_raise_but_all_after_teardown_method_are_called + assert true + end +end diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index 63ca22efb5..b59f3e9405 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -725,6 +725,21 @@ class TimeZoneTest < ActiveSupport::TestCase assert_not_includes all_zones, galapagos end + def test_all_doesnt_raise_exception_with_missing_tzinfo_data + mappings = { + "Puerto Rico" => "America/Unknown", + "Pittsburgh" => "America/New_York" + } + + with_tz_mappings(mappings) do + assert_nil ActiveSupport::TimeZone["Puerto Rico"] + assert_nil ActiveSupport::TimeZone[-9] + assert_nothing_raised do + ActiveSupport::TimeZone.all + end + end + end + def test_index assert_nil ActiveSupport::TimeZone["bogus"] assert_instance_of ActiveSupport::TimeZone, ActiveSupport::TimeZone["Central Time (US & Canada)"] diff --git a/activesupport/test/time_zone_test_helpers.rb b/activesupport/test/time_zone_test_helpers.rb index 051703a781..85ed727c9b 100644 --- a/activesupport/test/time_zone_test_helpers.rb +++ b/activesupport/test/time_zone_test_helpers.rb @@ -23,4 +23,17 @@ module TimeZoneTestHelpers ensure ActiveSupport.to_time_preserves_timezone = old_preserve_tz end + + def with_tz_mappings(mappings) + old_mappings = ActiveSupport::TimeZone::MAPPING.dup + ActiveSupport::TimeZone.clear + ActiveSupport::TimeZone::MAPPING.clear + ActiveSupport::TimeZone::MAPPING.merge!(mappings) + + yield + ensure + ActiveSupport::TimeZone.clear + ActiveSupport::TimeZone::MAPPING.clear + ActiveSupport::TimeZone::MAPPING.merge!(old_mappings) + end end diff --git a/ci/custom_cops/bin/test b/ci/custom_cops/bin/test new file mode 100755 index 0000000000..495ffec83a --- /dev/null +++ b/ci/custom_cops/bin/test @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +COMPONENT_ROOT = File.expand_path("..", __dir__) +require_relative "../../../tools/test" diff --git a/ci/custom_cops/lib/custom_cops.rb b/ci/custom_cops/lib/custom_cops.rb new file mode 100644 index 0000000000..157b8247e4 --- /dev/null +++ b/ci/custom_cops/lib/custom_cops.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +require_relative "custom_cops/refute_not" +require_relative "custom_cops/assert_not" diff --git a/ci/custom_cops/lib/custom_cops/assert_not.rb b/ci/custom_cops/lib/custom_cops/assert_not.rb new file mode 100644 index 0000000000..e722448e21 --- /dev/null +++ b/ci/custom_cops/lib/custom_cops/assert_not.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module CustomCops + # Enforces the use of `assert_not` over `assert !`. + # + # @example + # # bad + # assert !x + # assert ! x + # + # # good + # assert_not x + # + class AssertNot < RuboCop::Cop::Cop + MSG = "Prefer `assert_not` over `assert !`" + + def_node_matcher :offensive?, "(send nil? :assert (send ... :!))" + + def on_send(node) + add_offense(node) if offensive?(node) + end + + def autocorrect(node) + expression = node.loc.expression + + ->(corrector) do + corrector.replace( + expression, + corrected_source(expression.source) + ) + end + end + + private + + def corrected_source(source) + source.gsub(/^assert(\(| ) *! */, "assert_not\\1") + end + end +end diff --git a/ci/custom_cops/lib/custom_cops/refute_not.rb b/ci/custom_cops/lib/custom_cops/refute_not.rb new file mode 100644 index 0000000000..3e89e0fd32 --- /dev/null +++ b/ci/custom_cops/lib/custom_cops/refute_not.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module CustomCops + # Enforces the use of `#assert_not` methods over `#refute` methods. + # + # @example + # # bad + # refute false + # refute_empty [1, 2, 3] + # refute_equal true, false + # + # # good + # assert_not false + # assert_not_empty [1, 2, 3] + # assert_not_equal true, false + # + class RefuteNot < RuboCop::Cop::Cop + MSG = "Prefer `%<assert_method>s` over `%<refute_method>s`" + + CORRECTIONS = { + refute: "assert_not", + refute_empty: "assert_not_empty", + refute_equal: "assert_not_equal", + refute_in_delta: "assert_not_in_delta", + refute_in_epsilon: "assert_not_in_epsilon", + refute_includes: "assert_not_includes", + refute_instance_of: "assert_not_instance_of", + refute_kind_of: "assert_not_kind_of", + refute_nil: "assert_not_nil", + refute_operator: "assert_not_operator", + refute_predicate: "assert_not_predicate", + refute_respond_to: "assert_not_respond_to", + refute_same: "assert_not_same", + refute_match: "assert_no_match" + }.freeze + + OFFENSIVE_METHODS = CORRECTIONS.keys.freeze + + def_node_matcher :offensive?, "(send nil? #offensive_method? ...)" + + def on_send(node) + return unless offensive?(node) + + message = offense_message(node.method_name) + add_offense(node, location: :selector, message: message) + end + + def autocorrect(node) + ->(corrector) do + corrector.replace( + node.loc.selector, + CORRECTIONS[node.method_name] + ) + end + end + + private + + def offensive_method?(method_name) + OFFENSIVE_METHODS.include?(method_name) + end + + def offense_message(method_name) + format( + MSG, + refute_method: method_name, + assert_method: CORRECTIONS[method_name] + ) + end + end +end diff --git a/ci/custom_cops/test/custom_cops/assert_not_test.rb b/ci/custom_cops/test/custom_cops/assert_not_test.rb new file mode 100644 index 0000000000..abb151aeb4 --- /dev/null +++ b/ci/custom_cops/test/custom_cops/assert_not_test.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "support/cop_helper" +require_relative "../../lib/custom_cops/assert_not" + +class AssertNotTest < ActiveSupport::TestCase + include CopHelper + + setup do + @cop = CustomCops::AssertNot.new + end + + test "rejects 'assert !'" do + inspect_source @cop, "assert !x" + assert_offense @cop, "^^^^^^^^^ Prefer `assert_not` over `assert !`" + end + + test "rejects 'assert !' with a complex value" do + inspect_source @cop, "assert !a.b(c)" + assert_offense @cop, "^^^^^^^^^^^^^^ Prefer `assert_not` over `assert !`" + end + + test "autocorrects `assert !`" do + corrected = autocorrect_source(@cop, "assert !false") + assert_equal "assert_not false", corrected + end + + test "autocorrects `assert !` with extra spaces" do + corrected = autocorrect_source(@cop, "assert ! false") + assert_equal "assert_not false", corrected + end + + test "autocorrects `assert !` with parentheses" do + corrected = autocorrect_source(@cop, "assert(!false)") + assert_equal "assert_not(false)", corrected + end + + test "accepts `assert_not`" do + inspect_source @cop, "assert_not x" + assert_empty @cop.offenses + end +end diff --git a/ci/custom_cops/test/custom_cops/refute_not_test.rb b/ci/custom_cops/test/custom_cops/refute_not_test.rb new file mode 100644 index 0000000000..f0f6eaeda0 --- /dev/null +++ b/ci/custom_cops/test/custom_cops/refute_not_test.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require "support/cop_helper" +require_relative "../../lib/custom_cops/refute_not" + +class RefuteNotTest < ActiveSupport::TestCase + include CopHelper + + setup do + @cop = CustomCops::RefuteNot.new + end + + { + refute: :assert_not, + refute_empty: :assert_not_empty, + refute_equal: :assert_not_equal, + refute_in_delta: :assert_not_in_delta, + refute_in_epsilon: :assert_not_in_epsilon, + refute_includes: :assert_not_includes, + refute_instance_of: :assert_not_instance_of, + refute_kind_of: :assert_not_kind_of, + refute_nil: :assert_not_nil, + refute_operator: :assert_not_operator, + refute_predicate: :assert_not_predicate, + refute_respond_to: :assert_not_respond_to, + refute_same: :assert_not_same, + refute_match: :assert_no_match + }.each do |refute_method, assert_method| + test "rejects `#{refute_method}` with a single argument" do + inspect_source(@cop, "#{refute_method} a") + assert_offense @cop, offense_message(refute_method, assert_method) + end + + test "rejects `#{refute_method}` with multiple arguments" do + inspect_source(@cop, "#{refute_method} a, b, c") + assert_offense @cop, offense_message(refute_method, assert_method) + end + + test "autocorrects `#{refute_method}` with a single argument" do + corrected = autocorrect_source(@cop, "#{refute_method} a") + assert_equal "#{assert_method} a", corrected + end + + test "autocorrects `#{refute_method}` with multiple arguments" do + corrected = autocorrect_source(@cop, "#{refute_method} a, b, c") + assert_equal "#{assert_method} a, b, c", corrected + end + + test "accepts `#{assert_method}` with a single argument" do + inspect_source(@cop, "#{assert_method} a") + assert_empty @cop.offenses + end + + test "accepts `#{assert_method}` with multiple arguments" do + inspect_source(@cop, "#{assert_method} a, b, c") + assert_empty @cop.offenses + end + end + + private + + def offense_message(refute_method, assert_method) + carets = "^" * refute_method.to_s.length + "#{carets} Prefer `#{assert_method}` over `#{refute_method}`" + end +end diff --git a/ci/custom_cops/test/support/cop_helper.rb b/ci/custom_cops/test/support/cop_helper.rb new file mode 100644 index 0000000000..c2c6b969dd --- /dev/null +++ b/ci/custom_cops/test/support/cop_helper.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "rubocop" + +module CopHelper + def inspect_source(cop, source) + processed_source = parse_source(source) + raise "Error parsing example code" unless processed_source.valid_syntax? + investigate(cop, processed_source) + processed_source + end + + def autocorrect_source(cop, source) + cop.instance_variable_get(:@options)[:auto_correct] = true + processed_source = inspect_source(cop, source) + rewrite(cop, processed_source) + end + + def assert_offense(cop, expected_message) + assert_not_empty( + cop.offenses, + "Expected offense with message \"#{expected_message}\", but got no offense" + ) + + offense = cop.offenses.first + carets = "^" * offense.column_length + + assert_equal expected_message, "#{carets} #{offense.message}" + end + + private + TARGET_RUBY_VERSION = 2.4 + + def parse_source(source) + RuboCop::ProcessedSource.new(source, TARGET_RUBY_VERSION) + end + + def rewrite(cop, processed_source) + RuboCop::Cop::Corrector.new(processed_source.buffer, cop.corrections) + .rewrite + end + + def investigate(cop, processed_source) + RuboCop::Cop::Commissioner.new([cop], [], raise_error: true) + .investigate(processed_source) + end +end diff --git a/guides/assets/images/rails4_features.png b/guides/assets/images/4_0_release_notes/rails4_features.png Binary files differindex ac73f05cf7..ac73f05cf7 100644 --- a/guides/assets/images/rails4_features.png +++ b/guides/assets/images/4_0_release_notes/rails4_features.png diff --git a/guides/assets/images/akshaysurve.jpg b/guides/assets/images/akshaysurve.jpg Binary files differdeleted file mode 100644 index cfc3333958..0000000000 --- a/guides/assets/images/akshaysurve.jpg +++ /dev/null diff --git a/guides/assets/images/belongs_to.png b/guides/assets/images/association_basics/belongs_to.png Binary files differindex 2b8c1d52ea..2b8c1d52ea 100644 --- a/guides/assets/images/belongs_to.png +++ b/guides/assets/images/association_basics/belongs_to.png diff --git a/guides/assets/images/habtm.png b/guides/assets/images/association_basics/habtm.png Binary files differindex 7e508cc1a6..7e508cc1a6 100644 --- a/guides/assets/images/habtm.png +++ b/guides/assets/images/association_basics/habtm.png diff --git a/guides/assets/images/has_many.png b/guides/assets/images/association_basics/has_many.png Binary files differindex 36ccf9f0f6..36ccf9f0f6 100644 --- a/guides/assets/images/has_many.png +++ b/guides/assets/images/association_basics/has_many.png diff --git a/guides/assets/images/has_many_through.png b/guides/assets/images/association_basics/has_many_through.png Binary files differindex 9e9caabd73..9e9caabd73 100644 --- a/guides/assets/images/has_many_through.png +++ b/guides/assets/images/association_basics/has_many_through.png diff --git a/guides/assets/images/has_one.png b/guides/assets/images/association_basics/has_one.png Binary files differindex c29c6b9c59..c29c6b9c59 100644 --- a/guides/assets/images/has_one.png +++ b/guides/assets/images/association_basics/has_one.png diff --git a/guides/assets/images/has_one_through.png b/guides/assets/images/association_basics/has_one_through.png Binary files differindex fdf13286c4..fdf13286c4 100644 --- a/guides/assets/images/has_one_through.png +++ b/guides/assets/images/association_basics/has_one_through.png diff --git a/guides/assets/images/polymorphic.png b/guides/assets/images/association_basics/polymorphic.png Binary files differindex d630db9e01..d630db9e01 100644 --- a/guides/assets/images/polymorphic.png +++ b/guides/assets/images/association_basics/polymorphic.png diff --git a/guides/assets/images/credits_pic_blank.gif b/guides/assets/images/credits_pic_blank.gif Binary files differdeleted file mode 100644 index a6b335d0c9..0000000000 --- a/guides/assets/images/credits_pic_blank.gif +++ /dev/null diff --git a/guides/assets/images/fxn.png b/guides/assets/images/fxn.png Binary files differdeleted file mode 100644 index 733d380cba..0000000000 --- a/guides/assets/images/fxn.png +++ /dev/null diff --git a/guides/assets/images/getting_started/routing_error_no_route_matches.png b/guides/assets/images/getting_started/routing_error_no_route_matches.png Binary files differdeleted file mode 100644 index 08c54f921f..0000000000 --- a/guides/assets/images/getting_started/routing_error_no_route_matches.png +++ /dev/null diff --git a/guides/assets/images/header_backdrop.png b/guides/assets/images/header_backdrop.png Binary files differdeleted file mode 100644 index 81f4d91774..0000000000 --- a/guides/assets/images/header_backdrop.png +++ /dev/null diff --git a/guides/assets/images/icons/README b/guides/assets/images/icons/README deleted file mode 100644 index 09da77fc86..0000000000 --- a/guides/assets/images/icons/README +++ /dev/null @@ -1,5 +0,0 @@ -Replaced the plain DocBook XSL admonition icons with Jimmac's DocBook -icons (http://jimmac.musichall.cz/ikony.php3). I dropped transparency -from the Jimmac icons to get round MS IE and FOP PNG incompatibilities. - -Stuart Rackham diff --git a/guides/assets/images/icons/callouts/1.png b/guides/assets/images/icons/callouts/1.png Binary files differdeleted file mode 100644 index c5d02adcf4..0000000000 --- a/guides/assets/images/icons/callouts/1.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/10.png b/guides/assets/images/icons/callouts/10.png Binary files differdeleted file mode 100644 index fe89f9ef83..0000000000 --- a/guides/assets/images/icons/callouts/10.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/11.png b/guides/assets/images/icons/callouts/11.png Binary files differdeleted file mode 100644 index 3b7b9318e7..0000000000 --- a/guides/assets/images/icons/callouts/11.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/12.png b/guides/assets/images/icons/callouts/12.png Binary files differdeleted file mode 100644 index 7b95925e9d..0000000000 --- a/guides/assets/images/icons/callouts/12.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/13.png b/guides/assets/images/icons/callouts/13.png Binary files differdeleted file mode 100644 index 4b99fe8efc..0000000000 --- a/guides/assets/images/icons/callouts/13.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/14.png b/guides/assets/images/icons/callouts/14.png Binary files differdeleted file mode 100644 index dbde9ca749..0000000000 --- a/guides/assets/images/icons/callouts/14.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/15.png b/guides/assets/images/icons/callouts/15.png Binary files differdeleted file mode 100644 index 70e4bba615..0000000000 --- a/guides/assets/images/icons/callouts/15.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/2.png b/guides/assets/images/icons/callouts/2.png Binary files differdeleted file mode 100644 index 8c57970ba9..0000000000 --- a/guides/assets/images/icons/callouts/2.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/3.png b/guides/assets/images/icons/callouts/3.png Binary files differdeleted file mode 100644 index 57a33d15b4..0000000000 --- a/guides/assets/images/icons/callouts/3.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/4.png b/guides/assets/images/icons/callouts/4.png Binary files differdeleted file mode 100644 index f061ab02b8..0000000000 --- a/guides/assets/images/icons/callouts/4.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/5.png b/guides/assets/images/icons/callouts/5.png Binary files differdeleted file mode 100644 index b4de02da11..0000000000 --- a/guides/assets/images/icons/callouts/5.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/6.png b/guides/assets/images/icons/callouts/6.png Binary files differdeleted file mode 100644 index 0e055eec1e..0000000000 --- a/guides/assets/images/icons/callouts/6.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/7.png b/guides/assets/images/icons/callouts/7.png Binary files differdeleted file mode 100644 index 5ead87d040..0000000000 --- a/guides/assets/images/icons/callouts/7.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/8.png b/guides/assets/images/icons/callouts/8.png Binary files differdeleted file mode 100644 index cb99545eb6..0000000000 --- a/guides/assets/images/icons/callouts/8.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/9.png b/guides/assets/images/icons/callouts/9.png Binary files differdeleted file mode 100644 index 0ac03602f6..0000000000 --- a/guides/assets/images/icons/callouts/9.png +++ /dev/null diff --git a/guides/assets/images/icons/caution.png b/guides/assets/images/icons/caution.png Binary files differdeleted file mode 100644 index 7227b54b32..0000000000 --- a/guides/assets/images/icons/caution.png +++ /dev/null diff --git a/guides/assets/images/icons/example.png b/guides/assets/images/icons/example.png Binary files differdeleted file mode 100644 index a0e855befa..0000000000 --- a/guides/assets/images/icons/example.png +++ /dev/null diff --git a/guides/assets/images/icons/home.png b/guides/assets/images/icons/home.png Binary files differdeleted file mode 100644 index e70e164522..0000000000 --- a/guides/assets/images/icons/home.png +++ /dev/null diff --git a/guides/assets/images/icons/important.png b/guides/assets/images/icons/important.png Binary files differdeleted file mode 100644 index bab53bf3aa..0000000000 --- a/guides/assets/images/icons/important.png +++ /dev/null diff --git a/guides/assets/images/icons/next.png b/guides/assets/images/icons/next.png Binary files differdeleted file mode 100644 index a158832725..0000000000 --- a/guides/assets/images/icons/next.png +++ /dev/null diff --git a/guides/assets/images/icons/note.png b/guides/assets/images/icons/note.png Binary files differdeleted file mode 100644 index 62eec7845f..0000000000 --- a/guides/assets/images/icons/note.png +++ /dev/null diff --git a/guides/assets/images/icons/prev.png b/guides/assets/images/icons/prev.png Binary files differdeleted file mode 100644 index 8a96960422..0000000000 --- a/guides/assets/images/icons/prev.png +++ /dev/null diff --git a/guides/assets/images/icons/tip.png b/guides/assets/images/icons/tip.png Binary files differdeleted file mode 100644 index a5316d318f..0000000000 --- a/guides/assets/images/icons/tip.png +++ /dev/null diff --git a/guides/assets/images/icons/up.png b/guides/assets/images/icons/up.png Binary files differdeleted file mode 100644 index 6cac818170..0000000000 --- a/guides/assets/images/icons/up.png +++ /dev/null diff --git a/guides/assets/images/icons/warning.png b/guides/assets/images/icons/warning.png Binary files differdeleted file mode 100644 index 72a8a5d873..0000000000 --- a/guides/assets/images/icons/warning.png +++ /dev/null diff --git a/guides/assets/images/oscardelben.jpg b/guides/assets/images/oscardelben.jpg Binary files differdeleted file mode 100644 index 9f3f67c2c7..0000000000 --- a/guides/assets/images/oscardelben.jpg +++ /dev/null diff --git a/guides/assets/images/radar.png b/guides/assets/images/radar.png Binary files differdeleted file mode 100644 index 421b62b623..0000000000 --- a/guides/assets/images/radar.png +++ /dev/null diff --git a/guides/assets/images/rails_logo_remix.gif b/guides/assets/images/rails_logo_remix.gif Binary files differdeleted file mode 100644 index 58960ee4f9..0000000000 --- a/guides/assets/images/rails_logo_remix.gif +++ /dev/null diff --git a/guides/assets/images/csrf.png b/guides/assets/images/security/csrf.png Binary files differindex a8123d47c3..a8123d47c3 100644 --- a/guides/assets/images/csrf.png +++ b/guides/assets/images/security/csrf.png diff --git a/guides/assets/images/session_fixation.png b/guides/assets/images/security/session_fixation.png Binary files differindex e009484f09..e009484f09 100644 --- a/guides/assets/images/session_fixation.png +++ b/guides/assets/images/security/session_fixation.png diff --git a/guides/assets/images/tab_yellow.png b/guides/assets/images/tab_yellow.png Binary files differdeleted file mode 100644 index 053c807d28..0000000000 --- a/guides/assets/images/tab_yellow.png +++ /dev/null diff --git a/guides/assets/images/vijaydev.jpg b/guides/assets/images/vijaydev.jpg Binary files differdeleted file mode 100644 index fe5e4f1cb4..0000000000 --- a/guides/assets/images/vijaydev.jpg +++ /dev/null diff --git a/guides/assets/stylesheets/main.css b/guides/assets/stylesheets/main.css index b27776745a..a3ab745234 100644 --- a/guides/assets/stylesheets/main.css +++ b/guides/assets/stylesheets/main.css @@ -70,7 +70,7 @@ table { } table th, table td { - padding: 0.25em 1em; + padding: 9px 10px; border: 1px solid #CCC; border-collapse: collapse; } @@ -79,7 +79,6 @@ table th { border-bottom: 2px solid #CCC; background: #EEE; font-weight: bold; - padding: 0.5em 1em; } img { @@ -265,8 +264,6 @@ body { } } -#extraCol {display: none;} - #footer { padding: 2em 0; background: #222 url(../images/footer_tile.gif) repeat-x; @@ -555,8 +552,6 @@ h6 { font-size: 1.2857em; padding: 0.125em 0 0.25em 0; margin-bottom: 0; - /*background: url(../images/book_icon.gif) no-repeat left top; - padding: 0.125em 0 0.25em 28px;*/ } @media screen and (max-width: 480px) { @@ -665,10 +660,8 @@ div.code_container { visibility: hidden; } -.clearfix {display: inline-block;} * html .clearfix {height: 1%;} .clearfix {display: block;} -.clear { clear:both; } /* Same bottom margin for special boxes than for regular paragraphs, this way intermediate whitespace looks uniform. */ @@ -696,9 +689,6 @@ div.important p, div.caution p, div.warning p, div.note p, div.info p { /* Foundation v2.1.4 http://foundation.zurb.com */ /* Artfully masterminded by ZURB */ -table th { font-weight: bold; } -table td, table th { padding: 9px 10px; text-align: left; } - /* Mobile */ @media only screen and (max-width: 767px) { table.responsive { margin-bottom: 0; } diff --git a/guides/assets/stylesheets/print.css b/guides/assets/stylesheets/print.css index bdc8ec948d..6280422469 100644 --- a/guides/assets/stylesheets/print.css +++ b/guides/assets/stylesheets/print.css @@ -4,7 +4,7 @@ /* Modified January 31, 2009 --------------------------------------- */ -body, .wrapper, .note, .info, code, #topNav, .L, .R, #frame, #container, #header, #navigation, #footer, #feature, #mainCol, #subCol, #extraCol, .content {position: static; text-align: left; text-indent: 0; background: White; color: Black; border-color: Black; width: auto; height: auto; display: block; float: none; min-height: 0; margin: 0; padding: 0;} +body, .wrapper, .note, .info, code, #topNav, .L, .R, #frame, #container, #header, #navigation, #footer, #feature, #mainCol, #subCol, .content {position: static; text-align: left; text-indent: 0; background: White; color: Black; border-color: Black; width: auto; height: auto; display: block; float: none; min-height: 0; margin: 0; padding: 0;} body { background: #FFF; diff --git a/guides/assets/stylesheets/responsive-tables.css b/guides/assets/stylesheets/responsive-tables.css deleted file mode 100644 index f5fbcbf948..0000000000 --- a/guides/assets/stylesheets/responsive-tables.css +++ /dev/null @@ -1,50 +0,0 @@ -/* Foundation v2.1.4 http://foundation.zurb.com */ -/* Artfully masterminded by ZURB */ - -/* -------------------------------------------------- - Table of Contents ------------------------------------------------------ -:: Shared Styles -:: Page Name 1 -:: Page Name 2 -*/ - - -/* ----------------------------------------- - Shared Styles ------------------------------------------ */ - -table th { font-weight: bold; } -table td, table th { padding: 9px 10px; text-align: left; } - -/* Mobile */ -@media only screen and (max-width: 767px) { - - table { margin-bottom: 0; } - - .pinned { position: absolute; left: 0; top: 0; background: #fff; width: 35%; overflow: hidden; overflow-x: scroll; border-right: 1px solid #ccc; border-left: 1px solid #ccc; } - .pinned table { border-right: none; border-left: none; width: 100%; } - .pinned table th, .pinned table td { white-space: nowrap; } - .pinned td:last-child { border-bottom: 0; } - - div.table-wrapper { position: relative; margin-bottom: 20px; overflow: hidden; border-right: 1px solid #ccc; } - div.table-wrapper div.scrollable table { margin-left: 35%; } - div.table-wrapper div.scrollable { overflow: scroll; overflow-y: hidden; } - - table td, table th { position: relative; white-space: nowrap; overflow: hidden; } - table th:first-child, table td:first-child, table td:first-child, table.pinned td { display: none; } - -} - -/* ----------------------------------------- - Page Name 1 ------------------------------------------ */ - - - - -/* ----------------------------------------- - Page Name 2 ------------------------------------------ */ - - diff --git a/guides/bug_report_templates/action_controller_gem.rb b/guides/bug_report_templates/action_controller_gem.rb index 7fc85e636a..e8b6ad19dd 100644 --- a/guides/bug_report_templates/action_controller_gem.rb +++ b/guides/bug_report_templates/action_controller_gem.rb @@ -13,7 +13,7 @@ gemfile(true) do git_source(:github) { |repo| "https://github.com/#{repo}.git" } # Activate the gem you are reporting the issue against. - gem "rails", "5.2.0.rc1" + gem "rails", "5.2.0" end require "rack/test" diff --git a/guides/bug_report_templates/active_job_gem.rb b/guides/bug_report_templates/active_job_gem.rb index 6b30a7d446..720b7e9c51 100644 --- a/guides/bug_report_templates/active_job_gem.rb +++ b/guides/bug_report_templates/active_job_gem.rb @@ -13,7 +13,7 @@ gemfile(true) do git_source(:github) { |repo| "https://github.com/#{repo}.git" } # Activate the gem you are reporting the issue against. - gem "activejob", "5.2.0.rc1" + gem "activejob", "5.2.0" end require "minitest/autorun" diff --git a/guides/bug_report_templates/active_record_gem.rb b/guides/bug_report_templates/active_record_gem.rb index fabc2a2382..c0d705239b 100644 --- a/guides/bug_report_templates/active_record_gem.rb +++ b/guides/bug_report_templates/active_record_gem.rb @@ -13,7 +13,7 @@ gemfile(true) do git_source(:github) { |repo| "https://github.com/#{repo}.git" } # Activate the gem you are reporting the issue against. - gem "activerecord", "5.2.0.rc1" + gem "activerecord", "5.2.0" gem "sqlite3" end diff --git a/guides/bug_report_templates/active_record_migrations_gem.rb b/guides/bug_report_templates/active_record_migrations_gem.rb index ca9987f956..f47cf08766 100644 --- a/guides/bug_report_templates/active_record_migrations_gem.rb +++ b/guides/bug_report_templates/active_record_migrations_gem.rb @@ -13,7 +13,7 @@ gemfile(true) do git_source(:github) { |repo| "https://github.com/#{repo}.git" } # Activate the gem you are reporting the issue against. - gem "activerecord", "5.2.0.rc1" + gem "activerecord", "5.2.0" gem "sqlite3" end diff --git a/guides/bug_report_templates/generic_gem.rb b/guides/bug_report_templates/generic_gem.rb index 7a55d7c660..0935354bf4 100644 --- a/guides/bug_report_templates/generic_gem.rb +++ b/guides/bug_report_templates/generic_gem.rb @@ -13,7 +13,7 @@ gemfile(true) do git_source(:github) { |repo| "https://github.com/#{repo}.git" } # Activate the gem you are reporting the issue against. - gem "activesupport", "5.2.0.rc1" + gem "activesupport", "5.2.0" end require "active_support" diff --git a/guides/rails_guides/generator.rb b/guides/rails_guides/generator.rb index 7205f37be7..c83538ad48 100644 --- a/guides/rails_guides/generator.rb +++ b/guides/rails_guides/generator.rb @@ -141,32 +141,34 @@ module RailsGuides puts "Generating #{guide} as #{output_file}" layout = @kindle ? "kindle/layout" : "layout" - File.open(output_path, "w") do |f| - view = ActionView::Base.new( - @source_dir, - edge: @edge, - version: @version, - mobi: "kindle/#{mobi}", - language: @language - ) - view.extend(Helpers) - - if guide =~ /\.(\w+)\.erb$/ - # Generate the special pages like the home. - # Passing a template handler in the template name is deprecated. So pass the file name without the extension. - result = view.render(layout: layout, formats: [$1], file: $`) - else - body = File.read("#{@source_dir}/#{guide}") - result = RailsGuides::Markdown.new( - view: view, - layout: layout, - edge: @edge, - version: @version - ).render(body) - - warn_about_broken_links(result) - end + view = ActionView::Base.new( + @source_dir, + edge: @edge, + version: @version, + mobi: "kindle/#{mobi}", + language: @language + ) + view.extend(Helpers) + + if guide =~ /\.(\w+)\.erb$/ + return if %w[_license _welcome layout].include?($`) + + # Generate the special pages like the home. + # Passing a template handler in the template name is deprecated. So pass the file name without the extension. + result = view.render(layout: layout, formats: [$1], file: $`) + else + body = File.read("#{@source_dir}/#{guide}") + result = RailsGuides::Markdown.new( + view: view, + layout: layout, + edge: @edge, + version: @version + ).render(body) + + warn_about_broken_links(result) + end + File.open(output_path, "w") do |f| f.write(result) end end diff --git a/guides/rails_guides/helpers.rb b/guides/rails_guides/helpers.rb index a6970fb90c..5ab1388c29 100644 --- a/guides/rails_guides/helpers.rb +++ b/guides/rails_guides/helpers.rb @@ -38,15 +38,6 @@ module RailsGuides end end - def author(name, nick, image = "credits_pic_blank.gif", &block) - image = "images/#{image}" - - result = tag(:img, src: image, class: "left pic", alt: name, width: 91, height: 91) - result << content_tag(:h3, name) - result << content_tag(:p, capture(&block)) - content_tag(:div, result, class: "clearfix", id: nick) - end - def code(&block) c = capture(&block) content_tag(:code, c) diff --git a/guides/rails_guides/kindle.rb b/guides/rails_guides/kindle.rb index 5c4f7d159c..d370541d2e 100644 --- a/guides/rails_guides/kindle.rb +++ b/guides/rails_guides/kindle.rb @@ -35,7 +35,7 @@ module Kindle def generate_front_matter(html_pages) frontmatter = [] html_pages.delete_if { |x| - if x =~ /(toc|welcome|credits|copyright).html/ + if x =~ /(toc|welcome|copyright).html/ frontmatter << x unless x =~ /toc/ true end diff --git a/guides/source/2_2_release_notes.md b/guides/source/2_2_release_notes.md index afe0550a17..8b91b4853f 100644 --- a/guides/source/2_2_release_notes.md +++ b/guides/source/2_2_release_notes.md @@ -57,7 +57,6 @@ 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/). * More information: * [Rails Guides hackfest](http://hackfest.rubyonrails.org/guide) diff --git a/guides/source/4_0_release_notes.md b/guides/source/4_0_release_notes.md index 0921cd1979..a1a6a225b2 100644 --- a/guides/source/4_0_release_notes.md +++ b/guides/source/4_0_release_notes.md @@ -55,7 +55,7 @@ $ ruby /path/to/rails/railties/bin/rails new myapp --dev Major Features -------------- -[![Rails 4.0](images/rails4_features.png)](http://guides.rubyonrails.org/images/rails4_features.png) +[![Rails 4.0](images/4_0_release_notes/rails4_features.png)](http://guides.rubyonrails.org/images/4_0_release_notes/rails4_features.png) ### Upgrade diff --git a/guides/source/5_2_release_notes.md b/guides/source/5_2_release_notes.md index 541c025fac..ab24c7e590 100644 --- a/guides/source/5_2_release_notes.md +++ b/guides/source/5_2_release_notes.md @@ -75,6 +75,9 @@ secrets introduced in Rails 5.1. Furthermore, Rails 5.2 [opens API underlying Credentials](https://github.com/rails/rails/pull/30940), so you can easily deal with other encrypted configurations, keys, and files. +You can read more about this in the +[Securing Rails Applications](security.html#custom-credentials) +guide. ### Content Security Policy diff --git a/guides/source/_welcome.html.erb b/guides/source/_welcome.html.erb index 6959f992aa..5dd6bfdd23 100644 --- a/guides/source/_welcome.html.erb +++ b/guides/source/_welcome.html.erb @@ -10,17 +10,20 @@ </p> <% else %> <p> - These are the new guides for Rails 5.1 based on <a href="https://github.com/rails/rails/tree/<%= @version %>"><%= @version %></a>. + These are the new guides for Rails 5.2 based on <a href="https://github.com/rails/rails/tree/<%= @version %>"><%= @version %></a>. These guides are designed to make you immediately productive with Rails, and to help you understand how all of the pieces fit together. </p> <% end %> <p> The guides for earlier releases: +<a href="http://guides.rubyonrails.org/v5.2/">Rails 5.2</a>, <a href="http://guides.rubyonrails.org/v5.1/">Rails 5.1</a>, <a href="http://guides.rubyonrails.org/v5.0/">Rails 5.0</a>, <a href="http://guides.rubyonrails.org/v4.2/">Rails 4.2</a>, <a href="http://guides.rubyonrails.org/v4.1/">Rails 4.1</a>, <a href="http://guides.rubyonrails.org/v4.0/">Rails 4.0</a>, -<a href="http://guides.rubyonrails.org/v3.2/">Rails 3.2</a>, and +<a href="http://guides.rubyonrails.org/v3.2/">Rails 3.2</a>, +<a href="http://guides.rubyonrails.org/v3.1/">Rails 3.1</a>, +<a href="http://guides.rubyonrails.org/v3.0/">Rails 3.0</a>, and <a href="http://guides.rubyonrails.org/v2.3/">Rails 2.3</a>. </p> diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index eadd517f07..e0e85588a0 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -51,7 +51,7 @@ class ClientsController < ApplicationController end ``` -As an example, if a user goes to `/clients/new` in your application to add a new client, Rails will create an instance of `ClientsController` and call its `new` method. Note that the empty method from the example above would work just fine because Rails will by default render the `new.html.erb` view unless the action says otherwise. The `new` method could make available to the view a `@client` instance variable by creating a new `Client`: +As an example, if a user goes to `/clients/new` in your application to add a new client, Rails will create an instance of `ClientsController` and call its `new` method. Note that the empty method from the example above would work just fine because Rails will by default render the `new.html.erb` view unless the action says otherwise. By creating a new `Client`, the `new` method can make a `@client` instance variable accessible in the view: ```ruby def new @@ -1181,22 +1181,6 @@ NOTE: Certain exceptions are only rescuable from the `ApplicationController` cla Force HTTPS protocol -------------------- -Sometime you might want to force a particular controller to only be accessible via an HTTPS protocol for security reasons. You can use the `force_ssl` method in your controller to enforce that: - -```ruby -class DinnerController - force_ssl -end -``` - -Just like the filter, you could also pass `:only` and `:except` to enforce the secure connection only to specific actions: - -```ruby -class DinnerController - force_ssl only: :cheeseburger - # or - force_ssl except: :cheeseburger -end -``` - -Please note that if you find yourself adding `force_ssl` to many controllers, you may want to force the whole application to use HTTPS instead. In that case, you can set the `config.force_ssl` in your environment file. +If you'd like to ensure that communication to your controller is only possible +via HTTPS, you should do so by enabling the `ActionDispatch::SSL` middleware via +`config.force_ssl` in your environment configuration. diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md index c01d1082b6..37b8843d1e 100644 --- a/guides/source/action_view_overview.md +++ b/guides/source/action_view_overview.md @@ -1267,8 +1267,8 @@ password_field_tag 'pass' Creates a radio button; use groups of radio buttons named the same to allow users to select from a group of options. ```ruby -radio_button_tag 'gender', 'male' -# => <input id="gender_male" name="gender" type="radio" value="male" /> +radio_button_tag 'favorite_color', 'maroon' +# => <input id="favorite_color_maroon" name="favorite_color" type="radio" value="maroon" /> ``` #### select_tag diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index 75ad343613..ae2e1faf14 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -779,6 +779,14 @@ delegate :size, to: :attachment, prefix: :avatar In the previous example the macro generates `avatar_size` rather than `size`. +The option `:private` changes methods scope: + +```ruby +delegate :date_of_birth, to: :profile, private: true +``` + +The delegated methods are public by default. Pass `private: true` to change that. + NOTE: Defined in `active_support/core_ext/module/delegation.rb` #### `delegate_missing_to` diff --git a/guides/source/api_app.md b/guides/source/api_app.md index b4d90d31de..c2df6c45ad 100644 --- a/guides/source/api_app.md +++ b/guides/source/api_app.md @@ -24,7 +24,7 @@ With the advent of client-side frameworks, more developers are using Rails to build a back-end that is shared between their web application and other native applications. -For example, Twitter uses its [public API](https://dev.twitter.com) in its web +For example, Twitter uses its [public API](https://developer.twitter.com/) in its web application, which is built as a static site that consumes JSON resources. Instead of using Rails to generate HTML that communicates with the server @@ -375,7 +375,6 @@ controller modules by default: - `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 similar helpers. diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 2f5854fed0..88b87b78d2 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -728,8 +728,8 @@ Rails.application.config.assets.precompile += %w( admin.js admin.css ) NOTE. Always specify an expected compiled filename that ends with `.js` or `.css`, even if you want to add Sass or CoffeeScript files to the precompile array. -The task also generates a `.sprockets-manifest-md5hash.json` (where `md5hash` is -an MD5 hash) that contains a list with all your assets and their respective +The task also generates a `.sprockets-manifest-randomhex.json` (where `randomhex` is +a 16-byte random hex string) that contains a list with all your assets and their respective fingerprints. This is used by the Rails helper methods to avoid handing the mapping requests back to Sprockets. A typical manifest file looks like: diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index f895cadea5..6fbd52edbd 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -94,7 +94,7 @@ class Book < ApplicationRecord end ``` -![belongs_to Association Diagram](images/belongs_to.png) +![belongs_to Association Diagram](images/association_basics/belongs_to.png) NOTE: `belongs_to` associations _must_ use the singular term. If you used the pluralized form in the above example for the `author` association in the `Book` model, you would be told that there was an "uninitialized constant Book::Authors". This is because Rails automatically infers the class name from the association name. If the association name is wrongly pluralized, then the inferred class will be wrongly pluralized too. @@ -127,7 +127,7 @@ class Supplier < ApplicationRecord end ``` -![has_one Association Diagram](images/has_one.png) +![has_one Association Diagram](images/association_basics/has_one.png) The corresponding migration might look like this: @@ -171,7 +171,7 @@ end NOTE: The name of the other model is pluralized when declaring a `has_many` association. -![has_many Association Diagram](images/has_many.png) +![has_many Association Diagram](images/association_basics/has_many.png) The corresponding migration might look like this: @@ -213,7 +213,7 @@ class Patient < ApplicationRecord end ``` -![has_many :through Association Diagram](images/has_many_through.png) +![has_many :through Association Diagram](images/association_basics/has_many_through.png) The corresponding migration might look like this: @@ -299,7 +299,7 @@ class AccountHistory < ApplicationRecord end ``` -![has_one :through Association Diagram](images/has_one_through.png) +![has_one :through Association Diagram](images/association_basics/has_one_through.png) The corresponding migration might look like this: @@ -340,7 +340,7 @@ class Part < ApplicationRecord end ``` -![has_and_belongs_to_many Association Diagram](images/habtm.png) +![has_and_belongs_to_many Association Diagram](images/association_basics/habtm.png) The corresponding migration might look like this: @@ -494,7 +494,7 @@ class CreatePictures < ActiveRecord::Migration[5.0] end ``` -![Polymorphic Association Diagram](images/polymorphic.png) +![Polymorphic Association Diagram](images/association_basics/polymorphic.png) ### Self Joins @@ -505,7 +505,7 @@ class Employee < ApplicationRecord has_many :subordinates, class_name: "Employee", foreign_key: "manager_id" - belongs_to :manager, class_name: "Employee" + belongs_to :manager, class_name: "Employee", optional: true end ``` diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 368b74f708..7d5ca4b8a7 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -400,10 +400,16 @@ by adding the following to your `application.rb` file: Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true ``` -The schema dumper adds one additional configuration option: +The schema dumper adds two additional configuration options: * `ActiveRecord::SchemaDumper.ignore_tables` accepts an array of tables that should _not_ be included in any generated schema file. +* `ActiveRecord::SchemaDumper.fk_ignore_pattern` allows setting a different regular + expression that will be used to decide whether a foreign key's name should be + dumped to db/schema.rb or not. By default, foreign key names starting with + `fk_rails_` are not exported to the database schema dump. + Defaults to `/^fk_rails_[0-9a-f]{10}$/`. + ### Configuring Action Controller `config.action_controller` includes a number of configuration settings: @@ -594,6 +600,13 @@ Defaults to `'signed cookie'`. * `config.action_view.default_enforce_utf8` determines whether forms are generated with a hidden tag that forces older versions of Internet Explorer to submit forms encoded in UTF-8. This defaults to `false`. +* `config.action_view.finalize_compiled_template_methods` determines + whether the methods on `ActionView::CompiledTemplates` that templates + compile themselves to are removed when template instances are + destroyed by the garbage collector. This helps prevent memory leaks in + development mode, but for large test suites, disabling this option in + the test environment can improve performance. This defaults to `true`. + ### Configuring Action Mailer There are a number of settings available on `config.action_mailer`: diff --git a/guides/source/credits.html.erb b/guides/source/credits.html.erb deleted file mode 100644 index 5adbd12ac0..0000000000 --- a/guides/source/credits.html.erb +++ /dev/null @@ -1,80 +0,0 @@ -<% content_for :page_title do %> -Ruby on Rails Guides: Credits -<% end %> - -<% content_for :header_section do %> -<h2>Credits</h2> - -<p>We'd like to thank the following people for their tireless contributions to this project.</p> - -<% end %> - -<h3 class="section">Rails Guides Reviewers</h3> - -<%= author('Vijay Dev', 'vijaydev', 'vijaydev.jpg') do %> - Vijayakumar, found as Vijay Dev on the web, is a web applications developer and an open source enthusiast who lives in Chennai, India. He started using Rails in 2009 and began actively contributing to Rails documentation in late 2010. He <a href="https://twitter.com/vijay_dev">tweets</a> a lot and also <a href="http://vijaydev.wordpress.com">blogs</a>. -<% end %> - -<%= author('Xavier Noria', 'fxn', 'fxn.png') do %> - Xavier Noria has been into Ruby on Rails since 2005. He is a Rails core team member and enjoys combining his passion for Rails and his past life as a proofreader of math textbooks. Xavier is currently an independent Ruby on Rails consultant. Oh, he also <a href="http://twitter.com/fxn">tweets</a> and can be found everywhere as "fxn". -<% end %> - -<h3 class="section">Rails Guides Designers</h3> - -<%= author('Jason Zimdars', 'jz') do %> - Jason Zimdars is an experienced creative director and web designer who has lead UI and UX design for numerous websites and web applications. You can see more of his design and writing at <a href="http://www.thinkcage.com/">Thinkcage.com</a> or follow him on <a href="https://twitter.com/jasonzimdars">Twitter</a>. -<% end %> - -<h3 class="section">Rails Guides Authors</h3> - -<%= author('Ryan Bigg', 'radar', 'radar.png') do %> - Ryan Bigg works as a Rails developer at <a href="http://marketplacer.com">Marketplacer</a> and has been working with Rails since 2006. He's the author of <a href="https://leanpub.com/multi-tenancy-rails">Multi Tenancy With Rails</a> and co-author of <a href="http://manning.com/bigg2">Rails 4 in Action</a>. He's written many gems which can be seen on <a href="https://github.com/radar">his GitHub page</a> and he also tweets prolifically as <a href="http://twitter.com/ryanbigg">@ryanbigg</a>. -<% end %> - -<%= author('Oscar Del Ben', 'oscardelben', 'oscardelben.jpg') do %> -Oscar Del Ben is a software engineer at <a href="http://www.businessinsider.com/google-buys-wildfire-2012-8">Wildfire</a>. He's a regular open source contributor (<a href="https://github.com/oscardelben">GitHub account</a>) and tweets regularly at <a href="https://twitter.com/oscardelben">@oscardelben</a>. - <% end %> - -<%= author('Frederick Cheung', 'fcheung') do %> - Frederick Cheung is Chief Wizard at Texperts where he has been using Rails since 2006. He is based in Cambridge (UK) and when not consuming fine ales he blogs at <a href="http://www.spacevatican.org">spacevatican.org</a>. -<% end %> - -<%= author('Tore Darell', 'toretore') do %> - Tore Darell is an independent developer based in Menton, France who specialises in cruft-free web applications using Ruby, Rails and unobtrusive JavaScript. You can follow him on <a href="http://twitter.com/toretore">Twitter</a>. -<% end %> - -<%= author('Jeff Dean', 'zilkey') do %> - Jeff Dean is a software engineer with <a href="http://pivotallabs.com">Pivotal Labs</a>. -<% end %> - -<%= author('Mike Gunderloy', 'mgunderloy') do %> - Mike Gunderloy is a consultant with <a href="http://www.actionrails.com">ActionRails</a>. He brings 25 years of experience in a variety of languages to bear on his current work with Rails. His near-daily links and other blogging can be found at <a href="http://afreshcup.com">A Fresh Cup</a> and he <a href="http://twitter.com/MikeG1">twitters</a> too much. -<% end %> - -<%= author('Mikel Lindsaar', 'raasdnil') do %> - Mikel Lindsaar has been working with Rails since 2006 and is the author of the Ruby <a href="https://github.com/mikel/mail">Mail gem</a> and core contributor (he helped re-write Action Mailer's API). Mikel is the founder of <a href="http://rubyx.com/">RubyX</a>, has a <a href="http://lindsaar.net/">blog</a> and <a href="http://twitter.com/raasdnil">tweets</a>. -<% end %> - -<%= author('Cássio Marques', 'cmarques') do %> - Cássio Marques is a Brazilian software developer working with different programming languages such as Ruby, JavaScript, CPP and Java, as an independent consultant. He blogs at <a href="http://cassiomarques.wordpress.com">/* CODIFICANDO */</a>, which is mainly written in Portuguese, but will soon get a new section for posts with English translation. -<% end %> - -<%= author('James Miller', 'bensie') do %> - James Miller is a software developer for <a href="http://www.jk-tech.com">JK Tech</a> in San Diego, CA. You can find James on GitHub, Gmail, Twitter, and Freenode as "bensie". -<% end %> - -<%= author('Pratik Naik', 'lifo') do %> - 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 %> - Emilio Tagua —a.k.a. miloops— is an Argentinian entrepreneur, developer, open source contributor and Rails evangelist. Cofounder of <a href="http://eventioz.com">Eventioz</a>. He has been using Rails since 2006 and contributing since early 2008. Can be found at gmail, twitter, freenode, everywhere as "miloops". -<% end %> - -<%= author('Heiko Webers', 'hawe') do %> - Heiko Webers is the founder of <a href="http://www.bauland42.de">bauland42</a>, a German web application security consulting and development company focused on Ruby on Rails. He blogs at the <a href="http://www.rorsecurity.info">Ruby on Rails Security Project</a>. After 10 years of desktop application development, Heiko has rarely looked back. -<% end %> - -<%= author('Akshay Surve', 'startupjockey', 'akshaysurve.jpg') do %> - Akshay Surve is the Founder at <a href="http://www.deltax.com">DeltaX</a>, hackathon specialist, a midnight code junkie and occasionally writes prose. You can connect with him on <a href="https://twitter.com/akshaysurve">Twitter</a>, <a href="http://www.linkedin.com/in/akshaysurve">Linkedin</a>, <a href="http://www.akshaysurve.com/">Personal Blog</a> or <a href="http://www.quora.com/Akshay-Surve">Quora</a>. -<% end %> diff --git a/guides/source/i18n.md b/guides/source/i18n.md index 339b356a78..f42ab15b8b 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -829,14 +829,14 @@ For example when you add the following translations: en: activerecord: models: - user: Dude + user: Customer attributes: user: login: "Handle" # will translate User attribute "login" as "Handle" ``` -Then `User.model_name.human` will return "Dude" and `User.human_attribute_name("login")` will return "Handle". +Then `User.model_name.human` will return "Customer" and `User.human_attribute_name("login")` will return "Handle". You can also set a plural form for model names, adding as following: @@ -845,11 +845,11 @@ en: activerecord: models: user: - one: Dude - other: Dudes + one: Customer + other: Customers ``` -Then `User.model_name.human(count: 2)` will return "Dudes". With `count: 1` or without params will return "Dude". +Then `User.model_name.human(count: 2)` will return "Customers". With `count: 1` or without params will return "Customer". In the event you need to access nested attributes within a given model, you should nest these under `model/attribute` at the model level of your translation file: @@ -857,12 +857,12 @@ In the event you need to access nested attributes within a given model, you shou en: activerecord: attributes: - user/gender: - female: "Female" - male: "Male" + user/role: + admin: "Admin" + contributor: "Contributor" ``` -Then `User.human_attribute_name("gender.female")` will return "Female". +Then `User.human_attribute_name("role.admin")` will return "Admin". NOTE: If you are using a class which includes `ActiveModel` and does not inherit from `ActiveRecord::Base`, replace `activerecord` with `activemodel` in the above key paths. diff --git a/guides/source/index.html.erb b/guides/source/index.html.erb index 2fdf18a2e9..76f01fea0a 100644 --- a/guides/source/index.html.erb +++ b/guides/source/index.html.erb @@ -10,7 +10,9 @@ Ruby on Rails Guides <div id="subCol"> <dl> <dt></dt> - <dd class="kindle">Rails Guides are also available for <%= link_to 'Kindle', @mobi %>.</dd> + <% unless @edge -%> + <dd class="kindle">Rails Guides are also available for <%= link_to 'Kindle', @mobi %>.</dd> + <% end -%> <dd class="work-in-progress">Guides marked with this icon are currently being worked on and will not be available in the Guides Index menu. While still useful, they may contain incomplete information and even errors. You can help by reviewing them and posting your comments and corrections.</dd> </dl> </div> diff --git a/guides/source/kindle/rails_guides.opf.erb b/guides/source/kindle/rails_guides.opf.erb index 63eeb007d7..1882ec1005 100644 --- a/guides/source/kindle/rails_guides.opf.erb +++ b/guides/source/kindle/rails_guides.opf.erb @@ -26,7 +26,7 @@ <item id="<%= document['url'] %>" media-type="text/html" href="<%= document['url'] %>" /> <% end %> - <% %w{toc.html credits.html welcome.html copyright.html}.each do |url| %> + <% %w{toc.html welcome.html copyright.html}.each do |url| %> <item id="<%= url %>" media-type="text/html" href="<%= url %>" /> <% end %> @@ -38,7 +38,6 @@ <spine toc="toc"> <itemref idref="toc.html" /> <itemref idref="welcome.html" /> - <itemref idref="credits.html" /> <itemref idref="copyright.html" /> <% documents_flat.each do |document| %> <itemref idref="<%= document['url'] %>" /> diff --git a/guides/source/kindle/toc.html.erb b/guides/source/kindle/toc.html.erb index 0f4228ed6b..b77ac2e99d 100644 --- a/guides/source/kindle/toc.html.erb +++ b/guides/source/kindle/toc.html.erb @@ -18,7 +18,6 @@ Ruby on Rails Guides <% end %> <hr /> <ul> - <li><a href="credits.html">Credits</a></li> <li><a href="copyright.html">Copyright & License</a></li> </ul> </div> diff --git a/guides/source/kindle/toc.ncx.erb b/guides/source/kindle/toc.ncx.erb index 5094fea4ca..9b73bc9bea 100644 --- a/guides/source/kindle/toc.ncx.erb +++ b/guides/source/kindle/toc.ncx.erb @@ -30,10 +30,6 @@ </navLabel> <content src="welcome.html"/> </navPoint> - <navPoint class="article" id="credits" playOrder="3"> - <navLabel><text>Credits</text></navLabel> - <content src="credits.html"/> - </navPoint> <navPoint class="article" id="copyright" playOrder="4"> <navLabel><text>Copyright & License</text></navLabel> <content src="copyright.html"/> diff --git a/guides/source/layout.html.erb b/guides/source/layout.html.erb index 3981199e95..6e3aa9207e 100644 --- a/guides/source/layout.html.erb +++ b/guides/source/layout.html.erb @@ -59,7 +59,6 @@ </div> </li> <li><a class="nav-item" href="contributing_to_ruby_on_rails.html">Contribute</a></li> - <li><a class="nav-item" href="credits.html">Credits</a></li> <li class="guides-index guides-index-small"> <select class="guides-index-item nav-item"> <option value="index.html">Guides Index</option> diff --git a/guides/source/maintenance_policy.md b/guides/source/maintenance_policy.md index 1d6a4edb5b..2604d289e9 100644 --- a/guides/source/maintenance_policy.md +++ b/guides/source/maintenance_policy.md @@ -44,7 +44,7 @@ from. In special situations, where someone from the Core Team agrees to support more series, they are included in the list of supported series. -**Currently included series:** `5.1.Z`. +**Currently included series:** `5.2.Z`. Security Issues --------------- @@ -59,16 +59,16 @@ be built from 1.2.2, and then added to the end of 1-2-stable. This means that security releases are easy to upgrade to if you're running the latest version of Rails. -**Currently included series:** `5.1.Z`, `5.0.Z`. +**Currently included series:** `5.2.Z`, `5.1.Z`. Severe Security Issues ---------------------- -For severe security issues we will provide new versions as above, and also the +For severe security issues all releases in the current major series, and also the last major release series will receive patches and new versions. The classification of the security issue is judged by the core team. -**Currently included series:** `5.1.Z`, `5.0.Z`, `4.2.Z`. +**Currently included series:** `5.2.Z`, `5.1.Z`, `5.0.Z`, `4.2.Z`. Unsupported Release Series -------------------------- diff --git a/guides/source/security.md b/guides/source/security.md index b419f7b48d..3ac50fb147 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -74,7 +74,7 @@ Hence, the cookie serves as temporary authentication for the web application. An * Instead of stealing a cookie unknown to the attacker, they fix a user's session identifier (in the cookie) known to them. Read more about this so-called session fixation later. -The main objective of most attackers is to make money. The underground prices for stolen bank login accounts range from $10-$1000 (depending on the available amount of funds), $0.40-$20 for credit card numbers, $1-$8 for online auction site accounts and $4-$30 for email passwords, according to 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). +The main objective of most attackers is to make money. The underground prices for stolen bank login accounts range from 0.5%-10% of account balance, $0.5-$30 for credit card numbers ($20-$60 with full details), $0.1-$1.5 for identities (Name, SSN & DOB), $20-$50 for retailer accounts, and $6-$10 for cloud service provider accounts, according to the [Symantec Internet Security Threat Report (2017)](https://www.symantec.com/content/dam/symantec/docs/reports/istr-22-2017-en.pdf). ### Session Guidelines @@ -217,7 +217,7 @@ The best _solution against it is not to store this kind of data in a session, bu 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) +![Session fixation](images/security/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: @@ -272,7 +272,7 @@ Cross-Site Request Forgery (CSRF) This attack method works by including malicious code or a link in a page that accesses a web application that the user is believed to have authenticated. If the session for that web application has not timed out, an attacker may execute unauthorized commands. -![](images/csrf.png) +![](images/security/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: @@ -862,7 +862,7 @@ In December 2006, 34,000 actual user names and passwords were stolen in a [MySpa INFO: _CSS Injection is actually JavaScript injection, because some browsers (IE, some versions of Safari and others) allow JavaScript in CSS. Think twice about allowing custom CSS in your web application._ -CSS Injection is explained best by the well-known [MySpace Samy worm](https://samy.pl/popular/tech.html). This worm automatically sent a friend request to Samy (the attacker) simply by visiting his profile. Within several hours he had over 1 million friend requests, which created so much traffic that MySpace went offline. The following is a technical explanation of that worm. +CSS Injection is explained best by the well-known [MySpace Samy worm](https://samy.pl/myspace/tech.html). This worm automatically sent a friend request to Samy (the attacker) simply by visiting his profile. Within several hours he had over 1 million friend requests, which created so much traffic that MySpace went offline. The following is a technical explanation of that worm. MySpace blocked many tags, but allowed CSS. So the worm's author put JavaScript into CSS like this: @@ -1182,6 +1182,12 @@ as part of `html_options`. Example: <% end -%> ``` +The same works with `javascript_include_tag`: + +```html+erb +<%= javascript_include_tag "script", nonce: true %> +``` + Use [`csp_meta_tag`](http://api.rubyonrails.org/classes/ActionView/Helpers/CspHelper.html#method-i-csp_meta_tag) helper to create a meta tag "csp-nonce" with the per-session nonce value for allowing inline `<script>` tags. diff --git a/guides/source/testing.md b/guides/source/testing.md index b9b310cbba..82bf38c206 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -690,7 +690,7 @@ System Testing -------------- System tests allow you to test user interactions with your application, running tests -in either a real or a headless browser. System tests uses Capybara under the hood. +in either a real or a headless browser. System tests use Capybara under the hood. For creating Rails system tests, you use the `test/system` directory in your application. Rails provides a generator to create a system test skeleton for you. @@ -1484,7 +1484,7 @@ located under the `test/helpers` directory. Given we have the following helper: ```ruby -module UserHelper +module UsersHelper def link_to_user(user) link_to "#{user.first_name} #{user.last_name}", user end @@ -1494,7 +1494,7 @@ end We can test the output of this method like this: ```ruby -class UserHelperTest < ActionView::TestCase +class UsersHelperTest < ActionView::TestCase test "should return the user's full name" do user = users(:david) @@ -1600,7 +1600,7 @@ Functional testing for mailers involves more than just checking that the email b ```ruby require 'test_helper' -class UserControllerTest < ActionDispatch::IntegrationTest +class UsersControllerTest < ActionDispatch::IntegrationTest test "invite friend" do assert_difference 'ActionMailer::Base.deliveries.size', +1 do post invite_friend_url, params: { email: 'friend@example.com' } diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index d5dfaef591..c2fe012eeb 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -66,6 +66,17 @@ Overwrite /myapp/config/application.rb? (enter "h" for help) [Ynaqdh] Don't forget to review the difference, to see if there were any unexpected changes. +Upgrading from Rails 5.2 to Rails 6.0 +------------------------------------- + +### Force SSL + +The `force_ssl` method on controllers has been deprecated and will be removed in +Rails 6.1. You are encouraged to enable `config.force_ssl` to enforce HTTPS +connections throughout your application. If you need to exempt certain endpoints +from redirection, you can use `config.ssl_options` to configure that behavior. + + Upgrading from Rails 5.1 to Rails 5.2 ------------------------------------- diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md index b9ea4ad47a..a922bdc16b 100644 --- a/guides/source/working_with_javascript_in_rails.md +++ b/guides/source/working_with_javascript_in_rails.md @@ -382,10 +382,9 @@ have been bundled into `event.detail`. For information about the previously used `jquery-ujs` in Rails 5 and earlier, read the [`jquery-ujs` wiki](https://github.com/rails/jquery-ujs/wiki/ajax). ### Stoppable events - -If you stop `ajax:before` or `ajax:beforeSend` by returning false from the -handler method, the Ajax request will never take place. The `ajax:before` event -can manipulate form data before serialization and the +You can stop execution of the Ajax request by running `event.preventDefault()` +from the handlers methods `ajax:before` or `ajax:beforeSend`. +The `ajax:before` event can manipulate form data before serialization and the `ajax:beforeSend` event is useful for adding custom request headers. If you stop the `ajax:aborted:file` event, the default behavior of allowing the @@ -393,6 +392,9 @@ browser to submit the form via normal means (i.e. non-Ajax submission) will be canceled and the form will not be submitted at all. This is useful for implementing your own Ajax file upload workaround. +Note, you should use `return false` to prevent event for `jquery-ujs` and +`e.preventDefault()` for `rails-ujs` + Server-Side Concerns -------------------- diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 3b4b5330f7..a4d4a87a8b 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,28 @@ +* Make the master.key file read-only for the owner upon generation on + POSIX-compliant systems. + + Previously: + + $ ls -l config/master.key + -rw-r--r-- 1 owner group 32 Jan 1 00:00 master.key + + Now: + + $ ls -l config/master.key + -rw------- 1 owner group 32 Jan 1 00:00 master.key + + Fixes #32604. + + *Jose Luis Duran* + +* Deprecate support for using the `HOST` environment to specify the server IP. + + The `BINDING` environment should be used instead. + + Fixes #29516. + + *Yuji Yaginuma* + * Deprecate passing Rack server name as a regular argument to `rails server`. Previously: diff --git a/railties/RDOC_MAIN.rdoc b/railties/RDOC_MAIN.rdoc index c70a9f0ba0..a4a4b6b235 100644 --- a/railties/RDOC_MAIN.rdoc +++ b/railties/RDOC_MAIN.rdoc @@ -1,4 +1,6 @@ -== Welcome to \Rails += Welcome to \Rails + +== What's \Rails \Rails is a web-application framework that includes everything needed to create database-backed web applications according to the @@ -6,43 +8,48 @@ create database-backed web applications according to the pattern. Understanding the MVC pattern is key to understanding \Rails. MVC divides your -application into three layers, each with a specific responsibility. +application into three layers: Model, View, and Controller, each with a specific responsibility. + +== Model layer -The <em>Model layer</em> represents your domain model (such as Account, Product, -Person, Post, etc.) and encapsulates the business logic that is specific to +The <em><b>Model layer</b></em> represents the domain model (such as Account, Product, +Person, Post, etc.) and encapsulates the business logic specific to your application. In \Rails, database-backed model classes are derived from -ActiveRecord::Base. Active Record allows you to present the data from +<tt>ActiveRecord::Base</tt>. {Active Record}[link:files/activerecord/README_rdoc.html] allows you to present the data from database rows as objects and embellish these data objects with business logic -methods. You can read more about Active Record in its {README}[link:files/activerecord/README_rdoc.html]. -Although most \Rails models are backed by a database, models can also be ordinary +methods. Although most \Rails models are backed by a database, models can also be ordinary Ruby classes, or Ruby classes that implement a set of interfaces as provided by -the Active Model module. You can read more about Active Model in its {README}[link:files/activemodel/README_rdoc.html]. +the {Active Model}[link:files/activemodel/README_rdoc.html] module. + +== Controller layer -The <em>Controller layer</em> is responsible for handling incoming HTTP requests and +The <em><b>Controller layer</b></em> is responsible for handling incoming HTTP requests and providing a suitable response. Usually this means returning \HTML, but \Rails controllers can also generate XML, JSON, PDFs, mobile-specific views, and more. Controllers load and manipulate models, and render view templates in order to generate the appropriate HTTP response. In \Rails, incoming requests are routed by Action Dispatch to an appropriate controller, and -controller classes are derived from ActionController::Base. Action Dispatch and Action Controller -are bundled together in Action Pack. You can read more about Action Pack in its -{README}[link:files/actionpack/README_rdoc.html]. +controller classes are derived from <tt>ActionController::Base</tt>. Action Dispatch and Action Controller +are bundled together in {Action Pack}[link:files/actionpack/README_rdoc.html]. -The <em>View layer</em> is composed of "templates" that are responsible for providing +== View layer + +The <em><b>View layer</b></em> is composed of "templates" that are responsible for providing appropriate representations of your application's resources. Templates can come in a variety of formats, but most view templates are \HTML with embedded Ruby code (ERB files). Views are typically rendered to generate a controller response, -or to generate the body of an email. In \Rails, View generation is handled by Action View. -You can read more about Action View in its {README}[link:files/actionview/README_rdoc.html]. +or to generate the body of an email. In \Rails, View generation is handled by {Action View}[link:files/actionview/README_rdoc.html]. + +== Frameworks and libraries -Active Record, Active Model, Action Pack, and Action View can each be used independently outside \Rails. -In addition to that, \Rails also comes with Action Mailer ({README}[link:files/actionmailer/README_rdoc.html]), a library -to generate and send emails; Active Job ({README}[link:files/activejob/README_md.html]), a +{Active Record}[link:files/activerecord/README_rdoc.html], {Active Model}[link:files/activemodel/README_rdoc.html], +{Action Pack}[link:files/actionpack/README_rdoc.html], and {Action View}[link:files/actionview/README_rdoc.html] can each be used independently outside \Rails. +In addition to that, \Rails also comes with {Action Mailer}[link:files/actionmailer/README_rdoc.html], a library +to generate and send emails; {Active Job}[link:files/activejob/README_md.html], a framework for declaring jobs and making them run on a variety of queueing -backends; Action Cable ({README}[link:files/actioncable/README_md.html]), a framework to -integrate WebSockets with a \Rails application; -Active Storage ({README}[link:files/activestorage/README_md.html]), a library to attach cloud -and local files to \Rails applications; -and Active Support ({README}[link:files/activesupport/README_rdoc.html]), a collection +backends; {Action Cable}[link:files/actioncable/README_md.html], a framework to +integrate WebSockets with a \Rails application; {Active Storage}[link:files/activestorage/README_md.html], +a library to attach cloud and local files to \Rails applications; +and {Active Support}[link:files/activesupport/README_rdoc.html], a collection of utility classes and standard library extensions that are useful for \Rails, and may also be used independently outside \Rails. diff --git a/railties/lib/minitest/rails_plugin.rb b/railties/lib/minitest/rails_plugin.rb index 7193abbc33..6486fa1798 100644 --- a/railties/lib/minitest/rails_plugin.rb +++ b/railties/lib/minitest/rails_plugin.rb @@ -13,7 +13,7 @@ module Minitest end def self.plugin_rails_options(opts, options) - Rails::TestUnit::Runner.attach_before_load_options(opts) + ::Rails::TestUnit::Runner.attach_before_load_options(opts) opts.on("-b", "--backtrace", "Show the complete backtrace") do options[:full_backtrace] = true diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index a9dee10981..e346d5cc3a 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -427,7 +427,7 @@ module Rails # the correct place to store it is in the encrypted credentials file. def secret_key_base if Rails.env.test? || Rails.env.development? - Digest::MD5.hexdigest self.class.name + secrets.secret_key_base || Digest::MD5.hexdigest(self.class.name) else validate_secret_key_base( ENV["SECRET_KEY_BASE"] || credentials.secret_key_base || secrets.secret_key_base diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 912faed3e4..bba573499d 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -166,6 +166,18 @@ module Rails end end + # Loads the database YAML without evaluating ERB. People seem to + # write ERB that makes the database configuration depend on + # Rails configuration. But we want Rails configuration (specifically + # `rake` and `rails` tasks) to be generated based on information in + # the database yaml, so we need a method that loads the database + # yaml *without* the context of the Rails application. + def load_database_yaml # :nodoc: + path = paths["config/database"].existent.first + return {} unless path + YAML.load_file(path.to_s) + end + # Loads and returns the entire raw configuration of database from # values stored in <tt>config/database.yml</tt>. def database_configuration @@ -241,7 +253,7 @@ module Rails end def annotations - SourceAnnotationExtractor::Annotation + Rails::SourceAnnotationExtractor::Annotation end def content_security_policy(&block) diff --git a/railties/lib/rails/command/spellchecker.rb b/railties/lib/rails/command/spellchecker.rb index 59ccab4ea2..154358cd45 100644 --- a/railties/lib/rails/command/spellchecker.rb +++ b/railties/lib/rails/command/spellchecker.rb @@ -3,50 +3,8 @@ module Rails module Command module Spellchecker # :nodoc: - class << self - def suggest(word, from:, count: 3) - from.sort_by { |w| levenshtein_distance(word, w) }.take(count) - end - - private - # This code is based directly on the Text gem implementation. - # Copyright (c) 2006-2013 Paul Battley, Michael Neumann, Tim Fletcher. - # - # Returns a value representing the "cost" of transforming str1 into str2. - def levenshtein_distance(str1, str2) # :doc: - s = str1 - t = str2 - n = s.length - m = t.length - - return m if (0 == n) - return n if (0 == m) - - d = (0..m).to_a - x = nil - - # avoid duplicating an enumerable object in the loop - str2_codepoint_enumerable = str2.each_codepoint - - str1.each_codepoint.with_index do |char1, i| - e = i + 1 - - str2_codepoint_enumerable.with_index do |char2, j| - cost = (char1 == char2) ? 0 : 1 - x = [ - d[j + 1] + 1, # insertion - e + 1, # deletion - d[j] + cost # substitution - ].min - d[j] = e - e = x - end - - d[m] = x - end - - x - end + def self.suggest(word, from:) + DidYouMean::SpellChecker.new(dictionary: from.map(&:to_s)).correct(word).first end end end diff --git a/railties/lib/rails/commands/server/server_command.rb b/railties/lib/rails/commands/server/server_command.rb index 8588e2fd64..77b6c1f65d 100644 --- a/railties/lib/rails/commands/server/server_command.rb +++ b/railties/lib/rails/commands/server/server_command.rb @@ -219,7 +219,7 @@ module Rails user_supplied_options << name end end - user_supplied_options << :Host if ENV["HOST"] + user_supplied_options << :Host if ENV["HOST"] || ENV["BINDING"] user_supplied_options << :Port if ENV["PORT"] user_supplied_options.uniq end @@ -234,7 +234,17 @@ module Rails options[:binding] else default_host = environment == "development" ? "localhost" : "0.0.0.0" - ENV.fetch("HOST", default_host) + + if ENV["HOST"] && !ENV["BINDING"] + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Using the `HOST` environment to specify the IP is deprecated and will be removed in Rails 6.1. + Please use `BINDING` environment instead. + MSG + + return ENV["HOST"] + end + + ENV.fetch("BINDING", default_host) end end @@ -283,10 +293,10 @@ module Rails Run `rails server --help` for more options. MSG else - suggestions = Rails::Command::Spellchecker.suggest(server, from: RACK_SERVERS).map(&:inspect) + suggestions = Rails::Command::Spellchecker.suggest(server, from: RACK_SERVERS) <<~MSG - Could not find server "#{server}". Maybe you meant #{suggestions.first} or #{suggestions.second}? + Could not find server "#{server}". Maybe you meant #{suggestions.inspect}? Run `rails server --help` for more options. MSG end diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index 7248fbbc94..f8460bd4ee 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -276,12 +276,11 @@ module Rails klass.start(args, config) else options = sorted_groups.flat_map(&:last) - suggestions = Rails::Command::Spellchecker.suggest(namespace.to_s, from: options, count: 3) - suggestions.map! { |s| "'#{s}'" } - msg = "Could not find generator '#{namespace}'. ".dup - msg << "Maybe you meant #{ suggestions[0...-1].join(', ')} or #{suggestions[-1]}\n" - msg << "Run `rails generate --help` for more options." - puts msg + suggestion = Rails::Command::Spellchecker.suggest(namespace.to_s, from: options) + puts <<~MSG + Could not find generator '#{namespace}'. Maybe you meant #{suggestion.inspect}? + Run `rails generate --help` for more options. + MSG end end diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 8c5d872573..f51542f3ec 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -440,7 +440,7 @@ module Rails end def depend_on_bootsnap? - !options[:skip_bootsnap] && !options[:dev] + !options[:skip_bootsnap] && !options[:dev] && !defined?(JRUBY_VERSION) end def os_supports_listen_out_of_the_box? diff --git a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb index e2ea66415f..997602cb8c 100644 --- a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb +++ b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb @@ -35,7 +35,7 @@ module Erb # :nodoc: end def file_name - @_file_name ||= super.gsub(/_mailer/i, "") + @_file_name ||= super.sub(/_mailer\z/i, "") end end end diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt index 89263070ef..5e7455cdc7 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt @@ -69,7 +69,7 @@ end <%- if depends_on_system_test? -%> group :test do # Adds support for Capybara system testing and selenium driver - gem 'capybara', '>= 2.15', '< 4.0' + gem 'capybara', '>= 2.15' gem 'selenium-webdriver' # Easy installation and use of chromedriver to run system tests with Chrome gem 'chromedriver-helper' 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 a87649b50f..3807c8a9aa 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 @@ -28,7 +28,7 @@ Rails.application.configure do end <%- unless skip_active_storage? -%> - # Store uploaded files on the local file system (see config/storage.yml for options) + # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local <%- end -%> <%- unless options.skip_action_mailer? -%> @@ -60,7 +60,7 @@ Rails.application.configure do config.assets.quiet = true <%- end -%> - # Raises error for missing translations + # Raises error for missing translations. # config.action_view.raise_on_missing_translations = true # Use an evented file watcher to asynchronously detect changes in source code, 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 926326b5bb..d646694477 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 @@ -43,12 +43,12 @@ Rails.application.configure do # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX <%- unless skip_active_storage? -%> - # Store uploaded files on the local file system (see config/storage.yml for options) + # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local <%- end -%> <%- unless options[:skip_action_cable] -%> - # Mount Action Cable outside main process or domain + # 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.*/ ] @@ -67,7 +67,7 @@ Rails.application.configure do # Use a different cache store in production. # config.cache_store = :mem_cache_store - # Use a real queuing backend for Active Job (and separate queues per environment) + # Use a real queuing backend for Active Job (and separate queues per environment). # config.active_job.queue_adapter = :resque # config.active_job.queue_name_prefix = "<%= app_name %>_#{Rails.env}" 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 ff4c89219a..82f2a8aebe 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 @@ -29,7 +29,7 @@ Rails.application.configure do config.action_controller.allow_forgery_protection = false <%- unless skip_active_storage? -%> - # Store uploaded files on the local file system in a temporary directory + # Store uploaded files on the local file system in a temporary directory. config.active_storage.service = :test <%- end -%> @@ -45,6 +45,9 @@ Rails.application.configure do # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr - # Raises error for missing translations + # Raises error for missing translations. # config.action_view.raise_on_missing_translations = true + + # Prevent expensive template finalization at end of test suite runs. + config.action_view.finalize_compiled_template_methods = false end diff --git a/railties/lib/rails/generators/rails/app/templates/gitignore.tt b/railties/lib/rails/generators/rails/app/templates/gitignore.tt index 2cd8335aba..4e114fb1d9 100644 --- a/railties/lib/rails/generators/rails/app/templates/gitignore.tt +++ b/railties/lib/rails/generators/rails/app/templates/gitignore.tt @@ -24,8 +24,11 @@ <% unless skip_active_storage? -%> # Ignore uploaded files in development /storage/* - +<% if keeps? -%> +!/storage/.keep <% end -%> +<% end -%> + <% unless options.skip_yarn? -%> /node_modules /yarn-error.log diff --git a/railties/lib/rails/generators/rails/app/templates/ruby-version.tt b/railties/lib/rails/generators/rails/app/templates/ruby-version.tt index c444f33b0f..19f0d7f202 100644 --- a/railties/lib/rails/generators/rails/app/templates/ruby-version.tt +++ b/railties/lib/rails/generators/rails/app/templates/ruby-version.tt @@ -1 +1 @@ -<%= RUBY_VERSION -%> +<%= "#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}" -%> diff --git a/railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb b/railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb index 90068c678d..e2359e9ded 100644 --- a/railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb +++ b/railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb @@ -27,6 +27,7 @@ module Rails def add_key_file_silently(key_path, key = nil) create_file key_path, key || ActiveSupport::EncryptedFile.generate_key + key_path.chmod 0600 end def ignore_key_file(key_path, ignore: key_ignore(key_path)) diff --git a/railties/lib/rails/generators/test_unit/job/job_generator.rb b/railties/lib/rails/generators/test_unit/job/job_generator.rb index a99ce914c0..1dae3cb6a5 100644 --- a/railties/lib/rails/generators/test_unit/job/job_generator.rb +++ b/railties/lib/rails/generators/test_unit/job/job_generator.rb @@ -10,6 +10,11 @@ module TestUnit # :nodoc: def create_test_file template "unit_test.rb", File.join("test/jobs", class_path, "#{file_name}_job_test.rb") end + + private + def file_name + @_file_name ||= super.sub(/_job\z/i, "") + end end end end diff --git a/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb index 610d47a729..ab8331f31c 100644 --- a/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb +++ b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb @@ -21,7 +21,7 @@ module TestUnit # :nodoc: private def file_name - @_file_name ||= super.gsub(/_mailer/i, "") + @_file_name ||= super.sub(/_mailer\z/i, "") end end end diff --git a/railties/lib/rails/rack/logger.rb b/railties/lib/rails/rack/logger.rb index ec5212ee76..4ea7e40319 100644 --- a/railties/lib/rails/rack/logger.rb +++ b/railties/lib/rails/rack/logger.rb @@ -35,9 +35,9 @@ module Rails instrumenter = ActiveSupport::Notifications.instrumenter instrumenter.start "request.action_dispatch", request: request logger.info { started_request_message(request) } - resp = @app.call(env) - resp[2] = ::Rack::BodyProxy.new(resp[2]) { finish(request) } - resp + status, headers, body = @app.call(env) + body = ::Rack::BodyProxy.new(body) { finish(request) } + [status, headers, body] rescue Exception finish(request) raise diff --git a/railties/lib/rails/ruby_version_check.rb b/railties/lib/rails/ruby_version_check.rb index f8d3311156..b2d44d9b8e 100644 --- a/railties/lib/rails/ruby_version_check.rb +++ b/railties/lib/rails/ruby_version_check.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -if RUBY_VERSION < "2.4.1" && RUBY_ENGINE == "ruby" +if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.4.1") && RUBY_ENGINE == "ruby" desc = defined?(RUBY_DESCRIPTION) ? RUBY_DESCRIPTION : "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE})" abort <<-end_message diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb index 1db6c98537..7257aaeaae 100644 --- a/railties/lib/rails/source_annotation_extractor.rb +++ b/railties/lib/rails/source_annotation_extractor.rb @@ -1,141 +1,150 @@ # frozen_string_literal: true -# Implements the logic behind the rake tasks for annotations like -# -# rails notes -# rails notes:optimize -# -# and friends. See <tt>rails -T notes</tt> and <tt>railties/lib/rails/tasks/annotations.rake</tt>. -# -# Annotation objects are triplets <tt>:line</tt>, <tt>:tag</tt>, <tt>:text</tt> that -# represent the line where the annotation lives, its tag, and its text. Note -# the filename is not stored. -# -# Annotations are looked for in comments and modulus whitespace they have to -# start with the tag optionally followed by a colon. Everything up to the end -# of the line (or closing ERB comment tag) is considered to be their text. -class SourceAnnotationExtractor - Annotation = Struct.new(:line, :tag, :text) do - def self.directories - @@directories ||= %w(app config db lib test) + (ENV["SOURCE_ANNOTATION_DIRECTORIES"] || "").split(",") - end +require "active_support/deprecation" - # Registers additional directories to be included - # SourceAnnotationExtractor::Annotation.register_directories("spec", "another") - def self.register_directories(*dirs) - directories.push(*dirs) - end +# Remove this deprecated class in the next minor version +#:nodoc: +SourceAnnotationExtractor = ActiveSupport::Deprecation::DeprecatedConstantProxy. + new("SourceAnnotationExtractor", "Rails::SourceAnnotationExtractor") - def self.extensions - @@extensions ||= {} - end +module Rails + # Implements the logic behind the rake tasks for annotations like + # + # rails notes + # rails notes:optimize + # + # and friends. See <tt>rails -T notes</tt> and <tt>railties/lib/rails/tasks/annotations.rake</tt>. + # + # Annotation objects are triplets <tt>:line</tt>, <tt>:tag</tt>, <tt>:text</tt> that + # represent the line where the annotation lives, its tag, and its text. Note + # the filename is not stored. + # + # Annotations are looked for in comments and modulus whitespace they have to + # start with the tag optionally followed by a colon. Everything up to the end + # of the line (or closing ERB comment tag) is considered to be their text. + class SourceAnnotationExtractor + class Annotation < Struct.new(:line, :tag, :text) + def self.directories + @@directories ||= %w(app config db lib test) + (ENV["SOURCE_ANNOTATION_DIRECTORIES"] || "").split(",") + end - # Registers new Annotations File Extensions - # SourceAnnotationExtractor::Annotation.register_extensions("css", "scss", "sass", "less", "js") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ } - def self.register_extensions(*exts, &block) - extensions[/\.(#{exts.join("|")})$/] = block - end + # Registers additional directories to be included + # Rails::SourceAnnotationExtractor::Annotation.register_directories("spec", "another") + def self.register_directories(*dirs) + directories.push(*dirs) + end - register_extensions("builder", "rb", "rake", "yml", "yaml", "ruby") { |tag| /#\s*(#{tag}):?\s*(.*)$/ } - register_extensions("css", "js") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ } - register_extensions("erb") { |tag| /<%\s*#\s*(#{tag}):?\s*(.*?)\s*%>/ } + def self.extensions + @@extensions ||= {} + end - # Returns a representation of the annotation that looks like this: + # Registers new Annotations File Extensions + # Rails::SourceAnnotationExtractor::Annotation.register_extensions("css", "scss", "sass", "less", "js") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ } + def self.register_extensions(*exts, &block) + extensions[/\.(#{exts.join("|")})$/] = block + end + + register_extensions("builder", "rb", "rake", "yml", "yaml", "ruby") { |tag| /#\s*(#{tag}):?\s*(.*)$/ } + register_extensions("css", "js") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ } + register_extensions("erb") { |tag| /<%\s*#\s*(#{tag}):?\s*(.*?)\s*%>/ } + + # Returns a representation of the annotation that looks like this: + # + # [126] [TODO] This algorithm is simple and clearly correct, make it faster. + # + # If +options+ has a flag <tt>:tag</tt> the tag is shown as in the example above. + # Otherwise the string contains just line and text. + def to_s(options = {}) + s = "[#{line.to_s.rjust(options[:indent])}] ".dup + s << "[#{tag}] " if options[:tag] + s << text + end + end + + # Prints all annotations with tag +tag+ under the root directories +app+, + # +config+, +db+, +lib+, and +test+ (recursively). # - # [126] [TODO] This algorithm is simple and clearly correct, make it faster. + # Additional directories may be added using a comma-delimited list set using + # <tt>ENV['SOURCE_ANNOTATION_DIRECTORIES']</tt>. # - # If +options+ has a flag <tt>:tag</tt> the tag is shown as in the example above. - # Otherwise the string contains just line and text. - def to_s(options = {}) - s = "[#{line.to_s.rjust(options[:indent])}] ".dup - s << "[#{tag}] " if options[:tag] - s << text + # Directories may also be explicitly set using the <tt>:dirs</tt> key in +options+. + # + # Rails::SourceAnnotationExtractor.enumerate 'TODO|FIXME', dirs: %w(app lib), tag: true + # + # If +options+ has a <tt>:tag</tt> flag, it will be passed to each annotation's +to_s+. + # + # See <tt>#find_in</tt> for a list of file extensions that will be taken into account. + # + # This class method is the single entry point for the rake tasks. + def self.enumerate(tag, options = {}) + extractor = new(tag) + dirs = options.delete(:dirs) || Annotation.directories + extractor.display(extractor.find(dirs), options) end - end - # Prints all annotations with tag +tag+ under the root directories +app+, - # +config+, +db+, +lib+, and +test+ (recursively). - # - # Additional directories may be added using a comma-delimited list set using - # <tt>ENV['SOURCE_ANNOTATION_DIRECTORIES']</tt>. - # - # Directories may also be explicitly set using the <tt>:dirs</tt> key in +options+. - # - # SourceAnnotationExtractor.enumerate 'TODO|FIXME', dirs: %w(app lib), tag: true - # - # If +options+ has a <tt>:tag</tt> flag, it will be passed to each annotation's +to_s+. - # - # See <tt>#find_in</tt> for a list of file extensions that will be taken into account. - # - # This class method is the single entry point for the rake tasks. - def self.enumerate(tag, options = {}) - extractor = new(tag) - dirs = options.delete(:dirs) || Annotation.directories - extractor.display(extractor.find(dirs), options) - end - - attr_reader :tag - - def initialize(tag) - @tag = tag - end + attr_reader :tag - # Returns a hash that maps filenames under +dirs+ (recursively) to arrays - # with their annotations. - def find(dirs) - dirs.inject({}) { |h, dir| h.update(find_in(dir)) } - end + def initialize(tag) + @tag = tag + end - # Returns a hash that maps filenames under +dir+ (recursively) to arrays - # with their annotations. Only files with annotations are included. Files - # with extension +.builder+, +.rb+, +.rake+, +.yml+, +.yaml+, +.ruby+, - # +.css+, +.js+ and +.erb+ are taken into account. - def find_in(dir) - results = {} - - Dir.glob("#{dir}/*") do |item| - next if File.basename(item)[0] == ?. - - if File.directory?(item) - results.update(find_in(item)) - else - extension = Annotation.extensions.detect do |regexp, _block| - regexp.match(item) - end + # Returns a hash that maps filenames under +dirs+ (recursively) to arrays + # with their annotations. + def find(dirs) + dirs.inject({}) { |h, dir| h.update(find_in(dir)) } + end - if extension - pattern = extension.last.call(tag) - results.update(extract_annotations_from(item, pattern)) if pattern + # Returns a hash that maps filenames under +dir+ (recursively) to arrays + # with their annotations. Files with extensions registered in + # <tt>Rails::SourceAnnotationExtractor::Annotation.extensions</tt> are + # taken into account. Only files with annotations are included. + def find_in(dir) + results = {} + + Dir.glob("#{dir}/*") do |item| + next if File.basename(item)[0] == ?. + + if File.directory?(item) + results.update(find_in(item)) + else + extension = Annotation.extensions.detect do |regexp, _block| + regexp.match(item) + end + + if extension + pattern = extension.last.call(tag) + results.update(extract_annotations_from(item, pattern)) if pattern + end end end - end - results - end + results + end - # If +file+ is the filename of a file that contains annotations this method returns - # a hash with a single entry that maps +file+ to an array of its annotations. - # Otherwise it returns an empty hash. - def extract_annotations_from(file, pattern) - lineno = 0 - result = File.readlines(file, encoding: Encoding::BINARY).inject([]) do |list, line| - lineno += 1 - next list unless line =~ pattern - list << Annotation.new(lineno, $1, $2) + # If +file+ is the filename of a file that contains annotations this method returns + # a hash with a single entry that maps +file+ to an array of its annotations. + # Otherwise it returns an empty hash. + def extract_annotations_from(file, pattern) + lineno = 0 + result = File.readlines(file, encoding: Encoding::BINARY).inject([]) do |list, line| + lineno += 1 + next list unless line =~ pattern + list << Annotation.new(lineno, $1, $2) + end + result.empty? ? {} : { file => result } end - result.empty? ? {} : { file => result } - end - # Prints the mapping from filenames to annotations in +results+ ordered by filename. - # The +options+ hash is passed to each annotation's +to_s+. - def display(results, options = {}) - options[:indent] = results.flat_map { |f, a| a.map(&:line) }.max.to_s.size - results.keys.sort.each do |file| - puts "#{file}:" - results[file].each do |note| - puts " * #{note.to_s(options)}" + # Prints the mapping from filenames to annotations in +results+ ordered by filename. + # The +options+ hash is passed to each annotation's +to_s+. + def display(results, options = {}) + options[:indent] = results.flat_map { |f, a| a.map(&:line) }.max.to_s.size + results.keys.sort.each do |file| + puts "#{file}:" + results[file].each do |note| + puts " * #{note.to_s(options)}" + end + puts end - puts end end end diff --git a/railties/lib/rails/tasks/annotations.rake b/railties/lib/rails/tasks/annotations.rake index 93bc66e2db..60bcdc5e1b 100644 --- a/railties/lib/rails/tasks/annotations.rake +++ b/railties/lib/rails/tasks/annotations.rake @@ -4,19 +4,19 @@ require "rails/source_annotation_extractor" desc "Enumerate all annotations (use notes:optimize, :fixme, :todo for focus)" task :notes do - SourceAnnotationExtractor.enumerate "OPTIMIZE|FIXME|TODO", tag: true + Rails::SourceAnnotationExtractor.enumerate "OPTIMIZE|FIXME|TODO", tag: true end namespace :notes do ["OPTIMIZE", "FIXME", "TODO"].each do |annotation| # desc "Enumerate all #{annotation} annotations" task annotation.downcase.intern do - SourceAnnotationExtractor.enumerate annotation + Rails::SourceAnnotationExtractor.enumerate annotation end end desc "Enumerate a custom annotation, specify with ANNOTATION=CUSTOM" task :custom do - SourceAnnotationExtractor.enumerate ENV["ANNOTATION"] + Rails::SourceAnnotationExtractor.enumerate ENV["ANNOTATION"] end end diff --git a/railties/lib/rails/tasks/yarn.rake b/railties/lib/rails/tasks/yarn.rake index 48da7ffc51..10703a1808 100644 --- a/railties/lib/rails/tasks/yarn.rake +++ b/railties/lib/rails/tasks/yarn.rake @@ -3,7 +3,7 @@ namespace :yarn do desc "Install all JavaScript dependencies as specified via Yarn" task :install do - system("./bin/yarn install --no-progress --production") + system("./bin/yarn install --no-progress --frozen-lockfile --production") end end diff --git a/railties/test/app_loader_test.rb b/railties/test/app_loader_test.rb index bb556f1968..c7a6bdee1b 100644 --- a/railties/test/app_loader_test.rb +++ b/railties/test/app_loader_test.rb @@ -40,13 +40,13 @@ class AppLoaderTest < ActiveSupport::TestCase test "is not in a Rails application if #{exe} is not found in the current or parent directories" do def loader.find_executables; end - assert !loader.exec_app + assert_not loader.exec_app end test "is not in a Rails application if #{exe} exists but is a folder" do FileUtils.mkdir_p(exe) - assert !loader.exec_app + assert_not loader.exec_app end ["APP_PATH", "ENGINE_PATH"].each do |keyword| @@ -61,7 +61,7 @@ class AppLoaderTest < ActiveSupport::TestCase test "is not in a Rails application if #{exe} exists but doesn't contain #{keyword}" do write exe - assert !loader.exec_app + assert_not loader.exec_app end test "is in a Rails application if parent directory has #{exe} containing #{keyword} and chdirs to the root directory" do diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb index 0d3262d6f6..9ef123c5b6 100644 --- a/railties/test/application/assets_test.rb +++ b/railties/test/application/assets_test.rb @@ -76,7 +76,7 @@ module ApplicationTests # Load app env app "production" - assert !defined?(Uglifier) + assert_not defined?(Uglifier) get "/assets/demo.js" assert_match "alert()", last_response.body assert defined?(Uglifier) @@ -270,10 +270,10 @@ module ApplicationTests app "production" # Checking if Uglifier is defined we can know if Sprockets was reached or not - assert !defined?(Uglifier) + assert_not defined?(Uglifier) get "/assets/#{asset_path}" assert_match "alert()", last_response.body - assert !defined?(Uglifier) + assert_not defined?(Uglifier) end test "precompile properly refers files referenced with asset_path" do diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index bd9b87467c..84606d3b90 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -361,7 +361,7 @@ module ApplicationTests end RUBY - assert !$prepared + assert_not $prepared app "development" @@ -576,6 +576,7 @@ module ApplicationTests app "development" assert_equal "3b7cd727ee24e8444053437c36cc66c3", app.secrets.secret_key_base + assert_equal "3b7cd727ee24e8444053437c36cc66c3", app.secret_key_base end test "secret_key_base is copied from config to secrets when not set" do @@ -1416,7 +1417,7 @@ module ApplicationTests assert_equal "XML", last_response.body end - test "Rails.application#env_config exists and include some existing parameters" do + test "Rails.application#env_config exists and includes some existing parameters" do make_basic_app assert_equal app.env_config["action_dispatch.parameter_filter"], app.config.filter_parameters @@ -1509,7 +1510,7 @@ module ApplicationTests end end - assert_not_nil SourceAnnotationExtractor::Annotation.extensions[/\.(coffee)$/] + assert_not_nil Rails::SourceAnnotationExtractor::Annotation.extensions[/\.(coffee)$/] end test "rake_tasks block works at instance level" do diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb index e631318f82..1530ea82d6 100644 --- a/railties/test/application/initializers/frameworks_test.rb +++ b/railties/test/application/initializers/frameworks_test.rb @@ -226,7 +226,7 @@ module ApplicationTests rails %w(generate model post title:string) rails %w(db:migrate db:schema:cache:dump db:rollback) require "#{app_path}/config/environment" - assert !ActiveRecord::Base.connection.schema_cache.data_sources("posts") + assert_not ActiveRecord::Base.connection.schema_cache.data_sources("posts") end test "active record establish_connection uses Rails.env if DATABASE_URL is not set" do diff --git a/railties/test/application/middleware/sendfile_test.rb b/railties/test/application/middleware/sendfile_test.rb index 9def3a0ce7..818ad61c64 100644 --- a/railties/test/application/middleware/sendfile_test.rb +++ b/railties/test/application/middleware/sendfile_test.rb @@ -29,7 +29,7 @@ module ApplicationTests simple_controller get "/" - assert !last_response.headers["X-Sendfile"] + assert_not last_response.headers["X-Sendfile"] assert_equal File.read(__FILE__), last_response.body end diff --git a/railties/test/application/middleware/session_test.rb b/railties/test/application/middleware/session_test.rb index a17988235a..9182a63ab7 100644 --- a/railties/test/application/middleware/session_test.rb +++ b/railties/test/application/middleware/session_test.rb @@ -31,7 +31,7 @@ module ApplicationTests add_to_config "config.force_ssl = true" add_to_config "config.ssl_options = { secure_cookies: false }" require "#{app_path}/config/environment" - assert !app.config.session_options[:secure] + assert_not app.config.session_options[:secure] end test "session is not loaded if it's not used" do @@ -51,7 +51,7 @@ module ApplicationTests get "/" assert last_request.env["HTTP_COOKIE"] - assert !last_response.headers["Set-Cookie"] + assert_not last_response.headers["Set-Cookie"] end test "session is empty and isn't saved on unverified request when using :null_session protect method" do diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb index 5b4c42c189..0594236b1f 100644 --- a/railties/test/application/rake/dbs_test.rb +++ b/railties/test/application/rake/dbs_test.rb @@ -34,7 +34,7 @@ module ApplicationTests assert_equal expected_database, ActiveRecord::Base.connection_config[:database] if environment_loaded output = rails("db:drop") assert_match(/Dropped database/, output) - assert !File.exist?(expected_database) + assert_not File.exist?(expected_database) end end diff --git a/railties/test/application/rake/multi_dbs_test.rb b/railties/test/application/rake/multi_dbs_test.rb new file mode 100644 index 0000000000..07d96fcb56 --- /dev/null +++ b/railties/test/application/rake/multi_dbs_test.rb @@ -0,0 +1,164 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" + +module ApplicationTests + module RakeTests + class RakeMultiDbsTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app(multi_db: true) + FileUtils.rm_rf("#{app_path}/config/environments") + end + + def teardown + teardown_app + end + + def db_create_and_drop(namespace, expected_database, environment_loaded: true) + Dir.chdir(app_path) do + output = rails("db:create") + assert_match(/Created database/, output) + assert_match_namespace(namespace, output) + assert File.exist?(expected_database) + + output = rails("db:drop") + assert_match(/Dropped database/, output) + assert_match_namespace(namespace, output) + assert_not File.exist?(expected_database) + end + end + + def db_create_and_drop_namespace(namespace, expected_database, environment_loaded: true) + Dir.chdir(app_path) do + output = rails("db:create:#{namespace}") + assert_match(/Created database/, output) + assert_match_namespace(namespace, output) + assert File.exist?(expected_database) + + output = rails("db:drop:#{namespace}") + assert_match(/Dropped database/, output) + assert_match_namespace(namespace, output) + assert_not File.exist?(expected_database) + end + end + + def assert_match_namespace(namespace, output) + if namespace == "primary" + assert_match(/#{Rails.env}.sqlite3/, output) + else + assert_match(/#{Rails.env}_#{namespace}.sqlite3/, output) + end + end + + def db_migrate_and_schema_dump_and_load(namespace, expected_database, format) + Dir.chdir(app_path) do + rails "generate", "model", "book", "title:string" + rails "generate", "model", "dog", "name:string" + write_models_for_animals + rails "db:migrate", "db:#{format}:dump" + + if format == "schema" + schema_dump = File.read("db/#{format}.rb") + schema_dump_animals = File.read("db/animals_#{format}.rb") + assert_match(/create_table \"books\"/, schema_dump) + assert_match(/create_table \"dogs\"/, schema_dump_animals) + else + schema_dump = File.read("db/#{format}.sql") + schema_dump_animals = File.read("db/animals_#{format}.sql") + assert_match(/CREATE TABLE (?:IF NOT EXISTS )?\"books\"/, schema_dump) + assert_match(/CREATE TABLE (?:IF NOT EXISTS )?\"dogs\"/, schema_dump_animals) + end + + rails "db:#{format}:load" + + ar_tables = lambda { rails("runner", "p ActiveRecord::Base.connection.tables").strip } + animals_tables = lambda { rails("runner", "p AnimalsBase.connection.tables").strip } + + assert_equal '["schema_migrations", "ar_internal_metadata", "books"]', ar_tables[] + assert_equal '["schema_migrations", "ar_internal_metadata", "dogs"]', animals_tables[] + end + end + + def db_migrate_namespaced(namespace, expected_database) + Dir.chdir(app_path) do + rails "generate", "model", "book", "title:string" + rails "generate", "model", "dog", "name:string" + write_models_for_animals + output = rails("db:migrate:#{namespace}") + if namespace == "primary" + assert_match(/CreateBooks: migrated/, output) + else + assert_match(/CreateDogs: migrated/, output) + end + end + end + + def write_models_for_animals + # make a directory for the animals migration + FileUtils.mkdir_p("#{app_path}/db/animals_migrate") + # move the dogs migration if it unless it already lives there + FileUtils.mv(Dir.glob("#{app_path}/db/migrate/**/*dogs.rb").first, "db/animals_migrate/") unless Dir.glob("#{app_path}/db/animals_migrate/**/*dogs.rb").first + # delete the dogs migration if it's still present in the + # migrate folder. This is necessary because sometimes + # the code isn't fast enough and an extra migration gets made + FileUtils.rm(Dir.glob("#{app_path}/db/migrate/**/*dogs.rb").first) if Dir.glob("#{app_path}/db/migrate/**/*dogs.rb").first + + # change the base of the dog model + app_path("/app/models/dog.rb") do |file_name| + file = File.read("#{app_path}/app/models/dog.rb") + file.sub!(/ApplicationRecord/, "AnimalsBase") + File.write(file_name, file) + end + + # create the base model for dog to inherit from + File.open("#{app_path}/app/models/animals_base.rb", "w") do |file| + file.write(<<-EOS +class AnimalsBase < ActiveRecord::Base + self.abstract_class = true + + establish_connection :animals +end +EOS +) + end + end + + test "db:create and db:drop works on all databases for env" do + require "#{app_path}/config/environment" + ActiveRecord::Base.configurations[Rails.env].each do |namespace, config| + db_create_and_drop namespace, config["database"] + end + end + + test "db:create:namespace and db:drop:namespace works on specified databases" do + require "#{app_path}/config/environment" + ActiveRecord::Base.configurations[Rails.env].each do |namespace, config| + db_create_and_drop_namespace namespace, config["database"] + end + end + + test "db:migrate and db:schema:dump and db:schema:load works on all databases" do + require "#{app_path}/config/environment" + ActiveRecord::Base.configurations[Rails.env].each do |namespace, config| + db_migrate_and_schema_dump_and_load namespace, config["database"], "schema" + end + end + + test "db:migrate and db:structure:dump and db:structure:load works on all databases" do + require "#{app_path}/config/environment" + ActiveRecord::Base.configurations[Rails.env].each do |namespace, config| + db_migrate_and_schema_dump_and_load namespace, config["database"], "structure" + end + end + + test "db:migrate:namespace works" do + require "#{app_path}/config/environment" + ActiveRecord::Base.configurations[Rails.env].each do |namespace, config| + db_migrate_namespaced namespace, config["database"] + end + end + end + end +end diff --git a/railties/test/application/rake/notes_test.rb b/railties/test/application/rake/notes_test.rb index 8e9fe9b6b4..d73e5cdfa3 100644 --- a/railties/test/application/rake/notes_test.rb +++ b/railties/test/application/rake/notes_test.rb @@ -101,7 +101,7 @@ module ApplicationTests task :notes_custom do tags = 'TODO|FIXME' opts = { dirs: %w(lib test), tag: true } - SourceAnnotationExtractor.enumerate(tags, opts) + Rails::SourceAnnotationExtractor.enumerate(tags, opts) end EOS diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index 9683230d07..1522a2bbc5 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -41,7 +41,7 @@ module ApplicationTests rails "db:create", "db:migrate" output = rails("db:test:prepare", "test") - refute_match(/ActiveRecord::ProtectedEnvironmentError/, output) + assert_no_match(/ActiveRecord::ProtectedEnvironmentError/, output) end end @@ -229,7 +229,7 @@ module ApplicationTests def test_rake_clear_schema_cache rails "db:schema:cache:dump", "db:schema:cache:clear" - assert !File.exist?(File.join(app_path, "db", "schema_cache.yml")) + assert_not File.exist?(File.join(app_path, "db", "schema_cache.yml")) end def test_copy_templates diff --git a/railties/test/application/rendering_test.rb b/railties/test/application/rendering_test.rb index 3724886c54..ab1591f388 100644 --- a/railties/test/application/rendering_test.rb +++ b/railties/test/application/rendering_test.rb @@ -4,7 +4,7 @@ require "isolation/abstract_unit" require "rack/test" module ApplicationTests - class RoutingTest < ActiveSupport::TestCase + class RenderingTest < ActiveSupport::TestCase include ActiveSupport::Testing::Isolation include Rack::Test::Methods diff --git a/railties/test/command/spellchecker_test.rb b/railties/test/command/spellchecker_test.rb index aff50a3e73..e6a7a3957c 100644 --- a/railties/test/command/spellchecker_test.rb +++ b/railties/test/command/spellchecker_test.rb @@ -5,6 +5,6 @@ require "rails/command/spellchecker" class Rails::Command::SpellcheckerTest < ActiveSupport::TestCase test "suggests a word correction from dictionary" do - assert_equal %w(thin cgi puma), Rails::Command::Spellchecker.suggest("tin", from: %w(puma thin cgi)) + assert_equal "thin", Rails::Command::Spellchecker.suggest("tin", from: %w(puma thin cgi)) end end diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb index 6ad96b28c7..0aea21051a 100644 --- a/railties/test/commands/dbconsole_test.rb +++ b/railties/test/commands/dbconsole_test.rb @@ -123,31 +123,31 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase def test_mysql start(adapter: "mysql2", database: "db") - assert !aborted + assert_not aborted assert_equal [%w[mysql mysql5], "db"], dbconsole.find_cmd_and_exec_args end def test_mysql_full start(adapter: "mysql2", database: "db", host: "locahost", port: 1234, socket: "socket", username: "user", password: "qwerty", encoding: "UTF-8") - assert !aborted + assert_not aborted assert_equal [%w[mysql mysql5], "--host=locahost", "--port=1234", "--socket=socket", "--user=user", "--default-character-set=UTF-8", "-p", "db"], dbconsole.find_cmd_and_exec_args end def test_mysql_include_password start({ adapter: "mysql2", database: "db", username: "user", password: "qwerty" }, ["-p"]) - assert !aborted + assert_not aborted assert_equal [%w[mysql mysql5], "--user=user", "--password=qwerty", "db"], dbconsole.find_cmd_and_exec_args end def test_postgresql start(adapter: "postgresql", database: "db") - assert !aborted + assert_not aborted assert_equal ["psql", "db"], dbconsole.find_cmd_and_exec_args end def test_postgresql_full start(adapter: "postgresql", database: "db", username: "user", password: "q1w2e3", host: "host", port: 5432) - assert !aborted + assert_not aborted assert_equal ["psql", "db"], dbconsole.find_cmd_and_exec_args assert_equal "user", ENV["PGUSER"] assert_equal "host", ENV["PGHOST"] @@ -157,7 +157,7 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase def test_postgresql_include_password start({ adapter: "postgresql", database: "db", username: "user", password: "q1w2e3" }, ["-p"]) - assert !aborted + assert_not aborted assert_equal ["psql", "db"], dbconsole.find_cmd_and_exec_args assert_equal "user", ENV["PGUSER"] assert_equal "q1w2e3", ENV["PGPASSWORD"] @@ -165,13 +165,13 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase def test_sqlite3 start(adapter: "sqlite3", database: "db.sqlite3") - assert !aborted + assert_not aborted assert_equal ["sqlite3", Rails.root.join("db.sqlite3").to_s], dbconsole.find_cmd_and_exec_args end def test_sqlite3_mode start({ adapter: "sqlite3", database: "db.sqlite3" }, ["--mode", "html"]) - assert !aborted + assert_not aborted assert_equal ["sqlite3", "-html", Rails.root.join("db.sqlite3").to_s], dbconsole.find_cmd_and_exec_args end @@ -182,27 +182,27 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase def test_sqlite3_db_absolute_path start(adapter: "sqlite3", database: "/tmp/db.sqlite3") - assert !aborted + assert_not aborted assert_equal ["sqlite3", "/tmp/db.sqlite3"], dbconsole.find_cmd_and_exec_args end def test_sqlite3_db_without_defined_rails_root Rails.stub(:respond_to?, false) do start(adapter: "sqlite3", database: "config/db.sqlite3") - assert !aborted + assert_not aborted assert_equal ["sqlite3", Rails.root.join("../config/db.sqlite3").to_s], dbconsole.find_cmd_and_exec_args end end def test_oracle start(adapter: "oracle", database: "db", username: "user", password: "secret") - assert !aborted + assert_not aborted assert_equal ["sqlplus", "user@db"], dbconsole.find_cmd_and_exec_args end def test_oracle_include_password start({ adapter: "oracle", database: "db", username: "user", password: "secret" }, ["-p"]) - assert !aborted + assert_not aborted assert_equal ["sqlplus", "user/secret@db"], dbconsole.find_cmd_and_exec_args end diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb index 467afe7cea..e7a56b3e6d 100644 --- a/railties/test/commands/server_test.rb +++ b/railties/test/commands/server_test.rb @@ -29,7 +29,7 @@ class Rails::Command::ServerCommandTest < ActiveSupport::TestCase end def test_using_server_mistype - assert_match(/Could not find server "tin". Maybe you meant "thin" or "cgi"/, run_command("--using", "tin")) + assert_match(/Could not find server "tin". Maybe you meant "thin"?/, run_command("--using", "tin")) end def test_using_positional_argument_deprecation @@ -90,6 +90,15 @@ class Rails::Command::ServerCommandTest < ActiveSupport::TestCase def test_environment_with_host switch_env "HOST", "1.2.3.4" do + assert_deprecated do + options = parse_arguments + assert_equal "1.2.3.4", options[:Host] + end + end + end + + def test_environment_with_binding + switch_env "BINDING", "1.2.3.4" do options = parse_arguments assert_equal "1.2.3.4", options[:Host] end @@ -196,7 +205,7 @@ class Rails::Command::ServerCommandTest < ActiveSupport::TestCase assert_equal 3000, options[:Port] end - switch_env "HOST", "1.2.3.4" do + switch_env "BINDING", "1.2.3.4" do args = ["-b", "127.0.0.1"] options = parse_arguments(args) assert_equal "127.0.0.1", options[:Host] @@ -215,6 +224,11 @@ class Rails::Command::ServerCommandTest < ActiveSupport::TestCase server_options = parse_arguments(["--port=3001"]) assert_equal [:Port], server_options[:user_supplied_options] + + switch_env "BINDING", "1.2.3.4" do + server_options = parse_arguments + assert_equal [:Host], server_options[:user_supplied_options] + end end def test_default_options diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 294fdcd6a1..cd607ca8d8 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -816,9 +816,18 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_bootsnap run_generator - assert_gem "bootsnap" - assert_file "config/boot.rb" do |content| - assert_match(/require 'bootsnap\/setup'/, content) + unless defined?(JRUBY_VERSION) + assert_gem "bootsnap" + assert_file "config/boot.rb" do |content| + assert_match(/require 'bootsnap\/setup'/, content) + end + else + assert_file "Gemfile" do |content| + assert_no_match(/bootsnap/, content) + end + assert_file "config/boot.rb" do |content| + assert_no_match(/require 'bootsnap\/setup'/, content) + end end end @@ -851,7 +860,7 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_match(/ruby '#{RUBY_VERSION}'/, content) end assert_file ".ruby-version" do |content| - assert_match(/#{RUBY_VERSION}/, content) + assert_match(/#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}/, content) end end @@ -941,6 +950,15 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_directory("test/system") end + unless Gem.win_platform? + def test_master_key_is_only_readable_by_the_owner + run_generator + + stat = File.stat("config/master.key") + assert_equal "100600", sprintf("%o", stat.mode) + end + end + private def stub_rails_application(root) Rails.application.config.root = root diff --git a/railties/test/generators/channel_generator_test.rb b/railties/test/generators/channel_generator_test.rb index e238197eba..e543cc11b8 100644 --- a/railties/test/generators/channel_generator_test.rb +++ b/railties/test/generators/channel_generator_test.rb @@ -76,4 +76,14 @@ class ChannelGeneratorTest < Rails::Generators::TestCase assert_file "app/channels/application_cable/connection.rb" assert_file "app/assets/javascripts/cable.js" end + + def test_channel_suffix_is_not_duplicated + run_generator ["chat_channel"] + + assert_no_file "app/channels/chat_channel_channel.rb" + assert_file "app/channels/chat_channel.rb" + + assert_no_file "app/assets/javascripts/channels/chat_channel.js" + assert_file "app/assets/javascripts/channels/chat.js" + end end diff --git a/railties/test/generators/job_generator_test.rb b/railties/test/generators/job_generator_test.rb index 13276fac65..234ba6dad7 100644 --- a/railties/test/generators/job_generator_test.rb +++ b/railties/test/generators/job_generator_test.rb @@ -35,4 +35,14 @@ class JobGeneratorTest < Rails::Generators::TestCase assert_match(/class ApplicationJob < ActiveJob::Base/, job) end end + + def test_job_suffix_is_not_duplicated + run_generator ["notifier_job"] + + assert_no_file "app/jobs/notifier_job_job.rb" + assert_file "app/jobs/notifier_job.rb" + + assert_no_file "test/jobs/notifier_job_job_test.rb" + assert_file "test/jobs/notifier_job_test.rb" + end end diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb index 9f1f2a2289..a16a2d3f0a 100644 --- a/railties/test/generators_test.rb +++ b/railties/test/generators_test.rb @@ -33,7 +33,7 @@ class GeneratorsTest < Rails::Generators::TestCase def test_generator_suggestions name = :migrationz output = capture(:stdout) { Rails::Generators.invoke name } - assert_match "Maybe you meant 'migration'", output + assert_match 'Maybe you meant "migration"?', output end def test_generator_suggestions_except_en_locale @@ -43,7 +43,7 @@ class GeneratorsTest < Rails::Generators::TestCase I18n.default_locale = :ja name = :tas output = capture(:stdout) { Rails::Generators.invoke name } - assert_match "Maybe you meant 'task', 'job' or", output + assert_match 'Maybe you meant "task"?', output ensure I18n.available_locales = orig_available_locales I18n.default_locale = orig_default_locale @@ -52,7 +52,7 @@ class GeneratorsTest < Rails::Generators::TestCase def test_generator_multiple_suggestions name = :tas output = capture(:stdout) { Rails::Generators.invoke name } - assert_match "Maybe you meant 'task', 'job' or", output + assert_match 'Maybe you meant "task"?', output end def test_help_when_a_generator_with_required_arguments_is_invoked_without_arguments diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 0a4d2a9167..3cde7c03b0 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -112,22 +112,57 @@ module TestHelpers end end - File.open("#{app_path}/config/database.yml", "w") do |f| - f.puts <<-YAML - default: &default - adapter: sqlite3 - pool: 5 - timeout: 5000 - development: - <<: *default - database: db/development.sqlite3 - test: - <<: *default - database: db/test.sqlite3 - production: - <<: *default - database: db/production.sqlite3 - YAML + if options[:multi_db] + File.open("#{app_path}/config/database.yml", "w") do |f| + f.puts <<-YAML + default: &default + adapter: sqlite3 + pool: 5 + timeout: 5000 + development: + primary: + <<: *default + database: db/development.sqlite3 + animals: + <<: *default + database: db/development_animals.sqlite3 + migrations_paths: db/animals_migrate + test: + primary: + <<: *default + database: db/test.sqlite3 + animals: + <<: *default + database: db/test_animals.sqlite3 + migrations_paths: db/animals_migrate + production: + primary: + <<: *default + database: db/production.sqlite3 + animals: + <<: *default + database: db/production_animals.sqlite3 + migrations_paths: db/animals_migrate + YAML + end + else + File.open("#{app_path}/config/database.yml", "w") do |f| + f.puts <<-YAML + default: &default + adapter: sqlite3 + pool: 5 + timeout: 5000 + development: + <<: *default + database: db/development.sqlite3 + test: + <<: *default + database: db/test.sqlite3 + production: + <<: *default + database: db/production.sqlite3 + YAML + end end add_to_config <<-RUBY diff --git a/railties/test/rack_logger_test.rb b/railties/test/rack_logger_test.rb index e47f30d5b6..6e8f333e1d 100644 --- a/railties/test/rack_logger_test.rb +++ b/railties/test/rack_logger_test.rb @@ -14,14 +14,21 @@ module Rails attr_reader :logger - def initialize(logger = NULL, taggers = nil, &block) - super(->(_) { block.call; [200, {}, []] }, taggers) + def initialize(logger = NULL, app: nil, taggers: nil, &block) + app ||= ->(_) { block.call; [200, {}, []] } + super(app, taggers) @logger = logger end def development?; false; end end + class TestApp < Struct.new(:response) + def call(_env) + response + end + end + Subscriber = Struct.new(:starts, :finishes) do def initialize(starts = [], finishes = []) super @@ -72,6 +79,17 @@ module Rails end end end + + def test_logger_does_not_mutate_app_return + response = [].freeze + app = TestApp.new(response) + logger = TestLogger.new(app: app) + assert_no_changes("response") do + assert_nothing_raised do + logger.call("REQUEST_METHOD" => "GET") + end + end + end end end end diff --git a/railties/test/rails_info_test.rb b/railties/test/rails_info_test.rb index 43b60b9144..50522c1be6 100644 --- a/railties/test/rails_info_test.rb +++ b/railties/test/rails_info_test.rb @@ -9,7 +9,7 @@ class InfoTest < ActiveSupport::TestCase property("Bogus") { raise } end end - assert !property_defined?("Bogus") + assert_not property_defined?("Bogus") end def test_property_with_string diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index a59c63f343..9a3ddc8d5e 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -226,7 +226,7 @@ module RailtiesTest require "rdoc/task" require "rake/testtask" Rails.application.load_tasks - assert !Rake::Task.task_defined?("bukkits:install:migrations") + assert_not Rake::Task.task_defined?("bukkits:install:migrations") end test "puts its lib directory on load path" do @@ -745,7 +745,7 @@ YAML assert_equal "bukkits", Bukkits::Engine.engine_name assert_equal Bukkits.railtie_namespace, Bukkits::Engine assert ::Bukkits::MyMailer.method_defined?(:foo_url) - assert !::Bukkits::MyMailer.method_defined?(:bar_url) + assert_not ::Bukkits::MyMailer.method_defined?(:bar_url) get("/bukkits/from_app") assert_equal "false", last_response.body diff --git a/railties/test/railties/railtie_test.rb b/railties/test/railties/railtie_test.rb index 359ab0fdae..7c3d1e3759 100644 --- a/railties/test/railties/railtie_test.rb +++ b/railties/test/railties/railtie_test.rb @@ -65,7 +65,7 @@ module RailtiesTest test "railtie can add to_prepare callbacks" do $to_prepare = false class Foo < Rails::Railtie ; config.to_prepare { $to_prepare = true } ; end - assert !$to_prepare + assert_not $to_prepare require "#{app_path}/config/environment" require "rack/test" extend Rack::Test::Methods @@ -91,7 +91,7 @@ module RailtiesTest test "railtie can add after_initialize callbacks" do $after_initialize = false class Foo < Rails::Railtie ; config.after_initialize { $after_initialize = true } ; end - assert !$after_initialize + assert_not $after_initialize require "#{app_path}/config/environment" assert $after_initialize end @@ -107,7 +107,7 @@ module RailtiesTest require "#{app_path}/config/environment" - assert !$ran_block + assert_not $ran_block require "rake" require "rake/testtask" require "rdoc/task" @@ -151,7 +151,7 @@ module RailtiesTest require "#{app_path}/config/environment" - assert !$ran_block + assert_not $ran_block Rails.application.load_generators assert $ran_block end @@ -167,7 +167,7 @@ module RailtiesTest require "#{app_path}/config/environment" - assert !$ran_block + assert_not $ran_block Rails.application.load_console assert $ran_block end @@ -183,7 +183,7 @@ module RailtiesTest require "#{app_path}/config/environment" - assert !$ran_block + assert_not $ran_block Rails.application.load_runner assert $ran_block end @@ -197,7 +197,7 @@ module RailtiesTest end end - assert !$ran_block + assert_not $ran_block require "#{app_path}/config/environment" assert $ran_block end |