aboutsummaryrefslogtreecommitdiffstats
path: root/railties
diff options
context:
space:
mode:
Diffstat (limited to 'railties')
-rw-r--r--railties/.gitignore1
-rw-r--r--railties/CHANGELOG.md5
-rw-r--r--railties/Rakefile2
-rw-r--r--railties/lib/rails.rb5
-rw-r--r--railties/lib/rails/application.rb52
-rw-r--r--railties/lib/rails/application/configuration.rb17
-rw-r--r--railties/lib/rails/application/finisher.rb8
-rw-r--r--railties/lib/rails/autoloaders.rb32
-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.rb165
-rw-r--r--railties/test/application/loading_test.rb16
-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/dbs_test.rb6
-rw-r--r--railties/test/application/rake/multi_dbs_test.rb1
-rw-r--r--railties/test/application/rake_test.rb4
-rw-r--r--railties/test/application/server_test.rb16
-rw-r--r--railties/test/application/zeitwerk_integration_test.rb209
-rw-r--r--railties/test/isolation/abstract_unit.rb29
-rw-r--r--railties/test/isolation/assets/config/webpack/development.js3
-rw-r--r--railties/test/isolation/assets/config/webpack/production.js3
-rw-r--r--railties/test/isolation/assets/config/webpack/test.js3
-rw-r--r--railties/test/isolation/assets/config/webpacker.yml8
-rw-r--r--railties/test/isolation/assets/package.json11
-rw-r--r--railties/test/railties/engine_test.rb32
27 files changed, 605 insertions, 83 deletions
diff --git a/railties/.gitignore b/railties/.gitignore
index c08562e016..17a49da08c 100644
--- a/railties/.gitignore
+++ b/railties/.gitignore
@@ -2,4 +2,5 @@
/test/500.html
/test/fixtures/tmp/
/test/initializer/root/log/
+/test/isolation/assets/yarn.lock
/tmp/
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/Rakefile b/railties/Rakefile
index 1c4725e2d6..4ae546b202 100644
--- a/railties/Rakefile
+++ b/railties/Rakefile
@@ -38,7 +38,7 @@ namespace :test do
test_patterns = dirs.map { |dir| "test/#{dir}/*_test.rb" }
test_files = Dir[*test_patterns].select do |file|
- !file.start_with?("test/fixtures/")
+ !file.start_with?("test/fixtures/") && !file.start_with?("test/isolation/assets/")
end.sort
if ENV["BUILDKITE_PARALLEL_JOB_COUNT"]
diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb
index 092105d502..1f533a8c04 100644
--- a/railties/lib/rails.rb
+++ b/railties/lib/rails.rb
@@ -13,6 +13,7 @@ require "active_support/core_ext/object/blank"
require "rails/application"
require "rails/version"
+require "rails/autoloaders"
require "active_support/railtie"
require "action_dispatch/railtie"
@@ -110,5 +111,9 @@ module Rails
def public_path
application && Pathname.new(application.paths["public"].first)
end
+
+ def autoloaders
+ Autoloaders
+ end
end
end
diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb
index 5a924ab8e6..fbad3e5db3 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,52 @@ 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)
+ regular_writer(key.to_sym, convert_value(value, for: :assignment))
+ 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
+
+ def convert_value(value, options = {}) # :doc:
+ if value.is_a? Hash
+ if options[:for] == :to_hash
+ value.to_hash
+ else
+ self.class.new(value)
+ end
+ elsif value.is_a?(Array)
+ if options[:for] != :assignment || value.frozen?
+ value = value.dup
+ end
+ value.map! { |e| convert_value(e, options) }
+ else
+ value
+ end
+ 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/autoloaders.rb b/railties/lib/rails/autoloaders.rb
new file mode 100644
index 0000000000..b03499cf81
--- /dev/null
+++ b/railties/lib/rails/autoloaders.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Rails
+ module Autoloaders # :nodoc:
+ class << self
+ include Enumerable
+
+ def main
+ if zeitwerk_enabled?
+ @main ||= Zeitwerk::Loader.new.tap { |loader| loader.tag = "rails.main" }
+ end
+ end
+
+ def once
+ if zeitwerk_enabled?
+ @once ||= Zeitwerk::Loader.new.tap { |loader| loader.tag = "rails.once" }
+ end
+ end
+
+ def each
+ if zeitwerk_enabled?
+ yield main
+ yield once
+ end
+ end
+
+ def zeitwerk_enabled?
+ Rails.configuration.autoloader == :zeitwerk
+ end
+ end
+ 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..76be6a8ac7 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.autoloaders.zeitwerk_enabled?
+ 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..73773602a3 100644
--- a/railties/test/application/configuration_test.rb
+++ b/railties/test/application/configuration_test.rb
@@ -1157,6 +1157,34 @@ module ApplicationTests
end
end
+ test "autoloaders" do
+ app "development"
+
+ config = Rails.application.config
+ assert Rails.autoloaders.zeitwerk_enabled?
+ assert_instance_of Zeitwerk::Loader, Rails.autoloaders.main
+ assert_equal "rails.main", Rails.autoloaders.main.tag
+ assert_instance_of Zeitwerk::Loader, Rails.autoloaders.once
+ assert_equal "rails.once", Rails.autoloaders.once.tag
+ assert_equal [Rails.autoloaders.main, Rails.autoloaders.once], Rails.autoloaders.to_a
+
+ config.autoloader = :classic
+ assert_not Rails.autoloaders.zeitwerk_enabled?
+ assert_nil Rails.autoloaders.main
+ assert_nil Rails.autoloaders.once
+ assert_equal 0, Rails.autoloaders.count
+
+ config.autoloader = :zeitwerk
+ assert Rails.autoloaders.zeitwerk_enabled?
+ assert_instance_of Zeitwerk::Loader, Rails.autoloaders.main
+ assert_equal "rails.main", Rails.autoloaders.main.tag
+ assert_instance_of Zeitwerk::Loader, Rails.autoloaders.once
+ assert_equal "rails.once", Rails.autoloaders.once.tag
+ assert_equal [Rails.autoloaders.main, Rails.autoloaders.once], Rails.autoloaders.to_a
+
+ 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 +1742,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 +1755,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 +1775,47 @@ 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 loads nested custom configuration inside array 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"
+
+ config = Rails.application.config.my_custom_config
+ assert_instance_of Rails::Application::NonSymbolAccessDeprecatedHash, config[:foo][:bar].first
+
+ assert_deprecated do
+ assert_equal 1, config[:foo][:bar].first["baz"]
+ end
+ end
+
test "config_for makes all hash methods available" do
app_file "config/custom.yml", <<-RUBY
development:
@@ -1762,12 +1832,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/loading_test.rb b/railties/test/application/loading_test.rb
index bfa66770bd..9c98489590 100644
--- a/railties/test/application/loading_test.rb
+++ b/railties/test/application/loading_test.rb
@@ -34,6 +34,22 @@ class LoadingTest < ActiveSupport::TestCase
assert_equal "omg", p.title
end
+ test "constants without a matching file raise NameError" do
+ app_file "app/models/post.rb", <<-RUBY
+ class Post
+ NON_EXISTING_CONSTANT
+ end
+ RUBY
+
+ boot_app
+
+ e = assert_raise(NameError) { User }
+ assert_equal "uninitialized constant #{self.class}::User", e.message
+
+ e = assert_raise(NameError) { Post }
+ assert_equal "uninitialized constant Post::NON_EXISTING_CONSTANT", e.message
+ end
+
test "concerns in app are autoloaded" do
app_file "app/controllers/concerns/trackable.rb", <<-CONCERN
module Trackable
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/dbs_test.rb b/railties/test/application/rake/dbs_test.rb
index 5879949b61..ba5704c53e 100644
--- a/railties/test/application/rake/dbs_test.rb
+++ b/railties/test/application/rake/dbs_test.rb
@@ -179,9 +179,10 @@ module ApplicationTests
def db_fixtures_load(expected_database)
Dir.chdir(app_path) do
rails "generate", "model", "book", "title:string"
+ reload
rails "db:migrate", "db:fixtures:load"
+
assert_match expected_database, ActiveRecord::Base.connection_config[:database]
- require "#{app_path}/app/models/book"
assert_equal 2, Book.count
end
end
@@ -201,8 +202,9 @@ module ApplicationTests
require "#{app_path}/config/environment"
rails "generate", "model", "admin::book", "title:string"
+ reload
rails "db:migrate", "db:fixtures:load"
- require "#{app_path}/app/models/admin/book"
+
assert_equal 2, Admin::Book.count
end
diff --git a/railties/test/application/rake/multi_dbs_test.rb b/railties/test/application/rake/multi_dbs_test.rb
index ef99365e75..d676e7486e 100644
--- a/railties/test/application/rake/multi_dbs_test.rb
+++ b/railties/test/application/rake/multi_dbs_test.rb
@@ -170,6 +170,7 @@ module ApplicationTests
rails "generate", "model", "book", "title:string"
rails "generate", "model", "dog", "name:string"
write_models_for_animals
+ reload
end
test "db:create and db:drop works on all databases for env" do
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/server_test.rb b/railties/test/application/server_test.rb
index 5b8aab08d5..9df36b3444 100644
--- a/railties/test/application/server_test.rb
+++ b/railties/test/application/server_test.rb
@@ -30,17 +30,15 @@ module ApplicationTests
pid = nil
Bundler.with_original_env do
- begin
- pid = Process.spawn("bin/rails server -P tmp/dummy.pid", chdir: app_path, in: replica, out: replica, err: replica)
- assert_output("Listening", primary)
+ pid = Process.spawn("bin/rails server -P tmp/dummy.pid", chdir: app_path, in: replica, out: replica, err: replica)
+ assert_output("Listening", primary)
- rails("restart")
+ rails("restart")
- assert_output("Restarting", primary)
- assert_output("Inherited", primary)
- ensure
- kill(pid) if pid
- end
+ assert_output("Restarting", primary)
+ assert_output("Inherited", primary)
+ ensure
+ kill(pid) if pid
end
end
diff --git a/railties/test/application/zeitwerk_integration_test.rb b/railties/test/application/zeitwerk_integration_test.rb
new file mode 100644
index 0000000000..bbb97e983a
--- /dev/null
+++ b/railties/test/application/zeitwerk_integration_test.rb
@@ -0,0 +1,209 @@
+# 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 Rails.autoloaders.zeitwerk_enabled?
+ assert_instance_of Zeitwerk::Loader, Rails.autoloaders.main
+ assert_instance_of Zeitwerk::Loader, Rails.autoloaders.once
+ assert_equal [Rails.autoloaders.main, Rails.autoloaders.once], Rails.autoloaders.to_a
+ end
+
+ test "ActiveSupport::Dependencies is not decorated in classic mode" do
+ add_to_config "config.autoloader = :classic"
+ boot
+
+ assert_not decorated?
+ assert_not Rails.autoloaders.zeitwerk_enabled?
+ assert_nil Rails.autoloaders.main
+ assert_nil Rails.autoloaders.once
+ assert_equal 0, Rails.autoloaders.count
+ 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.autoloaders.main.dirs, "#{app_path}/extras"
+ assert_includes Rails.autoloaders.once.dirs, "#{app_path}/extras"
+ end
+
+ test "clear reloads the main autoloader, and does not reload the once one" do
+ boot
+
+ $zeitwerk_integration_reload_test = []
+
+ main_autoloader = Rails.autoloaders.main
+ def main_autoloader.reload
+ $zeitwerk_integration_reload_test << :main_autoloader
+ super
+ end
+
+ once_autoloader = Rails.autoloaders.once
+ def once_autoloader.reload
+ $zeitwerk_integration_reload_test << :once_autoloader
+ super
+ end
+
+ ActiveSupport::Dependencies.clear
+
+ assert_equal %i(main_autoloader), $zeitwerk_integration_reload_test
+ end
+
+ test "verbose = true sets the debug method of the dependencies logger if present" do
+ boot
+
+ logger = Logger.new(File::NULL)
+ ActiveSupport::Dependencies.logger = logger
+ ActiveSupport::Dependencies.verbose = true
+
+ Rails.autoloaders.each do |autoloader|
+ assert_equal logger.method(:debug), autoloader.logger
+ end
+ end
+
+ test "verbose = true sets the debug method of the Rails logger as fallback" do
+ boot
+
+ ActiveSupport::Dependencies.verbose = true
+
+ Rails.autoloaders.each do |autoloader|
+ assert_equal Rails.logger.method(:debug), autoloader.logger
+ end
+ end
+
+ test "verbose = false sets loggers to nil" do
+ boot
+
+ ActiveSupport::Dependencies.verbose = true
+ Rails.autoloaders.each do |autoloader|
+ assert autoloader.logger
+ end
+
+ ActiveSupport::Dependencies.verbose = false
+ Rails.autoloaders.each do |autoloader|
+ assert_nil autoloader.logger
+ end
+ end
+
+ test "unhooks" do
+ boot
+
+ assert_equal Module, Module.method(:const_missing).owner
+ assert_equal :no_op, deps.unhook!
+ end
+end
diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb
index ca7601f6fe..3f1638a516 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
@@ -454,12 +458,19 @@ module TestHelpers
end
end
end
+
+ module Reload
+ def reload
+ ActiveSupport::Dependencies.clear
+ end
+ end
end
class ActiveSupport::TestCase
include TestHelpers::Paths
include TestHelpers::Rack
include TestHelpers::Generation
+ include TestHelpers::Reload
include ActiveSupport::Testing::Stream
include ActiveSupport::Testing::MethodCallAssertions
end
@@ -472,21 +483,17 @@ Module.new do
FileUtils.rm_rf(app_template_path)
FileUtils.mkdir_p(app_template_path)
- Dir.chdir "#{RAILS_FRAMEWORK_ROOT}/actionview" do
- `yarn build`
- end
-
- `#{Gem.ruby} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails new #{app_template_path} --skip-bundle --skip-listen --no-rc`
+ `#{Gem.ruby} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails new #{app_template_path} --skip-bundle --skip-listen --no-rc --skip-webpack-install`
File.open("#{app_template_path}/config/boot.rb", "w") do |f|
f.puts "require 'rails/all'"
end
- Dir.chdir(app_template_path) { `yarn add https://github.com/rails/webpacker.git` } # Use the latest version.
-
- # Manually install `webpack` as bin symlinks are not created for sub dependencies
- # in workspaces. See https://github.com/yarnpkg/yarn/issues/4964
- Dir.chdir(app_template_path) { `yarn add webpack@4.17.1 --tilde` }
- Dir.chdir(app_template_path) { `yarn add webpack-cli` }
+ assets_path = "#{RAILS_FRAMEWORK_ROOT}/railties/test/isolation/assets"
+ FileUtils.cp("#{assets_path}/package.json", "#{app_template_path}/package.json")
+ FileUtils.cp("#{assets_path}/config/webpacker.yml", "#{app_template_path}/config/webpacker.yml")
+ FileUtils.cp_r("#{assets_path}/config/webpack", "#{app_template_path}/config/webpack")
+ FileUtils.ln_s("#{assets_path}/node_modules", "#{app_template_path}/node_modules")
+ FileUtils.chdir(app_template_path) { `bin/rails webpacker:binstubs` }
# Fake 'Bundler.require' -- we run using the repo's Gemfile, not an
# app-specific one: we don't want to require every gem that lists.
diff --git a/railties/test/isolation/assets/config/webpack/development.js b/railties/test/isolation/assets/config/webpack/development.js
new file mode 100644
index 0000000000..395290f431
--- /dev/null
+++ b/railties/test/isolation/assets/config/webpack/development.js
@@ -0,0 +1,3 @@
+process.env.NODE_ENV = process.env.NODE_ENV || 'development'
+const { environment } = require('@rails/webpacker')
+module.exports = environment.toWebpackConfig()
diff --git a/railties/test/isolation/assets/config/webpack/production.js b/railties/test/isolation/assets/config/webpack/production.js
new file mode 100644
index 0000000000..d064a6a7fb
--- /dev/null
+++ b/railties/test/isolation/assets/config/webpack/production.js
@@ -0,0 +1,3 @@
+process.env.NODE_ENV = process.env.NODE_ENV || 'production'
+const { environment } = require('@rails/webpacker')
+module.exports = environment.toWebpackConfig()
diff --git a/railties/test/isolation/assets/config/webpack/test.js b/railties/test/isolation/assets/config/webpack/test.js
new file mode 100644
index 0000000000..395290f431
--- /dev/null
+++ b/railties/test/isolation/assets/config/webpack/test.js
@@ -0,0 +1,3 @@
+process.env.NODE_ENV = process.env.NODE_ENV || 'development'
+const { environment } = require('@rails/webpacker')
+module.exports = environment.toWebpackConfig()
diff --git a/railties/test/isolation/assets/config/webpacker.yml b/railties/test/isolation/assets/config/webpacker.yml
new file mode 100644
index 0000000000..0b1f43a407
--- /dev/null
+++ b/railties/test/isolation/assets/config/webpacker.yml
@@ -0,0 +1,8 @@
+default: &default
+ check_yarn_integrity: false
+development:
+ <<: *default
+test:
+ <<: *default
+production:
+ <<: *default
diff --git a/railties/test/isolation/assets/package.json b/railties/test/isolation/assets/package.json
new file mode 100644
index 0000000000..7c34450fe0
--- /dev/null
+++ b/railties/test/isolation/assets/package.json
@@ -0,0 +1,11 @@
+{
+ "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