diff --git a/actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb index 7e73c397c4..7dc900aa68 100644 --- a/actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb +++ b/actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb @@ -19,7 +19,7 @@ class ActionMailbox::Ingresses::Mandrill::InboundEmailsControllerTest < ActionDi test "receiving an inbound email from Mandrill" do assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do post rails_mandrill_inbound_emails_url, - headers: { "X-Mandrill-Signature" => "gldscd2tAb/G+DmpiLcwukkLrC4=" }, params: { mandrill_events: @events } + headers: { "X-Mandrill-Signature" => "1bNbyqkMFL4VYIT5+RQCrPs/mRY=" }, params: { mandrill_events: @events } end assert_response :ok diff --git a/actionmailbox/test/fixtures/files/welcome.eml b/actionmailbox/test/fixtures/files/welcome.eml index 5d6b3c1ea5..27fd51c58a 100644 --- a/actionmailbox/test/fixtures/files/welcome.eml +++ b/actionmailbox/test/fixtures/files/welcome.eml @@ -27,7 +27,7 @@ Content-Type: multipart/related; Content-Transfer-Encoding: base64 Content-Disposition: inline; filename=avatar1.jpeg -Content-Type: image/jpg; +Content-Type: image/jpeg; name="avatar1.jpeg" Content-Id: <7AAEB353-2341-4D46-A054-5CA5CB2363B7> @@ -397,7 +397,7 @@ mk8VWW5WRGJGAOaAP//Z Content-Transfer-Encoding: base64 Content-Disposition: inline; filename=avatar2.jpg -Content-Type: image/jpg; +Content-Type: image/jpeg; x-unix-mode=0700; name="avatar2.jpg" Content-Id: <4594E827-6E69-4329-8691-6BC35E3E73A0> diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index e00cd1060e..560a4deeed 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -433,7 +433,7 @@ module ActionMailer # * deliveries - Keeps an array of all the emails sent out through the Action Mailer with # delivery_method :test. Most useful for unit and functional testing. # - # * deliver_later_queue_name - The name of the queue used with deliver_later. Defaults to +mailers+. + # * deliver_later_queue_name - The name of the queue used with deliver_later. class Base < AbstractController::Base include DeliveryMethods include Rescuable diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 0a80e7e6db..fda5dbbc03 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -103,7 +103,7 @@ module ActionController #:nodoc: # If you need to use a MIME type which isn't supported by default, you can register your own handlers in # +config/initializers/mime_types.rb+ as follows. # - # Mime::Type.register "image/jpg", :jpg + # Mime::Type.register "image/jpeg", :jpg # # +respond_to+ also allows you to specify a common block for different formats by using +any+: # diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 464cf8309e..18e72f9ad5 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -303,15 +303,15 @@ module ActionController #:nodoc: [form_authenticity_param, request.x_csrf_token] end - # Sets the token value for the current session. - def form_authenticity_token(form_options: {}) + # Creates the authenticity token for the current request. + def form_authenticity_token(form_options: {}) # :doc: masked_authenticity_token(session, form_options: form_options) end # Creates a masked version of the authenticity token that varies # on each request. The masking is used to mitigate SSL attacks # like BREACH. - def masked_authenticity_token(session, form_options: {}) # :doc: + def masked_authenticity_token(session, form_options: {}) action, method = form_options.values_at(:action, :method) raw_token = if per_form_csrf_tokens && action && method diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb index 04271d8e8a..fedeafc3e7 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb @@ -1,10 +1,3 @@ -<% unless @exception.blamed_files.blank? %> - <% if (hide = @exception.blamed_files.length > 8) %> - Toggle blamed files - <% end %> -
><%= @exception.describe_blame %>
-<% end %> -

Request

<% if params_valid? %>

Parameters:

