aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actioncable/lib/action_cable/channel.rb1
-rw-r--r--actioncable/lib/action_cable/channel/test_case.rb275
-rw-r--r--actioncable/test/channel/test_case_test.rb188
-rw-r--r--actionpack/lib/abstract_controller/railties/routes_helpers.rb7
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb3
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb42
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/routing.rb37
-rw-r--r--actionpack/test/abstract_unit.rb5
-rw-r--r--actionpack/test/controller/integration_test.rb7
-rw-r--r--actionpack/test/dispatch/routing/ipv6_redirect_test.rb10
-rw-r--r--actionpack/test/dispatch/url_generation_test.rb9
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb36
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb53
-rw-r--r--activerecord/lib/active_record/tasks/postgresql_database_tasks.rb4
-rw-r--r--activerecord/lib/rails/generators/active_record/model/model_generator.rb1
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb12
-rw-r--r--activerecord/test/cases/tasks/postgresql_rake_test.rb20
-rw-r--r--railties/CHANGELOG.md16
-rw-r--r--railties/lib/rails/engine.rb16
-rw-r--r--railties/lib/rails/engine/configuration.rb2
-rw-r--r--railties/test/generators/model_generator_test.rb9
-rw-r--r--railties/test/generators/scaffold_generator_test.rb6
22 files changed, 657 insertions, 102 deletions
diff --git a/actioncable/lib/action_cable/channel.rb b/actioncable/lib/action_cable/channel.rb
index d2f6fbbbc7..d5118b9dc9 100644
--- a/actioncable/lib/action_cable/channel.rb
+++ b/actioncable/lib/action_cable/channel.rb
@@ -11,6 +11,7 @@ module ActionCable
autoload :Naming
autoload :PeriodicTimers
autoload :Streams
+ autoload :TestCase
end
end
end
diff --git a/actioncable/lib/action_cable/channel/test_case.rb b/actioncable/lib/action_cable/channel/test_case.rb
new file mode 100644
index 0000000000..88d7c7092b
--- /dev/null
+++ b/actioncable/lib/action_cable/channel/test_case.rb
@@ -0,0 +1,275 @@
+# frozen_string_literal: true
+
+require "active_support"
+require "active_support/test_case"
+require "active_support/core_ext/hash/indifferent_access"
+require "json"
+
+module ActionCable
+ module Channel
+ class NonInferrableChannelError < ::StandardError
+ def initialize(name)
+ super "Unable to determine the channel to test from #{name}. " +
+ "You'll need to specify it using `tests YourChannel` in your " +
+ "test case definition."
+ end
+ end
+
+ # Stub `stream_from` to track streams for the channel.
+ # Add public aliases for `subscription_confirmation_sent?` and
+ # `subscription_rejected?`.
+ module ChannelStub
+ def confirmed?
+ subscription_confirmation_sent?
+ end
+
+ def rejected?
+ subscription_rejected?
+ end
+
+ def stream_from(broadcasting, *)
+ streams << broadcasting
+ end
+
+ def stop_all_streams
+ @_streams = []
+ end
+
+ def streams
+ @_streams ||= []
+ end
+
+ # Make periodic timers no-op
+ def start_periodic_timers; end
+ alias stop_periodic_timers start_periodic_timers
+ end
+
+ class ConnectionStub
+ attr_reader :transmissions, :identifiers, :subscriptions, :logger
+
+ def initialize(identifiers = {})
+ @transmissions = []
+
+ identifiers.each do |identifier, val|
+ define_singleton_method(identifier) { val }
+ end
+
+ @subscriptions = ActionCable::Connection::Subscriptions.new(self)
+ @identifiers = identifiers.keys
+ @logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new)
+ end
+
+ def transmit(cable_message)
+ transmissions << cable_message.with_indifferent_access
+ end
+ end
+
+ # Superclass for Action Cable channel functional tests.
+ #
+ # == Basic example
+ #
+ # Functional tests are written as follows:
+ # 1. First, one uses the +subscribe+ method to simulate subscription creation.
+ # 2. Then, one asserts whether the current state is as expected. "State" can be anything:
+ # transmitted messages, subscribed streams, etc.
+ #
+ # For example:
+ #
+ # class ChatChannelTest < ActionCable::Channel::TestCase
+ # def test_subscribed_with_room_number
+ # # Simulate a subscription creation
+ # subscribe room_number: 1
+ #
+ # # Asserts that the subscription was successfully created
+ # assert subscription.confirmed?
+ #
+ # # Asserts that the channel subscribes connection to a stream
+ # assert_equal "chat_1", streams.last
+ # end
+ #
+ # def test_does_not_subscribe_without_room_number
+ # subscribe
+ #
+ # # Asserts that the subscription was rejected
+ # assert subscription.rejected?
+ # end
+ # end
+ #
+ # You can also perform actions:
+ # def test_perform_speak
+ # subscribe room_number: 1
+ #
+ # perform :speak, message: "Hello, Rails!"
+ #
+ # assert_equal "Hello, Rails!", transmissions.last["text"]
+ # end
+ #
+ # == Special methods
+ #
+ # ActionCable::Channel::TestCase will also automatically provide the following instance
+ # methods for use in the tests:
+ #
+ # <b>connection</b>::
+ # An ActionCable::Channel::ConnectionStub, representing the current HTTP connection.
+ # <b>subscription</b>::
+ # An instance of the current channel, created when you call `subscribe`.
+ # <b>transmissions</b>::
+ # A list of all messages that have been transmitted into the channel.
+ # <b>streams</b>::
+ # A list of all created streams subscriptions (as identifiers) for the subscription.
+ #
+ #
+ # == Channel is automatically inferred
+ #
+ # ActionCable::Channel::TestCase will automatically infer the channel under test
+ # from the test class name. If the channel cannot be inferred from the test
+ # class name, you can explicitly set it with +tests+.
+ #
+ # class SpecialEdgeCaseChannelTest < ActionCable::Channel::TestCase
+ # tests SpecialChannel
+ # end
+ #
+ # == Specifying connection identifiers
+ #
+ # You need to set up your connection manually to privide values for the identifiers.
+ # To do this just use:
+ #
+ # stub_connection(user: users[:john])
+ #
+ # == Testing broadcasting
+ #
+ # ActionCable::Channel::TestCase enhances ActionCable::TestHelper assertions (e.g.
+ # +assert_broadcasts+) to handle broadcasting to models:
+ #
+ #
+ # # in your channel
+ # def speak(data)
+ # broadcast_to room, text: data["message"]
+ # end
+ #
+ # def test_speak
+ # subscribe room_id: rooms[:chat].id
+ #
+ # assert_broadcasts_on(rooms[:chat], text: "Hello, Rails!") do
+ # perform :speak, message: "Hello, Rails!"
+ # end
+ # end
+ class TestCase < ActiveSupport::TestCase
+ module Behavior
+ extend ActiveSupport::Concern
+
+ include ActiveSupport::Testing::ConstantLookup
+ include ActionCable::TestHelper
+
+ CHANNEL_IDENTIFIER = "test_stub"
+
+ included do
+ class_attribute :_channel_class
+
+ attr_reader :connection, :subscription
+ delegate :streams, to: :subscription
+
+ ActiveSupport.run_load_hooks(:action_cable_channel_test_case, self)
+ end
+
+ module ClassMethods
+ def tests(channel)
+ case channel
+ when String, Symbol
+ self._channel_class = channel.to_s.camelize.constantize
+ when Module
+ self._channel_class = channel
+ else
+ raise NonInferrableChannelError.new(channel)
+ end
+ end
+
+ def channel_class
+ if channel = self._channel_class
+ channel
+ else
+ tests determine_default_channel(name)
+ end
+ end
+
+ def determine_default_channel(name)
+ channel = determine_constant_from_test_name(name) do |constant|
+ Class === constant && constant < ActionCable::Channel::Base
+ end
+ raise NonInferrableChannelError.new(name) if channel.nil?
+ channel
+ end
+ end
+
+ # Setup test connection with the specified identifiers:
+ #
+ # class ApplicationCable < ActionCable::Connection::Base
+ # identified_by :user, :token
+ # end
+ #
+ # stub_connection(user: users[:john], token: 'my-secret-token')
+ def stub_connection(identifiers = {})
+ @connection = ConnectionStub.new(identifiers)
+ end
+
+ # Subsribe to the channel under test. Optionally pass subscription parameters as a Hash.
+ def subscribe(params = {})
+ @connection ||= stub_connection
+ # NOTE: Rails < 5.0.1 calls subscribe_to_channel during #initialize.
+ # We have to stub before it
+ @subscription = self.class.channel_class.allocate
+ @subscription.singleton_class.include(ChannelStub)
+ @subscription.send(:initialize, connection, CHANNEL_IDENTIFIER, params.with_indifferent_access)
+ # Call subscribe_to_channel if it's public (Rails 5.0.1+)
+ @subscription.subscribe_to_channel if ActionCable.gem_version >= Gem::Version.new("5.0.1")
+ @subscription
+ end
+
+ # Unsubscribe the subscription under test.
+ def unsubscribe
+ check_subscribed!
+ subscription.unsubscribe_from_channel
+ end
+
+ # Perform action on a channel.
+ #
+ # NOTE: Must be subscribed.
+ def perform(action, data = {})
+ check_subscribed!
+ subscription.perform_action(data.stringify_keys.merge("action" => action.to_s))
+ end
+
+ # Returns messages transmitted into channel
+ def transmissions
+ # Return only directly sent message (via #transmit)
+ connection.transmissions.map { |data| data["message"] }.compact
+ end
+
+ # Enhance TestHelper assertions to handle non-String
+ # broadcastings
+ def assert_broadcasts(stream_or_object, *args)
+ super(broadcasting_for(stream_or_object), *args)
+ end
+
+ def assert_broadcast_on(stream_or_object, *args)
+ super(broadcasting_for(stream_or_object), *args)
+ end
+
+ private
+ def check_subscribed!
+ raise "Must be subscribed!" if subscription.nil? || subscription.rejected?
+ end
+
+ def broadcasting_for(stream_or_object)
+ return stream_or_object if stream_or_object.is_a?(String)
+
+ self.class.channel_class.broadcasting_for(
+ [self.class.channel_class.channel_name, stream_or_object]
+ )
+ end
+ end
+
+ include Behavior
+ end
+ end
+end
diff --git a/actioncable/test/channel/test_case_test.rb b/actioncable/test/channel/test_case_test.rb
new file mode 100644
index 0000000000..63d0d6207e
--- /dev/null
+++ b/actioncable/test/channel/test_case_test.rb
@@ -0,0 +1,188 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class TestTestChannel < ActionCable::Channel::Base
+end
+
+class NonInferrableExplicitClassChannelTest < ActionCable::Channel::TestCase
+ tests TestTestChannel
+
+ def test_set_channel_class_manual
+ assert_equal TestTestChannel, self.class.channel_class
+ end
+end
+
+class NonInferrableSymbolNameChannelTest < ActionCable::Channel::TestCase
+ tests :test_test_channel
+
+ def test_set_channel_class_manual_using_symbol
+ assert_equal TestTestChannel, self.class.channel_class
+ end
+end
+
+class NonInferrableStringNameChannelTest < ActionCable::Channel::TestCase
+ tests "test_test_channel"
+
+ def test_set_channel_class_manual_using_string
+ assert_equal TestTestChannel, self.class.channel_class
+ end
+end
+
+class SubscriptionsTestChannel < ActionCable::Channel::Base
+end
+
+class SubscriptionsTestChannelTest < ActionCable::Channel::TestCase
+ def setup
+ stub_connection
+ end
+
+ def test_no_subscribe
+ assert_nil subscription
+ end
+
+ def test_subscribe
+ subscribe
+
+ assert subscription.confirmed?
+ assert_not subscription.rejected?
+ assert_equal 1, connection.transmissions.size
+ assert_equal ActionCable::INTERNAL[:message_types][:confirmation],
+ connection.transmissions.last["type"]
+ end
+end
+
+class StubConnectionTest < ActionCable::Channel::TestCase
+ tests SubscriptionsTestChannel
+
+ def test_connection_identifiers
+ stub_connection username: "John", admin: true
+
+ subscribe
+
+ assert_equal "John", subscription.username
+ assert subscription.admin
+ end
+end
+
+class RejectionTestChannel < ActionCable::Channel::Base
+ def subscribed
+ reject
+ end
+end
+
+class RejectionTestChannelTest < ActionCable::Channel::TestCase
+ def test_rejection
+ subscribe
+
+ assert_not subscription.confirmed?
+ assert subscription.rejected?
+ assert_equal 1, connection.transmissions.size
+ assert_equal ActionCable::INTERNAL[:message_types][:rejection],
+ connection.transmissions.last["type"]
+ end
+end
+
+class StreamsTestChannel < ActionCable::Channel::Base
+ def subscribed
+ stream_from "test_#{params[:id] || 0}"
+ end
+end
+
+class StreamsTestChannelTest < ActionCable::Channel::TestCase
+ def test_stream_without_params
+ subscribe
+
+ assert_equal "test_0", streams.last
+ end
+
+ def test_stream_with_params
+ subscribe id: 42
+
+ assert_equal "test_42", streams.last
+ end
+end
+
+class PerformTestChannel < ActionCable::Channel::Base
+ def echo(data)
+ data.delete("action")
+ transmit data
+ end
+
+ def ping
+ transmit type: "pong"
+ end
+end
+
+class PerformTestChannelTest < ActionCable::Channel::TestCase
+ def setup
+ stub_connection user_id: 2016
+ subscribe id: 5
+ end
+
+ def test_perform_with_params
+ perform :echo, text: "You are man!"
+
+ assert_equal({ "text" => "You are man!" }, transmissions.last)
+ end
+
+ def test_perform_and_transmit
+ perform :ping
+
+ assert_equal "pong", transmissions.last["type"]
+ end
+end
+
+class PerformUnsubscribedTestChannelTest < ActionCable::Channel::TestCase
+ tests PerformTestChannel
+
+ def test_perform_when_unsubscribed
+ assert_raises do
+ perform :echo
+ end
+ end
+end
+
+class BroadcastsTestChannel < ActionCable::Channel::Base
+ def broadcast(data)
+ ActionCable.server.broadcast(
+ "broadcast_#{params[:id]}",
+ text: data["message"], user_id: user_id
+ )
+ end
+
+ def broadcast_to_user(data)
+ user = User.new user_id
+
+ self.class.broadcast_to user, text: data["message"]
+ end
+end
+
+class BroadcastsTestChannelTest < ActionCable::Channel::TestCase
+ def setup
+ stub_connection user_id: 2017
+ subscribe id: 5
+ end
+
+ def test_broadcast_matchers_included
+ assert_broadcast_on("broadcast_5", user_id: 2017, text: "SOS") do
+ perform :broadcast, message: "SOS"
+ end
+ end
+
+ def test_broadcast_to_object
+ user = User.new(2017)
+
+ assert_broadcasts(user, 1) do
+ perform :broadcast_to_user, text: "SOS"
+ end
+ end
+
+ def test_broadcast_to_object_with_data
+ user = User.new(2017)
+
+ assert_broadcast_on(user, text: "SOS") do
+ perform :broadcast_to_user, message: "SOS"
+ end
+ end
+end
diff --git a/actionpack/lib/abstract_controller/railties/routes_helpers.rb b/actionpack/lib/abstract_controller/railties/routes_helpers.rb
index b6e5631a4e..c97be074c8 100644
--- a/actionpack/lib/abstract_controller/railties/routes_helpers.rb
+++ b/actionpack/lib/abstract_controller/railties/routes_helpers.rb
@@ -7,11 +7,8 @@ module AbstractController
Module.new do
define_method(:inherited) do |klass|
super(klass)
- if namespace = klass.parents.detect { |m| m.respond_to?(:railtie_routes_url_helpers) }
- klass.include(namespace.railtie_routes_url_helpers(include_path_helpers))
- else
- klass.include(routes.url_helpers(include_path_helpers))
- end
+
+ routes.include_helpers klass, include_path_helpers
end
end
end
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index 3f7cf0950d..b618b9c400 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -664,7 +664,6 @@ module ActionDispatch
def define_generate_prefix(app, name)
_route = @set.named_routes.get name
_routes = @set
- _url_helpers = @set.url_helpers
script_namer = ->(options) do
prefix_options = options.slice(*_route.segment_keys)
@@ -676,7 +675,7 @@ module ActionDispatch
# We must actually delete prefix segment keys to avoid passing them to next url_for.
_route.segment_keys.each { |k| options.delete(k) }
- _url_helpers.send("#{name}_path", prefix_options)
+ @set.url_helpers.send("#{name}_path", prefix_options)
end
app.routes.define_mounted_helper(name, script_namer)
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index acce8a7ef3..da4f285f61 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -378,6 +378,8 @@ module ActionDispatch
@disable_clear_and_finalize = false
@finalized = false
@env_key = "ROUTES_#{object_id}_SCRIPT_NAME".freeze
+ @url_helpers = nil
+ @deferred_classes = []
@set = Journey::Routes.new
@router = Journey::Router.new @set
@@ -433,10 +435,34 @@ module ActionDispatch
end
private :eval_block
+ def include_helpers(klass, include_path_helpers)
+ if @finalized
+ include_helpers_now klass, include_path_helpers
+ else
+ @deferred_classes << [klass, include_path_helpers]
+ end
+ end
+
+ def include_helpers_now(klass, include_path_helpers)
+ namespace = klass.parents.detect { |m| m.respond_to?(:railtie_include_helpers) }
+
+ if namespace && namespace.railtie_namespace.routes != self
+ namespace.railtie_include_helpers(klass, include_path_helpers)
+ else
+ klass.include(url_helpers(include_path_helpers))
+ end
+ end
+ private :include_helpers_now
+
def finalize!
return if @finalized
@append.each { |blk| eval_block(blk) }
@finalized = true
+ @url_helpers = build_url_helper_module true
+ @deferred_classes.each { |klass, include_path_helpers|
+ include_helpers klass, include_path_helpers
+ }
+ @deferred_classes.clear
end
def clear!
@@ -465,11 +491,10 @@ module ActionDispatch
return if MountedHelpers.method_defined?(name)
routes = self
- helpers = routes.url_helpers
MountedHelpers.class_eval do
define_method "_#{name}" do
- RoutesProxy.new(routes, _routes_context, helpers, script_namer)
+ RoutesProxy.new(routes, _routes_context, routes.url_helpers, script_namer)
end
end
@@ -480,7 +505,20 @@ module ActionDispatch
RUBY
end
+ class UnfinalizedRouteSet < StandardError
+ end
+
def url_helpers(supports_path = true)
+ raise UnfinalizedRouteSet, "routes have not been finalized. Please call `finalize!` or use `draw(&block)`" unless @finalized
+
+ if supports_path
+ @url_helpers
+ else
+ build_url_helper_module false
+ end
+ end
+
+ def build_url_helper_module(supports_path)
routes = self
Module.new do
diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
index af41521c5c..0e8712f8d9 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
@@ -138,6 +138,20 @@ module ActionDispatch
assert_generates(path.is_a?(Hash) ? path[:path] : path, generate_options, defaults, extras, message)
end
+ # Provides a hook on `finalize!` so we can mutate a controller after the
+ # route set has been drawn.
+ class WithRouting < ActionDispatch::Routing::RouteSet # :nodoc:
+ def initialize(&block)
+ super()
+ @block = block
+ end
+
+ def finalize!
+ super
+ @block.call self
+ end
+ end
+
# A helper to make it easier to test different route configurations.
# This method temporarily replaces @routes with a new RouteSet instance.
#
@@ -152,16 +166,19 @@ module ActionDispatch
# end
#
def with_routing
- old_routes, @routes = @routes, ActionDispatch::Routing::RouteSet.new
- if defined?(@controller) && @controller
- old_controller, @controller = @controller, @controller.clone
- _routes = @routes
-
- @controller.singleton_class.include(_routes.url_helpers)
-
- if @controller.respond_to? :view_context_class
- @controller.view_context_class = Class.new(@controller.view_context_class) do
- include _routes.url_helpers
+ old_routes = @routes
+ old_controller = nil
+ @routes = WithRouting.new do |_routes|
+ if defined?(@controller) && @controller
+ old_controller, @controller = @controller, @controller.clone
+ _routes = @routes
+
+ @controller.singleton_class.include(_routes.url_helpers)
+
+ if @controller.respond_to? :view_context_class
+ @controller.view_context_class = Class.new(@controller.view_context_class) do
+ include _routes.url_helpers
+ end
end
end
end
diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb
index 65dd28b3d7..7c9f15108d 100644
--- a/actionpack/test/abstract_unit.rb
+++ b/actionpack/test/abstract_unit.rb
@@ -100,7 +100,10 @@ end
class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
def self.build_app(routes = nil)
- RoutedRackApp.new(routes || ActionDispatch::Routing::RouteSet.new) do |middleware|
+ routes ||= ActionDispatch::Routing::RouteSet.new.tap { |rs|
+ rs.draw { }
+ }
+ RoutedRackApp.new(routes) do |middleware|
middleware.use ActionDispatch::ShowExceptions, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public")
middleware.use ActionDispatch::DebugExceptions
middleware.use ActionDispatch::Callbacks
diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb
index 39ede1442a..b078e8ad9f 100644
--- a/actionpack/test/controller/integration_test.rb
+++ b/actionpack/test/controller/integration_test.rb
@@ -542,9 +542,6 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
def with_test_route_set
with_routing do |set|
controller = ::IntegrationProcessTest::IntegrationController.clone
- controller.class_eval do
- include set.url_helpers
- end
set.draw do
get "moved" => redirect("/method")
@@ -555,6 +552,10 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
end
end
+ controller.class_eval do
+ include set.url_helpers
+ end
+
singleton_class.include(set.url_helpers)
yield
diff --git a/actionpack/test/dispatch/routing/ipv6_redirect_test.rb b/actionpack/test/dispatch/routing/ipv6_redirect_test.rb
index 31559bffc7..fb423d2951 100644
--- a/actionpack/test/dispatch/routing/ipv6_redirect_test.rb
+++ b/actionpack/test/dispatch/routing/ipv6_redirect_test.rb
@@ -4,6 +4,11 @@ require "abstract_unit"
class IPv6IntegrationTest < ActionDispatch::IntegrationTest
Routes = ActionDispatch::Routing::RouteSet.new
+ Routes.draw do
+ get "/", to: "bad_route_request#index", as: :index
+ get "/foo", to: "bad_route_request#foo", as: :foo
+ end
+
include Routes.url_helpers
class ::BadRouteRequestController < ActionController::Base
@@ -17,11 +22,6 @@ class IPv6IntegrationTest < ActionDispatch::IntegrationTest
end
end
- Routes.draw do
- get "/", to: "bad_route_request#index", as: :index
- get "/foo", to: "bad_route_request#foo", as: :foo
- end
-
def _routes
Routes
end
diff --git a/actionpack/test/dispatch/url_generation_test.rb b/actionpack/test/dispatch/url_generation_test.rb
index aef9351de1..b0096d26be 100644
--- a/actionpack/test/dispatch/url_generation_test.rb
+++ b/actionpack/test/dispatch/url_generation_test.rb
@@ -4,16 +4,13 @@ require "abstract_unit"
module TestUrlGeneration
class WithMountPoint < ActionDispatch::IntegrationTest
- Routes = ActionDispatch::Routing::RouteSet.new
- include Routes.url_helpers
-
class ::MyRouteGeneratingController < ActionController::Base
- include Routes.url_helpers
def index
render plain: foo_path
end
end
+ Routes = ActionDispatch::Routing::RouteSet.new
Routes.draw do
get "/foo", to: "my_route_generating#index", as: :foo
@@ -22,6 +19,10 @@ module TestUrlGeneration
mount MyRouteGeneratingController.action(:index), at: "/bar"
end
+ class ::MyRouteGeneratingController
+ include Routes.url_helpers
+ end
+
APP = build_app Routes
def _routes
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index 544aec5e8b..3346725f2d 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -34,14 +34,28 @@ module ActiveRecord
@updated
end
- def decrement_counters # :nodoc:
+ def decrement_counters
update_counters(-1)
end
- def increment_counters # :nodoc:
+ def increment_counters
update_counters(1)
end
+ def decrement_counters_before_last_save
+ if reflection.polymorphic?
+ model_was = owner.attribute_before_last_save(reflection.foreign_type).try(:constantize)
+ else
+ model_was = klass
+ end
+
+ foreign_key_was = owner.attribute_before_last_save(reflection.foreign_key)
+
+ if foreign_key_was && model_was < ActiveRecord::Base
+ update_counters_via_scope(model_was, foreign_key_was, -1)
+ end
+ end
+
def target_changed?
owner.saved_change_to_attribute?(reflection.foreign_key)
end
@@ -64,11 +78,16 @@ module ActiveRecord
if target && !stale_target?
target.increment!(reflection.counter_cache_column, by, touch: reflection.options[:touch])
else
- counter_cache_target.update_counters(reflection.counter_cache_column => by, touch: reflection.options[:touch])
+ update_counters_via_scope(klass, owner._read_attribute(reflection.foreign_key), by)
end
end
end
+ def update_counters_via_scope(klass, foreign_key, by)
+ scope = klass.unscoped.where!(primary_key(klass) => foreign_key)
+ scope.update_counters(reflection.counter_cache_column => by, touch: reflection.options[:touch])
+ end
+
def find_target?
!loaded? && foreign_key_present? && klass
end
@@ -78,11 +97,11 @@ module ActiveRecord
end
def replace_keys(record)
- owner[reflection.foreign_key] = record ? record._read_attribute(primary_key(record)) : nil
+ owner[reflection.foreign_key] = record ? record._read_attribute(primary_key(record.class)) : nil
end
- def primary_key(record)
- reflection.association_primary_key(record.class)
+ def primary_key(klass)
+ reflection.association_primary_key(klass)
end
def foreign_key_present?
@@ -96,11 +115,6 @@ module ActiveRecord
inverse && inverse.has_one?
end
- def counter_cache_target
- primary_key = reflection.association_primary_key(klass)
- klass.unscoped.where!(primary_key => owner._read_attribute(reflection.foreign_key))
- end
-
def stale_state
result = owner._read_attribute(reflection.foreign_key) { |n| owner.send(:missing_attribute, n, caller) }
result && result.to_s
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index 374247ffec..fc00f1e900 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -21,47 +21,16 @@ module ActiveRecord::Associations::Builder # :nodoc:
add_default_callbacks(model, reflection) if reflection.options[:default]
end
- def self.define_accessors(mixin, reflection)
- super
- add_counter_cache_methods mixin
- end
-
- def self.add_counter_cache_methods(mixin)
- return if mixin.method_defined? :belongs_to_counter_cache_after_update
-
- mixin.class_eval do
- def belongs_to_counter_cache_after_update(reflection)
- if association(reflection.name).target_changed?
- if reflection.polymorphic?
- model_was = attribute_before_last_save(reflection.foreign_type).try(:constantize)
- else
- model_was = reflection.klass
- end
-
- foreign_key_was = attribute_before_last_save(reflection.foreign_key)
- cache_column = reflection.counter_cache_column
-
- association(reflection.name).increment_counters
-
- if foreign_key_was && model_was < ActiveRecord::Base
- counter_cache_target(reflection, model_was, foreign_key_was).update_counters(cache_column => -1)
- end
- end
- end
-
- private
- def counter_cache_target(reflection, model, foreign_key)
- primary_key = reflection.association_primary_key(model)
- model.unscoped.where!(primary_key => foreign_key)
- end
- end
- end
-
def self.add_counter_cache_callbacks(model, reflection)
cache_column = reflection.counter_cache_column
model.after_update lambda { |record|
- record.belongs_to_counter_cache_after_update(reflection)
+ association = association(reflection.name)
+
+ if association.target_changed?
+ association.increment_counters
+ association.decrement_counters_before_last_save
+ end
}
klass = reflection.class_name.safe_constantize
@@ -112,12 +81,18 @@ module ActiveRecord::Associations::Builder # :nodoc:
BelongsTo.touch_record(record, record.send(changes_method), foreign_key, n, touch, belongs_to_touch_method)
}}
- unless reflection.counter_cache_column
+ if reflection.counter_cache_column
+ touch_callback = callback.(:saved_changes)
+ update_callback = lambda { |record|
+ instance_exec(record, &touch_callback) unless association(reflection.name).target_changed?
+ }
+ model.after_update update_callback, if: :saved_changes?
+ else
model.after_create callback.(:saved_changes), if: :saved_changes?
+ model.after_update callback.(:saved_changes), if: :saved_changes?
model.after_destroy callback.(:changes_to_save)
end
- model.after_update callback.(:saved_changes), if: :saved_changes?
model.after_touch callback.(:changes_to_save)
end
diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
index 4da918ada3..d3c85b161e 100644
--- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
@@ -61,7 +61,7 @@ module ActiveRecord
ActiveRecord::Base.dump_schemas
end
- args = ["-s", "-X", "-x", "-O", "-f", filename]
+ args = ["-s", "-x", "-O", "-f", filename]
args.concat(Array(extra_flags)) if extra_flags
unless search_path.blank?
args += search_path.split(",").map do |part|
@@ -82,7 +82,7 @@ module ActiveRecord
def structure_load(filename, extra_flags)
set_psql_env
- args = ["-v", ON_ERROR_STOP_1, "-q", "-f", filename]
+ args = ["-v", ON_ERROR_STOP_1, "-q", "-X", "-f", filename]
args.concat(Array(extra_flags)) if extra_flags
args << configuration["database"]
run_cmd("psql", args, "loading")
diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
index 25e54f3ac8..747c8493b1 100644
--- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
@@ -14,6 +14,7 @@ module ActiveRecord
class_option :parent, type: :string, desc: "The parent class for the generated model"
class_option :indexes, type: :boolean, default: true, desc: "Add indexes for references and belongs_to columns"
class_option :primary_key_type, type: :string, desc: "The type for primary key"
+ class_option :migrations_paths, type: :string, desc: "The migration path for your generated migrations. If this is not set it will default to db/migrate"
# creates the migration file for the model.
def create_migration_file
diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb
index 8b205f0b85..1fca1be181 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -616,8 +616,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
debate.touch(time: time)
debate2.touch(time: time)
- reply.parent_title = "debate"
- reply.save!
+ assert_queries(3) do
+ reply.parent_title = "debate"
+ reply.save!
+ end
assert_operator debate.reload.updated_at, :>, time
assert_operator debate2.reload.updated_at, :>, time
@@ -625,8 +627,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
debate.touch(time: time)
debate2.touch(time: time)
- reply.topic_with_primary_key = debate2
- reply.save!
+ assert_queries(3) do
+ reply.topic_with_primary_key = debate2
+ reply.save!
+ end
assert_operator debate.reload.updated_at, :>, time
assert_operator debate2.reload.updated_at, :>, time
diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb
index 0cb90781f1..065ba7734c 100644
--- a/activerecord/test/cases/tasks/postgresql_rake_test.rb
+++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb
@@ -366,7 +366,7 @@ if current_adapter?(:PostgreSQLAdapter)
assert_called_with(
Kernel,
:system,
- ["pg_dump", "-s", "-X", "-x", "-O", "-f", @filename, "my-app-db"],
+ ["pg_dump", "-s", "-x", "-O", "-f", @filename, "my-app-db"],
returns: true
) do
ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
@@ -383,7 +383,7 @@ if current_adapter?(:PostgreSQLAdapter)
end
def test_structure_dump_with_extra_flags
- expected_command = ["pg_dump", "-s", "-X", "-x", "-O", "-f", @filename, "--noop", "my-app-db"]
+ expected_command = ["pg_dump", "-s", "-x", "-O", "-f", @filename, "--noop", "my-app-db"]
assert_called_with(Kernel, :system, expected_command, returns: true) do
with_structure_dump_flags(["--noop"]) do
@@ -401,7 +401,7 @@ if current_adapter?(:PostgreSQLAdapter)
assert_called_with(
Kernel,
:system,
- ["pg_dump", "-s", "-X", "-x", "-O", "-f", @filename, "-T", "foo", "-T", "bar", "my-app-db"],
+ ["pg_dump", "-s", "-x", "-O", "-f", @filename, "-T", "foo", "-T", "bar", "my-app-db"],
returns: true
) do
ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
@@ -415,7 +415,7 @@ if current_adapter?(:PostgreSQLAdapter)
assert_called_with(
Kernel,
:system,
- ["pg_dump", "-s", "-X", "-x", "-O", "-f", @filename, "--schema=foo", "--schema=bar", "my-app-db"],
+ ["pg_dump", "-s", "-x", "-O", "-f", @filename, "--schema=foo", "--schema=bar", "my-app-db"],
returns: true
) do
ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
@@ -428,7 +428,7 @@ if current_adapter?(:PostgreSQLAdapter)
assert_called_with(
Kernel,
:system,
- ["pg_dump", "-s", "-X", "-x", "-O", "-f", @filename, "my-app-db"],
+ ["pg_dump", "-s", "-x", "-O", "-f", @filename, "my-app-db"],
returns: true
) do
with_dump_schemas(:all) do
@@ -441,7 +441,7 @@ if current_adapter?(:PostgreSQLAdapter)
assert_called_with(
Kernel,
:system,
- ["pg_dump", "-s", "-X", "-x", "-O", "-f", @filename, "--schema=foo", "--schema=bar", "my-app-db"],
+ ["pg_dump", "-s", "-x", "-O", "-f", @filename, "--schema=foo", "--schema=bar", "my-app-db"],
returns: true
) do
with_dump_schemas("foo,bar") do
@@ -455,7 +455,7 @@ if current_adapter?(:PostgreSQLAdapter)
assert_called_with(
Kernel,
:system,
- ["pg_dump", "-s", "-X", "-x", "-O", "-f", filename, "my-app-db"],
+ ["pg_dump", "-s", "-x", "-O", "-f", filename, "my-app-db"],
returns: nil
) do
e = assert_raise(RuntimeError) do
@@ -496,7 +496,7 @@ if current_adapter?(:PostgreSQLAdapter)
assert_called_with(
Kernel,
:system,
- ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-f", filename, @configuration["database"]],
+ ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-X", "-f", filename, @configuration["database"]],
returns: true
) do
ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
@@ -505,7 +505,7 @@ if current_adapter?(:PostgreSQLAdapter)
def test_structure_load_with_extra_flags
filename = "awesome-file.sql"
- expected_command = ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-f", filename, "--noop", @configuration["database"]]
+ expected_command = ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-X", "-f", filename, "--noop", @configuration["database"]]
assert_called_with(Kernel, :system, expected_command, returns: true) do
with_structure_load_flags(["--noop"]) do
@@ -519,7 +519,7 @@ if current_adapter?(:PostgreSQLAdapter)
assert_called_with(
Kernel,
:system,
- ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-f", filename, @configuration["database"]],
+ ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-X", "-f", filename, @configuration["database"]],
returns: true
) do
ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md
index 4342cf6968..a839f80eb3 100644
--- a/railties/CHANGELOG.md
+++ b/railties/CHANGELOG.md
@@ -1,3 +1,19 @@
+* Adds an option to the model generator to allow setting the
+ migrations paths for that migration. This is useful for
+ applications that use multiple databases and put migrations
+ per database in their own directories.
+
+ ```
+ bin/rails g model Room capacity:integer --migrations-paths=db/kingston_migrate
+ invoke active_record
+ create db/kingston_migrate/20180830151055_create_rooms.rb
+ ```
+
+ Because rails scaffolding uses the model generator, you can
+ also specify migrations paths with the scaffold generator.
+
+ *Gannon McGibbon*
+
* Raise an error when "recyclable cache keys" are being used by a cache store
that does not explicitly support it. Custom cache keys that do support this feature
can bypass this error by implementing the `supports_cache_versioning?` method on their
diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb
index 6a13a84108..901934826b 100644
--- a/railties/lib/rails/engine.rb
+++ b/railties/lib/rails/engine.rb
@@ -403,6 +403,12 @@ module Rails
define_method(:railtie_helpers_paths) { railtie.helpers_paths }
end
+ unless mod.respond_to?(:railtie_include_helpers)
+ define_method(:railtie_include_helpers) { |klass, include_path_helpers|
+ railtie.routes.include_helpers(klass, include_path_helpers)
+ }
+ end
+
unless mod.respond_to?(:railtie_routes_url_helpers)
define_method(:railtie_routes_url_helpers) { |include_path_helpers = true| railtie.routes.url_helpers(include_path_helpers) }
end
@@ -473,9 +479,13 @@ module Rails
# files inside eager_load paths.
def eager_load!
config.eager_load_paths.each do |load_path|
- matcher = /\A#{Regexp.escape(load_path.to_s)}\/(.*)\.rb\Z/
- Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
- require_dependency file.sub(matcher, '\1')
+ if File.file?(load_path)
+ require_dependency load_path
+ else
+ matcher = /\A#{Regexp.escape(load_path.to_s)}\/(.*)\.rb\Z/
+ Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
+ require_dependency file.sub(matcher, '\1')
+ end
end
end
end
diff --git a/railties/lib/rails/engine/configuration.rb b/railties/lib/rails/engine/configuration.rb
index 6bf0406b21..7595272c03 100644
--- a/railties/lib/rails/engine/configuration.rb
+++ b/railties/lib/rails/engine/configuration.rb
@@ -38,6 +38,7 @@ module Rails
@paths ||= begin
paths = Rails::Paths::Root.new(@root)
+ paths.add "config/routes.rb", eager_load: true
paths.add "app", eager_load: true, glob: "{*,*/concerns}"
paths.add "app/assets", glob: "*"
paths.add "app/controllers", eager_load: true
@@ -55,7 +56,6 @@ module Rails
paths.add "config/environments", glob: "#{Rails.env}.rb"
paths.add "config/initializers", glob: "**/*.rb"
paths.add "config/locales", glob: "*.{rb,yml}"
- paths.add "config/routes.rb"
paths.add "db"
paths.add "db/migrate"
diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb
index 7febdfae96..5a0c2f74c7 100644
--- a/railties/test/generators/model_generator_test.rb
+++ b/railties/test/generators/model_generator_test.rb
@@ -392,6 +392,15 @@ class ModelGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_migrations_paths_puts_migrations_in_that_folder
+ run_generator ["account", "--migrations_paths=db/test_migrate"]
+ assert_migration "db/test_migrate/create_accounts.rb" do |content|
+ assert_method :change, content do |change|
+ assert_match(/create_table :accounts/, change)
+ end
+ end
+ end
+
def test_required_belongs_to_adds_required_association
run_generator ["account", "supplier:references{required}"]
diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb
index e90834bc2b..dbcf49290e 100644
--- a/railties/test/generators/scaffold_generator_test.rb
+++ b/railties/test/generators/scaffold_generator_test.rb
@@ -476,6 +476,12 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_scaffold_generator_migrations_paths
+ run_generator ["posts", "--migrations-paths=db/kingston_migrate"]
+
+ assert_migration "db/kingston_migrate/create_posts.rb"
+ end
+
def test_scaffold_generator_password_digest
run_generator ["user", "name", "password:digest"]