diff options
73 files changed, 863 insertions, 156 deletions
diff --git a/actioncable/lib/action_cable/subscription_adapter/redis.rb b/actioncable/lib/action_cable/subscription_adapter/redis.rb index ad8fa52760..b4a9385c87 100644 --- a/actioncable/lib/action_cable/subscription_adapter/redis.rb +++ b/actioncable/lib/action_cable/subscription_adapter/redis.rb @@ -5,6 +5,8 @@ require "thread" gem "redis", ">= 3", "< 5" require "redis" +require "active_support/core_ext/hash/except" + module ActionCable module SubscriptionAdapter class Redis < Base # :nodoc: @@ -14,7 +16,7 @@ module ActionCable # This is needed, for example, when using Makara proxies for distributed Redis. cattr_accessor :redis_connector, default: ->(config) do config[:id] ||= "ActionCable-PID-#{$$}" - ::Redis.new(config.slice(:url, :host, :port, :db, :password, :id)) + ::Redis.new(config.except(:adapter, :channel_prefix)) end def initialize(*) diff --git a/actioncable/test/subscription_adapter/redis_test.rb b/actioncable/test/subscription_adapter/redis_test.rb index 35840a4036..d4eb89719f 100644 --- a/actioncable/test/subscription_adapter/redis_test.rb +++ b/actioncable/test/subscription_adapter/redis_test.rb @@ -34,11 +34,11 @@ class RedisAdapterTest::AlternateConfiguration < RedisAdapterTest end class RedisAdapterTest::Connector < ActionCable::TestCase - test "slices url, host, port, db, password and id from config" do + test "excludes adapter and channel prefix" do config = { url: 1, host: 2, port: 3, db: 4, password: 5, id: "Some custom ID" } assert_called_with ::Redis, :new, [ config ] do - connect config.merge(other: "unrelated", stuff: "here") + connect config.merge(adapter: "redis", channel_prefix: "custom") end end diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 79f6320a04..4502c6a2f9 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,21 @@ +* Make system tests take a failed screenshot in a `before_teardown` hook + rather than an `after_teardown` hook. + + This helps minimize the time gap between when an assertion fails and when + the screenshot is taken (reducing the time in which the page could have + been dynamically updated after the assertion failed). + + *Richard Macklin* + +* Introduce `ActionDispatch::ActionableExceptions`. + + The `ActionDispatch::ActionableExceptions` middleware dispatches actions + from `ActiveSupport::ActionableError` descendants. + + Actionable errors let's you dispatch actions from Rails' error pages. + + *Vipul A M*, *Yao Jie*, *Genadi Samokovarov* + * Raise an `ArgumentError` if a resource custom param contains a colon (`:`). After this change it's not possible anymore to configure routes like this: diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 8f39b88d56..6a4ba9af4a 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -53,6 +53,7 @@ module ActionDispatch autoload :RequestId autoload :Callbacks autoload :Cookies + autoload :ActionableExceptions autoload :DebugExceptions autoload :DebugLocks autoload :DebugView diff --git a/actionpack/lib/action_dispatch/middleware/actionable_exceptions.rb b/actionpack/lib/action_dispatch/middleware/actionable_exceptions.rb new file mode 100644 index 0000000000..e94cc46603 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/actionable_exceptions.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "erb" +require "action_dispatch/http/request" +require "active_support/actionable_error" + +module ActionDispatch + class ActionableExceptions # :nodoc: + cattr_accessor :endpoint, default: "/rails/actions" + + def initialize(app) + @app = app + end + + def call(env) + request = ActionDispatch::Request.new(env) + return @app.call(env) unless actionable_request?(request) + + ActiveSupport::ActionableError.dispatch(request.params[:error].to_s.safe_constantize, request.params[:action]) + + redirect_to request.params[:location] + end + + private + def actionable_request?(request) + request.show_exceptions? && request.post? && request.path == endpoint + end + + def redirect_to(location) + body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(location)}\">redirected</a>.</body></html>" + + [302, { + "Content-Type" => "text/html; charset=#{Response.default_charset}", + "Content-Length" => body.bytesize.to_s, + "Location" => location, + }, [body]] + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index 59113e13f4..0b15c94122 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -4,6 +4,8 @@ require "action_dispatch/http/request" require "action_dispatch/middleware/exception_wrapper" require "action_dispatch/routing/inspector" +require "active_support/actionable_error" + require "action_view" require "action_view/base" diff --git a/actionpack/lib/action_dispatch/middleware/debug_view.rb b/actionpack/lib/action_dispatch/middleware/debug_view.rb index 43c0a84504..a03650254e 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_view.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_view.rb @@ -52,5 +52,9 @@ module ActionDispatch super end end + + def protect_against_forgery? + false + end end end diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb new file mode 100644 index 0000000000..b6c6d2f50d --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb @@ -0,0 +1,13 @@ +<% actions = ActiveSupport::ActionableError.actions(exception) %> + +<% if actions.any? %> + <div class="actions"> + <% actions.each do |action, _| %> + <%= button_to action, ActionDispatch::ActionableExceptions.endpoint, params: { + error: exception.class.name, + action: action, + location: request.path + } %> + <% end %> + </div> +<% end %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb index bde26f46c2..999e84e4d6 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb @@ -8,7 +8,11 @@ </header> <div id="container"> - <h2><%= h @exception.message %></h2> + <h2> + <%= h @exception.message %> + + <%= render "rescues/actions", exception: @exception, request: @request %> + </h2> <%= render "rescues/source", source_extracts: @source_extracts, show_source_idx: @show_source_idx, error_index: 0 %> <%= render "rescues/trace", traces: @traces, trace_to_show: @trace_to_show, error_index: 0 %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb index 39ea25bdfc..0f78e23b7f 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb @@ -117,6 +117,10 @@ background-color: #FFCCCC; } + .button_to { + display: inline-block; + } + .hidden { display: none; } diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 4a24c35efb..bbb5762b3c 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -591,14 +591,14 @@ module ActionDispatch if route.segment_keys.include?(:controller) ActiveSupport::Deprecation.warn(<<-MSG.squish) Using a dynamic :controller segment in a route is deprecated and - will be removed in Rails 6.0. + will be removed in Rails 6.1. MSG end if route.segment_keys.include?(:action) ActiveSupport::Deprecation.warn(<<-MSG.squish) Using a dynamic :action segment in a route is deprecated and - will be removed in Rails 6.0. + will be removed in Rails 6.1. MSG end diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb index 79359a0c8b..056ce51a61 100644 --- a/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb @@ -39,7 +39,8 @@ module ActionDispatch private def image_name - failed? ? "failures_#{method_name}" : method_name + name = method_name[0...225] + failed? ? "failures_#{name}" : name end def image_path diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb index 600e9c733b..7080dbe022 100644 --- a/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb @@ -16,12 +16,14 @@ module ActionDispatch super end + def before_teardown + take_failed_screenshot + ensure + super + end + def after_teardown - begin - take_failed_screenshot - ensure - Capybara.reset_sessions! - end + Capybara.reset_sessions! ensure super end diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 6460ca6f8d..32a0b8efeb 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -96,6 +96,7 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase RoutedRackApp.new(routes || ActionDispatch::Routing::RouteSet.new) do |middleware| middleware.use ActionDispatch::ShowExceptions, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public") middleware.use ActionDispatch::DebugExceptions + middleware.use ActionDispatch::ActionableExceptions middleware.use ActionDispatch::Callbacks middleware.use ActionDispatch::Cookies middleware.use ActionDispatch::Flash diff --git a/actionpack/test/dispatch/actionable_exceptions_test.rb b/actionpack/test/dispatch/actionable_exceptions_test.rb new file mode 100644 index 0000000000..9215a91e9c --- /dev/null +++ b/actionpack/test/dispatch/actionable_exceptions_test.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class ActionableExceptionsTest < ActionDispatch::IntegrationTest + Actions = [] + + class ActionError < StandardError + include ActiveSupport::ActionableError + + action "Successful action" do + Actions << "Action!" + end + + action "Failed action" do + raise "Inaction!" + end + end + + Noop = -> env { [200, {}, [""]] } + + setup do + @app = ActionDispatch::ActionableExceptions.new(Noop) + + Actions.clear + end + + test "dispatches an actionable error" do + post ActionDispatch::ActionableExceptions.endpoint, params: { + error: ActionError.name, + action: "Successful action", + location: "/", + } + + assert_equal ["Action!"], Actions + + assert_equal 302, response.status + assert_equal "/", response.headers["Location"] + end + + test "cannot dispatch errors if not allowed" do + post ActionDispatch::ActionableExceptions.endpoint, params: { + error: ActionError.name, + action: "Successful action", + location: "/", + }, headers: { "action_dispatch.show_exceptions" => false } + + assert_empty Actions + end + + test "dispatched action can fail" do + assert_raise RuntimeError do + post ActionDispatch::ActionableExceptions.endpoint, params: { + error: ActionError.name, + action: "Failed action", + location: "/", + } + end + end + + test "cannot dispatch non-actionable errors" do + assert_raise ActiveSupport::ActionableError::NonActionable do + post ActionDispatch::ActionableExceptions.endpoint, params: { + error: RuntimeError.name, + action: "Inexistent action", + location: "/", + } + end + end + + test "cannot dispatch Inexistent errors" do + assert_raise ActiveSupport::ActionableError::NonActionable do + post ActionDispatch::ActionableExceptions.endpoint, params: { + error: "", + action: "Inexistent action", + location: "/", + } + end + end +end diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb index 2812b1b614..5ae8a20ae4 100644 --- a/actionpack/test/dispatch/debug_exceptions_test.rb +++ b/actionpack/test/dispatch/debug_exceptions_test.rb @@ -5,6 +5,18 @@ require "abstract_unit" class DebugExceptionsTest < ActionDispatch::IntegrationTest InterceptedErrorInstance = StandardError.new + class CustomActionableError < StandardError + include ActiveSupport::ActionableError + + action "Action 1" do + nil + end + + action "Action 2" do + nil + end + end + class Boomer attr_accessor :closed @@ -92,6 +104,8 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest method_that_raises when "/nested_exceptions" raise_nested_exceptions + when %r{/actionable_error} + raise CustomActionableError else raise "puke!" end @@ -589,4 +603,21 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest end end end + + test "shows a buttons for every action in an actionable error" do + @app = DevelopmentApp + Rails.stub :root, Pathname.new(".") do + cleaner = ActiveSupport::BacktraceCleaner.new.tap do |bc| + bc.add_silencer { |line| line !~ %r{test/dispatch/debug_exceptions_test.rb} } + end + + get "/actionable_error", headers: { "action_dispatch.backtrace_cleaner" => cleaner } + + # Assert correct error + assert_response 500 + + assert_select 'input[value="Action 1"]' + assert_select 'input[value="Action 2"]' + end + end end diff --git a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb index b756b91379..b0b36f9d74 100644 --- a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb +++ b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb @@ -36,6 +36,14 @@ class ScreenshotHelperTest < ActiveSupport::TestCase end end + test "image name truncates names over 225 characters" do + new_test = DrivenBySeleniumWithChrome.new("x" * 400) + + Rails.stub :root, Pathname.getwd do + assert_equal Rails.root.join("tmp/screenshots/#{"x" * 225}.png").to_s, new_test.send(:image_path) + end + end + test "defaults to simple output for the screenshot" do new_test = DrivenBySeleniumWithChrome.new("x") assert_equal "simple", new_test.send(:output_type) diff --git a/actiontext/app/helpers/action_text/content_helper.rb b/actiontext/app/helpers/action_text/content_helper.rb index 2005033d5c..ed2887d865 100644 --- a/actiontext/app/helpers/action_text/content_helper.rb +++ b/actiontext/app/helpers/action_text/content_helper.rb @@ -4,20 +4,27 @@ require "rails-html-sanitizer" module ActionText module ContentHelper - SANITIZER = Rails::Html::Sanitizer.white_list_sanitizer - ALLOWED_TAGS = SANITIZER.allowed_tags + [ ActionText::Attachment::TAG_NAME, "figure", "figcaption" ] - ALLOWED_ATTRIBUTES = SANITIZER.allowed_attributes + ActionText::Attachment::ATTRIBUTES + mattr_accessor(:sanitizer) { Rails::Html::Sanitizer.white_list_sanitizer.new } + mattr_accessor(:allowed_tags) { sanitizer.class.allowed_tags + [ ActionText::Attachment::TAG_NAME, "figure", "figcaption" ] } + mattr_accessor(:allowed_attributes) { sanitizer.class.allowed_attributes + ActionText::Attachment::ATTRIBUTES } + mattr_accessor(:scrubber) def render_action_text_content(content) - content = content.render_attachments do |attachment| + sanitize_action_text_content(render_action_text_attachments(content)) + end + + def sanitize_action_text_content(content) + sanitizer.sanitize(content.to_html, tags: allowed_tags, attributes: allowed_attributes, scrubber: scrubber).html_safe + end + + def render_action_text_attachments(content) + content.render_attachments do |attachment| unless attachment.in?(content.gallery_attachments) attachment.node.tap do |node| node.inner_html = render(attachment, in_gallery: false).chomp end end - end - - content = content.render_attachment_galleries do |attachment_gallery| + end.render_attachment_galleries do |attachment_gallery| render(layout: attachment_gallery, object: attachment_gallery) do attachment_gallery.attachments.map do |attachment| attachment.node.inner_html = render(attachment, in_gallery: true).chomp @@ -25,8 +32,6 @@ module ActionText end.join("").html_safe end.chomp end - - sanitize content.to_html, tags: ALLOWED_TAGS, attributes: ALLOWED_ATTRIBUTES end end end diff --git a/actiontext/lib/templates/installer.rb b/actiontext/lib/templates/installer.rb index a8000eb9fc..a15ada92bb 100644 --- a/actiontext/lib/templates/installer.rb +++ b/actiontext/lib/templates/installer.rb @@ -29,4 +29,17 @@ if APPLICATION_PACK_PATH.exist? append_to_file APPLICATION_PACK_PATH, "\n#{line}" end end +else + warn <<~WARNING + WARNING: Action Text can't locate your JavaScript bundle to add its package dependencies. + + Add these lines to any bundles: + + require("trix") + require("@rails/actiontext") + + Alternatively, install and setup the webpacker gem then rerun `bin/rails action_text:install` + to have these dependencies added automatically. + + WARNING end diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index be67aff543..f465e08859 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,14 @@ +* Fix partial caching skips same item issue + + If we render cached collection partials with repeated items, those repeated items + will get skipped. For example, if you have 5 identical items in your collection, Rails + only renders the first one when `cached` is set to true. But it should render all + 5 items instead. + + This fixes #35114 + + *Stan Lo* + * Only clear ActionView cache in development on file changes To speed up development mode, view caches are only cleared when files in diff --git a/activejob/test/cases/test_helper_test.rb b/activejob/test/cases/test_helper_test.rb index d6607cb6b6..32471cfacc 100644 --- a/activejob/test/cases/test_helper_test.rb +++ b/activejob/test/cases/test_helper_test.rb @@ -1710,28 +1710,28 @@ class PerformedJobsTest < ActiveJob::TestCase def test_assert_performed_with_time now = Time.now - args = [{ argument1: { now: now } }] + args = [{ argument1: { now: now }, argument2: now }] - assert_enqueued_with(job: MultipleKwargsJob, args: args) do - MultipleKwargsJob.perform_later(argument1: { now: now }) + assert_performed_with(job: MultipleKwargsJob, args: args) do + MultipleKwargsJob.perform_later(argument1: { now: now }, argument2: now) end end def test_assert_performed_with_date_time now = DateTime.now - args = [{ argument1: { now: now } }] + args = [{ argument1: { now: now }, argument2: now }] - assert_enqueued_with(job: MultipleKwargsJob, args: args) do - MultipleKwargsJob.perform_later(argument1: { now: now }) + assert_performed_with(job: MultipleKwargsJob, args: args) do + MultipleKwargsJob.perform_later(argument1: { now: now }, argument2: now) end end def test_assert_performed_with_time_with_zone now = Time.now.in_time_zone("Tokyo") - args = [{ argument1: { now: now } }] + args = [{ argument1: { now: now }, argument2: now }] - assert_enqueued_with(job: MultipleKwargsJob, args: args) do - MultipleKwargsJob.perform_later(argument1: { now: now }) + assert_performed_with(job: MultipleKwargsJob, args: args) do + MultipleKwargsJob.perform_later(argument1: { now: now }, argument2: now) end end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index af7e46e649..220043c061 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -24,7 +24,7 @@ module ActiveRecord RESTRICTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass) - class GeneratedAttributeMethodsBuilder < Module #:nodoc: + class GeneratedAttributeMethods < Module #:nodoc: include Mutex_m end @@ -35,7 +35,7 @@ module ActiveRecord end def initialize_generated_modules # :nodoc: - @generated_attribute_methods = const_set(:GeneratedAttributeMethods, GeneratedAttributeMethodsBuilder.new) + @generated_attribute_methods = const_set(:GeneratedAttributeMethods, GeneratedAttributeMethods.new) private_constant :GeneratedAttributeMethods @attribute_methods_generated = false include @generated_attribute_methods @@ -89,7 +89,7 @@ module ActiveRecord # If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass # defines its own attribute method, then we don't want to overwrite that. defined = method_defined_within?(method_name, superclass, Base) && - ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethodsBuilder) + ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods) defined || super end end @@ -197,7 +197,7 @@ module ActiveRecord "Dangerous query method (method whose arguments are used as raw " \ "SQL) called with non-attribute argument(s): " \ "#{unexpected.map(&:inspect).join(", ")}. Non-attribute " \ - "arguments will be disallowed in Rails 6.0. This method should " \ + "arguments will be disallowed in Rails 6.1. This method should " \ "not be called with user-provided values, such as request " \ "parameters or model attributes. Known-safe values can be passed " \ "by wrapping them in Arel.sql()." @@ -465,7 +465,7 @@ module ActiveRecord end def pk_attribute?(name) - name == self.class.primary_key + name == @primary_key end end end diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb index affcf2a4db..3d917ec9b1 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -46,7 +46,7 @@ module ActiveRecord # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21" # task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21" def read_attribute_before_type_cast(attr_name) - sync_with_transaction_state + sync_with_transaction_state if @transaction_state&.finalized? @attributes[attr_name.to_s].value_before_type_cast end @@ -61,7 +61,7 @@ module ActiveRecord # task.attributes_before_type_cast # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil} def attributes_before_type_cast - sync_with_transaction_state + sync_with_transaction_state if @transaction_state&.finalized? @attributes.values_before_type_cast end @@ -73,7 +73,7 @@ module ActiveRecord end def attribute_came_from_user?(attribute_name) - sync_with_transaction_state + sync_with_transaction_state if @transaction_state&.finalized? @attributes[attribute_name].came_from_user? end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 942fe48635..444568562b 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -157,12 +157,12 @@ module ActiveRecord private def mutations_from_database - sync_with_transaction_state + sync_with_transaction_state if @transaction_state&.finalized? super end def mutations_before_last_save - sync_with_transaction_state + sync_with_transaction_state if @transaction_state&.finalized? super end diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index feaef72a30..b4f5e6e75a 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -16,34 +16,32 @@ module ActiveRecord # Returns the primary key column's value. def id - primary_key = self.class.primary_key - _read_attribute(primary_key) if primary_key + _read_attribute(@primary_key) end # Sets the primary key column's value. def id=(value) - primary_key = self.class.primary_key - _write_attribute(primary_key, value) if primary_key + _write_attribute(@primary_key, value) end # Queries the primary key column's value. def id? - query_attribute(self.class.primary_key) + query_attribute(@primary_key) end # Returns the primary key column's value before type cast. def id_before_type_cast - read_attribute_before_type_cast(self.class.primary_key) + read_attribute_before_type_cast(@primary_key) end # Returns the primary key column's previous value. def id_was - attribute_was(self.class.primary_key) + attribute_was(@primary_key) end # Returns the primary key column's value from the database. def id_in_database - attribute_in_database(self.class.primary_key) + attribute_in_database(@primary_key) end private @@ -116,7 +114,7 @@ module ActiveRecord # # Project.primary_key # => "foo_id" def primary_key=(value) - @primary_key = value && value.to_s + @primary_key = value && -value.to_s @quoted_primary_key = nil @attributes_builder = nil end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 84b1ec2fea..c787fb6a94 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -31,15 +31,14 @@ module ActiveRecord name = self.class.attribute_alias(name) end - primary_key = self.class.primary_key - name = primary_key if name == "id" && primary_key + name = @primary_key if name == "id" && @primary_key _read_attribute(name, &block) end # This method exists to avoid the expensive primary_key check internally, without # breaking compatibility with the read_attribute API def _read_attribute(attr_name, &block) # :nodoc - sync_with_transaction_state + sync_with_transaction_state if @transaction_state&.finalized? @attributes.fetch_value(attr_name.to_s, &block) end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index d1cfe43bb2..6a54bd2fc2 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -35,22 +35,21 @@ module ActiveRecord name = self.class.attribute_alias(name) end - primary_key = self.class.primary_key - name = primary_key if name == "id" && primary_key + name = @primary_key if name == "id" && @primary_key _write_attribute(name, value) end # This method exists to avoid the expensive primary_key check internally, without # breaking compatibility with the write_attribute API def _write_attribute(attr_name, value) # :nodoc: - sync_with_transaction_state + sync_with_transaction_state if @transaction_state&.finalized? @attributes.write_from_user(attr_name.to_s, value) value end private def write_attribute_without_type_cast(attr_name, value) - sync_with_transaction_state + sync_with_transaction_state if @transaction_state&.finalized? @attributes.write_cast_value(attr_name.to_s, value) value end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 7041d92586..2b64e96450 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -770,6 +770,17 @@ module ActiveRecord # CREATE FULLTEXT INDEX index_developers_on_name ON developers (name) -- MySQL # # Note: only supported by MySQL. + # + # ====== Creating an index with a specific algorithm + # + # add_index(:developers, :name, algorithm: :concurrently) + # # CREATE INDEX CONCURRENTLY developers_on_name on developers (name) + # + # Note: only supported by PostgreSQL. + # + # Concurrently adding an index is not supported in a transaction. + # + # For more information see the {"Transactional Migrations" section}[rdoc-ref:Migration]. def add_index(table_name, column_name, options = {}) index_name, index_type, index_columns, index_options = add_index_options(table_name, column_name, options) execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options}" @@ -793,6 +804,15 @@ module ActiveRecord # # remove_index :accounts, name: :by_branch_party # + # Removes the index named +by_branch_party+ in the +accounts+ table +concurrently+. + # + # remove_index :accounts, name: :by_branch_party, algorithm: :concurrently + # + # Note: only supported by PostgreSQL. + # + # Concurrently removing an index is not supported in a transaction. + # + # For more information see the {"Transactional Migrations" section}[rdoc-ref:Migration]. def remove_index(table_name, options = {}) index_name = index_name_for_remove(table_name, options) execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}" diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 04b21b4d00..068ebf3c09 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -101,7 +101,6 @@ module ActiveRecord # environment where dumping schema is rarely needed. mattr_accessor :dump_schema_after_migration, instance_writer: false, default: true - mattr_accessor :database_selector, instance_writer: false ## # :singleton-method: # Specifies which database schemas to dump when calling db:structure:dump. @@ -175,8 +174,7 @@ module ActiveRecord record = statement.execute([id], connection)&.first unless record - raise RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id}", - name, primary_key, id) + raise RecordNotFound.new("Couldn't find #{name} with '#{key}'=#{id}", name, key, id) end record end @@ -399,7 +397,7 @@ module ActiveRecord ## def initialize_dup(other) # :nodoc: @attributes = @attributes.deep_dup - @attributes.reset(self.class.primary_key) + @attributes.reset(@primary_key) _run_initialize_callbacks @@ -466,7 +464,7 @@ module ActiveRecord # Returns +true+ if the attributes hash has been frozen. def frozen? - sync_with_transaction_state + sync_with_transaction_state if @transaction_state&.finalized? @attributes.frozen? end @@ -571,6 +569,7 @@ module ActiveRecord end def init_internals + @primary_key = self.class.primary_key @readonly = false @destroyed = false @marked_for_destruction = false diff --git a/activerecord/lib/active_record/insert_all.rb b/activerecord/lib/active_record/insert_all.rb index 959e5bd4d7..f6577dcbc4 100644 --- a/activerecord/lib/active_record/insert_all.rb +++ b/activerecord/lib/active_record/insert_all.rb @@ -21,9 +21,9 @@ module ActiveRecord end def execute - message = "#{model} " - message += "Bulk " if inserts.many? - message += (on_duplicate == :update ? "Upsert" : "Insert") + message = +"#{model} " + message << "Bulk " if inserts.many? + message << (on_duplicate == :update ? "Upsert" : "Insert") connection.exec_query to_sql, message end diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index c745bc1330..573a823dbc 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -162,7 +162,7 @@ module ActiveRecord end def collection_cache_key(collection = all, timestamp_column = :updated_at) # :nodoc: - collection.compute_cache_key(timestamp_column) + collection.send(:compute_cache_key, timestamp_column) end end diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index b7eecda59e..6711ee9bf4 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -87,7 +87,7 @@ module ActiveRecord affected_rows = self.class._update_record( attributes_with_values(attribute_names), - self.class.primary_key => id_in_database, + @primary_key => id_in_database, locking_column => previous_lock_value ) @@ -110,7 +110,7 @@ module ActiveRecord locking_column = self.class.locking_column affected_rows = self.class._delete_record( - self.class.primary_key => id_in_database, + @primary_key => id_in_database, locking_column => read_attribute_before_type_cast(locking_column) ) diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index ed0c6d48b8..f20edbeb93 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -4,6 +4,7 @@ require "benchmark" require "set" require "zlib" require "active_support/core_ext/module/attribute_accessors" +require "active_support/actionable_error" module ActiveRecord class MigrationError < ActiveRecordError #:nodoc: @@ -128,6 +129,12 @@ module ActiveRecord end class PendingMigrationError < MigrationError #:nodoc: + include ActiveSupport::ActionableError + + action "Run pending migrations" do + ActiveRecord::Tasks::DatabaseTasks.migrate + end + def initialize(message = nil) if !message && defined?(Rails.env) super("Migrations are pending. To resolve this issue, run:\n\n rails db:migrate RAILS_ENV=#{::Rails.env}") diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 8bade8cd28..1ff7ee4d89 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -353,6 +353,7 @@ module ActiveRecord end def _insert_record(values) # :nodoc: + primary_key = self.primary_key primary_key_value = nil if primary_key && Hash === values @@ -423,20 +424,20 @@ module ActiveRecord # Returns true if this object hasn't been saved yet -- that is, a record # for the object doesn't exist in the database yet; otherwise, returns false. def new_record? - sync_with_transaction_state + sync_with_transaction_state if @transaction_state&.finalized? @new_record end # Returns true if this object has been destroyed, otherwise returns false. def destroyed? - sync_with_transaction_state + sync_with_transaction_state if @transaction_state&.finalized? @destroyed end # Returns true if the record is persisted, i.e. it's not a new record and it was # not destroyed, otherwise returns false. def persisted? - sync_with_transaction_state + sync_with_transaction_state if @transaction_state&.finalized? !(@new_record || @destroyed) end @@ -674,7 +675,7 @@ module ActiveRecord affected_rows = self.class._update_record( attributes, - self.class.primary_key => id_in_database + @primary_key => id_in_database ) affected_rows == 1 @@ -874,7 +875,7 @@ module ActiveRecord end def _delete_row - self.class._delete_record(self.class.primary_key => id_in_database) + self.class._delete_record(@primary_key => id_in_database) end def _touch_row(attribute_names, time) @@ -890,7 +891,7 @@ module ActiveRecord def _update_row(attribute_names, attempted_action = "update") self.class._update_record( attributes_with_values(attribute_names), - self.class.primary_key => id_in_database + @primary_key => id_in_database ) end @@ -928,7 +929,7 @@ module ActiveRecord attributes_with_values(attribute_names) ) - self.id ||= new_id if self.class.primary_key + self.id ||= new_id if @primary_key @new_record = false diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 64a4cb0886..e0bc5180c0 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -128,6 +128,8 @@ db_namespace = namespace :db do # desc 'Runs the "up" for a given migration VERSION.' task up: :load_config do + ActiveRecord::Tasks::DatabaseTasks.raise_for_multi_db(command: "db:migrate:up") + raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty? ActiveRecord::Tasks::DatabaseTasks.check_target_version @@ -139,8 +141,29 @@ db_namespace = namespace :db do db_namespace["_dump"].invoke end + namespace :up do + ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| + task spec_name => :load_config do + raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty? + + db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name) + + ActiveRecord::Base.establish_connection(db_config.config) + ActiveRecord::Tasks::DatabaseTasks.check_target_version + ActiveRecord::Base.connection.migration_context.run( + :up, + ActiveRecord::Tasks::DatabaseTasks.target_version + ) + + db_namespace["_dump"].invoke + end + end + end + # desc 'Runs the "down" for a given migration VERSION.' task down: :load_config do + ActiveRecord::Tasks::DatabaseTasks.raise_for_multi_db(command: "db:migrate:down") + raise "VERSION is required - To go down one migration, use db:rollback" if !ENV["VERSION"] || ENV["VERSION"].empty? ActiveRecord::Tasks::DatabaseTasks.check_target_version @@ -152,6 +175,25 @@ db_namespace = namespace :db do db_namespace["_dump"].invoke end + namespace :down do + ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| + task spec_name => :load_config do + raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty? + + db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name) + + ActiveRecord::Base.establish_connection(db_config.config) + ActiveRecord::Tasks::DatabaseTasks.check_target_version + ActiveRecord::Base.connection.migration_context.run( + :down, + ActiveRecord::Tasks::DatabaseTasks.target_version + ) + + db_namespace["_dump"].invoke + end + end + end + desc "Display status of migrations" task status: :load_config do ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config| diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 8eb71e6454..add95f6a0a 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -310,7 +310,7 @@ module ActiveRecord # Product.where("name like ?", "%Game%").cache_key(:last_reviewed_at) def cache_key(timestamp_column = :updated_at) @cache_keys ||= {} - @cache_keys[timestamp_column] ||= @klass.collection_cache_key(self, timestamp_column) + @cache_keys[timestamp_column] ||= klass.collection_cache_key(self, timestamp_column) end def compute_cache_key(timestamp_column = :updated_at) # :nodoc: @@ -323,6 +323,7 @@ module ActiveRecord "#{key}-#{compute_cache_version(timestamp_column)}" end end + private :compute_cache_key # Returns a cache version that can be used together with the cache key to form # a recyclable caching scheme. The cache version is built with the number of records @@ -382,6 +383,7 @@ module ActiveRecord "#{size}" end end + private :compute_cache_version # Scope all queries to the current scope. # @@ -551,8 +553,8 @@ module ActiveRecord # # => ActiveRecord::ActiveRecordError: delete_all doesn't support distinct def delete_all invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method| - value = get_value(method) - SINGLE_VALUE_METHODS.include?(method) ? value : value.any? + value = @values[method] + method == :distinct ? value : value&.any? end if invalid_methods.any? raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}") diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 801e312658..0be9ba7d7b 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -260,10 +260,8 @@ module ActiveRecord def aggregate_column(column_name) return column_name if Arel::Expressions === column_name - if @klass.has_attribute?(column_name) || @klass.attribute_alias?(column_name) - @klass.arel_attribute(column_name) - else - Arel.sql(column_name == :all ? "*" : column_name.to_s) + arel_column(column_name.to_s) do |name| + Arel.sql(column_name == :all ? "*" : name) end end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 7a53a9d1c7..d59331053e 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -45,7 +45,10 @@ module ActiveRecord private def generated_relation_methods - @generated_relation_methods ||= GeneratedRelationMethods.new + @generated_relation_methods ||= GeneratedRelationMethods.new.tap do |mod| + const_set(:GeneratedRelationMethods, mod) + private_constant :GeneratedRelationMethods + end end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 90b5e9a118..5d3cea6741 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -67,11 +67,13 @@ module ActiveRecord end class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{method_name} # def includes_values - get_value(#{name.inspect}) # get_value(:includes) + default = DEFAULT_VALUES[:#{name}] # default = DEFAULT_VALUES[:includes] + @values.fetch(:#{name}, default) # @values.fetch(:includes, default) end # end def #{method_name}=(value) # def includes_values=(value) - set_value(#{name.inspect}, value) # set_value(:includes, value) + assert_mutability! # assert_mutability! + @values[:#{name}] = value # @values[:includes] = value end # end CODE end @@ -417,7 +419,8 @@ module ActiveRecord if !VALID_UNSCOPING_VALUES.include?(scope) raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}." end - set_value(scope, DEFAULT_VALUES[scope]) + assert_mutability! + @values[scope] = DEFAULT_VALUES[scope] when Hash scope.each do |key, target_value| if key != :where @@ -1005,17 +1008,6 @@ module ActiveRecord end private - # Returns a relation value with a given name - def get_value(name) - @values.fetch(name, DEFAULT_VALUES[name]) - end - - # Sets the relation value with the given name - def set_value(name, value) - assert_mutability! - @values[name] = value - end - def assert_mutability! raise ImmutableRelation if @loaded raise ImmutableRelation if defined?(@arel) && @arel @@ -1316,7 +1308,8 @@ module ActiveRecord def structurally_incompatible_values_for_or(other) values = other.values STRUCTURAL_OR_METHODS.reject do |method| - get_value(method) == values.fetch(method, DEFAULT_VALUES[method]) + default = DEFAULT_VALUES[method] + @values.fetch(method, default) == values.fetch(method, default) end end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 53e7e1e6d7..c79ed8db60 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -155,6 +155,20 @@ module ActiveRecord end end + def raise_for_multi_db(environment = env, command:) + db_configs = ActiveRecord::Base.configurations.configs_for(env_name: environment) + + if db_configs.count > 1 + dbs_list = [] + + db_configs.each do |db| + dbs_list << "#{command}:#{db.spec_name}" + end + + raise "You're using a multiple database application. To use `#{command}` you must run the namespaced task with a VERSION. Available tasks are #{dbs_list.to_sentence}." + end + end + def create_current(environment = env) each_current_configuration(environment) { |configuration| create configuration diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index ea288456b9..49a8206c84 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -365,7 +365,7 @@ module ActiveRecord status = nil self.class.transaction do unless has_transactional_callbacks? - sync_with_transaction_state + sync_with_transaction_state if @transaction_state&.finalized? @transaction_state = self.class.connection.transaction_state end remember_transaction_record_state @@ -432,9 +432,8 @@ module ActiveRecord end @mutations_from_database = nil @mutations_before_last_save = nil - pk = self.class.primary_key - if pk && @attributes.fetch_value(pk) != restore_state[:id] - @attributes.write_from_user(pk, restore_state[:id]) + if @attributes.fetch_value(@primary_key) != restore_state[:id] + @attributes.write_from_user(@primary_key, restore_state[:id]) end freeze if restore_state[:frozen?] end @@ -479,7 +478,7 @@ module ActiveRecord # the TransactionState, and rolls back or commits the Active Record object # as appropriate. def sync_with_transaction_state - if (transaction_state = @transaction_state)&.finalized? + if transaction_state = @transaction_state if transaction_state.fully_committed? force_clear_transaction_record_state elsif transaction_state.committed? diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 9fd62dcf72..5cbe5d796d 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -1081,9 +1081,9 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal ["title"], model.accessed_fields end - test "generated attribute methods ancestors have correct class" do + test "generated attribute methods ancestors have correct module" do mod = Topic.send(:generated_attribute_methods) - assert_match %r(Topic::GeneratedAttributeMethods), mod.inspect + assert_equal "Topic::GeneratedAttributeMethods", mod.inspect end private diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 99f47cfe37..ddafa468ed 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -67,6 +67,16 @@ end class BasicsTest < ActiveRecord::TestCase fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, "warehouse-things", :authors, :author_addresses, :categorizations, :categories, :posts + def test_generated_association_methods_module_name + mod = Post.send(:generated_association_methods) + assert_equal "Post::GeneratedAssociationMethods", mod.inspect + end + + def test_generated_relation_methods_module_name + mod = Post.send(:generated_relation_methods) + assert_equal "Post::GeneratedRelationMethods", mod.inspect + end + def test_column_names_are_escaped conn = ActiveRecord::Base.connection classname = conn.class.name[/[^:]*$/] @@ -1205,6 +1215,8 @@ class BasicsTest < ActiveRecord::TestCase wr.close assert Marshal.load rd.read rd.close + ensure + self.class.send(:remove_const, "Post") if self.class.const_defined?("Post", false) end end diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 4759d3b6b2..511d7fc982 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -203,6 +203,14 @@ class PrimaryKeysTest < ActiveRecord::TestCase assert_queries(3, ignore_none: true) { klass.create! } end + def test_assign_id_raises_error_if_primary_key_doesnt_exist + klass = Class.new(ActiveRecord::Base) do + self.table_name = "dashboards" + end + dashboard = klass.new + assert_raises(ActiveModel::MissingAttributeError) { dashboard.id = "1" } + end + if current_adapter?(:PostgreSQLAdapter) def test_serial_with_quoted_sequence_name column = MixedCaseMonkey.columns_hash[MixedCaseMonkey.primary_key] diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index dd4a0b0455..ffe94eee0f 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -760,7 +760,7 @@ module ActiveRecord end class DatabaseTasksMigrateTest < DatabaseTasksMigrationTestCase - def test_migrate_set_and_unset_verbose_and_version_env_vars + def test_can_migrate_from_pending_migration_error_action_dispatch verbose, version = ENV["VERBOSE"], ENV["VERSION"] ENV["VERSION"] = "2" ENV["VERBOSE"] = "false" @@ -772,7 +772,9 @@ module ActiveRecord ENV.delete("VERBOSE") # re-run up migration - assert_includes capture_migration_output, "migrating" + assert_includes(capture(:stdout) do + ActiveSupport::ActionableError.dispatch ActiveRecord::PendingMigrationError, "Run pending migrations" + end, "migrating") ensure ENV["VERBOSE"], ENV["VERSION"] = verbose, version end diff --git a/activestorage/CHANGELOG.md b/activestorage/CHANGELOG.md index 54fc949172..d524ecf7d6 100644 --- a/activestorage/CHANGELOG.md +++ b/activestorage/CHANGELOG.md @@ -1,3 +1,7 @@ +* Permit generating variants of BMP images. + + *Younes Serraj* + ## Rails 6.0.0.beta3 (March 11, 2019) ## * No changes. diff --git a/activestorage/app/models/active_storage/variation.rb b/activestorage/app/models/active_storage/variation.rb index 41b5a45f53..45ae71e0ca 100644 --- a/activestorage/app/models/active_storage/variation.rb +++ b/activestorage/app/models/active_storage/variation.rb @@ -40,7 +40,7 @@ class ActiveStorage::Variation end def initialize(transformations) - @transformations = transformations + @transformations = transformations.deep_symbolize_keys end # Accepts a File object, performs the +transformations+ against it, and diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb index fc75a8f816..cbb205627e 100644 --- a/activestorage/lib/active_storage/engine.rb +++ b/activestorage/lib/active_storage/engine.rb @@ -33,6 +33,7 @@ module ActiveStorage image/jpeg image/pjpeg image/tiff + image/bmp image/vnd.adobe.photoshop image/vnd.microsoft.icon ) @@ -56,6 +57,7 @@ module ActiveStorage image/jpg image/jpeg image/tiff + image/bmp image/vnd.adobe.photoshop image/vnd.microsoft.icon application/pdf diff --git a/activestorage/test/fixtures/files/colors.bmp b/activestorage/test/fixtures/files/colors.bmp Binary files differnew file mode 100644 index 0000000000..3cc1e8764d --- /dev/null +++ b/activestorage/test/fixtures/files/colors.bmp diff --git a/activestorage/test/models/variant_test.rb b/activestorage/test/models/variant_test.rb index d98935eb9f..6b43923159 100644 --- a/activestorage/test/models/variant_test.rb +++ b/activestorage/test/models/variant_test.rb @@ -4,6 +4,14 @@ require "test_helper" require "database/setup" class ActiveStorage::VariantTest < ActiveSupport::TestCase + test "variations have the same key for different types of the same transformation" do + blob = create_file_blob(filename: "racecar.jpg") + variant_a = blob.variant(resize: "100x100") + variant_b = blob.variant("resize" => "100x100") + + assert_equal variant_a.key, variant_b.key + end + test "resized variation of JPEG blob" do blob = create_file_blob(filename: "racecar.jpg") variant = blob.variant(resize: "100x100").processed @@ -144,6 +152,17 @@ class ActiveStorage::VariantTest < ActiveSupport::TestCase assert_equal 33, image.height end + test "resized variation of BMP blob" do + blob = create_file_blob(filename: "colors.bmp") + variant = blob.variant(resize: "15x15").processed + assert_match(/colors\.bmp/, variant.service_url) + + image = read_image(variant) + assert_equal "BMP", image.type + assert_equal 15, image.width + assert_equal 8, image.height + end + test "optimized variation of GIF blob" do blob = create_file_blob(filename: "image.gif", content_type: "image/gif") diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 301b0c8822..4c7b134c35 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,35 @@ +* Introduce `ActiveSupport::ActionableError`. + + Actionable errors let's you dispatch actions from Rails' error pages. This + can help you save time if you have a clear action for the resolution of + common development errors. + + The de-facto example are pending migrations. Every time pending migrations + are found, a middleware raises an error. With actionable errors, you can + run the migrations right from the error page. Other examples include Rails + plugins that need to run a rake task to setup themselves. They can now + raise actionable errors to run the setup straight from the error pages. + + Here is how to define an actionable error: + + ```ruby + class PendingMigrationError < MigrationError #:nodoc: + include ActiveSupport::ActionableError + + action "Run pending migrations" do + ActiveRecord::Tasks::DatabaseTasks.migrate + end + end + ``` + + To make an error actionable, include the `ActiveSupport::ActionableError` + module and invoke the `action` class macro to define the action. An action + needs a name and a procedure to execute. The name is shown as the name of a + button on the error pages. Once clicked, it will invoke the given + procedure. + + *Vipul A M*, *Yao Jie*, *Genadi Samokovarov* + * Preserve `html_safe?` status on `ActiveSupport::SafeBuffer#*`. Before: diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 5589c71281..9e242ddeaa 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -34,6 +34,7 @@ module ActiveSupport extend ActiveSupport::Autoload autoload :Concern + autoload :ActionableError autoload :CurrentAttributes autoload :Dependencies autoload :DescendantsTracker diff --git a/activesupport/lib/active_support/actionable_error.rb b/activesupport/lib/active_support/actionable_error.rb new file mode 100644 index 0000000000..7db14cd178 --- /dev/null +++ b/activesupport/lib/active_support/actionable_error.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module ActiveSupport + # Actionable errors let's you define actions to resolve an error. + # + # To make an error actionable, include the <tt>ActiveSupport::ActionableError</tt> + # module and invoke the +action+ class macro to define the action. An action + # needs a name and a block to execute. + module ActionableError + extend Concern + + class NonActionable < StandardError; end + + included do + class_attribute :_actions, default: {} + end + + def self.actions(error) # :nodoc: + case error + when ActionableError, -> it { Class === it && it < ActionableError } + error._actions + else + {} + end + end + + def self.dispatch(error, name) # :nodoc: + actions(error).fetch(name).call + rescue KeyError + raise NonActionable, "Cannot find action \"#{name}\"" + end + + module ClassMethods + # Defines an action that can resolve the error. + # + # class PendingMigrationError < MigrationError + # include ActiveSupport::ActionableError + # + # action "Run pending migrations" do + # ActiveRecord::Tasks::DatabaseTasks.migrate + # end + # end + def action(name, &block) + _actions[name] = block + end + end + end +end diff --git a/activesupport/lib/active_support/deprecation/method_wrappers.rb b/activesupport/lib/active_support/deprecation/method_wrappers.rb index d99571790f..7c0a54a1d0 100644 --- a/activesupport/lib/active_support/deprecation/method_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/method_wrappers.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "active_support/core_ext/array/extract_options" +require "active_support/core_ext/module/redefine_method" module ActiveSupport class Deprecation @@ -52,29 +53,17 @@ module ActiveSupport options = method_names.extract_options! deprecator = options.delete(:deprecator) || self method_names += options.keys - mod = Module.new + mod = nil method_names.each do |method_name| if target_module.method_defined?(method_name) || target_module.private_method_defined?(method_name) - aliased_method, punctuation = method_name.to_s.sub(/([?!=])$/, ""), $1 - with_method = "#{aliased_method}_with_deprecation#{punctuation}" - without_method = "#{aliased_method}_without_deprecation#{punctuation}" - - target_module.define_method(with_method) do |*args, &block| + method = target_module.instance_method(method_name) + target_module.redefine_method(method_name) do |*args, &block| deprecator.deprecation_warning(method_name, options[method_name]) - send(without_method, *args, &block) - end - - target_module.alias_method(without_method, method_name) - target_module.alias_method(method_name, with_method) - - case - when target_module.protected_method_defined?(without_method) - target_module.send(:protected, method_name) - when target_module.private_method_defined?(without_method) - target_module.send(:private, method_name) + method.bind(self).call(*args, &block) end else + mod ||= Module.new mod.define_method(method_name) do |*args, &block| deprecator.deprecation_warning(method_name, options[method_name]) super(*args, &block) @@ -82,7 +71,7 @@ module ActiveSupport end end - target_module.prepend(mod) unless mod.instance_methods(false).empty? + target_module.prepend(mod) if mod end end end diff --git a/activesupport/test/actionable_error_test.rb b/activesupport/test/actionable_error_test.rb new file mode 100644 index 0000000000..63046b937c --- /dev/null +++ b/activesupport/test/actionable_error_test.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/actionable_error" + +class ActionableErrorTest < ActiveSupport::TestCase + NonActionableError = Class.new(StandardError) + + class DispatchableError < StandardError + include ActiveSupport::ActionableError + + class_attribute :flip1, default: false + class_attribute :flip2, default: false + + action "Flip 1" do + self.flip1 = true + end + + action "Flip 2" do + self.flip2 = true + end + end + + test "returns all action of an actionable error" do + assert_equal ["Flip 1", "Flip 2"], ActiveSupport::ActionableError.actions(DispatchableError).keys + assert_equal ["Flip 1", "Flip 2"], ActiveSupport::ActionableError.actions(DispatchableError.new).keys + end + + test "returns no actions for non-actionable errors" do + assert ActiveSupport::ActionableError.actions(Exception).empty? + assert ActiveSupport::ActionableError.actions(Exception.new).empty? + end + + test "dispatches actions from error and name" do + assert_changes "DispatchableError.flip1", from: false, to: true do + ActiveSupport::ActionableError.dispatch DispatchableError, "Flip 1" + end + end + + test "cannot dispatch missing actions" do + err = assert_raises ActiveSupport::ActionableError::NonActionable do + ActiveSupport::ActionableError.dispatch NonActionableError, "action" + end + + assert_equal 'Cannot find action "action"', err.to_s + end +end diff --git a/activesupport/test/deprecation/method_wrappers_test.rb b/activesupport/test/deprecation/method_wrappers_test.rb index 18729941bc..0aa3233aab 100644 --- a/activesupport/test/deprecation/method_wrappers_test.rb +++ b/activesupport/test/deprecation/method_wrappers_test.rb @@ -89,12 +89,4 @@ class MethodWrappersTest < ActiveSupport::TestCase warning = /old_method is deprecated and will be removed from Rails \d.\d \(use new_method instead\)/ assert_deprecated(warning) { assert_equal "abc", @klass.old_method } end - - def test_method_with_without_deprecation_is_exposed - ActiveSupport::Deprecation.deprecate_methods(@klass, old_method: :new_method) - - warning = /old_method is deprecated and will be removed from Rails \d.\d \(use new_method instead\)/ - assert_deprecated(warning) { assert_equal "abc", @klass.new.old_method_with_deprecation } - assert_equal "abc", @klass.new.old_method_without_deprecation - end end diff --git a/guides/source/6_0_release_notes.md b/guides/source/6_0_release_notes.md index f1f41dc8fc..8c9f55fc85 100644 --- a/guides/source/6_0_release_notes.md +++ b/guides/source/6_0_release_notes.md @@ -453,6 +453,8 @@ Please refer to the [Changelog][active-storage] for detailed changes. ### Notable changes +* Updating an attached model via `update` or `update!` ala `@user.update!(images: [ … ])` now replaces the existing images instead of merely adding to them. + Active Model ------------ @@ -551,6 +553,15 @@ Please refer to the [Changelog][guides] for detailed changes. ### Notable changes +* Add a section about troubleshooting of autoloading constants. + ([Commit](https://github.com/rails/rails/commit/c03bba4f1f03bad7dc034af555b7f2b329cf76f5)) + +* Add Action Mailbox Basics guide. + ([Pull Request](https://github.com/rails/rails/pull/34812)) + +* Add Action Text Overview guide. + ([Pull Request](https://github.com/rails/rails/pull/34878)) + Credits ------- diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md index 2808527141..39935cd2ef 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -689,7 +689,7 @@ Just like the `:status` option for `render`, `:status` for `redirect_to` accepts #### The Difference Between `render` and `redirect_to` -Sometimes inexperienced developers think of `redirect_to` as a sort of `goto` command, moving execution from one place to another in your Rails code. This is _not_ correct. Your code stops running and waits for a new request for the browser. It just happens that you've told the browser what request it should make next, by sending back an HTTP 302 status code. +Sometimes inexperienced developers think of `redirect_to` as a sort of `goto` command, moving execution from one place to another in your Rails code. This is _not_ correct. Your code stops running and waits for a new request from the browser. It just happens that you've told the browser what request it should make next, by sending back an HTTP 302 status code. Consider these actions to see the difference: diff --git a/guides/source/testing.md b/guides/source/testing.md index 1fad02812b..26448958ea 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -1733,6 +1733,25 @@ class ProductTest < ActiveSupport::TestCase end ``` +### Asserting Time Arguments in Jobs + +When serializing job arguments, `Time`, `DateTime`, and `ActiveSupport::TimeWithZone` lose microsecond precision. This means comparing deserialized time with actual time doesn't always work. To compensate for the loss of precision, `assert_enqueued_with` and `assert_performed_with` will remove microseconds from time objects in argument assertions. + +```ruby +require 'test_helper' + +class ProductTest < ActiveSupport::TestCase + include ActiveJob::TestHelper + + test 'that product is reserved at a given time' do + now = Time.now + assert_performed_with(job: ReservationJob, args: [product, now]) do + product.reserve(now) + end + end +end +``` + Testing Action Cable -------------------- diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 109c4836d5..e5f9e37c33 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,16 @@ +* Allow loading seeds without ActiveJob. + + Fixes #35782 + + *Jeremy Weathers* + +* `null: false` is set in the migrations by default for column pointed by + `belongs_to` / `references` association generated by model generator. + + Also deprecate passing {required} to the model generator. + + *Prathamesh Sonpatki* + * New applications get `config.cache_classes = false` in `config/environments/test.rb` unless `--skip-spring`. diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb index 193cc59f3a..9800b19274 100644 --- a/railties/lib/rails/application/default_middleware_stack.rb +++ b/railties/lib/rails/application/default_middleware_stack.rb @@ -49,6 +49,7 @@ module Rails middleware.use ::Rails::Rack::Logger, config.log_tags middleware.use ::ActionDispatch::ShowExceptions, show_exceptions_app middleware.use ::ActionDispatch::DebugExceptions, app, config.debug_exception_response_format + middleware.use ::ActionDispatch::ActionableExceptions unless config.cache_classes middleware.use ::ActionDispatch::Reloader, app.reloader diff --git a/railties/lib/rails/commands/dev/dev_command.rb b/railties/lib/rails/commands/dev/dev_command.rb index a3f02f3172..9b2cb2b04a 100644 --- a/railties/lib/rails/commands/dev/dev_command.rb +++ b/railties/lib/rails/commands/dev/dev_command.rb @@ -5,8 +5,10 @@ require "rails/dev_caching" module Rails module Command class DevCommand < Base # :nodoc: - def help - say "rails dev:cache # Toggle development mode caching on/off." + no_commands do + def help + say "rails dev:cache # Toggle development mode caching on/off." + end end def cache diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index eb2f0e8fca..06b4019fc7 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -552,7 +552,7 @@ module Rails seed_file = paths["db/seeds.rb"].existent.first return unless seed_file - if config.active_job.queue_adapter == :async + if config.try(:active_job)&.queue_adapter == :async with_inline_jobs { load(seed_file) } else load(seed_file) diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 66f6b57379..7e3560d9d2 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -307,7 +307,7 @@ module Rails def assets_gemfile_entry return [] if options[:skip_sprockets] - GemfileEntry.version("sass-rails", "~> 5.0", "Use SCSS for stylesheets") + GemfileEntry.version("sass-rails", "~> 5", "Use SCSS for stylesheets") end def webpacker_gemfile_entry diff --git a/railties/lib/rails/generators/generated_attribute.rb b/railties/lib/rails/generators/generated_attribute.rb index 99c1bc4269..1a80e71eae 100644 --- a/railties/lib/rails/generators/generated_attribute.rb +++ b/railties/lib/rails/generators/generated_attribute.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "active_support/time" +require "active_support/deprecation" module Rails module Generators @@ -51,6 +52,12 @@ module Rails type = $1 provided_options = $2.split(/[,.-]/) options = Hash[provided_options.map { |opt| [opt.to_sym, true] }] + + if options[:required] + ActiveSupport::Deprecation.warn("Passing {required} option has no effect on the model generator. It will be removed in Rails 6.1.\n") + options.delete(:required) + end + return type, options else return type, {} @@ -137,7 +144,7 @@ module Rails end def required? - attr_options[:required] + reference? && Rails.application.config.active_record.belongs_to_required_by_default end def has_index? @@ -183,7 +190,6 @@ module Rails def options_for_migration @attr_options.dup.tap do |options| if required? - options.delete(:required) options[:null] = false end diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index 4242daf39a..54b2e95d75 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -38,6 +38,7 @@ module ApplicationTests "Rails::Rack::Logger", "ActionDispatch::ShowExceptions", "ActionDispatch::DebugExceptions", + "ActionDispatch::ActionableExceptions", "ActionDispatch::Reloader", "ActionDispatch::Callbacks", "ActiveRecord::Migration::CheckPending", @@ -70,6 +71,7 @@ module ApplicationTests "Rails::Rack::Logger", "ActionDispatch::ShowExceptions", "ActionDispatch::DebugExceptions", + "ActionDispatch::ActionableExceptions", "ActionDispatch::Reloader", "ActionDispatch::Callbacks", "Rack::Head", diff --git a/railties/test/application/rake/multi_dbs_test.rb b/railties/test/application/rake/multi_dbs_test.rb index 147b8f94e1..31ea2246a9 100644 --- a/railties/test/application/rake/multi_dbs_test.rb +++ b/railties/test/application/rake/multi_dbs_test.rb @@ -24,7 +24,6 @@ module ApplicationTests assert_no_match(/already exists/, output) assert File.exist?(expected_database) - output = rails("db:drop") assert_match(/Dropped database/, output) assert_match_namespace(namespace, output) @@ -137,6 +136,36 @@ module ApplicationTests end end + def db_up_and_down(version, namespace = nil) + Dir.chdir(app_path) do + generate_models_for_animals + rails("db:migrate") + + if namespace + down_output = rails("db:migrate:down:#{namespace}", "VERSION=#{version}") + up_output = rails("db:migrate:up:#{namespace}", "VERSION=#{version}") + else + assert_raises RuntimeError, /You're using a multiple database application/ do + down_output = rails("db:migrate:down", "VERSION=#{version}") + end + + assert_raises RuntimeError, /You're using a multiple database application/ do + up_output = rails("db:migrate:up", "VERSION=#{version}") + end + end + + case namespace + when "primary" + assert_match(/OneMigration: reverting/, down_output) + assert_match(/OneMigration: migrated/, up_output) + when nil + else + assert_match(/TwoMigration: reverting/, down_output) + assert_match(/TwoMigration: migrated/, up_output) + end + end + end + def db_prepare Dir.chdir(app_path) do generate_models_for_animals @@ -219,6 +248,34 @@ module ApplicationTests end end + test "db:migrate:down and db:migrate:up without a namespace raises in a multi-db application" do + require "#{app_path}/config/environment" + + app_file "db/migrate/01_one_migration.rb", <<-MIGRATION + class OneMigration < ActiveRecord::Migration::Current + end + MIGRATION + + db_up_and_down "01" + end + + test "db:migrate:down:namespace and db:migrate:up:namespace works" do + require "#{app_path}/config/environment" + + app_file "db/migrate/01_one_migration.rb", <<-MIGRATION + class OneMigration < ActiveRecord::Migration::Current + end + MIGRATION + + app_file "db/animals_migrate/02_two_migration.rb", <<-MIGRATION + class TwoMigration < ActiveRecord::Migration::Current + end + MIGRATION + + db_up_and_down "01", "primary" + db_up_and_down "02", "animals" + end + test "db:migrate:status works on all databases" do require "#{app_path}/config/environment" db_migrate_and_migrate_status diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb index 0bdd2b314d..1ab45abcd0 100644 --- a/railties/test/application/test_runner_test.rb +++ b/railties/test/application/test_runner_test.rb @@ -794,6 +794,32 @@ module ApplicationTests assert_match "Capybara.reset_sessions! called", output end + def test_failed_system_test_screenshot_should_be_taken_before_other_teardown + app_file "test/system/failed_system_test_screenshot_should_be_taken_before_other_teardown_test.rb", <<~RUBY + require "application_system_test_case" + require "selenium/webdriver" + + class FailedSystemTestScreenshotShouldBeTakenBeforeOtherTeardownTest < ApplicationSystemTestCase + ActionDispatch::SystemTestCase.class_eval do + def take_failed_screenshot + puts "take_failed_screenshot called" + super + end + end + + def teardown + puts "test teardown called" + super + end + + test "dummy" do + end + end + RUBY + output = run_test_command("test/system/failed_system_test_screenshot_should_be_taken_before_other_teardown_test.rb") + assert_match(/take_failed_screenshot called\n.*test teardown called/, output) + end + def test_system_tests_are_not_run_with_the_default_test_command app_file "test/system/dummy_test.rb", <<-RUBY require "application_system_test_case" diff --git a/railties/test/generators/generated_attribute_test.rb b/railties/test/generators/generated_attribute_test.rb index bf60d6bc22..82d550a124 100644 --- a/railties/test/generators/generated_attribute_test.rb +++ b/railties/test/generators/generated_attribute_test.rb @@ -6,6 +6,15 @@ require "rails/generators/generated_attribute" class GeneratedAttributeTest < Rails::Generators::TestCase include GeneratorsTestHelper + def setup + @old_belongs_to_required_by_default = Rails.application.config.active_record.belongs_to_required_by_default + Rails.application.config.active_record.belongs_to_required_by_default = true + end + + def teardown + Rails.application.config.active_record.belongs_to_required_by_default = @old_belongs_to_required_by_default + end + def test_field_type_returns_number_field assert_field_type :integer, :number_field end @@ -155,10 +164,16 @@ class GeneratedAttributeTest < Rails::Generators::TestCase end def test_parse_required_attribute_with_index - att = Rails::Generators::GeneratedAttribute.parse("supplier:references{required}:index") + att = Rails::Generators::GeneratedAttribute.parse("supplier:references:index") assert_equal "supplier", att.name assert_equal :references, att.type assert_predicate att, :has_index? assert_predicate att, :required? end + + def test_parse_required_attribute_with_index_false_when_belongs_to_required_by_default_global_config_is_false + Rails.application.config.active_record.belongs_to_required_by_default = false + att = Rails::Generators::GeneratedAttribute.parse("supplier:references:index") + assert_not_predicate att, :required? + end end diff --git a/railties/test/generators/migration_generator_test.rb b/railties/test/generators/migration_generator_test.rb index 540bed551b..e6b614a935 100644 --- a/railties/test/generators/migration_generator_test.rb +++ b/railties/test/generators/migration_generator_test.rb @@ -6,6 +6,16 @@ require "rails/generators/rails/migration/migration_generator" class MigrationGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper + def setup + @old_belongs_to_required_by_default = Rails.application.config.active_record.belongs_to_required_by_default + + Rails.application.config.active_record.belongs_to_required_by_default = true + end + + def teardown + Rails.application.config.active_record.belongs_to_required_by_default = @old_belongs_to_required_by_default + end + def test_migration migration = "change_title_body_from_posts" run_generator [migration] @@ -196,9 +206,9 @@ class MigrationGeneratorTest < Rails::Generators::TestCase end end - def test_add_migration_with_required_references + def test_add_migration_with_references_adds_null_false_by_default migration = "add_references_to_books" - run_generator [migration, "author:belongs_to{required}", "distributor:references{polymorphic,required}"] + run_generator [migration, "author:belongs_to", "distributor:references{polymorphic}"] assert_migration "db/migrate/#{migration}.rb" do |content| assert_method :change, content do |change| @@ -208,6 +218,21 @@ class MigrationGeneratorTest < Rails::Generators::TestCase end end + def test_add_migration_with_references_does_not_add_belongs_to_when_required_by_default_global_config_is_false + Rails.application.config.active_record.belongs_to_required_by_default = false + + migration = "add_references_to_books" + + run_generator [migration, "author:belongs_to", "distributor:references{polymorphic}"] + + assert_migration "db/migrate/#{migration}.rb" do |content| + assert_method :change, content do |change| + assert_match(/add_reference :books, :author/, change) + assert_match(/add_reference :books, :distributor, polymorphic: true/, change) + end + end + end + def test_add_migration_with_references_adds_foreign_keys migration = "add_references_to_books" run_generator [migration, "author:belongs_to", "distributor:references{polymorphic}"] diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb index f860e328f2..d6abaf7a6f 100644 --- a/railties/test/generators/model_generator_test.rb +++ b/railties/test/generators/model_generator_test.rb @@ -10,6 +10,14 @@ class ModelGeneratorTest < Rails::Generators::TestCase def setup super Rails::Generators::ModelHelpers.skip_warn = false + @old_belongs_to_required_by_default = Rails.application.config.active_record.belongs_to_required_by_default + + Rails.application.config.active_record.belongs_to_required_by_default = true + end + + + def teardown + Rails.application.config.active_record.belongs_to_required_by_default = @old_belongs_to_required_by_default end def test_help_shows_invoked_generators_options @@ -414,8 +422,8 @@ class ModelGeneratorTest < Rails::Generators::TestCase end end - def test_required_polymorphic_belongs_to_generates_correct_model - run_generator ["account", "supplier:references{required,polymorphic}"] + def test_polymorphic_belongs_to_generates_correct_model + run_generator ["account", "supplier:references{polymorphic}"] expected_file = <<~FILE class Account < ApplicationRecord @@ -425,23 +433,46 @@ class ModelGeneratorTest < Rails::Generators::TestCase assert_file "app/models/account.rb", expected_file end - def test_required_and_polymorphic_are_order_independent - run_generator ["account", "supplier:references{polymorphic.required}"] + def test_passing_required_to_model_generator_is_deprecated + assert_deprecated do + run_generator ["account", "supplier:references{required}"] + end - expected_file = <<~FILE - class Account < ApplicationRecord - belongs_to :supplier, polymorphic: true + assert_migration "db/migrate/create_accounts.rb" do |m| + assert_method :change, m do |up| + assert_match(/t\.references :supplier,.*\snull: false/, up) end - FILE - assert_file "app/models/account.rb", expected_file + end end - def test_required_adds_null_false_to_column - run_generator ["account", "supplier:references{required}"] + def test_null_false_is_added_for_references_by_default + run_generator ["account", "user:references"] assert_migration "db/migrate/create_accounts.rb" do |m| assert_method :change, m do |up| - assert_match(/t\.references :supplier,.*\snull: false/, up) + assert_match(/t\.references :user,.*\snull: false/, up) + end + end + end + + def test_null_false_is_added_for_belongs_to_by_default + run_generator ["account", "user:belongs_to"] + + assert_migration "db/migrate/create_accounts.rb" do |m| + assert_method :change, m do |up| + assert_match(/t\.belongs_to :user,.*\snull: false/, up) + end + end + end + + def test_null_false_is_not_added_when_belongs_to_required_by_default_global_config_is_false + Rails.application.config.active_record.belongs_to_required_by_default = false + + run_generator ["account", "user:belongs_to"] + + assert_migration "db/migrate/create_accounts.rb" do |m| + assert_method :change, m do |up| + assert_match(/t\.belongs_to :user/, up) end end end diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index fe5c62c07d..8ce68dbcfa 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -904,6 +904,32 @@ YAML assert_instance_of ActiveJob::QueueAdapters::DelayedJobAdapter, ActiveJob::Base.queue_adapter end + test "seed data can be loaded when ActiveJob is not present" do + @plugin.write "db/seeds.rb", <<-RUBY + Bukkits::Engine.config.bukkits_seeds_loaded = true + RUBY + + app_file "db/seeds.rb", <<-RUBY + Rails.application.config.app_seeds_loaded = true + RUBY + + boot_rails + + # In a real app, config.active_job would be undefined when + # NOT requiring rails/all AND NOT requiring active_job/railtie + # that doesn't work as expected in this test environment, so: + undefine_config_option(:active_job) + assert_raise(NoMethodError) { Rails.application.config.active_job } + + assert_raise(NoMethodError) { Rails.application.config.app_seeds_loaded } + assert_raise(NoMethodError) { Bukkits::Engine.config.bukkits_seeds_loaded } + + Rails.application.load_seed + assert Rails.application.config.app_seeds_loaded + Bukkits::Engine.load_seed + assert Bukkits::Engine.config.bukkits_seeds_loaded + end + test "skips nonexistent seed data" do FileUtils.rm "#{app_path}/db/seeds.rb" boot_rails @@ -1523,6 +1549,10 @@ YAML Rails.application end + def undefine_config_option(name) + Rails.application.config.class.class_variable_get(:@@options).delete(name) + end + # Restrict frameworks to load in order to avoid engine frameworks affect tests. def restrict_frameworks remove_from_config("require 'rails/all'") |