diff options
Diffstat (limited to 'railties')
104 files changed, 2354 insertions, 712 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..7bc7391f9e 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,314 +1,3 @@ -* Fix deeply nested namespace command printing. - *Gannon McGibbon* - -## Rails 6.0.0.beta1 (January 18, 2019) ## - -* Remove deprecated `after_bundle` helper inside plugins templates. - - *Rafael Mendonça França* - -* Remove deprecated support to old `config.ru` that use the application class as argument of `run`. - - *Rafael Mendonça França* - -* Remove deprecated `environment` argument from the rails commands. - - *Rafael Mendonça França* - -* Remove deprecated `capify!`. - - *Rafael Mendonça França* - -* Remove deprecated `config.secret_token`. - - *Rafael Mendonça França* - -* Seed database with inline ActiveJob job adapter. - - *Gannon McGibbon* - -* Add `rails db:system:change` command for changing databases. - - ``` - bin/rails db:system:change --to=postgresql - force config/database.yml - gsub Gemfile - ``` - - The change command copies a template `config/database.yml` with the target database adapter into your app, and replaces your database gem with the target database gem. - - *Gannon McGibbon* - -* Add `rails test:channels`. - - *bogdanvlviv* - -* Use original `bundler` environment variables during the process of generating a new rails project. - - *Marco Costa* - -* Send Active Storage analysis and purge jobs to dedicated queues by default. - - Analysis jobs now use the `:active_storage_analysis` queue, and purge jobs - now use the `:active_storage_purge` queue. This matches Action Mailbox, - which sends its jobs to dedicated queues by default. - - *George Claghorn* - -* Add `rails test:mailboxes`. - - *George Claghorn* - -* Introduce guard against DNS rebinding attacks - - The `ActionDispatch::HostAuthorization` is a new middleware that prevent - against DNS rebinding and other `Host` header attacks. It is included in - the development environment by default with the following configuration: - - Rails.application.config.hosts = [ - IPAddr.new("0.0.0.0/0"), # All IPv4 addresses. - IPAddr.new("::/0"), # All IPv6 addresses. - "localhost" # The localhost reserved domain. - ] - - In other environments `Rails.application.config.hosts` is empty and no - `Host` header checks will be done. If you want to guard against header - attacks on production, you have to manually permit the allowed hosts - with: - - Rails.application.config.hosts << "product.com" - - The host of a request is checked against the `hosts` entries with the case - operator (`#===`), which lets `hosts` support entries of type `RegExp`, - `Proc` and `IPAddr` to name a few. Here is an example with a regexp. - - # Allow requests from subdomains like `www.product.com` and - # `beta1.product.com`. - Rails.application.config.hosts << /.*\.product\.com/ - - A special case is supported that allows you to permit all sub-domains: - - # Allow requests from subdomains like `www.product.com` and - # `beta1.product.com`. - Rails.application.config.hosts << ".product.com" - - *Genadi Samokovarov* - -* Remove redundant suffixes on generated helpers. - - *Gannon McGibbon* - -* Remove redundant suffixes on generated integration tests. - - *Gannon McGibbon* - -* Fix boolean interaction in scaffold system tests. - - *Gannon McGibbon* - -* Remove redundant suffixes on generated system tests. - - *Gannon McGibbon* - -* Add an `abort_on_failure` boolean option to the generator method that shell - out (`generate`, `rake`, `rails_command`) to abort the generator if the - command fails. - - *David Rodríguez* - -* Remove `app/assets` and `app/javascript` from `eager_load_paths` and `autoload_paths`. - - *Gannon McGibbon* - -* Use Ids instead of memory addresses when displaying references in scaffold views. - - Fixes #29200. - - *Rasesh Patel* - -* Adds support for multiple databases to `rails db:migrate:status`. - Subtasks are also added to get the status of individual databases (eg. `rails db:migrate:status:animals`). - - *Gannon McGibbon* - -* Use Webpacker by default to manage app-level JavaScript through the new app/javascript directory. - Sprockets is now solely in charge, by default, of compiling CSS and other static assets. - Action Cable channel generators will create ES6 stubs rather than use CoffeeScript. - Active Storage, Action Cable, Turbolinks, and Rails-UJS are loaded by a new application.js pack. - Generators no longer generate JavaScript stubs. - - *DHH*, *Lachlan Sylvester* - -* Add `database` (aliased as `db`) option to model generator to allow - setting the database. This is useful for applications that use - multiple databases and put migrations per database in their own directories. - - ``` - bin/rails g model Room capacity:integer --database=kingston - invoke active_record - create db/kingston_migrate/20180830151055_create_rooms.rb - ``` - - Because rails scaffolding uses the model generator, you can - also specify a database with the scaffold generator. - - *Gannon McGibbon* - -* Raise an error when "recyclable cache keys" are being used by a cache store - that does not explicitly support it. Custom cache keys that do support this feature - can bypass this error by implementing the `supports_cache_versioning?` method on their - class and returning a truthy value. - - *Richard Schneeman* - -* Support environment specific credentials overrides. - - So any environment will look for `config/credentials/#{Rails.env}.yml.enc` and fall back - to `config/credentials.yml.enc`. - - The encryption key can be in `ENV["RAILS_MASTER_KEY"]` or `config/credentials/production.key`. - - Environment credentials overrides can be edited with `rails credentials:edit --environment production`. - If no override is setup for the passed environment, it will be created. - - Additionally, the default lookup paths can be overwritten with these configs: - - - `config.credentials.content_path` - - `config.credentials.key_path` - - *Wojciech Wnętrzak* - -* Make `ActiveSupport::Cache::NullStore` the default cache store in the test environment. - - *Michael C. Nelson* - -* Emit warning for unknown inflection rule when generating model. - - *Yoshiyuki Kinjo* - -* Add `database` (aliased as `db`) option to migration generator. - - If you're using multiple databases and have a folder for each database - for migrations (ex db/migrate and db/new_db_migrate) you can now pass the - `--database` option to the generator to make sure the the migration - is inserted into the correct folder. - - ``` - rails g migration CreateHouses --database=kingston - invoke active_record - create db/kingston_migrate/20180830151055_create_houses.rb - ``` - - *Eileen M. Uchitelle* - -* Deprecate `rake routes` in favor of `rails routes`. - - *Yuji Yaginuma* - -* Deprecate `rake initializers` in favor of `rails initializers`. - - *Annie-Claude Côté* - -* Deprecate `rake dev:cache` in favor of `rails dev:cache`. - - *Annie-Claude Côté* - -* Deprecate `rails notes` subcommands in favor of passing an `annotations` argument to `rails notes`. - - The following subcommands are replaced by passing `--annotations` or `-a` to `rails notes`: - - `rails notes:custom ANNOTATION=custom` is deprecated in favor of using `rails notes -a custom`. - - `rails notes:optimize` is deprecated in favor of using `rails notes -a OPTIMIZE`. - - `rails notes:todo` is deprecated in favor of using`rails notes -a TODO`. - - `rails notes:fixme` is deprecated in favor of using `rails notes -a FIXME`. - - *Annie-Claude Côté* - -* Deprecate `SOURCE_ANNOTATION_DIRECTORIES` environment variable used by `rails notes` - through `Rails::SourceAnnotationExtractor::Annotation` in favor of using `config.annotations.register_directories`. - - *Annie-Claude Côté* - -* Deprecate `rake notes` in favor of `rails notes`. - - *Annie-Claude Côté* - -* Don't generate unused files in `app:update` task. - - Skip the assets' initializer when sprockets isn't loaded. - - Skip `config/spring.rb` when spring isn't loaded. - - Skip yarn's contents when yarn integration isn't used. - - *Tsukuru Tanimichi* - -* Make the master.key file read-only for the owner upon generation on - POSIX-compliant systems. - - Previously: - - $ ls -l config/master.key - -rw-r--r-- 1 owner group 32 Jan 1 00:00 master.key - - Now: - - $ ls -l config/master.key - -rw------- 1 owner group 32 Jan 1 00:00 master.key - - Fixes #32604. - - *Jose Luis Duran* - -* Deprecate support for using the `HOST` environment to specify the server IP. - - The `BINDING` environment should be used instead. - - Fixes #29516. - - *Yuji Yaginuma* - -* Deprecate passing Rack server name as a regular argument to `rails server`. - - Previously: - - $ bin/rails server thin - - There wasn't an explicit option for the Rack server to use, now we have the - `--using` option with the `-u` short switch. - - Now: - - $ bin/rails server -u thin - - This change also improves the error message if a missing or mistyped rack - server is given. - - *Genadi Samokovarov* - -* Add "rails routes --expanded" option to output routes in expanded mode like - "psql --expanded". Result looks like: - - ``` - $ rails routes --expanded - --[ Route 1 ]------------------------------------------------------------ - Prefix | high_scores - Verb | GET - URI | /high_scores(.:format) - Controller#Action | high_scores#index - --[ Route 2 ]------------------------------------------------------------ - Prefix | new_high_score - Verb | GET - URI | /high_scores/new(.:format) - Controller#Action | high_scores#new - ``` - - *Benoit Tigeot* - -* Rails 6 requires Ruby 2.5.0 or newer. - - *Jeremy Daer*, *Kasper Timm Hansen* - - -Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/railties/CHANGELOG.md) for previous changes. +Please check [6-0-stable](https://github.com/rails/rails/blob/6-0-stable/railties/CHANGELOG.md) for previous changes. diff --git a/railties/RDOC_MAIN.rdoc b/railties/RDOC_MAIN.rdoc index 1c4252edd0..f1783a4737 100644 --- a/railties/RDOC_MAIN.rdoc +++ b/railties/RDOC_MAIN.rdoc @@ -4,7 +4,7 @@ \Rails is a web-application framework that includes everything needed to create database-backed web applications according to the -{Model-View-Controller (MVC)}[http://en.wikipedia.org/wiki/Model-view-controller] +{Model-View-Controller (MVC)}[https://en.wikipedia.org/wiki/Model-view-controller] pattern. Understanding the MVC pattern is key to understanding \Rails. MVC divides your @@ -79,7 +79,7 @@ and may also be used independently outside \Rails. * The \README file created within your application. * {Getting Started with \Rails}[https://guides.rubyonrails.org/getting_started.html]. * {Ruby on \Rails Guides}[https://guides.rubyonrails.org]. - * {The API Documentation}[http://api.rubyonrails.org]. + * {The API Documentation}[https://api.rubyonrails.org]. * {Ruby on \Rails Tutorial}[https://www.railstutorial.org/book]. == Contributing @@ -88,7 +88,7 @@ We encourage you to contribute to Ruby on \Rails! Please check out the {Contributing to Ruby on \Rails guide}[https://guides.rubyonrails.org/contributing_to_ruby_on_rails.html] for guidelines about how to proceed. {Join us!}[http://contributors.rubyonrails.org] Trying to report a possible security vulnerability in \Rails? Please -check out our {security policy}[http://rubyonrails.org/security/] for +check out our {security policy}[https://rubyonrails.org/security/] for guidelines about how to proceed. Everyone interacting in \Rails and its sub-projects' codebases, issue trackers, chat rooms, and mailing lists is expected to follow the \Rails {code of conduct}[http://rubyonrails.org/conduct/]. diff --git a/railties/README.rdoc b/railties/README.rdoc index 2715ccac3f..51437e3147 100644 --- a/railties/README.rdoc +++ b/railties/README.rdoc @@ -29,7 +29,7 @@ Railties is released under the MIT license: API documentation is at -* http://api.rubyonrails.org +* https://api.rubyonrails.org Bug reports can be filed for the Ruby on Rails project here: diff --git a/railties/Rakefile b/railties/Rakefile index 1c4725e2d6..51f46d1817 100644 --- a/railties/Rakefile +++ b/railties/Rakefile @@ -11,6 +11,33 @@ task test: "test:isolated" namespace :test do task :isolated do + estimated_duration = { + "test/application/test_runner_test.rb" => 201, + "test/application/assets_test.rb" => 131, + "test/application/rake/migrations_test.rb" => 65, + "test/generators/scaffold_generator_test.rb" => 57, + "test/generators/plugin_test_runner_test.rb" => 57, + "test/application/test_test.rb" => 52, + "test/application/configuration_test.rb" => 49, + "test/generators/app_generator_test.rb" => 43, + "test/application/rake/dbs_test.rb" => 43, + "test/application/rake_test.rb" => 33, + "test/generators/plugin_generator_test.rb" => 30, + "test/railties/engine_test.rb" => 27, + "test/generators/scaffold_controller_generator_test.rb" => 23, + "test/railties/generators_test.rb" => 19, + "test/application/console_test.rb" => 16, + "test/engine/commands_test.rb" => 15, + "test/application/routing_test.rb" => 15, + "test/application/mailer_previews_test.rb" => 15, + "test/application/rake/multi_dbs_test.rb" => 13, + "test/application/asset_debugging_test.rb" => 12, + "test/application/bin_setup_test.rb" => 11, + "test/engine/test_test.rb" => 10, + "test/application/runner_test.rb" => 10, + } + estimated_duration.default = 1 + dash_i = [ "test", "lib", @@ -38,14 +65,24 @@ 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/") - end.sort + !file.start_with?("test/fixtures/") && !file.start_with?("test/isolation/assets/") + end 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 + buckets = Array.new(m) { [] } + allocations = Array.new(m) { 0 } + test_files.sort_by { |file| [-estimated_duration[file], file] }.each do |file| + idx = allocations.index(allocations.min) + buckets[idx] << file + allocations[idx] += estimated_duration[file] + end + + puts "Running #{buckets[n].size} of #{test_files.size} test files, estimated duration #{allocations[n]}s" + + test_files = buckets[n] end test_files.each do |file| @@ -58,14 +95,18 @@ namespace :test do ]) puts fake_command - # We could run these in parallel, but pretty much all of the - # railties tests already run in parallel, so ¯\_(⊙︿⊙)_/¯ - Process.waitpid fork { - ARGV.clear.concat test_options - Rake.application = nil - - load file - } + if Process.respond_to?(:fork) + # We could run these in parallel, but pretty much all of the + # railties tests already run in parallel, so ¯\_(⊙︿⊙)_/¯ + Process.waitpid fork { + ARGV.clear.concat test_options + Rake.application = nil + + load file + } + else + Process.wait spawn(fake_command) + end unless $?.success? failing_files << file 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..038284ebdd 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}" @@ -408,14 +409,15 @@ module Rails # The secret_key_base is used as the input secret to the application's key generator, which in turn # is used to create all MessageVerifiers/MessageEncryptors, including the ones that sign and encrypt cookies. # - # In test and development, this is simply derived as a MD5 hash of the application's name. + # In development and test, this is randomly generated and stored in a + # temporary file in <tt>tmp/development_secret.txt</tt>. # # In all other environments, we look for it first in ENV["SECRET_KEY_BASE"], # then credentials.secret_key_base, and finally secrets.secret_key_base. For most applications, # the correct place to store it is in the encrypted credentials file. def secret_key_base - if Rails.env.test? || Rails.env.development? - secrets.secret_key_base || Digest::MD5.hexdigest(self.class.name) + if Rails.env.development? || Rails.env.test? + secrets.secret_key_base ||= generate_development_secret else validate_secret_key_base( ENV["SECRET_KEY_BASE"] || credentials.secret_key_base || secrets.secret_key_base @@ -580,6 +582,22 @@ module Rails private + def generate_development_secret + if secrets.secret_key_base.nil? + key_file = Rails.root.join("tmp/development_secret.txt") + + if !File.exist?(key_file) + random_key = SecureRandom.hex(64) + FileUtils.mkdir_p(key_file.dirname) + File.binwrite(key_file, random_key) + end + + secrets.secret_key_base = File.binread(key_file) + end + + secrets.secret_key_base + end + def build_request(env) req = super env["ORIGINAL_FULLPATH"] = req.fullpath @@ -590,5 +608,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/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb index e3c0759f95..50685a4d7a 100644 --- a/railties/lib/rails/application/bootstrap.rb +++ b/railties/lib/rails/application/bootstrap.rb @@ -19,14 +19,14 @@ module Rails initializer :set_eager_load, group: :all do if config.eager_load.nil? - warn <<-INFO -config.eager_load is set to nil. Please update your config/environments/*.rb files accordingly: + warn <<~INFO + config.eager_load is set to nil. Please update your config/environments/*.rb files accordingly: - * development - set it to false - * test - set it to false (unless you use a tool that preloads your test environment) - * production - set it to true + * development - set it to false + * test - set it to false (unless you use a tool that preloads your test environment) + * production - set it to true -INFO + INFO config.eager_load = config.cache_classes end end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index b7838f7e32..0b758dd3dd 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -18,9 +18,10 @@ module Rails :session_options, :time_zone, :reload_classes_only_on_change, :beginning_of_week, :filter_redirect, :x, :enable_dependency_loading, :read_encrypted_secrets, :log_level, :content_security_policy_report_only, - :content_security_policy_nonce_generator, :require_master_key, :credentials + :content_security_policy_nonce_generator, :require_master_key, :credentials, + :disable_sandbox, :add_autoload_paths_to_load_path - attr_reader :encoding, :api_only, :loaded_config_version + attr_reader :encoding, :api_only, :loaded_config_version, :autoloader def initialize(*) super @@ -64,6 +65,9 @@ module Rails @credentials = ActiveSupport::OrderedOptions.new @credentials.content_path = default_credentials_content_path @credentials.key_path = default_credentials_key_path + @autoloader = :classic + @disable_sandbox = false + @add_autoload_paths_to_load_path = true end def load_defaults(target_version) @@ -117,6 +121,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 @@ -137,6 +143,12 @@ module Rails active_storage.queues.analysis = :active_storage_analysis active_storage.queues.purge = :active_storage_purge end + + if respond_to?(:active_record) + active_record.collection_cache_versioning = true + end + when "6.1" + load_defaults "6.0" else raise "Unknown version #{target_version.to_s.inspect}" end @@ -181,6 +193,26 @@ module Rails end end + # Load the database YAML without evaluating ERB. This allows us to + # create the rake tasks for multiple databases without filling in the + # configuration values or loading the environment. Do not use this + # method. + # + # This uses a DummyERB custom compiler so YAML can ignore the ERB + # tags and load the database.yml for the rake tasks. + def load_database_yaml # :nodoc: + if path = paths["config/database"].existent.first + require "rails/application/dummy_erb_compiler" + + yaml = Pathname.new(path) + erb = DummyERB.new(yaml.read) + + YAML.load(erb.result) + else + {} + end + end + # Loads and returns the entire raw configuration of database from # values stored in <tt>config/database.yml</tt>. def database_configuration @@ -267,6 +299,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/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb index 193cc59f3a..9800b19274 100644 --- a/railties/lib/rails/application/default_middleware_stack.rb +++ b/railties/lib/rails/application/default_middleware_stack.rb @@ -49,6 +49,7 @@ module Rails middleware.use ::Rails::Rack::Logger, config.log_tags middleware.use ::ActionDispatch::ShowExceptions, show_exceptions_app middleware.use ::ActionDispatch::DebugExceptions, app, config.debug_exception_response_format + middleware.use ::ActionDispatch::ActionableExceptions unless config.cache_classes middleware.use ::ActionDispatch::Reloader, app.reloader diff --git a/railties/lib/rails/application/dummy_erb_compiler.rb b/railties/lib/rails/application/dummy_erb_compiler.rb new file mode 100644 index 0000000000..c4659123bb --- /dev/null +++ b/railties/lib/rails/application/dummy_erb_compiler.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# These classes are used to strip out the ERB configuration +# values so we can evaluate the database.yml without evaluating +# the ERB values. +class DummyERB < ERB # :nodoc: + def make_compiler(trim_mode) + DummyCompiler.new trim_mode + end +end + +class DummyCompiler < ERB::Compiler # :nodoc: + def compile_content(stag, out) + case stag + when "<%=" + out.push "_erbout << 'dummy_compiler'" + end + end +end diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index 04aaf6dd9a..109c560c80 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +require "active_support/core_ext/string/inflections" +require "active_support/core_ext/array/conversions" + module Rails class Application module Finisher @@ -21,6 +24,47 @@ module Rails end end + # This will become an error if/when we remove classic mode. The plan is + # autoloaders won't be configured up to this point in the finisher, so + # constants just won't be found, raising regular NameError exceptions. + initializer :warn_if_autoloaded, before: :let_zeitwerk_take_over do + next if config.cache_classes + next if ActiveSupport::Dependencies.autoloaded_constants.empty? + + autoloaded = ActiveSupport::Dependencies.autoloaded_constants + constants = "constant".pluralize(autoloaded.size) + enum = autoloaded.to_sentence + have = autoloaded.size == 1 ? "has" : "have" + these = autoloaded.size == 1 ? "This" : "These" + example = autoloaded.first + example_klass = example.constantize.class + + ActiveSupport::DescendantsTracker.clear + ActiveSupport::Dependencies.clear + + ActiveSupport::Deprecation.warn(<<~WARNING) + Initialization autoloaded the #{constants} #{enum}. + + Being able to do this is deprecated. Autoloading during initialization is going + to be an error condition in future versions of Rails. + + Reloading does not reboot the application, and therefore code executed during + initialization does not run again. So, if you reload #{example}, for example, + the expected changes won't be reflected in that stale #{example_klass} object. + + #{these} autoloaded #{constants} #{have} been unloaded. + + Please, check the "Autoloading and Reloading Constants" guide for solutions. + WARNING + end + + initializer :let_zeitwerk_take_over do + if config.autoloader == :zeitwerk + require "active_support/dependencies/zeitwerk_integration" + ActiveSupport::Dependencies::ZeitwerkIntegration.take_over(enable_reloading: !config.cache_classes) + end + end + initializer :add_builtin_route do |app| if Rails.env.development? app.routes.prepend do @@ -66,6 +110,10 @@ module Rails initializer :eager_load! do if config.eager_load ActiveSupport.run_load_hooks(:before_eager_load, self) + # Checks defined?(Zeitwerk) instead of zeitwerk_enabled? because we + # want to eager load any dependency managed by Zeitwerk regardless of + # the autoloading mode of the application. + 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..1bd3f18a74 --- /dev/null +++ b/railties/lib/rails/autoloaders.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require "active_support/dependencies/zeitwerk_integration" + +module Rails + module Autoloaders # :nodoc: + class << self + include Enumerable + + def main + if zeitwerk_enabled? + @main ||= Zeitwerk::Loader.new.tap do |loader| + loader.tag = "rails.main" + loader.inflector = ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector + end + end + end + + def once + if zeitwerk_enabled? + @once ||= Zeitwerk::Loader.new.tap do |loader| + loader.tag = "rails.once" + loader.inflector = ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector + end + end + end + + def each + if zeitwerk_enabled? + yield main + yield once + end + end + + def logger=(logger) + each { |loader| loader.logger = logger } + 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/command/environment_argument.rb b/railties/lib/rails/command/environment_argument.rb index fdc5ee92d9..9945fd1430 100644 --- a/railties/lib/rails/command/environment_argument.rb +++ b/railties/lib/rails/command/environment_argument.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "active_support" +require "active_support/core_ext/class/attribute" module Rails module Command @@ -8,16 +9,18 @@ module Rails extend ActiveSupport::Concern included do - class_option :environment, aliases: "-e", type: :string, - desc: "Specifies the environment to run this console under (test/development/production)." + no_commands do + class_attribute :environment_desc, default: "Specifies the environment to run this #{self.command_name} under (test/development/production)." + end + class_option :environment, aliases: "-e", type: :string, desc: environment_desc end private - def extract_environment_option_from_argument + def extract_environment_option_from_argument(default_environment: Rails::Command.environment) if options[:environment] self.options = options.merge(environment: acceptable_environment(options[:environment])) else - self.options = options.merge(environment: Rails::Command.environment) + self.options = options.merge(environment: default_environment) end end diff --git a/railties/lib/rails/commands/console/console_command.rb b/railties/lib/rails/commands/console/console_command.rb index e35faa5b01..7a9eaefea1 100644 --- a/railties/lib/rails/commands/console/console_command.rb +++ b/railties/lib/rails/commands/console/console_command.rb @@ -26,6 +26,12 @@ module Rails @options = options app.sandbox = sandbox? + + if sandbox? && app.config.disable_sandbox + puts "Error: Unable to start console in sandbox mode as sandbox mode is disabled (config.disable_sandbox is true)." + exit 1 + end + app.load_console @console = app.config.console || IRB diff --git a/railties/lib/rails/commands/credentials/USAGE b/railties/lib/rails/commands/credentials/USAGE index f7268a64d1..c8d3fb9eda 100644 --- a/railties/lib/rails/commands/credentials/USAGE +++ b/railties/lib/rails/commands/credentials/USAGE @@ -42,7 +42,7 @@ from leaking. === Environment Specific Credentials The `credentials` command supports passing an `--environment` option to create an -environment specific override. That override will takes precedence over the +environment specific override. That override will take precedence over the global `config/credentials.yml.enc` file when running in that environment. So: rails credentials:edit --environment development diff --git a/railties/lib/rails/commands/credentials/credentials_command.rb b/railties/lib/rails/commands/credentials/credentials_command.rb index 54ccd97506..e23a1b3008 100644 --- a/railties/lib/rails/commands/credentials/credentials_command.rb +++ b/railties/lib/rails/commands/credentials/credentials_command.rb @@ -2,14 +2,15 @@ require "active_support" require "rails/command/helpers/editor" +require "rails/command/environment_argument" module Rails module Command class CredentialsCommand < Rails::Command::Base # :nodoc: include Helpers::Editor + include EnvironmentArgument - class_option :environment, aliases: "-e", type: :string, - desc: "Uses credentials from config/credentials/:environment.yml.enc encrypted by config/credentials/:environment.key key" + self.environment_desc = "Uses credentials from config/credentials/:environment.yml.enc encrypted by config/credentials/:environment.key key" no_commands do def help @@ -20,6 +21,7 @@ module Rails end def edit + extract_environment_option_from_argument(default_environment: nil) require_application! ensure_editor_available(command: "bin/rails credentials:edit") || (return) @@ -37,6 +39,7 @@ module Rails end def show + extract_environment_option_from_argument(default_environment: nil) require_application! say credentials.read.presence || missing_credentials_message @@ -53,7 +56,11 @@ module Rails end def ensure_credentials_have_been_added - encrypted_file_generator.add_encrypted_file_silently(content_path, key_path) + if options[:environment] + encrypted_file_generator.add_encrypted_file_silently(content_path, key_path) + else + credentials_generator.add_credentials_file_silently + end end def change_credentials_in_system_editor @@ -93,6 +100,13 @@ module Rails Rails::Generators::EncryptedFileGenerator.new end + + def credentials_generator + require "rails/generators" + require "rails/generators/rails/credentials/credentials_generator" + + Rails::Generators::CredentialsGenerator.new + end end end end diff --git a/railties/lib/rails/commands/dbconsole/dbconsole_command.rb b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb index 0fac7d34a0..72f3235ce3 100644 --- a/railties/lib/rails/commands/dbconsole/dbconsole_command.rb +++ b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/deprecation" +require "active_support/core_ext/string/filters" require "rails/command/environment_argument" module Rails @@ -89,15 +91,15 @@ module Rails def config @config ||= begin - # We need to check whether the user passed the connection the + # We need to check whether the user passed the database the # first time around to show a consistent error message to people # relying on 2-level database configuration. - if @options["connection"] && configurations[connection].blank? - raise ActiveRecord::AdapterNotSpecified, "'#{connection}' connection is not configured. Available configuration: #{configurations.inspect}" - elsif configurations[environment].blank? && configurations[connection].blank? + if @options["database"] && configurations[database].blank? + raise ActiveRecord::AdapterNotSpecified, "'#{database}' database is not configured. Available configuration: #{configurations.inspect}" + elsif configurations[environment].blank? && configurations[database].blank? raise ActiveRecord::AdapterNotSpecified, "'#{environment}' database is not configured. Available configuration: #{configurations.inspect}" else - configurations[connection] || configurations[environment].presence + configurations[database] || configurations[environment].presence end end end @@ -106,8 +108,8 @@ module Rails Rails.respond_to?(:env) ? Rails.env : Rails::Command.environment end - def connection - @options.fetch(:connection, "primary") + def database + @options.fetch(:database, "primary") end private @@ -156,12 +158,22 @@ module Rails class_option :connection, aliases: "-c", type: :string, desc: "Specifies the connection to use." + class_option :database, aliases: "--db", type: :string, + desc: "Specifies the database to use." + def perform extract_environment_option_from_argument # RAILS_ENV needs to be set before config/application is required. ENV["RAILS_ENV"] = options[:environment] + if options["connection"] + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `connection` option is deprecated and will be removed in Rails 6.1. Please use `database` option instead. + MSG + options["database"] = options["connection"] + end + require_application_and_environment! Rails::DBConsole.start(options) end diff --git a/railties/lib/rails/commands/dev/dev_command.rb b/railties/lib/rails/commands/dev/dev_command.rb index a3f02f3172..9b2cb2b04a 100644 --- a/railties/lib/rails/commands/dev/dev_command.rb +++ b/railties/lib/rails/commands/dev/dev_command.rb @@ -5,8 +5,10 @@ require "rails/dev_caching" module Rails module Command class DevCommand < Base # :nodoc: - def help - say "rails dev:cache # Toggle development mode caching on/off." + no_commands do + def help + say "rails dev:cache # Toggle development mode caching on/off." + end end def cache diff --git a/railties/lib/rails/commands/encrypted/USAGE b/railties/lib/rails/commands/encrypted/USAGE new file mode 100644 index 0000000000..253eec2378 --- /dev/null +++ b/railties/lib/rails/commands/encrypted/USAGE @@ -0,0 +1,28 @@ +=== Storing Encrypted Files in Source Control + +The Rails `encrypted` commands provide access to encrypted files or configurations. +See the `Rails.application.encrypted` documentation for using them in your app. + +=== Encryption Keys + +By default, Rails looks for the encryption key in `config/master.key` or +`ENV["RAILS_MASTER_KEY"]`, but that lookup can be overridden with `--key`: + + rails encrypted:edit config/encrypted_file.yml.enc --key config/encrypted_file.key + +Don't commit the key! Add it to your source control's ignore file. If you use +Git, Rails handles this for you. + +=== Editing Files + +To edit or create an encrypted file use: + + rails encrypted:edit config/encrypted_file.yml.enc + +This opens a temporary file in `$EDITOR` with the decrypted contents for editing. + +=== Viewing Files + +To print the decrypted contents of an encrypted file use: + + rails encrypted:show config/encrypted_file.yml.enc diff --git a/railties/lib/rails/commands/encrypted/encrypted_command.rb b/railties/lib/rails/commands/encrypted/encrypted_command.rb index 8d5947652a..f10a07cdf8 100644 --- a/railties/lib/rails/commands/encrypted/encrypted_command.rb +++ b/railties/lib/rails/commands/encrypted/encrypted_command.rb @@ -16,6 +16,7 @@ module Rails def help say "Usage:\n #{self.class.banner}" say "" + say self.class.desc end end diff --git a/railties/lib/rails/commands/initializers/initializers_command.rb b/railties/lib/rails/commands/initializers/initializers_command.rb index 33596177af..bd2f3bed67 100644 --- a/railties/lib/rails/commands/initializers/initializers_command.rb +++ b/railties/lib/rails/commands/initializers/initializers_command.rb @@ -1,10 +1,17 @@ # frozen_string_literal: true +require "rails/command/environment_argument" + module Rails module Command class InitializersCommand < Base # :nodoc: + include EnvironmentArgument + desc "initializers", "Print out all defined initializers in the order they are invoked by Rails." def perform + extract_environment_option_from_argument + ENV["RAILS_ENV"] = options[:environment] + require_application_and_environment! Rails.application.initializers.tsort_each do |initializer| diff --git a/railties/lib/rails/commands/notes/notes_command.rb b/railties/lib/rails/commands/notes/notes_command.rb index 64b339b3cd..94cf183855 100644 --- a/railties/lib/rails/commands/notes/notes_command.rb +++ b/railties/lib/rails/commands/notes/notes_command.rb @@ -5,7 +5,7 @@ require "rails/source_annotation_extractor" module Rails module Command class NotesCommand < Base # :nodoc: - class_option :annotations, aliases: "-a", desc: "Filter by specific annotations, e.g. Foobar TODO", type: :array, default: %w(OPTIMIZE FIXME TODO) + class_option :annotations, aliases: "-a", desc: "Filter by specific annotations, e.g. Foobar TODO", type: :array, default: Rails::SourceAnnotationExtractor::Annotation.tags def perform(*) require_application_and_environment! diff --git a/railties/lib/rails/commands/runner/runner_command.rb b/railties/lib/rails/commands/runner/runner_command.rb index cb693bcf34..40fb5e4d89 100644 --- a/railties/lib/rails/commands/runner/runner_command.rb +++ b/railties/lib/rails/commands/runner/runner_command.rb @@ -1,11 +1,13 @@ # frozen_string_literal: true +require "rails/command/environment_argument" + module Rails module Command class RunnerCommand < Base # :nodoc: - class_option :environment, aliases: "-e", type: :string, - default: Rails::Command.environment.dup, - desc: "The environment for the runner to operate under (test/development/production)" + include EnvironmentArgument + + self.environment_desc = "The environment for the runner to operate under (test/development/production)" no_commands do def help @@ -19,6 +21,8 @@ module Rails end def perform(code_or_file = nil, *command_argv) + extract_environment_option_from_argument + unless code_or_file help exit 1 diff --git a/railties/lib/rails/commands/server/server_command.rb b/railties/lib/rails/commands/server/server_command.rb index 47c3f05bb3..982b83ead5 100644 --- a/railties/lib/rails/commands/server/server_command.rb +++ b/railties/lib/rails/commands/server/server_command.rb @@ -6,6 +6,7 @@ require "rails" require "active_support/deprecation" require "active_support/core_ext/string/filters" require "rails/dev_caching" +require "rails/command/environment_argument" module Rails class Server < ::Rack::Server @@ -91,6 +92,8 @@ module Rails module Command class ServerCommand < Base # :nodoc: + include EnvironmentArgument + # Hard-coding a bunch of handlers here as we don't have a public way of # querying them from the Rack::Handler registry. RACK_SERVERS = %w(cgi fastcgi webrick lsws scgi thin puma unicorn) @@ -109,8 +112,6 @@ module Rails desc: "Uses a custom rackup configuration.", banner: :file class_option :daemon, aliases: "-d", type: :boolean, default: false, desc: "Runs server as a Daemon." - class_option :environment, aliases: "-e", type: :string, - desc: "Specifies the environment to run this server under (development/test/production).", banner: :name class_option :using, aliases: "-u", type: :string, desc: "Specifies the Rack server used to run the application (thin/puma/webrick).", banner: :name class_option :pid, aliases: "-P", type: :string, default: DEFAULT_PID_PATH, @@ -130,6 +131,7 @@ module Rails end def perform + extract_environment_option_from_argument set_application_directory! prepare_restart @@ -221,8 +223,8 @@ module Rails if ENV["HOST"] && !ENV["BINDING"] ActiveSupport::Deprecation.warn(<<-MSG.squish) - Using the `HOST` environment to specify the IP is deprecated and will be removed in Rails 6.1. - Please use `BINDING` environment instead. + Using the `HOST` environment variable to specify the IP is deprecated and will be removed in Rails 6.1. + Please use `BINDING` environment variable instead. MSG return ENV["HOST"] @@ -255,7 +257,7 @@ module Rails end def self.banner(*) - "rails server [thin/puma/webrick] [options]" + "rails server -u [thin/puma/webrick] [options]" end def prepare_restart @@ -264,7 +266,7 @@ module Rails def deprecate_positional_rack_server_and_rewrite_to_option(original_options) if using - ActiveSupport::Deprecation.warn(<<~MSG) + ActiveSupport::Deprecation.warn(<<~MSG.squish) Passing the Rack server name as a regular argument is deprecated and will be removed in the next Rails version. Please, use the -u option instead. diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index d6c329b581..d1b8c7803f 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -230,7 +230,7 @@ module Rails # # If +MyEngine+ is isolated, The routes above will point to # <tt>MyEngine::ArticlesController</tt>. You also don't need to use longer - # url helpers like +my_engine_articles_path+. Instead, you should simply use + # URL helpers like +my_engine_articles_path+. Instead, you should simply use # +articles_path+, like you would do with your main application. # # To make this behavior consistent with other parts of the framework, @@ -238,7 +238,7 @@ module Rails # normal Rails app, when you use a namespaced model such as # <tt>Namespace::Article</tt>, <tt>ActiveModel::Naming</tt> will generate # names with the prefix "namespace". In an isolated engine, the prefix will - # be omitted in url helpers and form fields, for convenience. + # be omitted in URL helpers and form fields, for convenience. # # polymorphic_url(MyEngine::Article.new) # # => "articles_path" # not "my_engine_articles_path" @@ -286,11 +286,11 @@ module Rails # Note that the <tt>:as</tt> option given to mount takes the <tt>engine_name</tt> as default, so most of the time # you can simply omit it. # - # Finally, if you want to generate a url to an engine's route using + # Finally, if you want to generate a URL to an engine's route using # <tt>polymorphic_url</tt>, you also need to pass the engine helper. Let's # say that you want to create a form pointing to one of the engine's routes. # All you need to do is pass the helper as the first element in array with - # attributes for url: + # attributes for URL: # # form_for([my_engine, @user]) # @@ -469,9 +469,10 @@ module Rails self end - # Eager load the application by loading all ruby - # files inside eager_load paths. def eager_load! + # Already done by Zeitwerk::Loader.eager_load_all in the finisher. + return if Rails.autoloaders.zeitwerk_enabled? + 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 @@ -532,9 +533,9 @@ module Rails # Defines the routes for this engine. If a block is given to # routes, it is appended to the engine. - def routes + def routes(&block) @routes ||= ActionDispatch::Routing::RouteSet.new_with_config(config) - @routes.append(&Proc.new) if block_given? + @routes.append(&block) if block_given? @routes end @@ -549,12 +550,17 @@ module Rails # Blog::Engine.load_seed def load_seed seed_file = paths["db/seeds.rb"].existent.first - with_inline_jobs { load(seed_file) } if seed_file + return unless seed_file + + if config.try(:active_job)&.queue_adapter == :async + with_inline_jobs { load(seed_file) } + else + load(seed_file) + end end - # Add configured load paths to Ruby's load path, and remove duplicate entries. - initializer :set_load_path, before: :bootstrap_hook do - _all_load_paths.reverse_each do |path| + initializer :set_load_path, before: :bootstrap_hook do |app| + _all_load_paths(app.config.add_autoload_paths_to_load_path).reverse_each do |path| $LOAD_PATH.unshift(path) if File.directory?(path) end $LOAD_PATH.uniq! @@ -569,12 +575,15 @@ module Rails ActiveSupport::Dependencies.autoload_paths.unshift(*_all_autoload_paths) ActiveSupport::Dependencies.autoload_once_paths.unshift(*_all_autoload_once_paths) - # Freeze so future modifications will fail rather than do nothing mysteriously config.autoload_paths.freeze - config.eager_load_paths.freeze config.autoload_once_paths.freeze end + initializer :set_eager_load_paths, before: :bootstrap_hook do + ActiveSupport::Dependencies._eager_load_paths.merge(config.eager_load_paths) + config.eager_load_paths.freeze + end + initializer :add_routing_paths do |app| routing_paths = paths["config/routes.rb"].existent @@ -699,8 +708,12 @@ module Rails @_all_autoload_paths ||= (config.autoload_paths + config.eager_load_paths + config.autoload_once_paths).uniq end - def _all_load_paths - @_all_load_paths ||= (config.paths.load_paths + _all_autoload_paths).uniq + def _all_load_paths(add_autoload_paths_to_load_path) + @_all_load_paths ||= begin + load_paths = config.paths.load_paths + load_paths += _all_autoload_paths if add_autoload_paths_to_load_path + load_paths.uniq + end end def build_request(env) diff --git a/railties/lib/rails/gem_version.rb b/railties/lib/rails/gem_version.rb index 82df56559c..a6b3ccf8f8 100644 --- a/railties/lib/rails/gem_version.rb +++ b/railties/lib/rails/gem_version.rb @@ -8,9 +8,9 @@ module Rails module VERSION MAJOR = 6 - MINOR = 0 + MINOR = 1 TINY = 0 - PRE = "beta1" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index b835b3f3fd..0be00d5151 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -35,6 +35,8 @@ module Rails rails: { actions: "-a", orm: "-o", + javascripts: "-j", + javascript_engine: "-je", resource_controller: "-c", scaffold_controller: "-c", stylesheets: "-y", diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 0eed552042..8782a85391 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -307,7 +307,7 @@ module Rails def assets_gemfile_entry return [] if options[:skip_sprockets] - GemfileEntry.version("sass-rails", "~> 5.0", "Use SCSS for stylesheets") + GemfileEntry.version("sass-rails", ">= 5", "Use SCSS for stylesheets") end def webpacker_gemfile_entry @@ -316,7 +316,7 @@ module Rails if options.dev? || options.edge? GemfileEntry.github "webpacker", "rails/webpacker", nil, "Use development version of Webpacker" else - GemfileEntry.version "webpacker", ">= 4.0.0.rc.3", "Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker" + GemfileEntry.version "webpacker", "~> 4.0", "Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker" end end diff --git a/railties/lib/rails/generators/app_name.rb b/railties/lib/rails/generators/app_name.rb index c4f71694d8..5bb735c4e8 100644 --- a/railties/lib/rails/generators/app_name.rb +++ b/railties/lib/rails/generators/app_name.rb @@ -7,11 +7,11 @@ module Rails private def app_name - @app_name ||= original_app_name.tr("-", "_") + @app_name ||= original_app_name.tr('\\', "").tr("-. ", "_") end def original_app_name - @original_app_name ||= (defined_app_const_base? ? defined_app_name : File.basename(destination_root)).tr('\\', "").tr(". ", "_") + @original_app_name ||= defined_app_const_base? ? defined_app_name : File.basename(destination_root) end def defined_app_name diff --git a/railties/lib/rails/generators/database.rb b/railties/lib/rails/generators/database.rb index 18fc7be3ff..cc6e7b50e5 100644 --- a/railties/lib/rails/generators/database.rb +++ b/railties/lib/rails/generators/database.rb @@ -15,7 +15,7 @@ module Rails case database when "mysql" then ["mysql2", [">= 0.4.4"]] when "postgresql" then ["pg", [">= 0.18", "< 2.0"]] - when "sqlite3" then ["sqlite3", ["~> 1.3", ">= 1.3.6"]] + when "sqlite3" then ["sqlite3", ["~> 1.4"]] when "oracle" then ["activerecord-oracle_enhanced-adapter", nil] when "frontbase" then ["ruby-frontbase", nil] when "sqlserver" then ["activerecord-sqlserver-adapter", nil] diff --git a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb.tt b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb.tt index 518cb1121e..1dddc3d698 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb.tt +++ b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb.tt @@ -4,9 +4,9 @@ <h2><%%= pluralize(<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:</h2> <ul> - <%% <%= singular_table_name %>.errors.full_messages.each do |message| %> - <li><%%= message %></li> - <%% end %> + <%% <%= singular_table_name %>.errors.full_messages.each do |message| %> + <li><%%= message %></li> + <%% end %> </ul> </div> <%% end %> @@ -21,6 +21,9 @@ <div class="field"> <%%= form.label :password_confirmation %> <%%= form.password_field :password_confirmation %> +<% elsif attribute.attachments? -%> + <%%= form.label :<%= attribute.column_name %> %> + <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %>, multiple: true %> <% else -%> <%%= form.label :<%= attribute.column_name %> %> <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %> %> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt index 7deba07926..d3f996188c 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt +++ b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt @@ -3,7 +3,15 @@ <% attributes.reject(&:password_digest?).each do |attribute| -%> <p> <strong><%= attribute.human_name %>:</strong> +<% if attribute.attachment? -%> + <%%= link_to @<%= singular_table_name %>.<%= attribute.column_name %>.filename, @<%= singular_table_name %>.<%= attribute.column_name %> if @<%= singular_table_name %>.<%= attribute.column_name %>.attached? %> +<% elsif attribute.attachments? -%> + <%% @<%= singular_table_name %>.<%= attribute.column_name %>.each do |<%= attribute.singular_name %>| %> + <div><%%= link_to <%= attribute.singular_name %>.filename, <%= attribute.singular_name %> %></div> + <%% end %> +<% else -%> <%%= @<%= singular_table_name %>.<%= attribute.column_name %> %> +<% end -%> </p> <% end -%> diff --git a/railties/lib/rails/generators/generated_attribute.rb b/railties/lib/rails/generators/generated_attribute.rb index a8f7729fd3..1a80e71eae 100644 --- a/railties/lib/rails/generators/generated_attribute.rb +++ b/railties/lib/rails/generators/generated_attribute.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "active_support/time" +require "active_support/deprecation" module Rails module Generators @@ -51,6 +52,12 @@ module Rails type = $1 provided_options = $2.split(/[,.-]/) options = Hash[provided_options.map { |opt| [opt.to_sym, true] }] + + if options[:required] + ActiveSupport::Deprecation.warn("Passing {required} option has no effect on the model generator. It will be removed in Rails 6.1.\n") + options.delete(:required) + end + return type, options else return type, {} @@ -68,13 +75,15 @@ module Rails def field_type @field_type ||= case type - when :integer then :number_field - when :float, :decimal then :text_field - when :time then :time_select - when :datetime, :timestamp then :datetime_select - when :date then :date_select - when :text then :text_area - when :boolean then :check_box + when :integer then :number_field + when :float, :decimal then :text_field + when :time then :time_select + when :datetime, :timestamp then :datetime_select + when :date then :date_select + when :text then :text_area + when :rich_text then :rich_text_area + when :boolean then :check_box + when :attachment, :attachments then :file_field else :text_field end @@ -90,7 +99,9 @@ module Rails when :string then name == "type" ? "" : "MyString" when :text then "MyText" when :boolean then false - when :references, :belongs_to then nil + when :references, :belongs_to, + :attachment, :attachments, + :rich_text then nil else "" end @@ -133,7 +144,7 @@ module Rails end def required? - attr_options[:required] + reference? && Rails.application.config.active_record.belongs_to_required_by_default end def has_index? @@ -152,6 +163,22 @@ module Rails type == :token end + def rich_text? + type == :rich_text + end + + def attachment? + type == :attachment + end + + def attachments? + type == :attachments + end + + def virtual? + rich_text? || attachment? || attachments? + end + def inject_options (+"").tap { |s| options_for_migration.each { |k, v| s << ", #{k}: #{v.inspect}" } } end @@ -163,7 +190,6 @@ module Rails def options_for_migration @attr_options.dup.tap do |options| if required? - options.delete(:required) options[:null] = false end diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt index d39b5d311f..cf5462f7dc 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.4', require: false <%- end -%> <%- if options.api? -%> @@ -69,8 +69,8 @@ group :test do # Adds support for Capybara system testing and selenium driver gem 'capybara', '>= 2.15' gem 'selenium-webdriver' - # Easy installation and use of chromedriver to run system tests with Chrome - gem 'chromedriver-helper' + # Easy installation and use of web drivers to run system tests with browsers + gem 'webdrivers' end <%- end -%> diff --git a/railties/lib/rails/generators/rails/app/templates/app/javascript/packs/application.js.tt b/railties/lib/rails/generators/rails/app/templates/app/javascript/packs/application.js.tt index 908487d500..e67e742263 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/javascript/packs/application.js.tt +++ b/railties/lib/rails/generators/rails/app/templates/app/javascript/packs/application.js.tt @@ -13,3 +13,11 @@ require("@rails/activestorage").start() <%- unless options[:skip_action_cable] -%> require("channels") <%- end -%> + + +// Uncomment to copy all static images under ../images to the output folder and reference +// them with the image_pack_tag helper in views (e.g <%%= image_pack_tag 'rails.png' %>) +// or the `imagePath` JavaScript helper below. +// +// const images = require.context('../images', true) +// const imagePath = (name) => images(name, true) diff --git a/railties/lib/rails/generators/rails/app/templates/bin/setup.tt b/railties/lib/rails/generators/rails/app/templates/bin/setup.tt index 3f73bae3da..5928deb6aa 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/setup.tt +++ b/railties/lib/rails/generators/rails/app/templates/bin/setup.tt @@ -8,7 +8,8 @@ def system!(*args) end FileUtils.chdir APP_ROOT do - # This script is a starting point to setup your application. + # This script is a way to setup or update your development environment automatically. + # This script is idempotent, so that you can run it at anytime and get an expectable outcome. # Add necessary setup steps to this file. puts '== Installing dependencies ==' @@ -27,7 +28,7 @@ FileUtils.chdir APP_ROOT do # end puts "\n== Preparing database ==" - system! 'bin/rails db:setup' + system! 'bin/rails db:prepare' <% end -%> puts "\n== Removing old logs and tempfiles ==" diff --git a/railties/lib/rails/generators/rails/app/templates/bin/update.tt b/railties/lib/rails/generators/rails/app/templates/bin/update.tt deleted file mode 100644 index 03b77d0d46..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/bin/update.tt +++ /dev/null @@ -1,33 +0,0 @@ -require 'fileutils' - -# path to your application root. -APP_ROOT = File.expand_path('..', __dir__) - -def system!(*args) - system(*args) || abort("\n== Command #{args} failed ==") -end - -FileUtils.chdir APP_ROOT do - # This script is a way to update your development environment automatically. - # Add necessary update steps to this file. - - puts '== Installing dependencies ==' - system! 'gem install bundler --conservative' - system('bundle check') || system!('bundle install') -<% unless options.skip_javascript? -%> - - # Install JavaScript dependencies - # system('bin/yarn') -<% end -%> -<% unless options.skip_active_record? -%> - - puts "\n== Updating database ==" - system! 'rails db:migrate' -<% end -%> - - puts "\n== Removing old logs and tempfiles ==" - system! 'rails log:clear tmp:clear' - - puts "\n== Restarting application server ==" - system! 'rails restart' -end diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt index 2887bc2d67..404ab8b5de 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt @@ -15,9 +15,11 @@ Rails.application.configure do # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. if Rails.root.join('tmp', 'caching-dev.txt').exist? + <%- unless options.api? -%> config.action_controller.perform_caching = true config.action_controller.enable_fragment_cache_logging = true + <%- end -%> config.cache_store = :memory_store config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{2.days.to_i}" diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt index ed1cf09eeb..08d437a0be 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt @@ -12,7 +12,9 @@ Rails.application.configure do # Full error reports are disabled and caching is turned on. config.consider_all_requests_local = false + <%- unless options.api? -%> config.action_controller.perform_caching = true + <%- end -%> # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt index 63ed3fa952..c66e349442 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt @@ -1,11 +1,16 @@ +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - - # The test environment is used exclusively to run your application's - # test suite. You never need to work with it otherwise. Remember that - # your test database is "scratch space" for the test suite and is wiped - # and recreated between test runs. Don't rely on the data there! + <%# Spring executes the reloaders when files change. %> + <%- if spring_install? -%> + config.cache_classes = false + <%- else -%> config.cache_classes = true + <%- end -%> # Do not eager load code on boot. This avoids loading your whole application # just for the purpose of running a single test. If you are using a tool that diff --git a/railties/lib/rails/generators/rails/app/templates/ruby-version.tt b/railties/lib/rails/generators/rails/app/templates/ruby-version.tt index bac1339923..096cfd36a8 100644 --- a/railties/lib/rails/generators/rails/app/templates/ruby-version.tt +++ b/railties/lib/rails/generators/rails/app/templates/ruby-version.tt @@ -1 +1 @@ -<%= ENV["RBENV_VERSION"] || ENV["rvm_ruby_string"] || "#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}" -%> +<%= ENV["RBENV_VERSION"] || ENV["rvm_ruby_string"] || "#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}" %> diff --git a/railties/lib/rails/generators/rails/assets/assets_generator.rb b/railties/lib/rails/generators/rails/assets/assets_generator.rb index 9ce8570172..e60637ff37 100644 --- a/railties/lib/rails/generators/rails/assets/assets_generator.rb +++ b/railties/lib/rails/generators/rails/assets/assets_generator.rb @@ -3,7 +3,10 @@ module Rails module Generators class AssetsGenerator < NamedBase # :nodoc: + class_option :javascripts, type: :boolean, desc: "Generate JavaScripts" class_option :stylesheets, type: :boolean, desc: "Generate Stylesheets" + + class_option :javascript_engine, desc: "Engine for JavaScripts" class_option :stylesheet_engine, desc: "Engine for Stylesheets" private @@ -11,6 +14,10 @@ module Rails file_name end + hook_for :javascript_engine do |javascript_engine| + invoke javascript_engine, [name] if options[:javascripts] + end + hook_for :stylesheet_engine do |stylesheet_engine| invoke stylesheet_engine, [name] if options[:stylesheets] end diff --git a/railties/lib/rails/generators/rails/db/system/change/change_generator.rb b/railties/lib/rails/generators/rails/db/system/change/change_generator.rb index 63849eb18d..24db92fad7 100644 --- a/railties/lib/rails/generators/rails/db/system/change/change_generator.rb +++ b/railties/lib/rails/generators/rails/db/system/change/change_generator.rb @@ -35,8 +35,9 @@ module Rails end def edit_gemfile - database_gem_name, _ = gem_for_database - gsub_file("Gemfile", all_database_gems_regex, database_gem_name) + name, version = gem_for_database + gsub_file("Gemfile", all_database_gems_regex, name) + gsub_file("Gemfile", gem_entry_regex_for(name), gem_entry_for(name, *version)) end private @@ -48,6 +49,15 @@ module Rails all_database_gem_names = all_database_gems.map(&:first) /(\b#{all_database_gem_names.join('\b|\b')}\b)/ end + + def gem_entry_regex_for(gem_name) + /^gem.*\b#{gem_name}\b.*/ + end + + def gem_entry_for(*gem_name_and_version) + gem_name_and_version.map! { |segment| "'#{segment}'" } + "gem #{gem_name_and_version.join(", ")}" + end end end end diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb index 79a06648b5..895b3b2e92 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -144,17 +144,6 @@ task default: :test end end - def javascripts - return if options.skip_javascript? - - if mountable? - template "rails/javascripts.js", - "app/assets/javascripts/#{namespaced_name}/application.js" - elsif full? - empty_directory_with_keep_file "app/assets/javascripts/#{namespaced_name}" - end - end - def bin(force = false) bin_file = engine? ? "bin/rails.tt" : "bin/test.tt" template bin_file, force: force do |content| @@ -236,10 +225,6 @@ task default: :test build(:stylesheets) unless api? end - def create_javascript_files - build(:javascripts) unless api? - end - def create_bin_files build(:bin) end diff --git a/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb b/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb index 7030561a33..e1ca54ec91 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb +++ b/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb @@ -32,6 +32,20 @@ module Rails hook_for :helper, as: :scaffold do |invoked| invoke invoked, [ controller_name ] end + + private + + def permitted_params + attachments, others = attributes_names.partition { |name| attachments?(name) } + params = others.map { |name| ":#{name}" } + params += attachments.map { |name| "#{name}: []" } + params.join(", ") + end + + def attachments?(name) + attribute = attributes.find { |attr| attr.name == name } + attribute&.attachments? + end end end end diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb.tt b/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb.tt index 400afec6dc..bb26370276 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb.tt +++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb.tt @@ -54,7 +54,7 @@ class <%= controller_class_name %>Controller < ApplicationController <%- if attributes_names.empty? -%> params.fetch(:<%= singular_table_name %>, {}) <%- else -%> - params.require(:<%= singular_table_name %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>) + params.require(:<%= singular_table_name %>).permit(<%= permitted_params %>) <%- end -%> end end diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt index 05f1c2b2d3..82b43987b4 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt +++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt @@ -61,7 +61,7 @@ class <%= controller_class_name %>Controller < ApplicationController <%- if attributes_names.empty? -%> params.fetch(:<%= singular_table_name %>, {}) <%- else -%> - params.require(:<%= singular_table_name %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>) + params.require(:<%= singular_table_name %>).permit(<%= permitted_params %>) <%- end -%> end end diff --git a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml.tt b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml.tt index 0681780c97..0fd9f305d7 100644 --- a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml.tt +++ b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml.tt @@ -1,4 +1,4 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html <% unless attributes.empty? -%> <% %w(one two).each do |name| %> <%= name %>: @@ -7,7 +7,7 @@ password_digest: <%%= BCrypt::Password.create('secret') %> <%- elsif attribute.reference? -%> <%= yaml_key_value(attribute.column_name.sub(/_id$/, ''), attribute.default || name) %> - <%- else -%> + <%- elsif !attribute.virtual? -%> <%= yaml_key_value(attribute.column_name, attribute.default) %> <%- end -%> <%- if attribute.polymorphic? -%> diff --git a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb index 6df50c3217..26002a0704 100644 --- a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb @@ -49,16 +49,21 @@ module TestUnit # :nodoc: attributes_names.map do |name| if %w(password password_confirmation).include?(name) && attributes.any?(&:password_digest?) ["#{name}", "'secret'"] - else + elsif !virtual?(name) ["#{name}", "@#{singular_table_name}.#{name}"] end - end.sort.to_h + end.compact.sort.to_h end def boolean?(name) attribute = attributes.find { |attr| attr.name == name } attribute&.type == :boolean end + + def virtual?(name) + attribute = attributes.find { |attr| attr.name == name } + attribute&.virtual? + end end end end diff --git a/railties/lib/rails/mailers_controller.rb b/railties/lib/rails/mailers_controller.rb index 95dae3ec2d..4a1942790b 100644 --- a/railties/lib/rails/mailers_controller.rb +++ b/railties/lib/rails/mailers_controller.rb @@ -5,8 +5,9 @@ require "rails/application_controller" class Rails::MailersController < Rails::ApplicationController # :nodoc: prepend_view_path ActionDispatch::DebugView::RESCUES_TEMPLATE_PATH + around_action :set_locale, only: :preview + before_action :find_preview, only: :preview before_action :require_local!, unless: :show_previews? - before_action :find_preview, :set_locale, only: :preview helper_method :part_query, :locale_query @@ -38,7 +39,7 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: end else @part = find_preferred_part(request.format, Mime[:html], Mime[:text]) - render action: "email", layout: false, formats: %w[html] + render action: "email", layout: false, formats: [:html] end else raise AbstractController::ActionNotFound, "Email '#{@email_action}' not found in #{@preview.name}" @@ -92,6 +93,8 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: end def set_locale - I18n.locale = params[:locale] || I18n.default_locale + I18n.with_locale(params[:locale] || I18n.default_locale) do + yield + end end end diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb index d7170e6282..9ce22b96a6 100644 --- a/railties/lib/rails/source_annotation_extractor.rb +++ b/railties/lib/rails/source_annotation_extractor.rb @@ -29,6 +29,16 @@ module Rails directories.push(*dirs) end + def self.tags + @@tags ||= %w(OPTIMIZE FIXME TODO) + end + + # Registers additional tags + # Rails::SourceAnnotationExtractor::Annotation.register_tags("TESTME", "DEPRECATEME") + def self.register_tags(*additional_tags) + tags.push(*additional_tags) + end + def self.extensions @@extensions ||= {} end @@ -66,6 +76,8 @@ module Rails # Prints all annotations with tag +tag+ under the root directories +app+, # +config+, +db+, +lib+, and +test+ (recursively). # + # If +tag+ is <tt>nil</tt>, annotations with either default or registered tags are printed. + # # Specific directories can be explicitly set using the <tt>:dirs</tt> key in +options+. # # Rails::SourceAnnotationExtractor.enumerate 'TODO|FIXME', dirs: %w(app lib), tag: true @@ -75,7 +87,8 @@ module Rails # See <tt>#find_in</tt> for a list of file extensions that will be taken into account. # # This class method is the single entry point for the `rails notes` command. - def self.enumerate(tag, options = {}) + def self.enumerate(tag = nil, options = {}) + tag ||= Annotation.tags.join("|") extractor = new(tag) dirs = options.delete(:dirs) || Annotation.directories extractor.display(extractor.find(dirs), options) diff --git a/railties/lib/rails/tasks.rb b/railties/lib/rails/tasks.rb index 2f644a20c9..0e592e15c3 100644 --- a/railties/lib/rails/tasks.rb +++ b/railties/lib/rails/tasks.rb @@ -15,6 +15,7 @@ require "rake" routes tmp yarn + zeitwerk ).tap { |arr| arr << "statistics" if Rake.application.current_scope.empty? }.each do |task| diff --git a/railties/lib/rails/tasks/zeitwerk.rake b/railties/lib/rails/tasks/zeitwerk.rake new file mode 100644 index 0000000000..b7f5cd154b --- /dev/null +++ b/railties/lib/rails/tasks/zeitwerk.rake @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +ensure_classic_mode = ->() do + if Rails.autoloaders.zeitwerk_enabled? + abort <<~EOS + Please, enable temporarily :classic mode: + + # config/application.rb + config.autoloader = :classic + + and try again. When all is good, you can delete that line. + EOS + end +end + +eager_load = ->() do + Rails.configuration.eager_load_namespaces.each(&:eager_load!) +end + +mismatches = [] +check_directory = ->(directory, parent) do + # test/mailers/previews might not exist. + return unless File.exist?(directory) + + Dir.foreach(directory) do |entry| + next if entry.start_with?(".") + next if parent == Object && entry == "concerns" + + abspath = File.join(directory, entry) + + if File.directory?(abspath) || abspath.end_with?(".rb") + print "." + cname = File.basename(abspath, ".rb").camelize.to_sym + if parent.const_defined?(cname, false) + if File.directory?(abspath) + check_directory[abspath, parent.const_get(cname)] + end + else + mismatches << [abspath, parent, cname] + end + end + end +end + +report = ->() do + puts + if mismatches.empty? + puts "All is good!" + puts "Please, remember to delete `config.autoloader = :classic` from config/application.rb." + else + mismatches.each do |abspath, parent, cname| + relpath = abspath.sub(%r{\A#{Regexp.escape(Rails.root.to_path)}/}, "") + cpath = parent == Object ? cname : "#{parent.name}::#{cname}" + puts "expected #{relpath} to define #{cpath}" + end + puts + puts <<~EOS + Please revise the reported mismatches. You can normally fix them by adding + acronyms to config/initializers/inflections.rb or renaming the constants. + EOS + end +end + +namespace :zeitwerk do + desc "Checks project structure for Zeitwerk compatibility" + task check: :environment do + ensure_classic_mode[] + eager_load[] + + $stdout.sync = true + ActiveSupport::Dependencies.autoload_paths.each do |autoload_path| + check_directory[autoload_path, Object] + end + puts + + report[] + end +end diff --git a/railties/railties.gemspec b/railties/railties.gemspec index 1bfb0dfe48..519b08746a 100644 --- a/railties/railties.gemspec +++ b/railties/railties.gemspec @@ -15,7 +15,7 @@ Gem::Specification.new do |s| s.author = "David Heinemeier Hansson" s.email = "david@loudthinking.com" - s.homepage = "http://rubyonrails.org" + s.homepage = "https://rubyonrails.org" s.files = Dir["CHANGELOG.md", "README.rdoc", "MIT-LICENSE", "RDOC_MAIN.rdoc", "exe/**/*", "lib/**/{*,.[a-z]*}"] s.require_path = "lib" diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb index 9c866263f0..9600194ed6 100644 --- a/railties/test/abstract_unit.rb +++ b/railties/test/abstract_unit.rb @@ -32,3 +32,5 @@ class ActiveSupport::TestCase skip message if defined?(JRUBY_VERSION) end end + +require_relative "../../tools/test_common" diff --git a/railties/test/application/asset_debugging_test.rb b/railties/test/application/asset_debugging_test.rb index 3e0f31860b..7623e8e352 100644 --- a/railties/test/application/asset_debugging_test.rb +++ b/railties/test/application/asset_debugging_test.rb @@ -95,7 +95,7 @@ module ApplicationTests end end - test "public url methods are not over-written by the asset pipeline" do + test "public URL methods are not over-written by the asset pipeline" do contents = "doesnotexist" cases = { asset_url: %r{http://example.org/#{contents}}, diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb index 46ee0d670e..a80581211b 100644 --- a/railties/test/application/assets_test.rb +++ b/railties/test/application/assets_test.rb @@ -450,7 +450,7 @@ module ApplicationTests assert_equal 0, files.length, "Expected application.js asset to be removed, but still exists" end - test "asset urls should use the request's protocol by default" do + test "asset URLs should use the request's protocol by default" do app_with_assets_in_view add_to_config "config.asset_host = 'example.com'" add_to_env_config "development", "config.assets.digest = false" @@ -466,7 +466,7 @@ module ApplicationTests assert_match('src="https://example.com/assets/application.self.js', last_response.body) end - test "asset urls should be protocol-relative if no request is in scope" do + test "asset URLs should be protocol-relative if no request is in scope" do app_file "app/assets/images/rails.png", "notreallyapng" app_file "app/assets/javascripts/image_loader.js.erb", "var src='<%= image_path('rails.png') %>';" add_to_config "config.assets.precompile = %w{rails.png image_loader.js}" diff --git a/railties/test/application/bin_setup_test.rb b/railties/test/application/bin_setup_test.rb index a952d2466b..aa0da0931d 100644 --- a/railties/test/application/bin_setup_test.rb +++ b/railties/test/application/bin_setup_test.rb @@ -6,21 +6,12 @@ module ApplicationTests class BinSetupTest < ActiveSupport::TestCase include ActiveSupport::Testing::Isolation - def setup - build_app - end - - def teardown - teardown_app - end + setup :build_app + teardown :teardown_app def test_bin_setup Dir.chdir(app_path) do - app_file "db/schema.rb", <<-RUBY - ActiveRecord::Schema.define(version: 20140423102712) do - create_table(:articles) {} - end - RUBY + rails "generate", "model", "article" list_tables = lambda { rails("runner", "p ActiveRecord::Base.connection.tables").strip } File.write("log/test.log", "zomg!") @@ -28,15 +19,20 @@ module ApplicationTests assert_equal "[]", list_tables.call assert_equal 5, File.size("log/test.log") assert_not File.exist?("tmp/restart.txt") + `bin/setup 2>&1` assert_equal 0, File.size("log/test.log") - assert_equal '["articles", "schema_migrations", "ar_internal_metadata"]', list_tables.call + assert_equal '["schema_migrations", "ar_internal_metadata", "articles"]', list_tables.call assert File.exist?("tmp/restart.txt") end end def test_bin_setup_output Dir.chdir(app_path) do + # SQLite3 seems to auto-create the database on first checkout. + rails "db:system:change", "--to=postgresql" + rails "db:drop" + app_file "db/schema.rb", "" output = `bin/setup 2>&1` @@ -53,8 +49,8 @@ module ApplicationTests The Gemfile's dependencies are satisfied == Preparing database == - Created database 'db/development.sqlite3' - Created database 'db/test.sqlite3' + Created database 'app_development' + Created database 'app_test' == Removing old logs and tempfiles == diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 37fba72ee3..7c613585e0 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -4,6 +4,7 @@ require "isolation/abstract_unit" require "rack/test" require "env_helpers" require "set" +require "active_support/core_ext/string/starts_ends_with" class ::MyMailInterceptor def self.delivering_email(email); email; end @@ -596,6 +597,30 @@ module ApplicationTests assert_equal "some_value", verifier.verify(message) end + test "application will generate secret_key_base in tmp file if blank in development" do + app_file "config/initializers/secret_token.rb", <<-RUBY + Rails.application.credentials.secret_key_base = nil + RUBY + + # For test that works even if tmp dir does not exist. + Dir.chdir(app_path) { FileUtils.remove_dir("tmp") } + + app "development" + + assert_not_nil app.secrets.secret_key_base + assert File.exist?(app_path("tmp/development_secret.txt")) + end + + test "application will not generate secret_key_base in tmp file if blank in production" do + app_file "config/initializers/secret_token.rb", <<-RUBY + Rails.application.credentials.secret_key_base = nil + RUBY + + assert_raises ArgumentError do + app "production" + end + end + test "raises when secret_key_base is blank" do app_file "config/initializers/secret_token.rb", <<-RUBY Rails.application.credentials.secret_key_base = nil @@ -619,7 +644,6 @@ module ApplicationTests test "application verifier can build different verifiers" do make_basic_app do |application| - application.credentials.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33" application.config.session_store :disabled end @@ -1157,6 +1181,38 @@ 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 + assert_equal ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector, Rails.autoloaders.main.inflector + assert_equal ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector, Rails.autoloaders.once.inflector + + 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_equal ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector, Rails.autoloaders.main.inflector + assert_equal ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector, Rails.autoloaders.once.inflector + + 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" @@ -1649,6 +1705,88 @@ module ApplicationTests end end + test "autoload paths are added to $LOAD_PATH by default" do + app "development" + + # Action Mailer modifies AS::Dependencies.autoload_paths in-place. + autoload_paths = ActiveSupport::Dependencies.autoload_paths + autoload_paths_from_app_and_engines = autoload_paths.reject do |path| + path.ends_with?("mailers/previews") + end + assert_equal true, Rails.configuration.add_autoload_paths_to_load_path + assert_empty autoload_paths_from_app_and_engines - $LOAD_PATH + + # Precondition, ensure we are testing something next. + assert_not_empty Rails.configuration.paths.load_paths + assert_empty Rails.configuration.paths.load_paths - $LOAD_PATH + end + + test "autoload paths are not added to $LOAD_PATH if opted-out" do + add_to_config "config.add_autoload_paths_to_load_path = false" + app "development" + + assert_empty ActiveSupport::Dependencies.autoload_paths & $LOAD_PATH + + # Precondition, ensure we are testing something next. + assert_not_empty Rails.configuration.paths.load_paths + assert_empty Rails.configuration.paths.load_paths - $LOAD_PATH + end + + test "autoloading during initialization gets deprecation message and clearing if config.cache_classes is false" do + app_file "lib/c.rb", <<~EOS + class C + extend ActiveSupport::DescendantsTracker + end + + class X < C + end + EOS + + app_file "app/models/d.rb", <<~EOS + require "c" + + class D < C + end + EOS + + app_file "config/initializers/autoload.rb", "D.class" + + app "development" + + # TODO: Test deprecation message, assert_depcrecated { app "development" } + # does not collect it. + + assert_equal [X], C.descendants + assert_empty ActiveSupport::Dependencies.autoloaded_constants + end + + test "autoloading during initialization triggers nothing if config.cache_classes is true" do + app_file "lib/c.rb", <<~EOS + class C + extend ActiveSupport::DescendantsTracker + end + + class X < C + end + EOS + + app_file "app/models/d.rb", <<~EOS + require "c" + + class D < C + end + EOS + + app_file "config/initializers/autoload.rb", "D.class" + + app "production" + + # TODO: Test no deprecation message is issued. + + assert_equal [X, D], C.descendants + end + + test "raises with proper error message if no database configuration found" do FileUtils.rm("#{app_path}/config/database.yml") err = assert_raises RuntimeError do @@ -1714,7 +1852,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 +1865,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 +1885,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 +1942,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 @@ -2298,6 +2559,22 @@ module ApplicationTests assert_includes Rails.application.config.hosts, ".localhost" end + test "disable_sandbox is false by default" do + app "development" + + assert_equal false, Rails.configuration.disable_sandbox + end + + test "disable_sandbox can be overridden" do + add_to_config <<-RUBY + config.disable_sandbox = true + RUBY + + app "development" + + assert Rails.configuration.disable_sandbox + end + private def force_lazy_load_hooks yield # Tasty clarifying sugar, homie! We only need to reference a constant to load it. diff --git a/railties/test/application/console_test.rb b/railties/test/application/console_test.rb index b6270525f0..db16f4cc56 100644 --- a/railties/test/application/console_test.rb +++ b/railties/test/application/console_test.rb @@ -123,13 +123,17 @@ class FullStackConsoleTest < ActiveSupport::TestCase assert_output "> ", @primary end - def spawn_console(options) - Process.spawn( + def spawn_console(options, wait_for_prompt: true) + pid = Process.spawn( "#{app_path}/bin/rails console #{options}", in: @replica, out: @replica, err: @replica ) - assert_output "> ", @primary, 30 + if wait_for_prompt + assert_output "> ", @primary, 30 + end + + pid end def test_sandbox @@ -148,6 +152,17 @@ class FullStackConsoleTest < ActiveSupport::TestCase @primary.puts "quit" end + def test_sandbox_when_sandbox_is_disabled + add_to_config <<-RUBY + config.disable_sandbox = true + RUBY + + output = `#{app_path}/bin/rails console --sandbox` + + assert_includes output, "sandbox mode is disabled" + assert_equal 1, $?.exitstatus + end + def test_environment_option_and_irb_option spawn_console("-e test -- --verbose") diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb index 3cd4b8fe33..a35247fc43 100644 --- a/railties/test/application/initializers/frameworks_test.rb +++ b/railties/test/application/initializers/frameworks_test.rb @@ -39,7 +39,7 @@ module ApplicationTests assert_equal expanded_path, ActionMailer::Base.view_paths[0].to_s end - test "allows me to configure default url options for ActionMailer" do + test "allows me to configure default URL options for ActionMailer" do app_file "config/environments/development.rb", <<-RUBY Rails.application.configure do config.action_mailer.default_url_options = { :host => "test.rails" } @@ -61,7 +61,7 @@ module ApplicationTests assert_equal "https", ActionMailer::Base.default_url_options[:protocol] end - test "includes url helpers as action methods" do + test "includes URL helpers as action methods" do app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get "/foo", :to => lambda { |env| [200, {}, []] }, :as => :foo 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..fa9ed868c4 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") @@ -513,6 +515,13 @@ module ApplicationTests assert_match '<option selected value="locale=ja">ja', last_response.body end + test "preview does not leak I18n global setting changes" do + I18n.with_locale(:en) do + get "/rails/mailers/notifier/foo.txt?locale=ja" + assert_equal :en, I18n.locale + end + end + test "mailer previews create correct links when loaded on a subdirectory" do mailer "notifier", <<-RUBY class Notifier < ActionMailer::Base @@ -818,6 +827,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/middleware/exceptions_test.rb b/railties/test/application/middleware/exceptions_test.rb index 2d659ade8d..5fae521937 100644 --- a/railties/test/application/middleware/exceptions_test.rb +++ b/railties/test/application/middleware/exceptions_test.rb @@ -60,7 +60,7 @@ module ApplicationTests assert_equal "YOU FAILED", last_response.body end - test "url generation error when action_dispatch.show_exceptions is set raises an exception" do + test "URL generation error when action_dispatch.show_exceptions is set raises an exception" do controller :foo, <<-RUBY class FooController < ActionController::Base def index @@ -136,5 +136,21 @@ module ApplicationTests assert_match(/boooom/, last_response.body) assert_match(/測試テスト시험/, last_response.body) end + + test "displays diagnostics message when malformed query parameters are provided" do + controller :foo, <<-RUBY + class FooController < ActionController::Base + def index + end + end + RUBY + + app.config.action_dispatch.show_exceptions = true + app.config.consider_all_requests_local = true + + get "/foo?x[y]=1&x[y][][w]=2" + assert_equal 400, last_response.status + assert_match "Invalid query parameters", last_response.body + end end end diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index 4242daf39a..54b2e95d75 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -38,6 +38,7 @@ module ApplicationTests "Rails::Rack::Logger", "ActionDispatch::ShowExceptions", "ActionDispatch::DebugExceptions", + "ActionDispatch::ActionableExceptions", "ActionDispatch::Reloader", "ActionDispatch::Callbacks", "ActiveRecord::Migration::CheckPending", @@ -70,6 +71,7 @@ module ApplicationTests "Rails::Rack::Logger", "ActionDispatch::ShowExceptions", "ActionDispatch::DebugExceptions", + "ActionDispatch::ActionableExceptions", "ActionDispatch::Reloader", "ActionDispatch::Callbacks", "Rack::Head", diff --git a/railties/test/application/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..258066a7e6 100644 --- a/railties/test/application/rake/dbs_test.rb +++ b/railties/test/application/rake/dbs_test.rb @@ -1,11 +1,12 @@ # frozen_string_literal: true require "isolation/abstract_unit" +require "env_helpers" module ApplicationTests module RakeTests class RakeDbsTest < ActiveSupport::TestCase - include ActiveSupport::Testing::Isolation + include ActiveSupport::Testing::Isolation, EnvHelpers def setup build_app @@ -39,12 +40,12 @@ module ApplicationTests end end - test "db:create and db:drop without database url" do + test "db:create and db:drop without database URL" do require "#{app_path}/config/environment" db_create_and_drop ActiveRecord::Base.configurations[Rails.env]["database"] end - test "db:create and db:drop with database url" do + test "db:create and db:drop with database URL" do require "#{app_path}/config/environment" set_database_url db_create_and_drop database_url_db_name @@ -52,27 +53,73 @@ module ApplicationTests test "db:create and db:drop respect environment setting" do app_file "config/database.yml", <<-YAML + <% 1 %> development: - database: db/development.sqlite3 + database: <%= Rails.application.config.database %> adapter: sqlite3 YAML app_file "config/environments/development.rb", <<-RUBY Rails.application.configure do - config.read_encrypted_secrets = true + config.database = "db/development.sqlite3" end RUBY - app_file "lib/tasks/check_env.rake", <<-RUBY - Rake::Task["db:create"].enhance do - File.write("tmp/config_value", Rails.application.config.read_encrypted_secrets) + db_create_and_drop("db/development.sqlite3", environment_loaded: false) + end + + test "db:create and db:drop don't raise errors when loading YAML with multiline ERB" do + app_file "config/database.yml", <<-YAML + development: + database: <%= + Rails.application.config.database + %> + adapter: sqlite3 + YAML + + app_file "config/environments/development.rb", <<-RUBY + Rails.application.configure do + config.database = "db/development.sqlite3" end RUBY - db_create_and_drop("db/development.sqlite3", environment_loaded: false) do - assert File.exist?("tmp/config_value") - assert_equal "true", File.read("tmp/config_value") - end + db_create_and_drop("db/development.sqlite3", environment_loaded: false) + end + + test "db:create and db:drop don't raise errors when loading YAML containing conditional statements in ERB" do + app_file "config/database.yml", <<-YAML + development: + <% if Rails.application.config.database %> + database: <%= Rails.application.config.database %> + <% else %> + database: db/default.sqlite3 + <% end %> + adapter: sqlite3 + YAML + + app_file "config/environments/development.rb", <<-RUBY + Rails.application.configure do + config.database = "db/development.sqlite3" + end + RUBY + + db_create_and_drop("db/development.sqlite3", environment_loaded: false) + end + + test "db:create and db:drop don't raise errors when loading YAML containing multiple ERB statements on the same line" do + app_file "config/database.yml", <<-YAML + development: + database: <% if Rails.application.config.database %><%= Rails.application.config.database %><% else %>db/default.sqlite3<% end %> + adapter: sqlite3 + YAML + + app_file "config/environments/development.rb", <<-RUBY + Rails.application.configure do + config.database = "db/development.sqlite3" + end + RUBY + + db_create_and_drop("db/development.sqlite3", environment_loaded: false) end def with_database_existing @@ -139,6 +186,59 @@ module ApplicationTests end end + test "db:truncate_all truncates all non-internal tables" do + Dir.chdir(app_path) do + rails "generate", "model", "book", "title:string" + rails "db:migrate" + require "#{app_path}/config/environment" + Book.create!(title: "Remote") + assert_equal 1, Book.count + schema_migrations = ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.schema_migrations_table_name}\"") + internal_metadata = ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.internal_metadata_table_name}\"") + + rails "db:truncate_all" + + assert_equal( + schema_migrations, + ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.schema_migrations_table_name}\"") + ) + assert_equal( + internal_metadata, + ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.internal_metadata_table_name}\"") + ) + assert_equal 0, Book.count + end + end + + test "db:truncate_all does not truncate any tables when environment is protected" do + with_rails_env "production" do + Dir.chdir(app_path) do + rails "generate", "model", "book", "title:string" + rails "db:migrate" + require "#{app_path}/config/environment" + Book.create!(title: "Remote") + assert_equal 1, Book.count + schema_migrations = ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.schema_migrations_table_name}\"") + internal_metadata = ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.internal_metadata_table_name}\"") + books = ActiveRecord::Base.connection.execute("SELECT * from \"books\"") + + output = rails("db:truncate_all", allow_failure: true) + assert_match(/ActiveRecord::ProtectedEnvironmentError/, output) + + assert_equal( + schema_migrations, + ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.schema_migrations_table_name}\"") + ) + assert_equal( + internal_metadata, + ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.internal_metadata_table_name}\"") + ) + assert_equal 1, Book.count + assert_equal(books, ActiveRecord::Base.connection.execute("SELECT * from \"books\"")) + end + end + end + def db_migrate_and_status(expected_database) rails "generate", "model", "book", "title:string" rails "db:migrate" @@ -179,9 +279,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 +302,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 @@ -385,6 +487,88 @@ module ApplicationTests assert_equal "test", test_environment.call end + + test "db:seed:replant truncates all non-internal tables and loads the seeds" do + Dir.chdir(app_path) do + rails "generate", "model", "book", "title:string" + rails "db:migrate" + require "#{app_path}/config/environment" + Book.create!(title: "Remote") + assert_equal 1, Book.count + schema_migrations = ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.schema_migrations_table_name}\"") + internal_metadata = ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.internal_metadata_table_name}\"") + + app_file "db/seeds.rb", <<-RUBY + Book.create!(title: "Rework") + Book.create!(title: "Ruby Under a Microscope") + RUBY + + rails "db:seed:replant" + + assert_equal( + schema_migrations, + ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.schema_migrations_table_name}\"") + ) + assert_equal( + internal_metadata, + ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.internal_metadata_table_name}\"") + ) + assert_equal 2, Book.count + assert_not_predicate Book.where(title: "Remote"), :exists? + assert_predicate Book.where(title: "Rework"), :exists? + assert_predicate Book.where(title: "Ruby Under a Microscope"), :exists? + end + end + + test "db:seed:replant does not truncate any tables and does not load the seeds when environment is protected" do + with_rails_env "production" do + Dir.chdir(app_path) do + rails "generate", "model", "book", "title:string" + rails "db:migrate" + require "#{app_path}/config/environment" + Book.create!(title: "Remote") + assert_equal 1, Book.count + schema_migrations = ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.schema_migrations_table_name}\"") + internal_metadata = ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.internal_metadata_table_name}\"") + books = ActiveRecord::Base.connection.execute("SELECT * from \"books\"") + + app_file "db/seeds.rb", <<-RUBY + Book.create!(title: "Rework") + RUBY + + output = rails("db:seed:replant", allow_failure: true) + assert_match(/ActiveRecord::ProtectedEnvironmentError/, output) + + assert_equal( + schema_migrations, + ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.schema_migrations_table_name}\"") + ) + assert_equal( + internal_metadata, + ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.internal_metadata_table_name}\"") + ) + assert_equal 1, Book.count + assert_equal(books, ActiveRecord::Base.connection.execute("SELECT * from \"books\"")) + assert_not_predicate Book.where(title: "Rework"), :exists? + end + end + end + + test "db:prepare setup the database" do + Dir.chdir(app_path) do + rails "generate", "model", "book", "title:string" + output = rails("db:prepare") + assert_match(/CreateBooks: migrated/, output) + + output = rails("db:drop") + assert_match(/Dropped database/, output) + + rails "generate", "model", "recipe", "title:string" + output = rails("db:prepare") + assert_match(/CreateBooks: migrated/, output) + assert_match(/CreateRecipes: migrated/, output) + end + end end end end diff --git a/railties/test/application/rake/multi_dbs_test.rb b/railties/test/application/rake/multi_dbs_test.rb index ef99365e75..31ea2246a9 100644 --- a/railties/test/application/rake/multi_dbs_test.rb +++ b/railties/test/application/rake/multi_dbs_test.rb @@ -24,7 +24,6 @@ module ApplicationTests assert_no_match(/already exists/, output) assert File.exist?(expected_database) - output = rails("db:drop") assert_match(/Dropped database/, output) assert_match_namespace(namespace, output) @@ -137,6 +136,51 @@ module ApplicationTests end end + def db_up_and_down(version, namespace = nil) + Dir.chdir(app_path) do + generate_models_for_animals + rails("db:migrate") + + if namespace + down_output = rails("db:migrate:down:#{namespace}", "VERSION=#{version}") + up_output = rails("db:migrate:up:#{namespace}", "VERSION=#{version}") + else + assert_raises RuntimeError, /You're using a multiple database application/ do + down_output = rails("db:migrate:down", "VERSION=#{version}") + end + + assert_raises RuntimeError, /You're using a multiple database application/ do + up_output = rails("db:migrate:up", "VERSION=#{version}") + end + end + + case namespace + when "primary" + assert_match(/OneMigration: reverting/, down_output) + assert_match(/OneMigration: migrated/, up_output) + when nil + else + assert_match(/TwoMigration: reverting/, down_output) + assert_match(/TwoMigration: migrated/, up_output) + end + end + end + + def db_prepare + Dir.chdir(app_path) do + generate_models_for_animals + output = rails("db:prepare") + + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + if db_config.spec_name == "primary" + assert_match(/CreateBooks: migrated/, output) + else + assert_match(/CreateDogs: migrated/, output) + end + end + end + end + def write_models_for_animals # make a directory for the animals migration FileUtils.mkdir_p("#{app_path}/db/animals_migrate") @@ -170,6 +214,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 @@ -203,6 +248,34 @@ module ApplicationTests end end + test "db:migrate:down and db:migrate:up without a namespace raises in a multi-db application" do + require "#{app_path}/config/environment" + + app_file "db/migrate/01_one_migration.rb", <<-MIGRATION + class OneMigration < ActiveRecord::Migration::Current + end + MIGRATION + + db_up_and_down "01" + end + + test "db:migrate:down:namespace and db:migrate:up:namespace works" do + require "#{app_path}/config/environment" + + app_file "db/migrate/01_one_migration.rb", <<-MIGRATION + class OneMigration < ActiveRecord::Migration::Current + end + MIGRATION + + app_file "db/animals_migrate/02_two_migration.rb", <<-MIGRATION + class TwoMigration < ActiveRecord::Migration::Current + end + MIGRATION + + db_up_and_down "01", "primary" + db_up_and_down "02", "animals" + end + test "db:migrate:status works on all databases" do require "#{app_path}/config/environment" db_migrate_and_migrate_status @@ -225,6 +298,11 @@ module ApplicationTests require "#{app_path}/config/environment" db_migrate_and_schema_cache_dump_and_schema_cache_clear end + + test "db:prepare works on all databases" do + require "#{app_path}/config/environment" + db_prepare + end end end end diff --git a/railties/test/application/rake/routes_test.rb b/railties/test/application/rake/routes_test.rb index 9879d1f047..dffdae7bde 100644 --- a/railties/test/application/rake/routes_test.rb +++ b/railties/test/application/rake/routes_test.rb @@ -20,7 +20,6 @@ module ApplicationTests assert_equal <<~MESSAGE, run_rake_routes Prefix Verb URI Pattern Controller#Action cart GET /cart(.:format) cart#show - rails_amazon_inbound_emails POST /rails/action_mailbox/amazon/inbound_emails(.:format) action_mailbox/ingresses/amazon/inbound_emails#create rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create rails_relay_inbound_emails POST /rails/action_mailbox/relay/inbound_emails(.:format) action_mailbox/ingresses/relay/inbound_emails#create 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/runner_test.rb b/railties/test/application/runner_test.rb index 8f5f48c281..dfb9540093 100644 --- a/railties/test/application/runner_test.rb +++ b/railties/test/application/runner_test.rb @@ -105,6 +105,14 @@ module ApplicationTests assert_match "development", rails("runner", "puts Rails.env") end + def test_environment_option + assert_match "production", rails("runner", "-e", "production", "puts Rails.env") + end + + def test_environment_option_is_properly_expanded + assert_match "production", rails("runner", "-e", "prod", "puts Rails.env") + end + def test_runner_detects_syntax_errors output = rails("runner", "puts 'hello world", allow_failure: true) assert_not_predicate $?, :success? diff --git a/railties/test/application/server_test.rb b/railties/test/application/server_test.rb index 9df36b3444..5fe1b4e6e7 100644 --- a/railties/test/application/server_test.rb +++ b/railties/test/application/server_test.rb @@ -30,13 +30,13 @@ module ApplicationTests pid = nil Bundler.with_original_env do - pid = Process.spawn("bin/rails server -P tmp/dummy.pid", chdir: app_path, in: replica, out: replica, err: replica) + pid = Process.spawn("bin/rails server -b localhost -P tmp/dummy.pid", chdir: app_path, in: replica, out: replica, err: replica) assert_output("Listening", primary) rails("restart") assert_output("Restarting", primary) - assert_output("Inherited", primary) + assert_output("tcp://localhost:3000", primary) ensure kill(pid) if pid end diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb index 0bdd2b314d..1ab45abcd0 100644 --- a/railties/test/application/test_runner_test.rb +++ b/railties/test/application/test_runner_test.rb @@ -794,6 +794,32 @@ module ApplicationTests assert_match "Capybara.reset_sessions! called", output end + def test_failed_system_test_screenshot_should_be_taken_before_other_teardown + app_file "test/system/failed_system_test_screenshot_should_be_taken_before_other_teardown_test.rb", <<~RUBY + require "application_system_test_case" + require "selenium/webdriver" + + class FailedSystemTestScreenshotShouldBeTakenBeforeOtherTeardownTest < ApplicationSystemTestCase + ActionDispatch::SystemTestCase.class_eval do + def take_failed_screenshot + puts "take_failed_screenshot called" + super + end + end + + def teardown + puts "test teardown called" + super + end + + test "dummy" do + end + end + RUBY + output = run_test_command("test/system/failed_system_test_screenshot_should_be_taken_before_other_teardown_test.rb") + assert_match(/take_failed_screenshot called\n.*test teardown called/, output) + end + def test_system_tests_are_not_run_with_the_default_test_command app_file "test/system/dummy_test.rb", <<-RUBY require "application_system_test_case" diff --git a/railties/test/application/test_test.rb b/railties/test/application/test_test.rb index fb43bebfbe..83e63718df 100644 --- a/railties/test/application/test_test.rb +++ b/railties/test/application/test_test.rb @@ -234,7 +234,7 @@ module ApplicationTests # TODO: would be nice if we could detect the schema change automatically. # For now, the user has to synchronize the schema manually. - # This test-case serves as a reminder for this use-case. + # This test case serves as a reminder for this use case. test "manually synchronize test schema after rollback" do output = rails("generate", "model", "user", "name:string") version = output.match(/(\d+)_create_users\.rb/)[1] diff --git a/railties/test/application/url_generation_test.rb b/railties/test/application/url_generation_test.rb index 6bcc0b0acb..dc737089f6 100644 --- a/railties/test/application/url_generation_test.rb +++ b/railties/test/application/url_generation_test.rb @@ -20,6 +20,7 @@ module ApplicationTests config.active_support.deprecation = :log config.eager_load = false config.hosts << proc { true } + config.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33" end Rails.application.initialize! diff --git a/railties/test/application/zeitwerk_integration_test.rb b/railties/test/application/zeitwerk_integration_test.rb new file mode 100644 index 0000000000..9146222f73 --- /dev/null +++ b/railties/test/application/zeitwerk_integration_test.rb @@ -0,0 +1,350 @@ +# 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 "autoloaders inflect with Active Support" do + app_file "config/initializers/inflections.rb", <<-RUBY + ActiveSupport::Inflector.inflections(:en) do |inflect| + inflect.acronym 'RESTful' + end + RUBY + + app_file "app/controllers/restful_controller.rb", <<-RUBY + class RESTfulController < ApplicationController + end + RUBY + + boot + + basename = "restful_controller" + abspath = "#{Rails.root}/app/controllers/#{basename}.rb" + camelized = "RESTfulController" + + Rails.autoloaders.each do |autoloader| + assert_equal camelized, autoloader.inflector.camelize(basename, abspath) + end + + assert RESTfulController + 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 "unloadable constants (main)" 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") + + assert_equal ["Post"], deps.autoloaded_constants + end + + test "unloadable constants (once)" do + add_to_config 'config.autoload_once_paths << "#{Rails.root}/extras"' + app_file "extras/foo.rb", "class Foo; end" + app_file "extras/bar.rb", "class Bar; end" + boot + + assert Foo + + assert_not deps.autoloaded?("Foo") + assert_not deps.autoloaded?(Foo) + assert_not deps.autoloaded?("Bar") + + assert_empty deps.autoloaded_constants + end + + test "unloadable constants (reloading disabled)" do + app_file "app/models/user.rb", "class User; end" + app_file "app/models/post.rb", "class Post; end" + boot("production") + + assert Post + + assert_not deps.autoloaded?("Post") + assert_not deps.autoloaded?(Post) + assert_not deps.autoloaded?("User") + + assert_empty deps.autoloaded_constants + 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 "reloading is enabled if config.cache_classes is false" do + boot + + assert Rails.autoloaders.main.reloading_enabled? + assert_not Rails.autoloaders.once.reloading_enabled? + end + + test "reloading is disabled if config.cache_classes is true" do + boot("production") + + assert_not Rails.autoloaders.main.reloading_enabled? + assert_not Rails.autoloaders.once.reloading_enabled? + end + + test "reloading raises if config.cache_classes is true" do + boot("production") + + e = assert_raises(StandardError) do + deps.clear + end + assert_equal "reloading is disabled because config.cache_classes is true", e.message + end + + test "eager loading loads code in engines" do + $test_blog_engine_eager_loaded = false + + engine("blog") do |bukkit| + bukkit.write("lib/blog.rb", "class BlogEngine < Rails::Engine; end") + bukkit.write("app/models/post.rb", "Post = $test_blog_engine_eager_loaded = true") + end + + boot("production") + + assert $test_blog_engine_eager_loaded + 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 directories not present in eager load paths are not eager loaded" do + $zeitwerk_integration_test_user = false + app_file "app/models/user.rb", "class User; end; $zeitwerk_integration_test_user = true" + + $zeitwerk_integration_test_lib = false + app_dir "lib" + app_file "lib/webhook_hacks.rb", "WebhookHacks = 1; $zeitwerk_integration_test_lib = true" + + $zeitwerk_integration_test_extras = false + app_dir "extras" + app_file "extras/websocket_hacks.rb", "WebsocketHacks = 1; $zeitwerk_integration_test_extras = true" + + add_to_config "config.autoload_paths << '#{app_path}/lib'" + add_to_config "config.autoload_once_paths << '#{app_path}/extras'" + + boot("production") + + assert $zeitwerk_integration_test_user + assert_not $zeitwerk_integration_test_lib + assert_not $zeitwerk_integration_test_extras + + assert WebhookHacks + assert WebsocketHacks + + assert $zeitwerk_integration_test_lib + assert $zeitwerk_integration_test_extras + end + + test "autoload_paths are set as root dirs of main, and in the same order" do + boot + + existing_autoload_paths = deps.autoload_paths.select { |dir| File.directory?(dir) } + assert_equal existing_autoload_paths, Rails.autoloaders.main.dirs + end + + test "autoload_once_paths go to the once autoloader, and in the same order" do + extras = %w(e1 e2 e3) + extras.each do |extra| + app_dir extra + add_to_config %(config.autoload_once_paths << "\#{Rails.root}/#{extra}") + end + + boot + + extras = extras.map { |extra| "#{app_path}/#{extra}" } + extras.each do |extra| + assert_not_includes Rails.autoloaders.main.dirs, extra + end + assert_equal extras, Rails.autoloaders.once.dirs + 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 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_same logger, autoloader.logger + end + end + + test "verbose = true sets the Rails logger as fallback" do + boot + + ActiveSupport::Dependencies.verbose = true + + Rails.autoloaders.each do |autoloader| + assert_same Rails.logger, 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 + + test "autoloaders.logger=" do + boot + + logger = ->(_msg) { } + Rails.autoloaders.logger = logger + + Rails.autoloaders.each do |autoloader| + assert_same logger, autoloader.logger + end + + Rails.autoloaders.logger = Rails.logger + + Rails.autoloaders.each do |autoloader| + assert_same Rails.logger, autoloader.logger + end + + Rails.autoloaders.logger = nil + + Rails.autoloaders.each do |autoloader| + assert_nil autoloader.logger + end + end +end diff --git a/railties/test/backtrace_cleaner_test.rb b/railties/test/backtrace_cleaner_test.rb index 90e084ddca..6de23acebe 100644 --- a/railties/test/backtrace_cleaner_test.rb +++ b/railties/test/backtrace_cleaner_test.rb @@ -17,8 +17,18 @@ class BacktraceCleanerTest < ActiveSupport::TestCase assert_equal 1, result.length end + test "can filter for noise" do + backtrace = [ "(irb):1", + "/Path/to/rails/railties/lib/rails/commands/console.rb:77:in `start'", + "bin/rails:4:in `<main>'" ] + result = @cleaner.clean(backtrace, :noise) + assert_equal "/Path/to/rails/railties/lib/rails/commands/console.rb:77:in `start'", result[0] + assert_equal "bin/rails:4:in `<main>'", result[1] + assert_equal 2, result.length + end + test "should omit ActionView template methods names" do - method_name = ActionView::Template.new(nil, "app/views/application/index.html.erb", nil, {}).send :method_name + method_name = ActionView::Template.new(nil, "app/views/application/index.html.erb", nil, locals: []).send :method_name backtrace = [ "app/views/application/index.html.erb:4:in `block in #{method_name}'"] result = @cleaner.clean(backtrace, :all) assert_equal "app/views/application/index.html.erb:4", result[0] diff --git a/railties/test/commands/console_test.rb b/railties/test/commands/console_test.rb index 1941c83d6d..f6df2b694a 100644 --- a/railties/test/commands/console_test.rb +++ b/railties/test/commands/console_test.rb @@ -129,7 +129,7 @@ class Rails::ConsoleTest < ActiveSupport::TestCase def build_app(console) mocked_console = Class.new do attr_accessor :sandbox - attr_reader :console + attr_reader :console, :disable_sandbox def initialize(console) @console = console diff --git a/railties/test/commands/credentials_test.rb b/railties/test/commands/credentials_test.rb index 10d0ba1cae..0ee36081c0 100644 --- a/railties/test/commands/credentials_test.rb +++ b/railties/test/commands/credentials_test.rb @@ -63,6 +63,14 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase end end + test "edit command properly expands environment option" do + assert_match(/access_key_id: 123/, run_edit_command(environment: "prod")) + Dir.chdir(app_path) do + assert File.exist?("config/credentials/production.key") + assert File.exist?("config/credentials/production.yml.enc") + end + end + test "edit command does not raise when an initializer tries to access non-existent credentials" do app_file "config/initializers/raise_when_loaded.rb", <<-RUBY Rails.application.credentials.missing_key! @@ -71,11 +79,20 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase assert_match(/access_key_id: 123/, run_edit_command(environment: "qa")) end + test "edit command generates template file when the file does not exist" do + FileUtils.rm("#{app_path}/config/credentials.yml.enc") + run_edit_command + + output = run_show_command + assert_match(/access_key_id: 123/, output) + assert_match(/secret_key_base/, output) + end + test "show credentials" do assert_match(/access_key_id: 123/, run_show_command) end - test "show command raise error when require_master_key is specified and key does not exist" do + test "show command raises error when require_master_key is specified and key does not exist" do remove_file "config/master.key" add_to_config "config.require_master_key = true" @@ -95,6 +112,14 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase assert_match(/access_key_id: 123/, run_show_command(environment: "production")) end + test "show command properly expands environment option" do + run_edit_command(environment: "production") + + output = run_show_command(environment: "prod") + assert_match(/access_key_id: 123/, output) + assert_no_match(/secret_key_base/, output) + end + private def run_edit_command(editor: "cat", environment: nil, **options) switch_env("EDITOR", editor) do diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb index 65f6916acb..76a7cd055f 100644 --- a/railties/test/commands/dbconsole_test.rb +++ b/railties/test/commands/dbconsole_test.rb @@ -216,22 +216,22 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase end end - def test_specifying_a_custom_connection_and_environment + def test_specifying_a_custom_database_and_environment stub_available_environments(["development"]) do - dbconsole = parse_arguments(["-c", "custom", "-e", "development"]) + dbconsole = parse_arguments(["--db", "custom", "-e", "development"]) assert_equal "development", dbconsole[:environment] - assert_equal "custom", dbconsole.connection + assert_equal "custom", dbconsole.database end end - def test_specifying_a_missing_connection + def test_specifying_a_missing_database app_db_config({}) do e = assert_raises(ActiveRecord::AdapterNotSpecified) do - Rails::Command.invoke(:dbconsole, ["-c", "i_do_not_exist"]) + Rails::Command.invoke(:dbconsole, ["--db", "i_do_not_exist"]) end - assert_includes e.message, "'i_do_not_exist' connection is not configured." + assert_includes e.message, "'i_do_not_exist' database is not configured." end end @@ -245,6 +245,18 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase end end + def test_connection_options_is_deprecate + command = Rails::Command::DbconsoleCommand.new([], ["-c", "custom"]) + Rails::DBConsole.stub(:start, nil) do + assert_deprecated("`connection` option is deprecated") do + command.perform + end + end + + assert_equal "custom", command.options["connection"] + assert_equal "custom", command.options["database"] + end + def test_print_help_short stdout = capture(:stdout) do Rails::Command.invoke(:dbconsole, ["-h"]) diff --git a/railties/test/commands/initializers_test.rb b/railties/test/commands/initializers_test.rb index bdfbb3021c..793365ef3d 100644 --- a/railties/test/commands/initializers_test.rb +++ b/railties/test/commands/initializers_test.rb @@ -25,8 +25,24 @@ class Rails::Command::InitializersTest < ActiveSupport::TestCase assert final_output.include?("set_added_test_module") end + + test "prints out initializers only specified in environment option" do + add_to_config <<-RUBY + initializer(:set_added_development_module) { } if Rails.env.development? + initializer(:set_added_production_module) { } if Rails.env.production? + RUBY + + output = run_initializers_command.split("\n") + assert_includes output, "AppTemplate::Application.set_added_development_module" + assert_not_includes output, "AppTemplate::Application.set_added_production_module" + + output = run_initializers_command(["-e", "production"]).split("\n") + assert_not_includes output, "AppTemplate::Application.set_added_development_module" + assert_includes output, "AppTemplate::Application.set_added_production_module" + end + private - def run_initializers_command - rails "initializers" + def run_initializers_command(args = []) + rails "initializers", args end end diff --git a/railties/test/commands/notes_test.rb b/railties/test/commands/notes_test.rb index 147019e299..9182541413 100644 --- a/railties/test/commands/notes_test.rb +++ b/railties/test/commands/notes_test.rb @@ -121,6 +121,47 @@ class Rails::Command::NotesTest < ActiveSupport::TestCase OUTPUT end + test "displays results from additional tags added to the default tags from a config file" do + app_file "app/models/profile.rb", "# TESTME: some method to test" + app_file "app/controllers/hello_controller.rb", "# DEPRECATEME: this action is no longer needed" + app_file "db/some_seeds.rb", "# TODO: default tags such as TODO are still present" + + add_to_config 'config.annotations.register_tags "TESTME", "DEPRECATEME"' + + assert_equal <<~OUTPUT, run_notes_command + app/controllers/hello_controller.rb: + * [1] [DEPRECATEME] this action is no longer needed + + app/models/profile.rb: + * [1] [TESTME] some method to test + + db/some_seeds.rb: + * [1] [TODO] default tags such as TODO are still present + + OUTPUT + end + + test "does not display results from tags that are neither default nor registered" do + app_file "app/models/profile.rb", "# TESTME: some method to test" + app_file "app/controllers/hello_controller.rb", "# DEPRECATEME: this action is no longer needed" + app_file "db/some_seeds.rb", "# TODO: default tags such as TODO are still present" + app_file "db/some_other_seeds.rb", "# BAD: this note should not be listed" + + add_to_config 'config.annotations.register_tags "TESTME", "DEPRECATEME"' + + assert_equal <<~OUTPUT, run_notes_command + app/controllers/hello_controller.rb: + * [1] [DEPRECATEME] this action is no longer needed + + app/models/profile.rb: + * [1] [TESTME] some method to test + + db/some_seeds.rb: + * [1] [TODO] default tags such as TODO are still present + + OUTPUT + end + private def run_notes_command(args = []) rails "notes", args diff --git a/railties/test/commands/routes_test.rb b/railties/test/commands/routes_test.rb index b4f927060e..a2dbd944f5 100644 --- a/railties/test/commands/routes_test.rb +++ b/railties/test/commands/routes_test.rb @@ -62,7 +62,6 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase assert_equal <<~MESSAGE, run_routes_command([ "-g", "POST" ]) Prefix Verb URI Pattern Controller#Action POST /cart(.:format) cart#create - rails_amazon_inbound_emails POST /rails/action_mailbox/amazon/inbound_emails(.:format) action_mailbox/ingresses/amazon/inbound_emails#create rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create rails_relay_inbound_emails POST /rails/action_mailbox/relay/inbound_emails(.:format) action_mailbox/ingresses/relay/inbound_emails#create @@ -166,7 +165,6 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase assert_equal <<~MESSAGE, run_routes_command Prefix Verb URI Pattern Controller#Action - rails_amazon_inbound_emails POST /rails/action_mailbox/amazon/inbound_emails(.:format) action_mailbox/ingresses/amazon/inbound_emails#create rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create rails_relay_inbound_emails POST /rails/action_mailbox/relay/inbound_emails(.:format) action_mailbox/ingresses/relay/inbound_emails#create @@ -207,101 +205,96 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase URI | /cart(.:format) Controller#Action | cart#show --[ Route 2 ]-------------- - Prefix | rails_amazon_inbound_emails - Verb | POST - URI | /rails/action_mailbox/amazon/inbound_emails(.:format) - Controller#Action | action_mailbox/ingresses/amazon/inbound_emails#create - --[ Route 3 ]-------------- Prefix | rails_mandrill_inbound_emails Verb | POST URI | /rails/action_mailbox/mandrill/inbound_emails(.:format) Controller#Action | action_mailbox/ingresses/mandrill/inbound_emails#create - --[ Route 4 ]-------------- + --[ Route 3 ]-------------- Prefix | rails_postmark_inbound_emails Verb | POST URI | /rails/action_mailbox/postmark/inbound_emails(.:format) Controller#Action | action_mailbox/ingresses/postmark/inbound_emails#create - --[ Route 5 ]-------------- + --[ Route 4 ]-------------- Prefix | rails_relay_inbound_emails Verb | POST URI | /rails/action_mailbox/relay/inbound_emails(.:format) Controller#Action | action_mailbox/ingresses/relay/inbound_emails#create - --[ Route 6 ]-------------- + --[ Route 5 ]-------------- Prefix | rails_sendgrid_inbound_emails Verb | POST URI | /rails/action_mailbox/sendgrid/inbound_emails(.:format) Controller#Action | action_mailbox/ingresses/sendgrid/inbound_emails#create - --[ Route 7 ]-------------- + --[ Route 6 ]-------------- Prefix | rails_mailgun_inbound_emails Verb | POST URI | /rails/action_mailbox/mailgun/inbound_emails/mime(.:format) Controller#Action | action_mailbox/ingresses/mailgun/inbound_emails#create - --[ Route 8 ]-------------- + --[ Route 7 ]-------------- Prefix | rails_conductor_inbound_emails Verb | GET URI | /rails/conductor/action_mailbox/inbound_emails(.:format) Controller#Action | rails/conductor/action_mailbox/inbound_emails#index - --[ Route 9 ]-------------- + --[ Route 8 ]-------------- Prefix | Verb | POST URI | /rails/conductor/action_mailbox/inbound_emails(.:format) Controller#Action | rails/conductor/action_mailbox/inbound_emails#create - --[ Route 10 ]------------- + --[ Route 9 ]-------------- Prefix | new_rails_conductor_inbound_email Verb | GET URI | /rails/conductor/action_mailbox/inbound_emails/new(.:format) Controller#Action | rails/conductor/action_mailbox/inbound_emails#new - --[ Route 11 ]------------- + --[ Route 10 ]------------- Prefix | edit_rails_conductor_inbound_email Verb | GET URI | /rails/conductor/action_mailbox/inbound_emails/:id/edit(.:format) Controller#Action | rails/conductor/action_mailbox/inbound_emails#edit - --[ Route 12 ]------------- + --[ Route 11 ]------------- Prefix | rails_conductor_inbound_email Verb | GET URI | /rails/conductor/action_mailbox/inbound_emails/:id(.:format) Controller#Action | rails/conductor/action_mailbox/inbound_emails#show - --[ Route 13 ]------------- + --[ Route 12 ]------------- Prefix | Verb | PATCH URI | /rails/conductor/action_mailbox/inbound_emails/:id(.:format) Controller#Action | rails/conductor/action_mailbox/inbound_emails#update - --[ Route 14 ]------------- + --[ Route 13 ]------------- Prefix | Verb | PUT URI | /rails/conductor/action_mailbox/inbound_emails/:id(.:format) Controller#Action | rails/conductor/action_mailbox/inbound_emails#update - --[ Route 15 ]------------- + --[ Route 14 ]------------- Prefix | Verb | DELETE URI | /rails/conductor/action_mailbox/inbound_emails/:id(.:format) Controller#Action | rails/conductor/action_mailbox/inbound_emails#destroy - --[ Route 16 ]------------- + --[ Route 15 ]------------- Prefix | rails_conductor_inbound_email_reroute Verb | POST URI | /rails/conductor/action_mailbox/:inbound_email_id/reroute(.:format) Controller#Action | rails/conductor/action_mailbox/reroutes#create - --[ Route 17 ]------------- + --[ Route 16 ]------------- Prefix | rails_service_blob Verb | GET URI | /rails/active_storage/blobs/:signed_id/*filename(.:format) Controller#Action | active_storage/blobs#show - --[ Route 18 ]------------- + --[ Route 17 ]------------- Prefix | rails_blob_representation Verb | GET URI | /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) Controller#Action | active_storage/representations#show - --[ Route 19 ]------------- + --[ Route 18 ]------------- Prefix | rails_disk_service Verb | GET URI | /rails/active_storage/disk/:encoded_key/*filename(.:format) Controller#Action | active_storage/disk#show - --[ Route 20 ]------------- + --[ Route 19 ]------------- Prefix | update_rails_disk_service Verb | PUT URI | /rails/active_storage/disk/:encoded_token(.:format) Controller#Action | active_storage/disk#update - --[ Route 21 ]------------- + --[ Route 20 ]------------- Prefix | rails_direct_uploads Verb | POST URI | /rails/active_storage/direct_uploads(.:format) diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb index 25b89ecbd8..b78370a233 100644 --- a/railties/test/commands/server_test.rb +++ b/railties/test/commands/server_test.rb @@ -22,6 +22,12 @@ class Rails::Command::ServerCommandTest < ActiveSupport::TestCase assert_nil options[:server] end + def test_environment_option_is_properly_expanded + args = ["-e", "prod"] + options = parse_arguments(args) + assert_equal "production", options[:environment] + end + def test_explicit_using_option args = ["-u", "thin"] options = parse_arguments(args) @@ -285,6 +291,8 @@ class Rails::Command::ServerCommandTest < ActiveSupport::TestCase end def parse_arguments(args = []) - Rails::Command::ServerCommand.new([], args).server_options + command = Rails::Command::ServerCommand.new([], args) + command.send(:extract_environment_option_from_argument) + command.server_options end end diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb index 44d4e92256..d913bb5438 100644 --- a/railties/test/generators/actions_test.rb +++ b/railties/test/generators/actions_test.rb @@ -200,7 +200,7 @@ class ActionsTest < Rails::Generators::TestCase run_generator action :environment do - _ = "# This wont be added"# assignment to silence parse-time warning "unused literal ignored" + _ = "# This wont be added" # assignment to silence parse-time warning "unused literal ignored" "# This will be added" end diff --git a/railties/test/generators/api_app_generator_test.rb b/railties/test/generators/api_app_generator_test.rb index 4b9878187b..d03178da66 100644 --- a/railties/test/generators/api_app_generator_test.rb +++ b/railties/test/generators/api_app_generator_test.rb @@ -50,6 +50,13 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase assert_file "config/application.rb", /config\.api_only = true/ assert_file "app/controllers/application_controller.rb", /ActionController::API/ + + assert_file "config/environments/development.rb" do |content| + assert_no_match(/action_controller\.perform_caching = true/, content) + end + assert_file "config/environments/production.rb" do |content| + assert_no_match(/action_controller\.perform_caching = true/, content) + end end def test_generator_if_skip_action_cable_is_given @@ -120,7 +127,6 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase bin/rails bin/rake bin/setup - bin/update config/application.rb config/boot.rb config/cable.yml diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 1ee9e43e89..5b439fdcba 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -41,7 +41,6 @@ DEFAULT_APP_FILES = %w( bin/rails bin/rake bin/setup - bin/update bin/yarn config/application.rb config/boot.rb @@ -321,10 +320,6 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "#{app_root}/bin/setup" do |content| assert_no_match(/system\('bin\/yarn'\)/, content) end - - assert_file "#{app_root}/bin/update" do |content| - assert_no_match(/system\('bin\/yarn'\)/, content) - end end end @@ -345,39 +340,44 @@ class AppGeneratorTest < Rails::Generators::TestCase app_root = File.join(destination_root, "myapp") run_generator [app_root, "--skip-spring"] - FileUtils.cd(app_root) do - quietly { system("bin/rails app:update") } - end + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], { update: true, skip_spring: true }, { destination_root: app_root, shell: @shell } + generator.send(:app_const) + quietly { generator.send(:update_config_files) } - assert_no_file "#{app_root}/config/spring.rb" + assert_no_file "#{app_root}/config/spring.rb" + end end def test_app_update_does_not_generate_action_cable_contents_when_skip_action_cable_is_given app_root = File.join(destination_root, "myapp") run_generator [app_root, "--skip-action-cable"] - FileUtils.cd(app_root) do - quietly { system("bin/rails app:update") } - end + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], { update: true, skip_action_cable: true }, { destination_root: app_root, shell: @shell } + generator.send(:app_const) + quietly { generator.send(:update_config_files) } - assert_no_file "#{app_root}/config/cable.yml" - assert_file "#{app_root}/config/environments/production.rb" do |content| - assert_no_match(/config\.action_cable/, content) + assert_no_file "#{app_root}/config/cable.yml" + assert_file "#{app_root}/config/environments/production.rb" do |content| + assert_no_match(/config\.action_cable/, content) + end + assert_no_file "#{app_root}/test/channels/application_cable/connection_test.rb" end - - assert_no_file "#{app_root}/test/channels/application_cable/connection_test.rb" end def test_app_update_does_not_generate_bootsnap_contents_when_skip_bootsnap_is_given app_root = File.join(destination_root, "myapp") run_generator [app_root, "--skip-bootsnap"] - FileUtils.cd(app_root) do - quietly { system("bin/rails app:update") } - end + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], { update: true, skip_bootsnap: true }, { destination_root: app_root, shell: @shell } + generator.send(:app_const) + quietly { generator.send(:update_config_files) } - assert_file "#{app_root}/config/boot.rb" do |content| - assert_no_match(/require 'bootsnap\/setup'/, content) + assert_file "#{app_root}/config/boot.rb" do |content| + assert_no_match(/require 'bootsnap\/setup'/, content) + end end end @@ -396,46 +396,50 @@ class AppGeneratorTest < Rails::Generators::TestCase app_root = File.join(destination_root, "myapp") run_generator [app_root, "--skip-active-storage"] - FileUtils.cd(app_root) do - quietly { system("bin/rails app:update") } - end + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], { update: true, skip_active_storage: true }, { destination_root: app_root, shell: @shell } + generator.send(:app_const) + quietly { generator.send(:update_config_files) } - assert_file "#{app_root}/config/environments/development.rb" do |content| - assert_no_match(/config\.active_storage/, content) - end + assert_file "#{app_root}/config/environments/development.rb" do |content| + assert_no_match(/config\.active_storage/, content) + end - assert_file "#{app_root}/config/environments/production.rb" do |content| - assert_no_match(/config\.active_storage/, content) - end + assert_file "#{app_root}/config/environments/production.rb" do |content| + assert_no_match(/config\.active_storage/, content) + end - assert_file "#{app_root}/config/environments/test.rb" do |content| - assert_no_match(/config\.active_storage/, content) - end + assert_file "#{app_root}/config/environments/test.rb" do |content| + assert_no_match(/config\.active_storage/, content) + end - assert_no_file "#{app_root}/config/storage.yml" + assert_no_file "#{app_root}/config/storage.yml" + end end def test_app_update_does_not_generate_active_storage_contents_when_skip_active_record_is_given app_root = File.join(destination_root, "myapp") run_generator [app_root, "--skip-active-record"] - FileUtils.cd(app_root) do - quietly { system("bin/rails app:update") } - end + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], { update: true, skip_active_record: true }, { destination_root: app_root, shell: @shell } + generator.send(:app_const) + quietly { generator.send(:update_config_files) } - assert_file "#{app_root}/config/environments/development.rb" do |content| - assert_no_match(/config\.active_storage/, content) - end + assert_file "#{app_root}/config/environments/development.rb" do |content| + assert_no_match(/config\.active_storage/, content) + end - assert_file "#{app_root}/config/environments/production.rb" do |content| - assert_no_match(/config\.active_storage/, content) - end + assert_file "#{app_root}/config/environments/production.rb" do |content| + assert_no_match(/config\.active_storage/, content) + end - assert_file "#{app_root}/config/environments/test.rb" do |content| - assert_no_match(/config\.active_storage/, content) - end + assert_file "#{app_root}/config/environments/test.rb" do |content| + assert_no_match(/config\.active_storage/, content) + end - assert_no_file "#{app_root}/config/storage.yml" + assert_no_file "#{app_root}/config/storage.yml" + end end def test_generator_skips_action_mailbox_when_skip_action_mailbox_is_given @@ -469,16 +473,17 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_app_update_does_not_change_config_target_version - run_generator + app_root = File.join(destination_root, "myapp") + run_generator [app_root, "--skip-spring"] - FileUtils.cd(destination_root) do + FileUtils.cd(app_root) do config = "config/application.rb" content = File.read(config) File.write(config, content.gsub(/config\.load_defaults #{Rails::VERSION::STRING.to_f}/, "config.load_defaults 5.1")) quietly { system("bin/rails app:update") } end - assert_file "config/application.rb", /\s+config\.load_defaults 5\.1/ + assert_file "#{app_root}/config/application.rb", /\s+config\.load_defaults 5\.1/ end def test_app_update_does_not_change_app_name_when_app_name_is_hyphenated_name @@ -495,13 +500,15 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_no_match(/hyphenated-app/, content) end - FileUtils.cd(app_root) do - quietly { system("bin/rails app:update") } - end + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], { update: true }, { destination_root: app_root, shell: @shell } + generator.send(:app_const) + quietly { generator.send(:update_config_files) } - assert_file "#{app_root}/config/cable.yml" do |content| - assert_match(/hyphenated_app/, content) - assert_no_match(/hyphenated-app/, content) + assert_file "#{app_root}/config/cable.yml" do |content| + assert_match(/hyphenated_app/, content) + assert_no_match(/hyphenated-app/, content) + end end end @@ -526,7 +533,7 @@ class AppGeneratorTest < Rails::Generators::TestCase if defined?(JRUBY_VERSION) assert_gem "activerecord-jdbcsqlite3-adapter" else - assert_gem "sqlite3", "'~> 1.3', '>= 1.3.6'" + assert_gem "sqlite3", "'~> 1.4'" end end @@ -617,7 +624,7 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_no_gem "capybara" assert_no_gem "selenium-webdriver" - assert_no_gem "chromedriver-helper" + assert_no_gem "webdrivers" assert_no_directory("test") end @@ -626,7 +633,7 @@ class AppGeneratorTest < Rails::Generators::TestCase run_generator [destination_root, "--skip-system-test"] assert_no_gem "capybara" assert_no_gem "selenium-webdriver" - assert_no_gem "chromedriver-helper" + assert_no_gem "webdrivers" assert_directory("test") @@ -678,6 +685,21 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_inclusion_of_listen_related_configuration_on_other_rubies + ruby_engine = Object.send(:remove_const, :RUBY_ENGINE) + Object.const_set(:RUBY_ENGINE, "MyRuby") + + run_generator + if RbConfig::CONFIG["host_os"] =~ /darwin|linux/ + assert_listen_related_configuration + else + assert_no_listen_related_configuration + end + ensure + Object.send(:remove_const, :RUBY_ENGINE) + Object.const_set(:RUBY_ENGINE, ruby_engine) + end + def test_non_inclusion_of_listen_related_configuration_if_skip_listen run_generator [destination_root, "--skip-listen"] assert_no_listen_related_configuration @@ -812,6 +834,9 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_spring run_generator assert_gem "spring" + assert_file("config/environments/test.rb") do |contents| + assert_match("config.cache_classes = false", contents) + end end def test_bundler_binstub @@ -830,7 +855,7 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_spring_no_fork jruby_skip "spring doesn't run on JRuby" - assert_called_with(Process, :respond_to?, [[:fork], [:fork]], returns: false) do + assert_called_with(Process, :respond_to?, [[:fork], [:fork], [:fork]], returns: false) do run_generator assert_no_gem "spring" @@ -842,6 +867,9 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_no_file "config/spring.rb" assert_no_gem "spring" + assert_file("config/environments/test.rb") do |contents| + assert_match("config.cache_classes = true", contents) + end end def test_spring_with_dev_option @@ -968,6 +996,8 @@ class AppGeneratorTest < Rails::Generators::TestCase else assert_match(/#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}/, content) end + + assert content.end_with?("\n"), "expected .ruby-version to end with newline" end end diff --git a/railties/test/generators/db_system_change_generator_test.rb b/railties/test/generators/db_system_change_generator_test.rb index d476bfd2dc..607db96906 100644 --- a/railties/test/generators/db_system_change_generator_test.rb +++ b/railties/test/generators/db_system_change_generator_test.rb @@ -40,7 +40,7 @@ module Rails assert_file("Gemfile") do |content| assert_match "# Use pg as the database for Active Record", content - assert_match "gem 'pg'", content + assert_match "gem 'pg', '>= 0.18', '< 2.0'", content end end @@ -54,7 +54,7 @@ module Rails assert_file("Gemfile") do |content| assert_match "# Use mysql2 as the database for Active Record", content - assert_match "gem 'mysql2'", content + assert_match "gem 'mysql2', '>= 0.4.4'", content end end @@ -68,7 +68,22 @@ module Rails assert_file("Gemfile") do |content| assert_match "# Use sqlite3 as the database for Active Record", content - assert_match "gem 'sqlite3'", content + assert_match "gem 'sqlite3', '~> 1.4'", content + end + end + + test "change from versioned gem to other versioned gem" do + run_generator ["--to", "sqlite3"] + run_generator ["--to", "mysql", "--force"] + + assert_file("config/database.yml") do |content| + assert_match "adapter: mysql2", content + assert_match "database: test_app", content + end + + assert_file("Gemfile") do |content| + assert_match "# Use mysql2 as the database for Active Record", content + assert_match "gem 'mysql2', '>= 0.4.4'", content end end end diff --git a/railties/test/generators/generated_attribute_test.rb b/railties/test/generators/generated_attribute_test.rb index 772b4f6f0d..82d550a124 100644 --- a/railties/test/generators/generated_attribute_test.rb +++ b/railties/test/generators/generated_attribute_test.rb @@ -6,6 +6,15 @@ require "rails/generators/generated_attribute" class GeneratedAttributeTest < Rails::Generators::TestCase include GeneratorsTestHelper + def setup + @old_belongs_to_required_by_default = Rails.application.config.active_record.belongs_to_required_by_default + Rails.application.config.active_record.belongs_to_required_by_default = true + end + + def teardown + Rails.application.config.active_record.belongs_to_required_by_default = @old_belongs_to_required_by_default + end + def test_field_type_returns_number_field assert_field_type :integer, :number_field end @@ -38,6 +47,16 @@ class GeneratedAttributeTest < Rails::Generators::TestCase assert_field_type :boolean, :check_box end + def test_field_type_returns_rich_text_area + assert_field_type :rich_text, :rich_text_area + end + + def test_field_type_returns_file_field + %w(attachment attachments).each do |attribute_type| + assert_field_type attribute_type, :file_field + end + end + def test_field_type_with_unknown_type_returns_text_field %w(foo bar baz).each do |attribute_type| assert_field_type attribute_type, :text_field @@ -84,7 +103,7 @@ class GeneratedAttributeTest < Rails::Generators::TestCase end def test_default_value_is_nil - %w(references belongs_to).each do |attribute_type| + %w(references belongs_to rich_text attachment attachments).each do |attribute_type| assert_field_default_value attribute_type, nil end end @@ -145,10 +164,16 @@ class GeneratedAttributeTest < Rails::Generators::TestCase end def test_parse_required_attribute_with_index - att = Rails::Generators::GeneratedAttribute.parse("supplier:references{required}:index") + att = Rails::Generators::GeneratedAttribute.parse("supplier:references:index") assert_equal "supplier", att.name assert_equal :references, att.type assert_predicate att, :has_index? assert_predicate att, :required? end + + def test_parse_required_attribute_with_index_false_when_belongs_to_required_by_default_global_config_is_false + Rails.application.config.active_record.belongs_to_required_by_default = false + att = Rails::Generators::GeneratedAttribute.parse("supplier:references:index") + assert_not_predicate att, :required? + end end diff --git a/railties/test/generators/migration_generator_test.rb b/railties/test/generators/migration_generator_test.rb index b8958f6f99..65cde91436 100644 --- a/railties/test/generators/migration_generator_test.rb +++ b/railties/test/generators/migration_generator_test.rb @@ -6,6 +6,16 @@ require "rails/generators/rails/migration/migration_generator" class MigrationGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper + def setup + @old_belongs_to_required_by_default = Rails.application.config.active_record.belongs_to_required_by_default + + Rails.application.config.active_record.belongs_to_required_by_default = true + end + + def teardown + Rails.application.config.active_record.belongs_to_required_by_default = @old_belongs_to_required_by_default + end + def test_migration migration = "change_title_body_from_posts" run_generator [migration] @@ -196,9 +206,9 @@ class MigrationGeneratorTest < Rails::Generators::TestCase end end - def test_add_migration_with_required_references + def test_add_migration_with_references_adds_null_false_by_default migration = "add_references_to_books" - run_generator [migration, "author:belongs_to{required}", "distributor:references{polymorphic,required}"] + run_generator [migration, "author:belongs_to", "distributor:references{polymorphic}"] assert_migration "db/migrate/#{migration}.rb" do |content| assert_method :change, content do |change| @@ -208,6 +218,21 @@ class MigrationGeneratorTest < Rails::Generators::TestCase end end + def test_add_migration_with_references_does_not_add_belongs_to_when_required_by_default_global_config_is_false + Rails.application.config.active_record.belongs_to_required_by_default = false + + migration = "add_references_to_books" + + run_generator [migration, "author:belongs_to", "distributor:references{polymorphic}"] + + assert_migration "db/migrate/#{migration}.rb" do |content| + assert_method :change, content do |change| + assert_match(/add_reference :books, :author/, change) + assert_match(/add_reference :books, :distributor, polymorphic: true/, change) + end + end + end + def test_add_migration_with_references_adds_foreign_keys migration = "add_references_to_books" run_generator [migration, "author:belongs_to", "distributor:references{polymorphic}"] @@ -280,6 +305,17 @@ class MigrationGeneratorTest < Rails::Generators::TestCase end end + def test_database_puts_migrations_in_configured_folder_with_aliases + with_secondary_database_configuration do + run_generator ["create_books", "--db=secondary"] + assert_migration "db/secondary_migrate/create_books.rb" do |content| + assert_method :change, content do |change| + assert_match(/create_table :books/, change) + end + end + end + end + def test_should_create_empty_migrations_if_name_not_start_with_add_or_remove_or_create migration = "delete_books" run_generator [migration, "title:string", "content:text"] @@ -370,6 +406,44 @@ class MigrationGeneratorTest < Rails::Generators::TestCase Rails.application.config.paths["db/migrate"] = old_paths end + def test_add_migration_ignores_virtual_attributes + migration = "add_rich_text_content_to_messages" + run_generator [migration, "content:rich_text", "video:attachment", "photos:attachments"] + + assert_migration "db/migrate/#{migration}.rb" do |content| + assert_method :change, content do |change| + assert_no_match(/add_column :messages, :content, :rich_text/, change) + assert_no_match(/add_column :messages, :video, :attachment/, change) + assert_no_match(/add_column :messages, :photos, :attachments/, change) + end + end + end + + def test_create_table_migration_ignores_virtual_attributes + run_generator ["create_messages", "content:rich_text", "video:attachment", "photos:attachments"] + assert_migration "db/migrate/create_messages.rb" do |content| + assert_method :change, content do |change| + assert_match(/create_table :messages/, change) + assert_no_match(/ t\.rich_text :content/, change) + assert_no_match(/ t\.attachment :video/, change) + assert_no_match(/ t\.attachments :photos/, change) + end + end + end + + def test_remove_migration_with_virtual_attributes + migration = "remove_content_from_messages" + run_generator [migration, "content:rich_text", "video:attachment", "photos:attachments"] + + assert_migration "db/migrate/#{migration}.rb" do |content| + assert_method :change, content do |change| + assert_no_match(/remove_column :messages, :content, :rich_text/, change) + assert_no_match(/remove_column :messages, :video, :attachment/, change) + assert_no_match(/remove_column :messages, :photos, :attachments/, change) + end + end + end + private def with_singular_table_name diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb index 134659f285..d6abaf7a6f 100644 --- a/railties/test/generators/model_generator_test.rb +++ b/railties/test/generators/model_generator_test.rb @@ -10,6 +10,14 @@ class ModelGeneratorTest < Rails::Generators::TestCase def setup super Rails::Generators::ModelHelpers.skip_warn = false + @old_belongs_to_required_by_default = Rails.application.config.active_record.belongs_to_required_by_default + + Rails.application.config.active_record.belongs_to_required_by_default = true + end + + + def teardown + Rails.application.config.active_record.belongs_to_required_by_default = @old_belongs_to_required_by_default end def test_help_shows_invoked_generators_options @@ -403,45 +411,68 @@ class ModelGeneratorTest < Rails::Generators::TestCase end end - def test_required_belongs_to_adds_required_association - run_generator ["account", "supplier:references{required}"] + def test_database_puts_migrations_in_configured_folder_with_aliases + with_secondary_database_configuration do + run_generator ["account", "--db=secondary"] + assert_migration "db/secondary_migrate/create_accounts.rb" do |content| + assert_method :change, content do |change| + assert_match(/create_table :accounts/, change) + end + end + end + end + + def test_polymorphic_belongs_to_generates_correct_model + run_generator ["account", "supplier:references{polymorphic}"] expected_file = <<~FILE class Account < ApplicationRecord - belongs_to :supplier, required: true + belongs_to :supplier, polymorphic: true end FILE assert_file "app/models/account.rb", expected_file end - def test_required_polymorphic_belongs_to_generages_correct_model - run_generator ["account", "supplier:references{required,polymorphic}"] + def test_passing_required_to_model_generator_is_deprecated + assert_deprecated do + run_generator ["account", "supplier:references{required}"] + end - expected_file = <<~FILE - class Account < ApplicationRecord - belongs_to :supplier, polymorphic: true, required: true + assert_migration "db/migrate/create_accounts.rb" do |m| + assert_method :change, m do |up| + assert_match(/t\.references :supplier,.*\snull: false/, up) end - FILE - assert_file "app/models/account.rb", expected_file + end end - def test_required_and_polymorphic_are_order_independent - run_generator ["account", "supplier:references{polymorphic.required}"] + def test_null_false_is_added_for_references_by_default + run_generator ["account", "user:references"] - expected_file = <<~FILE - class Account < ApplicationRecord - belongs_to :supplier, polymorphic: true, required: true + assert_migration "db/migrate/create_accounts.rb" do |m| + assert_method :change, m do |up| + assert_match(/t\.references :user,.*\snull: false/, up) end - FILE - assert_file "app/models/account.rb", expected_file + end end - def test_required_adds_null_false_to_column - run_generator ["account", "supplier:references{required}"] + def test_null_false_is_added_for_belongs_to_by_default + run_generator ["account", "user:belongs_to"] assert_migration "db/migrate/create_accounts.rb" do |m| assert_method :change, m do |up| - assert_match(/t\.references :supplier,.*\snull: false/, up) + assert_match(/t\.belongs_to :user,.*\snull: false/, up) + end + end + end + + def test_null_false_is_not_added_when_belongs_to_required_by_default_global_config_is_false + Rails.application.config.active_record.belongs_to_required_by_default = false + + run_generator ["account", "user:belongs_to"] + + assert_migration "db/migrate/create_accounts.rb" do |m| + assert_method :change, m do |up| + assert_match(/t\.belongs_to :user/, up) end end end @@ -488,6 +519,43 @@ class ModelGeneratorTest < Rails::Generators::TestCase assert_file "app/models/user.rb", expected_file end + def test_model_with_rich_text_attribute_adds_has_rich_text + run_generator ["message", "content:rich_text"] + expected_file = <<~FILE + class Message < ApplicationRecord + has_rich_text :content + end + FILE + assert_file "app/models/message.rb", expected_file + end + + def test_model_with_attachment_attribute_adds_has_one_attached + run_generator ["message", "video:attachment"] + expected_file = <<~FILE + class Message < ApplicationRecord + has_one_attached :video + end + FILE + assert_file "app/models/message.rb", expected_file + end + + def test_model_with_attachments_attribute_adds_has_many_attached + run_generator ["message", "photos:attachments"] + expected_file = <<~FILE + class Message < ApplicationRecord + has_many_attached :photos + end + FILE + assert_file "app/models/message.rb", expected_file + end + + def test_skip_virtual_fields_in_fixtures + run_generator ["message", "content:rich_text", "video:attachment", "photos:attachments"] + + assert_generated_fixture("test/fixtures/messages.yml", + "one" => nil, "two" => nil) + end + private def assert_generated_fixture(path, parsed_contents) fixture_file = File.new File.expand_path(path, destination_root) diff --git a/railties/test/generators/scaffold_controller_generator_test.rb b/railties/test/generators/scaffold_controller_generator_test.rb index fd5aa817b4..8278b72a5a 100644 --- a/railties/test/generators/scaffold_controller_generator_test.rb +++ b/railties/test/generators/scaffold_controller_generator_test.rb @@ -80,6 +80,24 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase end end + def test_controller_permit_attachment_attributes + run_generator ["Message", "video:attachment", "photos:attachments"] + + assert_file "app/controllers/messages_controller.rb" do |content| + assert_match(/def message_params/, content) + assert_match(/params\.require\(:message\)\.permit\(:video, photos: \[\]\)/, content) + end + end + + def test_controller_permit_attachments_attributes_only + run_generator ["Message", "photos:attachments"] + + assert_file "app/controllers/messages_controller.rb" do |content| + assert_match(/def message_params/, content) + assert_match(/params\.require\(:message\)\.permit\(photos: \[\]\)/, content) + end + end + def test_helper_are_invoked_with_a_pluralized_name run_generator assert_file "app/helpers/users_helper.rb", /module UsersHelper/ @@ -276,4 +294,13 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase assert_no_match(/assert_redirected_to/, content) end end + + def test_api_only_generates_params_for_attachments + run_generator ["Message", "video:attachment", "photos:attachments", "--api"] + + assert_file "app/controllers/messages_controller.rb" do |content| + assert_match(/def message_params/, content) + assert_match(/params\.require\(:message\)\.permit\(:video, photos: \[\]\)/, content) + end + end end diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb index f672e301a7..fa9a42215b 100644 --- a/railties/test/generators/scaffold_generator_test.rb +++ b/railties/test/generators/scaffold_generator_test.rb @@ -471,6 +471,54 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase end end + def test_scaffold_generator_attachments + run_generator ["message", "video:attachment", "photos:attachments", "images:attachments"] + + assert_file "app/models/message.rb", /has_one_attached :video/ + assert_file "app/models/message.rb", /has_many_attached :photos/ + + assert_file "app/controllers/messages_controller.rb" do |content| + assert_instance_method :message_params, content do |m| + assert_match(/permit\(:video, photos: \[\], images: \[\]\)/, m) + end + end + + assert_file "app/views/messages/_form.html.erb" do |content| + assert_match(/^\W{4}<%= form\.file_field :video %>/, content) + assert_match(/^\W{4}<%= form\.file_field :photos, multiple: true %>/, content) + end + + assert_file "app/views/messages/show.html.erb" do |content| + assert_match(/^\W{2}<%= link_to @message\.video\.filename, @message\.video if @message\.video\.attached\? %>/, content) + assert_match(/^\W{4}<div><%= link_to photo\.filename, photo %>/, content) + end + + assert_file "test/system/messages_test.rb" do |content| + assert_no_match(/fill_in "Video"/, content) + assert_no_match(/fill_in "Photos"/, content) + end + end + + def test_scaffold_generator_rich_text + run_generator ["message", "content:rich_text"] + + assert_file "app/models/message.rb", /rich_text :content/ + + assert_file "app/controllers/messages_controller.rb" do |content| + assert_instance_method :message_params, content do |m| + assert_match(/permit\(:content\)/, m) + end + end + + assert_file "app/views/messages/_form.html.erb" do |content| + assert_match(/^\W{4}<%= form\.rich_text_area :content %>/, content) + end + + assert_file "test/system/messages_test.rb" do |content| + assert_no_match(/fill_in "Content"/, content) + end + end + def test_scaffold_generator_database with_secondary_database_configuration do run_generator ["posts", "--database=secondary"] @@ -479,6 +527,14 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase end end + def test_scaffold_generator_database_with_aliases + with_secondary_database_configuration do + run_generator ["posts", "--db=secondary"] + + assert_migration "db/secondary_migrate/create_posts.rb" + end + end + def test_scaffold_generator_password_digest run_generator ["user", "name", "password:digest"] diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb index 26ce487c5f..3e8ce1c018 100644 --- a/railties/test/generators/shared_generator_tests.rb +++ b/railties/test/generators/shared_generator_tests.rb @@ -191,10 +191,7 @@ module SharedGeneratorTests assert_no_match(/fixtures :all/, helper_content) end assert_file "#{application_path}/bin/setup" do |setup_content| - assert_no_match(/db:setup/, setup_content) - end - assert_file "#{application_path}/bin/update" do |update_content| - assert_no_match(/db:migrate/, update_content) + assert_no_match(/db:prepare/, setup_content) end assert_file ".gitignore" do |content| assert_no_match(/sqlite/i, content) diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index ca7601f6fe..fab704944b 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -18,7 +18,9 @@ require "active_support/testing/method_call_assertions" require "active_support/test_case" require "minitest/retry" -Minitest::Retry.use!(verbose: false, retry_count: 1) +if ENV["BUILDKITE"] + Minitest::Retry.use!(verbose: false, retry_count: 1) +end RAILS_FRAMEWORK_ROOT = File.expand_path("../../..", __dir__) @@ -123,6 +125,8 @@ module TestHelpers adapter: sqlite3 pool: 5 timeout: 5000 + variables: + statement_timeout: 1000 development: primary: <<: *default @@ -226,6 +230,7 @@ module TestHelpers @app.config.session_store :cookie_store, key: "_myapp_session" @app.config.active_support.deprecation = :log @app.config.log_level = :info + @app.secrets.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33" yield @app if block_given? @app.initialize! @@ -421,6 +426,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 +463,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 +488,24 @@ 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. + unless File.exist?("#{RAILS_FRAMEWORK_ROOT}/actionview/lib/assets/compiled/rails-ujs.js") + Dir.chdir("#{RAILS_FRAMEWORK_ROOT}/actionview") { `yarn build` } + end - # 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" + unless Dir.exist?("#{assets_path}/node_modules") + Dir.chdir(assets_path) { `yarn install` } + end + 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/rails_info_controller_test.rb b/railties/test/rails_info_controller_test.rb index 878a238f8d..185a2f9f09 100644 --- a/railties/test/rails_info_controller_test.rb +++ b/railties/test/rails_info_controller_test.rb @@ -10,6 +10,7 @@ end class InfoControllerTest < ActionController::TestCase tests Rails::InfoController + Rails.application.config.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33" def setup Rails.application.routes.draw do diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index 851407dede..8ce68dbcfa 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 @@ -877,7 +879,7 @@ YAML assert Bukkits::Engine.config.bukkits_seeds_loaded end - test "jobs are ran inline while loading seeds" do + test "jobs are ran inline while loading seeds with async adapter configured" do app_file "db/seeds.rb", <<-RUBY Rails.application.config.seed_queue_adapter = ActiveJob::Base.queue_adapter RUBY @@ -889,6 +891,45 @@ YAML assert_instance_of ActiveJob::QueueAdapters::AsyncAdapter, ActiveJob::Base.queue_adapter end + test "jobs are ran with original adapter while loading seeds with custom adapter configured" do + app_file "db/seeds.rb", <<-RUBY + Rails.application.config.seed_queue_adapter = ActiveJob::Base.queue_adapter + RUBY + + boot_rails + Rails.application.config.active_job.queue_adapter = :delayed_job + Rails.application.load_seed + + assert_instance_of ActiveJob::QueueAdapters::DelayedJobAdapter, Rails.application.config.seed_queue_adapter + assert_instance_of ActiveJob::QueueAdapters::DelayedJobAdapter, ActiveJob::Base.queue_adapter + end + + test "seed data can be loaded when ActiveJob is not present" do + @plugin.write "db/seeds.rb", <<-RUBY + Bukkits::Engine.config.bukkits_seeds_loaded = true + RUBY + + app_file "db/seeds.rb", <<-RUBY + Rails.application.config.app_seeds_loaded = true + RUBY + + boot_rails + + # In a real app, config.active_job would be undefined when + # NOT requiring rails/all AND NOT requiring active_job/railtie + # that doesn't work as expected in this test environment, so: + undefine_config_option(:active_job) + assert_raise(NoMethodError) { Rails.application.config.active_job } + + assert_raise(NoMethodError) { Rails.application.config.app_seeds_loaded } + assert_raise(NoMethodError) { Bukkits::Engine.config.bukkits_seeds_loaded } + + Rails.application.load_seed + assert Rails.application.config.app_seeds_loaded + Bukkits::Engine.load_seed + assert Bukkits::Engine.config.bukkits_seeds_loaded + end + test "skips nonexistent seed data" do FileUtils.rm "#{app_path}/db/seeds.rb" boot_rails @@ -1508,6 +1549,10 @@ YAML Rails.application end + def undefine_config_option(name) + Rails.application.config.class.class_variable_get(:@@options).delete(name) + end + # Restrict frameworks to load in order to avoid engine frameworks affect tests. def restrict_frameworks remove_from_config("require 'rails/all'") |