Merge branch 'master' into combine-structure-and-schema-tasks

This commit is contained in:
Eileen M. Uchitelle 2020-08-17 08:47:51 -04:00 committed by GitHub
commit 9a36b50b84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
85 changed files with 1114 additions and 780 deletions

View File

@ -21,7 +21,7 @@ https://buildkite.com/rails/rails
Sam Ruby keeps a [test suite](https://github.com/rubys/awdwr) that makes
sure the code samples in his book
([Agile Web Development with Rails](https://pragprog.com/book/rails51/agile-web-development-with-rails-51))
([Agile Web Development with Rails](https://pragprog.com/titles/rails6))
all work. These are valuable system tests
for Rails. You can check the status of these tests here:

View File

@ -1,3 +1,7 @@
* Allow `assert_recognizes` routing assertions to work on mounted root routes.
*Gannon McGibbon*
* Change default redirection status code for non-GET/HEAD requests to 308 Permanent Redirect for `ActionDispatch::SSL`.
*Alan Tan*, *Oz Ben-David*

View File

@ -66,7 +66,8 @@ module ActionDispatch
find_routes(rails_req).each do |match, parameters, route|
unless route.path.anchored
rails_req.script_name = match.to_s
rails_req.path_info = match.post_match.sub(/^([^\/])/, '/\1')
rails_req.path_info = match.post_match
rails_req.path_info = "/" + rails_req.path_info unless rails_req.path_info.start_with? "/"
end
parameters = route.defaults.merge parameters

View File

@ -199,7 +199,8 @@ module ActionDispatch
method = :get
end
request = ActionController::TestRequest.create @controller.class
controller = @controller if defined?(@controller)
request = ActionController::TestRequest.create controller&.class
if %r{://}.match?(path)
fail_on(URI::InvalidURIError, msg) do

View File

@ -14,11 +14,22 @@ class QueryBooksController < BooksController; end
class RoutingAssertionsTest < ActionController::TestCase
def setup
root_engine = Class.new(Rails::Engine) do
def self.name
"root_engine"
end
end
root_engine.routes.draw do
root to: "books#index"
end
engine = Class.new(Rails::Engine) do
def self.name
"blog_engine"
end
end
engine.routes.draw do
resources :books
@ -53,6 +64,8 @@ class RoutingAssertionsTest < ActionController::TestCase
mount engine => "/shelf"
mount root_engine => "/"
get "/shelf/foo", controller: "query_articles", action: "index"
end
end
@ -118,6 +131,10 @@ class RoutingAssertionsTest < ActionController::TestCase
assert_recognizes({ controller: "books", action: "show", id: "1" }, "/shelf/books/1")
end
def test_assert_recognizes_with_engine_at_root
assert_recognizes({ controller: "books", action: "index" }, "/")
end
def test_assert_recognizes_with_engine_and_extras
assert_recognizes({ controller: "books", action: "index", page: "1" }, "/shelf/books", page: "1")
end

View File

@ -190,6 +190,14 @@ module ActionView #:nodoc:
# correctly.
define_method(:compiled_method_container) { subclass }
define_singleton_method(:compiled_method_container) { subclass }
def self.name
superclass.name
end
def inspect
"#<#{self.class.name}:#{'%#016x' % (object_id << 1)}>"
end
}
end

View File

@ -86,11 +86,11 @@ module ActionView
def javascript_include_tag(*sources)
options = sources.extract_options!.stringify_keys
path_options = options.extract!("protocol", "extname", "host", "skip_pipeline").symbolize_keys
early_hints_links = []
preload_links = []
sources_tags = sources.uniq.map { |source|
href = path_to_javascript(source, path_options)
early_hints_links << "<#{href}>; rel=preload; as=script"
preload_links << "<#{href}>; rel=preload; as=script"
tag_options = {
"src" => href
}.merge!(options)
@ -100,7 +100,7 @@ module ActionView
content_tag("script", "", tag_options)
}.join("\n").html_safe
request.send_early_hints("Link" => early_hints_links.join("\n")) if respond_to?(:request) && request
send_preload_links_header(preload_links)
sources_tags
end
@ -136,11 +136,11 @@ module ActionView
def stylesheet_link_tag(*sources)
options = sources.extract_options!.stringify_keys
path_options = options.extract!("protocol", "host", "skip_pipeline").symbolize_keys
early_hints_links = []
preload_links = []
sources_tags = sources.uniq.map { |source|
href = path_to_stylesheet(source, path_options)
early_hints_links << "<#{href}>; rel=preload; as=style"
preload_links << "<#{href}>; rel=preload; as=style"
tag_options = {
"rel" => "stylesheet",
"media" => "screen",
@ -149,7 +149,7 @@ module ActionView
tag(:link, tag_options)
}.join("\n").html_safe
request.send_early_hints("Link" => early_hints_links.join("\n")) if respond_to?(:request) && request
send_preload_links_header(preload_links)
sources_tags
end
@ -281,12 +281,12 @@ module ActionView
crossorigin: crossorigin
}.merge!(options.symbolize_keys))
early_hints_link = "<#{href}>; rel=preload; as=#{as_type}"
early_hints_link += "; type=#{mime_type}" if mime_type
early_hints_link += "; crossorigin=#{crossorigin}" if crossorigin
early_hints_link += "; nopush" if nopush
preload_link = "<#{href}>; rel=preload; as=#{as_type}"
preload_link += "; type=#{mime_type}" if mime_type
preload_link += "; crossorigin=#{crossorigin}" if crossorigin
preload_link += "; nopush" if nopush
request.send_early_hints("Link" => early_hints_link) if respond_to?(:request) && request
send_preload_links_header([preload_link])
link_tag
end
@ -482,6 +482,16 @@ module ActionView
type
end
end
def send_preload_links_header(preload_links)
if respond_to?(:request) && request
request.send_early_hints("Link" => preload_links.join("\n"))
end
if respond_to?(:response) && response
response.headers["Link"] = [response.headers["Link"].presence, *preload_links].compact.join(",")
end
end
end
end
end

View File

@ -84,7 +84,7 @@ class RoutedRackApp
end
class BasicController
attr_accessor :request
attr_accessor :request, :response
def config
@config ||= ActiveSupport::InheritableOptions.new(ActionController::Base.config).tap do |config|
@ -151,7 +151,7 @@ module ActionController
define_method(:setup) do
super()
@routes = routes
@controller.singleton_class.include @routes.url_helpers
@controller.singleton_class.include @routes.url_helpers if @controller
end
}
routes

View File

@ -9,23 +9,33 @@ ActionView::Template::Types.delegate_to Mime
class AssetTagHelperTest < ActionView::TestCase
tests ActionView::Helpers::AssetTagHelper
attr_reader :request
attr_reader :request, :response
class FakeRequest
attr_accessor :script_name
def protocol() "http://" end
def ssl?() false end
def host_with_port() "localhost" end
def base_url() "http://www.example.com" end
def send_early_hints(links) end
end
class FakeResponse
def headers
@headers ||= {}
end
end
def setup
super
@controller = BasicController.new
@request = Class.new do
attr_accessor :script_name
def protocol() "http://" end
def ssl?() false end
def host_with_port() "localhost" end
def base_url() "http://www.example.com" end
def send_early_hints(links) end
end.new
@request = FakeRequest.new
@controller.request = @request
@response = FakeResponse.new
@controller.response = @response
end
def url_for(*args)
@ -499,6 +509,14 @@ class AssetTagHelperTest < ActionView::TestCase
assert_dom_equal %(<script src="/javascripts/foo.js"></script>), javascript_include_tag("foo.js")
end
def test_should_set_preload_links
stylesheet_link_tag("http://example.com/style.css")
javascript_include_tag("http://example.com/all.js")
expected = "<http://example.com/style.css>; rel=preload; as=style,<http://example.com/all.js>; rel=preload; as=script"
assert_equal expected, @response.headers["Link"]
end
def test_image_path
ImagePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end

View File

@ -327,6 +327,11 @@ module RenderTestCases
assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name
end
def test_undefined_method_error_references_named_class
e = assert_raises(ActionView::Template::Error) { @view.render(inline: "<%= undefined %>") }
assert_match(/`undefined' for #<ActionView::Base:0x[0-9a-f]+>/, e.message)
end
def test_render_object
assert_equal "Hello: david", @view.render(partial: "test/customer", object: Customer.new("david"))
assert_equal "FalseClass", @view.render(partial: "test/klass", object: false)

View File

@ -4,6 +4,26 @@
*fatkodima*
* Respect the `select` values for eager loading.
```ruby
post = Post.select("UPPER(title) AS title").first
post.title # => "WELCOME TO THE WEBLOG"
post.body # => ActiveModel::MissingAttributeError
# Rails 6.0 (ignore the `select` values)
post = Post.select("UPPER(title) AS title").eager_load(:comments).first
post.title # => "Welcome to the weblog"
post.body # => "Such a lovely day"
# Rails 6.1 (respect the `select` values)
post = Post.select("UPPER(title) AS title").eager_load(:comments).first
post.title # => "WELCOME TO THE WEBLOG"
post.body # => ActiveModel::MissingAttributeError
```
*Ryuta Kamizono*
* Allow attribute's default to be configured but keeping its own type.
```ruby

View File

@ -36,7 +36,6 @@ require "active_record/errors"
module ActiveRecord
extend ActiveSupport::Autoload
autoload :AdvisoryLockBase
autoload :Base
autoload :Callbacks
autoload :Core

View File

@ -1,18 +0,0 @@
# frozen_string_literal: true
module ActiveRecord
# This class is used to create a connection that we can use for advisory
# locks. This will take out a "global" lock that can't be accidentally
# removed if a new connection is established during a migration.
class AdvisoryLockBase < ActiveRecord::Base # :nodoc:
self.abstract_class = true
self.connection_specification_name = "AdvisoryLockBase"
class << self
def _internal?
true
end
end
end
end

View File

@ -52,7 +52,7 @@ module ActiveRecord
attr_reader :value_transformation
def join(table, constraint)
table.create_join(table, table.create_on(constraint), Arel::Nodes::LeadingJoin)
Arel::Nodes::LeadingJoin.new(table, Arel::Nodes::On.new(constraint))
end
def last_chain_scope(scope, reflection, owner)

View File

@ -34,7 +34,7 @@ module ActiveRecord
Table = Struct.new(:node, :columns) do # :nodoc:
def column_aliases
t = node.table
columns.map { |column| t[column.name].as Arel.sql column.alias }
columns.map { |column| t[column.name].as(column.alias) }
end
end
Column = Struct.new(:name, :alias)
@ -80,6 +80,7 @@ module ActiveRecord
def join_constraints(joins_to_add, alias_tracker)
@alias_tracker = alias_tracker
@joined_tables = {}
joins = make_join_constraints(join_root, join_type)
@ -105,13 +106,20 @@ module ActiveRecord
parents = model_cache[join_root]
column_aliases = aliases.column_aliases(join_root)
column_names = explicit_selections(column_aliases, result_set)
column_names = []
result_set.columns.each do |name|
column_names << name unless /\At\d+_r\d+\z/.match?(name)
end
if column_names.empty?
column_types = {}
else
column_types = result_set.column_types
column_types = column_types.slice(*column_names) unless column_types.empty?
unless column_types.empty?
attribute_types = join_root.attribute_types
column_types = column_types.slice(*column_names).delete_if { |k, _| attribute_types.key?(k) }
end
column_aliases += column_names.map! { |name| Aliases::Column.new(name, name) }
end
@ -134,6 +142,7 @@ module ActiveRecord
end
def apply_column_aliases(relation)
@join_root_alias = relation.select_values.empty?
relation._select!(-> { aliases.columns })
end
@ -145,18 +154,18 @@ module ActiveRecord
attr_reader :join_root, :join_type
private
attr_reader :alias_tracker
def explicit_selections(root_column_aliases, result_set)
root_names = root_column_aliases.map(&:name).to_set
result_set.columns.each_with_object([]) do |name, result|
result << name unless /\At\d+_r\d+\z/.match?(name) || root_names.include?(name)
end
end
attr_reader :alias_tracker, :join_root_alias
def aliases
@aliases ||= Aliases.new join_root.each_with_index.map { |join_part, i|
columns = join_part.column_names.each_with_index.map { |column_name, j|
column_names = if join_part == join_root && !join_root_alias
primary_key = join_root.primary_key
primary_key ? [primary_key] : []
else
join_part.column_names
end
columns = column_names.each_with_index.map { |column_name, j|
Aliases::Column.new column_name, "t#{i}_r#{j}"
}
Aliases::Table.new(join_part, columns)
@ -173,15 +182,22 @@ module ActiveRecord
foreign_table = parent.table
foreign_klass = parent.base_klass
child.join_constraints(foreign_table, foreign_klass, join_type, alias_tracker) do |reflection|
alias_tracker.aliased_table_for(reflection.klass.arel_table) do
table_alias_for(reflection, parent, reflection != child.reflection)
end
end.concat child.children.flat_map { |c| make_constraints(child, c, join_type) }
end
table, terminated = @joined_tables[reflection]
root = reflection == child.reflection
def table_alias_for(reflection, parent, join)
name = reflection.alias_candidate(parent.table_name)
join ? "#{name}_join" : name
if table && (!root || !terminated)
@joined_tables[reflection] = [table, root] if root
next table, true
end
table = alias_tracker.aliased_table_for(reflection.klass.arel_table) do
name = reflection.alias_candidate(parent.table_name)
root ? name : "#{name}_join"
end
@joined_tables[reflection] ||= [table, root] if join_type == Arel::Nodes::OuterJoin
table
end.concat child.children.flat_map { |c| make_constraints(child, c, join_type) }
end
def walk(left, right, join_type)

View File

@ -21,15 +21,25 @@ module ActiveRecord
super && reflection == other.reflection
end
def join_constraints(foreign_table, foreign_klass, join_type, alias_tracker, &block)
def join_constraints(foreign_table, foreign_klass, join_type, alias_tracker)
joins = []
tables = reflection.chain.map(&block)
@table = tables.first
chain = []
reflection.chain.each do |reflection|
table, terminated = yield reflection
@table ||= table
if terminated
foreign_table, foreign_klass = table, reflection.klass
break
end
chain << [reflection, table]
end
# The chain starts with the target table, but we want to end with it here (makes
# more sense in this context), so we reverse
reflection.chain.reverse_each.with_index(1) do |reflection, i|
table = tables[-i]
chain.reverse_each do |reflection, table|
klass = reflection.klass
join_scope = reflection.join_scope(table, foreign_table, foreign_klass)
@ -50,7 +60,7 @@ module ActiveRecord
end
end
joins << table.create_join(table, table.create_on(nodes), join_type)
joins << join_type.new(table, Arel::Nodes::On.new(nodes))
if others && !others.empty?
joins.concat arel.join_sources
@ -79,7 +89,7 @@ module ActiveRecord
private
def append_constraints(join, constraints)
if join.is_a?(Arel::Nodes::StringJoin)
join_string = table.create_and(constraints.unshift(join.left))
join_string = Arel::Nodes::And.new(constraints.unshift join.left)
join.left = Arel.sql(base_klass.connection.visitor.compile(join_string))
else
right = join.right

View File

@ -17,7 +17,7 @@ module ActiveRecord
# association.
attr_reader :base_klass, :children
delegate :table_name, :column_names, :primary_key, to: :base_klass
delegate :table_name, :column_names, :primary_key, :attribute_types, to: :base_klass
def initialize(base_klass, children)
@base_klass = base_klass

View File

@ -96,6 +96,10 @@ module ActiveRecord
end
end
def initialize(associate_by_default: true)
@associate_by_default = associate_by_default
end
private
# Loads all the given data into +records+ for the +association+.
def preloaders_on(association, records, scope, polymorphic_parent = false)
@ -144,7 +148,7 @@ module ActiveRecord
def preloaders_for_reflection(reflection, records, scope)
records.group_by { |record| record.association(reflection.name).klass }.map do |rhs_klass, rs|
preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope).run
preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope, @associate_by_default).run
end
end
@ -159,7 +163,7 @@ module ActiveRecord
end
class AlreadyLoaded # :nodoc:
def initialize(klass, owners, reflection, preload_scope)
def initialize(klass, owners, reflection, preload_scope, associate_by_default = true)
@owners = owners
@reflection = reflection
end

View File

@ -4,25 +4,22 @@ module ActiveRecord
module Associations
class Preloader
class Association #:nodoc:
def initialize(klass, owners, reflection, preload_scope)
def initialize(klass, owners, reflection, preload_scope, associate_by_default = true)
@klass = klass
@owners = owners.uniq(&:__id__)
@reflection = reflection
@preload_scope = preload_scope
@associate = associate_by_default || !preload_scope || preload_scope.empty_scope?
@model = owners.first && owners.first.class
end
def run
if !preload_scope || preload_scope.empty_scope?
owners.each do |owner|
associate_records_to_owner(owner, records_by_owner[owner] || [])
end
else
# Custom preload scope is used and
# the association cannot be marked as loaded
# Loading into a Hash instead
records_by_owner
end
records = records_by_owner
owners.each do |owner|
associate_records_to_owner(owner, records[owner] || [])
end if @associate
self
end

View File

@ -4,7 +4,7 @@ module ActiveRecord
module Associations
class Preloader
class ThroughAssociation < Association # :nodoc:
PRELOADER = ActiveRecord::Associations::Preloader.new
PRELOADER = ActiveRecord::Associations::Preloader.new(associate_by_default: false)
def initialize(*)
super

View File

@ -1027,11 +1027,11 @@ module ActiveRecord
end
def connection_pool_list
owner_to_pool_manager.values.compact.flat_map { |m| m.pool_configs.map(&:pool) }
owner_to_pool_manager.values.flat_map { |m| m.pool_configs.map(&:pool) }
end
alias :connection_pools :connection_pool_list
def establish_connection(config, pool_key = Base.default_pool_key, owner_name = Base.name)
def establish_connection(config, owner_name: Base.name, shard: Base.default_shard)
owner_name = config.to_s if config.is_a?(Symbol)
pool_config = resolve_pool_config(config, owner_name)
@ -1040,7 +1040,7 @@ module ActiveRecord
# Protects the connection named `ActiveRecord::Base` from being removed
# if the user calls `establish_connection :primary`.
if owner_to_pool_manager.key?(pool_config.connection_specification_name)
remove_connection_pool(pool_config.connection_specification_name, pool_key)
remove_connection_pool(pool_config.connection_specification_name, shard: shard)
end
message_bus = ActiveSupport::Notifications.instrumenter
@ -1052,7 +1052,7 @@ module ActiveRecord
owner_to_pool_manager[pool_config.connection_specification_name] ||= PoolManager.new
pool_manager = get_pool_manager(pool_config.connection_specification_name)
pool_manager.set_pool_config(pool_key, pool_config)
pool_manager.set_pool_config(shard, pool_config)
message_bus.instrument("!connection.active_record", payload) do
pool_config.pool
@ -1094,12 +1094,12 @@ module ActiveRecord
# active or defined connection: if it is the latter, it will be
# opened and set as the active connection for the class it was defined
# for (not necessarily the current class).
def retrieve_connection(spec_name, pool_key = ActiveRecord::Base.default_pool_key) # :nodoc:
pool = retrieve_connection_pool(spec_name, pool_key)
def retrieve_connection(spec_name, shard: ActiveRecord::Base.default_shard) # :nodoc:
pool = retrieve_connection_pool(spec_name, shard: shard)
unless pool
if pool_key != ActiveRecord::Base.default_pool_key
message = "No connection pool for '#{spec_name}' found for the '#{pool_key}' shard."
if shard != ActiveRecord::Base.default_shard
message = "No connection pool for '#{spec_name}' found for the '#{shard}' shard."
elsif ActiveRecord::Base.connection_handler != ActiveRecord::Base.default_connection_handler
message = "No connection pool for '#{spec_name}' found for the '#{ActiveRecord::Base.current_role}' role."
else
@ -1114,8 +1114,8 @@ module ActiveRecord
# Returns true if a connection that's accessible to this class has
# already been opened.
def connected?(spec_name, pool_key = ActiveRecord::Base.default_pool_key)
pool = retrieve_connection_pool(spec_name, pool_key)
def connected?(spec_name, shard: ActiveRecord::Base.default_shard)
pool = retrieve_connection_pool(spec_name, shard: shard)
pool && pool.connected?
end
@ -1123,14 +1123,14 @@ module ActiveRecord
# connection and the defined connection (if they exist). The result
# can be used as an argument for #establish_connection, for easily
# re-establishing the connection.
def remove_connection(owner, pool_key = ActiveRecord::Base.default_pool_key)
remove_connection_pool(owner, pool_key)&.configuration_hash
def remove_connection(owner, shard: ActiveRecord::Base.default_shard)
remove_connection_pool(owner, shard: shard)&.configuration_hash
end
deprecate remove_connection: "Use #remove_connection_pool, which now returns a DatabaseConfig object instead of a Hash"
def remove_connection_pool(owner, pool_key = ActiveRecord::Base.default_pool_key)
def remove_connection_pool(owner, shard: ActiveRecord::Base.default_shard)
if pool_manager = get_pool_manager(owner)
pool_config = pool_manager.remove_pool_config(pool_key)
pool_config = pool_manager.remove_pool_config(shard)
if pool_config
pool_config.disconnect!
@ -1142,8 +1142,8 @@ module ActiveRecord
# Retrieving the connection pool happens a lot, so we cache it in @owner_to_pool_manager.
# This makes retrieving the connection pool O(1) once the process is warm.
# When a connection is established or removed, we invalidate the cache.
def retrieve_connection_pool(owner, pool_key = ActiveRecord::Base.default_pool_key)
pool_config = get_pool_manager(owner)&.get_pool_config(pool_key)
def retrieve_connection_pool(owner, shard: ActiveRecord::Base.default_shard)
pool_config = get_pool_manager(owner)&.get_pool_config(shard)
pool_config&.pool
end

View File

@ -49,7 +49,7 @@ module ActiveRecord
def establish_connection(config_or_env = nil)
config_or_env ||= DEFAULT_ENV.call.to_sym
db_config, owner_name = resolve_config_for_connection(config_or_env)
connection_handler.establish_connection(db_config, current_pool_key, owner_name)
connection_handler.establish_connection(db_config, owner_name: owner_name, shard: current_shard)
end
# Connects a model to the databases specified. The +database+ keyword
@ -89,15 +89,15 @@ module ActiveRecord
db_config, owner_name = resolve_config_for_connection(database_key)
handler = lookup_connection_handler(role.to_sym)
connections << handler.establish_connection(db_config, default_pool_key, owner_name)
connections << handler.establish_connection(db_config, owner_name: owner_name)
end
shards.each do |pool_key, database_keys|
shards.each do |shard, database_keys|
database_keys.each do |role, database_key|
db_config, owner_name = resolve_config_for_connection(database_key)
handler = lookup_connection_handler(role.to_sym)
connections << handler.establish_connection(db_config, pool_key.to_sym, owner_name)
connections << handler.establish_connection(db_config, owner_name: owner_name, shard: shard.to_sym)
end
end
@ -154,7 +154,7 @@ module ActiveRecord
db_config, owner_name = resolve_config_for_connection(database)
handler = lookup_connection_handler(role)
handler.establish_connection(db_config, default_pool_key, owner_name)
handler.establish_connection(db_config, owner_name: owner_name)
with_handler(role, &blk)
elsif shard
@ -172,8 +172,8 @@ module ActiveRecord
# ActiveRecord::Base.connected_to?(role: :writing) #=> true
# ActiveRecord::Base.connected_to?(role: :reading) #=> false
# end
def connected_to?(role:, shard: ActiveRecord::Base.default_pool_key)
current_role == role.to_sym && current_pool_key == shard.to_sym
def connected_to?(role:, shard: ActiveRecord::Base.default_shard)
current_role == role.to_sym && current_shard == shard.to_sym
end
# Returns the symbol representing the current connected role.
@ -247,16 +247,16 @@ module ActiveRecord
end
def connection_pool
connection_handler.retrieve_connection_pool(connection_specification_name, current_pool_key) || raise(ConnectionNotEstablished)
connection_handler.retrieve_connection_pool(connection_specification_name, shard: current_shard) || raise(ConnectionNotEstablished)
end
def retrieve_connection
connection_handler.retrieve_connection(connection_specification_name, current_pool_key)
connection_handler.retrieve_connection(connection_specification_name, shard: current_shard)
end
# Returns +true+ if Active Record is connected.
def connected?
connection_handler.connected?(connection_specification_name, current_pool_key)
connection_handler.connected?(connection_specification_name, shard: current_shard)
end
def remove_connection(name = nil)
@ -264,11 +264,11 @@ module ActiveRecord
# if removing a connection that has a pool, we reset the
# connection_specification_name so it will use the parent
# pool.
if connection_handler.retrieve_connection_pool(name, current_pool_key)
if connection_handler.retrieve_connection_pool(name, shard: current_shard)
self.connection_specification_name = nil
end
connection_handler.remove_connection_pool(name, current_pool_key)
connection_handler.remove_connection_pool(name, shard: current_shard)
end
def clear_cache! # :nodoc:
@ -302,15 +302,15 @@ module ActiveRecord
end
end
def with_shard(pool_key, role, prevent_writes)
old_pool_key = current_pool_key
def with_shard(shard, role, prevent_writes)
old_shard = current_shard
with_role(role, prevent_writes) do
self.current_pool_key = pool_key
self.current_shard = shard
yield
end
ensure
self.current_pool_key = old_pool_key
self.current_shard = old_shard
end
def swap_connection_handler(handler, &blk) # :nodoc:

View File

@ -135,7 +135,7 @@ module ActiveRecord
class_attribute :default_connection_handler, instance_writer: false
class_attribute :default_pool_key, instance_writer: false
class_attribute :default_shard, instance_writer: false
self.filter_attributes = []
@ -147,16 +147,16 @@ module ActiveRecord
Thread.current.thread_variable_set(:ar_connection_handler, handler)
end
def self.current_pool_key
Thread.current.thread_variable_get(:ar_pool_key) || default_pool_key
def self.current_shard
Thread.current.thread_variable_get(:ar_shard) || default_shard
end
def self.current_pool_key=(pool_key)
Thread.current.thread_variable_set(:ar_pool_key, pool_key)
def self.current_shard=(shard)
Thread.current.thread_variable_set(:ar_shard, shard)
end
self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new
self.default_pool_key = :default
self.default_shard = :default
end
module ClassMethods

View File

@ -136,7 +136,6 @@ module ActiveRecord
end
def deserialize(value)
return if value.nil?
mapping.key(subtype.deserialize(value))
end

View File

@ -1376,20 +1376,29 @@ module ActiveRecord
def with_advisory_lock
lock_id = generate_migrator_advisory_lock_id
AdvisoryLockBase.establish_connection(ActiveRecord::Base.connection_db_config) unless AdvisoryLockBase.connected?
connection = AdvisoryLockBase.connection
got_lock = connection.get_advisory_lock(lock_id)
raise ConcurrentMigrationError unless got_lock
load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock
yield
ensure
if got_lock && !connection.release_advisory_lock(lock_id)
raise ConcurrentMigrationError.new(
ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE
)
with_advisory_lock_connection do |connection|
got_lock = connection.get_advisory_lock(lock_id)
raise ConcurrentMigrationError unless got_lock
load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock
yield
ensure
if got_lock && !connection.release_advisory_lock(lock_id)
raise ConcurrentMigrationError.new(
ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE
)
end
end
end
def with_advisory_lock_connection
pool = ActiveRecord::ConnectionAdapters::ConnectionHandler.new.establish_connection(
ActiveRecord::Base.connection_db_config
)
pool.with_connection { |connection| yield(connection) }
end
MIGRATOR_SALT = 2053462845
def generate_migrator_advisory_lock_id
db_name_hash = Zlib.crc32(Base.connection.current_database)

View File

@ -1252,7 +1252,7 @@ module ActiveRecord
end
joins.each_with_index do |join, i|
joins[i] = table.create_string_join(Arel.sql(join.strip)) if join.is_a?(String)
joins[i] = Arel::Nodes::StringJoin.new(Arel.sql(join.strip)) if join.is_a?(String)
end
while joins.first.is_a?(Arel::Nodes::Join)

View File

@ -14,9 +14,9 @@ module ActiveRecord
class RuntimeRegistry # :nodoc:
extend ActiveSupport::PerThreadRegistry
attr_accessor :connection_handler, :sql_runtime
attr_accessor :sql_runtime
[:connection_handler, :sql_runtime].each do |val|
[:sql_runtime].each do |val|
class_eval %{ def self.#{val}; instance.#{val}; end }, __FILE__, __LINE__
class_eval %{ def self.#{val}=(x); instance.#{val}=x; end }, __FILE__, __LINE__
end

View File

@ -316,7 +316,7 @@ data_sources:
triangles: true
non_poly_ones: true
non_poly_twos: true
men: true
humans: true
faces: true
interests: true
zines: true

View File

@ -120,7 +120,7 @@ class Sink < ActiveRecord::Base
end
class Source < ActiveRecord::Base
self.table_name = "men"
self.table_name = "humans"
has_and_belongs_to_many :sinks, join_table: :edges
end

View File

@ -1069,6 +1069,18 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal expected, Author.eager_load(:lazy_readers_skimmers_or_not_2).last.lazy_readers_skimmers_or_not_2
end
def test_duplicated_has_many_through_with_join_scope
Categorization.create!(author: authors(:david), post: posts(:thinking), category: categories(:technology))
expected = [categorizations(:david_welcome_general)]
assert_equal expected, Author.preload(:general_posts, :general_categorizations).first.general_categorizations
assert_equal expected, Author.eager_load(:general_posts, :general_categorizations).first.general_categorizations
expected = [posts(:welcome)]
assert_equal expected, Author.preload(:general_categorizations, :general_posts).first.general_posts
assert_equal expected, Author.eager_load(:general_categorizations, :general_posts).first.general_posts
end
def test_has_many_through_polymorphic_with_rewhere
post = TaggedPost.create!(title: "Tagged", body: "Post")
tag = post.tags.create!(name: "Tag")

View File

@ -19,7 +19,7 @@ require "models/ship"
require "models/liquid"
require "models/molecule"
require "models/electron"
require "models/man"
require "models/human"
require "models/interest"
require "models/pirate"
require "models/parrot"
@ -240,13 +240,13 @@ class AssociationProxyTest < ActiveRecord::TestCase
end
test "inverses get set of subsets of the association" do
man = Man.create
man.interests.create
human = Human.create
human.interests.create
man = Man.find(man.id)
human = Human.find(human.id)
assert_queries(1) do
assert_equal man, man.interests.where("1=1").first.man
assert_equal human, human.interests.where("1=1").first.human
end
end
@ -354,8 +354,23 @@ class OverridingAssociationsTest < ActiveRecord::TestCase
end
end
class PreloaderTest < ActiveRecord::TestCase
fixtures :posts, :comments
def test_preload_with_scope
post = posts(:welcome)
preloader = ActiveRecord::Associations::Preloader.new
preloader.preload([post], :comments, Comment.where(body: "Thank you for the welcome"))
assert_predicate post.comments, :loaded?
assert_equal [comments(:greetings)], post.comments
end
end
class GeneratedMethodsTest < ActiveRecord::TestCase
fixtures :developers, :computers, :posts, :comments
def test_association_methods_override_attribute_methods_of_same_name
assert_equal(developers(:david), computers(:workstation).developer)
# this next line will fail if the attribute methods module is generated lazily

View File

@ -1719,7 +1719,7 @@ class BasicsTest < ActiveRecord::TestCase
assert_match %r/\AWrite query attempted while in readonly mode: INSERT /, conn2_error.message
ensure
ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
clean_up_connection_handler
ActiveRecord::Base.establish_connection(:arunit)
end
end

View File

@ -21,7 +21,7 @@ module ActiveRecord
end
def teardown
ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
clean_up_connection_handler
end
class MultiConnectionTestModel < ActiveRecord::Base
@ -217,6 +217,14 @@ module ActiveRecord
assert_equal "`connected_to` cannot accept a `database` argument with any other arguments.", error.message
end
def test_database_argument_is_deprecated
assert_deprecated do
ActiveRecord::Base.connected_to(database: { writing: { adapter: "sqlite3", database: "test/db/primary.sqlite3" } }) { }
end
ensure
ActiveRecord::Base.establish_connection(:arunit)
end
def test_switching_connections_without_database_and_role_raises
error = assert_raises(ArgumentError) do
ActiveRecord::Base.connected_to { }
@ -369,8 +377,6 @@ module ActiveRecord
end
def test_connection_handlers_are_per_thread_and_not_per_fiber
original_handlers = ActiveRecord::Base.connection_handlers
ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler, reading: ActiveRecord::ConnectionAdapters::ConnectionHandler.new }
reading_handler = ActiveRecord::Base.connection_handlers[:reading]
@ -382,7 +388,7 @@ module ActiveRecord
assert_not_equal reading, ActiveRecord::Base.connection_handler
assert_equal reading, reading_handler
ensure
ActiveRecord::Base.connection_handlers = original_handlers
clean_up_connection_handler
end
def test_connection_handlers_swapping_connections_in_fiber

View File

@ -15,7 +15,7 @@ module ActiveRecord
end
def teardown
ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
clean_up_connection_handler
end
unless in_memory_db?
@ -31,10 +31,10 @@ module ActiveRecord
@prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
@writing_handler.establish_connection(:primary)
@writing_handler.establish_connection(:primary, :pool_config_two)
@writing_handler.establish_connection(:primary, shard: :pool_config_two)
default_pool = @writing_handler.retrieve_connection_pool("primary", :default)
other_pool = @writing_handler.retrieve_connection_pool("primary", :pool_config_two)
default_pool = @writing_handler.retrieve_connection_pool("primary", shard: :default)
other_pool = @writing_handler.retrieve_connection_pool("primary", shard: :pool_config_two)
assert_not_nil default_pool
assert_not_equal default_pool, other_pool
@ -59,13 +59,13 @@ module ActiveRecord
@prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
@writing_handler.establish_connection(:primary)
@writing_handler.establish_connection(:primary, :pool_config_two)
@writing_handler.establish_connection(:primary, shard: :pool_config_two)
# remove default
@writing_handler.remove_connection_pool("primary")
assert_nil @writing_handler.retrieve_connection_pool("primary")
assert_not_nil @writing_handler.retrieve_connection_pool("primary", :pool_config_two)
assert_not_nil @writing_handler.retrieve_connection_pool("primary", shard: :pool_config_two)
ensure
ActiveRecord::Base.configurations = @prev_configs
ActiveRecord::Base.establish_connection(:arunit)
@ -84,14 +84,14 @@ module ActiveRecord
@prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
@writing_handler.establish_connection(:primary)
@writing_handler.establish_connection(:primary, :pool_config_two)
@writing_handler.establish_connection(:primary, shard: :pool_config_two)
# connect to default
@writing_handler.connection_pool_list.first.checkout
assert @writing_handler.connected?("primary")
assert @writing_handler.connected?("primary", :default)
assert_not @writing_handler.connected?("primary", :pool_config_two)
assert @writing_handler.connected?("primary", shard: :default)
assert_not @writing_handler.connected?("primary", shard: :pool_config_two)
ensure
ActiveRecord::Base.configurations = @prev_configs
ActiveRecord::Base.establish_connection(:arunit)

View File

@ -21,10 +21,20 @@ module ActiveRecord
end
def teardown
ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
clean_up_connection_handler
end
unless in_memory_db?
def test_establishing_a_connection_in_connected_to_block_uses_current_role_and_shard
ActiveRecord::Base.connected_to(shard: :shard_one) do
db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary")
ActiveRecord::Base.establish_connection(db_config)
assert_nothing_raised { Person.first }
assert_equal [:default, :shard_one], ActiveRecord::Base.connection_handlers[:writing].send(:owner_to_pool_manager).fetch("ActiveRecord::Base").instance_variable_get(:@name_to_pool_config).keys
end
end
def test_establish_connection_using_3_levels_config
previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env"
@ -43,13 +53,13 @@ module ActiveRecord
})
base_pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("ActiveRecord::Base")
default_pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("ActiveRecord::Base", :default)
default_pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("ActiveRecord::Base", shard: :default)
assert_equal base_pool, default_pool
assert_equal "test/db/primary.sqlite3", default_pool.db_config.database
assert_equal "primary", default_pool.db_config.name
assert_not_nil pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("ActiveRecord::Base", :shard_one)
assert_not_nil pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("ActiveRecord::Base", shard: :shard_one)
assert_equal "test/db/primary_shard_one.sqlite3", pool.db_config.database
assert_equal "primary_shard_one", pool.db_config.name
ensure
@ -77,23 +87,23 @@ module ActiveRecord
shard_one: { writing: :primary_shard_one, reading: :primary_shard_one_replica }
})
default_writing_pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("ActiveRecord::Base", :default)
default_writing_pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("ActiveRecord::Base", shard: :default)
base_writing_pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("ActiveRecord::Base")
assert_equal base_writing_pool, default_writing_pool
assert_equal "test/db/primary.sqlite3", default_writing_pool.db_config.database
assert_equal "primary", default_writing_pool.db_config.name
default_reading_pool = ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("ActiveRecord::Base", :default)
default_reading_pool = ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("ActiveRecord::Base", shard: :default)
base_reading_pool = ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("ActiveRecord::Base")
assert_equal base_reading_pool, default_reading_pool
assert_equal "test/db/primary.sqlite3", default_reading_pool.db_config.database
assert_equal "primary_replica", default_reading_pool.db_config.name
assert_not_nil pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("ActiveRecord::Base", :shard_one)
assert_not_nil pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("ActiveRecord::Base", shard: :shard_one)
assert_equal "test/db/primary_shard_one.sqlite3", pool.db_config.database
assert_equal "primary_shard_one", pool.db_config.name
assert_not_nil pool = ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("ActiveRecord::Base", :shard_one)
assert_not_nil pool = ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("ActiveRecord::Base", shard: :shard_one)
assert_equal "test/db/primary_shard_one.sqlite3", pool.db_config.database
assert_equal "primary_shard_one_replica", pool.db_config.name
ensure
@ -233,10 +243,10 @@ module ActiveRecord
def test_retrieve_connection_pool_with_invalid_shard
assert_not_nil @rw_handler.retrieve_connection_pool("ActiveRecord::Base")
assert_nil @rw_handler.retrieve_connection_pool("ActiveRecord::Base", :foo)
assert_nil @rw_handler.retrieve_connection_pool("ActiveRecord::Base", shard: :foo)
assert_not_nil @ro_handler.retrieve_connection_pool("ActiveRecord::Base")
assert_nil @ro_handler.retrieve_connection_pool("ActiveRecord::Base", :foo)
assert_nil @ro_handler.retrieve_connection_pool("ActiveRecord::Base", shard: :foo)
end
def test_calling_connected_to_on_a_non_existent_shard_raises

View File

@ -12,7 +12,7 @@ module ActiveRecord
end
teardown do
ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
clean_up_connection_handler
end
def test_empty_session

View File

@ -236,6 +236,10 @@ class EnumTest < ActiveRecord::TestCase
assert_nil @book.reload.status
end
test "deserialize nil value to enum which defines nil value to hash" do
assert_equal "forgotten", books(:ddd).last_read
end
test "assign nil value" do
@book.status = nil
assert_nil @book.status

View File

@ -1375,7 +1375,14 @@ class FinderTest < ActiveRecord::TestCase
limit: 3, order: "posts.id"
).to_a
assert_equal 3, posts.size
assert_equal [0, 1, 1], posts.map(&:author_id).sort
assert_equal [1, 1, nil], posts.map(&:author_id)
end
def test_custom_select_takes_precedence_over_original_value
posts = Post.select("UPPER(title) AS title")
assert_equal "WELCOME TO THE WEBLOG", posts.first.title
assert_equal "WELCOME TO THE WEBLOG", posts.preload(:comments).first.title
assert_equal "WELCOME TO THE WEBLOG", posts.eager_load(:comments).first.title
end
def test_eager_load_for_no_has_many_with_limit_and_joins_for_has_many