<%= debug_params(@request.filtered_parameters) %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb index b3a4182ac7..f49bee3437 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb @@ -153,6 +153,7 @@ font-weight: bold; margin: 0; padding: 10px 18px; + cursor: pointer; -webkit-appearance: none; } input[type="submit"]:focus, @@ -238,9 +239,6 @@ var hide = function(id) { document.getElementById(id).style.display = 'none'; } - var toggleTrace = function() { - return toggle('blame_trace'); - } var toggleSessionDump = function() { return toggle('session_dump'); } diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb index c26e09d8ac..b19d1488de 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -12,7 +12,7 @@ class ActionController::Base def before_actions filters = _process_action_callbacks.select { |c| c.kind == :before } - filters.map!(&:raw_filter) + filters.map!(&:filter) end end end diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index 2b73a471f4..2e1a54fbf8 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -1229,7 +1229,7 @@ class IntegrationFileUploadTest < ActionDispatch::IntegrationTest def test_fixture_file_upload post "/test_file_upload", params: { - file: fixture_file_upload("/ruby_on_rails.jpg", "image/jpg") + file: fixture_file_upload("/ruby_on_rails.jpg", "image/jpeg") } assert_equal "45142", @response.body end diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index 445b1c0aac..03a16e5843 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -909,7 +909,7 @@ XML def test_fixture_file_upload_with_binary filename = "ruby_on_rails.jpg" path = "#{FILES_DIR}/#{filename}" - content_type = "image/jpg" + content_type = "image/jpeg" binary_file_upload = fixture_file_upload(path, content_type, :binary) assert_equal File.open(path, READ_BINARY).read, binary_file_upload.read @@ -919,14 +919,14 @@ XML end def test_fixture_file_upload_should_be_able_access_to_tempfile - file = fixture_file_upload(FILES_DIR + "/ruby_on_rails.jpg", "image/jpg") + file = fixture_file_upload(FILES_DIR + "/ruby_on_rails.jpg", "image/jpeg") assert_respond_to file, :tempfile end def test_fixture_file_upload post :test_file_upload, params: { - file: fixture_file_upload(FILES_DIR + "/ruby_on_rails.jpg", "image/jpg") + file: fixture_file_upload(FILES_DIR + "/ruby_on_rails.jpg", "image/jpeg") } assert_equal "45142", @response.body end @@ -935,7 +935,7 @@ XML TestCaseTest.stub :fixture_path, File.expand_path("../fixtures", __dir__) do TestCaseTest.stub :file_fixture_path, nil do assert_deprecated(/In Rails 7.0, the path needs to be relative to `file_fixture_path`/) do - fixture_file_upload("multipart/ruby_on_rails.jpg", "image/jpg") + fixture_file_upload("multipart/ruby_on_rails.jpg", "image/jpeg") end end end @@ -944,7 +944,7 @@ XML def test_fixture_file_upload_does_not_output_deprecation_when_file_fixture_path_is_set TestCaseTest.stub :fixture_path, File.expand_path("../fixtures", __dir__) do assert_not_deprecated do - fixture_file_upload("ruby_on_rails.jpg", "image/jpg") + fixture_file_upload("ruby_on_rails.jpg", "image/jpeg") end end end @@ -954,7 +954,7 @@ XML expected = "`fixture_file_upload(\"multipart/ruby_on_rails.jpg\")` to `fixture_file_upload(\"ruby_on_rails.jpg\")`" assert_deprecated(expected) do - uploaded_file = fixture_file_upload("multipart/ruby_on_rails.jpg", "image/jpg") + uploaded_file = fixture_file_upload("multipart/ruby_on_rails.jpg", "image/jpeg") assert_equal File.open("#{FILES_DIR}/ruby_on_rails.jpg", READ_PLAIN).read, uploaded_file.read end end @@ -975,13 +975,13 @@ XML def test_fixture_file_upload_ignores_fixture_path_given_full_path TestCaseTest.stub :fixture_path, __dir__ do - uploaded_file = fixture_file_upload("#{FILES_DIR}/ruby_on_rails.jpg", "image/jpg") + uploaded_file = fixture_file_upload("#{FILES_DIR}/ruby_on_rails.jpg", "image/jpeg") assert_equal File.open("#{FILES_DIR}/ruby_on_rails.jpg", READ_PLAIN).read, uploaded_file.read end end def test_fixture_file_upload_ignores_nil_fixture_path - uploaded_file = fixture_file_upload("#{FILES_DIR}/ruby_on_rails.jpg", "image/jpg") + uploaded_file = fixture_file_upload("#{FILES_DIR}/ruby_on_rails.jpg", "image/jpeg") assert_equal File.open("#{FILES_DIR}/ruby_on_rails.jpg", READ_PLAIN).read, uploaded_file.read end @@ -989,7 +989,7 @@ XML filename = "ruby_on_rails.jpg" path = "#{FILES_DIR}/#{filename}" post :test_file_upload, params: { - file: Rack::Test::UploadedFile.new(path, "image/jpg", true) + file: Rack::Test::UploadedFile.new(path, "image/jpeg", true) } assert_equal "45142", @response.body end diff --git a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb index 7c0d2bd4b8..830920f375 100644 --- a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb @@ -157,7 +157,7 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest test "uploads and reads binary file" do with_test_routing do fixture = FIXTURE_PATH + "/ruby_on_rails.jpg" - params = { uploaded_data: fixture_file_upload(fixture, "image/jpg") } + params = { uploaded_data: fixture_file_upload(fixture, "image/jpeg") } post "/read", params: params end end diff --git a/actiontext/CHANGELOG.md b/actiontext/CHANGELOG.md index 5853a0e4d2..8a2c1b3586 100644 --- a/actiontext/CHANGELOG.md +++ b/actiontext/CHANGELOG.md @@ -1,3 +1,7 @@ +* Add `config.action_text.attachment_tag_name`, to specify the HTML tag that contains attachments. + + *Mark VanLandingham* + * Expose how we render the HTML _surrounding_ rich text content as an extensible `layouts/action_view/contents/_content.html.erb` template to encourage user-land customizations, while retaining private API control over how diff --git a/actiontext/app/helpers/action_text/content_helper.rb b/actiontext/app/helpers/action_text/content_helper.rb index d7c32a470d..f0f3521ccd 100644 --- a/actiontext/app/helpers/action_text/content_helper.rb +++ b/actiontext/app/helpers/action_text/content_helper.rb @@ -5,7 +5,7 @@ require "rails-html-sanitizer" module ActionText module ContentHelper mattr_accessor(:sanitizer) { Rails::Html::Sanitizer.safe_list_sanitizer.new } - mattr_accessor(:allowed_tags) { sanitizer.class.allowed_tags + [ ActionText::Attachment::TAG_NAME, "figure", "figcaption" ] } + mattr_accessor(:allowed_tags) { sanitizer.class.allowed_tags + [ ActionText::Attachment.tag_name, "figure", "figcaption" ] } mattr_accessor(:allowed_attributes) { sanitizer.class.allowed_attributes + ActionText::Attachment::ATTRIBUTES } mattr_accessor(:scrubber) diff --git a/actiontext/lib/action_text/attachment.rb b/actiontext/lib/action_text/attachment.rb index 4bd537c7c2..b91d6133bc 100644 --- a/actiontext/lib/action_text/attachment.rb +++ b/actiontext/lib/action_text/attachment.rb @@ -6,8 +6,8 @@ module ActionText class Attachment include Attachments::TrixConversion, Attachments::Minification, Attachments::Caching - TAG_NAME = "action-text-attachment" - SELECTOR = TAG_NAME + mattr_accessor :tag_name, default: "action-text-attachment" + ATTRIBUTES = %w( sgid content-type url href filename filesize width height previewable presentation caption ) class << self @@ -38,7 +38,7 @@ module ActionText private def node_from_attributes(attributes) if attributes = process_attributes(attributes).presence - ActionText::HtmlConversion.create_element(TAG_NAME, attributes) + ActionText::HtmlConversion.create_element(tag_name, attributes) end end diff --git a/actiontext/lib/action_text/attachment_gallery.rb b/actiontext/lib/action_text/attachment_gallery.rb index 48ba9f830c..a3c1c33f80 100644 --- a/actiontext/lib/action_text/attachment_gallery.rb +++ b/actiontext/lib/action_text/attachment_gallery.rb @@ -57,7 +57,7 @@ module ActionText end TAG_NAME = "div" - ATTACHMENT_SELECTOR = "#{ActionText::Attachment::SELECTOR}[presentation=gallery]" + ATTACHMENT_SELECTOR = "#{ActionText::Attachment.tag_name}[presentation=gallery]" SELECTOR = "#{TAG_NAME}:has(#{ATTACHMENT_SELECTOR} + #{ATTACHMENT_SELECTOR})" private_constant :TAG_NAME, :ATTACHMENT_SELECTOR, :SELECTOR diff --git a/actiontext/lib/action_text/attachments/minification.rb b/actiontext/lib/action_text/attachments/minification.rb index edc8f876d6..75a113163f 100644 --- a/actiontext/lib/action_text/attachments/minification.rb +++ b/actiontext/lib/action_text/attachments/minification.rb @@ -7,7 +7,7 @@ module ActionText class_methods do def fragment_by_minifying_attachments(content) - Fragment.wrap(content).replace(ActionText::Attachment::SELECTOR) do |node| + Fragment.wrap(content).replace(ActionText::Attachment.tag_name) do |node| node.tap { |n| n.inner_html = "" } end end diff --git a/actiontext/lib/action_text/content.rb b/actiontext/lib/action_text/content.rb index de43880a0c..c238d0266c 100644 --- a/actiontext/lib/action_text/content.rb +++ b/actiontext/lib/action_text/content.rb @@ -58,7 +58,7 @@ module ActionText end def render_attachments(**options, &block) - content = fragment.replace(ActionText::Attachment::SELECTOR) do |node| + content = fragment.replace(ActionText::Attachment.tag_name) do |node| block.call(attachment_for_node(node, **options)) end self.class.new(content, canonicalize: false) @@ -111,7 +111,7 @@ module ActionText private def attachment_nodes - @attachment_nodes ||= fragment.find_all(ActionText::Attachment::SELECTOR) + @attachment_nodes ||= fragment.find_all(ActionText::Attachment.tag_name) end def attachment_gallery_nodes diff --git a/actiontext/lib/action_text/engine.rb b/actiontext/lib/action_text/engine.rb index e78c95b69b..b17a48d372 100644 --- a/actiontext/lib/action_text/engine.rb +++ b/actiontext/lib/action_text/engine.rb @@ -12,6 +12,13 @@ module ActionText isolate_namespace ActionText config.eager_load_namespaces << ActionText + config.action_text = ActiveSupport::OrderedOptions.new + config.action_text.attachment_tag_name = "action-text-attachment" + config.autoload_once_paths = %W( + #{root}/app/helpers + #{root}/app/models + ) + initializer "action_text.attribute" do ActiveSupport.on_load(:active_record) do include ActionText::Attribute @@ -64,5 +71,9 @@ module ActionText include ActionText::SystemTestHelper end end + + initializer "action_text.configure" do |app| + ActionText::Attachment.tag_name = app.config.action_text.attachment_tag_name + end end end diff --git a/actiontext/package.json b/actiontext/package.json index 4bebab9754..aa15f02358 100644 --- a/actiontext/package.json +++ b/actiontext/package.json @@ -21,7 +21,7 @@ ], "license": "MIT", "dependencies": { - "@rails/activestorage": "^6.0.0-alpha" + "@rails/activestorage": "^6.0.0" }, "peerDependencies": { "trix": "^1.2.0" diff --git a/actiontext/test/unit/attachment_test.rb b/actiontext/test/unit/attachment_test.rb index 9ed1967450..4b5b07ac7c 100644 --- a/actiontext/test/unit/attachment_test.rb +++ b/actiontext/test/unit/attachment_test.rb @@ -66,6 +66,6 @@ class ActionText::AttachmentTest < ActiveSupport::TestCase end def attachable - @attachment ||= create_file_blob(filename: "racecar.jpg", content_type: "image/jpg") + @attachment ||= create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg") end end diff --git a/actiontext/test/unit/content_test.rb b/actiontext/test/unit/content_test.rb index dcedb8b1d4..c275e523f3 100644 --- a/actiontext/test/unit/content_test.rb +++ b/actiontext/test/unit/content_test.rb @@ -28,7 +28,7 @@ class ActionText::ContentTest < ActiveSupport::TestCase end test "extracts attachables" do - attachable = create_file_blob(filename: "racecar.jpg", content_type: "image/jpg") + attachable = create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg") html = %Q() content = content_from_html(html) @@ -56,7 +56,7 @@ class ActionText::ContentTest < ActiveSupport::TestCase end test "identifies destroyed attachables as missing" do - attachable = create_file_blob(filename: "racecar.jpg", content_type: "image/jpg") + attachable = create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg") html = %Q() attachable.destroy! content = content_from_html(html) @@ -78,6 +78,15 @@ class ActionText::ContentTest < ActiveSupport::TestCase assert_equal '', content.to_html end + test "converts Trix-formatted attachments with custom tag name" do + with_attachment_tag_name("arbitrary-tag") do + html = %Q(
) + content = content_from_html(html) + assert_equal 1, content.attachments.size + assert_equal '', content.to_html + end + end + test "ignores Trix-formatted attachments with malformed JSON" do html = %Q(
) content = content_from_html(html) @@ -131,4 +140,13 @@ class ActionText::ContentTest < ActiveSupport::TestCase assert_nothing_raised { content.to_s } end end + + def with_attachment_tag_name(tag_name) + previous_tag_name = ActionText::Attachment.tag_name + ActionText::Attachment.tag_name = tag_name + + yield + ensure + ActionText::Attachment.tag_name = previous_tag_name + end end diff --git a/actiontext/test/unit/model_test.rb b/actiontext/test/unit/model_test.rb index a26cebab3d..7193a82714 100644 --- a/actiontext/test/unit/model_test.rb +++ b/actiontext/test/unit/model_test.rb @@ -32,14 +32,14 @@ class ActionText::ModelTest < ActiveSupport::TestCase end test "embed extraction" do - blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpg") + blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg") message = Message.create!(subject: "Greetings", content: ActionText::Content.new("Hello world").append_attachables(blob)) assert_equal "racecar.jpg", message.content.embeds.first.filename.to_s end test "embed extraction only extracts file attachments" do remote_image_html = '' - blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpg") + blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg") content = ActionText::Content.new(remote_image_html).append_attachables(blob) message = Message.create!(subject: "Greetings", content: content) assert_equal [ActionText::Attachables::RemoteImage, ActiveStorage::Blob], message.content.body.attachables.map(&:class) @@ -47,7 +47,7 @@ class ActionText::ModelTest < ActiveSupport::TestCase end test "embed extraction deduplicates file attachments" do - blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpg") + blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg") content = ActionText::Content.new("Hello world").append_attachables([ blob, blob ]) assert_nothing_raised do diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index 8a4d1e3a8f..6baeba1fbf 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -646,10 +646,10 @@ module ActionView # # => 1234567890 # # phone_to "1234567890", "Phone me" - # # => Phone me + # # => Phone me # # phone_to "1234567890", country_code: "01" - # # => 1234567890 + # # => 1234567890 # # You can use a block as well if your link target is hard to fit into the name parameter. \ERB example: # diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index b87f24bb6d..3e173b930c 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,33 @@ +* Type cast enum values by the original attribute type. + + The notable thing about this change is that unknown labels will no longer match 0 on MySQL. + + ```ruby + class Book < ActiveRecord::Base + enum :status, { proposed: 0, written: 1, published: 2 } + end + ``` + + Before: + + ```ruby + # SELECT `books`.* FROM `books` WHERE `books`.`status` = 'prohibited' LIMIT 1 + Book.find_by(status: :prohibited) + # => # (for mysql2 adapter) + # => ActiveRecord::StatementInvalid: PG::InvalidTextRepresentation: ERROR: invalid input syntax for type integer: "prohibited" (for postgresql adapter) + # => nil (for sqlite3 adapter) + ``` + + After: + + ```ruby + # SELECT `books`.* FROM `books` WHERE `books`.`status` IS NULL LIMIT 1 + Book.find_by(status: :prohibited) + # => nil (for all adapters) + ``` + + *Ryuta Kamizono* + * Fixtures for `has_many :through` associations now load timestamps on join tables Given this fixture: diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index e06c991e0f..570d50a53c 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -849,6 +849,11 @@ module ActiveRecord # person.pets.count # => 1 # person.pets.any? # => true # + # Calling it without a block when the collection is not yet + # loaded is equivalent to collection.exists?. + # If you're going to load the collection anyway, it is better + # to call collection.load.any? to avoid an extra query. + # # You can also pass a +block+ to define criteria. The behavior # is the same, it returns true if the collection based on the # criteria is not empty. diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index f8d4f2f527..2490d196dc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -97,7 +97,7 @@ module ActiveRecord include ConnectionAdapters::AbstractPool attr_accessor :automatic_reconnect, :checkout_timeout - attr_reader :db_config, :size, :reaper, :pool_config, :connection_klass + attr_reader :db_config, :size, :reaper, :pool_config, :connection_klass, :async_executor delegate :schema_cache, :schema_cache=, to: :pool_config @@ -461,12 +461,14 @@ module ActiveRecord def build_async_executor case Base.async_query_executor when :multi_thread_pool - Concurrent::ThreadPoolExecutor.new( - min_threads: @db_config.min_threads, - max_threads: @db_config.max_threads, - max_queue: @db_config.max_queue, - fallback_policy: :caller_runs - ) + if @db_config.max_threads > 0 + Concurrent::ThreadPoolExecutor.new( + min_threads: @db_config.min_threads, + max_threads: @db_config.max_threads, + max_queue: @db_config.max_queue, + fallback_policy: :caller_runs + ) + end when :global_thread_pool Base.global_thread_pool_async_query_executor end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 51429fe760..24974dbb7f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -477,8 +477,7 @@ module ActiveRecord end table = Arel::Table.new(table_name) - manager = Arel::InsertManager.new - manager.into(table) + manager = Arel::InsertManager.new(table) if values_list.size == 1 values = values_list.shift diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index 216c924a01..55cce5e501 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -94,8 +94,8 @@ module ActiveRecord sql = ["CREATE"] sql << "UNIQUE" if index.unique sql << "INDEX" - sql << "IF NOT EXISTS" if o.if_not_exists sql << o.algorithm if o.algorithm + sql << "IF NOT EXISTS" if o.if_not_exists sql << index.type if index.type sql << "#{quote_column_name(index.name)} ON #{quote_table_name(index.table)}" sql << "USING #{index.using}" if supports_index_using? && index.using diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index bb051528bc..22768c9611 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -429,8 +429,9 @@ module ActiveRecord true end - def async_enabled? - supports_concurrent_connections? && !Base.async_query_executor.nil? + def async_enabled? # :nodoc: + supports_concurrent_connections? && + !Base.async_query_executor.nil? && !pool.async_executor.nil? end # This is meant to be implemented by the adapters that support extensions diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 405754464b..ee0cbf9d38 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -161,7 +161,7 @@ module ActiveRecord # set to +nil+ which will not run queries in the background. Applications must configure # a thread pool executor to use this feature. Options are: # - # * nil - Does not initalize a thread pool executor. Any async calls will be + # * nil - Does not initialize a thread pool executor. Any async calls will be # run in the foreground. # * :global_thread_pool - Initializes a single +Concurrent::ThreadPoolExecutor+ # that uses the +async_query_concurrency+ for the +max_threads+ value. diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 1c5743e571..6ba12c32a2 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -115,7 +115,7 @@ module ActiveRecord end class EnumType < Type::Value # :nodoc: - delegate :type, to: :subtype + delegate :type, :serializable?, to: :subtype def initialize(name, mapping, subtype) @name = name @@ -128,10 +128,8 @@ module ActiveRecord value.to_s elsif mapping.has_value?(value) mapping.key(value) - elsif value.blank? - nil else - assert_valid_value(value) + value.presence end end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index c713949da0..9406b84274 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -120,6 +120,14 @@ module ActiveRecord # { id: 1, title: "Rework", author: "David" }, # { id: 1, title: "Eloquent Ruby", author: "Russ" } # ]) + # + # # insert_all works on chained scopes, and you can use create_with + # # to set default attributes for all inserted records. + # + # author.books.create_with(created_at: Time.now).insert_all([ + # { id: 1, title: "Rework" }, + # { id: 2, title: "Eloquent Ruby" } + # ]) def insert_all(attributes, returning: nil, unique_by: nil) InsertAll.new(self, attributes, on_duplicate: :skip, returning: returning, unique_by: unique_by).execute end @@ -363,13 +371,12 @@ module ActiveRecord end end - im = Arel::InsertManager.new - im.into(arel_table) + im = Arel::InsertManager.new(arel_table) if values.empty? im.insert(connection.empty_insert_statement_value(primary_key)) else - im.insert(_substitute_values(values)) + im.insert(values.transform_keys { |name| arel_table[name] }) end connection.insert(im, "#{self} Create", primary_key || false, primary_key_value) @@ -386,9 +393,8 @@ module ActiveRecord constraints << current_scope.where_clause.ast end - um = Arel::UpdateManager.new - um.table(arel_table) - um.set(_substitute_values(values)) + um = Arel::UpdateManager.new(arel_table) + um.set(values.transform_keys { |name| arel_table[name] }) um.wheres = constraints connection.update(um, "#{self} Update") @@ -405,8 +411,7 @@ module ActiveRecord constraints << current_scope.where_clause.ast end - dm = Arel::DeleteManager.new - dm.from(arel_table) + dm = Arel::DeleteManager.new(arel_table) dm.wheres = constraints connection.delete(dm, "#{self} Destroy") @@ -428,12 +433,6 @@ module ActiveRecord def discriminate_class_for_record(record) self end - - def _substitute_values(values) - values.map do |name, value| - [ arel_table[name], Arel::Nodes::BindParam.new(value) ] - end - end end # Returns true if this object hasn't been saved yet -- that is, a record diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 4ff93125d7..a2f6189bc3 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -310,6 +310,7 @@ module ActiveRecord if operation != "count" type = column.try(:type_caster) || lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value + type = type.subtype if Enum::EnumType === type end type_cast_calculated_value(result.cast_values.first, operation, type) @@ -384,6 +385,7 @@ module ActiveRecord if operation != "count" type = column.try(:type_caster) || lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value + type = type.subtype if Enum::EnumType === type end hash_rows.each_with_object({}) do |row, result| diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index e436373012..42bbeb1938 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -51,30 +51,25 @@ module ActiveRecord @rewhere = rewhere end - NORMAL_VALUES = Relation::VALUE_METHODS - - Relation::CLAUSE_METHODS - - [:includes, :preload, :joins, :left_outer_joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc: - - def normal_values - NORMAL_VALUES - end + NORMAL_VALUES = Relation::VALUE_METHODS - Relation::CLAUSE_METHODS - + [ + :select, :includes, :preload, :joins, :left_outer_joins, + :order, :reverse_order, :lock, :create_with, :reordering + ] def merge - normal_values.each do |name| + NORMAL_VALUES.each do |name| value = values[name] # The unless clause is here mostly for performance reasons (since the `send` call might be moderately # expensive), most of the time the value is going to be `nil` or `.blank?`, the only catch is that # `false.blank?` returns `true`, so there needs to be an extra check so that explicit `false` values # don't fall through the cracks. unless value.nil? || (value.blank? && false != value) - if name == :select - relation._select!(*value) - else - relation.public_send("#{name}!", *value) - end + relation.public_send(:"#{name}!", *value) end end + merge_select_values merge_multi_values merge_single_values merge_clauses @@ -86,6 +81,18 @@ module ActiveRecord end private + def merge_select_values + return if other.select_values.empty? + + if other.klass == relation.klass + relation.select_values |= other.select_values + else + relation.select_values |= other.instance_eval do + arel_columns(select_values) + end + end + end + def merge_preloads return if other.preload_values.empty? && other.includes_values.empty? diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index ccf0d10d05..1a1091556e 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -65,8 +65,7 @@ module ActiveRecord end def build_bind_attribute(column_name, value) - attr = Relation::QueryAttribute.new(column_name, value, table.type(column_name)) - Arel::Nodes::BindParam.new(attr) + Relation::QueryAttribute.new(column_name, value, table.type(column_name)) end def resolve_arel_attribute(table_name, column_name, &block) diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb index cd18f27330..fdd69a806e 100644 --- a/activerecord/lib/active_record/relation/query_attribute.rb +++ b/activerecord/lib/active_record/relation/query_attribute.rb @@ -31,14 +31,10 @@ module ActiveRecord end def unboundable? - if defined?(@_unboundable) - @_unboundable - else - value_for_database unless value_before_type_cast.is_a?(StatementCache::Substitute) - @_unboundable = nil + unless defined?(@_unboundable) + @_unboundable = !type.serializable?(value) && type.cast(value) <=> 0 end - rescue ::RangeError - @_unboundable = type.cast(value_before_type_cast) <=> 0 + @_unboundable end private diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index d2285257c7..8863d12b83 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1130,10 +1130,9 @@ module ActiveRecord # scoping. def excluding(*records) records.flatten!(1) + records.compact! - raise ArgumentError, "You must pass at least one #{klass.name} object to #excluding." if records.empty? - - if records.any? { |record| !record.is_a?(klass) } + unless records.all?(klass) raise ArgumentError, "You must only pass a single or collection of #{klass.name} objects to #excluding." end @@ -1263,8 +1262,7 @@ module ActiveRecord end def build_cast_value(name, value) - cast_value = ActiveModel::Attribute.with_cast_value(name, value, Type.default_value) - Arel::Nodes::BindParam.new(cast_value) + ActiveModel::Attribute.with_cast_value(name, value, Type.default_value) end def build_from diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb index 37fbb12575..97ec0cdc0c 100644 --- a/activerecord/lib/active_record/relation/where_clause.rb +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -224,11 +224,10 @@ module ActiveRecord end def extract_node_value(node) - case node - when Array - node.map { |v| extract_node_value(v) } - when Arel::Nodes::BindParam, Arel::Nodes::Casted, Arel::Nodes::Quoted + if node.respond_to?(:value_before_type_cast) node.value_before_type_cast + elsif Array === node + node.map { |v| extract_node_value(v) } end end end diff --git a/activerecord/lib/arel/delete_manager.rb b/activerecord/lib/arel/delete_manager.rb index fdba937d64..707376a566 100644 --- a/activerecord/lib/arel/delete_manager.rb +++ b/activerecord/lib/arel/delete_manager.rb @@ -4,9 +4,9 @@ module Arel # :nodoc: all class DeleteManager < Arel::TreeManager include TreeManager::StatementMethods - def initialize - super - @ast = Nodes::DeleteStatement.new + def initialize(table = nil) + super() + @ast = Nodes::DeleteStatement.new(table) @ctx = @ast end diff --git a/activerecord/lib/arel/insert_manager.rb b/activerecord/lib/arel/insert_manager.rb index cb31e3060b..1fa7612e16 100644 --- a/activerecord/lib/arel/insert_manager.rb +++ b/activerecord/lib/arel/insert_manager.rb @@ -2,9 +2,9 @@ module Arel # :nodoc: all class InsertManager < Arel::TreeManager - def initialize - super - @ast = Nodes::InsertStatement.new + def initialize(table = nil) + super() + @ast = Nodes::InsertStatement.new(table) end def into(table) diff --git a/activerecord/lib/arel/nodes/casted.rb b/activerecord/lib/arel/nodes/casted.rb index 98a7f6d0e2..bd389f3930 100644 --- a/activerecord/lib/arel/nodes/casted.rb +++ b/activerecord/lib/arel/nodes/casted.rb @@ -47,7 +47,7 @@ module Arel # :nodoc: all def self.build_quoted(other, attribute = nil) case other - when Arel::Nodes::Node, Arel::Attributes::Attribute, Arel::Table, Arel::SelectManager, Arel::Nodes::SqlLiteral + when Arel::Nodes::Node, Arel::Attributes::Attribute, Arel::Table, Arel::SelectManager, Arel::Nodes::SqlLiteral, ActiveModel::Attribute other else case attribute diff --git a/activerecord/lib/arel/nodes/delete_statement.rb b/activerecord/lib/arel/nodes/delete_statement.rb index a419975335..0a1a3b0e94 100644 --- a/activerecord/lib/arel/nodes/delete_statement.rb +++ b/activerecord/lib/arel/nodes/delete_statement.rb @@ -3,17 +3,12 @@ module Arel # :nodoc: all module Nodes class DeleteStatement < Arel::Nodes::Node - attr_accessor :left, :right, :orders, :limit, :offset, :key - - alias :relation :left - alias :relation= :left= - alias :wheres :right - alias :wheres= :right= + attr_accessor :relation, :wheres, :orders, :limit, :offset, :key def initialize(relation = nil, wheres = []) super() - @left = relation - @right = wheres + @relation = relation + @wheres = wheres @orders = [] @limit = nil @offset = nil @@ -22,18 +17,18 @@ module Arel # :nodoc: all def initialize_copy(other) super - @left = @left.clone if @left - @right = @right.clone if @right + @relation = @relation.clone if @relation + @wheres = @wheres.clone if @wheres end def hash - [self.class, @left, @right, @orders, @limit, @offset, @key].hash + [self.class, @relation, @wheres, @orders, @limit, @offset, @key].hash end def eql?(other) self.class == other.class && - self.left == other.left && - self.right == other.right && + self.relation == other.relation && + self.wheres == other.wheres && self.orders == other.orders && self.limit == other.limit && self.offset == other.offset && diff --git a/activerecord/lib/arel/nodes/insert_statement.rb b/activerecord/lib/arel/nodes/insert_statement.rb index d28fd1f6c8..fdfc179e66 100644 --- a/activerecord/lib/arel/nodes/insert_statement.rb +++ b/activerecord/lib/arel/nodes/insert_statement.rb @@ -5,9 +5,9 @@ module Arel # :nodoc: all class InsertStatement < Arel::Nodes::Node attr_accessor :relation, :columns, :values, :select - def initialize + def initialize(relation = nil) super() - @relation = nil + @relation = relation @columns = [] @values = nil @select = nil diff --git a/activerecord/lib/arel/nodes/select_core.rb b/activerecord/lib/arel/nodes/select_core.rb index 11b4f39ece..a9bc7986d0 100644 --- a/activerecord/lib/arel/nodes/select_core.rb +++ b/activerecord/lib/arel/nodes/select_core.rb @@ -6,9 +6,9 @@ module Arel # :nodoc: all attr_accessor :projections, :wheres, :groups, :windows, :comment attr_accessor :havings, :source, :set_quantifier, :optimizer_hints - def initialize + def initialize(relation = nil) super() - @source = JoinSource.new nil + @source = JoinSource.new(relation) # https://ronsavage.github.io/SQL/sql-92.bnf.html#set%20quantifier @set_quantifier = nil diff --git a/activerecord/lib/arel/nodes/select_statement.rb b/activerecord/lib/arel/nodes/select_statement.rb index eff5dad939..ad2162b541 100644 --- a/activerecord/lib/arel/nodes/select_statement.rb +++ b/activerecord/lib/arel/nodes/select_statement.rb @@ -6,9 +6,9 @@ module Arel # :nodoc: all attr_reader :cores attr_accessor :limit, :orders, :lock, :offset, :with - def initialize(cores = [SelectCore.new]) + def initialize(relation = nil) super() - @cores = cores + @cores = [SelectCore.new(relation)] @orders = [] @limit = nil @lock = nil diff --git a/activerecord/lib/arel/nodes/update_statement.rb b/activerecord/lib/arel/nodes/update_statement.rb index cfaa19e392..c7554837e7 100644 --- a/activerecord/lib/arel/nodes/update_statement.rb +++ b/activerecord/lib/arel/nodes/update_statement.rb @@ -5,8 +5,9 @@ module Arel # :nodoc: all class UpdateStatement < Arel::Nodes::Node attr_accessor :relation, :wheres, :values, :orders, :limit, :offset, :key - def initialize - @relation = nil + def initialize(relation = nil) + super() + @relation = relation @wheres = [] @values = [] @orders = [] diff --git a/activerecord/lib/arel/predications.rb b/activerecord/lib/arel/predications.rb index 0d2d1acc93..cec54269d9 100644 --- a/activerecord/lib/arel/predications.rb +++ b/activerecord/lib/arel/predications.rb @@ -52,7 +52,7 @@ module Arel # :nodoc: all else left = quoted_node(other.begin) right = quoted_node(other.end) - Nodes::Between.new(self, left.and(right)) + Nodes::Between.new(self, Nodes::And.new([left, right])) end end @@ -207,11 +207,11 @@ module Arel # :nodoc: all end def contains(other) - Arel::Nodes::Contains.new(self, other) + Arel::Nodes::Contains.new self, quoted_node(other) end def overlaps(other) - Arel::Nodes::Overlaps.new(self, other) + Arel::Nodes::Overlaps.new self, quoted_node(other) end def quoted_array(others) diff --git a/activerecord/lib/arel/select_manager.rb b/activerecord/lib/arel/select_manager.rb index dd896cefb3..5f3e39a7f1 100644 --- a/activerecord/lib/arel/select_manager.rb +++ b/activerecord/lib/arel/select_manager.rb @@ -8,9 +8,8 @@ module Arel # :nodoc: all def initialize(table = nil) super() - @ast = Nodes::SelectStatement.new + @ast = Nodes::SelectStatement.new(table) @ctx = @ast.cores.last - from table end def initialize_copy(other) diff --git a/activerecord/lib/arel/table.rb b/activerecord/lib/arel/table.rb index 6790fb6e96..01464f7239 100644 --- a/activerecord/lib/arel/table.rb +++ b/activerecord/lib/arel/table.rb @@ -2,7 +2,6 @@ module Arel # :nodoc: all class Table - include Arel::Crud include Arel::FactoryMethods include Arel::AliasPredication diff --git a/activerecord/lib/arel/update_manager.rb b/activerecord/lib/arel/update_manager.rb index a809dbb307..434e82a0b0 100644 --- a/activerecord/lib/arel/update_manager.rb +++ b/activerecord/lib/arel/update_manager.rb @@ -4,9 +4,9 @@ module Arel # :nodoc: all class UpdateManager < Arel::TreeManager include TreeManager::StatementMethods - def initialize - super - @ast = Nodes::UpdateStatement.new + def initialize(table = nil) + super() + @ast = Nodes::UpdateStatement.new(table) @ctx = @ast end diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb index fac8c921da..f593f4980a 100644 --- a/activerecord/lib/arel/visitors/to_sql.rb +++ b/activerecord/lib/arel/visitors/to_sql.rb @@ -103,7 +103,7 @@ module Arel # :nodoc: all row.each_with_index do |value, k| collector << ", " unless k == 0 case value - when Nodes::SqlLiteral, Nodes::BindParam + when Nodes::SqlLiteral, Nodes::BindParam, ActiveModel::Attribute collector = visit(value, collector) else collector << quote(value).to_s @@ -585,7 +585,7 @@ module Arel # :nodoc: all def visit_Arel_Nodes_Assignment(o, collector) case o.right - when Arel::Nodes::Node, Arel::Attributes::Attribute + when Arel::Nodes::Node, Arel::Attributes::Attribute, ActiveModel::Attribute collector = visit o.left, collector collector << " = " visit o.right, collector @@ -695,6 +695,10 @@ module Arel # :nodoc: all def bind_block; BIND_BLOCK; end + def visit_ActiveModel_Attribute(o, collector) + collector.add_bind(o, &bind_block) + end + def visit_Arel_Nodes_BindParam(o, collector) collector.add_bind(o.value, &bind_block) end diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb index b5a197cae5..0d00830fa1 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -40,6 +40,9 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase expected = %(CREATE INDEX CONCURRENTLY "index_people_on_last_name" ON "people" ("last_name")) assert_equal expected, add_index(:people, :last_name, algorithm: :concurrently) + expected = %(CREATE INDEX CONCURRENTLY IF NOT EXISTS "index_people_on_last_name" ON "people" ("last_name")) + assert_equal expected, add_index(:people, :last_name, if_not_exists: true, algorithm: :concurrently) + expected = %(CREATE INDEX "index_people_on_last_name_and_first_name" ON "people" ("last_name" DESC, "first_name" ASC)) assert_equal expected, add_index(:people, [:last_name, :first_name], order: { last_name: :desc, first_name: :asc }) assert_equal expected, add_index(:people, ["last_name", :first_name], order: { last_name: :desc, "first_name" => :asc }) diff --git a/activerecord/test/cases/arel/attributes/attribute_test.rb b/activerecord/test/cases/arel/attributes/attribute_test.rb index cb3dd47c84..456fca2749 100644 --- a/activerecord/test/cases/arel/attributes/attribute_test.rb +++ b/activerecord/test/cases/arel/attributes/attribute_test.rb @@ -1038,15 +1038,13 @@ module Arel describe "#contains" do it "should create a Contains node" do relation = Table.new(:products) - query = Nodes.build_quoted("{foo,bar}") - _(relation[:tags].contains(query)).must_be_kind_of Nodes::Contains + _(relation[:tags].contains(["foo", "bar"])).must_be_kind_of Nodes::Contains end it "should generate @> in sql" do - relation = Table.new(:products) + relation = Table.new(:products, type_caster: fake_pg_caster) mgr = relation.project relation[:id] - query = Nodes.build_quoted("{foo,bar}") - mgr.where relation[:tags].contains(query) + mgr.where relation[:tags].contains(["foo", "bar"]) _(mgr.to_sql).must_be_like %{ SELECT "products"."id" FROM "products" WHERE "products"."tags" @> '{foo,bar}' } end end @@ -1054,15 +1052,13 @@ module Arel describe "#overlaps" do it "should create an Overlaps node" do relation = Table.new(:products) - query = Nodes.build_quoted("{foo,bar}") - _(relation[:tags].overlaps(query)).must_be_kind_of Nodes::Overlaps + _(relation[:tags].overlaps(["foo", "bar"])).must_be_kind_of Nodes::Overlaps end it "should generate && in sql" do - relation = Table.new(:products) + relation = Table.new(:products, type_caster: fake_pg_caster) mgr = relation.project relation[:id] - query = Nodes.build_quoted("{foo,bar}") - mgr.where relation[:tags].overlaps(query) + mgr.where relation[:tags].overlaps(["foo", "bar"]) _(mgr.to_sql).must_be_like %{ SELECT "products"."id" FROM "products" WHERE "products"."tags" && '{foo,bar}' } end end @@ -1123,6 +1119,19 @@ module Arel exclude_end?: exclude, ) end + + # Mimic PG::TextDecoder::Array casting + def fake_pg_caster + Object.new.tap do |caster| + def caster.type_cast_for_database(attr_name, value) + if attr_name == "tags" + "{#{value.join(",")}}" + else + value + end + end + end + end end end end diff --git a/activerecord/test/cases/arel/table_test.rb b/activerecord/test/cases/arel/table_test.rb index 90c281bbd8..fab59052bc 100644 --- a/activerecord/test/cases/arel/table_test.rb +++ b/activerecord/test/cases/arel/table_test.rb @@ -42,13 +42,6 @@ module Arel assert_equal "bar", join.right end - it "should return an insert manager" do - im = @relation.compile_insert "VALUES(NULL)" - assert_kind_of Arel::InsertManager, im - im.into Table.new(:users) - assert_equal "INSERT INTO \"users\" VALUES(NULL)", im.to_sql - end - describe "skip" do it "should add an offset" do sm = @relation.skip 2 diff --git a/activerecord/test/cases/asynchronous_queries_test.rb b/activerecord/test/cases/asynchronous_queries_test.rb index cc7da91ab6..ba8fec4dff 100644 --- a/activerecord/test/cases/asynchronous_queries_test.rb +++ b/activerecord/test/cases/asynchronous_queries_test.rb @@ -178,15 +178,15 @@ class AsynchronousExecutorTypeTest < ActiveRecord::TestCase assert async_pool1.is_a?(Concurrent::ThreadPoolExecutor) assert async_pool2.is_a?(Concurrent::ThreadPoolExecutor) - assert 0, async_pool1.min_length - assert 4, async_pool1.max_length - assert 16, async_pool1.max_queue - assert :caller_runs, async_pool1.fallback_policy + assert_equal 0, async_pool1.min_length + assert_equal 4, async_pool1.max_length + assert_equal 16, async_pool1.max_queue + assert_equal :caller_runs, async_pool1.fallback_policy - assert 0, async_pool2.min_length - assert 4, async_pool2.max_length - assert 16, async_pool2.max_queue - assert :caller_runs, async_pool2.fallback_policy + assert_equal 0, async_pool2.min_length + assert_equal 4, async_pool2.max_length + assert_equal 16, async_pool2.max_queue + assert_equal :caller_runs, async_pool2.fallback_policy assert_equal 2, handler.all_connection_pools.count assert_equal async_pool1, async_pool2 @@ -199,6 +199,8 @@ class AsynchronousExecutorTypeTest < ActiveRecord::TestCase old_value = ActiveRecord::Base.async_query_executor ActiveRecord::Base.async_query_executor = :global_thread_pool old_concurrency = ActiveRecord::Base.global_executor_concurrency + old_global_thread_pool_async_query_executor = ActiveRecord::Core.class_variable_get(:@@global_thread_pool_async_query_executor) + ActiveRecord::Core.class_variable_set(:@@global_thread_pool_async_query_executor, nil) ActiveRecord::Base.global_executor_concurrency = 8 handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new @@ -213,15 +215,15 @@ class AsynchronousExecutorTypeTest < ActiveRecord::TestCase assert async_pool1.is_a?(Concurrent::ThreadPoolExecutor) assert async_pool2.is_a?(Concurrent::ThreadPoolExecutor) - assert 0, async_pool1.min_length - assert 8, async_pool1.max_length - assert 32, async_pool1.max_queue - assert :caller_runs, async_pool1.fallback_policy + assert_equal 0, async_pool1.min_length + assert_equal 8, async_pool1.max_length + assert_equal 32, async_pool1.max_queue + assert_equal :caller_runs, async_pool1.fallback_policy - assert 0, async_pool2.min_length - assert 8, async_pool2.max_length - assert 32, async_pool2.max_queue - assert :caller_runs, async_pool2.fallback_policy + assert_equal 0, async_pool2.min_length + assert_equal 8, async_pool2.max_length + assert_equal 32, async_pool2.max_queue + assert_equal :caller_runs, async_pool2.fallback_policy assert_equal 2, handler.all_connection_pools.count assert_equal async_pool1, async_pool2 @@ -229,6 +231,7 @@ class AsynchronousExecutorTypeTest < ActiveRecord::TestCase clean_up_connection_handler ActiveRecord::Base.global_executor_concurrency = old_concurrency ActiveRecord::Base.async_query_executor = old_value + ActiveRecord::Core.class_variable_set(:@@global_thread_pool_async_query_executor, old_global_thread_pool_async_query_executor) end def test_concurrency_cannot_be_set_with_null_executor_or_multi_thread_pool @@ -248,7 +251,7 @@ class AsynchronousExecutorTypeTest < ActiveRecord::TestCase ActiveRecord::Base.async_query_executor = old_value end - def test_one_global_thread_pool_uses_concurrency_if_set + def test_multi_thread_pool_executor_configuration old_value = ActiveRecord::Base.async_query_executor ActiveRecord::Base.async_query_executor = :multi_thread_pool @@ -266,15 +269,50 @@ class AsynchronousExecutorTypeTest < ActiveRecord::TestCase assert async_pool1.is_a?(Concurrent::ThreadPoolExecutor) assert async_pool2.is_a?(Concurrent::ThreadPoolExecutor) - assert 0, async_pool1.min_length - assert 10, async_pool1.max_length - assert 40, async_pool1.max_queue - assert :caller_runs, async_pool1.fallback_policy + assert_equal 0, async_pool1.min_length + assert_equal 10, async_pool1.max_length + assert_equal 40, async_pool1.max_queue + assert_equal :caller_runs, async_pool1.fallback_policy - assert 0, async_pool2.min_length - assert 4, async_pool2.max_length - assert 16, async_pool2.max_queue - assert :caller_runs, async_pool2.fallback_policy + assert_equal 0, async_pool2.min_length + assert_equal 5, async_pool2.max_length + assert_equal 20, async_pool2.max_queue + assert_equal :caller_runs, async_pool2.fallback_policy + + assert_equal 2, handler.all_connection_pools.count + assert_not_equal async_pool1, async_pool2 + ensure + clean_up_connection_handler + ActiveRecord::Base.async_query_executor = old_value + end + + def test_multi_thread_pool_is_used_only_by_configurations_that_enable_it + old_value = ActiveRecord::Base.async_query_executor + ActiveRecord::Base.async_query_executor = :multi_thread_pool + + handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new + + config_hash1 = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary").configuration_hash + new_config1 = config_hash1.merge(min_threads: 0, max_threads: 10) + db_config1 = ActiveRecord::DatabaseConfigurations::HashConfig.new("arunit", "primary", new_config1) + + config_hash2 = ActiveRecord::Base.configurations.configs_for(env_name: "arunit2", name: "primary").configuration_hash + new_config2 = config_hash2.merge(min_threads: 0, max_threads: 0) + db_config2 = ActiveRecord::DatabaseConfigurations::HashConfig.new("arunit2", "primary", new_config2) + + pool1 = handler.establish_connection(db_config1) + pool2 = handler.establish_connection(db_config2, owner_name: ARUnit2Model) + + async_pool1 = pool1.instance_variable_get(:@async_executor) + async_pool2 = pool2.instance_variable_get(:@async_executor) + + assert async_pool1.is_a?(Concurrent::ThreadPoolExecutor) + assert_nil async_pool2 + + assert_equal 0, async_pool1.min_length + assert_equal 10, async_pool1.max_length + assert_equal 40, async_pool1.max_queue + assert_equal :caller_runs, async_pool1.fallback_policy assert_equal 2, handler.all_connection_pools.count assert_not_equal async_pool1, async_pool2 diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 4e42cade62..0e20ceb3a1 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -430,14 +430,14 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal "a", topic[:title] end - test "read overriden attribute with predicate respects override" do + test "read overridden attribute with predicate respects override" do topic = Topic.new topic.approved = true def topic.approved; false; end - assert_not topic.approved?, "overriden approved should be false" + assert_not topic.approved?, "overridden approved should be false" end test "string attribute predicate" do diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 04bc9a95ed..7e12f2f13b 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -1162,15 +1162,15 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal({ "proposed" => 2, "published" => 2 }, Book.group(:status).count) end - def test_aggregate_attribute_on_custom_type - assert_nil Book.sum(:status) - assert_equal "medium", Book.sum(:difficulty) - assert_equal "easy", Book.minimum(:difficulty) - assert_equal "medium", Book.maximum(:difficulty) - assert_equal({ "proposed" => "proposed", "published" => nil }, Book.group(:status).sum(:status)) - assert_equal({ "proposed" => "easy", "published" => "medium" }, Book.group(:status).sum(:difficulty)) - assert_equal({ "proposed" => "easy", "published" => "easy" }, Book.group(:status).minimum(:difficulty)) - assert_equal({ "proposed" => "easy", "published" => "medium" }, Book.group(:status).maximum(:difficulty)) + def test_aggregate_attribute_on_enum_type + assert_equal 4, Book.sum(:status) + assert_equal 1, Book.sum(:difficulty) + assert_equal 0, Book.minimum(:difficulty) + assert_equal 1, Book.maximum(:difficulty) + assert_equal({ "proposed" => 0, "published" => 4 }, Book.group(:status).sum(:status)) + assert_equal({ "proposed" => 0, "published" => 1 }, Book.group(:status).sum(:difficulty)) + assert_equal({ "proposed" => 0, "published" => 0 }, Book.group(:status).minimum(:difficulty)) + assert_equal({ "proposed" => 0, "published" => 1 }, Book.group(:status).maximum(:difficulty)) end def test_minimum_and_maximum_on_non_numeric_type diff --git a/activerecord/test/cases/database_configurations/hash_config_test.rb b/activerecord/test/cases/database_configurations/hash_config_test.rb index f7991d5866..c898b4bdf5 100644 --- a/activerecord/test/cases/database_configurations/hash_config_test.rb +++ b/activerecord/test/cases/database_configurations/hash_config_test.rb @@ -47,7 +47,7 @@ module ActiveRecord assert_equal 1, config.max_threads end - def test_max_queue_is_pool_multipled_by_4 + def test_max_queue_is_pool_multiplied_by_4 config = HashConfig.new("default_env", "primary", {}) assert_equal 5, config.max_threads assert_equal config.max_threads * 4, config.max_queue diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index c50833b62f..dcdfe2dd89 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -80,6 +80,7 @@ class EnumTest < ActiveRecord::TestCase assert_not_equal @book, Book.where.not(status: :published).first assert_equal @book, Book.where.not(status: :written).first assert_equal books(:ddd), Book.where(last_read: :forgotten).first + assert_nil Book.where(status: :prohibited).first end test "find via where with strings" do @@ -90,6 +91,11 @@ class EnumTest < ActiveRecord::TestCase assert_not_equal @book, Book.where.not(status: "published").first assert_equal @book, Book.where.not(status: "written").first assert_equal books(:ddd), Book.where(last_read: "forgotten").first + assert_nil Book.where(status: "prohibited").first + end + + test "find via where with large number" do + assert_equal @book, Book.where(status: [2, 9223372036854775808]).first end test "find via where should be type casted" do diff --git a/activerecord/test/cases/excluding_test.rb b/activerecord/test/cases/excluding_test.rb index 35fe26c614..e2fb7d61a3 100644 --- a/activerecord/test/cases/excluding_test.rb +++ b/activerecord/test/cases/excluding_test.rb @@ -7,71 +7,52 @@ require "models/comment" class ExcludingTest < ActiveRecord::TestCase fixtures :posts, :comments - def test_result_set_does_not_include_single_excluded_record - post = posts(:welcome) + setup { @post = posts(:welcome) } - assert_not_includes Post.excluding(post).to_a, post + def test_result_set_does_not_include_single_excluded_record + assert_not_includes Post.excluding(@post), @post + assert_not_includes Post.excluding(@post).to_a, @post + + assert_not_includes Post.without(@post), @post end def test_result_set_does_not_include_collection_of_excluded_records - post_welcome = posts(:welcome) - post_thinking = posts(:thinking) - - relation = Post.excluding(post_welcome, post_thinking) - - assert_not_includes relation.to_a, post_welcome - assert_not_includes relation.to_a, post_thinking + relation = Post.excluding(@post, posts(:thinking)) + assert_not_includes relation, @post + assert_not_includes relation, posts(:thinking) end def test_result_set_through_association_does_not_include_single_excluded_record - post = posts(:welcome) - comment_greetings = comments(:greetings) - comment_more_greetings = comments(:more_greetings) + comment_greetings, comment_more_greetings = comments(:greetings, :more_greetings) - relation = post.comments.excluding(comment_greetings) - - assert_not_includes relation.to_a, comment_greetings - assert_includes relation.to_a, comment_more_greetings + relation = @post.comments.excluding(comment_greetings) + assert_not_includes relation, comment_greetings + assert_includes relation, comment_more_greetings end def test_result_set_through_association_does_not_include_collection_of_excluded_records - post = posts(:welcome) - comment_greetings = comments(:greetings) - comment_more_greetings = comments(:more_greetings) + comment_greetings, comment_more_greetings = comments(:greetings, :more_greetings) - relation = post.comments.excluding([comment_greetings, comment_more_greetings]) - - assert_not_includes relation.to_a, comment_greetings - assert_not_includes relation.to_a, comment_more_greetings + relation = @post.comments.excluding([ comment_greetings, comment_more_greetings ]) + assert_not_includes relation, comment_greetings + assert_not_includes relation, comment_more_greetings end - def test_raises_on_no_arguments - exception = assert_raises ArgumentError do - Post.excluding() - end - assert_equal "You must pass at least one Post object to #excluding.", exception.message - end - - def test_raises_on_empty_collection_argument - exception = assert_raises ArgumentError do - Post.excluding([]) - end - assert_equal "You must pass at least one Post object to #excluding.", exception.message + def test_does_not_exclude_records_when_no_arguments + assert_no_excludes Post.excluding + assert_no_excludes Post.excluding(nil) + assert_no_excludes Post.excluding([]) + assert_no_excludes Post.excluding([ nil ]) end def test_raises_on_record_from_different_class - post = posts(:welcome) - comment = comments(:greetings) + error = assert_raises(ArgumentError) { Post.excluding(@post, comments(:greetings)) } + assert_equal "You must only pass a single or collection of Post objects to #excluding.", error.message + end - exception = assert_raises ArgumentError do - Post.excluding(post, comment) + private + def assert_no_excludes(relation) + assert_includes relation, @post + assert_equal Post.count, relation.count end - assert_equal "You must only pass a single or collection of Post objects to #excluding.", exception.message - end - - def test_result_set_does_not_include_without_record - post = posts(:welcome) - - assert_not_includes Post.without(post).to_a, post - end end diff --git a/activerecord/test/cases/relation/load_async_test.rb b/activerecord/test/cases/relation/load_async_test.rb index 72df32a9dc..bacde8c4ab 100644 --- a/activerecord/test/cases/relation/load_async_test.rb +++ b/activerecord/test/cases/relation/load_async_test.rb @@ -3,6 +3,7 @@ require "cases/helper" require "models/post" require "models/comment" +require "models/other_dog" module ActiveRecord class LoadAsyncTest < ActiveRecord::TestCase @@ -280,5 +281,234 @@ module ActiveRecord assert_predicate defered_posts, :loaded? end end + + class LoadAsyncMultiThreadPoolExecutorTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + fixtures :posts, :comments + + def setup + @old_config = ActiveRecord::Base.async_query_executor + ActiveRecord::Base.async_query_executor = :multi_thread_pool + + handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new + config_hash1 = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary").configuration_hash + new_config1 = config_hash1.merge(min_threads: 0, max_threads: 10) + db_config1 = ActiveRecord::DatabaseConfigurations::HashConfig.new("arunit", "primary", new_config1) + + + config_hash2 = ActiveRecord::Base.configurations.configs_for(env_name: "arunit2", name: "primary").configuration_hash + new_config2 = config_hash2.merge(min_threads: 0, max_threads: 10) + db_config2 = ActiveRecord::DatabaseConfigurations::HashConfig.new("arunit2", "primary", new_config2) + + handler.establish_connection(db_config1) + handler.establish_connection(db_config2, owner_name: ARUnit2Model) + end + + def teardown + ActiveRecord::Base.async_query_executor = @old_config + clean_up_connection_handler + end + + def test_scheduled? + defered_posts = Post.where(author_id: 1).load_async + assert_predicate defered_posts, :scheduled? + assert_predicate defered_posts, :loaded? + defered_posts.to_a + assert_not_predicate defered_posts, :scheduled? + end + + def test_reset + defered_posts = Post.where(author_id: 1).load_async + assert_predicate defered_posts, :scheduled? + defered_posts.reset + assert_not_predicate defered_posts, :scheduled? + end + + def test_simple_query + expected_records = Post.where(author_id: 1).to_a + + status = {} + monitor = Monitor.new + condition = monitor.new_cond + + subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |event| + if event.payload[:name] == "Post Load" + status[:executed] = true + status[:async] = event.payload[:async] + monitor.synchronize { condition.signal } + end + end + + defered_posts = Post.where(author_id: 1).load_async + + monitor.synchronize do + condition.wait_until { status[:executed] } + end + + assert_equal expected_records, defered_posts.to_a + assert_equal Post.connection.supports_concurrent_connections?, status[:async] + ensure + ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber + end + + def test_load_async_from_transaction + posts = nil + Post.transaction do + Post.where(author_id: 1).update_all(title: "In Transaction") + posts = Post.where(author_id: 1).load_async + assert_predicate posts, :scheduled? + assert_predicate posts, :loaded? + raise ActiveRecord::Rollback + end + + assert_not_nil posts + assert_equal ["In Transaction"], posts.map(&:title).uniq + end + + def test_eager_loading_query + expected_records = Post.where(author_id: 1).eager_load(:comments).to_a + + status = {} + monitor = Monitor.new + condition = monitor.new_cond + + subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |event| + if event.payload[:name] == "SQL" + status[:executed] = true + status[:async] = event.payload[:async] + monitor.synchronize { condition.signal } + end + end + + defered_posts = Post.where(author_id: 1).eager_load(:comments).load_async + + assert_predicate defered_posts, :scheduled? + + monitor.synchronize do + condition.wait_until { status[:executed] } + end + + assert_equal expected_records, defered_posts.to_a + assert_queries(0) do + defered_posts.each(&:comments) + end + assert_equal Post.connection.supports_concurrent_connections?, status[:async] + ensure + ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber + end + + def test_contradiction + assert_queries(0) do + assert_equal [], Post.where(id: []).load_async.to_a + end + + Post.where(id: []).load_async.reset + end + + def test_pluck + titles = Post.where(author_id: 1).pluck(:title) + assert_equal titles, Post.where(author_id: 1).load_async.pluck(:title) + end + + def test_size + expected_size = Post.where(author_id: 1).size + + defered_posts = Post.where(author_id: 1).load_async + + assert_equal expected_size, defered_posts.size + assert_predicate defered_posts, :loaded? + end + + def test_empty? + defered_posts = Post.where(author_id: 1).load_async + + assert_equal false, defered_posts.empty? + assert_predicate defered_posts, :loaded? + end + end + + class LoadAsyncMixedThreadPoolExecutorTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + fixtures :posts, :comments, :other_dogs + + def setup + @previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + @old_config = ActiveRecord::Base.async_query_executor + ActiveRecord::Base.async_query_executor = :multi_thread_pool + config_hash1 = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary").configuration_hash + config_hash2 = ActiveRecord::Base.configurations.configs_for(env_name: "arunit2", name: "primary").configuration_hash + config = { + "default_env" => { + "animals" => config_hash2.merge({ min_threads: 0, max_threads: 0 }), + "primary" => config_hash1.merge({ min_threads: 0, max_threads: 10 }) + } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + ActiveRecord::Base.establish_connection(:primary) + ARUnit2Model.establish_connection(:animals) + end + + def teardown + ENV["RAILS_ENV"] = @previous_env + ActiveRecord::Base.configurations = @prev_configs + ActiveRecord::Base.async_query_executor = @old_config + clean_up_connection_handler + end + + def test_scheduled? + defered_posts = Post.where(author_id: 1).load_async + assert_predicate defered_posts, :scheduled? + assert_predicate defered_posts, :loaded? + defered_posts.to_a + assert_not_predicate defered_posts, :scheduled? + + defered_dogs = OtherDog.where(id: 1).load_async + assert_not_predicate defered_dogs, :scheduled? + assert_predicate defered_dogs, :loaded? + end + + def test_simple_query + expected_records = Post.where(author_id: 1).to_a + expected_dogs = OtherDog.where(id: 1).to_a + + status = {} + dog_status = {} + monitor = Monitor.new + condition = monitor.new_cond + + subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |event| + if event.payload[:name] == "Post Load" + status[:executed] = true + status[:async] = event.payload[:async] + monitor.synchronize { condition.signal } + end + + if event.payload[:name] == "OtherDog Load" + dog_status[:executed] = true + dog_status[:async] = event.payload[:async] + monitor.synchronize { condition.signal } + end + end + + defered_posts = Post.where(author_id: 1).load_async + defered_dogs = OtherDog.where(id: 1).load_async + + monitor.synchronize do + condition.wait_until { status[:executed] } + condition.wait_until { dog_status[:executed] } + end + + assert_equal expected_records, defered_posts.to_a + assert_equal expected_dogs, defered_dogs.to_a + + assert_equal Post.connection.async_enabled?, status[:async] + assert_equal OtherDog.connection.async_enabled?, dog_status[:async] + ensure + ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber + end + end end end diff --git a/activerecord/test/cases/relation/select_test.rb b/activerecord/test/cases/relation/select_test.rb index 7c98df8424..7a7ee9299a 100644 --- a/activerecord/test/cases/relation/select_test.rb +++ b/activerecord/test/cases/relation/select_test.rb @@ -41,6 +41,20 @@ module ActiveRecord end private :assert_non_select_columns_wont_be_loaded + def test_merging_select_from_different_model + posts = Post.select(:id, :title).joins(:comments) + comments = Comment.where(body: "Thank you for the welcome") + + [ + posts.merge(comments.select(:body)).first, + posts.merge(comments.select("comments.body")).first, + ].each do |post| + assert_equal 1, post.id + assert_equal "Welcome to the weblog", post.title + assert_equal "Thank you for the welcome", post.body + end + end + 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 diff --git a/activerecord/test/cases/validations/numericality_validation_test.rb b/activerecord/test/cases/validations/numericality_validation_test.rb index 941637a860..80d3e9a9e9 100644 --- a/activerecord/test/cases/validations/numericality_validation_test.rb +++ b/activerecord/test/cases/validations/numericality_validation_test.rb @@ -102,7 +102,7 @@ class NumericalityValidationTest < ActiveRecord::TestCase if RUBY_VERSION > "3.0.0" # BigDecimal's to_d behavior changed in BigDecimal 3.0.1, see https://github.com/ruby/bigdecimal/issues/70 - # TOOD: replace this with a check against BigDecimal::VERSION + # TODO: replace this with a check against BigDecimal::VERSION assert_not_predicate subject, :valid? else assert_predicate subject, :valid? diff --git a/activestorage/README.md b/activestorage/README.md index b5bca0af26..6cc791c839 100644 --- a/activestorage/README.md +++ b/activestorage/README.md @@ -32,7 +32,7 @@ class User < ApplicationRecord end # Attach an avatar to the user. -user.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpg") +user.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpeg") # Does the user have an avatar? user.avatar.attached? # => true diff --git a/activestorage/lib/active_storage/attached/many.rb b/activestorage/lib/active_storage/attached/many.rb index 913b534f52..5afa3a3951 100644 --- a/activestorage/lib/active_storage/attached/many.rb +++ b/activestorage/lib/active_storage/attached/many.rb @@ -25,7 +25,7 @@ module ActiveStorage # # document.images.attach(params[:images]) # Array of ActionDispatch::Http::UploadedFile objects # document.images.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload - # document.images.attach(io: File.open("/path/to/racecar.jpg"), filename: "racecar.jpg", content_type: "image/jpg") + # document.images.attach(io: File.open("/path/to/racecar.jpg"), filename: "racecar.jpg", content_type: "image/jpeg") # document.images.attach([ first_blob, second_blob ]) def attach(*attachables) if record.persisted? && !record.changed? diff --git a/activestorage/lib/active_storage/attached/one.rb b/activestorage/lib/active_storage/attached/one.rb index 2e8b2a91cf..73527b08d2 100644 --- a/activestorage/lib/active_storage/attached/one.rb +++ b/activestorage/lib/active_storage/attached/one.rb @@ -25,7 +25,7 @@ module ActiveStorage # # person.avatar.attach(params[:avatar]) # ActionDispatch::Http::UploadedFile object # person.avatar.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload - # person.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpg") + # person.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpeg") # person.avatar.attach(avatar_blob) # ActiveStorage::Blob object def attach(attachable) if record.persisted? && !record.changed? diff --git a/activestorage/test/controllers/disk_controller_test.rb b/activestorage/test/controllers/disk_controller_test.rb index f0248f6854..0183855ed3 100644 --- a/activestorage/test/controllers/disk_controller_test.rb +++ b/activestorage/test/controllers/disk_controller_test.rb @@ -5,12 +5,12 @@ require "database/setup" class ActiveStorage::DiskControllerTest < ActionDispatch::IntegrationTest test "showing blob inline" do - blob = create_blob(filename: "hello.jpg", content_type: "image/jpg") + blob = create_blob(filename: "hello.jpg", content_type: "image/jpeg") get blob.url assert_response :ok assert_equal "inline; filename=\"hello.jpg\"; filename*=UTF-8''hello.jpg", response.headers["Content-Disposition"] - assert_equal "image/jpg", response.headers["Content-Type"] + assert_equal "image/jpeg", response.headers["Content-Type"] assert_equal "Hello world!", response.body end @@ -46,11 +46,11 @@ class ActiveStorage::DiskControllerTest < ActionDispatch::IntegrationTest test "showing public blob" do with_service("local_public") do - blob = create_blob(content_type: "image/jpg") + blob = create_blob(content_type: "image/jpeg") get blob.url assert_response :ok - assert_equal "image/jpg", response.headers["Content-Type"] + assert_equal "image/jpeg", response.headers["Content-Type"] assert_equal "Hello world!", response.body end end diff --git a/activestorage/test/models/attached/many_test.rb b/activestorage/test/models/attached/many_test.rb index 1cfdd3182b..410abdac1c 100644 --- a/activestorage/test/models/attached/many_test.rb +++ b/activestorage/test/models/attached/many_test.rb @@ -31,7 +31,7 @@ class ActiveStorage::ManyAttachedTest < ActiveSupport::TestCase test "attaching new blobs from Hashes to an existing record" do @user.highlights.attach( - { io: StringIO.new("STUFF"), filename: "funky.jpg", content_type: "image/jpg" }, + { io: StringIO.new("STUFF"), filename: "funky.jpg", content_type: "image/jpeg" }, { io: StringIO.new("THINGS"), filename: "town.jpg", content_type: "image/jpeg" }) assert_equal "funky.jpg", @user.highlights.first.filename.to_s @@ -81,7 +81,7 @@ class ActiveStorage::ManyAttachedTest < ActiveSupport::TestCase assert @user.changed? @user.highlights.attach( - { io: StringIO.new("STUFF"), filename: "funky.jpg", content_type: "image/jpg" }, + { io: StringIO.new("STUFF"), filename: "funky.jpg", content_type: "image/jpeg" }, { io: StringIO.new("THINGS"), filename: "town.jpg", content_type: "image/jpeg" }) assert_equal "funky.jpg", @user.highlights.first.filename.to_s @@ -337,8 +337,8 @@ class ActiveStorage::ManyAttachedTest < ActiveSupport::TestCase test "attaching new blobs from Hashes to a new record" do User.new(name: "Jason").tap do |user| user.highlights.attach( - { io: StringIO.new("STUFF"), filename: "funky.jpg", content_type: "image/jpg" }, - { io: StringIO.new("THINGS"), filename: "town.jpg", content_type: "image/jpg" }) + { io: StringIO.new("STUFF"), filename: "funky.jpg", content_type: "image/jpeg" }, + { io: StringIO.new("THINGS"), filename: "town.jpg", content_type: "image/jpeg" }) assert user.new_record? assert user.highlights.first.new_record? @@ -600,8 +600,8 @@ class ActiveStorage::ManyAttachedTest < ActiveSupport::TestCase test "attaching a new blob from a Hash with a custom service" do with_service("mirror") do - @user.highlights.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" - @user.vlogs.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" + @user.highlights.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpeg" + @user.vlogs.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpeg" assert_instance_of ActiveStorage::Service::MirrorService, @user.highlights.first.service assert_instance_of ActiveStorage::Service::DiskService, @user.vlogs.first.service diff --git a/activestorage/test/models/attached/one_test.rb b/activestorage/test/models/attached/one_test.rb index ac42dc5c65..6c5f885bb5 100644 --- a/activestorage/test/models/attached/one_test.rb +++ b/activestorage/test/models/attached/one_test.rb @@ -38,12 +38,12 @@ class ActiveStorage::OneAttachedTest < ActiveSupport::TestCase end test "attaching a new blob from a Hash to an existing record" do - @user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" + @user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpeg" assert_equal "town.jpg", @user.avatar.filename.to_s end test "attaching a new blob from a Hash to an existing record passes record" do - hash = { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" } + hash = { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpeg" } blob = ActiveStorage::Blob.build_after_unfurling(**hash) arguments = [hash.merge(record: @user, service_name: nil)] assert_called_with(ActiveStorage::Blob, :build_after_unfurling, arguments, returns: blob) do @@ -98,7 +98,7 @@ class ActiveStorage::OneAttachedTest < ActiveSupport::TestCase @user.name = "Tina" assert @user.changed? - @user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" + @user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpeg" assert_equal "town.jpg", @user.avatar.filename.to_s assert_not @user.avatar.persisted? assert @user.will_save_change_to_name? @@ -320,7 +320,7 @@ class ActiveStorage::OneAttachedTest < ActiveSupport::TestCase 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 = Group.create!(users_attributes: [{ name: "John", avatar: { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpeg" } }]) group.save! new_user = User.find_by(name: "John") assert new_user.avatar.attached? @@ -358,7 +358,7 @@ class ActiveStorage::OneAttachedTest < ActiveSupport::TestCase test "attaching a new blob from a Hash to a new record" do User.new(name: "Jason").tap do |user| - user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" + user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpeg" assert user.new_record? assert user.avatar.attachment.new_record? assert user.avatar.blob.new_record? @@ -582,8 +582,8 @@ class ActiveStorage::OneAttachedTest < ActiveSupport::TestCase test "attaching a new blob from a Hash with a custom service" do with_service("mirror") do - @user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" - @user.cover_photo.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" + @user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpeg" + @user.cover_photo.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpeg" assert_instance_of ActiveStorage::Service::MirrorService, @user.avatar.service assert_instance_of ActiveStorage::Service::DiskService, @user.cover_photo.service diff --git a/activestorage/test/models/variant_test.rb b/activestorage/test/models/variant_test.rb index 705ea89b0d..e89f075ce2 100644 --- a/activestorage/test/models/variant_test.rb +++ b/activestorage/test/models/variant_test.rb @@ -60,16 +60,6 @@ class ActiveStorage::VariantTest < ActiveSupport::TestCase assert_match(/RGB/, image.colorspace) end - test "variation with :combine_options is not supported" do - blob = create_file_blob(filename: "racecar.jpg") - assert_raises(ArgumentError) do - blob.variant(combine_options: { - resize: "100x100", - monochrome: false - }).processed - end - end - test "center-weighted crop of JPEG blob using :resize_to_fill" do blob = create_file_blob(filename: "racecar.jpg") variant = blob.variant(resize_to_fill: [100, 100]).processed diff --git a/activestorage/test/service/shared_service_tests.rb b/activestorage/test/service/shared_service_tests.rb index 3d4884b50c..c6d83a3553 100644 --- a/activestorage/test/service/shared_service_tests.rb +++ b/activestorage/test/service/shared_service_tests.rb @@ -50,7 +50,7 @@ module ActiveStorage::Service::SharedServiceTests StringIO.new(data), checksum: Digest::MD5.base64digest(data), filename: "racecar.jpg", - content_type: "image/jpg" + content_type: "image/jpeg" ) assert_equal data, @service.download(key) diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index a86af00527..a65ff5cf72 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -289,21 +289,17 @@ module ActiveSupport end attr_accessor :kind, :name - attr_reader :chain_config + attr_reader :chain_config, :filter def initialize(name, filter, kind, options, chain_config) @chain_config = chain_config @name = name @kind = kind @filter = filter - @key = compute_identifier filter @if = check_conditionals(options[:if]) @unless = check_conditionals(options[:unless]) end - def filter; @key; end - def raw_filter; @filter; end - def merge_conditional_options(chain, if_option:, unless_option:) options = { if: @if.dup, @@ -367,15 +363,6 @@ module ActiveSupport conditionals.freeze end - def compute_identifier(filter) - case filter - when ::Proc - filter.object_id - else - filter - end - end - def conditions_lambdas @if.map { |c| CallTemplate.build(c, self).make_lambda } + @unless.map { |c| CallTemplate.build(c, self).inverted_lambda } diff --git a/activesupport/lib/active_support/current_attributes.rb b/activesupport/lib/active_support/current_attributes.rb index 3d3031ad94..93d6131516 100644 --- a/activesupport/lib/active_support/current_attributes.rb +++ b/activesupport/lib/active_support/current_attributes.rb @@ -2,6 +2,7 @@ require "active_support/callbacks" require "active_support/core_ext/enumerable" +require "active_support/core_ext/module/delegation" module ActiveSupport # Abstract super class that provides a thread-isolated attributes singleton, which resets automatically @@ -155,14 +156,15 @@ module ActiveSupport @current_instances_key ||= name.to_sym end - def method_missing(name, *args, **kwargs, &block) + def method_missing(name, *args, &block) # Caches the method definition as a singleton method of the receiver. # # By letting #delegate handle it, we avoid an enclosure that'll capture args. singleton_class.delegate name, to: :instance - send(name, *args, **kwargs, &block) + send(name, *args, &block) end + ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true) def respond_to_missing?(name, _) super || instance.respond_to?(name) diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 552ba7ee33..de124a3c81 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -86,12 +86,6 @@ module ActiveSupport #:nodoc: # to allow arbitrary constants to be marked for unloading. mattr_accessor :explicitly_unloadable_constants, default: [] - # The logger used when tracing autoloads. - mattr_accessor :logger - - # If true, trace autoloads with +logger.debug+. - mattr_accessor :verbose, default: false - # The WatchStack keeps a stack of the modules being watched as files are # loaded. If a file in the process of being loaded (parent.rb) triggers the # load of another file (child.rb) the stack will ensure that child.rb @@ -298,9 +292,6 @@ module ActiveSupport #:nodoc: else yield end - rescue Exception => exception # errors from loading file - exception.blame_file! file if exception.respond_to? :blame_file! - raise end # Mark the given constant as unloadable. Unloadable constants are removed @@ -334,31 +325,9 @@ module ActiveSupport #:nodoc: end end - # Exception file-blaming. - module Blamable #:nodoc: - def blame_file!(file) - (@blamed_files ||= []).unshift file - end - - def blamed_files - @blamed_files ||= [] - end - - def describe_blame - return nil if blamed_files.empty? - "This error occurred while loading the following files:\n #{blamed_files.join "\n "}" - end - - def copy_blame!(exc) - @blamed_files = exc.blamed_files.clone - self - end - end - def hook! Loadable.include_into(Object) ModuleConstMissing.include_into(Module) - Exception.include(Blamable) end def unhook! @@ -381,7 +350,6 @@ module ActiveSupport #:nodoc: load_error.message end load_error_message.replace(message % file_name) - load_error.copy_blame!(load_error) end raise end @@ -500,7 +468,6 @@ module ActiveSupport #:nodoc: return nil unless base_path = autoloadable_module?(path_suffix) mod = Module.new into.const_set const_name, mod - log("constant #{qualified_name} autoloaded (module autovivified from #{File.join(base_path, path_suffix)})") autoloaded_constants << qualified_name unless autoload_once_paths.include?(base_path) autoloaded_constants.uniq! mod @@ -558,7 +525,6 @@ module ActiveSupport #:nodoc: require_or_load(expanded, qualified_name) if from_mod.const_defined?(const_name, false) - log("constant #{qualified_name} autoloaded from #{expanded}.rb") return from_mod.const_get(const_name) else raise LoadError, "Unable to autoload constant #{qualified_name}, expected #{file_path} to define it" @@ -611,66 +577,21 @@ module ActiveSupport #:nodoc: # as the environment will be in an inconsistent state, e.g. other constants # may have already been unloaded and not accessible. def remove_unloadable_constants! - log("removing unloadable constants") autoloaded_constants.each { |const| remove_constant const } autoloaded_constants.clear - Reference.clear! explicitly_unloadable_constants.each { |const| remove_constant const } end - class ClassCache - def initialize - @store = Concurrent::Map.new - end - - def empty? - @store.empty? - end - - def key?(key) - @store.key?(key) - end - - def get(key) - key = key.name if key.respond_to?(:name) - @store[key] ||= Inflector.constantize(key) - end - alias :[] :get - - def safe_get(key) - key = key.name if key.respond_to?(:name) - @store[key] ||= Inflector.safe_constantize(key) - end - - def store(klass) - return self unless klass.respond_to?(:name) - raise(ArgumentError, "anonymous classes cannot be cached") if klass.name.empty? - @store[klass.name] = klass - self - end - - def clear! - @store.clear - end - end - - Reference = ClassCache.new - - # Store a reference to a class +klass+. - def reference(klass) - Reference.store klass - end - # Get the reference for class named +name+. # Raises an exception if referenced class does not exist. def constantize(name) - Reference.get(name) + Inflector.constantize(name) end # Get the reference for class named +name+ if one exists. # Otherwise returns +nil+. def safe_constantize(name) - Reference.safe_get(name) + Inflector.safe_constantize(name) end # Determine if the given constant has been automatically loaded. @@ -802,10 +723,6 @@ module ActiveSupport #:nodoc: end end - def log(message) - logger.debug("autoloading: #{message}") if logger && verbose - end - private def uninitialized_constant(qualified_name, const_name, receiver:) NameError.new("uninitialized constant #{qualified_name}", const_name, receiver: receiver) diff --git a/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb index 2155fba0a9..f7aec0ffb3 100644 --- a/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb +++ b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb @@ -2,6 +2,7 @@ require "set" require "active_support/core_ext/string/inflections" +require "zeitwerk" module ActiveSupport module Dependencies diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb index 499698c1a3..a1cba3ce45 100644 --- a/activesupport/test/callbacks_test.rb +++ b/activesupport/test/callbacks_test.rb @@ -1129,15 +1129,6 @@ module CallbacksTest } end - def test_skip_lambda # raises error - calls = [] - callback = ->(o) { calls << o } - klass = build_class(callback) - assert_raises(ArgumentError) { klass.skip callback } - klass.new.run - assert_equal 10, calls.length - end - def test_skip_symbol # removes all calls = [] klass = build_class(:bar) diff --git a/activesupport/test/class_cache_test.rb b/activesupport/test/class_cache_test.rb deleted file mode 100644 index 4c517d00b1..0000000000 --- a/activesupport/test/class_cache_test.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -require_relative "abstract_unit" -require "active_support/dependencies" - -module ActiveSupport - module Dependencies - class ClassCacheTest < ActiveSupport::TestCase - def setup - @cache = ClassCache.new - end - - def test_empty? - assert_empty @cache - @cache.store(ClassCacheTest) - assert_not_empty @cache - end - - def test_clear! - assert_empty @cache - @cache.store(ClassCacheTest) - assert_not_empty @cache - @cache.clear! - assert_empty @cache - end - - def test_set_key - @cache.store(ClassCacheTest) - assert @cache.key?(ClassCacheTest.name) - end - - def test_get_with_class - @cache.store(ClassCacheTest) - assert_equal ClassCacheTest, @cache.get(ClassCacheTest) - end - - def test_get_with_name - @cache.store(ClassCacheTest) - assert_equal ClassCacheTest, @cache.get(ClassCacheTest.name) - end - - def test_get_constantizes - assert_empty @cache - assert_equal ClassCacheTest, @cache.get(ClassCacheTest.name) - end - - def test_get_constantizes_fails_on_invalid_names - assert_empty @cache - assert_raise NameError do - @cache.get("OmgTotallyInvalidConstantName") - end - end - - def test_get_alias - assert_empty @cache - assert_equal @cache[ClassCacheTest.name], @cache.get(ClassCacheTest.name) - end - - def test_safe_get_constantizes - assert_empty @cache - assert_equal ClassCacheTest, @cache.safe_get(ClassCacheTest.name) - end - - def test_safe_get_constantizes_doesnt_fail_on_invalid_names - assert_empty @cache - assert_nil @cache.safe_get("OmgTotallyInvalidConstantName") - end - - def test_new_rejects_strings - @cache.store ClassCacheTest.name - assert_not @cache.key?(ClassCacheTest.name) - end - - def test_store_returns_self - x = @cache.store ClassCacheTest - assert_equal @cache, x - end - end - end -end diff --git a/activesupport/test/current_attributes_test.rb b/activesupport/test/current_attributes_test.rb index 024ccec499..802fbedba9 100644 --- a/activesupport/test/current_attributes_test.rb +++ b/activesupport/test/current_attributes_test.rb @@ -36,6 +36,12 @@ class CurrentAttributesTest < ActiveSupport::TestCase self.account = account end + def get_world_and_account(hash) + hash[:world] = world + hash[:account] = account + hash + end + def respond_to_test; end def request @@ -138,6 +144,11 @@ class CurrentAttributesTest < ActiveSupport::TestCase assert_equal "world/1", Current.world assert_equal "account/1", Current.account + + hash = {} + assert_same hash, Current.get_world_and_account(hash) + assert_equal "world/1", hash[:world] + assert_equal "account/1", hash[:account] end setup { @testing_teardown = false } diff --git a/activesupport/test/dependencies/raises_exception_without_blame_file.rb b/activesupport/test/dependencies/raises_exception_without_blame_file.rb deleted file mode 100644 index 3a6533cd3a..0000000000 --- a/activesupport/test/dependencies/raises_exception_without_blame_file.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -exception = Exception.new("I am not blamable!") -class << exception - undef_method(:blame_file!) -end -raise exception diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index 6801d951e8..25b7d0d9c4 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -104,13 +104,6 @@ class DependenciesTest < ActiveSupport::TestCase end end - def test_dependency_which_raises_doesnt_blindly_call_blame_file! - with_loading do - filename = "dependencies/raises_exception_without_blame_file" - assert_raises(Exception) { require_dependency filename } - end - end - def test_warnings_should_be_enabled_on_first_load with_loading "dependencies" do old_warnings, ActiveSupport::Dependencies.warnings_on_first_load = ActiveSupport::Dependencies.warnings_on_first_load, true @@ -708,21 +701,6 @@ class DependenciesTest < ActiveSupport::TestCase remove_constants(:ServiceOne) end - def test_references_should_work - with_loading "dependencies" do - c = ActiveSupport::Dependencies.reference("ServiceOne") - service_one_first = ServiceOne - assert_equal service_one_first, c.get("ServiceOne") - ActiveSupport::Dependencies.clear - assert_not defined?(ServiceOne) - service_one_second = ServiceOne - assert_not_equal service_one_first, c.get("ServiceOne") - assert_equal service_one_second, c.get("ServiceOne") - end - ensure - remove_constants(:ServiceOne) - end - def test_constantize_shortcut_for_cached_constant_lookups with_loading "dependencies" do assert_equal ServiceOne, ActiveSupport::Dependencies.constantize("ServiceOne") @@ -1169,52 +1147,3 @@ class DependenciesTest < ActiveSupport::TestCase ActiveSupport::Dependencies.hook! end end - -class DependenciesLogging < ActiveSupport::TestCase - MESSAGE = "message" - - def with_settings(logger, verbose) - original_logger = ActiveSupport::Dependencies.logger - original_verbose = ActiveSupport::Dependencies.verbose - - ActiveSupport::Dependencies.logger = logger - ActiveSupport::Dependencies.verbose = verbose - - yield - ensure - ActiveSupport::Dependencies.logger = original_logger - ActiveSupport::Dependencies.verbose = original_verbose - end - - def fake_logger - Class.new do - def self.debug(message) - message - end - end - end - - test "does not log if the logger is nil and verbose is false" do - with_settings(nil, false) do - assert_nil ActiveSupport::Dependencies.log(MESSAGE) - end - end - - test "does not log if the logger is nil and verbose is true" do - with_settings(nil, true) do - assert_nil ActiveSupport::Dependencies.log(MESSAGE) - end - end - - test "does not log if the logger is set and verbose is false" do - with_settings(fake_logger, false) do - assert_nil ActiveSupport::Dependencies.log(MESSAGE) - end - end - - test "logs if the logger is set and verbose is true" do - with_settings(fake_logger, true) do - assert_equal "autoloading: #{MESSAGE}", ActiveSupport::Dependencies.log(MESSAGE) - end - end -end diff --git a/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb index ac2923a313..e6342e8382 100644 --- a/activesupport/test/test_case_test.rb +++ b/activesupport/test/test_case_test.rb @@ -325,9 +325,9 @@ class SetupAndTeardownTest < ActiveSupport::TestCase teardown :foo, :sentinel def test_inherited_setup_callbacks - assert_equal [:reset_callback_record, :foo], self.class._setup_callbacks.map(&:raw_filter) + assert_equal [:reset_callback_record, :foo], self.class._setup_callbacks.map(&:filter) assert_equal [:foo], @called_back - assert_equal [:foo, :sentinel], self.class._teardown_callbacks.map(&:raw_filter) + assert_equal [:foo, :sentinel], self.class._teardown_callbacks.map(&:filter) end def setup @@ -355,9 +355,9 @@ class SubclassSetupAndTeardownTest < SetupAndTeardownTest teardown :bar def test_inherited_setup_callbacks - assert_equal [:reset_callback_record, :foo, :bar], self.class._setup_callbacks.map(&:raw_filter) + assert_equal [:reset_callback_record, :foo, :bar], self.class._setup_callbacks.map(&:filter) assert_equal [:foo, :bar], @called_back - assert_equal [:foo, :sentinel, :bar], self.class._teardown_callbacks.map(&:raw_filter) + assert_equal [:foo, :sentinel, :bar], self.class._teardown_callbacks.map(&:filter) end private diff --git a/guides/source/action_view_helpers.md b/guides/source/action_view_helpers.md index 304ca6d02b..332b36ebcc 100644 --- a/guides/source/action_view_helpers.md +++ b/guides/source/action_view_helpers.md @@ -347,8 +347,8 @@ number_to_human(1234567) # => 1.23 Million Formats the bytes in size into a more understandable representation; useful for reporting file sizes to users. ```ruby -number_to_human_size(1234) # => 1.2 KB -number_to_human_size(1234567) # => 1.2 MB +number_to_human_size(1234) # => 1.21 KB +number_to_human_size(1234567) # => 1.18 MB ``` #### number_to_percentage diff --git a/guides/source/active_record_multiple_databases.md b/guides/source/active_record_multiple_databases.md index e593ff2354..ce39250be0 100644 --- a/guides/source/active_record_multiple_databases.md +++ b/guides/source/active_record_multiple_databases.md @@ -50,7 +50,7 @@ The `database.yml` looks like this: production: database: my_primary_database user: root - adapter: mysql + adapter: mysql2 ``` Let's add a replica for the first configuration, and a second database called animals and a @@ -68,21 +68,21 @@ production: primary: database: my_primary_database user: root - adapter: mysql + adapter: mysql2 primary_replica: database: my_primary_database user: root_readonly - adapter: mysql + adapter: mysql2 replica: true animals: database: my_animals_database user: animals_root - adapter: mysql + adapter: mysql2 migrations_paths: db/animals_migrate animals_replica: database: my_animals_database user: animals_readonly - adapter: mysql + adapter: mysql2 replica: true ``` @@ -338,17 +338,17 @@ Shards are declared in the three-tier config like this: production: primary: database: my_primary_database - adapter: mysql + adapter: mysql2 primary_replica: database: my_primary_database - adapter: mysql + adapter: mysql2 replica: true primary_shard_one: database: my_primary_shard_one - adapter: mysql + adapter: mysql2 primary_shard_one_replica: database: my_primary_shard_one - adapter: mysql + adapter: mysql2 replica: true ``` diff --git a/guides/source/autoloading_and_reloading_constants.md b/guides/source/autoloading_and_reloading_constants.md index 8f27ead76f..04e5829749 100644 --- a/guides/source/autoloading_and_reloading_constants.md +++ b/guides/source/autoloading_and_reloading_constants.md @@ -89,7 +89,7 @@ INFO. Autoload paths are called _root directories_ in Zeitwerk documentation, bu Within an autoload path, file names must match the constants they define as documented [here](https://github.com/fxn/zeitwerk#file-structure). -By default, the autoload paths of an application consist of all the subdirectories of `app` that exist when the application boots ---except for `assets`, `javascript`, `views`,--- plus the autoload paths of engines it might depend on. +By default, the autoload paths of an application consist of all the subdirectories of `app` that exist when the application boots ---except for `assets`, `javascript`, and `views`--- plus the autoload paths of engines it might depend on. For example, if `UsersHelper` is implemented in `app/helpers/users_helper.rb`, the module is autoloadable, you do not need (and should not write) a `require` call for it: @@ -98,17 +98,17 @@ $ bin/rails runner 'p UsersHelper' UsersHelper ``` -Autoload paths automatically pick any custom directories under `app`. For example, if your application has `app/presenters`, or `app/services`, etc., they are added to autoload paths. +Autoload paths automatically pick up any custom directories under `app`. For example, if your application has `app/presenters`, or `app/services`, etc., they are added to autoload paths. The array of autoload paths can be extended by mutating `config.autoload_paths`, in `config/application.rb`, but nowadays this is discouraged. -WARNING. Please, do not mutate `ActiveSupport::Dependencies.autoload_paths`, the public interface to change autoload paths is `config.autoload_paths`. +WARNING. Please do not mutate `ActiveSupport::Dependencies.autoload_paths`; the public interface to change autoload paths is `config.autoload_paths`. $LOAD_PATH ---------- -Autoload paths are added to `$LOAD_PATH` by default. However, Zeitwerk uses absolute file names internally, and your application should not issue `require` calls for autoloadable files, so those directories are actually not needed there. You can opt-out with this flag: +Autoload paths are added to `$LOAD_PATH` by default. However, Zeitwerk uses absolute file names internally, and your application should not issue `require` calls for autoloadable files, so those directories are actually not needed there. You can opt out with this flag: ```ruby config.add_autoload_paths_to_load_path = false @@ -122,13 +122,13 @@ Reloading Rails automatically reloads classes and modules if application files change. -More precisely, if the web server is running and application files have been modified, Rails unloads all autoloaded constants just before the next request is processed. That way, application classes or modules used during that request are going to be autoloaded, thus picking up their current implementation in the file system. +More precisely, if the web server is running and application files have been modified, Rails unloads all autoloaded constants just before the next request is processed. That way, application classes or modules used during that request will be autoloaded again, thus picking up their current implementation in the file system. Reloading can be enabled or disabled. The setting that controls this behavior is `config.cache_classes`, which is false by default in `development` mode (reloading enabled), and true by default in `production` mode (reloading disabled). -Rails detects files have changed using an evented file monitor (default), or walking the autoload paths, depending on `config.file_watcher`. +Rails uses an evented file monitor to detect files changes by default. It can be configured instead to detect file changes by walking the autoload paths. This is controlled by the `config.file_watcher` setting. -In a Rails console there is no file watcher active regardless of the value of `config.cache_classes`. This is so because, normally, it would be confusing to have code reloaded in the middle of a console session, the same way you generally want an individual request to be served by a consistent, non-changing set of application classes and modules. +In a Rails console there is no file watcher active regardless of the value of `config.cache_classes`. This is because, normally, it would be confusing to have code reloaded in the middle of a console session. Similar to an individual request, you generally want a console session to be served by a consistent, non-changing set of application classes and modules. However, you can force a reload in the console by executing `reload!`: @@ -142,7 +142,7 @@ irb(main):003:0> User.object_id => 70136284426020 ``` -as you can see, the class object stored in the `User` constant is different after reloading. +As you can see, the class object stored in the `User` constant is different after reloading. ### Reloading and Stale Objects @@ -167,7 +167,7 @@ There are several ways to do this safely. For instance, the application could de Let's see other situations that involve stale class or module objects. -Check this Rails console session: +Check out this Rails console session: ```irb irb> joe = User.new @@ -177,7 +177,7 @@ irb> joe.class == alice.class => false ``` -`joe` is an instance of the original `User` class. When there is a reload, the `User` constant evaluates to a different, reloaded class. `alice` is an instance of the current one, but `joe` is not, his class is stale. You may define `joe` again, start an IRB subsession, or just launch a new console instead of calling `reload!`. +`joe` is an instance of the original `User` class. When there is a reload, the `User` constant then evaluates to a different, reloaded class. `alice` is an instance of the newly loaded `User`, but `joe` is not — his class is stale. You may define `joe` again, start an IRB subsession, or just launch a new console instead of calling `reload!`. Another situation in which you may find this gotcha is subclassing reloadable classes in a place that is not reloaded: @@ -205,7 +205,7 @@ That block runs when the application boots, and every time code is reloaded. NOTE: For historical reasons, this callback may run twice. The code it executes must be idempotent. -However, if you do not need to reload the class, it is easier to define it in a directory which does not belong to the autoload paths. For instance, `lib` is an idiomatic choice, it does not belong to the autoload paths by default but it belongs to `$LOAD_PATH`. Then, in the place the class is needed at boot time, just perform a regular `require` to load it. +However, if you do not need to reload the class, it is easier to define it in a directory which does not belong to the autoload paths. For instance, `lib` is an idiomatic choice. It does not belong to the autoload paths by default, but it does belong to `$LOAD_PATH`. Then, in the place the class is needed at boot time, just perform a regular `require` to load it. For example, there is no point in defining reloadable Rack middleware, because changes would not be reflected in the instance stored in the middleware stack anyway. If `lib/my_app/middleware/foo.rb` defines a middleware class, then in `config/application.rb` you write: @@ -225,19 +225,19 @@ In production-like environments it is generally better to load all the applicati Eager loading is controlled by the flag `config.eager_load`, which is enabled by default in `production` mode. -The order in which files are eager loaded is undefined. +The order in which files are eager-loaded is undefined. -if the `Zeitwerk` constant is defined, Rails invokes `Zeitwerk::Loader.eager_load_all` regardless of the application autoloading mode. That ensures dependencies managed by Zeitwerk are eager loaded. +If the `Zeitwerk` constant is defined, Rails invokes `Zeitwerk::Loader.eager_load_all` regardless of the application autoloading mode. That ensures dependencies managed by Zeitwerk are eager-loaded. Single Table Inheritance ------------------------ -Single Table Inheritance is a feature that doesn't play well with lazy loading. Reason is, its API generally needs to be able to enumerate the STI hierarchy to work correctly, whereas lazy loading defers loading classes until they are referenced. You can't enumerate what you haven't referenced yet. +Single Table Inheritance is a feature that doesn't play well with lazy loading. The reason is: its API generally needs to be able to enumerate the STI hierarchy to work correctly, whereas lazy loading defers loading classes until they are referenced. You can't enumerate what you haven't referenced yet. In a sense, applications need to eager load STI hierarchies regardless of the loading mode. -Of course, if the application eager loads on boot, that is already accomplished. When it does not, it is in practice enough to instantiate the existing types in the database, which in development or test modes is usually fine. One way to do that is to throw this module into the `lib` directory: +Of course, if the application eager loads on boot, that is already accomplished. When it does not, it is in practice enough to instantiate the existing types in the database, which in development or test modes is usually fine. One way to do that is to include an STI preloading module in your `lib` directory: ```ruby module StiPreload @@ -306,7 +306,7 @@ end Customizing Inflections ----------------------- -By default, Rails uses `String#camelize` to know which constant should a given file or directory name define. For example, `posts_controller.rb` should define `PostsController` because that is what `"posts_controller".camelize` returns. +By default, Rails uses `String#camelize` to know which constant a given file or directory name should define. For example, `posts_controller.rb` should define `PostsController` because that is what `"posts_controller".camelize` returns. It could be the case that some particular file or directory name does not get inflected as you want. For instance, `html_parser.rb` is expected to define `HtmlParser` by default. What if you prefer the class to be `HTMLParser`? There are a few ways to customize this. @@ -344,22 +344,22 @@ Rails.autoloaders.each do |autoloader| end ``` -There is no global configuration that can affect said instances, they are deterministic. +There is no global configuration that can affect said instances; they are deterministic. -You can even define a custom inflector for full flexibility. Please, check the [Zeitwerk documentation](https://github.com/fxn/zeitwerk#custom-inflector) for further details. +You can even define a custom inflector for full flexibility. Please check the [Zeitwerk documentation](https://github.com/fxn/zeitwerk#custom-inflector) for further details. Troubleshooting --------------- The best way to follow what the loaders are doing is to inspect their activity. -The easiest way to do that is to throw +The easiest way to do that is to include ```ruby Rails.autoloaders.log! ``` -to `config/application.rb` after loading the framework defaults. That will print traces to standard output. +in `config/application.rb` after loading the framework defaults. That will print traces to standard output. If you prefer logging to a file, configure this instead: @@ -367,7 +367,7 @@ If you prefer logging to a file, configure this instead: Rails.autoloaders.logger = Logger.new("#{Rails.root}/log/autoloading.log") ``` -The Rails logger is still not ready in `config/application.rb`, but it is in initializers: +The Rails logger is not yet available when `config/application.rb` executes. If you prefer to use the Rails logger, configure this setting in an initializer instead: ```ruby # config/initializers/log_autoloaders.rb @@ -406,7 +406,7 @@ class Admin::UsersController < ApplicationController end ``` -was not recommended because the resolution of constants inside their body was brittle. You'd better write them in this style: +was not recommended because the resolution of constants inside their body was brittle. It was better to write them in this style: ```ruby module Admin @@ -415,11 +415,11 @@ module Admin end ``` -In `zeitwerk` mode that does not matter anymore, you can pick either style. +In `zeitwerk` mode that does not matter anymore. You can pick either style. -The resolution of a constant could depend on load order, the definition of a class or module object could depend on load order, there was edge cases with singleton classes, oftentimes you had to use `require_dependency` as a workaround, .... The guide for `classic` mode documents [these issues](autoloading_and_reloading_constants_classic_mode.html#common-gotchas). +In `classic` mode, the resolution of a constant could depend on load order, the definition of a class or module object could depend on load order, there were edge cases with singleton classes, oftentimes you had to use `require_dependency` as a workaround, .... The list goes on. The guide for `classic` mode documents [these issues](autoloading_and_reloading_constants_classic_mode.html#common-gotchas). -All these problems are solved in `zeitwerk` mode, it just works as expected, and `require_dependency` should not be used anymore, it is no longer needed. +All these problems are solved in `zeitwerk` mode. It just works as expected. `require_dependency` should not be used anymore, because it is no longer needed. ### Less File Lookups @@ -437,26 +437,4 @@ While in common names these operations match, if acronyms or custom inflection r ### More Differences -There are some other subtle differences, please check [this section of _Upgrading Ruby on Rails_](upgrading_ruby_on_rails.html#autoloading) guide for details. - -Classic Mode is Deprecated --------------------------- - -By now, it is still possible to use `classic` mode. However, `classic` is deprecated and will be eventually removed. - -New applications should use `zeitwerk` mode (which is the default), and applications being upgrade are strongly encouraged to migrate to `zeitwerk` mode. Please check the [_Upgrading Ruby on Rails_](upgrading_ruby_on_rails.html#autoloading) guide for details. - -Opting Out ----------- - -Applications can load Rails 6 defaults and still use the classic autoloader this way: - -```ruby -# config/application.rb -config.load_defaults 6.0 -config.autoloader = :classic -``` - -That may be handy if upgrading to Rails 6 in different phases, but classic mode is discouraged for new applications. - -`zeitwerk` mode is not available in versions of Rails previous to 6.0. +There are some other subtle differences. Please check the [autoloading section](upgrading_ruby_on_rails.html#autoloading) of the _Upgrading Ruby on Rails_] guide for details. diff --git a/guides/source/configuring.md b/guides/source/configuring.md index ed369fb16e..b58cef2866 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -159,13 +159,6 @@ numbers. It also filters out sensitive values of database columns when call `#in * `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` 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 - config.autoloader = :classic - ``` - ### Configuring Assets * `config.assets.enabled` a flag that controls whether the asset @@ -1044,6 +1037,10 @@ text/javascript image/svg+xml application/postscript application/x-shockwave-fla The default is `:rails_storage_redirect`. +### Configuring Action Text + +* `config.action_text.attachment_tag_name` accepts a string for the HTML tag used to wrap attachments. Defaults to `"action-text-attachment"`. + ### Results of `config.load_defaults` `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. diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index 197e1c2b2b..5cb8de8fa5 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -1430,7 +1430,7 @@ Associations](association_basics.html) guide. ### Adding a Route for Comments -As with the `welcome` controller, we will need to add a route so that Rails +As with the `articles` controller, we will need to add a route so that Rails knows where we would like to navigate to see `comments`. Open up the `config/routes.rb` file again, and edit it as follows: diff --git a/guides/source/kindle/rails_guides.opf.erb b/guides/source/kindle/rails_guides.opf.erb index 83d00cfc2e..06bfac8fc8 100644 --- a/guides/source/kindle/rails_guides.opf.erb +++ b/guides/source/kindle/rails_guides.opf.erb @@ -32,7 +32,7 @@ - + diff --git a/guides/source/security.md b/guides/source/security.md index 15f536d584..4421599000 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -64,7 +64,7 @@ Hence, the cookie serves as temporary authentication for the web application. An * Instead of stealing a cookie unknown to the attacker, they fix a user's session identifier (in the cookie) known to them. Read more about this so-called session fixation later. -The main objective of most attackers is to make money. The underground prices for stolen bank login accounts range from 0.5%-10% of account balance, $0.5-$30 for credit card numbers ($20-$60 with full details), $0.1-$1.5 for identities (Name, SSN, and DOB), $20-$50 for retailer accounts, and $6-$10 for cloud service provider accounts, according to the [Symantec Internet Security Threat Report (2017)](https://www.symantec.com/content/dam/symantec/docs/reports/istr-22-2017-en.pdf). +The main objective of most attackers is to make money. The underground prices for stolen bank login accounts range from 0.5%-10% of account balance, $0.5-$30 for credit card numbers ($20-$60 with full details), $0.1-$1.5 for identities (Name, SSN, and DOB), $20-$50 for retailer accounts, and $6-$10 for cloud service provider accounts, according to the [Symantec Internet Security Threat Report (2017)](https://docs.broadcom.com/docs/istr-22-2017-en). ### Session Storage @@ -657,7 +657,7 @@ SELECT * FROM projects WHERE (name = '') UNION The result won't be a list of projects (because there is no project with an empty name), but a list of user names and their password. So hopefully you encrypted the passwords in the database! The only problem for the attacker is, that the number of columns has to be the same in both queries. That's why the second query includes a list of ones (1), which will be always the value 1, in order to match the number of columns in the first query. -Also, the second query renames some columns with the AS statement so that the web application displays the values from the user table. Be sure to update your Rails [to at least 2.1.1](http://www.rorsecurity.info/2008/09/08/sql-injection-issue-in-limit-and-offset-parameter/). +Also, the second query renames some columns with the AS statement so that the web application displays the values from the user table. Be sure to update your Rails [to at least 2.1.1](https://rorsecurity.info/journal/2008/09/08/sql-injection-issue-in-limit-and-offset-parameter.html). #### Countermeasures @@ -669,7 +669,7 @@ Instead of passing a string to the conditions option, you can pass an array to s Model.where("login = ? AND password = ?", entered_user_name, entered_password).first ``` -As you can see, the first part of the array is an SQL fragment with question marks. The sanitized versions of the variables in the second part of the array replace the question marks. Or you can pass a hash for the same result: +As you can see, the first part of the array is a SQL fragment with question marks. The sanitized versions of the variables in the second part of the array replace the question marks. Or you can pass a hash for the same result: ```ruby Model.where(login: entered_user_name, password: entered_password).first @@ -689,7 +689,7 @@ The most common entry points are message posts, user comments, and guest books, XSS attacks work like this: An attacker injects some code, the web application saves it and displays it on a page, later presented to a victim. Most XSS examples simply display an alert box, but it is more powerful than that. XSS can steal the cookie, hijack the session, redirect the victim to a fake website, display advertisements for the benefit of the attacker, change elements on the web site to get confidential information or install malicious software through security holes in the web browser. -During the second half of 2007, there were 88 vulnerabilities reported in Mozilla browsers, 22 in Safari, 18 in IE, and 12 in Opera. The [Symantec Global Internet Security threat report](http://eval.symantec.com/mktginfo/enterprise/white_papers/b-whitepaper_internet_security_threat_report_xiii_04-2008.en-us.pdf) also documented 239 browser plug-in vulnerabilities in the last six months of 2007. [Mpack](http://pandalabs.pandasecurity.com/mpack-uncovered/) is a very active and up-to-date attack framework which exploits these vulnerabilities. For criminal hackers, it is very attractive to exploit an SQL-Injection vulnerability in a web application framework and insert malicious code in every textual table column. In April 2008 more than 510,000 sites were hacked like this, among them the British government, United Nations, and many more high profile targets. +During the second half of 2007, there were 88 vulnerabilities reported in Mozilla browsers, 22 in Safari, 18 in IE, and 12 in Opera. The Symantec Global Internet Security threat report also documented 239 browser plug-in vulnerabilities in the last six months of 2007. [Mpack](https://www.pandasecurity.com/en/mediacenter/malware/mpack-uncovered/) is a very active and up-to-date attack framework which exploits these vulnerabilities. For criminal hackers, it is very attractive to exploit a SQL-Injection vulnerability in a web application framework and insert malicious code in every textual table column. In April 2008 more than 510,000 sites were hacked like this, among them the British government, United Nations, and many more high profile targets. #### HTML/JavaScript Injection @@ -728,7 +728,7 @@ The log files on www.attacker.com will read like this: GET http://www.attacker.com/_app_session=836c1c25278e5b321d6bea4f19cb57e2 ``` -You can mitigate these attacks (in the obvious way) by adding the **httpOnly** flag to cookies, so that `document.cookie` may not be read by JavaScript. HTTP only cookies can be used from IE v6.SP1, Firefox v2.0.0.5, Opera 9.5, Safari 4, and Chrome 1.0.154 onwards. But other, older browsers (such as WebTV and IE 5.5 on Mac) can actually cause the page to fail to load. Be warned that cookies [will still be visible using Ajax](https://www.owasp.org/index.php/HTTPOnly#Browsers_Supporting_HttpOnly), though. +You can mitigate these attacks (in the obvious way) by adding the **httpOnly** flag to cookies, so that `document.cookie` may not be read by JavaScript. HTTP only cookies can be used from IE v6.SP1, Firefox v2.0.0.5, Opera 9.5, Safari 4, and Chrome 1.0.154 onwards. But other, older browsers (such as WebTV and IE 5.5 on Mac) can actually cause the page to fail to load. Be warned that cookies [will still be visible using Ajax](https://owasp.org/www-community/HttpOnly#browsers-supporting-httponly), though. ##### Defacement @@ -787,7 +787,7 @@ This example pops up a message box. It will be recognized by the above `sanitize _In order to understand today's attacks on web applications, it's best to take a look at some real-world attack vectors._ -The following is an excerpt from the [Js.Yamanner@m](http://www.symantec.com/security_response/writeup.jsp?docid=2006-061211-4111-99&tabid=1) Yahoo! Mail [worm](http://groovin.net/stuff/yammer.txt). It appeared on June 11, 2006 and was the first webmail interface worm: +The following is an excerpt from the [Js.Yamanner@m Yahoo! Mail worm](https://community.broadcom.com/symantecenterprise/communities/community-home/librarydocuments/viewdocument?DocumentKey=12d8d106-1137-4d7c-8bb4-3ea1faec83fa). It appeared on June 11, 2006 and was the first webmail interface worm: ```html test`, which makes the text italic. However, up to the current version 3.0.4, it is still vulnerable to XSS. Get the [all-new version 4](http://www.redcloth.org) that removed serious bugs. However, even that version has [some security bugs](http://www.rorsecurity.info/journal/2008/10/13/new-redcloth-security.html), so the countermeasures still apply. Here is an example for version 3.0.4: +For example, RedCloth translates `_test_` to `test`, which makes the text italic. However, up to the current version 3.0.4, it is still vulnerable to XSS. Get the [all-new version 4](http://www.redcloth.org) that removed serious bugs. However, even that version has [some security bugs](https://rorsecurity.info/journal/2008/10/13/new-redcloth-security.html), so the countermeasures still apply. Here is an example for version 3.0.4: ```ruby RedCloth.new('').to_html @@ -1027,7 +1027,7 @@ Here is a list of common headers: * **X-Frame-Options:** _`SAMEORIGIN` in Rails by default_ - allow framing on same domain. Set it to 'DENY' to deny framing at all or remove this header completely if you want to allow framing on all websites. * **X-XSS-Protection:** _`1; mode=block` in Rails by default_ - use XSS Auditor and block page if XSS attack is detected. Set it to '0;' if you want to switch XSS Auditor off(useful if response contents scripts from request parameters) * **X-Content-Type-Options:** _`nosniff` in Rails by default_ - stops the browser from guessing the MIME type of a file. -* **X-Content-Security-Policy:** [A powerful mechanism for controlling which sites certain content types can be loaded from](http://w3c.github.io/webappsec/specs/content-security-policy/csp-specification.dev.html) +* **X-Content-Security-Policy:** [A powerful mechanism for controlling which sites certain content types can be loaded from](https://w3c.github.io/webappsec-csp/) * **Access-Control-Allow-Origin:** Used to control which sites are allowed to bypass same origin policies and send cross-origin requests. * **Strict-Transport-Security:** [Used to control if the browser is allowed to only access a site over a secure connection](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) @@ -1192,4 +1192,4 @@ The security landscape shifts and it is important to keep up to date, because mi * Subscribe to the Rails security [mailing list](https://groups.google.com/forum/#!forum/rubyonrails-security). * [Brakeman - Rails Security Scanner](https://brakemanscanner.org/) - To perform static security analysis for Rails applications. * [Mozilla's Web Security Guidelines](https://infosec.mozilla.org/guidelines/web_security.html) - Recommendations on topics covering Content Security Policy, HTTP headers, Cookies, TLS configuration, etc. -* A [good security blog](https://www.owasp.org) including the [Cross-Site scripting Cheat Sheet](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.md). +* A [good security blog](https://owasp.org/) including the [Cross-Site scripting Cheat Sheet](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.md). diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 43fedc50ee..b87df6aa02 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -2,6 +2,10 @@ *Josh Abernathy*, *Debbie Milburn* +* Add app concern and test keepfiles to generated engine plugins. + + *Gannon McGibbon* + * Stop generating a license for in-app plugins. *Gannon McGibbon* diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 453a284715..3f8f58c41e 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -23,7 +23,7 @@ module Rails :require_master_key, :credentials, :disable_sandbox, :add_autoload_paths_to_load_path, :rake_eager_load - attr_reader :encoding, :api_only, :loaded_config_version, :autoloader + attr_reader :encoding, :api_only, :loaded_config_version def initialize(*) super @@ -70,7 +70,6 @@ module Rails @credentials = ActiveSupport::OrderedOptions.new @credentials.content_path = default_credentials_content_path @credentials.key_path = default_credentials_key_path - @autoloader = :classic @disable_sandbox = false @add_autoload_paths_to_load_path = true @permissions_policy = nil @@ -129,8 +128,6 @@ module Rails when "6.0" load_defaults "5.2" - self.autoloader = :zeitwerk if RUBY_ENGINE == "ruby" - if respond_to?(:action_view) action_view.default_enforce_utf8 = false end @@ -156,8 +153,6 @@ module Rails when "6.1" load_defaults "6.0" - self.autoloader = :zeitwerk if %w[ruby truffleruby].include?(RUBY_ENGINE) - if respond_to?(:active_record) active_record.has_many_inversing = true active_record.legacy_connection_handling = false @@ -366,18 +361,6 @@ module Rails end end - def autoloader=(autoloader) - case autoloader - when :classic - @autoloader = autoloader - when :zeitwerk - require "zeitwerk" - @autoloader = autoloader - else - raise ArgumentError, "config.autoloader may be :classic or :zeitwerk, got #{autoloader.inspect} instead" - end - end - def default_log_file path = paths["log"].first unless File.exist? File.dirname path diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index e4efebb615..900eb8c6e0 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -2,6 +2,7 @@ require "active_support/core_ext/string/inflections" require "active_support/core_ext/array/conversions" +require "zeitwerk" module Rails class Application @@ -39,14 +40,10 @@ module Rails example = autoloaded.first example_klass = example.constantize.class - if config.autoloader == :zeitwerk - ActiveSupport::DescendantsTracker.clear - ActiveSupport::Dependencies.clear + ActiveSupport::DescendantsTracker.clear + ActiveSupport::Dependencies.clear - unload_message = "#{these} autoloaded #{constants} #{have} been unloaded." - else - unload_message = "`config.autoloader` is set to `#{config.autoloader}`. #{these} autoloaded #{constants} would have been unloaded if `config.autoloader` had been set to `:zeitwerk`." - end + unload_message = "#{these} autoloaded #{constants} #{have} been unloaded." ActiveSupport::Deprecation.warn(<<~WARNING) Initialization autoloaded the #{constants} #{enum}. @@ -76,10 +73,8 @@ module Rails end initializer :let_zeitwerk_take_over do - if config.autoloader == :zeitwerk - require "active_support/dependencies/zeitwerk_integration" - ActiveSupport::Dependencies::ZeitwerkIntegration.take_over(enable_reloading: !config.cache_classes) - end + require "active_support/dependencies/zeitwerk_integration" + ActiveSupport::Dependencies::ZeitwerkIntegration.take_over(enable_reloading: !config.cache_classes) end # Setup default session store if not already set in config/application.rb @@ -113,10 +108,7 @@ module Rails initializer :eager_load! do if config.eager_load ActiveSupport.run_load_hooks(:before_eager_load, self) - # Checks defined?(Zeitwerk) instead of zeitwerk_enabled? because we - # want to eager load any dependency managed by Zeitwerk regardless of - # the autoloading mode of the application. - Zeitwerk::Loader.eager_load_all if defined?(Zeitwerk) + Zeitwerk::Loader.eager_load_all config.eager_load_namespaces.each(&:eager_load!) end end diff --git a/railties/lib/rails/autoloaders.rb b/railties/lib/rails/autoloaders.rb index ce3ba269fe..773d2e58e0 100644 --- a/railties/lib/rails/autoloaders.rb +++ b/railties/lib/rails/autoloaders.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "active_support/dependencies/zeitwerk_integration" +require "zeitwerk" module Rails module Autoloaders # :nodoc: @@ -41,7 +42,7 @@ module Rails end def zeitwerk_enabled? - Rails.configuration.autoloader == :zeitwerk + true end end end diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 7cbe3ae99e..6d2de46a95 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -276,8 +276,9 @@ module Rails GemfileEntry.path("rails", Rails::Generators::RAILS_DEV_PATH) ] elsif options.edge? + edge_branch = Rails.gem_version.prerelease? ? "main" : [*Rails.gem_version.segments.first(2), "stable"].join("-") [ - GemfileEntry.github("rails", "rails/rails", "main") + GemfileEntry.github("rails", "rails/rails", edge_branch) ] elsif options.main? [ diff --git a/railties/lib/rails/generators/rails/controller/templates/controller.rb.tt b/railties/lib/rails/generators/rails/controller/templates/controller.rb.tt index 633e0b3177..52243f4a2f 100644 --- a/railties/lib/rails/generators/rails/controller/templates/controller.rb.tt +++ b/railties/lib/rails/generators/rails/controller/templates/controller.rb.tt @@ -1,7 +1,3 @@ -<% if namespaced? -%> -require_dependency "<%= namespaced_path %>/application_controller" - -<% end -%> <% module_namespacing do -%> class <%= class_name %>Controller < ApplicationController <% actions.each do |action| -%> diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb index b8c0e62db4..acdd5c6f28 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -26,11 +26,15 @@ module Rails empty_directory_with_keep_file "app/assets/images/#{namespaced_name}" end + empty_directory_with_keep_file "app/models/concerns" + empty_directory_with_keep_file "app/controllers/concerns" 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/models/concerns" + empty_directory_with_keep_file "app/controllers/concerns" empty_directory_with_keep_file "app/mailers" unless options[:skip_action_mailer] empty_directory_with_keep_file "app/jobs" unless options[:skip_active_job] @@ -90,12 +94,22 @@ module Rails def test template "test/test_helper.rb" template "test/%namespaced_name%_test.rb" - append_file "Rakefile", <<-EOF - -#{rakefile_test_tasks} -task default: :test + append_file "Rakefile", <<~EOF + #{rakefile_test_tasks} + task default: :test EOF + if engine? + empty_directory_with_keep_file "test/fixtures/files" + empty_directory_with_keep_file "test/controllers" + empty_directory_with_keep_file "test/mailers" + empty_directory_with_keep_file "test/models" + empty_directory_with_keep_file "test/integration" + + unless api? + empty_directory_with_keep_file "test/helpers" + end + template "test/integration/navigation_test.rb" end end diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb.tt b/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb.tt index 0936314a2c..25c8be553e 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb.tt +++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb.tt @@ -1,7 +1,3 @@ -<% if namespaced? -%> -require_dependency "<%= namespaced_path %>/application_controller" - -<% end -%> <% module_namespacing do -%> class <%= controller_class_name %>Controller < ApplicationController before_action :set_<%= singular_table_name %>, only: %i[ show update destroy ] diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt index dbc675e521..382aeecf27 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt +++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt @@ -1,7 +1,3 @@ -<% if namespaced? -%> -require_dependency "<%= namespaced_path %>/application_controller" - -<% end -%> <% module_namespacing do -%> class <%= controller_class_name %>Controller < ApplicationController before_action :set_<%= singular_table_name %>, only: %i[ show edit update destroy ] diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index f3ee033a30..9bc8eb3e6c 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -1227,7 +1227,6 @@ module ApplicationTests test "autoloaders" do app "development" - config = Rails.application.config assert Rails.autoloaders.zeitwerk_enabled? assert_instance_of Zeitwerk::Loader, Rails.autoloaders.main assert_equal "rails.main", Rails.autoloaders.main.tag @@ -1236,24 +1235,6 @@ module ApplicationTests assert_equal [Rails.autoloaders.main, Rails.autoloaders.once], Rails.autoloaders.to_a assert_equal ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector, Rails.autoloaders.main.inflector assert_equal ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector, Rails.autoloaders.once.inflector - - config.autoloader = :classic - assert_not Rails.autoloaders.zeitwerk_enabled? - assert_nil Rails.autoloaders.main - assert_nil Rails.autoloaders.once - assert_equal 0, Rails.autoloaders.count - - config.autoloader = :zeitwerk - assert Rails.autoloaders.zeitwerk_enabled? - assert_instance_of Zeitwerk::Loader, Rails.autoloaders.main - assert_equal "rails.main", Rails.autoloaders.main.tag - assert_instance_of Zeitwerk::Loader, Rails.autoloaders.once - assert_equal "rails.once", Rails.autoloaders.once.tag - assert_equal [Rails.autoloaders.main, Rails.autoloaders.once], Rails.autoloaders.to_a - assert_equal ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector, Rails.autoloaders.main.inflector - assert_equal ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector, Rails.autoloaders.once.inflector - - assert_raises(ArgumentError) { config.autoloader = :unknown } end test "config.action_view.cache_template_loading with cache_classes default" do @@ -2768,6 +2749,30 @@ module ApplicationTests assert_kind_of ActiveSupport::HashWithIndifferentAccess, ActionCable.server.config.cable end + test "action_text.config.attachment_tag_name is 'action-text-attachment' with Rails 6 defaults" do + add_to_config 'config.load_defaults "6.1"' + + app "development" + + assert_equal "action-text-attachment", ActionText::Attachment.tag_name + end + + test "action_text.config.attachment_tag_name is 'action-text-attachment' without defaults" do + remove_from_config '.*config\.load_defaults.*\n' + + app "development" + + assert_equal "action-text-attachment", ActionText::Attachment.tag_name + end + + test "action_text.config.attachment_tag_name is can be overridden" do + add_to_config "config.action_text.attachment_tag_name = 'link'" + + app "development" + + assert_equal "link", ActionText::Attachment.tag_name + end + test "ActionMailbox.logger is Rails.logger by default" do app "development" diff --git a/railties/test/application/zeitwerk_integration_test.rb b/railties/test/application/zeitwerk_integration_test.rb index 268a7467eb..95778ecfd0 100644 --- a/railties/test/application/zeitwerk_integration_test.rb +++ b/railties/test/application/zeitwerk_integration_test.rb @@ -26,7 +26,7 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase deps.singleton_class < deps::ZeitwerkIntegration::Decorations end - test "ActiveSupport::Dependencies is decorated by default" do + test "ActiveSupport::Dependencies is decorated" do boot assert decorated? @@ -36,17 +36,6 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase assert_equal [Rails.autoloaders.main, Rails.autoloaders.once], Rails.autoloaders.to_a end - test "ActiveSupport::Dependencies is not decorated in classic mode" do - add_to_config "config.autoloader = :classic" - boot - - assert_not decorated? - assert_not Rails.autoloaders.zeitwerk_enabled? - assert_nil Rails.autoloaders.main - assert_nil Rails.autoloaders.once - assert_equal 0, Rails.autoloaders.count - end - test "autoloaders inflect with Active Support" do app_file "config/initializers/inflections.rb", <<-RUBY ActiveSupport::Inflector.inflections(:en) do |inflect| @@ -295,10 +284,12 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase assert $zeitwerk_integration_test_extras end - test "autoload_paths are set as root dirs of main, and in the same order" do + test "autoload_paths not in autoload_once_paths are set as root dirs of main, and in the same order" do boot - existing_autoload_paths = deps.autoload_paths.select { |dir| File.directory?(dir) } + existing_autoload_paths = \ + deps.autoload_paths.select { |dir| File.directory?(dir) } - + deps.autoload_once_paths assert_equal existing_autoload_paths, Rails.autoloaders.main.dirs end @@ -315,7 +306,10 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase extras.each do |extra| assert_not_includes Rails.autoloaders.main.dirs, extra end - assert_equal extras, Rails.autoloaders.once.dirs + + e1_index = Rails.autoloaders.once.dirs.index(extras.first) + assert e1_index + assert_equal extras, Rails.autoloaders.once.dirs.slice(e1_index, extras.length) end test "clear reloads the main autoloader, and does not reload the once one" do @@ -340,42 +334,6 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase assert_equal %i(main_autoloader), $zeitwerk_integration_reload_test end - test "verbose = true sets the dependencies logger if present" do - boot - - logger = Logger.new(File::NULL) - ActiveSupport::Dependencies.logger = logger - ActiveSupport::Dependencies.verbose = true - - Rails.autoloaders.each do |autoloader| - assert_same logger, autoloader.logger - end - end - - test "verbose = true sets the Rails logger as fallback" do - boot - - ActiveSupport::Dependencies.verbose = true - - Rails.autoloaders.each do |autoloader| - assert_same Rails.logger, autoloader.logger - end - end - - test "verbose = false sets loggers to nil" do - boot - - ActiveSupport::Dependencies.verbose = true - Rails.autoloaders.each do |autoloader| - assert autoloader.logger - end - - ActiveSupport::Dependencies.verbose = false - Rails.autoloaders.each do |autoloader| - assert_nil autoloader.logger - end - end - test "unhooks" do boot diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 1317e76b51..cff9972943 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -863,8 +863,20 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_edge_option - generator([destination_root], edge: true, skip_webpack_install: true) - run_generator_instance + Rails.stub(:gem_version, Gem::Version.new("2.1.0")) do + generator([destination_root], edge: true, skip_webpack_install: true) + run_generator_instance + end + + assert_equal 1, @bundle_commands.count("install") + assert_file "Gemfile", %r{^gem\s+["']rails["'],\s+github:\s+["']#{Regexp.escape("rails/rails")}["'],\s+branch:\s+["']2-1-stable["']$} + end + + def test_edge_option_during_alpha + Rails.stub(:gem_version, Gem::Version.new("2.1.0.alpha")) do + generator([destination_root], edge: true, skip_webpack_install: true) + run_generator_instance + end assert_equal 1, @bundle_commands.count("install") assert_file "Gemfile", %r{^gem\s+["']rails["'],\s+github:\s+["']#{Regexp.escape("rails/rails")}["'],\s+branch:\s+["']main["']$} diff --git a/railties/test/generators/namespaced_generators_test.rb b/railties/test/generators/namespaced_generators_test.rb index 12ccbe6ff1..6b9a53f226 100644 --- a/railties/test/generators/namespaced_generators_test.rb +++ b/railties/test/generators/namespaced_generators_test.rb @@ -25,7 +25,6 @@ class NamespacedControllerGeneratorTest < NamespacedGeneratorTestCase def test_namespaced_controller_skeleton_is_created run_generator assert_file "app/controllers/test_app/account_controller.rb", - /require_dependency "test_app\/application_controller"/, /module TestApp/, / class AccountController < ApplicationController/ @@ -42,9 +41,7 @@ class NamespacedControllerGeneratorTest < NamespacedGeneratorTestCase def test_namespaced_controller_with_additional_namespace run_generator ["admin/account"] - assert_file "app/controllers/test_app/admin/account_controller.rb", /module TestApp/, / class Admin::AccountController < ApplicationController/ do |contents| - assert_match %r(require_dependency "test_app/application_controller"), contents - end + assert_file "app/controllers/test_app/admin/account_controller.rb", /module TestApp/, / class Admin::AccountController < ApplicationController/ end def test_helper_is_also_namespaced @@ -216,7 +213,6 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase # Controller assert_file "app/controllers/test_app/product_lines_controller.rb", - /require_dependency "test_app\/application_controller"/, /module TestApp/, /class ProductLinesController < ApplicationController/ @@ -284,7 +280,6 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase # Controller assert_file "app/controllers/test_app/admin/roles_controller.rb" do |content| assert_match(/module TestApp\n class Admin::RolesController < ApplicationController/, content) - assert_match(%r(require_dependency "test_app/application_controller"), content) end assert_file "test/controllers/test_app/admin/roles_controller_test.rb", @@ -418,7 +413,6 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase # Controller assert_file "app/controllers/test_app/admin/roles_controller.rb" do |content| assert_match(/module TestApp\n class Admin::RolesController < ApplicationController/, content) - assert_match(%r(require_dependency "test_app/application_controller"), content) end assert_file "test/controllers/test_app/admin/roles_controller_test.rb", /module TestApp\n class Admin::RolesControllerTest < ActionDispatch::IntegrationTest/ diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index 49975403ee..5a10cf1e32 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -250,8 +250,20 @@ class PluginGeneratorTest < Rails::Generators::TestCase end def test_edge_option - generator([destination_root], edge: true) - run_generator_instance + Rails.stub(:gem_version, Gem::Version.new("2.1.0")) do + generator([destination_root], edge: true) + run_generator_instance + end + + assert_empty @bundle_commands + assert_file "Gemfile", %r{^gem\s+["']rails["'],\s+github:\s+["']#{Regexp.escape("rails/rails")}["'],\s+branch:\s+["']2-1-stable["']$} + end + + def test_edge_option_during_alpha + Rails.stub(:gem_version, Gem::Version.new("2.1.0.alpha")) do + generator([destination_root], edge: true) + run_generator_instance + end assert_empty @bundle_commands assert_file "Gemfile", %r{^gem\s+["']rails["'],\s+github:\s+["']#{Regexp.escape("rails/rails")}["'],\s+branch:\s+["']main["']$} @@ -469,6 +481,57 @@ class PluginGeneratorTest < Rails::Generators::TestCase end end + def test_creating_full_engine_mode_adds_keeps + run_generator [destination_root, "--full"] + folders_with_keep = %w( + app/models/concerns + app/controllers/concerns + test/fixtures/files + test/controllers + test/mailers + test/models + test/helpers + test/integration + ) + folders_with_keep.each do |folder| + assert_file("#{folder}/.keep") + end + end + + def test_creating_full_api_engine_adds_keeps + run_generator [destination_root, "--full", "--api"] + folders_with_keep = %w( + app/models/concerns + app/controllers/concerns + test/fixtures/files + test/controllers + test/mailers + test/models + test/integration + ) + folders_with_keep.each do |folder| + assert_file("#{folder}/.keep") + end + assert_no_file("test/helpers/.keep") + end + + def test_creating_mountable_engine_mode_adds_keeps + run_generator [destination_root, "--mountable"] + folders_with_keep = %w( + app/models/concerns + app/controllers/concerns + test/fixtures/files + test/controllers + test/mailers + test/models + test/helpers + test/integration + ) + folders_with_keep.each do |folder| + assert_file("#{folder}/.keep") + end + end + def test_creating_gemspec run_generator assert_file "bukkits.gemspec", /spec\.name\s+= "bukkits"/