aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock20
-rw-r--r--actioncable/CHANGELOG.md23
-rw-r--r--actioncable/lib/action_cable/server/base.rb7
-rw-r--r--actioncable/test/subscription_adapter/channel_prefix.rb10
-rw-r--r--actionmailer/lib/action_mailer/base.rb2
-rw-r--r--actionmailer/lib/action_mailer/log_subscriber.rb5
-rw-r--r--actionmailer/lib/action_mailer/railtie.rb16
-rw-r--r--actionmailer/test/base_test.rb17
-rw-r--r--actionmailer/test/log_subscriber_test.rb8
-rw-r--r--actionmailer/test/mailers/base_mailer.rb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_view.rb4
-rw-r--r--actionpack/lib/action_dispatch/testing/integration.rb2
-rw-r--r--actionpack/test/dispatch/routing/non_dispatch_routed_app_test.rb27
-rw-r--r--actionview/lib/action_view/base.rb58
-rw-r--r--actionview/lib/action_view/helpers/rendering_helper.rb10
-rw-r--r--actionview/lib/action_view/lookup_context.rb7
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer.rb16
-rw-r--r--actionview/lib/action_view/rendering.rb4
-rw-r--r--actionview/lib/action_view/template/handlers/erb/erubi.rb2
-rw-r--r--actionview/test/fixtures/test/_first.html.erb1
-rw-r--r--actionview/test/fixtures/test/_first.xml.erb1
-rw-r--r--actionview/test/fixtures/test/_first_layer.html.erb4
-rw-r--r--actionview/test/fixtures/test/_first_layer.xml.erb4
-rw-r--r--actionview/test/fixtures/test/_second.html.erb1
-rw-r--r--actionview/test/fixtures/test/_second.xml.erb1
-rw-r--r--actionview/test/fixtures/test/_second_layer.html.erb4
-rw-r--r--actionview/test/fixtures/test/_second_layer.xml.erb4
-rw-r--r--actionview/test/fixtures/test/mixing_formats.html.erb5
-rw-r--r--actionview/test/fixtures/test/mixing_formats_deep.html.erb5
-rw-r--r--actionview/test/template/csp_helper_test.rb31
-rw-r--r--actionview/test/template/log_subscriber_test.rb3
-rw-r--r--actionview/test/template/render_test.rb33
-rw-r--r--activejob/lib/active_job/core.rb7
-rw-r--r--activejob/lib/active_job/logging.rb2
-rw-r--r--activejob/lib/active_job/queue_name.rb20
-rw-r--r--activejob/test/cases/job_serialization_test.rb11
-rw-r--r--activejob/test/cases/logging_test.rb2
-rw-r--r--activerecord/CHANGELOG.md6
-rw-r--r--activerecord/Rakefile39
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb3
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb14
-rw-r--r--activerecord/lib/active_record/database_configurations.rb2
-rw-r--r--activerecord/lib/active_record/migration/compatibility.rb26
-rw-r--r--activerecord/lib/active_record/querying.rb3
-rw-r--r--activerecord/lib/active_record/relation.rb4
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb6
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb22
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb2
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb9
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb50
-rw-r--r--activerecord/test/cases/base_test.rb5
-rw-r--r--activerecord/test/cases/calculations_test.rb5
-rw-r--r--activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb10
-rw-r--r--activerecord/test/cases/database_selector_test.rb4
-rw-r--r--activerecord/test/cases/relations_test.rb7
-rw-r--r--activerecord/test/cases/scoping/named_scoping_test.rb5
-rw-r--r--activerecord/test/models/company.rb2
-rw-r--r--activerecord/test/models/contract.rb8
-rw-r--r--activerecord/test/models/post.rb1
-rw-r--r--activerecord/test/models/reply.rb2
-rw-r--r--activerecord/test/models/topic.rb3
-rw-r--r--activerecord/test/schema/schema.rb1
-rw-r--r--activesupport/CHANGELOG.md8
-rw-r--r--activesupport/activesupport.gemspec7
-rw-r--r--activesupport/lib/active_support/cache/file_store.rb13
-rw-r--r--activesupport/lib/active_support/dependencies/zeitwerk_integration.rb75
-rw-r--r--activesupport/lib/active_support/notifications.rb9
-rw-r--r--activesupport/lib/active_support/notifications/fanout.rb38
-rw-r--r--activesupport/test/cache/stores/file_store_test.rb2
-rw-r--r--activesupport/test/notifications/evented_notification_test.rb33
-rw-r--r--activesupport/test/notifications_test.rb19
-rw-r--r--guides/source/action_cable_overview.md20
-rw-r--r--guides/source/configuring.md3
-rw-r--r--railties/CHANGELOG.md5
-rw-r--r--railties/lib/rails.rb20
-rw-r--r--railties/lib/rails/application.rb38
-rw-r--r--railties/lib/rails/application/configuration.rb17
-rw-r--r--railties/lib/rails/application/finisher.rb8
-rw-r--r--railties/lib/rails/command/behavior.rb5
-rw-r--r--railties/lib/rails/engine.rb26
-rw-r--r--railties/lib/rails/generators/rails/app/templates/Gemfile.tt2
-rw-r--r--railties/test/application/configuration_test.rb136
-rw-r--r--railties/test/application/mailer_previews_test.rb3
-rw-r--r--railties/test/application/multiple_applications_test.rb24
-rw-r--r--railties/test/application/rake_test.rb4
-rw-r--r--railties/test/application/zeitwerk_integration_test.rb164
-rw-r--r--railties/test/isolation/abstract_unit.rb4
-rw-r--r--railties/test/isolation/assets/package.json2
-rw-r--r--railties/test/railties/engine_test.rb32
90 files changed, 1085 insertions, 254 deletions
diff --git a/Gemfile b/Gemfile
index f665321625..3e91ac321d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -44,7 +44,7 @@ gem "libxml-ruby", platforms: :ruby
gem "connection_pool", require: false
# for railties app_generator_test
-gem "bootsnap", ">= 1.1.0", require: false
+gem "bootsnap", ">= 1.4.0", require: false
# Active Job
group :job do
diff --git a/Gemfile.lock b/Gemfile.lock
index b05ae7915e..b89c64d34e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -70,6 +70,7 @@ PATH
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
+ zeitwerk (~> 1.0)
rails (6.0.0.beta1)
actioncable (= 6.0.0.beta1)
actionmailbox (= 6.0.0.beta1)
@@ -166,9 +167,9 @@ GEM
childprocess
faraday
selenium-webdriver
- bootsnap (1.3.2)
+ bootsnap (1.4.0)
msgpack (~> 1.0)
- bootsnap (1.3.2-java)
+ bootsnap (1.4.0-java)
msgpack (~> 1.0)
builder (3.2.3)
bunny (2.13.0)
@@ -276,7 +277,7 @@ GEM
hiredis (0.6.3-java)
http_parser.rb (0.6.0)
httpclient (2.8.3)
- i18n (1.5.2)
+ i18n (1.5.3)
concurrent-ruby (~> 1.0)
image_processing (1.7.1)
mini_magick (~> 4.0)
@@ -324,10 +325,10 @@ GEM
minitest-server (1.0.5)
minitest (~> 5.0)
mono_logger (1.1.0)
- msgpack (1.2.4)
- msgpack (1.2.4-java)
- msgpack (1.2.4-x64-mingw32)
- msgpack (1.2.4-x86-mingw32)
+ msgpack (1.2.6)
+ msgpack (1.2.6-java)
+ msgpack (1.2.6-x64-mingw32)
+ msgpack (1.2.6-x86-mingw32)
multi_json (1.13.1)
multipart-post (2.0.0)
mustache (1.1.0)
@@ -516,6 +517,7 @@ GEM
websocket-extensions (0.1.3)
xpath (3.2.0)
nokogiri (~> 1.8)
+ zeitwerk (1.0.0)
PLATFORMS
java
@@ -535,7 +537,7 @@ DEPENDENCIES
benchmark-ips
blade
blade-sauce_labs_plugin
- bootsnap (>= 1.1.0)
+ bootsnap (>= 1.4.0)
byebug
capybara (>= 2.15)
chromedriver-helper
@@ -590,4 +592,4 @@ DEPENDENCIES
websocket-client-simple!
BUNDLED WITH
- 1.17.2
+ 1.17.3
diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md
index 42edbd6954..3ce35edbb8 100644
--- a/actioncable/CHANGELOG.md
+++ b/actioncable/CHANGELOG.md
@@ -1,3 +1,26 @@
+* Allow passing custom configuration to `ActionCable::Server::Base`.
+
+ You can now create a standalone Action Cable server with a custom configuration
+ (e.g. to run it in isolation from the default one):
+
+ ```ruby
+ config = ActionCable::Server::Configuration.new
+ config.cable = { adapter: "redis", channel_prefix: "custom_" }
+
+ CUSTOM_CABLE = ActionCable::Server::Base.new(config: config)
+ ```
+
+ Then you can mount it in the `routes.rb` file:
+
+ ```ruby
+ Rails.application.routes.draw do
+ mount CUSTOM_CABLE => "/custom_cable"
+ # ...
+ end
+ ```
+
+ *Vladimir Dementyev*
+
* Add `:action_cable_connection` and `:action_cable_channel` load hooks.
You can use them to extend `ActionCable::Connection::Base` and `ActionCable::Channel::Base`
diff --git a/actioncable/lib/action_cable/server/base.rb b/actioncable/lib/action_cable/server/base.rb
index 2b9e1cba3b..98b3743175 100644
--- a/actioncable/lib/action_cable/server/base.rb
+++ b/actioncable/lib/action_cable/server/base.rb
@@ -12,14 +12,17 @@ module ActionCable
include ActionCable::Server::Broadcasting
include ActionCable::Server::Connections
- cattr_accessor :config, instance_accessor: true, default: ActionCable::Server::Configuration.new
+ cattr_accessor :config, instance_accessor: false, default: ActionCable::Server::Configuration.new
+
+ attr_reader :config
def self.logger; config.logger; end
delegate :logger, to: :config
attr_reader :mutex
- def initialize
+ def initialize(config: self.class.config)
+ @config = config
@mutex = Monitor.new
@remote_connections = @event_loop = @worker_pool = @pubsub = nil
end
diff --git a/actioncable/test/subscription_adapter/channel_prefix.rb b/actioncable/test/subscription_adapter/channel_prefix.rb
index 3071facd9d..475e6cfd3a 100644
--- a/actioncable/test/subscription_adapter/channel_prefix.rb
+++ b/actioncable/test/subscription_adapter/channel_prefix.rb
@@ -2,17 +2,9 @@
require "test_helper"
-class ActionCable::Server::WithIndependentConfig < ActionCable::Server::Base
- # ActionCable::Server::Base defines config as a class variable.
- # Need config to be an instance variable here as we're testing 2 separate configs
- def config
- @config ||= ActionCable::Server::Configuration.new
- end
-end
-
module ChannelPrefixTest
def test_channel_prefix
- server2 = ActionCable::Server::WithIndependentConfig.new
+ server2 = ActionCable::Server::Base.new(config: ActionCable::Server::Configuration.new)
server2.config.cable = alt_cable_config
server2.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN }
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb
index 5610212fad..033a7f5b9e 100644
--- a/actionmailer/lib/action_mailer/base.rb
+++ b/actionmailer/lib/action_mailer/base.rb
@@ -593,6 +593,7 @@ module ActionMailer
private
def set_payload_for_mail(payload, mail)
+ payload[:mail] = mail.encoded
payload[:mailer] = name
payload[:message_id] = mail.message_id
payload[:subject] = mail.subject
@@ -601,7 +602,6 @@ module ActionMailer
payload[:bcc] = mail.bcc if mail.bcc.present?
payload[:cc] = mail.cc if mail.cc.present?
payload[:date] = mail.date
- payload[:mail] = mail.encoded
payload[:perform_deliveries] = mail.perform_deliveries
end
diff --git a/actionmailer/lib/action_mailer/log_subscriber.rb b/actionmailer/lib/action_mailer/log_subscriber.rb
index 25c99342c2..26910f20f0 100644
--- a/actionmailer/lib/action_mailer/log_subscriber.rb
+++ b/actionmailer/lib/action_mailer/log_subscriber.rb
@@ -10,11 +10,10 @@ module ActionMailer
def deliver(event)
info do
perform_deliveries = event.payload[:perform_deliveries]
- recipients = Array(event.payload[:to]).join(", ")
if perform_deliveries
- "Sent mail to #{recipients} (#{event.duration.round(1)}ms)"
+ "Delivered mail #{event.payload[:message_id]} (#{event.duration.round(1)}ms)"
else
- "Skipped sending mail to #{recipients} as `perform_deliveries` is false"
+ "Skipped delivery of mail #{event.payload[:message_id]} as `perform_deliveries` is false"
end
end
diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb
index 23488db790..893a4a25b1 100644
--- a/actionmailer/lib/action_mailer/railtie.rb
+++ b/actionmailer/lib/action_mailer/railtie.rb
@@ -59,6 +59,14 @@ module ActionMailer
end
end
+ initializer "action_mailer.set_autoload_paths" do |app|
+ options = app.config.action_mailer
+
+ if options.show_previews && options.preview_path
+ ActiveSupport::Dependencies.autoload_paths << options.preview_path
+ end
+ end
+
initializer "action_mailer.compile_config_methods" do
ActiveSupport.on_load(:action_mailer) do
config.compile_methods! if config.respond_to?(:compile_methods!)
@@ -76,12 +84,8 @@ module ActionMailer
if options.show_previews
app.routes.prepend do
- get "/rails/mailers" => "rails/mailers#index", internal: true
- get "/rails/mailers/*path" => "rails/mailers#preview", internal: true
- end
-
- if options.preview_path
- ActiveSupport::Dependencies.autoload_paths << options.preview_path
+ get "/rails/mailers" => "rails/mailers#index", internal: true
+ get "/rails/mailers/*path" => "rails/mailers#preview", internal: true
end
end
end
diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb
index d0c4f189fd..15613d4dce 100644
--- a/actionmailer/test/base_test.rb
+++ b/actionmailer/test/base_test.rb
@@ -913,6 +913,23 @@ class BaseTest < ActiveSupport::TestCase
ActiveSupport::Notifications.unsubscribe "process.action_mailer"
end
+ test "notification for deliver" do
+ begin
+ events = []
+ ActiveSupport::Notifications.subscribe("deliver.action_mailer") do |*args|
+ events << ActiveSupport::Notifications::Event.new(*args)
+ end
+
+ BaseMailer.welcome(body: "Hello there").deliver_now
+
+ assert_equal 1, events.length
+ assert_equal "deliver.action_mailer", events[0].name
+ assert_not_nil events[0].payload[:message_id]
+ ensure
+ ActiveSupport::Notifications.unsubscribe "deliver.action_mailer"
+ end
+ end
+
private
# Execute the block setting the given values and restoring old values after
diff --git a/actionmailer/test/log_subscriber_test.rb b/actionmailer/test/log_subscriber_test.rb
index fb569ce45f..f09f1997c0 100644
--- a/actionmailer/test/log_subscriber_test.rb
+++ b/actionmailer/test/log_subscriber_test.rb
@@ -24,11 +24,11 @@ class AMLogSubscriberTest < ActionMailer::TestCase
end
def test_deliver_is_notified
- BaseMailer.welcome.deliver_now
+ BaseMailer.welcome(message_id: "123@abc").deliver_now
wait
assert_equal(1, @logger.logged(:info).size)
- assert_match(/Sent mail to system@test\.lindsaar\.net/, @logger.logged(:info).first)
+ assert_match(/Delivered mail 123@abc/, @logger.logged(:info).first)
assert_equal(2, @logger.logged(:debug).size)
assert_match(/BaseMailer#welcome: processed outbound mail in [\d.]+ms/, @logger.logged(:debug).first)
@@ -38,11 +38,11 @@ class AMLogSubscriberTest < ActionMailer::TestCase
end
def test_deliver_message_when_perform_deliveries_is_false
- BaseMailer.welcome_without_deliveries.deliver_now
+ BaseMailer.welcome_without_deliveries(message_id: "123@abc").deliver_now
wait
assert_equal(1, @logger.logged(:info).size)
- assert_match("Skipped sending mail to system@test.lindsaar.net as `perform_deliveries` is false", @logger.logged(:info).first)
+ assert_match("Skipped delivery of mail 123@abc as `perform_deliveries` is false", @logger.logged(:info).first)
assert_equal(2, @logger.logged(:debug).size)
assert_match(/BaseMailer#welcome_without_deliveries: processed outbound mail in [\d.]+ms/, @logger.logged(:debug).first)
diff --git a/actionmailer/test/mailers/base_mailer.rb b/actionmailer/test/mailers/base_mailer.rb
index c1bb48cc96..dbe1c4f0e6 100644
--- a/actionmailer/test/mailers/base_mailer.rb
+++ b/actionmailer/test/mailers/base_mailer.rb
@@ -21,8 +21,8 @@ class BaseMailer < ActionMailer::Base
mail(template_name: "welcome", template_path: path)
end
- def welcome_without_deliveries
- mail(template_name: "welcome")
+ def welcome_without_deliveries(hash = {})
+ mail({ template_name: "welcome" }.merge!(hash))
mail.perform_deliveries = false
end
diff --git a/actionpack/lib/action_dispatch/middleware/debug_view.rb b/actionpack/lib/action_dispatch/middleware/debug_view.rb
index f16484d1ea..43c0a84504 100644
--- a/actionpack/lib/action_dispatch/middleware/debug_view.rb
+++ b/actionpack/lib/action_dispatch/middleware/debug_view.rb
@@ -11,8 +11,8 @@ module ActionDispatch
def initialize(assigns)
paths = [RESCUES_TEMPLATE_PATH]
- renderer = ActionView::Renderer.new ActionView::LookupContext.new(paths)
- super(renderer, assigns)
+ lookup_context = ActionView::LookupContext.new(paths)
+ super(lookup_context, assigns)
end
def compiled_method_container
diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb
index 45439a3bb1..10d85037ae 100644
--- a/actionpack/lib/action_dispatch/testing/integration.rb
+++ b/actionpack/lib/action_dispatch/testing/integration.rb
@@ -335,7 +335,7 @@ module ActionDispatch
klass = APP_SESSIONS[app] ||= Class.new(Integration::Session) {
# If the app is a Rails app, make url_helpers available on the session.
# This makes app.url_for and app.foo_path available in the console.
- if app.respond_to?(:routes)
+ if app.respond_to?(:routes) && app.routes.is_a?(ActionDispatch::Routing::RouteSet)
include app.routes.url_helpers
include app.routes.mounted_helpers
end
diff --git a/actionpack/test/dispatch/routing/non_dispatch_routed_app_test.rb b/actionpack/test/dispatch/routing/non_dispatch_routed_app_test.rb
new file mode 100644
index 0000000000..676a8c38d4
--- /dev/null
+++ b/actionpack/test/dispatch/routing/non_dispatch_routed_app_test.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module ActionDispatch
+ module Routing
+ class NonDispatchRoutedAppTest < ActionDispatch::IntegrationTest
+ # For example, Grape::API
+ class SimpleApp
+ def self.call(env)
+ [ 200, { "Content-Type" => "text/plain" }, [] ]
+ end
+
+ def self.routes
+ []
+ end
+ end
+
+ setup { @app = SimpleApp }
+
+ test "does not except" do
+ get "/foo"
+ assert_response :success
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb
index 420136d6de..712e5d251e 100644
--- a/actionview/lib/action_view/base.rb
+++ b/actionview/lib/action_view/base.rb
@@ -196,10 +196,9 @@ module ActionView #:nodoc:
end
end
- attr_reader :view_renderer
+ attr_reader :view_renderer, :lookup_context
attr_internal :config, :assigns
- delegate :lookup_context, to: :view_renderer
delegate :formats, :formats=, :locale, :locale=, :view_paths, :view_paths=, to: :lookup_context
def assign(new_assigns) # :nodoc:
@@ -208,12 +207,17 @@ module ActionView #:nodoc:
# :stopdoc:
- def self.build_renderer(context, controller, formats)
- lookup_context = context.is_a?(ActionView::LookupContext) ?
- context : ActionView::LookupContext.new(context)
- lookup_context.formats = formats if formats
- lookup_context.prefixes = controller._prefixes if controller
- ActionView::Renderer.new(lookup_context)
+ def self.build_lookup_context(context)
+ case context
+ when ActionView::Renderer
+ context.lookup_context
+ when Array
+ ActionView::LookupContext.new(context)
+ when nil
+ ActionView::LookupContext.new([])
+ else
+ raise NotImplementedError, context.class.name
+ end
end
def self.empty
@@ -225,34 +229,35 @@ module ActionView #:nodoc:
end
def self.with_context(context, assigns = {}, controller = nil)
- new ActionView::Renderer.new(context), assigns, controller
+ new context, assigns, controller
end
NULL = Object.new
# :startdoc:
- def initialize(renderer = nil, assigns = {}, controller = nil, formats = NULL) #:nodoc:
+ def initialize(lookup_context = nil, assigns = {}, controller = nil, formats = NULL) #:nodoc:
@_config = ActiveSupport::InheritableOptions.new
- if formats == NULL
- formats = nil
- else
+ unless formats == NULL
ActiveSupport::Deprecation.warn <<~eowarn
Passing formats to ActionView::Base.new is deprecated
eowarn
end
- if renderer.is_a?(ActionView::Renderer)
- @view_renderer = renderer
+ case lookup_context
+ when ActionView::LookupContext
+ @lookup_context = lookup_context
else
ActiveSupport::Deprecation.warn <<~eowarn
- ActionView::Base instances should be constructed with a view renderer,
+ ActionView::Base instances should be constructed with a lookup context,
assigments, and a controller.
eowarn
- @view_renderer = self.class.build_renderer(renderer, controller, formats)
+ @lookup_context = self.class.build_lookup_context(lookup_context)
end
+ @view_renderer = ActionView::Renderer.new @lookup_context
+
@cache_hit = {}
assign(assigns)
assign_controller(controller)
@@ -275,6 +280,25 @@ module ActionView #:nodoc:
msg
end
+ def in_context(options, locals)
+ old_view_renderer = @view_renderer
+ old_lookup_context = @lookup_context
+
+ if !lookup_context.html_fallback_for_js && options[:formats]
+ formats = Array(options[:formats])
+ if formats == [:js]
+ formats << :html
+ end
+ @lookup_context = lookup_context.with_prepended_formats(formats)
+ @view_renderer = ActionView::Renderer.new @lookup_context
+ end
+
+ yield @view_renderer
+ ensure
+ @view_renderer = old_view_renderer
+ @lookup_context = old_lookup_context
+ end
+
ActiveSupport.run_load_hooks(:action_view, self)
end
end
diff --git a/actionview/lib/action_view/helpers/rendering_helper.rb b/actionview/lib/action_view/helpers/rendering_helper.rb
index 1e12aa2736..7323963c72 100644
--- a/actionview/lib/action_view/helpers/rendering_helper.rb
+++ b/actionview/lib/action_view/helpers/rendering_helper.rb
@@ -27,10 +27,12 @@ module ActionView
def render(options = {}, locals = {}, &block)
case options
when Hash
- if block_given?
- view_renderer.render_partial(self, options.merge(partial: options[:layout]), &block)
- else
- view_renderer.render(self, options)
+ in_context(options, locals) do |renderer|
+ if block_given?
+ view_renderer.render_partial(self, options.merge(partial: options[:layout]), &block)
+ else
+ view_renderer.render(self, options)
+ end
end
else
view_renderer.render_partial(self, partial: options, locals: locals, &block)
diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb
index 61fe11cf45..125ab4dbe3 100644
--- a/actionview/lib/action_view/lookup_context.rb
+++ b/actionview/lib/action_view/lookup_context.rb
@@ -260,6 +260,13 @@ module ActionView
@digest_cache ||= DetailsKey.digest_cache(@details)
end
+ def with_prepended_formats(formats)
+ details = @details.dup
+ details[:formats] = formats
+
+ self.class.new(@view_paths, details, @prefixes)
+ end
+
def initialize_details(target, details)
registered_details.each do |k|
target[k] = details[k] || Accessors::DEFAULT_PROCS[k].call
diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb
index 801916954f..f8a6f13ae9 100644
--- a/actionview/lib/action_view/renderer/partial_renderer.rb
+++ b/actionview/lib/action_view/renderer/partial_renderer.rb
@@ -308,6 +308,9 @@ module ActionView
template = find_partial(@path, @template_keys)
@variable ||= template.variable
else
+ if options[:cached]
+ raise NotImplementedError, "render caching requires a template. Please specify a partial when rendering"
+ end
template = nil
end
@@ -337,9 +340,14 @@ module ActionView
spacer = find_template(@options[:spacer_template], @locals.keys).render(view, @locals)
end
- cache_collection_render(payload, view, template) do
- template ? collection_with_template(view, template) : collection_without_template(view)
- end.join(spacer).html_safe
+ collection_body = if template
+ cache_collection_render(payload, view, template) do
+ collection_with_template(view, template)
+ end
+ else
+ collection_without_template(view)
+ end
+ collection_body.join(spacer).html_safe
end
end
@@ -379,8 +387,6 @@ module ActionView
@locals = options[:locals] || {}
@details = extract_details(options)
- prepend_formats(options[:formats])
-
partial = options[:partial]
if String === partial
diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb
index da92ce1f5e..b798e80b04 100644
--- a/actionview/lib/action_view/rendering.rb
+++ b/actionview/lib/action_view/rendering.rb
@@ -82,7 +82,7 @@ module ActionView
#
# Override this method in a module to change the default behavior.
def view_context
- view_context_class.new(view_renderer, view_assigns, self)
+ view_context_class.new(lookup_context, view_assigns, self)
end
# Returns an object that is able to render templates.
@@ -112,7 +112,7 @@ module ActionView
lookup_context.rendered_format = nil if options[:formats]
lookup_context.variants = variant if variant
- view_renderer.render(context, options)
+ context.view_renderer.render(context, options)
end
# Assign the rendered format to look up context.
diff --git a/actionview/lib/action_view/template/handlers/erb/erubi.rb b/actionview/lib/action_view/template/handlers/erb/erubi.rb
index 15ca202024..20510c3062 100644
--- a/actionview/lib/action_view/template/handlers/erb/erubi.rb
+++ b/actionview/lib/action_view/template/handlers/erb/erubi.rb
@@ -26,7 +26,7 @@ module ActionView
view = Class.new(ActionView::Base) {
include action_view_erb_handler_context._routes.url_helpers
class_eval("define_method(:_template) { |local_assigns, output_buffer| #{src} }", @filename || "(erubi)", 0)
- }.with_context(action_view_erb_handler_context)
+ }.empty
view.run(:_template, {}, ActionView::OutputBuffer.new)
end
diff --git a/actionview/test/fixtures/test/_first.html.erb b/actionview/test/fixtures/test/_first.html.erb
new file mode 100644
index 0000000000..2e2c825acb
--- /dev/null
+++ b/actionview/test/fixtures/test/_first.html.erb
@@ -0,0 +1 @@
+"HTML"
diff --git a/actionview/test/fixtures/test/_first.xml.erb b/actionview/test/fixtures/test/_first.xml.erb
new file mode 100644
index 0000000000..9cf4f4ec85
--- /dev/null
+++ b/actionview/test/fixtures/test/_first.xml.erb
@@ -0,0 +1 @@
+"XML"
diff --git a/actionview/test/fixtures/test/_first_layer.html.erb b/actionview/test/fixtures/test/_first_layer.html.erb
new file mode 100644
index 0000000000..c1f1acb410
--- /dev/null
+++ b/actionview/test/fixtures/test/_first_layer.html.erb
@@ -0,0 +1,4 @@
+{"format":"HTML", "children":
+[
+ <%= render(partial: "first").chomp.html_safe %>,
+]}
diff --git a/actionview/test/fixtures/test/_first_layer.xml.erb b/actionview/test/fixtures/test/_first_layer.xml.erb
new file mode 100644
index 0000000000..b8581bbbfc
--- /dev/null
+++ b/actionview/test/fixtures/test/_first_layer.xml.erb
@@ -0,0 +1,4 @@
+{"format":"XML", "children":
+[
+ <%= render(partial: "first").chomp.html_safe %>
+]}
diff --git a/actionview/test/fixtures/test/_second.html.erb b/actionview/test/fixtures/test/_second.html.erb
new file mode 100644
index 0000000000..2e2c825acb
--- /dev/null
+++ b/actionview/test/fixtures/test/_second.html.erb
@@ -0,0 +1 @@
+"HTML"
diff --git a/actionview/test/fixtures/test/_second.xml.erb b/actionview/test/fixtures/test/_second.xml.erb
new file mode 100644
index 0000000000..9cf4f4ec85
--- /dev/null
+++ b/actionview/test/fixtures/test/_second.xml.erb
@@ -0,0 +1 @@
+"XML"
diff --git a/actionview/test/fixtures/test/_second_layer.html.erb b/actionview/test/fixtures/test/_second_layer.html.erb
new file mode 100644
index 0000000000..307706abd2
--- /dev/null
+++ b/actionview/test/fixtures/test/_second_layer.html.erb
@@ -0,0 +1,4 @@
+{"format":"HTML", "children":
+[
+ <%= render(partial: "first").chomp.html_safe %>
+]}
diff --git a/actionview/test/fixtures/test/_second_layer.xml.erb b/actionview/test/fixtures/test/_second_layer.xml.erb
new file mode 100644
index 0000000000..b8581bbbfc
--- /dev/null
+++ b/actionview/test/fixtures/test/_second_layer.xml.erb
@@ -0,0 +1,4 @@
+{"format":"XML", "children":
+[
+ <%= render(partial: "first").chomp.html_safe %>
+]}
diff --git a/actionview/test/fixtures/test/mixing_formats.html.erb b/actionview/test/fixtures/test/mixing_formats.html.erb
new file mode 100644
index 0000000000..c65cdd7dd4
--- /dev/null
+++ b/actionview/test/fixtures/test/mixing_formats.html.erb
@@ -0,0 +1,5 @@
+{"format":"HTML", "children":
+[
+ <%= render(partial: "first", formats: :xml).chomp.html_safe %>,
+ <%= render(partial: "second").chomp.html_safe %>
+]}
diff --git a/actionview/test/fixtures/test/mixing_formats_deep.html.erb b/actionview/test/fixtures/test/mixing_formats_deep.html.erb
new file mode 100644
index 0000000000..e328887eeb
--- /dev/null
+++ b/actionview/test/fixtures/test/mixing_formats_deep.html.erb
@@ -0,0 +1,5 @@
+{"format":"HTML", "children":
+[
+ <%= render(partial: "first_layer", formats: :xml).chomp.html_safe %>,
+ <%= render(partial: "second_layer").chomp.html_safe %>
+]}
diff --git a/actionview/test/template/csp_helper_test.rb b/actionview/test/template/csp_helper_test.rb
new file mode 100644
index 0000000000..8bad25ba7d
--- /dev/null
+++ b/actionview/test/template/csp_helper_test.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class CspHelperWithCspEnabledTest < ActionView::TestCase
+ tests ActionView::Helpers::CspHelper
+
+ def content_security_policy_nonce
+ "iyhD0Yc0W+c="
+ end
+
+ def content_security_policy?
+ true
+ end
+
+ def test_csp_meta_tag
+ assert_equal "<meta name=\"csp-nonce\" content=\"iyhD0Yc0W+c=\" />", csp_meta_tag
+ end
+end
+
+class CspHelperWithCspDisabledTest < ActionView::TestCase
+ tests ActionView::Helpers::CspHelper
+
+ def content_security_policy?
+ false
+ end
+
+ def test_csp_meta_tag
+ assert_nil csp_meta_tag
+ end
+end
diff --git a/actionview/test/template/log_subscriber_test.rb b/actionview/test/template/log_subscriber_test.rb
index 83bb651ea3..85735139c1 100644
--- a/actionview/test/template/log_subscriber_test.rb
+++ b/actionview/test/template/log_subscriber_test.rb
@@ -16,8 +16,7 @@ class AVLogSubscriberTest < ActiveSupport::TestCase
view_paths = ActionController::Base.view_paths
lookup_context = ActionView::LookupContext.new(view_paths, {}, ["test"])
- renderer = ActionView::Renderer.new(lookup_context)
- @view = ActionView::Base.with_empty_template_cache.new(renderer, {})
+ @view = ActionView::Base.with_empty_template_cache.new(lookup_context, {})
ActionView::LogSubscriber.attach_to :action_view
diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb
index 3f298d81f3..cda8c942d8 100644
--- a/actionview/test/template/render_test.rb
+++ b/actionview/test/template/render_test.rb
@@ -20,7 +20,10 @@ module RenderTestCases
controller = TestController.new
- @controller_view = controller.view_context_class.with_empty_template_cache.new(controller.view_renderer, controller.view_assigns, controller)
+ @controller_view = controller.view_context_class.with_empty_template_cache.new(
+ controller.lookup_context,
+ controller.view_assigns,
+ controller)
# Reload and register danish language for testing
I18n.backend.store_translations "da", {}
@@ -30,6 +33,21 @@ module RenderTestCases
assert_equal ORIGINAL_LOCALES, I18n.available_locales.map(&:to_s).sort
end
+ def test_implicit_format_comes_from_parent_template
+ rendered_templates = JSON.parse(@controller_view.render(template: "test/mixing_formats"))
+ assert_equal({ "format" => "HTML",
+ "children" => ["XML", "HTML"] }, rendered_templates)
+ end
+
+ def test_implicit_format_comes_from_parent_template_cascading
+ rendered_templates = JSON.parse(@controller_view.render(template: "test/mixing_formats_deep"))
+ assert_equal({ "format" => "HTML",
+ "children" => [
+ { "format" => "XML", "children" => ["XML"] },
+ { "format" => "HTML", "children" => ["HTML"] },
+ ] }, rendered_templates)
+ end
+
def test_render_without_options
e = assert_raises(ArgumentError) { @view.render() }
assert_match(/You invoked render but did not give any of (.+) option\./, e.message)
@@ -68,7 +86,7 @@ module RenderTestCases
def test_render_partial_use_last_prepended_format_for_partials_with_the_same_names
@view.lookup_context.formats = [:html]
- assert_equal "\nHTML Template, but JSON partial", @view.render(template: "test/change_priority")
+ assert_equal "\nHTML Template, but HTML partial", @view.render(template: "test/change_priority")
end
def test_render_template_with_a_missing_partial_of_another_format
@@ -752,6 +770,17 @@ class CachedCollectionViewRenderTest < ActiveSupport::TestCase
@view.render(partial: "test/cached_customer", collection: [customer], cached: true)
end
+ test "collection caching does not work on multi-partials" do
+ a = Object.new
+ b = Object.new
+ def a.to_partial_path; "test/partial_iteration_1"; end
+ def b.to_partial_path; "test/partial_iteration_2"; end
+
+ assert_raises(NotImplementedError) do
+ @controller_view.render(partial: [a, b], cached: true)
+ end
+ end
+
private
def cache_key(*names, virtual_path)
digest = ActionView::Digestor.digest name: virtual_path, finder: @view.lookup_context, dependencies: []
diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb
index 487cdd6d38..2ce008e3da 100644
--- a/activejob/lib/active_job/core.rb
+++ b/activejob/lib/active_job/core.rb
@@ -40,6 +40,9 @@ module ActiveJob
# Timezone to be used during the job.
attr_accessor :timezone
+ # Track when a job was enqueded
+ attr_accessor :enqueued_at
+
# These methods will be included into any Active Job object, adding
# helpers for de/serialization and creation of job instances.
module ClassMethods
@@ -97,7 +100,8 @@ module ActiveJob
"executions" => executions,
"exception_executions" => exception_executions,
"locale" => I18n.locale.to_s,
- "timezone" => Time.zone.try(:name)
+ "timezone" => Time.zone.try(:name),
+ "enqueued_at" => Time.now.utc.iso8601
}
end
@@ -137,6 +141,7 @@ module ActiveJob
self.exception_executions = job_data["exception_executions"]
self.locale = job_data["locale"] || I18n.locale.to_s
self.timezone = job_data["timezone"] || Time.zone.try(:name)
+ self.enqueued_at = job_data["enqueued_at"]
end
private
diff --git a/activejob/lib/active_job/logging.rb b/activejob/lib/active_job/logging.rb
index 416be83c24..1134e718a8 100644
--- a/activejob/lib/active_job/logging.rb
+++ b/activejob/lib/active_job/logging.rb
@@ -70,7 +70,7 @@ module ActiveJob
def perform_start(event)
info do
job = event.payload[:job]
- "Performing #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)}" + args_info(job)
+ "Performing #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)} enqueued at #{job.enqueued_at}" + args_info(job)
end
end
diff --git a/activejob/lib/active_job/queue_name.rb b/activejob/lib/active_job/queue_name.rb
index 7bb1e35181..de259261de 100644
--- a/activejob/lib/active_job/queue_name.rb
+++ b/activejob/lib/active_job/queue_name.rb
@@ -18,6 +18,26 @@ module ActiveJob
# post.to_feed!
# end
# end
+ #
+ # Can be given a block that will evaluate in the context of the job
+ # allowing +self.arguments+ to be accessed so that a dynamic queue name
+ # can be applied:
+ #
+ # class PublishToFeedJob < ApplicationJob
+ # queue_as do
+ # post = self.arguments.first
+ #
+ # if post.paid?
+ # :paid_feeds
+ # else
+ # :feeds
+ # end
+ # end
+ #
+ # def perform(post)
+ # post.to_feed!
+ # end
+ # end
def queue_as(part_name = nil, &block)
if block_given?
self.queue_name = block
diff --git a/activejob/test/cases/job_serialization_test.rb b/activejob/test/cases/job_serialization_test.rb
index 86f3651564..c1cec1f1d6 100644
--- a/activejob/test/cases/job_serialization_test.rb
+++ b/activejob/test/cases/job_serialization_test.rb
@@ -61,4 +61,15 @@ class JobSerializationTest < ActiveSupport::TestCase
assert_equal "Hawaii", job.serialize["timezone"]
end
end
+
+ test "serialize stores the enqueued_at time" do
+ h1 = HelloJob.new
+ type = h1.serialize["enqueued_at"].class
+ assert_equal String, type
+
+ h2 = HelloJob.deserialize(h1.serialize)
+ # We should be able to parse a timestamp
+ type = Time.parse(h2.enqueued_at).class
+ assert_equal Time, type
+ end
end
diff --git a/activejob/test/cases/logging_test.rb b/activejob/test/cases/logging_test.rb
index 6154ba301d..acd37456c9 100644
--- a/activejob/test/cases/logging_test.rb
+++ b/activejob/test/cases/logging_test.rb
@@ -115,6 +115,8 @@ class LoggingTest < ActiveSupport::TestCase
perform_enqueued_jobs do
LoggingJob.perform_later "Dummy"
assert_match(/Performing LoggingJob \(Job ID: .*?\) from .*? with arguments:.*Dummy/, @logger.messages)
+
+ assert_match(/enqueued at /, @logger.messages)
assert_match(/Dummy, here is it: Dummy/, @logger.messages)
assert_match(/Performed LoggingJob \(Job ID: .*?\) from .*? in .*ms/, @logger.messages)
end
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index f16a746b91..7b4f3c617c 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -4,12 +4,6 @@
*Ryuta Kamizono*
-* Chaining named scope is no longer leaking to class level querying methods.
-
- Fixes #14003.
-
- *Ryuta Kamizono*
-
* Allow applications to automatically switch connections.
Adds a middleware and configuration options that can be used in your
diff --git a/activerecord/Rakefile b/activerecord/Rakefile
index 9824787658..90921dec8b 100644
--- a/activerecord/Rakefile
+++ b/activerecord/Rakefile
@@ -65,11 +65,42 @@ end
task adapter => "#{adapter}:env" do
adapter_short = adapter == "db2" ? adapter : adapter[/^[a-z0-9]+/]
puts [adapter, adapter_short].inspect
- (Dir["test/cases/**/*_test.rb"].reject {
+
+ failing_files = []
+
+ test_files = (Dir["test/cases/**/*_test.rb"].reject {
|x| x.include?("/adapters/")
- } + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file|
- sh(Gem.ruby, "-w", "-Itest", file)
- end || raise("Failures")
+ } + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).sort
+
+ if ENV["BUILDKITE_PARALLEL_JOB_COUNT"]
+ n = ENV["BUILDKITE_PARALLEL_JOB"].to_i
+ m = ENV["BUILDKITE_PARALLEL_JOB_COUNT"].to_i
+
+ test_files = test_files.each_slice(m).map { |slice| slice[n] }.compact
+ end
+
+ test_files.each do |file|
+ puts "--- #{file}"
+ success = sh(Gem.ruby, "-w", "-Itest", file)
+ unless success
+ failing_files << file
+ puts "^^^ +++"
+ end
+ puts
+ end
+
+ puts "--- All tests completed"
+ unless failing_files.empty?
+ puts "^^^ +++"
+ puts
+ puts "Failed in:"
+ failing_files.each do |file|
+ puts " #{file}"
+ end
+ puts
+
+ exit 1
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index b0c0beac0e..c3d4eab562 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -209,8 +209,7 @@ module ActiveRecord
# This method is abstract in the sense that it relies on
# +count_records+, which is a method descendants have to provide.
def size
- if !find_target?
- loaded! unless loaded?
+ if !find_target? || loaded?
target.size
elsif @association_ids
@association_ids.size
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index 6f67934a79..5972846940 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -52,7 +52,11 @@ module ActiveRecord
# If the collection is empty the target is set to an empty array and
# the loaded flag is set to true as well.
def count_records
- count = counter_cache_value || scope.count(:all)
+ count = if reflection.has_cached_counter?
+ owner._read_attribute(reflection.counter_cache_column).to_i
+ else
+ scope.count(:all)
+ end
# If there's nothing in the database and @target has no new records
# we are certain the current target is an empty array. This is a
@@ -62,14 +66,6 @@ module ActiveRecord
[association_scope.limit_value, count].compact.min
end
- def counter_cache_value
- reflection.has_cached_counter? ? owner._read_attribute(reflection.counter_cache_column).to_i : nil
- end
-
- def find_target?
- super && !counter_cache_value&.zero?
- end
-
def update_counter(difference, reflection = reflection())
if reflection.has_cached_counter?
owner.increment!(reflection.counter_cache_column, difference)
diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb
index c36b87829e..9879b01d3a 100644
--- a/activerecord/lib/active_record/database_configurations.rb
+++ b/activerecord/lib/active_record/database_configurations.rb
@@ -158,7 +158,7 @@ module ActiveRecord
configs
else
configs.map do |config|
- ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, config.spec_name, url, config.config)
+ ActiveRecord::DatabaseConfigurations::UrlConfig.new(config.env_name, config.spec_name, url, config.config)
end
end
else
diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb
index 94906a2943..abc939826b 100644
--- a/activerecord/lib/active_record/migration/compatibility.rb
+++ b/activerecord/lib/active_record/migration/compatibility.rb
@@ -77,18 +77,18 @@ module ActiveRecord
class V5_1 < V5_2
def change_column(table_name, column_name, type, options = {})
- if adapter_name == "PostgreSQL"
+ if connection.adapter_name == "PostgreSQL"
super(table_name, column_name, type, options.except(:default, :null, :comment))
- change_column_default(table_name, column_name, options[:default]) if options.key?(:default)
- change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
- change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment)
+ connection.change_column_default(table_name, column_name, options[:default]) if options.key?(:default)
+ connection.change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
+ connection.change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment)
else
super
end
end
def create_table(table_name, options = {})
- if adapter_name == "Mysql2"
+ if connection.adapter_name == "Mysql2"
super(table_name, options: "ENGINE=InnoDB", **options)
else
super
@@ -110,13 +110,13 @@ module ActiveRecord
end
def create_table(table_name, options = {})
- if adapter_name == "PostgreSQL"
+ if connection.adapter_name == "PostgreSQL"
if options[:id] == :uuid && !options.key?(:default)
options[:default] = "uuid_generate_v4()"
end
end
- unless adapter_name == "Mysql2" && options[:id] == :bigint
+ unless connection.adapter_name == "Mysql2" && options[:id] == :bigint
if [:integer, :bigint].include?(options[:id]) && !options.key?(:default)
options[:default] = nil
end
@@ -190,7 +190,7 @@ module ActiveRecord
if options[:name].present?
options[:name].to_s
else
- index_name(table_name, column: column_names)
+ connection.index_name(table_name, column: column_names)
end
super
end
@@ -210,15 +210,17 @@ module ActiveRecord
end
def index_name_for_remove(table_name, options = {})
- index_name = index_name(table_name, options)
+ index_name = connection.index_name(table_name, options)
- unless index_name_exists?(table_name, index_name)
+ unless connection.index_name_exists?(table_name, index_name)
if options.is_a?(Hash) && options.has_key?(:name)
options_without_column = options.dup
options_without_column.delete :column
- index_name_without_column = index_name(table_name, options_without_column)
+ index_name_without_column = connection.index_name(table_name, options_without_column)
- return index_name_without_column if index_name_exists?(table_name, index_name_without_column)
+ if connection.index_name_exists?(table_name, index_name_without_column)
+ return index_name_without_column
+ end
end
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist"
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 8c1b2e2be1..2a27f53f06 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -40,8 +40,7 @@ module ActiveRecord
def find_by_sql(sql, binds = [], preparable: nil, &block)
result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds, preparable: preparable)
column_types = result_set.column_types.dup
- cached_columns_hash = connection.schema_cache.columns_hash(table_name)
- cached_columns_hash.each_key { |k| column_types.delete k }
+ attribute_types.each_key { |k| column_types.delete k }
message_bus = ActiveSupport::Notifications.instrumenter
payload = {
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 347d745d19..f98d9bb2c0 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -321,12 +321,12 @@ module ActiveRecord
# Please check unscoped if you want to remove all previous scopes (including
# the default_scope) during the execution of a block.
def scoping
- @delegate_to_klass && klass.current_scope(true) ? yield : _scoping(self) { yield }
+ @delegate_to_klass ? yield : _scoping(self) { yield }
end
def _exec_scope(*args, &block) # :nodoc:
@delegate_to_klass = true
- _scoping(nil) { instance_exec(*args, &block) || self }
+ instance_exec(*args, &block) || self
ensure
@delegate_to_klass = false
end
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index cef31bea94..bdd3c540bb 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -186,11 +186,9 @@ module ActiveRecord
relation = apply_join_dependency
relation.pluck(*column_names)
else
- disallow_raw_sql!(column_names)
+ klass.disallow_raw_sql!(column_names)
relation = spawn
- relation.select_values = column_names.map { |cn|
- @klass.has_attribute?(cn) || @klass.attribute_alias?(cn) ? arel_attribute(cn) : cn
- }
+ relation.select_values = column_names
result = skip_query_cache_if_necessary { klass.connection.select_all(relation.arel, nil) }
result.cast_values(klass.attribute_types)
end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index eb80aab701..75976aa8fc 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -1052,11 +1052,13 @@ module ActiveRecord
def arel_columns(columns)
columns.flat_map do |field|
- if (Symbol === field || String === field) && (klass.has_attribute?(field) || klass.attribute_alias?(field)) && !from_clause.value
- arel_attribute(field)
- elsif Symbol === field
- connection.quote_table_name(field.to_s)
- elsif Proc === field
+ case field
+ when Symbol
+ field = field.to_s
+ arel_column(field) { connection.quote_table_name(field) }
+ when String
+ arel_column(field) { field }
+ when Proc
field.call
else
field
@@ -1064,6 +1066,16 @@ module ActiveRecord
end
end
+ def arel_column(field)
+ field = klass.attribute_alias(field) if klass.attribute_alias?(field)
+
+ if klass.columns_hash.key?(field) && !from_clause.value
+ arel_attribute(field)
+ else
+ yield
+ end
+ end
+
def reverse_sql_order(order_query)
if order_query.empty?
return [arel_attribute(primary_key).desc] if primary_key
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index 8cf4fc469f..7874c4c35a 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -8,7 +8,7 @@ module ActiveRecord
module SpawnMethods
# This is overridden by Associations::CollectionProxy
def spawn #:nodoc:
- @delegate_to_klass && klass.current_scope(true) ? klass.all : clone
+ @delegate_to_klass ? klass.all : clone
end
# Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an ActiveRecord::Relation.
diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb
index c01138c059..3525fa2ab8 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -448,8 +448,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
def test_with_select
- assert_equal 1, Company.find(2).firm_with_select.attributes.size
- assert_equal 1, Company.all.merge!(includes: :firm_with_select).find(2).firm_with_select.attributes.size
+ assert_equal 1, Post.find(2).author_with_select.attributes.size
+ assert_equal 1, Post.includes(:author_with_select).find(2).author_with_select.attributes.size
+ end
+
+ def test_custom_attribute_with_select
+ assert_equal 2, Company.find(2).firm_with_select.attributes.size
+ assert_equal 2, Company.includes(:firm_with_select).find(2).firm_with_select.attributes.size
end
def test_belongs_to_without_counter_cache_option
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index bf44be1811..7d669198ca 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -1230,10 +1230,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_not_predicate Ship.reflect_on_association(:treasures), :has_cached_counter?
# Count should come from sql count() of treasures rather than treasures_count attribute
- assert_queries(1) do
- assert_equal ship.treasures.size, 0
- assert_predicate ship.treasures, :loaded?
- end
+ assert_equal ship.treasures.size, 0
assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed" do
ship.treasures.create(name: "Gold")
@@ -1354,20 +1351,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
post = posts(:welcome)
assert_no_queries do
assert_not_empty post.comments
- assert_equal 2, post.comments.size
- assert_not_predicate post.comments, :loaded?
- end
- post = posts(:misc_by_bob)
- assert_no_queries do
- assert_empty post.comments
- assert_predicate post.comments, :loaded?
- end
- end
-
- def test_empty_association_loading_with_counter_cache
- post = posts(:misc_by_bob)
- assert_no_queries do
- assert_empty post.comments.to_a
end
end
@@ -1755,6 +1738,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_nil posts.first
end
+ def test_destroy_all_on_desynced_counter_cache_association
+ category = categories(:general)
+ assert_operator category.categorizations.count, :>, 0
+
+ category.categorizations.destroy_all
+ assert_equal 0, category.categorizations.count
+ end
+
def test_destroy_on_association_clears_scope
author = Author.create!(name: "Gannon")
posts = author.posts
@@ -2004,6 +1995,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_not_predicate company.clients, :loaded?
end
+ def test_counter_cache_on_unloaded_association
+ car = Car.create(name: "My AppliCar")
+ assert_equal 0, car.engines.size
+ end
+
def test_ids_reader_cache_not_used_for_size_when_association_is_dirty
firm = Firm.create!(name: "Startup")
assert_equal 0, firm.client_ids.size
@@ -2019,24 +2015,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal [3, 11], firm.client_ids
end
- def test_zero_counter_cache_usage_on_unloaded_association
- car = Car.create!(name: "My AppliCar")
- assert_no_queries do
- assert_equal car.engines.size, 0
- assert_predicate car.engines, :loaded?
- end
- end
-
- def test_counter_cache_on_new_record_unloaded_association
- car = Car.new(name: "My AppliCar")
- # Ensure no schema queries inside assertion
- Engine.primary_key
- assert_no_queries do
- assert_equal car.engines.size, 0
- assert_predicate car.engines, :loaded?
- end
- end
-
def test_get_ids_ignores_include_option
assert_equal [readers(:michael_welcome).id], posts(:welcome).readers_with_person_ids
end
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 363593ca19..1b8f748bad 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -1226,14 +1226,15 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_attribute_names
- assert_equal ["id", "type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id", "description"],
- Company.attribute_names
+ expected = ["id", "type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id", "description", "metadata"]
+ assert_equal expected, Company.attribute_names
end
def test_has_attribute
assert Company.has_attribute?("id")
assert Company.has_attribute?("type")
assert Company.has_attribute?("name")
+ assert Company.has_attribute?("metadata")
assert_not Company.has_attribute?("lastname")
assert_not Company.has_attribute?("age")
end
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index 850bc49676..b001667ac9 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -690,8 +690,9 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_pluck_not_auto_table_name_prefix_if_column_joined
- Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)])
- assert_equal [7], Company.joins(:contracts).pluck(:developer_id)
+ company = Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)])
+ metadata = company.contracts.first.metadata
+ assert_equal [metadata], Company.joins(:contracts).pluck(:metadata)
end
def test_pluck_with_selection_clause
diff --git a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
index 225cccc62c..515bf5df06 100644
--- a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
+++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
@@ -72,6 +72,16 @@ module ActiveRecord
assert_equal expected, actual
end
+ def test_resolver_with_database_uri_and_multiple_envs
+ ENV["DATABASE_URL"] = "postgres://localhost"
+ ENV["RAILS_ENV"] = "test"
+
+ config = { "production" => { "adapter" => "postgresql", "database" => "foo_prod" }, "test" => { "adapter" => "postgresql", "database" => "foo_test" } }
+ actual = resolve_spec(:test, config)
+ expected = { "adapter" => "postgresql", "database" => "foo_test", "host" => "localhost", "name" => "test" }
+ assert_equal expected, actual
+ end
+
def test_resolver_with_database_uri_and_unknown_symbol_key
ENV["DATABASE_URL"] = "postgres://localhost/foo"
config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } }
diff --git a/activerecord/test/cases/database_selector_test.rb b/activerecord/test/cases/database_selector_test.rb
index 4106a6ec46..fd02d2acb4 100644
--- a/activerecord/test/cases/database_selector_test.rb
+++ b/activerecord/test/cases/database_selector_test.rb
@@ -11,6 +11,10 @@ module ActiveRecord
@session = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.new(@session_store)
end
+ teardown do
+ ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
+ end
+
def test_empty_session
assert_equal Time.at(0), @session.last_write_timestamp
end
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index 0ab0459c38..857d743605 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -14,6 +14,7 @@ require "models/person"
require "models/computer"
require "models/reply"
require "models/company"
+require "models/contract"
require "models/bird"
require "models/car"
require "models/engine"
@@ -1815,6 +1816,12 @@ class RelationTest < ActiveRecord::TestCase
assert_equal [1, 1, 1], posts.map(&:author_address_id)
end
+ test "joins with select custom attribute" do
+ contract = Company.create!(name: "test").contracts.create!
+ company = Company.joins(:contracts).select(:id, :metadata).find(contract.company_id)
+ assert_equal contract.metadata, company.metadata
+ end
+
test "delegations do not leak to other classes" do
Topic.all.by_lifo
assert Topic.all.class.method_defined?(:by_lifo)
diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb
index 27f9df295f..1a3a95f168 100644
--- a/activerecord/test/cases/scoping/named_scoping_test.rb
+++ b/activerecord/test/cases/scoping/named_scoping_test.rb
@@ -447,9 +447,8 @@ class NamedScopingTest < ActiveRecord::TestCase
assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).to_a.uniq
end
- def test_chaining_doesnt_leak_conditions_to_another_scopes
- expected = Topic.where(approved: false).where(id: Topic.children.select(:parent_id))
- assert_equal expected.to_a, Topic.rejected.has_children.to_a
+ def test_class_method_in_scope
+ assert_equal [topics(:second)], topics(:first).approved_replies.ordered
end
def test_nested_scoping
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index 838f515aad..a0f48d23f1 100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -13,6 +13,8 @@ class Company < AbstractCompany
has_many :contracts
has_many :developers, through: :contracts
+ attribute :metadata, :json
+
scope :of_first_firm, lambda {
joins(account: :firm).
where("firms.id" => 1)
diff --git a/activerecord/test/models/contract.rb b/activerecord/test/models/contract.rb
index 3f663375c4..89719775c4 100644
--- a/activerecord/test/models/contract.rb
+++ b/activerecord/test/models/contract.rb
@@ -5,7 +5,9 @@ class Contract < ActiveRecord::Base
belongs_to :developer, primary_key: :id
belongs_to :firm, foreign_key: "company_id"
- before_save :hi
+ attribute :metadata, :json
+
+ before_save :hi, :update_metadata
after_save :bye
attr_accessor :hi_count, :bye_count
@@ -19,6 +21,10 @@ class Contract < ActiveRecord::Base
@bye_count ||= 0
@bye_count += 1
end
+
+ def update_metadata
+ self.metadata = { company_id: company_id, developer_id: developer_id }
+ end
end
class NewContract < Contract
diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb
index e32cc59399..65d1fce66d 100644
--- a/activerecord/test/models/post.rb
+++ b/activerecord/test/models/post.rb
@@ -31,6 +31,7 @@ class Post < ActiveRecord::Base
belongs_to :author_with_posts, -> { includes(:posts) }, class_name: "Author", foreign_key: :author_id
belongs_to :author_with_address, -> { includes(:author_address) }, class_name: "Author", foreign_key: :author_id
+ belongs_to :author_with_select, -> { select(:id) }, class_name: "Author", foreign_key: :author_id
def first_comment
super.body
diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb
index 0807bcf875..b35623a344 100644
--- a/activerecord/test/models/reply.rb
+++ b/activerecord/test/models/reply.rb
@@ -7,6 +7,8 @@ class Reply < Topic
belongs_to :topic_with_primary_key, class_name: "Topic", primary_key: "title", foreign_key: "parent_title", counter_cache: "replies_count", touch: true
has_many :replies, class_name: "SillyReply", dependent: :destroy, foreign_key: "parent_id"
has_many :silly_unique_replies, dependent: :destroy, foreign_key: "parent_id"
+
+ scope :ordered, -> { Reply.order(:id) }
end
class SillyReply < Topic
diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb
index fdb461ed7f..75890c327a 100644
--- a/activerecord/test/models/topic.rb
+++ b/activerecord/test/models/topic.rb
@@ -10,9 +10,6 @@ class Topic < ActiveRecord::Base
scope :approved, -> { where(approved: true) }
scope :rejected, -> { where(approved: false) }
- scope :children, -> { where.not(parent_id: nil) }
- scope :has_children, -> { where(id: Topic.children.select(:parent_id)) }
-
scope :scope_with_lambda, lambda { all }
scope :by_lifo, -> { where(author_name: "lifo") }
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 86d5a67a13..349b8afc48 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -247,6 +247,7 @@ ActiveRecord::Schema.define do
create_table :contracts, force: true do |t|
t.references :developer, index: false
t.references :company, index: false
+ t.string :metadata
end
create_table :customers, force: true do |t|
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index 7002474b8d..c46b56833d 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,3 +1,11 @@
+* New autoloading based on [Zeitwerk](https://github.com/fxn/zeitwerk).
+
+ *Xavier Noria*
+
+* Revise `ActiveSupport::Notifications.unsubscribe` to correctly handle Regex or other multiple-pattern subscribers.
+
+ *Zach Kemp*
+
* Add `before_reset` callback to `CurrentAttributes` and define `after_reset` as an alias of `resets` for symmetry.
*Rosa Gutierrez*
diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec
index bdd7bc70a0..92cdfd89fe 100644
--- a/activesupport/activesupport.gemspec
+++ b/activesupport/activesupport.gemspec
@@ -30,8 +30,9 @@ Gem::Specification.new do |s|
# NOTE: Please read our dependency guidelines before updating versions:
# https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves
- s.add_dependency "i18n", ">= 0.7", "< 2"
- s.add_dependency "tzinfo", "~> 1.1"
- s.add_dependency "minitest", "~> 5.1"
+ s.add_dependency "i18n", ">= 0.7", "< 2"
+ s.add_dependency "tzinfo", "~> 1.1"
+ s.add_dependency "minitest", "~> 5.1"
s.add_dependency "concurrent-ruby", "~> 1.0", ">= 1.0.2"
+ s.add_dependency "zeitwerk", "~> 1.0"
end
diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb
index de1fb1886c..f43894a1ea 100644
--- a/activesupport/lib/active_support/cache/file_store.rb
+++ b/activesupport/lib/active_support/cache/file_store.rb
@@ -18,7 +18,6 @@ module ActiveSupport
DIR_FORMATTER = "%03X"
FILENAME_MAX_SIZE = 228 # max filename size on file system is 255, minus room for timestamp and random characters appended by Tempfile (used by atomic write)
FILEPATH_MAX_SIZE = 900 # max is 1024, plus some room
- EXCLUDED_DIRS = [".", ".."].freeze
GITKEEP_FILES = [".gitkeep", ".keep"].freeze
def initialize(cache_path, options = nil)
@@ -35,7 +34,7 @@ module ActiveSupport
# file store directory except for .keep or .gitkeep. Be careful which directory is specified in your
# config file when using +FileStore+ because everything in that directory will be deleted.
def clear(options = nil)
- root_dirs = exclude_from(cache_path, EXCLUDED_DIRS + GITKEEP_FILES)
+ root_dirs = (Dir.children(cache_path) - GITKEEP_FILES)
FileUtils.rm_r(root_dirs.collect { |f| File.join(cache_path, f) })
rescue Errno::ENOENT
end
@@ -154,7 +153,7 @@ module ActiveSupport
# Delete empty directories in the cache.
def delete_empty_directories(dir)
return if File.realpath(dir) == File.realpath(cache_path)
- if exclude_from(dir, EXCLUDED_DIRS).empty?
+ if Dir.children(dir).empty?
Dir.delete(dir) rescue nil
delete_empty_directories(File.dirname(dir))
end
@@ -167,8 +166,7 @@ module ActiveSupport
def search_dir(dir, &callback)
return if !File.exist?(dir)
- Dir.foreach(dir) do |d|
- next if EXCLUDED_DIRS.include?(d)
+ Dir.each_child(dir) do |d|
name = File.join(dir, d)
if File.directory?(name)
search_dir(name, &callback)
@@ -193,11 +191,6 @@ module ActiveSupport
end
end
end
-
- # Exclude entries from source directory
- def exclude_from(source, excludes)
- Dir.entries(source).reject { |f| excludes.include?(f) }
- end
end
end
end
diff --git a/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb
new file mode 100644
index 0000000000..75624bae09
--- /dev/null
+++ b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+module ActiveSupport
+ module Dependencies
+ module ZeitwerkIntegration # :nodoc: all
+ module Decorations
+ def clear
+ Dependencies.unload_interlock do
+ Rails.autoloader.reload
+ end
+ end
+
+ def constantize(cpath)
+ Inflector.constantize(cpath)
+ end
+
+ def safe_constantize(cpath)
+ Inflector.safe_constantize(cpath)
+ end
+
+ def autoloaded_constants
+ Rails.autoloaders.flat_map do |autoloader|
+ autoloader.loaded.to_a
+ end
+ end
+
+ def autoloaded?(object)
+ cpath = object.is_a?(Module) ? object.name : object.to_s
+ Rails.autoloaders.any? { |autoloader| autoloader.loaded?(cpath) }
+ end
+ end
+
+ class << self
+ def take_over
+ setup_autoloaders
+ freeze_autoload_paths
+ decorate_dependencies
+ end
+
+ private
+
+ def setup_autoloaders
+ Dependencies.autoload_paths.each do |autoload_path|
+ # Zeitwerk only accepts existing directories in `push_dir` to
+ # prevent misconfigurations.
+ next unless File.directory?(autoload_path)
+
+ if autoload_once?(autoload_path)
+ Rails.once_autoloader.push_dir(autoload_path)
+ else
+ Rails.autoloader.push_dir(autoload_path)
+ end
+ end
+
+ Rails.autoloaders.each(&:setup)
+ end
+
+ def autoload_once?(autoload_path)
+ Dependencies.autoload_once_paths.include?(autoload_path) ||
+ Gem.path.any? { |gem_path| autoload_path.to_s.start_with?(gem_path) }
+ end
+
+ def freeze_autoload_paths
+ Dependencies.autoload_paths.freeze
+ Dependencies.autoload_once_paths.freeze
+ end
+
+ def decorate_dependencies
+ Dependencies.singleton_class.prepend(Decorations)
+ Object.class_eval { alias_method :require_dependency, :require }
+ end
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb
index 7ccc333463..d9e93b530c 100644
--- a/activesupport/lib/active_support/notifications.rb
+++ b/activesupport/lib/active_support/notifications.rb
@@ -153,6 +153,15 @@ module ActiveSupport
#
# ActiveSupport::Notifications.unsubscribe("render")
#
+ # Subscribers using a regexp or other pattern-matching object will remain subscribed
+ # to all events that match their original pattern, unless those events match a string
+ # passed to `unsubscribe`:
+ #
+ # subscriber = ActiveSupport::Notifications.subscribe(/render/) { }
+ # ActiveSupport::Notifications.unsubscribe('render_template.action_view')
+ # subscriber.matches?('render_template.action_view') # => false
+ # subscriber.matches?('render_partial.action_view') # => true
+ #
# == Default Queue
#
# Notifications ships with a queue implementation that consumes and publishes events
diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb
index 11721db103..f06504cf2c 100644
--- a/activesupport/lib/active_support/notifications/fanout.rb
+++ b/activesupport/lib/active_support/notifications/fanout.rb
@@ -2,6 +2,7 @@
require "mutex_m"
require "concurrent/map"
+require "set"
module ActiveSupport
module Notifications
@@ -39,6 +40,7 @@ module ActiveSupport
when String
@string_subscribers[subscriber_or_name].clear
@listeners_for.delete(subscriber_or_name)
+ @other_subscribers.each { |sub| sub.unsubscribe!(subscriber_or_name) }
else
pattern = subscriber_or_name.try(:pattern)
if String === pattern
@@ -113,11 +115,33 @@ module ActiveSupport
end
end
+ class Matcher #:nodoc:
+ attr_reader :pattern, :exclusions
+
+ def self.wrap(pattern)
+ return pattern if String === pattern
+ new(pattern)
+ end
+
+ def initialize(pattern)
+ @pattern = pattern
+ @exclusions = Set.new
+ end
+
+ def unsubscribe!(name)
+ exclusions << -name if pattern === name
+ end
+
+ def ===(name)
+ pattern === name && !exclusions.include?(name)
+ end
+ end
+
class Evented #:nodoc:
attr_reader :pattern
def initialize(pattern, delegate)
- @pattern = pattern
+ @pattern = Matcher.wrap(pattern)
@delegate = delegate
@can_publish = delegate.respond_to?(:publish)
end
@@ -137,11 +161,15 @@ module ActiveSupport
end
def subscribed_to?(name)
- @pattern === name
+ pattern === name
end
def matches?(name)
- @pattern && @pattern === name
+ pattern && pattern === name
+ end
+
+ def unsubscribe!(name)
+ pattern.unsubscribe!(name)
end
end
@@ -204,6 +232,10 @@ module ActiveSupport
true
end
+ def unsubscribe!(*)
+ false
+ end
+
alias :matches? :===
end
end
diff --git a/activesupport/test/cache/stores/file_store_test.rb b/activesupport/test/cache/stores/file_store_test.rb
index f6855bb308..0364d9ab64 100644
--- a/activesupport/test/cache/stores/file_store_test.rb
+++ b/activesupport/test/cache/stores/file_store_test.rb
@@ -101,7 +101,7 @@ class FileStoreTest < ActiveSupport::TestCase
end
assert File.exist?(cache_dir), "Parent of top level cache dir was deleted!"
assert File.exist?(sub_cache_dir), "Top level cache dir was deleted!"
- assert_empty Dir.entries(sub_cache_dir).reject { |f| ActiveSupport::Cache::FileStore::EXCLUDED_DIRS.include?(f) }
+ assert_empty Dir.children(sub_cache_dir)
end
def test_log_exception_when_cache_read_fails
diff --git a/activesupport/test/notifications/evented_notification_test.rb b/activesupport/test/notifications/evented_notification_test.rb
index 4beb8194b9..ab2a9b8659 100644
--- a/activesupport/test/notifications/evented_notification_test.rb
+++ b/activesupport/test/notifications/evented_notification_test.rb
@@ -84,6 +84,39 @@ module ActiveSupport
[:finish, "hi", 1, {}]
], listener.events
end
+
+ def test_listen_to_regexp
+ notifier = Fanout.new
+ listener = Listener.new
+ notifier.subscribe(/[a-z]*.world/, listener)
+ notifier.start("hi.world", 1, {})
+ notifier.finish("hi.world", 2, {})
+ notifier.start("hello.world", 1, {})
+ notifier.finish("hello.world", 2, {})
+
+ assert_equal [
+ [:start, "hi.world", 1, {}],
+ [:finish, "hi.world", 2, {}],
+ [:start, "hello.world", 1, {}],
+ [:finish, "hello.world", 2, {}]
+ ], listener.events
+ end
+
+ def test_listen_to_regexp_with_exclusions
+ notifier = Fanout.new
+ listener = Listener.new
+ notifier.subscribe(/[a-z]*.world/, listener)
+ notifier.unsubscribe("hi.world")
+ notifier.start("hi.world", 1, {})
+ notifier.finish("hi.world", 2, {})
+ notifier.start("hello.world", 1, {})
+ notifier.finish("hello.world", 2, {})
+
+ assert_equal [
+ [:start, "hello.world", 1, {}],
+ [:finish, "hello.world", 2, {}]
+ ], listener.events
+ end
end
end
end
diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb
index b5d72d1a42..bb20d26a25 100644
--- a/activesupport/test/notifications_test.rb
+++ b/activesupport/test/notifications_test.rb
@@ -128,6 +128,25 @@ module Notifications
assert_equal [["named.subscription", :foo], ["named.subscription", :foo]], @events
end
+ def test_unsubscribing_by_name_leaves_regexp_matched_subscriptions
+ @matched_events = []
+ @notifier.subscribe(/subscription/) { |*args| @matched_events << event(*args) }
+ @notifier.publish("named.subscription", :before)
+ @notifier.wait
+ [@events, @named_events, @matched_events].each do |collector|
+ assert_includes(collector, ["named.subscription", :before])
+ end
+ @notifier.unsubscribe("named.subscription")
+ @notifier.publish("named.subscription", :after)
+ @notifier.publish("other.subscription", :after)
+ @notifier.wait
+ assert_includes(@events, ["named.subscription", :after])
+ assert_includes(@events, ["other.subscription", :after])
+ assert_includes(@matched_events, ["other.subscription", :after])
+ assert_not_includes(@matched_events, ["named.subscription", :after])
+ assert_not_includes(@named_events, ["named.subscription", :after])
+ end
+
private
def event(*args)
args
diff --git a/guides/source/action_cable_overview.md b/guides/source/action_cable_overview.md
index 8f5c44849a..7a9ff96c44 100644
--- a/guides/source/action_cable_overview.md
+++ b/guides/source/action_cable_overview.md
@@ -671,6 +671,21 @@ To configure the URL, add a call to `action_cable_meta_tag` in your HTML layout
HEAD. This uses a URL or path typically set via `config.action_cable.url` in the
environment configuration files.
+### Worker Pool Configuration
+
+The worker pool is used to run connection callbacks and channel actions in
+isolation from the server's main thread. Action Cable allows the application
+to configure the number of simultaneously processed threads in the worker pool.
+
+```ruby
+config.action_cable.worker_pool_size = 4
+```
+
+Also, note that your server must provide at least the same number of database
+connections as you have workers. The default worker pool size is set to 4, so
+that means you have to make at least 4 database connections available.
+ You can change that in `config/database.yml` through the `pool` attribute.
+
### Other Configurations
The other common option to configure is the log tags applied to the
@@ -688,11 +703,6 @@ config.action_cable.log_tags = [
For a full list of all configuration options, see the
`ActionCable::Server::Configuration` class.
-Also, note that your server must provide at least the same number of database
-connections as you have workers. The default worker pool size is set to 4, so
-that means you have to make at least that available. You can change that in
-`config/database.yml` through the `pool` attribute.
-
## Running Standalone Cable Servers
### In App
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index e7129dd274..ad82614a0d 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -791,6 +791,9 @@ main application.
You can set this as nil to not mount Action Cable as part of your
normal Rails server.
+You can find more detailed configuration options in the
+[Action Cable Overview](action_cable_overview.html#configuration).
+
### Configuring Active Storage
diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md
index 19f4de8a1d..d66add2ca0 100644
--- a/railties/CHANGELOG.md
+++ b/railties/CHANGELOG.md
@@ -1,3 +1,8 @@
+* Fix non-symbol access to nested hashes returned from `Rails::Application.config_for`
+ being broken by allowing non-symbol access with a deprecation notice.
+
+ *Ufuk Kayserilioglu*
+
* Fix deeply nested namespace command printing.
*Gannon McGibbon*
diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb
index 092105d502..bca2cf34e1 100644
--- a/railties/lib/rails.rb
+++ b/railties/lib/rails.rb
@@ -110,5 +110,25 @@ module Rails
def public_path
application && Pathname.new(application.paths["public"].first)
end
+
+ def autoloader
+ if configuration.autoloader == :zeitwerk
+ @autoloader ||= Zeitwerk::Loader.new
+ end
+ end
+
+ def once_autoloader
+ if configuration.autoloader == :zeitwerk
+ @once_autoloader ||= Zeitwerk::Loader.new
+ end
+ end
+
+ def autoloaders
+ if configuration.autoloader == :zeitwerk
+ [autoloader, once_autoloader]
+ else
+ []
+ end
+ end
end
end
diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb
index 5a924ab8e6..d0417f8a49 100644
--- a/railties/lib/rails/application.rb
+++ b/railties/lib/rails/application.rb
@@ -7,6 +7,7 @@ require "active_support/key_generator"
require "active_support/message_verifier"
require "active_support/encrypted_configuration"
require "active_support/deprecation"
+require "active_support/hash_with_indifferent_access"
require "rails/engine"
require "rails/secrets"
@@ -230,8 +231,8 @@ module Rails
config = YAML.load(ERB.new(yaml.read).result) || {}
config = (config["shared"] || {}).merge(config[env] || {})
- ActiveSupport::OrderedOptions.new.tap do |config_as_ordered_options|
- config_as_ordered_options.update(config.deep_symbolize_keys)
+ ActiveSupport::OrderedOptions.new.tap do |options|
+ options.update(NonSymbolAccessDeprecatedHash.new(config))
end
else
raise "Could not load configuration. No such file - #{yaml}"
@@ -590,5 +591,38 @@ module Rails
def build_middleware
config.app_middleware + super
end
+
+ class NonSymbolAccessDeprecatedHash < HashWithIndifferentAccess # :nodoc:
+ def initialize(value = nil)
+ if value.is_a?(Hash)
+ value.each_pair { |k, v| self[k] = v }
+ else
+ super
+ end
+ end
+
+ def []=(key, value)
+ if value.is_a?(Hash)
+ value = self.class.new(value)
+ end
+ super(key.to_sym, value)
+ end
+
+ private
+
+ def convert_key(key)
+ unless key.kind_of?(Symbol)
+ ActiveSupport::Deprecation.warn(<<~MESSAGE.squish)
+ Accessing hashes returned from config_for by non-symbol keys
+ is deprecated and will be removed in Rails 6.1.
+ Use symbols for access instead.
+ MESSAGE
+
+ key = key.to_sym
+ end
+
+ key
+ end
+ end
end
end
diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb
index b7838f7e32..af3ec36064 100644
--- a/railties/lib/rails/application/configuration.rb
+++ b/railties/lib/rails/application/configuration.rb
@@ -20,7 +20,7 @@ module Rails
:read_encrypted_secrets, :log_level, :content_security_policy_report_only,
:content_security_policy_nonce_generator, :require_master_key, :credentials
- attr_reader :encoding, :api_only, :loaded_config_version
+ attr_reader :encoding, :api_only, :loaded_config_version, :autoloader
def initialize(*)
super
@@ -64,6 +64,7 @@ module Rails
@credentials = ActiveSupport::OrderedOptions.new
@credentials.content_path = default_credentials_content_path
@credentials.key_path = default_credentials_key_path
+ @autoloader = :classic
end
def load_defaults(target_version)
@@ -117,6 +118,8 @@ module Rails
when "6.0"
load_defaults "5.2"
+ self.autoloader = :zeitwerk if RUBY_ENGINE == "ruby"
+
if respond_to?(:action_view)
action_view.default_enforce_utf8 = false
end
@@ -267,6 +270,18 @@ module Rails
end
end
+ def autoloader=(autoloader)
+ case autoloader
+ when :classic
+ @autoloader = autoloader
+ when :zeitwerk
+ require "zeitwerk"
+ @autoloader = autoloader
+ else
+ raise ArgumentError, "config.autoloader may be :classic or :zeitwerk, got #{autoloader.inspect} instead"
+ end
+ end
+
class Custom #:nodoc:
def initialize
@configurations = Hash.new
diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb
index 04aaf6dd9a..39e8ef6631 100644
--- a/railties/lib/rails/application/finisher.rb
+++ b/railties/lib/rails/application/finisher.rb
@@ -21,6 +21,13 @@ module Rails
end
end
+ initializer :let_zeitwerk_take_over do
+ if config.autoloader == :zeitwerk
+ require "active_support/dependencies/zeitwerk_integration"
+ ActiveSupport::Dependencies::ZeitwerkIntegration.take_over
+ end
+ end
+
initializer :add_builtin_route do |app|
if Rails.env.development?
app.routes.prepend do
@@ -66,6 +73,7 @@ module Rails
initializer :eager_load! do
if config.eager_load
ActiveSupport.run_load_hooks(:before_eager_load, self)
+ Zeitwerk::Loader.eager_load_all if defined?(Zeitwerk)
config.eager_load_namespaces.each(&:eager_load!)
end
end
diff --git a/railties/lib/rails/command/behavior.rb b/railties/lib/rails/command/behavior.rb
index 7f32b04cf1..7fb2a99e67 100644
--- a/railties/lib/rails/command/behavior.rb
+++ b/railties/lib/rails/command/behavior.rb
@@ -71,8 +71,9 @@ module Rails
paths = []
namespaces.each do |namespace|
pieces = namespace.split(":")
- paths << pieces.dup.push(pieces.last).join("/")
- paths << pieces.join("/")
+ path = pieces.join("/")
+ paths << "#{path}/#{pieces.last}"
+ paths << path
end
paths.uniq!
paths
diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb
index d6c329b581..2485158a7b 100644
--- a/railties/lib/rails/engine.rb
+++ b/railties/lib/rails/engine.rb
@@ -472,12 +472,10 @@ module Rails
# Eager load the application by loading all ruby
# files inside eager_load paths.
def eager_load!
- config.eager_load_paths.each do |load_path|
- # Starts after load_path plus a slash, ends before ".rb".
- relname_range = (load_path.to_s.length + 1)...-3
- Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
- require_dependency file[relname_range]
- end
+ if Rails.autoloader
+ eager_load_with_zeitwerk!
+ else
+ eager_load_with_dependencies!
end
end
@@ -653,6 +651,22 @@ module Rails
private
+ def eager_load_with_zeitwerk!
+ (config.eager_load_paths - Zeitwerk::Loader.all_dirs).each do |path|
+ Dir.glob("#{path}/**/*.rb").sort.each { |file| require file }
+ end
+ end
+
+ def eager_load_with_dependencies!
+ config.eager_load_paths.each do |load_path|
+ # Starts after load_path plus a slash, ends before ".rb".
+ relname_range = (load_path.to_s.length + 1)...-3
+ Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
+ require_dependency file[relname_range]
+ end
+ end
+ end
+
def load_config_initializer(initializer) # :doc:
ActiveSupport::Notifications.instrument("load_config_initializer.railties", initializer: initializer) do
load(initializer)
diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt
index d39b5d311f..783254b54d 100644
--- a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt
+++ b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt
@@ -28,7 +28,7 @@ ruby <%= "'#{RUBY_VERSION}'" -%>
<% if depend_on_bootsnap? -%>
# Reduces boot times through caching; required in config/boot.rb
-gem 'bootsnap', '>= 1.1.0', require: false
+gem 'bootsnap', '>= 1.4.0', require: false
<%- end -%>
<%- if options.api? -%>
diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb
index 37fba72ee3..960f708bdf 100644
--- a/railties/test/application/configuration_test.rb
+++ b/railties/test/application/configuration_test.rb
@@ -1157,6 +1157,27 @@ module ApplicationTests
end
end
+ test "autoloader & autoloader=" do
+ app "development"
+
+ config = Rails.application.config
+ assert_instance_of Zeitwerk::Loader, Rails.autoloader
+ assert_instance_of Zeitwerk::Loader, Rails.once_autoloader
+ assert_equal [Rails.autoloader, Rails.once_autoloader], Rails.autoloaders
+
+ config.autoloader = :classic
+ assert_nil Rails.autoloader
+ assert_nil Rails.once_autoloader
+ assert_empty Rails.autoloaders
+
+ config.autoloader = :zeitwerk
+ assert_instance_of Zeitwerk::Loader, Rails.autoloader
+ assert_instance_of Zeitwerk::Loader, Rails.once_autoloader
+ assert_equal [Rails.autoloader, Rails.once_autoloader], Rails.autoloaders
+
+ assert_raises(ArgumentError) { config.autoloader = :unknown }
+ end
+
test "config.action_view.cache_template_loading with cache_classes default" do
add_to_config "config.cache_classes = true"
@@ -1714,7 +1735,7 @@ module ApplicationTests
assert_equal true, Rails.application.config.action_mailer.show_previews
end
- test "config_for loads custom configuration from yaml accessible as symbol" do
+ test "config_for loads custom configuration from yaml accessible as symbol or string" do
app_file "config/custom.yml", <<-RUBY
development:
foo: 'bar'
@@ -1727,6 +1748,7 @@ module ApplicationTests
app "development"
assert_equal "bar", Rails.application.config.my_custom_config[:foo]
+ assert_equal "bar", Rails.application.config.my_custom_config["foo"]
end
test "config_for loads nested custom configuration from yaml as symbol keys" do
@@ -1746,6 +1768,25 @@ module ApplicationTests
assert_equal 1, Rails.application.config.my_custom_config[:foo][:bar][:baz]
end
+ test "config_for loads nested custom configuration from yaml with deprecated non-symbol access" do
+ app_file "config/custom.yml", <<-RUBY
+ development:
+ foo:
+ bar:
+ baz: 1
+ RUBY
+
+ add_to_config <<-RUBY
+ config.my_custom_config = config_for('custom')
+ RUBY
+
+ app "development"
+
+ assert_deprecated do
+ assert_equal 1, Rails.application.config.my_custom_config["foo"]["bar"]["baz"]
+ end
+ end
+
test "config_for makes all hash methods available" do
app_file "config/custom.yml", <<-RUBY
development:
@@ -1762,12 +1803,93 @@ module ApplicationTests
actual = Rails.application.config.my_custom_config
- assert_equal actual, foo: 0, bar: { baz: 1 }
- assert_equal actual.keys, [ :foo, :bar ]
- assert_equal actual.values, [ 0, baz: 1]
- assert_equal actual.to_h, foo: 0, bar: { baz: 1 }
- assert_equal actual[:foo], 0
- assert_equal actual[:bar], baz: 1
+ assert_equal({ foo: 0, bar: { baz: 1 } }, actual)
+ assert_equal([ :foo, :bar ], actual.keys)
+ assert_equal([ 0, baz: 1], actual.values)
+ assert_equal({ foo: 0, bar: { baz: 1 } }, actual.to_h)
+ assert_equal(0, actual[:foo])
+ assert_equal({ baz: 1 }, actual[:bar])
+ end
+
+ test "config_for generates deprecation notice when nested hash methods are called with non-symbols" do
+ app_file "config/custom.yml", <<-RUBY
+ development:
+ foo:
+ bar: 1
+ baz: 2
+ qux:
+ boo: 3
+ RUBY
+
+ app "development"
+
+ actual = Rails.application.config_for("custom")[:foo]
+
+ # slice
+ assert_deprecated do
+ assert_equal({ bar: 1, baz: 2 }, actual.slice("bar", "baz"))
+ end
+
+ # except
+ assert_deprecated do
+ assert_equal({ qux: { boo: 3 } }, actual.except("bar", "baz"))
+ end
+
+ # dig
+ assert_deprecated do
+ assert_equal(3, actual.dig("qux", "boo"))
+ end
+
+ # fetch - hit
+ assert_deprecated do
+ assert_equal(1, actual.fetch("bar", 0))
+ end
+
+ # fetch - miss
+ assert_deprecated do
+ assert_equal(0, actual.fetch("does-not-exist", 0))
+ end
+
+ # fetch_values
+ assert_deprecated do
+ assert_equal([1, 2], actual.fetch_values("bar", "baz"))
+ end
+
+ # key? - hit
+ assert_deprecated do
+ assert(actual.key?("bar"))
+ end
+
+ # key? - miss
+ assert_deprecated do
+ assert_not(actual.key?("does-not-exist"))
+ end
+
+ # slice!
+ actual = Rails.application.config_for("custom")[:foo]
+
+ assert_deprecated do
+ slice = actual.slice!("bar", "baz")
+ assert_equal({ bar: 1, baz: 2 }, actual)
+ assert_equal({ qux: { boo: 3 } }, slice)
+ end
+
+ # extract!
+ actual = Rails.application.config_for("custom")[:foo]
+
+ assert_deprecated do
+ extracted = actual.extract!("bar", "baz")
+ assert_equal({ bar: 1, baz: 2 }, extracted)
+ assert_equal({ qux: { boo: 3 } }, actual)
+ end
+
+ # except!
+ actual = Rails.application.config_for("custom")[:foo]
+
+ assert_deprecated do
+ actual.except!("bar", "baz")
+ assert_equal({ qux: { boo: 3 } }, actual)
+ end
end
test "config_for uses the Pathname object if it is provided" do
diff --git a/railties/test/application/mailer_previews_test.rb b/railties/test/application/mailer_previews_test.rb
index ba186bda44..fb84276b8a 100644
--- a/railties/test/application/mailer_previews_test.rb
+++ b/railties/test/application/mailer_previews_test.rb
@@ -85,6 +85,7 @@ module ApplicationTests
end
test "mailer previews are loaded from a custom preview_path" do
+ app_dir "lib/mailer_previews"
add_to_config "config.action_mailer.preview_path = '#{app_path}/lib/mailer_previews'"
mailer "notifier", <<-RUBY
@@ -254,6 +255,7 @@ module ApplicationTests
end
test "mailer previews are reloaded from a custom preview_path" do
+ app_dir "lib/mailer_previews"
add_to_config "config.action_mailer.preview_path = '#{app_path}/lib/mailer_previews'"
app("development")
@@ -818,6 +820,7 @@ module ApplicationTests
def build_app
super
app_file "config/routes.rb", "Rails.application.routes.draw do; end"
+ app_dir "test/mailers/previews"
end
def mailer(name, contents)
diff --git a/railties/test/application/multiple_applications_test.rb b/railties/test/application/multiple_applications_test.rb
index 432344bccc..f0f1112f6b 100644
--- a/railties/test/application/multiple_applications_test.rb
+++ b/railties/test/application/multiple_applications_test.rb
@@ -100,30 +100,6 @@ module ApplicationTests
assert_nothing_raised { AppTemplate::Application.new }
end
- def test_initializers_run_on_different_applications_go_to_the_same_class
- application1 = AppTemplate::Application.new
- run_count = 0
-
- AppTemplate::Application.initializer :init0 do
- run_count += 1
- end
-
- application1.initializer :init1 do
- run_count += 1
- end
-
- AppTemplate::Application.new.initializer :init2 do
- run_count += 1
- end
-
- assert_equal 0, run_count, "Without loading the initializers, the count should be 0"
-
- # Set config.eager_load to false so that an eager_load warning doesn't pop up
- AppTemplate::Application.create { config.eager_load = false }.initialize!
-
- assert_equal 3, run_count, "There should have been three initializers that incremented the count"
- end
-
def test_consoles_run_on_different_applications_go_to_the_same_class
run_count = 0
AppTemplate::Application.console { run_count += 1 }
diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb
index 44e3b0f66b..fe56e3d076 100644
--- a/railties/test/application/rake_test.rb
+++ b/railties/test/application/rake_test.rb
@@ -145,8 +145,8 @@ module ApplicationTests
# loading a specific fixture
rails "db:fixtures:load", "FIXTURES=products"
- assert_equal 2, ::AppTemplate::Application::Product.count
- assert_equal 0, ::AppTemplate::Application::User.count
+ assert_equal 2, Product.count
+ assert_equal 0, User.count
end
def test_loading_only_yml_fixtures
diff --git a/railties/test/application/zeitwerk_integration_test.rb b/railties/test/application/zeitwerk_integration_test.rb
new file mode 100644
index 0000000000..628a85acd8
--- /dev/null
+++ b/railties/test/application/zeitwerk_integration_test.rb
@@ -0,0 +1,164 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "active_support/dependencies/zeitwerk_integration"
+
+class ZeitwerkIntegrationTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ def setup
+ build_app
+ end
+
+ def boot(env = "development")
+ app(env)
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ def deps
+ ActiveSupport::Dependencies
+ end
+
+ def decorated?
+ deps.singleton_class < deps::ZeitwerkIntegration::Decorations
+ end
+
+ test "ActiveSupport::Dependencies is decorated by default" do
+ boot
+
+ assert decorated?
+ assert_instance_of Zeitwerk::Loader, Rails.autoloader
+ assert_instance_of Zeitwerk::Loader, Rails.once_autoloader
+ assert_equal [Rails.autoloader, Rails.once_autoloader], Rails.autoloaders
+ end
+
+ test "ActiveSupport::Dependencies is not decorated in classic mode" do
+ add_to_config "config.autoloader = :classic"
+ boot
+
+ assert_not decorated?
+ assert_nil Rails.autoloader
+ assert_nil Rails.once_autoloader
+ assert_empty Rails.autoloaders
+ end
+
+ test "constantize returns the value stored in the constant" do
+ app_file "app/models/admin/user.rb", "class Admin::User; end"
+ boot
+
+ assert_same Admin::User, deps.constantize("Admin::User")
+ end
+
+ test "constantize raises if the constant is unknown" do
+ boot
+
+ assert_raises(NameError) { deps.constantize("Admin") }
+ end
+
+ test "safe_constantize returns the value stored in the constant" do
+ app_file "app/models/admin/user.rb", "class Admin::User; end"
+ boot
+
+ assert_same Admin::User, deps.safe_constantize("Admin::User")
+ end
+
+ test "safe_constantize returns nil for unknown constants" do
+ boot
+
+ assert_nil deps.safe_constantize("Admin")
+ end
+
+ test "autoloaded_constants returns autoloaded constant paths" do
+ app_file "app/models/admin/user.rb", "class Admin::User; end"
+ app_file "app/models/post.rb", "class Post; end"
+ boot
+
+ assert Admin::User
+ assert_equal ["Admin", "Admin::User"], deps.autoloaded_constants
+ end
+
+ test "autoloaded? says if a constant has been autoloaded" do
+ app_file "app/models/user.rb", "class User; end"
+ app_file "app/models/post.rb", "class Post; end"
+ boot
+
+ assert Post
+ assert deps.autoloaded?("Post")
+ assert deps.autoloaded?(Post)
+ assert_not deps.autoloaded?("User")
+ end
+
+ test "eager loading loads the application code" do
+ $zeitwerk_integration_test_user = false
+ $zeitwerk_integration_test_post = false
+
+ app_file "app/models/user.rb", "class User; end; $zeitwerk_integration_test_user = true"
+ app_file "app/models/post.rb", "class Post; end; $zeitwerk_integration_test_post = true"
+ boot("production")
+
+ assert $zeitwerk_integration_test_user
+ assert $zeitwerk_integration_test_post
+ end
+
+ test "eager loading loads anything managed by Zeitwerk" do
+ $zeitwerk_integration_test_user = false
+ app_file "app/models/user.rb", "class User; end; $zeitwerk_integration_test_user = true"
+
+ $zeitwerk_integration_test_extras = false
+ app_dir "extras"
+ app_file "extras/webhook_hacks.rb", "WebhookHacks = 1; $zeitwerk_integration_test_extras = true"
+
+ require "zeitwerk"
+ autoloader = Zeitwerk::Loader.new
+ autoloader.push_dir("#{app_path}/extras")
+ autoloader.setup
+
+ boot("production")
+
+ assert $zeitwerk_integration_test_user
+ assert $zeitwerk_integration_test_extras
+ end
+
+ test "autoload paths that are below Gem.path go to the once autoloader" do
+ app_dir "extras"
+ add_to_config 'config.autoload_paths << "#{Rails.root}/extras"'
+
+ # Mocks Gem.path to include the extras directory.
+ Gem.singleton_class.prepend(
+ Module.new do
+ def path
+ super + ["#{Rails.root}/extras"]
+ end
+ end
+ )
+ boot
+
+ assert_not_includes Rails.autoloader.dirs, "#{app_path}/extras"
+ assert_includes Rails.once_autoloader.dirs, "#{app_path}/extras"
+ end
+
+ test "clear reloads the main autoloader, and does not reload the once one" do
+ boot
+
+ $zeitwerk_integration_reload_test = []
+
+ autoloader = Rails.autoloader
+ def autoloader.reload
+ $zeitwerk_integration_reload_test << :autoloader
+ super
+ end
+
+ once_autoloader = Rails.once_autoloader
+ def once_autoloader.reload
+ $zeitwerk_integration_reload_test << :once_autoloader
+ super
+ end
+
+ ActiveSupport::Dependencies.clear
+
+ assert_equal %i(autoloader), $zeitwerk_integration_reload_test
+ end
+end
diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb
index 0e8e0e86ee..4442cdf4bf 100644
--- a/railties/test/isolation/abstract_unit.rb
+++ b/railties/test/isolation/abstract_unit.rb
@@ -421,6 +421,10 @@ module TestHelpers
file_name
end
+ def app_dir(path)
+ FileUtils.mkdir_p("#{app_path}/#{path}")
+ end
+
def remove_file(path)
FileUtils.rm_rf "#{app_path}/#{path}"
end
diff --git a/railties/test/isolation/assets/package.json b/railties/test/isolation/assets/package.json
index 106b1029f0..7c34450fe0 100644
--- a/railties/test/isolation/assets/package.json
+++ b/railties/test/isolation/assets/package.json
@@ -2,6 +2,8 @@
"name": "dummy",
"private": true,
"dependencies": {
+ "@rails/actioncable": "file:../../../../actioncable",
+ "@rails/activestorage": "file:../../../../activestorage",
"@rails/ujs": "file:../../../../actionview",
"@rails/webpacker": "https://github.com/rails/webpacker.git",
"turbolinks": "^5.2.0"
diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb
index 851407dede..69f6e34d58 100644
--- a/railties/test/railties/engine_test.rb
+++ b/railties/test/railties/engine_test.rb
@@ -704,25 +704,27 @@ YAML
RUBY
@plugin.write "app/controllers/bukkits/foo_controller.rb", <<-RUBY
- class Bukkits::FooController < ActionController::Base
- def index
- render inline: "<%= help_the_engine %>"
- end
+ module Bukkits
+ class FooController < ActionController::Base
+ def index
+ render inline: "<%= help_the_engine %>"
+ end
- def show
- render plain: foo_path
- end
+ def show
+ render plain: foo_path
+ end
- def from_app
- render inline: "<%= (self.respond_to?(:bar_path) || self.respond_to?(:something)) %>"
- end
+ def from_app
+ render inline: "<%= (self.respond_to?(:bar_path) || self.respond_to?(:something)) %>"
+ end
- def routes_helpers_in_view
- render inline: "<%= foo_path %>, <%= main_app.bar_path %>"
- end
+ def routes_helpers_in_view
+ render inline: "<%= foo_path %>, <%= main_app.bar_path %>"
+ end
- def polymorphic_path_without_namespace
- render plain: polymorphic_path(Post.new)
+ def polymorphic_path_without_namespace
+ render plain: polymorphic_path(Post.new)
+ end
end
end
RUBY