View File

@ -1399,7 +1399,6 @@ if current_adapter?(:SQLite3Adapter) && !in_memory_db?
def setup
@old_handler = ActiveRecord::Base.connection_handler
@old_handlers = ActiveRecord::Base.connection_handlers
@prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
db_config = ActiveRecord::DatabaseConfigurations::HashConfig.new(ENV["RAILS_ENV"], "readonly", readonly_config)
@ -1413,7 +1412,7 @@ if current_adapter?(:SQLite3Adapter) && !in_memory_db?
def teardown
ActiveRecord::Base.configurations = @prev_configs
ActiveRecord::Base.connection_handler = @old_handler
ActiveRecord::Base.connection_handlers = @old_handlers
clean_up_connection_handler
end
def test_uses_writing_connection_for_fixtures

View File

@ -149,6 +149,10 @@ def disable_extension!(extension, connection)
connection.reconnect!
end
def clean_up_connection_handler
ActiveRecord::Base.connection_handlers = { ActiveRecord::Base.writing_role => ActiveRecord::Base.default_connection_handler }
end
def load_schema
# silence verbose schema loading
original_stdout = $stdout

View File

@ -940,8 +940,10 @@ class MigrationTest < ActiveRecord::TestCase
e = assert_raises(ActiveRecord::ConcurrentMigrationError) do
silence_stream($stderr) do
migrator.send(:with_advisory_lock) do
ActiveRecord::AdvisoryLockBase.connection.release_advisory_lock(lock_id)
migrator.stub(:with_advisory_lock_connection, ->(&block) { block.call(ActiveRecord::Base.connection) }) do
migrator.send(:with_advisory_lock) do
ActiveRecord::Base.connection.release_advisory_lock(lock_id)
end
end
end
end

