diff options
Diffstat (limited to 'railties/test/isolation')
-rw-r--r-- | railties/test/isolation/abstract_unit.rb | 395 |
1 files changed, 289 insertions, 106 deletions
diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 65d8a55421..d4eed69a87 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -1,40 +1,53 @@ +# frozen_string_literal: true + # Note: # It is important to keep this file as light as possible # the goal for tests that require this is to test booting up -# rails from an empty state, so anything added here could +# Rails from an empty state, so anything added here could # hide potential failures # # It is also good to know what is the bare minimum to get # Rails booted up. -require 'fileutils' +require "fileutils" + +require "bundler/setup" unless defined?(Bundler) +require "active_support" +require "active_support/testing/autorun" +require "active_support/testing/stream" +require "active_support/testing/method_call_assertions" +require "active_support/test_case" +require "minitest/retry" -require 'bundler/setup' unless defined?(Bundler) -require 'active_support' -require 'active_support/testing/autorun' -require 'active_support/testing/stream' -require 'active_support/test_case' +Minitest::Retry.use!(verbose: false, retry_count: 1) -RAILS_FRAMEWORK_ROOT = File.expand_path("#{File.dirname(__FILE__)}/../../..") +RAILS_FRAMEWORK_ROOT = File.expand_path("../../..", __dir__) # These files do not require any others and are needed # to run the tests +require "active_support/core_ext/object/blank" require "active_support/testing/isolation" require "active_support/core_ext/kernel/reporting" -require 'tmpdir' +require "tmpdir" +require "rails/secrets" module TestHelpers module Paths def app_template_path - File.join Dir.tmpdir, 'app_template' + File.join RAILS_FRAMEWORK_ROOT, "tmp/templates/app_template" end def tmp_path(*args) - @tmp_path ||= File.realpath(Dir.mktmpdir) + @tmp_path ||= File.realpath(Dir.mktmpdir(nil, File.join(RAILS_FRAMEWORK_ROOT, "tmp"))) File.join(@tmp_path, *args) end def app_path(*args) - tmp_path(*%w[app] + args) + path = tmp_path(*%w[app] + args) + if block_given? + yield path + else + path + end end def framework_path @@ -51,7 +64,9 @@ module TestHelpers old_env = ENV["RAILS_ENV"] @app ||= begin ENV["RAILS_ENV"] = env + require "#{app_path}/config/environment" + Rails.application end ensure @@ -59,8 +74,8 @@ module TestHelpers end def extract_body(response) - "".tap do |body| - response[2].each {|chunk| body << chunk } + (+"").tap do |body| + response[2].each { |chunk| body << chunk } end end @@ -69,88 +84,130 @@ module TestHelpers end def assert_welcome(resp) - assert_equal 200, resp[0] - assert_match 'text/html', resp[1]["Content-Type"] - assert_match 'charset=utf-8', resp[1]["Content-Type"] - assert extract_body(resp).match(/Welcome aboard/) - end - - def assert_success(resp) - assert_equal 202, resp[0] - end - - def assert_missing(resp) - assert_equal 404, resp[0] - end - - def assert_header(key, value, resp) - assert_equal value, resp[1][key.to_s] - end + resp = Array(resp) - def assert_body(expected, resp) - assert_equal expected, extract_body(resp) + assert_equal 200, resp[0] + assert_match "text/html", resp[1]["Content-Type"] + assert_match "charset=utf-8", resp[1]["Content-Type"] + assert extract_body(resp).match(/Yay! You.*re on Rails!/) end end module Generation # Build an application by invoking the generator and going through the whole stack. def build_app(options = {}) - @prev_rails_env = ENV['RAILS_ENV'] - ENV['RAILS_ENV'] = "development" - ENV['SECRET_KEY_BASE'] ||= SecureRandom.hex(16) + @prev_rails_env = ENV["RAILS_ENV"] + ENV["RAILS_ENV"] = "development" FileUtils.rm_rf(app_path) FileUtils.cp_r(app_template_path, app_path) # Delete the initializers unless requested unless options[:initializers] - Dir["#{app_path}/config/initializers/*.rb"].each do |initializer| + Dir["#{app_path}/config/initializers/**/*.rb"].each do |initializer| File.delete(initializer) end end - gemfile_path = "#{app_path}/Gemfile" - if options[:gemfile].blank? && File.exist?(gemfile_path) - File.delete gemfile_path - end - routes = File.read("#{app_path}/config/routes.rb") - if routes =~ /(\n\s*end\s*)\Z/ - File.open("#{app_path}/config/routes.rb", 'w') do |f| - f.puts $` + "\nmatch ':controller(/:action(/:id))(.:format)', via: :all\n" + $1 + if routes =~ /(\n\s*end\s*)\z/ + File.open("#{app_path}/config/routes.rb", "w") do |f| + f.puts $` + "\nActiveSupport::Deprecation.silence { match ':controller(/:action(/:id))(.:format)', via: :all }\n" + $1 end end - File.open("#{app_path}/config/database.yml", "w") do |f| - f.puts <<-YAML - default: &default - adapter: sqlite3 - pool: 5 - timeout: 5000 - development: - <<: *default - database: db/development.sqlite3 - test: - <<: *default - database: db/test.sqlite3 - production: - <<: *default - database: db/production.sqlite3 - YAML + if options[:multi_db] + File.open("#{app_path}/config/database.yml", "w") do |f| + f.puts <<-YAML + default: &default + adapter: sqlite3 + pool: 5 + timeout: 5000 + development: + primary: + <<: *default + database: db/development.sqlite3 + primary_readonly: + <<: *default + database: db/development.sqlite3 + replica: true + animals: + <<: *default + database: db/development_animals.sqlite3 + migrations_paths: db/animals_migrate + animals_readonly: + <<: *default + database: db/development_animals.sqlite3 + migrations_paths: db/animals_migrate + replica: true + test: + primary: + <<: *default + database: db/test.sqlite3 + primary_readonly: + <<: *default + database: db/test.sqlite3 + replica: true + animals: + <<: *default + database: db/test_animals.sqlite3 + migrations_paths: db/animals_migrate + animals_readonly: + <<: *default + database: db/test_animals.sqlite3 + migrations_paths: db/animals_migrate + replica: true + production: + primary: + <<: *default + database: db/production.sqlite3 + primary_readonly: + <<: *default + database: db/production.sqlite3 + replica: true + animals: + <<: *default + database: db/production_animals.sqlite3 + migrations_paths: db/animals_migrate + animals_readonly: + <<: *default + database: db/production_animals.sqlite3 + migrations_paths: db/animals_migrate + readonly: true + YAML + end + else + File.open("#{app_path}/config/database.yml", "w") do |f| + f.puts <<-YAML + default: &default + adapter: sqlite3 + pool: 5 + timeout: 5000 + development: + <<: *default + database: db/development.sqlite3 + test: + <<: *default + database: db/test.sqlite3 + production: + <<: *default + database: db/production.sqlite3 + YAML + end end add_to_config <<-RUBY config.eager_load = false config.session_store :cookie_store, key: "_myapp_session" config.active_support.deprecation = :log - config.active_support.test_order = :random config.action_controller.allow_forgery_protection = false config.log_level = :info RUBY end def teardown_app - ENV['RAILS_ENV'] = @prev_rails_env if @prev_rails_env + ENV["RAILS_ENV"] = @prev_rails_env if @prev_rails_env + FileUtils.rm_rf(tmp_path) end # Make a very basic app, without creating the whole directory structure. @@ -160,22 +217,22 @@ module TestHelpers require "action_controller/railtie" require "action_view/railtie" - app = Class.new(Rails::Application) - app.config.eager_load = false - app.secrets.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4" - app.config.session_store :cookie_store, key: "_myapp_session" - app.config.active_support.deprecation = :log - app.config.active_support.test_order = :random - app.config.log_level = :info + @app = Class.new(Rails::Application) do + def self.name; "RailtiesTestApp"; end + end + @app.config.eager_load = false + @app.config.session_store :cookie_store, key: "_myapp_session" + @app.config.active_support.deprecation = :log + @app.config.log_level = :info - yield app if block_given? - app.initialize! + yield @app if block_given? + @app.initialize! - app.routes.draw do + @app.routes.draw do get "/" => "omg#index" end - require 'rack/test' + require "rack/test" extend ::Rack::Test::Methods end @@ -183,12 +240,12 @@ module TestHelpers controller :foo, <<-RUBY class FooController < ApplicationController def index - render text: "foo" + render plain: "foo" end end RUBY - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get ':controller(/:action)' end @@ -205,7 +262,7 @@ module TestHelpers def write(file, string) path = "#{@path}/#{file}" FileUtils.mkdir_p(File.dirname(path)) - File.open(path, "w") {|f| f.puts string } + File.open(path, "w") { |f| f.puts string } end def delete(file) @@ -218,10 +275,10 @@ module TestHelpers FileUtils.mkdir_p(dir) app = File.readlines("#{app_path}/config/application.rb") - app.insert(2, "$:.unshift(\"#{dir}/lib\")") - app.insert(3, "require #{name.inspect}") + app.insert(4, "$:.unshift(\"#{dir}/lib\")") + app.insert(5, "require #{name.inspect}") - File.open("#{app_path}/config/application.rb", 'r+') do |f| + File.open("#{app_path}/config/application.rb", "r+") do |f| f.puts app end @@ -230,16 +287,92 @@ module TestHelpers end end - def script(script) - Dir.chdir(app_path) do - `#{Gem.ruby} #{app_path}/bin/rails #{script}` + # Invoke a bin/rails command inside the app + # + # allow_failure:: true to return normally if the command exits with + # a non-zero status. By default, this method will raise. + # stderr:: true to pass STDERR output straight to the "real" STDERR. + # By default, the STDERR and STDOUT of the process will be + # combined in the returned string. + def rails(*args, allow_failure: false, stderr: false) + args = args.flatten + fork = true + + command = "bin/rails #{Shellwords.join args}#{' 2>&1' unless stderr}" + + # Don't fork if the environment has disabled it + fork = false if ENV["NO_FORK"] + + # Don't fork if the runtime isn't able to + fork = false if !Process.respond_to?(:fork) + + # Don't fork if we're re-invoking minitest + fork = false if args.first == "t" || args.grep(/\Atest(:|\z)/).any? + + if fork + out_read, out_write = IO.pipe + if stderr + err_read, err_write = IO.pipe + else + err_write = out_write + end + + pid = fork do + out_read.close + err_read.close if err_read + + $stdin.reopen(File::NULL, "r") + $stdout.reopen(out_write) + $stderr.reopen(err_write) + + at_exit do + case $! + when SystemExit + exit! $!.status + when nil + exit! 0 + else + err_write.puts "#{$!.class}: #{$!}" + exit! 1 + end + end + + Rails.instance_variable_set :@_env, nil + + $-v = $-w = false + Dir.chdir app_path unless Dir.pwd == app_path + + ARGV.replace(args) + load "./bin/rails" + + exit! 0 + end + + out_write.close + + if err_read + err_write.close + + $stderr.write err_read.read + end + + output = out_read.read + + Process.waitpid pid + + else + output = `cd #{app_path}; #{command}` end + + raise "rails command failed (#{$?.exitstatus}): #{command}\n#{output}" unless allow_failure || $?.success? + + output end def add_to_top_of_config(str) environment = File.read("#{app_path}/config/application.rb") if environment =~ /(Rails::Application\s*)/ - File.open("#{app_path}/config/application.rb", 'w') do |f| + File.open("#{app_path}/config/application.rb", "w") do |f| f.puts $` + $1 + "\n#{str}\n" + $' end end @@ -247,8 +380,8 @@ module TestHelpers def add_to_config(str) environment = File.read("#{app_path}/config/application.rb") - if environment =~ /(\n\s*end\s*end\s*)\Z/ - File.open("#{app_path}/config/application.rb", 'w') do |f| + if environment =~ /(\n\s*end\s*end\s*)\z/ + File.open("#{app_path}/config/application.rb", "w") do |f| f.puts $` + "\n#{str}\n" + $1 end end @@ -256,25 +389,34 @@ module TestHelpers def add_to_env_config(env, str) environment = File.read("#{app_path}/config/environments/#{env}.rb") - if environment =~ /(\n\s*end\s*)\Z/ - File.open("#{app_path}/config/environments/#{env}.rb", 'w') do |f| + if environment =~ /(\n\s*end\s*)\z/ + File.open("#{app_path}/config/environments/#{env}.rb", "w") do |f| f.puts $` + "\n#{str}\n" + $1 end end end def remove_from_config(str) - file = "#{app_path}/config/application.rb" + remove_from_file("#{app_path}/config/application.rb", str) + end + + def remove_from_env_config(env, str) + remove_from_file("#{app_path}/config/environments/#{env}.rb", str) + end + + def remove_from_file(file, str) contents = File.read(file) contents.sub!(/#{str}/, "") - File.open(file, "w+") { |f| f.puts contents } + File.write(file, contents) end - def app_file(path, contents, mode = 'w') - FileUtils.mkdir_p File.dirname("#{app_path}/#{path}") - File.open("#{app_path}/#{path}", mode) do |f| + def app_file(path, contents, mode = "w") + file_name = "#{app_path}/#{path}" + FileUtils.mkdir_p File.dirname(file_name) + File.open(file_name, mode) do |f| f.puts contents end + file_name end def remove_file(path) @@ -286,17 +428,28 @@ module TestHelpers end def use_frameworks(arr) - to_remove = [:actionmailer, :activerecord] - arr + to_remove = [:actionmailer, :activerecord, :activestorage, :activejob] - arr if to_remove.include?(:activerecord) - remove_from_config 'config.active_record.*' + remove_from_config "config.active_record.*" end - $:.reject! {|path| path =~ %r'/(#{to_remove.join('|')})/' } + $:.reject! { |path| path =~ %r'/(#{to_remove.join('|')})/' } end - def boot_rails - require File.expand_path('../../../../load_paths', __FILE__) + def use_postgresql + File.open("#{app_path}/config/database.yml", "w") do |f| + f.puts <<-YAML + default: &default + adapter: postgresql + pool: 5 + database: railties_test + development: + <<: *default + test: + <<: *default + YAML + end end end end @@ -306,9 +459,11 @@ class ActiveSupport::TestCase include TestHelpers::Rack include TestHelpers::Generation include ActiveSupport::Testing::Stream + include ActiveSupport::Testing::MethodCallAssertions - self.test_order = :sorted - + def frozen_error_class + Object.const_defined?(:FrozenError) ? FrozenError : RuntimeError + end end # Create a scope and build a fixture rails app @@ -317,14 +472,42 @@ Module.new do # Build a rails app FileUtils.rm_rf(app_template_path) - FileUtils.mkdir(app_template_path) + FileUtils.mkdir_p(app_template_path) - environment = File.expand_path('../../../../load_paths', __FILE__) - require_environment = "-r #{environment}" + Dir.chdir "#{RAILS_FRAMEWORK_ROOT}/actionview" do + `yarn build` + end - `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails new #{app_template_path} --skip-gemfile --no-rc` - File.open("#{app_template_path}/config/boot.rb", 'w') do |f| - f.puts "require '#{environment}'" + `#{Gem.ruby} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails new #{app_template_path} --skip-gemfile --skip-listen --no-rc` + File.open("#{app_template_path}/config/boot.rb", "w") do |f| f.puts "require 'rails/all'" end + + Dir.chdir(app_template_path) { `yarn add https://github.com/rails/webpacker.git` } # Use the latest version. + + # Manually install `webpack` as bin symlinks are not created for subdependencies + # 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` } + + # 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. + contents = File.read("#{app_template_path}/config/application.rb") + contents.sub!(/^Bundler\.require.*/, "%w(turbolinks webpacker).each { |r| require r }") + File.write("#{app_template_path}/config/application.rb", contents) + + require "rails" + + require "active_model" + require "active_job" + require "active_record" + require "action_controller" + require "action_mailer" + require "action_view" + require "active_storage" + require "action_cable" + require "sprockets" + + require "action_view/helpers" + require "action_dispatch/routing/route_set" end unless defined?(RAILS_ISOLATED_ENGINE) |