View File

@ -7,7 +7,7 @@ require "models/ship_part"
require "models/bird"
require "models/parrot"
require "models/treasure"
require "models/man"
require "models/human"
require "models/interest"
require "models/owner"
require "models/pet"
@ -140,19 +140,19 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
end
def test_reject_if_with_a_proc_which_returns_true_always_for_has_many
Man.accepts_nested_attributes_for :interests, reject_if: proc { |attributes| true }
man = Man.create(name: "John")
interest = man.interests.create(topic: "photography")
man.update(interests_attributes: { topic: "gardening", id: interest.id })
Human.accepts_nested_attributes_for :interests, reject_if: proc { |attributes| true }
human = Human.create(name: "John")
interest = human.interests.create(topic: "photography")
human.update(interests_attributes: { topic: "gardening", id: interest.id })
assert_equal "photography", interest.reload.topic
end
def test_destroy_works_independent_of_reject_if
Man.accepts_nested_attributes_for :interests, reject_if: proc { |attributes| true }, allow_destroy: true
man = Man.create(name: "Jon")
interest = man.interests.create(topic: "the ladies")
man.update(interests_attributes: { _destroy: "1", id: interest.id })
assert_empty man.reload.interests
Human.accepts_nested_attributes_for :interests, reject_if: proc { |attributes| true }, allow_destroy: true
human = Human.create(name: "Jon")
interest = human.interests.create(topic: "the ladies")
human.update(interests_attributes: { _destroy: "1", id: interest.id })
assert_empty human.reload.interests
end
def test_reject_if_is_not_short_circuited_if_allow_destroy_is_false
@ -169,10 +169,10 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
end
def test_has_many_association_updating_a_single_record
Man.accepts_nested_attributes_for(:interests)
man = Man.create(name: "John")
interest = man.interests.create(topic: "photography")
man.update(interests_attributes: { topic: "gardening", id: interest.id })
Human.accepts_nested_attributes_for(:interests)
human = Human.create(name: "John")
interest = human.interests.create(topic: "photography")
human.update(interests_attributes: { topic: "gardening", id: interest.id })
assert_equal "gardening", interest.reload.topic
end
@ -186,12 +186,12 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
end
def test_first_and_array_index_zero_methods_return_the_same_value_when_nested_attributes_are_set_to_update_existing_record
Man.accepts_nested_attributes_for(:interests)
man = Man.create(name: "John")
interest = man.interests.create topic: "gardening"
man = Man.find man.id
man.interests_attributes = [{ id: interest.id, topic: "gardening" }]
assert_equal man.interests.first.topic, man.interests[0].topic
Human.accepts_nested_attributes_for(:interests)
human = Human.create(name: "John")
interest = human.interests.create topic: "gardening"
human = Human.find human.id
human.interests_attributes = [{ id: interest.id, topic: "gardening" }]
assert_equal human.interests.first.topic, human.interests[0].topic
end
def test_allows_class_to_override_setter_and_call_super
@ -219,10 +219,10 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
end
def test_should_not_create_duplicates_with_create_with
Man.accepts_nested_attributes_for(:interests)
Human.accepts_nested_attributes_for(:interests)
assert_difference("Interest.count", 1) do
Man.create_with(
Human.create_with(
interests_attributes: [{ topic: "Pirate king" }]
).find_or_create_by!(
name: "Monkey D. Luffy"
@ -817,17 +817,17 @@ module NestedAttributesOnACollectionAssociationTests
end
def test_validate_presence_of_parent_works_with_inverse_of
Man.accepts_nested_attributes_for(:interests)
assert_equal :man, Man.reflect_on_association(:interests).options[:inverse_of]
assert_equal :interests, Interest.reflect_on_association(:man).options[:inverse_of]
Human.accepts_nested_attributes_for(:interests)
assert_equal :human, Human.reflect_on_association(:interests).options[:inverse_of]
assert_equal :interests, Interest.reflect_on_association(:human).options[:inverse_of]
repair_validations(Interest) do
Interest.validates_presence_of(:man)
assert_difference "Man.count" do
Interest.validates_presence_of(:human)
assert_difference "Human.count" do
assert_difference "Interest.count", 2 do
man = Man.create!(name: "John",
human = Human.create!(name: "John",
interests_attributes: [{ topic: "Cars" }, { topic: "Sports" }])
assert_equal 2, man.interests.count
assert_equal 2, human.interests.count
end
end
end
@ -839,14 +839,14 @@ module NestedAttributesOnACollectionAssociationTests
end
def test_numeric_column_changes_from_zero_to_no_empty_string
Man.accepts_nested_attributes_for(:interests)
Human.accepts_nested_attributes_for(:interests)
repair_validations(Interest) do
Interest.validates_numericality_of(:zine_id)
man = Man.create(name: "John")
interest = man.interests.create(topic: "bar", zine_id: 0)
human = Human.create(name: "John")
interest = human.interests.create(topic: "bar", zine_id: 0)
assert interest.save
assert_not man.update(interests_attributes: { id: interest.id, zine_id: "foo" })
assert_not human.update(interests_attributes: { id: interest.id, zine_id: "foo" })
end
end

View File

@ -93,7 +93,7 @@ class QueryCacheTest < ActiveRecord::TestCase
mw.call({})
ensure
ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
clean_up_connection_handler
end
@ -157,7 +157,7 @@ class QueryCacheTest < ActiveRecord::TestCase
rd.close
ensure
ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
clean_up_connection_handler
end
end
@ -445,17 +445,15 @@ class QueryCacheTest < ActiveRecord::TestCase
def test_cache_is_available_when_using_a_not_connected_connection
skip "In-Memory DB can't test for using a not connected connection" if in_memory_db?
with_temporary_connection_pool do
db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary").dup
db_config.owner_name = "test2"
ActiveRecord::Base.connection_handler.establish_connection(db_config)
assert_not_predicate Task, :connected?
db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary").dup
db_config.owner_name = "test2"
ActiveRecord::Base.connection_handler.establish_connection(db_config)
assert_not_predicate Task, :connected?
Task.cache do
assert_queries(1) { Task.find(1); Task.find(1) }
ensure
ActiveRecord::Base.connection_handler.remove_connection_pool(db_config.owner_name)
end
Task.cache do
assert_queries(1) { Task.find(1); Task.find(1) }
ensure
ActiveRecord::Base.connection_handler.remove_connection_pool(db_config.owner_name)
end
end
@ -532,44 +530,38 @@ class QueryCacheTest < ActiveRecord::TestCase
end
def test_query_cache_does_not_establish_connection_if_unconnected
with_temporary_connection_pool do
ActiveRecord::Base.clear_active_connections!
assert_not ActiveRecord::Base.connection_handler.active_connections? # sanity check
ActiveRecord::Base.clear_active_connections!
assert_not ActiveRecord::Base.connection_handler.active_connections? # sanity check
middleware {
assert_not ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in setup"
}.call({})
middleware {
assert_not ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in setup"
}.call({})
assert_not ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in cleanup"
end
assert_not ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in cleanup"
end
def test_query_cache_is_enabled_on_connections_established_after_middleware_runs
with_temporary_connection_pool do
ActiveRecord::Base.clear_active_connections!
assert_not ActiveRecord::Base.connection_handler.active_connections? # sanity check
ActiveRecord::Base.clear_active_connections!
assert_not ActiveRecord::Base.connection_handler.active_connections? # sanity check
middleware {
assert_predicate ActiveRecord::Base.connection, :query_cache_enabled
}.call({})
assert_not_predicate ActiveRecord::Base.connection, :query_cache_enabled
end
middleware {
assert_predicate ActiveRecord::Base.connection, :query_cache_enabled
}.call({})
assert_not_predicate ActiveRecord::Base.connection, :query_cache_enabled
end
def test_query_caching_is_local_to_the_current_thread
with_temporary_connection_pool do
ActiveRecord::Base.clear_active_connections!
ActiveRecord::Base.clear_active_connections!
middleware {
assert ActiveRecord::Base.connection_pool.query_cache_enabled
assert ActiveRecord::Base.connection.query_cache_enabled
middleware {
assert ActiveRecord::Base.connection_pool.query_cache_enabled
assert ActiveRecord::Base.connection.query_cache_enabled
Thread.new {
assert_not ActiveRecord::Base.connection_pool.query_cache_enabled
assert_not ActiveRecord::Base.connection.query_cache_enabled
}.join
}.call({})
end
Thread.new {
assert_not ActiveRecord::Base.connection_pool.query_cache_enabled
assert_not ActiveRecord::Base.connection.query_cache_enabled
}.join
}.call({})
end
def test_query_cache_is_enabled_on_all_connection_pools
@ -583,41 +575,39 @@ class QueryCacheTest < ActiveRecord::TestCase
def test_clear_query_cache_is_called_on_all_connections
skip "with in memory db, reading role won't be able to see database on writing role" if in_memory_db?
with_temporary_connection_pool do
ActiveRecord::Base.connection_handlers = {
writing: ActiveRecord::Base.default_connection_handler,
reading: ActiveRecord::ConnectionAdapters::ConnectionHandler.new
}
ActiveRecord::Base.connection_handlers = {
writing: ActiveRecord::Base.default_connection_handler,
reading: ActiveRecord::ConnectionAdapters::ConnectionHandler.new
}
ActiveRecord::Base.connected_to(role: :reading) do
db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary")
ActiveRecord::Base.establish_connection(db_config)
end
mw = middleware { |env|
ActiveRecord::Base.connected_to(role: :reading) do
db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary")
ActiveRecord::Base.establish_connection(db_config)
@topic = Topic.first
end
mw = middleware { |env|
ActiveRecord::Base.connected_to(role: :reading) do
@topic = Topic.first
end
assert @topic
assert @topic
ActiveRecord::Base.connected_to(role: :writing) do
@topic.title = "It doesn't have to be crazy at work"
@topic.save!
end
ActiveRecord::Base.connected_to(role: :writing) do
@topic.title = "It doesn't have to be crazy at work"
@topic.save!
end
assert_equal "It doesn't have to be crazy at work", @topic.title
ActiveRecord::Base.connected_to(role: :reading) do
@topic = Topic.first
assert_equal "It doesn't have to be crazy at work", @topic.title
end
}
ActiveRecord::Base.connected_to(role: :reading) do
@topic = Topic.first
assert_equal "It doesn't have to be crazy at work", @topic.title
end
}
mw.call({})
end
mw.call({})
ensure
ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
clean_up_connection_handler
end
test "query cache is enabled in threads with shared connection" do

View File

@ -25,6 +25,22 @@ module ActiveRecord
assert_equal expected, actual
end
def test_non_select_columns_wont_be_loaded
posts = Post.select("UPPER(title) AS title")
assert_non_select_columns_wont_be_loaded(posts.first)
assert_non_select_columns_wont_be_loaded(posts.preload(:comments).first)
assert_non_select_columns_wont_be_loaded(posts.eager_load(:comments).first)
end
def assert_non_select_columns_wont_be_loaded(post)
assert_equal "WELCOME TO THE WEBLOG", post.title
assert_raise(ActiveModel::MissingAttributeError) do
post.body
end
end
private :assert_non_select_columns_wont_be_loaded
def test_type_casted_extra_select_with_eager_loading
posts = Post.select("posts.id * 1.1 AS foo").eager_load(:comments)
assert_equal 1.1, posts.first.foo
@ -32,18 +48,12 @@ module ActiveRecord
def test_aliased_select_using_as_with_joins_and_includes
posts = Post.select("posts.id AS field_alias").joins(:comments).includes(:comments)
assert_equal %w(
id author_id title body type legacy_comments_count taggings_with_delete_all_count taggings_with_destroy_count
tags_count indestructible_tags_count tags_with_destroy_count tags_with_nullify_count field_alias
), posts.first.attributes.keys
assert_equal %w(id field_alias), posts.first.attributes.keys
end
def test_aliased_select_not_using_as_with_joins_and_includes
posts = Post.select("posts.id field_alias").joins(:comments).includes(:comments)
assert_equal %w(
id author_id title body type legacy_comments_count taggings_with_delete_all_count taggings_with_destroy_count
tags_count indestructible_tags_count tags_with_destroy_count tags_with_nullify_count field_alias
), posts.first.attributes.keys
assert_equal %w(id field_alias), posts.first.attributes.keys
end
def test_star_select_with_joins_and_includes

View File

@ -3,7 +3,7 @@
require "cases/helper"
require "models/post"
require "models/author"
require "models/man"
require "models/human"
require "models/essay"
require "models/comment"
require "models/categorization"
@ -11,7 +11,7 @@ require "models/categorization"
module ActiveRecord
class WhereChainTest < ActiveRecord::TestCase
fixtures :posts, :comments, :authors, :men, :essays
fixtures :posts, :comments, :authors, :humans, :essays
def test_missing_with_association
assert posts(:authorless).author.blank?
@ -105,8 +105,8 @@ module ActiveRecord
end
def test_rewhere_with_polymorphic_association
relation = Essay.where(writer: authors(:david)).rewhere(writer: men(:steve))
expected = Essay.where(writer: men(:steve))
relation = Essay.where(writer: authors(:david)).rewhere(writer: humans(:steve))
expected = Essay.where(writer: humans(:steve))
assert_equal expected.to_a, relation.to_a
end

View File

@ -1112,7 +1112,7 @@ module ActiveRecord
def teardown
SchemaMigration.delete_all
InternalMetadata.delete_all
ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
clean_up_connection_handler
end
def test_truncate_tables

View File

@ -43,7 +43,6 @@ class TestFixturesTest < ActiveRecord::TestCase
end
end
old_handlers = ActiveRecord::Base.connection_handlers
old_handler = ActiveRecord::Base.connection_handler
ActiveRecord::Base.connection_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
ActiveRecord::Base.connection_handlers = {}
@ -53,7 +52,7 @@ class TestFixturesTest < ActiveRecord::TestCase
assert_predicate(test_result, :passed?)
ensure
ActiveRecord::Base.connection_handler = old_handler
ActiveRecord::Base.connection_handlers = old_handlers
clean_up_connection_handler
FileUtils.rm_r(tmp_dir)
end
end

View File

@ -3,12 +3,12 @@
require "cases/helper"
require "models/face"
require "models/interest"
require "models/man"
require "models/human"
require "models/topic"
class AbsenceValidationTest < ActiveRecord::TestCase
def test_non_association
boy_klass = Class.new(Man) do
boy_klass = Class.new(Human) do
def self.name; "Boy" end
validates_absence_of :name
end
@ -18,7 +18,7 @@ class AbsenceValidationTest < ActiveRecord::TestCase
end
def test_has_one_marked_for_destruction
boy_klass = Class.new(Man) do
boy_klass = Class.new(Human) do
def self.name; "Boy" end
validates_absence_of :face
end
@ -32,7 +32,7 @@ class AbsenceValidationTest < ActiveRecord::TestCase
end
def test_has_many_marked_for_destruction
boy_klass = Class.new(Man) do
boy_klass = Class.new(Human) do
def self.name; "Boy" end
validates_absence_of :interests
end
@ -48,7 +48,7 @@ class AbsenceValidationTest < ActiveRecord::TestCase
end
def test_does_not_call_to_a_on_associations
boy_klass = Class.new(Man) do
boy_klass = Class.new(Human) do
def self.name; "Boy" end
validates_absence_of :face
end

View File

@ -3,7 +3,7 @@
require "cases/helper"
require "models/topic"
require "models/reply"
require "models/man"
require "models/human"
require "models/interest"
class AssociationValidationTest < ActiveRecord::TestCase
@ -80,20 +80,20 @@ class AssociationValidationTest < ActiveRecord::TestCase
def test_validates_presence_of_belongs_to_association__parent_is_new_record
repair_validations(Interest) do
# Note that Interest and Man have the :inverse_of option set
Interest.validates_presence_of(:man)
man = Man.new(name: "John")
interest = man.interests.build(topic: "Airplanes")
assert interest.valid?, "Expected interest to be valid, but was not. Interest should have a man object associated"
# Note that Interest and Human have the :inverse_of option set
Interest.validates_presence_of(:human)
human = Human.new(name: "John")
interest = human.interests.build(topic: "Airplanes")
assert interest.valid?, "Expected interest to be valid, but was not. Interest should have a human object associated"
end
end
def test_validates_presence_of_belongs_to_association__existing_parent
repair_validations(Interest) do
Interest.validates_presence_of(:man)
man = Man.create!(name: "John")
interest = man.interests.build(topic: "Airplanes")
assert interest.valid?, "Expected interest to be valid, but was not. Interest should have a man object associated"
Interest.validates_presence_of(:human)
human = Human.create!(name: "John")
interest = human.interests.build(topic: "Airplanes")
assert interest.valid?, "Expected interest to be valid, but was not. Interest should have a human object associated"
end
end
end

View File

@ -1,14 +1,14 @@
# frozen_string_literal: true
require "cases/helper"
require "models/man"
require "models/human"
require "models/face"
require "models/interest"
require "models/speedometer"
require "models/dashboard"
class PresenceValidationTest < ActiveRecord::TestCase
class Boy < Man; end
class Boy < Human; end
repair_validations(Boy)

View File

@ -12,5 +12,5 @@ mary_stay_home:
steve_connecting_the_dots:
name: Connecting The Dots
writer_type: Man
writer_type: Human
writer_id: Steve

View File

@ -1,11 +1,11 @@
trusting:
description: trusting
man: gordon
human: gordon
weather_beaten:
description: weather beaten
man: steve
human: steve
confused:
description: confused
polymorphic_man: gordon (Man)
polymorphic_human: gordon (Human)

View File

@ -1,33 +1,33 @@
trainspotting:
topic: Trainspotting
zine: staying_in
man: gordon
human: gordon
birdwatching:
topic: Birdwatching
zine: staying_in
man: gordon
human: gordon
stamp_collecting:
topic: Stamp Collecting
zine: staying_in
man: gordon
human: gordon
hunting:
topic: Hunting
zine: going_out
man: steve
human: steve
woodsmanship:
topic: Woodsmanship
zine: going_out
man: steve
human: steve
survival:
topic: Survival
zine: going_out
man: steve
human: steve
llama_wrangling:
topic: Llama Wrangling
polymorphic_man: gordon (Man)
polymorphic_human: gordon (Human)

View File

@ -93,6 +93,9 @@ class Author < ActiveRecord::Base
has_many :special_categories, through: :special_categorizations, source: :category
has_one :special_category, through: :special_categorizations, source: :category
has_many :general_categorizations, -> { joins(:category).where("categories.name": "General") }, class_name: "Categorization"
has_many :general_posts, through: :general_categorizations, source: :post
has_many :special_categories_with_conditions, -> { where(categorizations: { special: true }) }, through: :categorizations, source: :category
has_many :nonspecial_categories_with_conditions, -> { where(categorizations: { special: false }) }, through: :categorizations, source: :category

View File

@ -1,16 +1,16 @@
# frozen_string_literal: true
class Face < ActiveRecord::Base
belongs_to :man, inverse_of: :face
belongs_to :human, polymorphic: true
belongs_to :polymorphic_man, polymorphic: true, inverse_of: :polymorphic_face
belongs_to :human, inverse_of: :face
belongs_to :super_human, polymorphic: true
belongs_to :polymorphic_human, polymorphic: true, inverse_of: :polymorphic_face
# Oracle identifier length is limited to 30 bytes or less, `polymorphic` renamed `poly`
belongs_to :poly_man_without_inverse, polymorphic: true
belongs_to :poly_human_without_inverse, polymorphic: true
# These are "broken" inverse_of associations for the purposes of testing
belongs_to :horrible_man, class_name: "Man", inverse_of: :horrible_face
belongs_to :horrible_polymorphic_man, polymorphic: true, inverse_of: :horrible_polymorphic_face
belongs_to :horrible_human, class_name: "Human", inverse_of: :horrible_face
belongs_to :horrible_polymorphic_human, polymorphic: true, inverse_of: :horrible_polymorphic_face
validate do
man
human
end
end

View File

@ -1,28 +1,30 @@
# frozen_string_literal: true
class Man < ActiveRecord::Base
has_one :face, inverse_of: :man
has_one :polymorphic_face, class_name: "Face", as: :polymorphic_man, inverse_of: :polymorphic_man
has_one :polymorphic_face_without_inverse, class_name: "Face", as: :poly_man_without_inverse
has_many :interests, inverse_of: :man
class Human < ActiveRecord::Base
self.table_name = "humans"
has_one :face, inverse_of: :human
has_one :polymorphic_face, class_name: "Face", as: :polymorphic_human, inverse_of: :polymorphic_human
has_one :polymorphic_face_without_inverse, class_name: "Face", as: :poly_human_without_inverse
has_many :interests, inverse_of: :human
has_many :interests_with_callbacks,
class_name: "Interest",
before_add: :add_called,
after_add: :add_called,
inverse_of: :man_with_callbacks
inverse_of: :human_with_callbacks
has_many :polymorphic_interests,
class_name: "Interest",
as: :polymorphic_man,
inverse_of: :polymorphic_man
as: :polymorphic_human,
inverse_of: :polymorphic_human
has_many :polymorphic_interests_with_callbacks,
class_name: "Interest",
as: :polymorphic_man,
as: :polymorphic_human,
before_add: :add_called,
after_add: :add_called,
inverse_of: :polymorphic_man
inverse_of: :polymorphic_human
# These are "broken" inverse_of associations for the purposes of testing
has_one :dirty_face, class_name: "Face", inverse_of: :dirty_man
has_many :secret_interests, class_name: "Interest", inverse_of: :secret_man
has_one :dirty_face, class_name: "Face", inverse_of: :dirty_human
has_many :secret_interests, class_name: "Interest", inverse_of: :secret_human
has_one :mixed_case_monkey
attribute :add_callback_called, :boolean, default: false
@ -32,5 +34,5 @@ class Man < ActiveRecord::Base
end
end
class Human < Man
class SuperHuman < Human
end

View File

@ -1,15 +1,15 @@
# frozen_string_literal: true
class Interest < ActiveRecord::Base
belongs_to :man, inverse_of: :interests
belongs_to :man_with_callbacks,
class_name: "Man",
foreign_key: :man_id,
belongs_to :human, inverse_of: :interests
belongs_to :human_with_callbacks,
class_name: "Human",
foreign_key: :human_id,
inverse_of: :interests_with_callbacks
belongs_to :polymorphic_man, polymorphic: true, inverse_of: :polymorphic_interests
belongs_to :polymorphic_man_with_callbacks,
foreign_key: :polymorphic_man_id,
foreign_type: :polymorphic_man_type,
belongs_to :polymorphic_human, polymorphic: true, inverse_of: :polymorphic_interests
belongs_to :polymorphic_human_with_callbacks,
foreign_key: :polymorphic_human_id,
foreign_type: :polymorphic_human_type,
polymorphic: true,
inverse_of: :polymorphic_interests_with_callbacks
belongs_to :zine, inverse_of: :interests

View File

@ -1,5 +1,5 @@
# frozen_string_literal: true
class MixedCaseMonkey < ActiveRecord::Base
belongs_to :man
belongs_to :human
end

View File

@ -1009,27 +1009,27 @@ ActiveRecord::Schema.define do
create_table(t, force: true) { }
end
create_table :men, force: true do |t|
create_table :humans, force: true do |t|
t.string :name
end
create_table :faces, force: true do |t|
t.string :description
t.integer :man_id
t.integer :polymorphic_man_id
t.string :polymorphic_man_type
t.integer :poly_man_without_inverse_id
t.string :poly_man_without_inverse_type
t.integer :horrible_polymorphic_man_id
t.string :horrible_polymorphic_man_type
t.references :human, polymorphic: true, index: false
t.integer :human_id
t.integer :polymorphic_human_id
t.string :polymorphic_human_type
t.integer :poly_human_without_inverse_id
t.string :poly_human_without_inverse_type
t.integer :horrible_polymorphic_human_id
t.string :horrible_polymorphic_human_type
t.references :super_human, polymorphic: true, index: false
end
create_table :interests, force: true do |t|
t.string :topic
t.integer :man_id
t.integer :polymorphic_man_id
t.string :polymorphic_man_type
t.integer :human_id
t.integer :polymorphic_human_id
t.string :polymorphic_human_type
t.integer :zine_id
end

View File

@ -11,12 +11,12 @@ class ActiveStorage::Attachment < ActiveRecord::Base
self.table_name = "active_storage_attachments"
belongs_to :record, polymorphic: true, touch: true
belongs_to :blob, class_name: "ActiveStorage::Blob"
belongs_to :blob, class_name: "ActiveStorage::Blob", autosave: true
delegate_missing_to :blob
delegate :signed_id, to: :blob
after_create_commit :mirror_blob_later, :analyze_blob_later, :identify_blob
after_create_commit :mirror_blob_later, :analyze_blob_later
after_destroy_commit :purge_dependent_blob_later
# Synchronously deletes the attachment and {purges the blob}[rdoc-ref:ActiveStorage::Blob#purge].
@ -38,10 +38,6 @@ class ActiveStorage::Attachment < ActiveRecord::Base
end
private
def identify_blob
blob.identify
end
def analyze_blob_later
blob.analyze_later unless blob.analyzed?
end

View File

@ -53,6 +53,8 @@ class ActiveStorage::Blob < ActiveRecord::Base
self.service_name ||= self.class.service.name
end
after_update_commit :update_service_metadata, if: :content_type_previously_changed?
before_destroy(prepend: true) do
raise ActiveRecord::InvalidForeignKey if attachments.exists?
end
@ -326,6 +328,10 @@ class ActiveStorage::Blob < ActiveRecord::Base
{ content_type: content_type }
end
end
def update_service_metadata
service.update_metadata key, **service_metadata if service_metadata.any?
end
end
ActiveSupport.run_load_hooks :active_storage_blob, ActiveStorage::Blob

View File

@ -2,9 +2,14 @@
module ActiveStorage::Blob::Identifiable
def identify
identify_without_saving
save!
end
def identify_without_saving
unless identified?
update! content_type: identify_content_type, identified: true
update_service_metadata
self.content_type = identify_content_type
self.identified = true
end
end
@ -24,8 +29,4 @@ module ActiveStorage::Blob::Identifiable
""
end
end
def update_service_metadata
service.update_metadata key, **service_metadata if service_metadata.any?
end
end

View File

@ -6,6 +6,7 @@ module ActiveStorage
def initialize(name, record, attachables)
@name, @record, @attachables = name, record, Array(attachables)
blobs.each(&:identify_without_saving)
end
def attachments

View File

@ -9,6 +9,7 @@ module ActiveStorage
def initialize(name, record, attachable)
@name, @record, @attachable = name, record, attachable
blob.identify_without_saving
end
def attachment
@ -65,7 +66,7 @@ module ActiveStorage
**attachable.reverse_merge(
record: record,
service_name: attachment_service_name
)
).symbolize_keys
)
when String
ActiveStorage::Blob.find_signed!(attachable, record: record)

View File

@ -29,7 +29,8 @@ module ActiveStorage
# document.images.attach([ first_blob, second_blob ])
def attach(*attachables)
if record.persisted? && !record.changed?
record.update(name => blobs + attachables.flatten)
record.public_send("#{name}=", blobs + attachables.flatten)
record.save
else
record.public_send("#{name}=", (change&.attachables || blobs) + attachables.flatten)
end

View File

@ -29,7 +29,8 @@ module ActiveStorage
# person.avatar.attach(avatar_blob) # ActiveStorage::Blob object
def attach(attachable)
if record.persisted? && !record.changed?
record.update(name => attachable)
record.public_send("#{name}=", attachable)
record.save
else
record.public_send("#{name}=", attachable)
end

View File

@ -319,6 +319,13 @@ class ActiveStorage::OneAttachedTest < ActiveSupport::TestCase
assert_equal 2736, @user.avatar.metadata[:height]
end
test "creating an attachment as part of an autosave association through nested attributes" do
group = Group.create!(users_attributes: [{ name: "John", avatar: { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" } }])
group.save!
new_user = User.find_by(name: "John")
assert new_user.avatar.attached?
end
test "updating an attachment as part of an autosave association" do
group = Group.create!(users: [@user])
@user.avatar = fixture_file_upload("racecar.jpg")

View File

@ -2,6 +2,7 @@
require "test_helper"
require "database/setup"
require "active_support/testing/method_call_assertions"
class ActiveStorage::AttachmentTest < ActiveSupport::TestCase
include ActiveJob::TestHelper
@ -50,6 +51,38 @@ class ActiveStorage::AttachmentTest < ActiveSupport::TestCase
end
end
test "directly-uploaded blob identification for one attached occurs before validation" do
blob = directly_upload_file_blob(filename: "racecar.jpg", content_type: "application/octet-stream")
assert_blob_identified_before_owner_validated(@user, blob, "image/jpeg") do
@user.avatar.attach(blob)
end
end
test "directly-uploaded blob identification for many attached occurs before validation" do
blob = directly_upload_file_blob(filename: "racecar.jpg", content_type: "application/octet-stream")
assert_blob_identified_before_owner_validated(@user, blob, "image/jpeg") do
@user.highlights.attach(blob)
end
end
test "directly-uploaded blob identification for one attached occurs outside transaction" do
blob = directly_upload_file_blob(filename: "racecar.jpg")
assert_blob_identified_outside_transaction(blob) do
@user.avatar.attach(blob)
end
end
test "directly-uploaded blob identification for many attached occurs outside transaction" do
blob = directly_upload_file_blob(filename: "racecar.jpg")
assert_blob_identified_outside_transaction(blob) do
@user.highlights.attach(blob)
end
end
test "getting a signed blob ID from an attachment" do
blob = create_blob
@user.avatar.attach(blob)
@ -65,4 +98,33 @@ class ActiveStorage::AttachmentTest < ActiveSupport::TestCase
signed_id_generated_old_way = ActiveStorage.verifier.generate(@user.avatar.id, purpose: :blob_id)
assert_equal blob, ActiveStorage::Blob.find_signed!(signed_id_generated_old_way)
end
private
def assert_blob_identified_before_owner_validated(owner, blob, content_type)
validated_content_type = nil
owner.class.validate do
validated_content_type ||= blob.content_type
end
yield
assert_equal content_type, validated_content_type
assert_equal content_type, blob.reload.content_type
end
def assert_blob_identified_outside_transaction(blob)
baseline_transaction_depth = ActiveRecord::Base.connection.open_transactions
max_transaction_depth = -1
track_transaction_depth = ->(*) do
max_transaction_depth = [ActiveRecord::Base.connection.open_transactions, max_transaction_depth].max
end
blob.stub(:identify_without_saving, track_transaction_depth) do
yield
end
assert_equal 0, (max_transaction_depth - baseline_transaction_depth)
end
end

View File

@ -260,6 +260,16 @@ class ActiveStorage::BlobTest < ActiveSupport::TestCase
assert_equal ["is invalid"], blob.errors[:service_name]
end
test "updating the content_type updates service metadata" do
blob = directly_upload_file_blob(filename: "racecar.jpg", content_type: "application/octet-stream")
expected_arguments = [blob.key, content_type: "image/jpeg"]
assert_called_with(blob.service, :update_metadata, expected_arguments) do
blob.update!(content_type: "image/jpeg")
end
end
private
def expected_url_for(blob, disposition: :attachment, filename: nil, content_type: nil, service_name: :local)
filename ||= blob.filename

View File

@ -162,12 +162,12 @@ class ActiveStorage::VariantTest < ActiveSupport::TestCase
end
test "resized variation of BMP blob" do
blob = create_file_blob(filename: "colors.bmp")
blob = create_file_blob(filename: "colors.bmp", content_type: "image/bmp")
variant = blob.variant(resize: "15x15").processed
assert_match(/colors\.bmp/, variant.url)
assert_match(/colors\.png/, variant.url)
image = read_image(variant)
assert_equal "BMP", image.type
assert_equal "PNG", image.type
assert_equal 15, image.width
assert_equal 8, image.height
end

View File

@ -127,6 +127,8 @@ end
class Group < ActiveRecord::Base
has_one_attached :avatar
has_many :users, autosave: true
accepts_nested_attributes_for :users
end
require_relative "../../tools/test_common"

View File

@ -153,7 +153,8 @@ module Enumerable
if keys.many?
map { |element| keys.map { |key| element[key] } }
else
map { |element| element[keys.first] }
key = keys.first
map { |element| element[key] }
end
end

View File

@ -318,9 +318,9 @@ You can use `uuid` type to define references in migrations:
```ruby
# db/migrate/20150418012400_create_blog.rb
enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto')
create_table :posts, id: :uuid, default: 'gen_random_uuid()'
create_table :posts, id: :uuid
create_table :comments, id: :uuid, default: 'gen_random_uuid()' do |t|
create_table :comments, id: :uuid do |t|
# t.belongs_to :post, type: :uuid
t.references :post, type: :uuid
end
@ -414,7 +414,7 @@ extension to generate random UUIDs.
```ruby
# db/migrate/20131220144913_create_devices.rb
enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto')
create_table :devices, id: :uuid, default: 'gen_random_uuid()' do |t|
create_table :devices, id: :uuid do |t|
t.string :kind
end

View File

@ -151,7 +151,7 @@ and `region` keys in the example above. The S3 Service supports all of the
authentication options described in the [AWS SDK documentation]
(https://docs.aws.amazon.com/sdk-for-ruby/v3/developer-guide/setup-config.html).
To connect to an S3-compatible object storage API such as Digital Ocean Spaces, provide the `endpoint`:
To connect to an S3-compatible object storage API such as DigitalOcean Spaces, provide the `endpoint`:
```yaml
digitalocean:
@ -500,7 +500,7 @@ message.video.open do |file|
end
```
It's important to know that the file are not yet available in the `after_create` callback but in the `after_create_commit` only.
It's important to know that the file is not yet available in the `after_create` callback but in the `after_create_commit` only.
Analyzing Files
---------------

View File

@ -93,7 +93,7 @@ Handled at the Action Pack layer:
means not having to spend time thinking about how to model your API in terms
of HTTP.
- URL Generation: The flip side of routing is URL generation. A good API based
on HTTP includes URLs (see [the GitHub Gist API](https://developer.github.com/v3/gists/)
on HTTP includes URLs (see [the GitHub Gist API](https://docs.github.com/en/rest/reference/gists)
for an example).
- Header and Redirection Responses: `head :no_content` and
`redirect_to user_url(current_user)` come in handy. Sure, you could manually
@ -350,8 +350,12 @@ Instead of the initializer, you'll have to set the relevant options somewhere be
built (like `config/application.rb`) and pass them to your preferred middleware, like this:
```ruby
config.session_store :cookie_store, key: '_interslice_session' # <-- this also configures session_options for use below
config.middleware.use ActionDispatch::Cookies # Required for all session management (regardless of session_store)
# This also configures session_options for use below
config.session_store :cookie_store, key: '_interslice_session'
# Required for all session management (regardless of session_store)
config.middleware.use ActionDispatch::Cookies
config.middleware.use config.session_store, config.session_options
```
@ -405,7 +409,7 @@ controller modules by default:
more information regarding this).
- `ActionController::ParamsWrapper`: Wraps the parameters hash into a nested hash,
so that you don't have to specify root elements sending POST requests for instance.
- `ActionController::Head`: Support for returning a response with no content, only headers
- `ActionController::Head`: Support for returning a response with no content, only headers.
Other plugins may add additional modules. You can get a list of all modules
included into `ActionController::API` in the rails console:
@ -433,21 +437,22 @@ Some common modules you might want to add:
- `AbstractController::Translation`: Support for the `l` and `t` localization
and translation methods.
- Support for basic, digest, or token HTTP authentication:
* `ActionController::HttpAuthentication::Basic::ControllerMethods`,
* `ActionController::HttpAuthentication::Digest::ControllerMethods`,
* `ActionController::HttpAuthentication::Basic::ControllerMethods`
* `ActionController::HttpAuthentication::Digest::ControllerMethods`
* `ActionController::HttpAuthentication::Token::ControllerMethods`
- `ActionView::Layouts`: Support for layouts when rendering.
- `ActionController::MimeResponds`: Support for `respond_to`.
- `ActionController::Cookies`: Support for `cookies`, which includes
support for signed and encrypted cookies. This requires the cookies middleware.
- `ActionController::Caching`: Support view caching for the API controller. Please notice that
you will need to manually specify cache store inside the controller like:
```ruby
class ApplicationController < ActionController::API
include ::ActionController::Caching
self.cache_store = :mem_cache_store
end
```
- `ActionController::Caching`: Support view caching for the API controller. Please note
that you will need to manually specify the cache store inside the controller like this:
```ruby
class ApplicationController < ActionController::API
include ::ActionController::Caching
self.cache_store = :mem_cache_store
end
```
Rails does *not* pass this configuration automatically.
The best place to add a module is in your `ApplicationController`, but you can

View File

@ -118,7 +118,7 @@ end
```
When used alone, `belongs_to` produces a one-directional one-to-one connection. Therefore each book in the above example "knows" its author, but the authors don't know about their books.
To setup a [bi-directional association](#bi-directional-associations) - use `belongs_to` in combination with a `has_one` or `has_many` on the other model.
To setup a [bi-directional association](#bi-directional-associations) - use `belongs_to` in combination with a `has_one` or `has_many` on the other model.
`belongs_to` does not ensure reference consistency, so depending on the use case, you might also need to add a database-level foreign key constraint on the reference column, like this:
@ -356,7 +356,9 @@ end
### The `has_and_belongs_to_many` Association
A `has_and_belongs_to_many` association creates a direct many-to-many connection with another model, with no intervening model. For example, if your application includes assemblies and parts, with each assembly having many parts and each part appearing in many assemblies, you could declare the models this way:
A `has_and_belongs_to_many` association creates a direct many-to-many connection with another model, with no intervening model.
This association indicates that each instance of the declaring model refers to zero or more instances of another model.
For example, if your application includes assemblies and parts, with each assembly having many parts and each part appearing in many assemblies, you could declare the models this way:
```ruby
class Assembly < ApplicationRecord

View File

@ -393,6 +393,12 @@ With the `helper` method it is possible to access Rails and your application's h
INFO: You can also use the alias "db" to invoke the dbconsole: `bin/rails db`.
If you are using multiple databases, `bin/rails dbconsole` will connect to the primary database by default. You can specify which database to connect to using `--database` or `--db`:
```bash
$ bin/rails dbconsole --database=animals
```
### `bin/rails runner`
`runner` runs Ruby code in the context of Rails non-interactively. For instance:

View File

@ -161,7 +161,7 @@ defaults to `:debug` for all environments. The available log levels are: `:debug
* `config.time_zone` sets the default time zone for the application and enables time zone awareness for Active Record.
* `config.autoloader` sets the autoloading mode. This option defaults to `:zeitwerk` if `6.0` is specified in `config.load_defaults`. Applications can still use the classic autoloader by setting this value to `:classic` after loading the framework defaults:
* `config.autoloader` sets the autoloading mode. This option defaults to `:zeitwerk` when `config.load_defaults` is called with `6.0` or greater. Applications can still use the classic autoloader by setting this value to `:classic` after loading the framework defaults:
```ruby
config.load_defaults 6.0
@ -263,7 +263,7 @@ Every Rails application comes with a standard set of middleware which it uses in
# `beta1.product.com`.
Rails.application.config.hosts << /.*\.product\.com/
```
The provided regexp will be wrapped with both anchors (`\A` and `\z`) so it
must match the entire hostname. `/product.com/`, for example, once anchored,
would fail to match `www.product.com`.
@ -460,10 +460,9 @@ in controllers and views. This defaults to `false`.
to be reused when the object being cached of type `ActiveRecord::Relation`
changes by moving the volatile information (max updated at and count) of
the relation's cache key into the cache version to support recycling cache key.
Defaults to `false`.
* `config.active_record.has_many_inversing` enables setting the inverse record
when traversing `belongs_to` to `has_many` associations. Defaults to `false`.
when traversing `belongs_to` to `has_many` associations.
The MySQL adapter adds one additional configuration option:
@ -509,9 +508,9 @@ The schema dumper adds two additional configuration options:
* `config.action_controller.per_form_csrf_tokens` configures whether CSRF tokens are only valid for the method/action they were generated for.
* `config.action_controller.default_protect_from_forgery` determines whether forgery protection is added on `ActionController::Base`. This is false by default.
* `config.action_controller.default_protect_from_forgery` determines whether forgery protection is added on `ActionController::Base`.
* `config.action_controller.urlsafe_csrf_tokens` configures whether generated CSRF tokens are URL-safe. Defaults to `false`.
* `config.action_controller.urlsafe_csrf_tokens` configures whether generated CSRF tokens are URL-safe.
* `config.action_controller.relative_url_root` can be used to tell Rails that you are [deploying to a subdirectory](configuring.html#deploy-to-a-subdirectory-relative-url-root). The default is `ENV['RAILS_RELATIVE_URL_ROOT']`.
@ -629,8 +628,8 @@ Defaults to `'signed cookie'`.
header without modification. Defaults to `false`.
* `config.action_dispatch.cookies_same_site_protection` configures the default
value of the `SameSite` attribute when setting cookies. Defaults to `nil`,
which means the `SameSite` attribute is not added.
value of the `SameSite` attribute when setting cookies. When set to `nil`, the
`SameSite` attribute is not added.
* `config.action_dispatch.ssl_default_redirect_status` configures the default
HTTP status code used when redirecting non-GET/HEAD requests from HTTP to HTTPS
@ -688,7 +687,7 @@ Defaults to `'signed cookie'`.
* `config.action_view.form_with_generates_remote_forms` determines whether `form_with` generates remote forms or not. This defaults to `true`.
* `config.action_view.form_with_generates_ids` determines whether `form_with` generates ids on inputs. This defaults to `false`.
* `config.action_view.form_with_generates_ids` determines whether `form_with` generates ids on inputs.
* `config.action_view.default_enforce_utf8` determines whether forms are generated with a hidden tag that forces older versions of Internet Explorer to submit forms encoded in UTF-8. This defaults to `false`.
@ -795,7 +794,7 @@ There are a number of settings available on `config.action_mailer`:
* `config.action_mailer.perform_caching` specifies whether the mailer templates should perform fragment caching or not. If it's not specified, the default will be `true`.
* `config.action_mailer.delivery_job` specifies delivery job for mail. Defaults to `ActionMailer::DeliveryJob`.
* `config.action_mailer.delivery_job` specifies delivery job for mail.
### Configuring Active Support
@ -812,9 +811,9 @@ There are a few configuration options available in Active Support:
* `config.active_support.time_precision` sets the precision of JSON encoded time values. Defaults to `3`.
* `config.active_support.use_sha1_digests` specifies whether to use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header. Defaults to false.
* `config.active_support.use_sha1_digests` specifies whether to use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header.
* `config.active_support.use_authenticated_message_encryption` specifies whether to use AES-256-GCM authenticated encryption as the default cipher for encrypting messages instead of AES-256-CBC. This is false by default.
* `config.active_support.use_authenticated_message_encryption` specifies whether to use AES-256-GCM authenticated encryption as the default cipher for encrypting messages instead of AES-256-CBC.
* `ActiveSupport::Logger.silencer` is set to `false` to disable the ability to silence logging in a block. The default is `true`.
@ -832,7 +831,7 @@ There are a few configuration options available in Active Support:
* `ActiveSupport.utc_to_local_returns_utc_offset_times` configures
`ActiveSupport::TimeZone.utc_to_local` to return a time with a UTC offset
instead of a UTC time incorporating that offset. Defaults to `false`.
instead of a UTC time incorporating that offset.
### Configuring Active Job
@ -889,15 +888,15 @@ There are a few configuration options available in Active Support:
* `config.active_job.custom_serializers` allows to set custom argument serializers. Defaults to `[]`.
* `config.active_job.return_false_on_aborted_enqueue` change the return value of `#enqueue` to false instead of the job instance when the enqueuing is aborted. Defaults to `false`.
* `config.active_job.return_false_on_aborted_enqueue` change the return value of `#enqueue` to false instead of the job instance when the enqueuing is aborted.
* `config.active_job.log_arguments` controls if the arguments of a job are logged. Defaults to `true`.
* `config.active_job.retry_jitter` controls the amount of "jitter" (random variation) applied to the delay time calculated when retrying failed jobs. Defaults to `0.0`.
* `config.active_job.retry_jitter` controls the amount of "jitter" (random variation) applied to the delay time calculated when retrying failed jobs.
* `config.active_job.skip_after_callbacks_if_terminated` controls whether
`after_enqueue` / `after_perform` callbacks run when a `before_enqueue` /
`before_perform` callback halts with `throw :abort`. Defaults to `false`.
`before_perform` callback halts with `throw :abort`.
### Configuring Action Cable
@ -940,7 +939,7 @@ You can find more detailed configuration options in the
* `config.active_storage.content_types_to_serve_as_binary` accepts an array of strings indicating the content types that Active Storage will always serve as an attachment, rather than inline. The default is `%w(text/html
text/javascript image/svg+xml application/postscript application/x-shockwave-flash text/xml application/xml application/xhtml+xml application/mathml+xml text/cache-manifest)`.
* `config.active_storage.content_types_allowed_inline` accepts an array of strings indicating the content types that Active Storage allows to serve as inline. The default is `%w(image/png image/gif image/jpg image/jpeg image/vnd.adobe.photoshop image/vnd.microsoft.icon application/pdf)`.
* `config.active_storage.content_types_allowed_inline` accepts an array of strings indicating the content types that Active Storage allows to serve as inline. The default is `%w(image/png image/gif image/jpg image/jpeg image/tiff image/bmp image/vnd.adobe.photoshop image/vnd.microsoft.icon application/pdf)`.
* `config.active_storage.queues.analysis` accepts a symbol indicating the Active Job queue to use for analysis jobs. When this option is `nil`, analysis jobs are sent to the default Active Job queue (see `config.active_job.default_queue_name`).
@ -999,7 +998,7 @@ text/javascript image/svg+xml application/postscript application/x-shockwave-fla
`config.load_defaults` sets new defaults up to and including the version passed. Such that passing, say, '6.0' also gets the new defaults from every version before it.
#### For '6.1', new defaults from previous versions below and:
#### For '6.1', defaults from previous versions below and:
- `config.active_record.has_many_inversing`: `true`
- `config.active_storage.track_variants`: `true`
@ -1010,7 +1009,7 @@ text/javascript image/svg+xml application/postscript application/x-shockwave-fla
- `ActiveSupport.utc_to_local_returns_utc_offset_times`: `true`
- `config.action_controller.urlsafe_csrf_tokens`: `true`
#### For '6.0', new defaults from previous versions below and:
#### For '6.0', defaults from previous versions below and:
- `config.autoloader`: `:zeitwerk`
- `config.action_view.default_enforce_utf8`: `false`
@ -1023,7 +1022,7 @@ text/javascript image/svg+xml application/postscript application/x-shockwave-fla
- `config.active_storage.replace_on_assign_to_many`: `true`
- `config.active_record.collection_cache_versioning`: `true`
#### For '5.2', new defaults from previous versions below and:
#### For '5.2', defaults from previous versions below and:
- `config.active_record.cache_versioning`: `true`
- `config.action_dispatch.use_authenticated_cookie_encryption`: `true`
@ -1032,12 +1031,12 @@ text/javascript image/svg+xml application/postscript application/x-shockwave-fla
- `config.action_controller.default_protect_from_forgery`: `true`
- `config.action_view.form_with_generates_ids`: `true`
#### For '5.1', new defaults from previous versions below and:
#### For '5.1', defaults from previous versions below and:
- `config.assets.unknown_asset_fallback`: `false`
- `config.action_view.form_with_generates_remote_forms`: `true`
#### For '5.0':
#### For '5.0', baseline defaults from below and:
- `config.action_controller.per_form_csrf_tokens`: `true`
- `config.action_controller.forgery_protection_origin_check`: `true`
@ -1045,6 +1044,22 @@ text/javascript image/svg+xml application/postscript application/x-shockwave-fla
- `config.active_record.belongs_to_required_by_default`: `true`
- `config.ssl_options`: `{ hsts: { subdomains: true } }`
#### Baseline defaults:
- `config.action_controller.default_protect_from_forgery`: `false`
- `config.action_controller.urlsafe_csrf_tokens`: `false`
- `config.action_dispatch.cookies_same_site_protection`: `nil`
- `config.action_mailer.delivery_job`: `ActionMailer::DeliveryJob`
- `config.action_view.form_with_generates_ids`: `false`
- `config.active_job.retry_jitter`: `0.0`
- `config.active_job.return_false_on_aborted_enqueue`: `false`
- `config.active_job.skip_after_callbacks_if_terminated`: `false`
- `config.active_record.collection_cache_versioning`: `false`
- `config.active_record.has_many_inversing`: `false`
- `config.active_support.use_authenticated_message_encryption`: `false`
- `config.active_support.use_sha1_digests`: `false`
- `ActiveSupport.utc_to_local_returns_utc_offset_times`: `false`
### Configuring a Database
Just about every Rails application will interact with a database. You can connect to the database by setting an environment variable `ENV['DATABASE_URL']` or by using a configuration file called `config/database.yml`.

View File

@ -751,6 +751,35 @@ end
Both the `matches?` method and the lambda gets the `request` object as an argument.
#### Constraints in a block form
You can specify constraints in a block form. This is useful for when you need to apply the same rule to several routes. For example
```
class RestrictedListConstraint
# ...Same as the example above
end
Rails.application.routes.draw do
constraints(RestrictedListConstraint.new) do
get '*path', to: 'restricted_list#index',
get '*other-path', to: 'other_restricted_list#index',
end
end
```
You also use a `lambda`:
```
Rails.application.routes.draw do
constraints(lambda { |request| RestrictedList.retrieve_ips.include?(request.remote_ip) }) do
get '*path', to: 'restricted_list#index',
get '*other-path', to: 'other_restricted_list#index',
end
end
```
### Route Globbing and Wildcard Segments
Route globbing is a way to specify that a particular parameter should be matched to all the remaining parts of a route. For example:

View File

@ -292,13 +292,13 @@ There are many other possibilities, like using a `<script>` tag to make a cross-
NOTE: We can't distinguish a `<script>` tag's origin—whether it's a tag on your own site or on some other malicious site—so we must block all `<script>` across the board, even if it's actually a safe same-origin script served from your own site. In these cases, explicitly skip CSRF protection on actions that serve JavaScript meant for a `<script>` tag.
To protect against all other forged requests, we introduce a _required security token_ that our site knows but other sites don't know. We include the security token in requests and verify it on the server. This is a one-liner in your application controller, and is the default for newly created Rails applications:
To protect against all other forged requests, we introduce a _required security token_ that our site knows but other sites don't know. We include the security token in requests and verify it on the server. This is done automatically when `config.action_controller.default_protect_from_forgery` is set to `true`, which is the default for newly created Rails applications. You can also do it manually by adding the following to your application controller:
```ruby
protect_from_forgery with: :exception
```
This will automatically include a security token in all forms and Ajax requests generated by Rails. If the security token doesn't match what was expected, an exception will be thrown.
This will include a security token in all forms and Ajax requests generated by Rails. If the security token doesn't match what was expected, an exception will be thrown.
NOTE: By default, Rails includes an [unobtrusive scripting adapter](https://github.com/rails/rails/blob/master/actionview/app/assets/javascripts),
which adds a header called `X-CSRF-Token` with the security token on every non-GET

View File

@ -320,7 +320,7 @@ specify to make your test failure messages clearer.
| `assert_in_delta( expected, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are within `delta` of each other.|
| `assert_not_in_delta( expected, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are not within `delta` of each other.|
| `assert_in_epsilon ( expected, actual, [epsilon], [msg] )` | Ensures that the numbers `expected` and `actual` have a relative error less than `epsilon`.|
| `assert_not_in_epsilon ( expected, actual, [epsilon], [msg] )` | Ensures that the numbers `expected` and `actual` don't have a relative error less than `epsilon`.|
| `assert_not_in_epsilon ( expected, actual, [epsilon], [msg] )` | Ensures that the numbers `expected` and `actual` have a relative error not less than `epsilon`.|
| `assert_throws( symbol, [msg] ) { block }` | Ensures that the given block throws the symbol.|
| `assert_raises( exception1, exception2, ... ) { block }` | Ensures that the given block raises one of the given exceptions.|
| `assert_instance_of( class, obj, [msg] )` | Ensures that `obj` is an instance of `class`.|

View File

@ -25,10 +25,14 @@ module Rails
directory "app"
empty_directory_with_keep_file "app/assets/images/#{namespaced_name}"
end
remove_dir "app/mailers" if options[:skip_action_mailer]
remove_dir "app/jobs" if options[:skip_active_job]
elsif full?
empty_directory_with_keep_file "app/models"
empty_directory_with_keep_file "app/controllers"
empty_directory_with_keep_file "app/mailers"
empty_directory_with_keep_file "app/mailers" unless options[:skip_action_mailer]
empty_directory_with_keep_file "app/jobs" unless options[:skip_active_job]
unless api?
empty_directory_with_keep_file "app/assets/images/#{namespaced_name}"

View File

@ -232,6 +232,24 @@ class PluginGeneratorTest < Rails::Generators::TestCase
end
end
def test_skip_action_mailer_and_skip_active_job_with_mountable
run_generator [destination_root, "--mountable", "--skip-action-mailer", "--skip-active-job"]
assert_no_directory "app/mailers"
assert_no_directory "app/jobs"
end
def test_skip_action_mailer_and_skip_active_job_with_api_and_mountable
run_generator [destination_root, "--api", "--mountable", "--skip-action-mailer", "--skip-active-job"]
assert_no_directory "app/mailers"
assert_no_directory "app/jobs"
end
def test_skip_action_mailer_and_skip_active_job_with_full
run_generator [destination_root, "--full", "--skip-action-mailer", "--skip-active-job"]
assert_no_directory "app/mailers"
assert_no_directory "app/jobs"
end
def test_template_from_dir_pwd
FileUtils.cd(Rails.root)
assert_match(/It works from file!/, run_generator([destination_root, "-m", "lib/template.rb"]))
@ -268,6 +286,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
assert_file "app/views"
assert_file "app/helpers"
assert_file "app/mailers"
assert_file "app/jobs"
assert_file "bin/rails", /\s+require\s+["']rails\/all["']/
assert_file "config/routes.rb", /Rails.application.routes.draw do/
assert_file "lib/bukkits/engine.rb", /module Bukkits\n class Engine < ::Rails::Engine\n end\nend/
@ -284,6 +303,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
assert_file "hyphenated-name/app/views"
assert_file "hyphenated-name/app/helpers"
assert_file "hyphenated-name/app/mailers"
assert_file "hyphenated-name/app/jobs"
assert_file "hyphenated-name/bin/rails"
assert_file "hyphenated-name/config/routes.rb", /Rails.application.routes.draw do/
assert_file "hyphenated-name/lib/hyphenated/name/engine.rb", /module Hyphenated\n module Name\n class Engine < ::Rails::Engine\n end\n end\nend/
@ -301,6 +321,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
assert_file "my_hyphenated-name/app/views"
assert_file "my_hyphenated-name/app/helpers"
assert_file "my_hyphenated-name/app/mailers"
assert_file "my_hyphenated-name/app/jobs"
assert_file "my_hyphenated-name/bin/rails"
assert_file "my_hyphenated-name/config/routes.rb", /Rails\.application\.routes\.draw do/
assert_file "my_hyphenated-name/lib/my_hyphenated/name/engine.rb", /module MyHyphenated\n module Name\n class Engine < ::Rails::Engine\n end\n end\nend/