diff options
102 files changed, 1864 insertions, 1532 deletions
diff --git a/actioncable/README.md b/actioncable/README.md index d14f20d75b..6946dbefb0 100644 --- a/actioncable/README.md +++ b/actioncable/README.md @@ -409,7 +409,7 @@ application. The recommended basic setup is as follows: ```ruby # cable/config.ru -require ::File.expand_path('../config/environment', __dir__) +require_relative '../config/environment' Rails.application.eager_load! run ActionCable.server diff --git a/actioncable/bin/test b/actioncable/bin/test index a7beb14b27..470ce93f10 100755 --- a/actioncable/bin/test +++ b/actioncable/bin/test @@ -1,4 +1,4 @@ #!/usr/bin/env ruby COMPONENT_ROOT = File.expand_path("..", __dir__) -require File.expand_path("../tools/test", COMPONENT_ROOT) +require_relative "../../tools/test" diff --git a/actioncable/test/subscription_adapter/async_test.rb b/actioncable/test/subscription_adapter/async_test.rb index 7bc2e55d40..8a447c7a4f 100644 --- a/actioncable/test/subscription_adapter/async_test.rb +++ b/actioncable/test/subscription_adapter/async_test.rb @@ -1,5 +1,5 @@ require "test_helper" -require_relative "./common" +require_relative "common" class AsyncAdapterTest < ActionCable::TestCase include CommonSubscriptionAdapterTest diff --git a/actioncable/test/subscription_adapter/evented_redis_test.rb b/actioncable/test/subscription_adapter/evented_redis_test.rb index 256458bc24..5453511549 100644 --- a/actioncable/test/subscription_adapter/evented_redis_test.rb +++ b/actioncable/test/subscription_adapter/evented_redis_test.rb @@ -1,6 +1,6 @@ require "test_helper" -require_relative "./common" -require_relative "./channel_prefix" +require_relative "common" +require_relative "channel_prefix" class EventedRedisAdapterTest < ActionCable::TestCase include CommonSubscriptionAdapterTest diff --git a/actioncable/test/subscription_adapter/inline_test.rb b/actioncable/test/subscription_adapter/inline_test.rb index 52bfa00aba..eafa3df2df 100644 --- a/actioncable/test/subscription_adapter/inline_test.rb +++ b/actioncable/test/subscription_adapter/inline_test.rb @@ -1,5 +1,5 @@ require "test_helper" -require_relative "./common" +require_relative "common" class InlineAdapterTest < ActionCable::TestCase include CommonSubscriptionAdapterTest diff --git a/actioncable/test/subscription_adapter/postgresql_test.rb b/actioncable/test/subscription_adapter/postgresql_test.rb index beb6efab28..ada4c2e2bd 100644 --- a/actioncable/test/subscription_adapter/postgresql_test.rb +++ b/actioncable/test/subscription_adapter/postgresql_test.rb @@ -1,5 +1,5 @@ require "test_helper" -require_relative "./common" +require_relative "common" require "active_record" diff --git a/actioncable/test/subscription_adapter/redis_test.rb b/actioncable/test/subscription_adapter/redis_test.rb index 4df5e0cbcd..11120b3f85 100644 --- a/actioncable/test/subscription_adapter/redis_test.rb +++ b/actioncable/test/subscription_adapter/redis_test.rb @@ -1,6 +1,6 @@ require "test_helper" -require_relative "./common" -require_relative "./channel_prefix" +require_relative "common" +require_relative "channel_prefix" class RedisAdapterTest < ActionCable::TestCase include CommonSubscriptionAdapterTest diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index e488d867de..9993a11c9d 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1 +1,12 @@ +* Allow Action Mailer classes to configure their delivery job. + + class MyMailer < ApplicationMailer + self.delivery_job = MyCustomDeliveryJob + + ... + end + + *Matthew Mongeau* + + Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/actionmailer/CHANGELOG.md) for previous changes. diff --git a/actionmailer/bin/test b/actionmailer/bin/test index a7beb14b27..470ce93f10 100755 --- a/actionmailer/bin/test +++ b/actionmailer/bin/test @@ -1,4 +1,4 @@ #!/usr/bin/env ruby COMPONENT_ROOT = File.expand_path("..", __dir__) -require File.expand_path("../tools/test", COMPONENT_ROOT) +require_relative "../../tools/test" diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 7133670b65..f8aa54bd44 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -459,6 +459,7 @@ module ActionMailer helper ActionMailer::MailHelper + class_attribute :delivery_job, default: ::ActionMailer::DeliveryJob class_attribute :default_params, default: { mime_version: "1.0", charset: "UTF-8", diff --git a/actionmailer/lib/action_mailer/message_delivery.rb b/actionmailer/lib/action_mailer/message_delivery.rb index cf7c57e6bf..0b54e12431 100644 --- a/actionmailer/lib/action_mailer/message_delivery.rb +++ b/actionmailer/lib/action_mailer/message_delivery.rb @@ -51,6 +51,14 @@ module ActionMailer # Notifier.welcome(User.first).deliver_later!(wait: 1.hour) # Notifier.welcome(User.first).deliver_later!(wait_until: 10.hours.from_now) # + # By default, the email will be enqueued using <tt>ActionMailer::DeliveryJob</tt>. Each + # <tt>ActionMailer::Base</tt> class can specify the job to use by setting the class variable + # +delivery_job+. + # + # class AccountRegistrationMailer < ApplicationMailer + # self.delivery_job = RegistrationDeliveryJob + # end + # # Options: # # * <tt>:wait</tt> - Enqueue the email to be delivered with a delay @@ -67,6 +75,14 @@ module ActionMailer # Notifier.welcome(User.first).deliver_later(wait: 1.hour) # Notifier.welcome(User.first).deliver_later(wait_until: 10.hours.from_now) # + # By default, the email will be enqueued using <tt>ActionMailer::DeliveryJob</tt>. Each + # <tt>ActionMailer::Base</tt> class can specify the job to use by setting the class variable + # +delivery_job+. + # + # class AccountRegistrationMailer < ApplicationMailer + # self.delivery_job = RegistrationDeliveryJob + # end + # # Options: # # * <tt>:wait</tt> - Enqueue the email to be delivered with a delay. @@ -118,7 +134,8 @@ module ActionMailer "method*, or 3. use a custom Active Job instead of #deliver_later." else args = @mailer_class.name, @action.to_s, delivery_method.to_s, *@args - ::ActionMailer::DeliveryJob.set(options).perform_later(*args) + job = @mailer_class.delivery_job + job.set(options).perform_later(*args) end end end diff --git a/actionmailer/test/message_delivery_test.rb b/actionmailer/test/message_delivery_test.rb index c0683be94d..51f10b0bf1 100644 --- a/actionmailer/test/message_delivery_test.rb +++ b/actionmailer/test/message_delivery_test.rb @@ -95,6 +95,19 @@ class MessageDeliveryTest < ActiveSupport::TestCase end end + test "should enqueue the job with the correct delivery job" do + old_delivery_job = DelayedMailer.delivery_job + DelayedMailer.delivery_job = DummyJob + + assert_performed_with(job: DummyJob, args: ["DelayedMailer", "test_message", "deliver_now", 1, 2, 3]) do + @mail.deliver_later + end + + DelayedMailer.delivery_job = old_delivery_job + end + + class DummyJob < ActionMailer::DeliveryJob; end + test "can override the queue when enqueuing mail" do assert_performed_with(job: ActionMailer::DeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now", 1, 2, 3], queue: "another_queue") do @mail.deliver_later(queue: :another_queue) diff --git a/actionpack/bin/test b/actionpack/bin/test index a7beb14b27..470ce93f10 100755 --- a/actionpack/bin/test +++ b/actionpack/bin/test @@ -1,4 +1,4 @@ #!/usr/bin/env ruby COMPONENT_ROOT = File.expand_path("..", __dir__) -require File.expand_path("../tools/test", COMPONENT_ROOT) +require_relative "../../tools/test" diff --git a/actionpack/lib/abstract_controller/translation.rb b/actionpack/lib/abstract_controller/translation.rb index 9e3858802a..e4ac95df50 100644 --- a/actionpack/lib/abstract_controller/translation.rb +++ b/actionpack/lib/abstract_controller/translation.rb @@ -13,7 +13,7 @@ module AbstractController path = controller_path.tr("/", ".") defaults = [:"#{path}#{key}"] defaults << options[:default] if options[:default] - options[:default] = defaults + options[:default] = defaults.flatten key = "#{path}.#{action_name}#{key}" end I18n.translate(key, options) diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 6e7a68cdf8..533925ebe1 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -630,7 +630,7 @@ module ActionDispatch secret = key_generator.generate_key(request.encrypted_cookie_salt || "")[0, ActiveSupport::MessageEncryptor.key_len] sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || "") - @legacy_encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) + @legacy_encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end def decrypt_and_verify_legacy_encrypted_message(name, signed_message) diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index 5d10129d21..fb99f13a1c 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -6,11 +6,11 @@ module ActionDispatch # When initialized, it can accept optional HTTP headers, which will be set # when a response containing a file's contents is delivered. # - # This middleware will render the file specified in `env["PATH_INFO"]` + # This middleware will render the file specified in <tt>env["PATH_INFO"]</tt> # where the base path is in the +root+ directory. For example, if the +root+ - # is set to `public/`, then a request with `env["PATH_INFO"]` of - # `assets/application.js` will return a response with the contents of a file - # located at `public/assets/application.js` if the file exists. If the file + # is set to +public/+, then a request with <tt>env["PATH_INFO"]</tt> of + # +assets/application.js+ will return a response with the contents of a file + # located at +public/assets/application.js+ if the file exists. If the file # does not exist, a 404 "File not Found" response will be returned. class FileHandler def initialize(root, index: "index", headers: {}) @@ -23,8 +23,8 @@ module ActionDispatch # correct read permissions, the return value is a URI-escaped string # representing the filename. Otherwise, false is returned. # - # Used by the `Static` class to check the existence of a valid file - # in the server's `public/` directory (see Static#call). + # Used by the +Static+ class to check the existence of a valid file + # in the server's +public/+ directory (see Static#call). def match?(path) path = ::Rack::Utils.unescape_path path return false unless ::Rack::Utils.valid_path? path @@ -99,7 +99,7 @@ module ActionDispatch # This middleware will attempt to return the contents of a file's body from # disk in the response. If a file is not found on disk, the request will be # delegated to the application stack. This middleware is commonly initialized - # to serve assets from a server's `public/` directory. + # to serve assets from a server's +public/+ directory. # # This middleware verifies the path to ensure that only files # living in the root directory can be rendered. A request cannot diff --git a/actionpack/test/abstract/translation_test.rb b/actionpack/test/abstract/translation_test.rb index 0c4071df8d..4893144905 100644 --- a/actionpack/test/abstract/translation_test.rb +++ b/actionpack/test/abstract/translation_test.rb @@ -62,6 +62,7 @@ module AbstractController def test_default_translation @controller.stub :action_name, :index do assert_equal "bar", @controller.t("one.two") + assert_equal "baz", @controller.t(".twoz", default: ["baz", :twoz]) end end diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 122c42c5bd..ceaea40d18 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,11 @@ +* Generate field ids in `collection_check_boxes` and `collection_radio_buttons`. + + This makes sure that the labels are linked up with the fields. + + Fixes #29014. + + *Yuji Yaginuma* + * Add `:json` type to `auto_discovery_link_tag` to support [JSON Feeds](https://jsonfeed.org/version/1) *Mike Gunderloy* diff --git a/actionview/bin/test b/actionview/bin/test index a7beb14b27..470ce93f10 100755 --- a/actionview/bin/test +++ b/actionview/bin/test @@ -1,4 +1,4 @@ #!/usr/bin/env ruby COMPONENT_ROOT = File.expand_path("..", __dir__) -require File.expand_path("../tools/test", COMPONENT_ROOT) +require_relative "../../tools/test" diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb index 37f0dd1ee4..1808553239 100644 --- a/actionview/lib/action_view/base.rb +++ b/actionview/lib/action_view/base.rb @@ -202,6 +202,7 @@ module ActionView #:nodoc: @view_renderer = ActionView::Renderer.new(lookup_context) end + @cache_hit = {} assign(assigns) assign_controller(controller) _prepare_context diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb index c3aecadcd6..b7c7324f31 100644 --- a/actionview/lib/action_view/helpers/cache_helper.rb +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -214,8 +214,6 @@ module ActionView end end - attr_reader :cache_hit # :nodoc: - private def fragment_name_with_digest(name, virtual_path) @@ -236,10 +234,10 @@ module ActionView def fragment_for(name = {}, options = nil, &block) if content = read_fragment_for(name, options) - @cache_hit = true + @view_renderer.cache_hits[@virtual_path] = :hit if defined?(@view_renderer) content else - @cache_hit = false + @view_renderer.cache_hits[@virtual_path] = :miss if defined?(@view_renderer) write_fragment_for(name, options, &block) end end diff --git a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb index 7252d4f2d9..e02b7bdb2e 100644 --- a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb +++ b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb @@ -10,6 +10,7 @@ module ActionView def check_box(extra_html_options = {}) html_options = extra_html_options.merge(@input_html_options) html_options[:multiple] = true + html_options[:skip_default_ids] = false @template_object.check_box(@object_name, @method_name, html_options, @value, nil) end end diff --git a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb index a5f72af9ff..f085a5fb73 100644 --- a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb +++ b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb @@ -9,6 +9,7 @@ module ActionView class RadioButtonBuilder < Builder # :nodoc: def radio_button(extra_html_options = {}) html_options = extra_html_options.merge(@input_html_options) + html_options[:skip_default_ids] = false @template_object.radio_button(@object_name, @method_name, @value, html_options) end end diff --git a/actionview/lib/action_view/log_subscriber.rb b/actionview/lib/action_view/log_subscriber.rb index d03e1a51b8..ab8ec0aa42 100644 --- a/actionview/lib/action_view/log_subscriber.rb +++ b/actionview/lib/action_view/log_subscriber.rb @@ -25,7 +25,7 @@ module ActionView message = " Rendered #{from_rails_root(event.payload[:identifier])}" message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] message << " (#{event.duration.round(1)}ms)" - message << " #{cache_message(event.payload)}" if event.payload.key?(:cache_hit) + message << " #{cache_message(event.payload)}" unless event.payload[:cache_hit].nil? message end end @@ -73,9 +73,10 @@ module ActionView end def cache_message(payload) # :doc: - if payload[:cache_hit] + case payload[:cache_hit] + when :hit "[cache hit]" - else + when :miss "[cache miss]" end end diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index 647b15ea94..1f8f997a2d 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -344,7 +344,7 @@ module ActionView end content = layout.render(view, locals) { content } if layout - payload[:cache_hit] = view.cache_hit + payload[:cache_hit] = view.view_renderer.cache_hits[@template.virtual_path] content end end diff --git a/actionview/lib/action_view/renderer/renderer.rb b/actionview/lib/action_view/renderer/renderer.rb index 2a3b89aebf..bcdeb85d30 100644 --- a/actionview/lib/action_view/renderer/renderer.rb +++ b/actionview/lib/action_view/renderer/renderer.rb @@ -46,5 +46,9 @@ module ActionView def render_partial(context, options, &block) #:nodoc: PartialRenderer.new(@lookup_context).render(context, options, block) end + + def cache_hits # :nodoc: + @cache_hits ||= {} + end end end diff --git a/actionview/test/activerecord/relation_cache_test.rb b/actionview/test/activerecord/relation_cache_test.rb index fbab512c41..d12c426586 100644 --- a/actionview/test/activerecord/relation_cache_test.rb +++ b/actionview/test/activerecord/relation_cache_test.rb @@ -4,7 +4,11 @@ class RelationCacheTest < ActionView::TestCase tests ActionView::Helpers::CacheHelper def setup - @virtual_path = "path" + view_paths = ActionController::Base.view_paths + lookup_context = ActionView::LookupContext.new(view_paths, {}, ["test"]) + @view_renderer = ActionView::Renderer.new(lookup_context) + @virtual_path = "path" + controller.cache_store = ActiveSupport::Cache::MemoryStore.new end diff --git a/actionview/test/fixtures/test/_cached_nested_cached_customer.erb b/actionview/test/fixtures/test/_cached_nested_cached_customer.erb new file mode 100644 index 0000000000..01bf025cd3 --- /dev/null +++ b/actionview/test/fixtures/test/_cached_nested_cached_customer.erb @@ -0,0 +1,3 @@ +<% cache cached_customer do %> + <%= render partial: "test/cached_customer", locals: { cached_customer: cached_customer } %> +<% end %> diff --git a/actionview/test/fixtures/test/_nested_cached_customer.erb b/actionview/test/fixtures/test/_nested_cached_customer.erb new file mode 100644 index 0000000000..f43adc94c9 --- /dev/null +++ b/actionview/test/fixtures/test/_nested_cached_customer.erb @@ -0,0 +1 @@ +<%= render partial: "test/cached_customer", locals: { cached_customer: cached_customer } %> diff --git a/actionview/test/template/form_helper/form_with_test.rb b/actionview/test/template/form_helper/form_with_test.rb index df580f0369..ecdd5ce672 100644 --- a/actionview/test/template/form_helper/form_with_test.rb +++ b/actionview/test/template/form_helper/form_with_test.rb @@ -403,9 +403,9 @@ class FormWithActsLikeFormForTest < FormWithTest expected = whole_form("/posts") do "<input type='hidden' name='post[active]' value='' />" \ - "<input name='post[active]' type='radio' value='true' />" \ + "<input name='post[active]' type='radio' value='true' id='post_active_true' />" \ "<label for='post_active_true'>true</label>" \ - "<input checked='checked' name='post[active]' type='radio' value='false' />" \ + "<input checked='checked' name='post[active]' type='radio' value='false' id='post_active_false' />" \ "<label for='post_active_false'>false</label>" end @@ -426,10 +426,10 @@ class FormWithActsLikeFormForTest < FormWithTest expected = whole_form("/posts") do "<input type='hidden' name='post[active]' value='' />" \ "<label for='post_active_true'>" \ - "<input name='post[active]' type='radio' value='true' />" \ + "<input name='post[active]' type='radio' value='true' id='post_active_true' />" \ "true</label>" \ "<label for='post_active_false'>" \ - "<input checked='checked' name='post[active]' type='radio' value='false' />" \ + "<input checked='checked' name='post[active]' type='radio' value='false' id='post_active_false' />" \ "false</label>" end @@ -452,10 +452,10 @@ class FormWithActsLikeFormForTest < FormWithTest expected = whole_form("/posts") do "<input type='hidden' name='post[active]' value='' />" \ "<label for='post_active_true'>" \ - "<input name='post[active]' type='radio' value='true' />" \ + "<input name='post[active]' type='radio' value='true' id='post_active_true' />" \ "true</label>" \ "<label for='post_active_false'>" \ - "<input checked='checked' name='post[active]' type='radio' value='false' />" \ + "<input checked='checked' name='post[active]' type='radio' value='false' id='post_active_false' />" \ "false</label>" \ "<input name='post[id]' type='hidden' value='1' />" end @@ -473,9 +473,9 @@ class FormWithActsLikeFormForTest < FormWithTest expected = whole_form("/posts") do "<input type='hidden' name='post[1][active]' value='' />" \ - "<input name='post[1][active]' type='radio' value='true' />" \ + "<input name='post[1][active]' type='radio' value='true' id='post_1_active_true' />" \ "<label for='post_1_active_true'>true</label>" \ - "<input checked='checked' name='post[1][active]' type='radio' value='false' />" \ + "<input checked='checked' name='post[1][active]' type='radio' value='false' id='post_1_active_false' />" \ "<label for='post_1_active_false'>false</label>" end @@ -492,11 +492,11 @@ class FormWithActsLikeFormForTest < FormWithTest expected = whole_form("/posts") do "<input name='post[tag_ids][]' type='hidden' value='' />" \ - "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='1' />" \ + "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='1' id='post_tag_ids_1' />" \ "<label for='post_tag_ids_1'>Tag 1</label>" \ - "<input name='post[tag_ids][]' type='checkbox' value='2' />" \ + "<input name='post[tag_ids][]' type='checkbox' value='2' id='post_tag_ids_2' />" \ "<label for='post_tag_ids_2'>Tag 2</label>" \ - "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' />" \ + "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' id='post_tag_ids_3' />" \ "<label for='post_tag_ids_3'>Tag 3</label>" end @@ -517,13 +517,13 @@ class FormWithActsLikeFormForTest < FormWithTest expected = whole_form("/posts") do "<input name='post[tag_ids][]' type='hidden' value='' />" \ "<label for='post_tag_ids_1'>" \ - "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='1' />" \ + "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='1' id='post_tag_ids_1' />" \ "Tag 1</label>" \ "<label for='post_tag_ids_2'>" \ - "<input name='post[tag_ids][]' type='checkbox' value='2' />" \ + "<input name='post[tag_ids][]' type='checkbox' value='2' id='post_tag_ids_2' />" \ "Tag 2</label>" \ "<label for='post_tag_ids_3'>" \ - "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' />" \ + "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' id='post_tag_ids_3' />" \ "Tag 3</label>" end @@ -547,13 +547,13 @@ class FormWithActsLikeFormForTest < FormWithTest expected = whole_form("/posts") do "<input name='post[tag_ids][]' type='hidden' value='' />" \ "<label for='post_tag_ids_1'>" \ - "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='1' />" \ + "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='1' id='post_tag_ids_1' />" \ "Tag 1</label>" \ "<label for='post_tag_ids_2'>" \ - "<input name='post[tag_ids][]' type='checkbox' value='2' />" \ + "<input name='post[tag_ids][]' type='checkbox' value='2' id='post_tag_ids_2' />" \ "Tag 2</label>" \ "<label for='post_tag_ids_3'>" \ - "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' />" \ + "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' id='post_tag_ids_3' />" \ "Tag 3</label>" \ "<input name='post[id]' type='hidden' value='1' />" end @@ -572,7 +572,7 @@ class FormWithActsLikeFormForTest < FormWithTest expected = whole_form("/posts") do "<input name='post[1][tag_ids][]' type='hidden' value='' />" \ - "<input checked='checked' name='post[1][tag_ids][]' type='checkbox' value='1' />" \ + "<input checked='checked' name='post[1][tag_ids][]' type='checkbox' value='1' id='post_1_tag_ids_1' />" \ "<label for='post_1_tag_ids_1'>Tag 1</label>" end diff --git a/actionview/test/template/log_subscriber_test.rb b/actionview/test/template/log_subscriber_test.rb index 584666d54b..4c9f84f277 100644 --- a/actionview/test/template/log_subscriber_test.rb +++ b/actionview/test/template/log_subscriber_test.rb @@ -8,11 +8,14 @@ class AVLogSubscriberTest < ActiveSupport::TestCase def setup super - view_paths = ActionController::Base.view_paths + + view_paths = ActionController::Base.view_paths lookup_context = ActionView::LookupContext.new(view_paths, {}, ["test"]) - renderer = ActionView::Renderer.new(lookup_context) - @view = ActionView::Base.new(renderer, {}) + renderer = ActionView::Renderer.new(lookup_context) + @view = ActionView::Base.new(renderer, {}) + ActionView::LogSubscriber.attach_to :action_view + unless Rails.respond_to?(:root) @defined_root = true def Rails.root; :defined_root; end # Minitest `stub` expects the method to be defined. @@ -21,7 +24,9 @@ class AVLogSubscriberTest < ActiveSupport::TestCase def teardown super + ActiveSupport::LogSubscriber.log_subscribers.clear + # We need to undef `root`, RenderTestCases don't want this to be defined Rails.instance_eval { undef :root } if @defined_root end @@ -103,9 +108,9 @@ class AVLogSubscriberTest < ActiveSupport::TestCase set_view_cache_dependencies set_cache_controller - @view.render(partial: "test/cached_customer", locals: { cached_customer: Customer.new("david") }) # Second render should hit cache. @view.render(partial: "test/cached_customer", locals: { cached_customer: Customer.new("david") }) + @view.render(partial: "test/cached_customer", locals: { cached_customer: Customer.new("david") }) wait assert_equal 2, @logger.logged(:info).size @@ -113,6 +118,46 @@ class AVLogSubscriberTest < ActiveSupport::TestCase end end + def test_render_uncached_outer_partial_with_inner_cached_partial_wont_mix_cache_hits_or_misses + Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do + set_view_cache_dependencies + set_cache_controller + + @view.render(partial: "test/nested_cached_customer", locals: { cached_customer: Customer.new("Stan") }) + wait + *, cached_inner, uncached_outer = @logger.logged(:info) + assert_match(/Rendered test\/_cached_customer\.erb (.*) \[cache miss\]/, cached_inner) + assert_match(/Rendered test\/_nested_cached_customer\.erb \(.*?ms\)$/, uncached_outer) + + # Second render hits the cache for the _cached_customer partial. Outer template's log shouldn't be affected. + @view.render(partial: "test/nested_cached_customer", locals: { cached_customer: Customer.new("Stan") }) + wait + *, cached_inner, uncached_outer = @logger.logged(:info) + assert_match(/Rendered test\/_cached_customer\.erb (.*) \[cache hit\]/, cached_inner) + assert_match(/Rendered test\/_nested_cached_customer\.erb \(.*?ms\)$/, uncached_outer) + end + end + + def test_render_cached_outer_partial_with_cached_inner_partial + Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do + set_view_cache_dependencies + set_cache_controller + + @view.render(partial: "test/cached_nested_cached_customer", locals: { cached_customer: Customer.new("Stan") }) + wait + *, cached_inner, cached_outer = @logger.logged(:info) + assert_match(/Rendered test\/_cached_customer\.erb (.*) \[cache miss\]/, cached_inner) + assert_match(/Rendered test\/_cached_nested_cached_customer\.erb (.*) \[cache miss\]/, cached_outer) + + # One render: inner partial skipped, because the outer has been cached. + assert_difference -> { @logger.logged(:info).size }, +1 do + @view.render(partial: "test/cached_nested_cached_customer", locals: { cached_customer: Customer.new("Stan") }) + wait + end + assert_match(/Rendered test\/_cached_nested_cached_customer\.erb (.*) \[cache hit\]/, @logger.logged(:info).last) + end + end + def test_render_partial_with_cache_hitted_and_missed Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do set_view_cache_dependencies diff --git a/activejob/bin/test b/activejob/bin/test index a7beb14b27..470ce93f10 100755 --- a/activejob/bin/test +++ b/activejob/bin/test @@ -1,4 +1,4 @@ #!/usr/bin/env ruby COMPONENT_ROOT = File.expand_path("..", __dir__) -require File.expand_path("../tools/test", COMPONENT_ROOT) +require_relative "../../tools/test" diff --git a/activemodel/bin/test b/activemodel/bin/test index a7beb14b27..470ce93f10 100755 --- a/activemodel/bin/test +++ b/activemodel/bin/test @@ -1,4 +1,4 @@ #!/usr/bin/env ruby COMPONENT_ROOT = File.expand_path("..", __dir__) -require File.expand_path("../tools/test", COMPONENT_ROOT) +require_relative "../../tools/test" diff --git a/activemodel/lib/active_model/attribute_assignment.rb b/activemodel/lib/active_model/attribute_assignment.rb index 930e89d611..aa931119ff 100644 --- a/activemodel/lib/active_model/attribute_assignment.rb +++ b/activemodel/lib/active_model/attribute_assignment.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/hash/keys" module ActiveModel @@ -42,8 +44,9 @@ module ActiveModel end def _assign_attribute(k, v) - if respond_to?("#{k}=") - public_send("#{k}=", v) + setter = :"#{k}=" + if respond_to?(setter) + public_send(setter, v) else raise UnknownAttributeError.new(self, k) end diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 2d0d5bd657..fe5f9d1071 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -1,7 +1,7 @@ require "rake/testtask" -require File.expand_path("test/config", __dir__) -require File.expand_path("test/support/config", __dir__) +require_relative "test/config" +require_relative "test/support/config" def run_without_aborting(*tasks) errors = [] diff --git a/activerecord/bin/test b/activerecord/bin/test index 3a9547e5c1..ab69f4f603 100755 --- a/activerecord/bin/test +++ b/activerecord/bin/test @@ -1,7 +1,7 @@ #!/usr/bin/env ruby COMPONENT_ROOT = File.expand_path("..", __dir__) -require File.expand_path("../tools/test", COMPONENT_ROOT) +require_relative "../../tools/test" module Minitest def self.plugin_active_record_options(opts, options) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 7c37132d3a..f05a122544 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -342,7 +342,7 @@ module ActiveRecord # | | belongs_to | # generated methods | belongs_to | :polymorphic | has_one # ----------------------------------+------------+--------------+--------- - # other(force_reload=false) | X | X | X + # other | X | X | X # other=(other) | X | X | X # build_other(attributes={}) | X | | X # create_other(attributes={}) | X | | X @@ -352,7 +352,7 @@ module ActiveRecord # | | | has_many # generated methods | habtm | has_many | :through # ----------------------------------+-------+----------+---------- - # others(force_reload=false) | X | X | X + # others | X | X | X # others=(other,other,...) | X | X | X # other_ids | X | X | X # other_ids=(id,id,...) | X | X | X @@ -1187,7 +1187,7 @@ module ActiveRecord # +collection+ is a placeholder for the symbol passed as the +name+ argument, so # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>. # - # [collection(force_reload = false)] + # [collection] # Returns an array of all the associated objects. # An empty array is returned if none are found. # [collection<<(object, ...)] @@ -1407,7 +1407,7 @@ module ActiveRecord # +association+ is a placeholder for the symbol passed as the +name+ argument, so # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>. # - # [association(force_reload = false)] + # [association] # Returns the associated object. +nil+ is returned if none is found. # [association=(associate)] # Assigns the associate object, extracts the primary key, sets it as the foreign key, @@ -1539,7 +1539,7 @@ module ActiveRecord # +association+ is a placeholder for the symbol passed as the +name+ argument, so # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>. # - # [association(force_reload = false)] + # [association] # Returns the associated object. +nil+ is returned if none is found. # [association=(associate)] # Assigns the associate object, extracts the primary key, and sets it as the foreign key. @@ -1701,7 +1701,7 @@ module ActiveRecord # +collection+ is a placeholder for the symbol passed as the +name+ argument, so # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>. # - # [collection(force_reload = false)] + # [collection] # Returns an array of all the associated objects. # An empty array is returned if none are found. # [collection<<(object, ...)] diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 91580a28d0..ca138635d2 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -63,6 +63,10 @@ module ActiveRecord end def _create_record(attributes, raise_error = false) + unless owner.persisted? + raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" + end + record = build_record(attributes) yield(record) if block_given? saved = record.save diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 2f32caa257..b9b2acff37 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -8,17 +8,14 @@ module ActiveRecord # Returns this record's primary key value wrapped in an array if one is # available. def to_key - sync_with_transaction_state key = id [key] if key end # Returns the primary key value. def id - if pk = self.class.primary_key - sync_with_transaction_state - _read_attribute(pk) - end + sync_with_transaction_state + _read_attribute(self.class.primary_key) if self.class.primary_key end # Sets the primary key value. @@ -57,16 +54,12 @@ module ActiveRecord end module ClassMethods - def define_method_attribute(attr_name) - super + ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was id_in_database).to_set - if attr_name == primary_key && attr_name != "id" - generated_attribute_methods.send(:alias_method, :id, primary_key) - end + def instance_method_already_implemented?(method_name) + super || primary_key && ID_ATTRIBUTE_METHODS.include?(method_name) end - ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was id_in_database).to_set - def dangerous_attribute_method?(method_name) super && !ID_ATTRIBUTE_METHODS.include?(method_name) end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index fe0e01db28..75c5a1a600 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -35,11 +35,15 @@ module ActiveRecord attr_name.to_s end - write_attribute_with_type_cast(name, value, true) + name = self.class.primary_key if name == "id".freeze && self.class.primary_key + @attributes.write_from_user(name, value) + value end def raw_write_attribute(attr_name, value) # :nodoc: - write_attribute_with_type_cast(attr_name, value, false) + name = attr_name.to_s + @attributes.write_cast_value(name, value) + value end private @@ -47,19 +51,6 @@ module ActiveRecord def attribute=(attribute_name, value) write_attribute(attribute_name, value) end - - def write_attribute_with_type_cast(attr_name, value, should_type_cast) - attr_name = attr_name.to_s - attr_name = self.class.primary_key if attr_name == "id" && self.class.primary_key - - if should_type_cast - @attributes.write_from_user(attr_name, value) - else - @attributes.write_cast_value(attr_name, value) - end - - value - end end end end 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 a4fecc4a8e..93f4529202 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -55,7 +55,7 @@ module ActiveRecord create_sql << "(#{statements.join(', ')})" if statements.present? add_table_options!(create_sql, table_options(o)) - create_sql << " AS #{@conn.to_sql(o.as)}" if o.as + create_sql << " AS #{to_sql(o.as)}" if o.as create_sql end @@ -114,6 +114,11 @@ module ActiveRecord sql end + def to_sql(sql) + sql = sql.to_sql if sql.respond_to?(:to_sql) + sql + end + def foreign_key_in_create(from_table, to_table, options) options = foreign_key_options(from_table, to_table, options) accept ForeignKeyDefinition.new(from_table, to_table, options) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index bcb939855f..22d7791dec 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -512,8 +512,7 @@ module ActiveRecord # * <tt>:default</tt> - # The column's default value. Use +nil+ for +NULL+. # * <tt>:null</tt> - - # Allows or disallows +NULL+ values in the column. This option could - # have been named <tt>:null_allowed</tt>. + # Allows or disallows +NULL+ values in the column. # * <tt>:precision</tt> - # Specifies the precision for the <tt>:decimal</tt> and <tt>:numeric</tt> columns. # * <tt>:scale</tt> - @@ -1011,7 +1010,7 @@ module ActiveRecord def dump_schema_information #:nodoc: versions = ActiveRecord::SchemaMigration.all_versions - insert_versions_sql(versions) + insert_versions_sql(versions) if versions.any? end def initialize_schema_migrations_table # :nodoc: diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 79e65baae5..6ccdd7adcb 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1167,7 +1167,7 @@ module ActiveRecord end end - STRUCTURAL_OR_METHODS = Relation::VALUE_METHODS - [:extending, :where, :having] + STRUCTURAL_OR_METHODS = Relation::VALUE_METHODS - [:extending, :where, :having, :unscope] def structurally_incompatible_values_for_or(other) STRUCTURAL_OR_METHODS.reject do |method| get_value(method) == other.get_value(method) diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index d124b64861..8eddd81c38 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -40,7 +40,8 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase drop_table "uuid_data_type" end - if ActiveRecord::Base.connection.supports_pgcrypto_uuid? + if ActiveRecord::Base.connection.respond_to?(:supports_pgcrypto_uuid?) && + ActiveRecord::Base.connection.supports_pgcrypto_uuid? def test_uuid_column_default connection.add_column :uuid_data_type, :thingy, :uuid, null: false, default: "gen_random_uuid()" UUIDType.reset_column_information diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 7c11d2e7fc..bf3b8dcd63 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -307,6 +307,15 @@ class HasOneAssociationsTest < ActiveRecord::TestCase end end + def test_create_when_parent_is_new_raises + firm = Firm.new + error = assert_raise(ActiveRecord::RecordNotSaved) do + firm.create_account + end + + assert_equal "You cannot call create unless the parent is saved", error.message + end + def test_reload_association odegy = companies(:odegy) diff --git a/activerecord/test/cases/errors_test.rb b/activerecord/test/cases/errors_test.rb index 73feb831d0..e90669e0c7 100644 --- a/activerecord/test/cases/errors_test.rb +++ b/activerecord/test/cases/errors_test.rb @@ -1,4 +1,4 @@ -require_relative "../cases/helper" +require "cases/helper" class ErrorsTest < ActiveRecord::TestCase def test_can_be_instantiated_with_no_args diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb index 7a80bfb899..596a21dcbc 100644 --- a/activerecord/test/cases/migration/compatibility_test.rb +++ b/activerecord/test/cases/migration/compatibility_test.rb @@ -90,6 +90,21 @@ module ActiveRecord connection.drop_table :more_testings rescue nil end + def test_timestamps_have_null_constraints_if_not_present_in_migration_of_change_table + migration = Class.new(ActiveRecord::Migration[4.2]) { + def migrate(x) + change_table :testings do |t| + t.timestamps + end + end + }.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + assert connection.columns(:testings).find { |c| c.name == "created_at" }.null + assert connection.columns(:testings).find { |c| c.name == "updated_at" }.null + end + def test_timestamps_have_null_constraints_if_not_present_in_migration_for_adding_timestamps_to_existing_table migration = Class.new(ActiveRecord::Migration[4.2]) { def migrate(x) diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 57f94950f9..3a49a41580 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -502,11 +502,10 @@ class MigrationTest < ActiveRecord::TestCase unless mysql_enforcing_gtid_consistency? def test_create_table_with_query - Person.connection.create_table(:person, force: true) - - Person.connection.create_table :table_from_query_testings, as: "SELECT id FROM person" + Person.connection.create_table :table_from_query_testings, as: "SELECT id FROM people WHERE id = 1" columns = Person.connection.columns(:table_from_query_testings) + assert_equal [1], Person.connection.select_values("SELECT * FROM table_from_query_testings") assert_equal 1, columns.length assert_equal "id", columns.first.name ensure @@ -514,11 +513,10 @@ class MigrationTest < ActiveRecord::TestCase end def test_create_table_with_query_from_relation - Person.connection.create_table(:person, force: true) - - Person.connection.create_table :table_from_query_testings, as: Person.select(:id) + Person.connection.create_table :table_from_query_testings, as: Person.select(:id).where(id: 1) columns = Person.connection.columns(:table_from_query_testings) + assert_equal [1], Person.connection.select_values("SELECT * FROM table_from_query_testings") assert_equal 1, columns.length assert_equal "id", columns.first.name ensure diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 5a62cbd3a6..154faa56aa 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -117,7 +117,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase def test_reject_if_with_a_proc_which_returns_true_always_for_has_one Pirate.accepts_nested_attributes_for :ship, reject_if: proc { |attributes| true } - pirate = Pirate.new(catchphrase: "Stop wastin' me time") + pirate = Pirate.create(catchphrase: "Stop wastin' me time") ship = pirate.create_ship(name: "s1") pirate.update(ship_attributes: { name: "s2", id: ship.id }) assert_equal "s1", ship.reload.name diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 5ded619716..56229b70bc 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -46,7 +46,7 @@ class PrimaryKeysTest < ActiveRecord::TestCase topic = Topic.new topic.title = "New Topic" assert_nil topic.id - assert_nothing_raised { topic.save! } + topic.save! id = topic.id topicReloaded = Topic.find(id) @@ -56,23 +56,36 @@ class PrimaryKeysTest < ActiveRecord::TestCase def test_customized_primary_key_auto_assigns_on_save Keyboard.delete_all keyboard = Keyboard.new(name: "HHKB") - assert_nothing_raised { keyboard.save! } + keyboard.save! assert_equal keyboard.id, Keyboard.find_by_name("HHKB").id end def test_customized_primary_key_can_be_get_before_saving keyboard = Keyboard.new assert_nil keyboard.id - assert_nothing_raised { assert_nil keyboard.key_number } + assert_nil keyboard.key_number end def test_customized_string_primary_key_settable_before_save subscriber = Subscriber.new - assert_nothing_raised { subscriber.id = "webster123" } + subscriber.id = "webster123" assert_equal "webster123", subscriber.id assert_equal "webster123", subscriber.nick end + def test_update_with_non_primary_key_id_column + subscriber = Subscriber.first + subscriber.update(update_count: 1) + subscriber.reload + assert_equal 1, subscriber.update_count + end + + def test_update_columns_with_non_primary_key_id_column + subscriber = Subscriber.first + subscriber.update_columns(id: 1) + assert_not_equal 1, subscriber.nick + end + def test_string_key subscriber = Subscriber.find(subscribers(:first).nick) assert_equal(subscribers(:first).name, subscriber.name) @@ -83,7 +96,7 @@ class PrimaryKeysTest < ActiveRecord::TestCase subscriber.id = "jdoe" assert_equal("jdoe", subscriber.id) subscriber.name = "John Doe" - assert_nothing_raised { subscriber.save! } + subscriber.save! assert_equal("jdoe", subscriber.id) subscriberReloaded = Subscriber.find("jdoe") diff --git a/activerecord/test/cases/relation/or_test.rb b/activerecord/test/cases/relation/or_test.rb index abb7ca72dd..61b6601580 100644 --- a/activerecord/test/cases/relation/or_test.rb +++ b/activerecord/test/cases/relation/or_test.rb @@ -59,6 +59,31 @@ module ActiveRecord assert_equal "Relation passed to #or must be structurally compatible. Incompatible values: [:order]", error.message end + def test_or_with_unscope_where + expected = Post.where("id = 1 or id = 2") + partial = Post.where("id = 1 and id != 2") + assert_equal expected, partial.or(partial.unscope(:where).where("id = 2")).to_a + end + + def test_or_with_unscope_where_column + expected = Post.where("id = 1 or id = 2") + partial = Post.where(id: 1).where.not(id: 2) + assert_equal expected, partial.or(partial.unscope(where: :id).where("id = 2")).to_a + end + + def test_or_with_unscope_order + expected = Post.where("id = 1 or id = 2") + assert_equal expected, Post.order("body asc").where("id = 1").unscope(:order).or(Post.where("id = 2")).to_a + end + + def test_or_with_incompatible_unscope + error = assert_raises ArgumentError do + Post.order("body asc").where("id = 1").or(Post.order("body asc").where("id = 2").unscope(:order)).to_a + end + + assert_equal "Relation passed to #or must be structurally compatible. Incompatible values: [:order]", error.message + end + def test_or_when_grouping groups = Post.where("id < 10").group("body").select("body, COUNT(*) AS c") expected = groups.having("COUNT(*) > 1 OR body like 'Such%'").to_a.map { |o| [o.body, o.c] } diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 417c6f4832..4c81e825fa 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -17,6 +17,12 @@ class SchemaDumperTest < ActiveRecord::TestCase dump_all_table_schema [] end + def test_dump_schema_information_with_empty_versions + ActiveRecord::SchemaMigration.delete_all + schema_info = ActiveRecord::Base.connection.dump_schema_information + assert_no_match(/INSERT INTO/, schema_info) + end + def test_dump_schema_information_outputs_lexically_ordered_versions versions = %w{ 20100101010101 20100201010101 20100301010101 } versions.reverse_each do |v| @@ -320,7 +326,7 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dump_keeps_id_false_when_id_is_false_and_unique_not_null_column_added output = standard_dump - assert_match %r{create_table "subscribers", id: false}, output + assert_match %r{create_table "string_key_objects", id: false}, output end if ActiveRecord::Base.connection.supports_foreign_keys? diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 5c6d78b574..79ba306ef5 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -595,6 +595,52 @@ class TransactionTest < ActiveRecord::TestCase assert_not topic.frozen? end + def test_restore_id_after_rollback + topic = Topic.new + + Topic.transaction do + topic.save! + raise ActiveRecord::Rollback + end + + assert_nil topic.id + end + + def test_restore_custom_primary_key_after_rollback + movie = Movie.new(name: "foo") + + Movie.transaction do + movie.save! + raise ActiveRecord::Rollback + end + + assert_nil movie.id + end + + def test_assign_id_after_rollback + topic = Topic.create! + + Topic.transaction do + topic.save! + raise ActiveRecord::Rollback + end + + topic.id = nil + assert_nil topic.id + end + + def test_assign_custom_primary_key_after_rollback + movie = Movie.create!(name: "foo") + + Movie.transaction do + movie.save! + raise ActiveRecord::Rollback + end + + movie.id = nil + assert_nil movie.id + end + def test_rollback_of_frozen_records topic = Topic.create.freeze Topic.transaction do diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 50f1d9bfe7..dc8bdb98ff 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -807,16 +807,19 @@ ActiveRecord::Schema.define do t.string :sponsorable_type end - create_table :string_key_objects, id: false, primary_key: :id, force: true do |t| - t.string :id - t.string :name - t.integer :lock_version, null: false, default: 0 + create_table :string_key_objects, id: false, force: true do |t| + t.string :id, null: false + t.string :name + t.integer :lock_version, null: false, default: 0 + t.index :id, unique: true end - create_table :subscribers, force: true, id: false do |t| + create_table :subscribers, id: false, force: true do |t| t.string :nick, null: false t.string :name - t.column :books_count, :integer, null: false, default: 0 + t.integer :id + t.integer :books_count, null: false, default: 0 + t.integer :update_count, null: false, default: 0 t.index :nick, unique: true end diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index d1d61ac8d7..ab5237488a 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,10 @@ +* Default `ActiveSupport::MessageEncryptor` to use AES 256 GCM encryption. + + On for new Rails 5.2 apps. Upgrading apps can find the config as a new + framework default. + + *Assain Jaleel* + * Cache: `write_multi` Rails.cache.write_multi foo: 'bar', baz: 'qux' diff --git a/activesupport/bin/test b/activesupport/bin/test index a7beb14b27..470ce93f10 100755 --- a/activesupport/bin/test +++ b/activesupport/bin/test @@ -1,4 +1,4 @@ #!/usr/bin/env ruby COMPONENT_ROOT = File.expand_path("..", __dir__) -require File.expand_path("../tools/test", COMPONENT_ROOT) +require_relative "../../tools/test" diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index fa487060e2..3847d8b7ae 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -499,7 +499,7 @@ module ActiveSupport # The options hash is passed to the underlying cache implementation. # # All implementations may not support this method. - def clear + def clear(options = nil) raise NotImplementedError.new("#{self.class.name} does not support clear") end diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index d5c8585816..945f50a56e 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -27,7 +27,7 @@ module ActiveSupport # Deletes all items from the cache. In this case it deletes all the entries in the specified # file store directory except for .keep or .gitkeep. Be careful which directory is specified in your # config file when using +FileStore+ because everything in that directory will be deleted. - def clear + def clear(options = nil) root_dirs = exclude_from(cache_path, EXCLUDED_DIRS + GITKEEP_FILES) FileUtils.rm_r(root_dirs.collect { |f| File.join(cache_path, f) }) rescue Errno::ENOENT diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb index 91875a56f5..69b3a93a05 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -44,7 +44,7 @@ module ActiveSupport yield end - def clear + def clear(options = nil) @data.clear end @@ -79,15 +79,15 @@ module ActiveSupport local_cache_key) end - def clear # :nodoc: + def clear(options = nil) # :nodoc: return super unless cache = local_cache - cache.clear + cache.clear(options) super end def cleanup(options = nil) # :nodoc: return super unless cache = local_cache - cache.clear(options) + cache.clear super end diff --git a/activesupport/lib/active_support/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb index e533c6662e..a05758d6aa 100644 --- a/activesupport/lib/active_support/log_subscriber.rb +++ b/activesupport/lib/active_support/log_subscriber.rb @@ -80,8 +80,10 @@ module ActiveSupport def finish(name, id, payload) super if logger - rescue Exception => e - logger.error "Could not log #{name.inspect} event. #{e.class}: #{e.message} #{e.backtrace}" + rescue => e + if logger + logger.error "Could not log #{name.inspect} event. #{e.class}: #{e.message} #{e.backtrace}" + end end private diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index 726e1464ad..e576766c64 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -19,7 +19,17 @@ module ActiveSupport # encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..." # crypt.decrypt_and_verify(encrypted_data) # => "my secret data" class MessageEncryptor - DEFAULT_CIPHER = "aes-256-cbc" + class << self + attr_accessor :use_authenticated_message_encryption #:nodoc: + + def default_cipher #:nodoc: + if use_authenticated_message_encryption + "aes-256-gcm" + else + "aes-256-cbc" + end + end + end module NullSerializer #:nodoc: def self.load(value) @@ -45,7 +55,7 @@ module ActiveSupport OpenSSLCipherError = OpenSSL::Cipher::CipherError # Initialize a new MessageEncryptor. +secret+ must be at least as long as - # the cipher key size. For the default 'aes-256-cbc' cipher, this is 256 + # the cipher key size. For the default 'aes-256-gcm' cipher, this is 256 # bits. If you are using a user-entered secret, you can generate a suitable # key by using <tt>ActiveSupport::KeyGenerator</tt> or a similar key # derivation function. @@ -57,7 +67,7 @@ module ActiveSupport # # Options: # * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by - # <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-cbc'. + # <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-gcm'. # * <tt>:digest</tt> - String of digest to use for signing. Default is # +SHA1+. Ignored when using an AEAD cipher like 'aes-256-gcm'. # * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+. @@ -66,7 +76,7 @@ module ActiveSupport sign_secret = signature_key_or_options.first @secret = secret @sign_secret = sign_secret - @cipher = options[:cipher] || DEFAULT_CIPHER + @cipher = options[:cipher] || self.class.default_cipher @digest = options[:digest] || "SHA1" unless aead_mode? @verifier = resolve_verifier @serializer = options[:serializer] || Marshal @@ -85,7 +95,7 @@ module ActiveSupport end # Given a cipher, returns the key length of the cipher to help generate the key of desired size - def self.key_len(cipher = DEFAULT_CIPHER) + def self.key_len(cipher = default_cipher) OpenSSL::Cipher.new(cipher).key_len end diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb index af1d5bd615..45bc51311b 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -7,6 +7,13 @@ module ActiveSupport config.eager_load_namespaces << ActiveSupport + initializer "active_support.set_authenticated_message_encryption" do |app| + if app.config.active_support.respond_to?(:use_authenticated_message_encryption) + ActiveSupport::MessageEncryptor.use_authenticated_message_encryption = + app.config.active_support.use_authenticated_message_encryption + end + end + initializer "active_support.reset_all_current_attributes_instances" do |app| app.reloader.before_class_unload { ActiveSupport::CurrentAttributes.clear_all } app.executor.to_run { ActiveSupport::CurrentAttributes.reset_all } diff --git a/activesupport/test/cache/behaviors.rb b/activesupport/test/cache/behaviors.rb new file mode 100644 index 0000000000..efd045ac5e --- /dev/null +++ b/activesupport/test/cache/behaviors.rb @@ -0,0 +1,7 @@ +require_relative "behaviors/autoloading_cache_behavior" +require_relative "behaviors/cache_delete_matched_behavior" +require_relative "behaviors/cache_increment_decrement_behavior" +require_relative "behaviors/cache_store_behavior" +require_relative "behaviors/cache_store_version_behavior" +require_relative "behaviors/encoded_key_cache_behavior" +require_relative "behaviors/local_cache_behavior" diff --git a/activesupport/test/cache/behaviors/autoloading_cache_behavior.rb b/activesupport/test/cache/behaviors/autoloading_cache_behavior.rb new file mode 100644 index 0000000000..5f8af331f6 --- /dev/null +++ b/activesupport/test/cache/behaviors/autoloading_cache_behavior.rb @@ -0,0 +1,41 @@ +require "dependencies_test_helpers" + +module AutoloadingCacheBehavior + include DependenciesTestHelpers + + def test_simple_autoloading + with_autoloading_fixtures do + @cache.write("foo", EM.new) + end + + remove_constants(:EM) + ActiveSupport::Dependencies.clear + + with_autoloading_fixtures do + assert_kind_of EM, @cache.read("foo") + end + + remove_constants(:EM) + ActiveSupport::Dependencies.clear + end + + def test_two_classes_autoloading + with_autoloading_fixtures do + @cache.write("foo", [EM.new, ClassFolder.new]) + end + + remove_constants(:EM, :ClassFolder) + ActiveSupport::Dependencies.clear + + with_autoloading_fixtures do + loaded = @cache.read("foo") + assert_kind_of Array, loaded + assert_equal 2, loaded.size + assert_kind_of EM, loaded[0] + assert_kind_of ClassFolder, loaded[1] + end + + remove_constants(:EM, :ClassFolder) + ActiveSupport::Dependencies.clear + end +end diff --git a/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb b/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb new file mode 100644 index 0000000000..b872eb0279 --- /dev/null +++ b/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb @@ -0,0 +1,13 @@ +module CacheDeleteMatchedBehavior + def test_delete_matched + @cache.write("foo", "bar") + @cache.write("fu", "baz") + @cache.write("foo/bar", "baz") + @cache.write("fu/baz", "bar") + @cache.delete_matched(/oo/) + assert !@cache.exist?("foo") + assert @cache.exist?("fu") + assert !@cache.exist?("foo/bar") + assert @cache.exist?("fu/baz") + end +end diff --git a/activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb b/activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb new file mode 100644 index 0000000000..0d32339565 --- /dev/null +++ b/activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb @@ -0,0 +1,21 @@ +module CacheIncrementDecrementBehavior + def test_increment + @cache.write("foo", 1, raw: true) + assert_equal 1, @cache.read("foo").to_i + assert_equal 2, @cache.increment("foo") + assert_equal 2, @cache.read("foo").to_i + assert_equal 3, @cache.increment("foo") + assert_equal 3, @cache.read("foo").to_i + assert_nil @cache.increment("bar") + end + + def test_decrement + @cache.write("foo", 3, raw: true) + assert_equal 3, @cache.read("foo").to_i + assert_equal 2, @cache.decrement("foo") + assert_equal 2, @cache.read("foo").to_i + assert_equal 1, @cache.decrement("foo") + assert_equal 1, @cache.read("foo").to_i + assert_nil @cache.decrement("bar") + end +end diff --git a/activesupport/test/cache/behaviors/cache_store_behavior.rb b/activesupport/test/cache/behaviors/cache_store_behavior.rb new file mode 100644 index 0000000000..03c366e164 --- /dev/null +++ b/activesupport/test/cache/behaviors/cache_store_behavior.rb @@ -0,0 +1,329 @@ +# Tests the base functionality that should be identical across all cache stores. +module CacheStoreBehavior + def test_should_read_and_write_strings + assert @cache.write("foo", "bar") + assert_equal "bar", @cache.read("foo") + end + + def test_should_overwrite + @cache.write("foo", "bar") + @cache.write("foo", "baz") + assert_equal "baz", @cache.read("foo") + end + + def test_fetch_without_cache_miss + @cache.write("foo", "bar") + assert_not_called(@cache, :write) do + assert_equal "bar", @cache.fetch("foo") { "baz" } + end + end + + def test_fetch_with_cache_miss + assert_called_with(@cache, :write, ["foo", "baz", @cache.options]) do + assert_equal "baz", @cache.fetch("foo") { "baz" } + end + end + + def test_fetch_with_cache_miss_passes_key_to_block + cache_miss = false + assert_equal 3, @cache.fetch("foo") { |key| cache_miss = true; key.length } + assert cache_miss + + cache_miss = false + assert_equal 3, @cache.fetch("foo") { |key| cache_miss = true; key.length } + assert !cache_miss + end + + def test_fetch_with_forced_cache_miss + @cache.write("foo", "bar") + assert_not_called(@cache, :read) do + assert_called_with(@cache, :write, ["foo", "bar", @cache.options.merge(force: true)]) do + @cache.fetch("foo", force: true) { "bar" } + end + end + end + + def test_fetch_with_cached_nil + @cache.write("foo", nil) + assert_not_called(@cache, :write) do + assert_nil @cache.fetch("foo") { "baz" } + end + end + + def test_fetch_with_forced_cache_miss_with_block + @cache.write("foo", "bar") + assert_equal "foo_bar", @cache.fetch("foo", force: true) { "foo_bar" } + end + + def test_fetch_with_forced_cache_miss_without_block + @cache.write("foo", "bar") + assert_raises(ArgumentError) do + @cache.fetch("foo", force: true) + end + + assert_equal "bar", @cache.read("foo") + end + + def test_should_read_and_write_hash + assert @cache.write("foo", a: "b") + assert_equal({ a: "b" }, @cache.read("foo")) + end + + def test_should_read_and_write_integer + assert @cache.write("foo", 1) + assert_equal 1, @cache.read("foo") + end + + def test_should_read_and_write_nil + assert @cache.write("foo", nil) + assert_nil @cache.read("foo") + end + + def test_should_read_and_write_false + assert @cache.write("foo", false) + assert_equal false, @cache.read("foo") + end + + def test_read_multi + @cache.write("foo", "bar") + @cache.write("fu", "baz") + @cache.write("fud", "biz") + assert_equal({ "foo" => "bar", "fu" => "baz" }, @cache.read_multi("foo", "fu")) + end + + def test_read_multi_with_expires + time = Time.now + @cache.write("foo", "bar", expires_in: 10) + @cache.write("fu", "baz") + @cache.write("fud", "biz") + Time.stub(:now, time + 11) do + assert_equal({ "fu" => "baz" }, @cache.read_multi("foo", "fu")) + end + end + + def test_fetch_multi + @cache.write("foo", "bar") + @cache.write("fud", "biz") + + values = @cache.fetch_multi("foo", "fu", "fud") { |value| value * 2 } + + assert_equal({ "foo" => "bar", "fu" => "fufu", "fud" => "biz" }, values) + assert_equal("fufu", @cache.read("fu")) + end + + def test_multi_with_objects + cache_struct = Struct.new(:cache_key, :title) + foo = cache_struct.new("foo", "FOO!") + bar = cache_struct.new("bar") + + @cache.write("bar", "BAM!") + + values = @cache.fetch_multi(foo, bar) { |object| object.title } + + assert_equal({ foo => "FOO!", bar => "BAM!" }, values) + end + + def test_fetch_multi_without_block + assert_raises(ArgumentError) do + @cache.fetch_multi("foo") + end + end + + def test_read_and_write_compressed_small_data + @cache.write("foo", "bar", compress: true) + assert_equal "bar", @cache.read("foo") + end + + def test_read_and_write_compressed_large_data + @cache.write("foo", "bar", compress: true, compress_threshold: 2) + assert_equal "bar", @cache.read("foo") + end + + def test_read_and_write_compressed_nil + @cache.write("foo", nil, compress: true) + assert_nil @cache.read("foo") + end + + def test_cache_key + obj = Object.new + def obj.cache_key + :foo + end + @cache.write(obj, "bar") + assert_equal "bar", @cache.read("foo") + end + + def test_param_as_cache_key + obj = Object.new + def obj.to_param + "foo" + end + @cache.write(obj, "bar") + assert_equal "bar", @cache.read("foo") + end + + def test_array_as_cache_key + @cache.write([:fu, "foo"], "bar") + assert_equal "bar", @cache.read("fu/foo") + end + + def test_hash_as_cache_key + @cache.write({ foo: 1, fu: 2 }, "bar") + assert_equal "bar", @cache.read("foo=1/fu=2") + end + + def test_keys_are_case_sensitive + @cache.write("foo", "bar") + assert_nil @cache.read("FOO") + end + + def test_exist + @cache.write("foo", "bar") + assert_equal true, @cache.exist?("foo") + assert_equal false, @cache.exist?("bar") + end + + def test_nil_exist + @cache.write("foo", nil) + assert @cache.exist?("foo") + end + + def test_delete + @cache.write("foo", "bar") + assert @cache.exist?("foo") + assert @cache.delete("foo") + assert !@cache.exist?("foo") + end + + def test_original_store_objects_should_not_be_immutable + bar = "bar" + @cache.write("foo", bar) + assert_nothing_raised { bar.gsub!(/.*/, "baz") } + end + + def test_expires_in + time = Time.local(2008, 4, 24) + + Time.stub(:now, time) do + @cache.write("foo", "bar") + assert_equal "bar", @cache.read("foo") + end + + Time.stub(:now, time + 30) do + assert_equal "bar", @cache.read("foo") + end + + Time.stub(:now, time + 61) do + assert_nil @cache.read("foo") + end + end + + def test_race_condition_protection_skipped_if_not_defined + @cache.write("foo", "bar") + time = @cache.send(:read_entry, @cache.send(:normalize_key, "foo", {}), {}).expires_at + + Time.stub(:now, Time.at(time)) do + result = @cache.fetch("foo") do + assert_nil @cache.read("foo") + "baz" + end + assert_equal "baz", result + end + end + + def test_race_condition_protection_is_limited + time = Time.now + @cache.write("foo", "bar", expires_in: 60) + Time.stub(:now, time + 71) do + result = @cache.fetch("foo", race_condition_ttl: 10) do + assert_nil @cache.read("foo") + "baz" + end + assert_equal "baz", result + end + end + + def test_race_condition_protection_is_safe + time = Time.now + @cache.write("foo", "bar", expires_in: 60) + Time.stub(:now, time + 61) do + begin + @cache.fetch("foo", race_condition_ttl: 10) do + assert_equal "bar", @cache.read("foo") + raise ArgumentError.new + end + rescue ArgumentError + end + assert_equal "bar", @cache.read("foo") + end + Time.stub(:now, time + 91) do + assert_nil @cache.read("foo") + end + end + + def test_race_condition_protection + time = Time.now + @cache.write("foo", "bar", expires_in: 60) + Time.stub(:now, time + 61) do + result = @cache.fetch("foo", race_condition_ttl: 10) do + assert_equal "bar", @cache.read("foo") + "baz" + end + assert_equal "baz", result + end + end + + def test_crazy_key_characters + crazy_key = "#/:*(<+=> )&$%@?;'\"\'`~-" + assert @cache.write(crazy_key, "1", raw: true) + assert_equal "1", @cache.read(crazy_key) + assert_equal "1", @cache.fetch(crazy_key) + assert @cache.delete(crazy_key) + assert_equal "2", @cache.fetch(crazy_key, raw: true) { "2" } + assert_equal 3, @cache.increment(crazy_key) + assert_equal 2, @cache.decrement(crazy_key) + end + + def test_really_long_keys + key = "" + 900.times { key << "x" } + assert @cache.write(key, "bar") + assert_equal "bar", @cache.read(key) + assert_equal "bar", @cache.fetch(key) + assert_nil @cache.read("#{key}x") + assert_equal({ key => "bar" }, @cache.read_multi(key)) + assert @cache.delete(key) + end + + def test_cache_hit_instrumentation + key = "test_key" + @events = [] + ActiveSupport::Notifications.subscribe "cache_read.active_support" do |*args| + @events << ActiveSupport::Notifications::Event.new(*args) + end + assert @cache.write(key, "1", raw: true) + assert @cache.fetch(key) {} + assert_equal 1, @events.length + assert_equal "cache_read.active_support", @events[0].name + assert_equal :fetch, @events[0].payload[:super_operation] + assert @events[0].payload[:hit] + ensure + ActiveSupport::Notifications.unsubscribe "cache_read.active_support" + end + + def test_cache_miss_instrumentation + @events = [] + ActiveSupport::Notifications.subscribe(/^cache_(.*)\.active_support$/) do |*args| + @events << ActiveSupport::Notifications::Event.new(*args) + end + assert_not @cache.fetch("bad_key") {} + assert_equal 3, @events.length + assert_equal "cache_read.active_support", @events[0].name + assert_equal "cache_generate.active_support", @events[1].name + assert_equal "cache_write.active_support", @events[2].name + assert_equal :fetch, @events[0].payload[:super_operation] + assert_not @events[0].payload[:hit] + ensure + ActiveSupport::Notifications.unsubscribe "cache_read.active_support" + end +end diff --git a/activesupport/test/cache/behaviors/cache_store_version_behavior.rb b/activesupport/test/cache/behaviors/cache_store_version_behavior.rb new file mode 100644 index 0000000000..a0170c896f --- /dev/null +++ b/activesupport/test/cache/behaviors/cache_store_version_behavior.rb @@ -0,0 +1,86 @@ +module CacheStoreVersionBehavior + ModelWithKeyAndVersion = Struct.new(:cache_key, :cache_version) + + def test_fetch_with_right_version_should_hit + @cache.fetch("foo", version: 1) { "bar" } + assert_equal "bar", @cache.read("foo", version: 1) + end + + def test_fetch_with_wrong_version_should_miss + @cache.fetch("foo", version: 1) { "bar" } + assert_nil @cache.read("foo", version: 2) + end + + def test_read_with_right_version_should_hit + @cache.write("foo", "bar", version: 1) + assert_equal "bar", @cache.read("foo", version: 1) + end + + def test_read_with_wrong_version_should_miss + @cache.write("foo", "bar", version: 1) + assert_nil @cache.read("foo", version: 2) + end + + def test_exist_with_right_version_should_be_true + @cache.write("foo", "bar", version: 1) + assert @cache.exist?("foo", version: 1) + end + + def test_exist_with_wrong_version_should_be_false + @cache.write("foo", "bar", version: 1) + assert !@cache.exist?("foo", version: 2) + end + + def test_reading_and_writing_with_model_supporting_cache_version + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m1v2 = ModelWithKeyAndVersion.new("model/1", 2) + + @cache.write(m1v1, "bar") + assert_equal "bar", @cache.read(m1v1) + assert_nil @cache.read(m1v2) + end + + def test_reading_and_writing_with_model_supporting_cache_version_using_nested_key + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m1v2 = ModelWithKeyAndVersion.new("model/1", 2) + + @cache.write([ "something", m1v1 ], "bar") + assert_equal "bar", @cache.read([ "something", m1v1 ]) + assert_nil @cache.read([ "something", m1v2 ]) + end + + def test_fetching_with_model_supporting_cache_version + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m1v2 = ModelWithKeyAndVersion.new("model/1", 2) + + @cache.fetch(m1v1) { "bar" } + assert_equal "bar", @cache.fetch(m1v1) { "bu" } + assert_equal "bu", @cache.fetch(m1v2) { "bu" } + end + + def test_exist_with_model_supporting_cache_version + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m1v2 = ModelWithKeyAndVersion.new("model/1", 2) + + @cache.write(m1v1, "bar") + assert @cache.exist?(m1v1) + assert_not @cache.fetch(m1v2) + end + + def test_fetch_multi_with_model_supporting_cache_version + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m2v1 = ModelWithKeyAndVersion.new("model/2", 1) + m2v2 = ModelWithKeyAndVersion.new("model/2", 2) + + first_fetch_values = @cache.fetch_multi(m1v1, m2v1) { |m| m.cache_key } + second_fetch_values = @cache.fetch_multi(m1v1, m2v2) { |m| m.cache_key + " 2nd" } + + assert_equal({ m1v1 => "model/1", m2v1 => "model/2" }, first_fetch_values) + assert_equal({ m1v1 => "model/1", m2v2 => "model/2 2nd" }, second_fetch_values) + end + + def test_version_is_normalized + @cache.write("foo", "bar", version: 1) + assert_equal "bar", @cache.read("foo", version: "1") + end +end diff --git a/activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb b/activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb new file mode 100644 index 0000000000..4d8e2946b2 --- /dev/null +++ b/activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb @@ -0,0 +1,34 @@ +# https://rails.lighthouseapp.com/projects/8994/tickets/6225-memcachestore-cant-deal-with-umlauts-and-special-characters +# The error is caused by character encodings that can't be compared with ASCII-8BIT regular expressions and by special +# characters like the umlaut in UTF-8. +module EncodedKeyCacheBehavior + Encoding.list.each do |encoding| + define_method "test_#{encoding.name.underscore}_encoded_values" do + key = "foo".force_encoding(encoding) + assert @cache.write(key, "1", raw: true) + assert_equal "1", @cache.read(key) + assert_equal "1", @cache.fetch(key) + assert @cache.delete(key) + assert_equal "2", @cache.fetch(key, raw: true) { "2" } + assert_equal 3, @cache.increment(key) + assert_equal 2, @cache.decrement(key) + end + end + + def test_common_utf8_values + key = "\xC3\xBCmlaut".force_encoding(Encoding::UTF_8) + assert @cache.write(key, "1", raw: true) + assert_equal "1", @cache.read(key) + assert_equal "1", @cache.fetch(key) + assert @cache.delete(key) + assert_equal "2", @cache.fetch(key, raw: true) { "2" } + assert_equal 3, @cache.increment(key) + assert_equal 2, @cache.decrement(key) + end + + def test_retains_encoding + key = "\xC3\xBCmlaut".force_encoding(Encoding::UTF_8) + assert @cache.write(key, "1", raw: true) + assert_equal Encoding::UTF_8, key.encoding + end +end diff --git a/activesupport/test/cache/behaviors/local_cache_behavior.rb b/activesupport/test/cache/behaviors/local_cache_behavior.rb new file mode 100644 index 0000000000..8530296374 --- /dev/null +++ b/activesupport/test/cache/behaviors/local_cache_behavior.rb @@ -0,0 +1,126 @@ +module LocalCacheBehavior + def test_local_writes_are_persistent_on_the_remote_cache + retval = @cache.with_local_cache do + @cache.write("foo", "bar") + end + assert retval + assert_equal "bar", @cache.read("foo") + end + + def test_clear_also_clears_local_cache + @cache.with_local_cache do + @cache.write("foo", "bar") + @cache.clear + assert_nil @cache.read("foo") + end + + assert_nil @cache.read("foo") + end + + def test_cleanup_clears_local_cache_but_not_remote_cache + skip unless @cache.class.instance_methods(false).include?(:cleanup) + + @cache.with_local_cache do + @cache.write("foo", "bar") + assert_equal "bar", @cache.read("foo") + + @cache.send(:bypass_local_cache) { @cache.write("foo", "baz") } + assert_equal "bar", @cache.read("foo") + + @cache.cleanup + assert_equal "baz", @cache.read("foo") + end + end + + def test_local_cache_of_write + @cache.with_local_cache do + @cache.write("foo", "bar") + @peek.delete("foo") + assert_equal "bar", @cache.read("foo") + end + end + + def test_local_cache_of_read + @cache.write("foo", "bar") + @cache.with_local_cache do + assert_equal "bar", @cache.read("foo") + end + end + + def test_local_cache_of_read_nil + @cache.with_local_cache do + assert_nil @cache.read("foo") + @cache.send(:bypass_local_cache) { @cache.write "foo", "bar" } + assert_nil @cache.read("foo") + end + end + + def test_local_cache_fetch + @cache.with_local_cache do + @cache.send(:local_cache).write "foo", "bar" + assert_equal "bar", @cache.send(:local_cache).fetch("foo") + end + end + + def test_local_cache_of_write_nil + @cache.with_local_cache do + assert @cache.write("foo", nil) + assert_nil @cache.read("foo") + @peek.write("foo", "bar") + assert_nil @cache.read("foo") + end + end + + def test_local_cache_of_write_with_unless_exist + @cache.with_local_cache do + @cache.write("foo", "bar") + @cache.write("foo", "baz", unless_exist: true) + assert_equal @peek.read("foo"), @cache.read("foo") + end + end + + def test_local_cache_of_delete + @cache.with_local_cache do + @cache.write("foo", "bar") + @cache.delete("foo") + assert_nil @cache.read("foo") + end + end + + def test_local_cache_of_exist + @cache.with_local_cache do + @cache.write("foo", "bar") + @peek.delete("foo") + assert @cache.exist?("foo") + end + end + + def test_local_cache_of_increment + @cache.with_local_cache do + @cache.write("foo", 1, raw: true) + @peek.write("foo", 2, raw: true) + @cache.increment("foo") + assert_equal 3, @cache.read("foo") + end + end + + def test_local_cache_of_decrement + @cache.with_local_cache do + @cache.write("foo", 1, raw: true) + @peek.write("foo", 3, raw: true) + @cache.decrement("foo") + assert_equal 2, @cache.read("foo") + end + end + + def test_middleware + app = lambda { |env| + result = @cache.write("foo", "bar") + assert_equal "bar", @cache.read("foo") # make sure 'foo' was written + assert result + [200, {}, []] + } + app = @cache.middleware.new(app) + app.call({}) + end +end diff --git a/activesupport/test/cache/cache_entry_test.rb b/activesupport/test/cache/cache_entry_test.rb new file mode 100644 index 0000000000..e446e39b10 --- /dev/null +++ b/activesupport/test/cache/cache_entry_test.rb @@ -0,0 +1,28 @@ +require "abstract_unit" +require "active_support/cache" + +class CacheEntryTest < ActiveSupport::TestCase + def test_expired + entry = ActiveSupport::Cache::Entry.new("value") + assert !entry.expired?, "entry not expired" + entry = ActiveSupport::Cache::Entry.new("value", expires_in: 60) + assert !entry.expired?, "entry not expired" + Time.stub(:now, Time.now + 61) do + assert entry.expired?, "entry is expired" + end + end + + def test_compress_values + value = "value" * 100 + entry = ActiveSupport::Cache::Entry.new(value, compress: true, compress_threshold: 1) + assert_equal value, entry.value + assert(value.bytesize > entry.size, "value is compressed") + end + + def test_non_compress_values + value = "value" * 100 + entry = ActiveSupport::Cache::Entry.new(value) + assert_equal value, entry.value + assert_equal value.bytesize, entry.size + end +end diff --git a/activesupport/test/cache/cache_key_test.rb b/activesupport/test/cache/cache_key_test.rb new file mode 100644 index 0000000000..149d0f66ee --- /dev/null +++ b/activesupport/test/cache/cache_key_test.rb @@ -0,0 +1,88 @@ +require "abstract_unit" +require "active_support/cache" + +class CacheKeyTest < ActiveSupport::TestCase + def test_entry_legacy_optional_ivars + legacy = Class.new(ActiveSupport::Cache::Entry) do + def initialize(value, options = {}) + @value = value + @expires_in = nil + @created_at = nil + super + end + end + + entry = legacy.new "foo" + assert_equal "foo", entry.value + end + + def test_expand_cache_key + assert_equal "1/2/true", ActiveSupport::Cache.expand_cache_key([1, "2", true]) + assert_equal "name/1/2/true", ActiveSupport::Cache.expand_cache_key([1, "2", true], :name) + end + + def test_expand_cache_key_with_rails_cache_id + with_env("RAILS_CACHE_ID" => "c99") do + assert_equal "c99/foo", ActiveSupport::Cache.expand_cache_key(:foo) + assert_equal "c99/foo", ActiveSupport::Cache.expand_cache_key([:foo]) + assert_equal "c99/foo/bar", ActiveSupport::Cache.expand_cache_key([:foo, :bar]) + assert_equal "nm/c99/foo", ActiveSupport::Cache.expand_cache_key(:foo, :nm) + assert_equal "nm/c99/foo", ActiveSupport::Cache.expand_cache_key([:foo], :nm) + assert_equal "nm/c99/foo/bar", ActiveSupport::Cache.expand_cache_key([:foo, :bar], :nm) + end + end + + def test_expand_cache_key_with_rails_app_version + with_env("RAILS_APP_VERSION" => "rails3") do + assert_equal "rails3/foo", ActiveSupport::Cache.expand_cache_key(:foo) + end + end + + def test_expand_cache_key_rails_cache_id_should_win_over_rails_app_version + with_env("RAILS_CACHE_ID" => "c99", "RAILS_APP_VERSION" => "rails3") do + assert_equal "c99/foo", ActiveSupport::Cache.expand_cache_key(:foo) + end + end + + def test_expand_cache_key_respond_to_cache_key + key = "foo" + def key.cache_key + :foo_key + end + assert_equal "foo_key", ActiveSupport::Cache.expand_cache_key(key) + end + + def test_expand_cache_key_array_with_something_that_responds_to_cache_key + key = "foo" + def key.cache_key + :foo_key + end + assert_equal "foo_key", ActiveSupport::Cache.expand_cache_key([key]) + end + + def test_expand_cache_key_of_nil + assert_equal "", ActiveSupport::Cache.expand_cache_key(nil) + end + + def test_expand_cache_key_of_false + assert_equal "false", ActiveSupport::Cache.expand_cache_key(false) + end + + def test_expand_cache_key_of_true + assert_equal "true", ActiveSupport::Cache.expand_cache_key(true) + end + + def test_expand_cache_key_of_array_like_object + assert_equal "foo/bar/baz", ActiveSupport::Cache.expand_cache_key(%w{foo bar baz}.to_enum) + end + + private + + def with_env(kv) + old_values = {} + kv.each { |key, value| old_values[key], ENV[key] = ENV[key], value } + yield + ensure + old_values.each { |key, value| ENV[key] = value } + end +end diff --git a/activesupport/test/cache/cache_store_logger_test.rb b/activesupport/test/cache/cache_store_logger_test.rb new file mode 100644 index 0000000000..621cfebb10 --- /dev/null +++ b/activesupport/test/cache/cache_store_logger_test.rb @@ -0,0 +1,34 @@ +require "abstract_unit" +require "active_support/cache" + +class CacheStoreLoggerTest < ActiveSupport::TestCase + def setup + @cache = ActiveSupport::Cache.lookup_store(:memory_store) + + @buffer = StringIO.new + @cache.logger = ActiveSupport::Logger.new(@buffer) + end + + def test_logging + @cache.fetch("foo") { "bar" } + assert @buffer.string.present? + end + + def test_log_with_string_namespace + @cache.fetch("foo", namespace: "string_namespace") { "bar" } + assert_match %r{string_namespace:foo}, @buffer.string + end + + def test_log_with_proc_namespace + proc = Proc.new do + "proc_namespace" + end + @cache.fetch("foo", namespace: proc) { "bar" } + assert_match %r{proc_namespace:foo}, @buffer.string + end + + def test_mute_logging + @cache.mute { @cache.fetch("foo") { "bar" } } + assert @buffer.string.blank? + end +end diff --git a/activesupport/test/cache/cache_store_namespace_test.rb b/activesupport/test/cache/cache_store_namespace_test.rb new file mode 100644 index 0000000000..e395c88271 --- /dev/null +++ b/activesupport/test/cache/cache_store_namespace_test.rb @@ -0,0 +1,38 @@ +require "abstract_unit" +require "active_support/cache" + +class CacheStoreNamespaceTest < ActiveSupport::TestCase + def test_static_namespace + cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: "tester") + cache.write("foo", "bar") + assert_equal "bar", cache.read("foo") + assert_equal "bar", cache.instance_variable_get(:@data)["tester:foo"].value + end + + def test_proc_namespace + test_val = "tester" + proc = lambda { test_val } + cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: proc) + cache.write("foo", "bar") + assert_equal "bar", cache.read("foo") + assert_equal "bar", cache.instance_variable_get(:@data)["tester:foo"].value + end + + def test_delete_matched_key_start + cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: "tester") + cache.write("foo", "bar") + cache.write("fu", "baz") + cache.delete_matched(/^fo/) + assert !cache.exist?("foo") + assert cache.exist?("fu") + end + + def test_delete_matched_key + cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: "foo") + cache.write("foo", "bar") + cache.write("fu", "baz") + cache.delete_matched(/OO/i) + assert !cache.exist?("foo") + assert cache.exist?("fu") + end +end diff --git a/activesupport/test/cache/cache_store_setting_test.rb b/activesupport/test/cache/cache_store_setting_test.rb new file mode 100644 index 0000000000..cb9b006abe --- /dev/null +++ b/activesupport/test/cache/cache_store_setting_test.rb @@ -0,0 +1,66 @@ +require "abstract_unit" +require "active_support/cache" +require "dalli" + +class CacheStoreSettingTest < ActiveSupport::TestCase + def test_memory_store_gets_created_if_no_arguments_passed_to_lookup_store_method + store = ActiveSupport::Cache.lookup_store + assert_kind_of(ActiveSupport::Cache::MemoryStore, store) + end + + def test_memory_store + store = ActiveSupport::Cache.lookup_store :memory_store + assert_kind_of(ActiveSupport::Cache::MemoryStore, store) + end + + def test_file_fragment_cache_store + store = ActiveSupport::Cache.lookup_store :file_store, "/path/to/cache/directory" + assert_kind_of(ActiveSupport::Cache::FileStore, store) + assert_equal "/path/to/cache/directory", store.cache_path + end + + def test_mem_cache_fragment_cache_store + assert_called_with(Dalli::Client, :new, [%w[localhost], {}]) do + store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost" + assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + end + end + + def test_mem_cache_fragment_cache_store_with_given_mem_cache + mem_cache = Dalli::Client.new + assert_not_called(Dalli::Client, :new) do + store = ActiveSupport::Cache.lookup_store :mem_cache_store, mem_cache + assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + end + end + + def test_mem_cache_fragment_cache_store_with_not_dalli_client + assert_not_called(Dalli::Client, :new) do + memcache = Object.new + assert_raises(ArgumentError) do + ActiveSupport::Cache.lookup_store :mem_cache_store, memcache + end + end + end + + def test_mem_cache_fragment_cache_store_with_multiple_servers + assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], {}]) do + store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", "192.168.1.1" + assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + end + end + + def test_mem_cache_fragment_cache_store_with_options + assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], { timeout: 10 }]) do + store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", "192.168.1.1", namespace: "foo", timeout: 10 + assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + assert_equal "foo", store.options[:namespace] + end + end + + def test_object_assigned_fragment_cache_store + store = ActiveSupport::Cache.lookup_store ActiveSupport::Cache::FileStore.new("/path/to/cache/directory") + assert_kind_of(ActiveSupport::Cache::FileStore, store) + assert_equal "/path/to/cache/directory", store.cache_path + end +end diff --git a/activesupport/test/cache/cache_store_write_multi_test.rb b/activesupport/test/cache/cache_store_write_multi_test.rb new file mode 100644 index 0000000000..16e3f3b842 --- /dev/null +++ b/activesupport/test/cache/cache_store_write_multi_test.rb @@ -0,0 +1,60 @@ +require "abstract_unit" +require "active_support/cache" + +class CacheStoreWriteMultiEntriesStoreProviderInterfaceTest < ActiveSupport::TestCase + setup do + @cache = ActiveSupport::Cache.lookup_store(:null_store) + end + + test "fetch_multi uses write_multi_entries store provider interface" do + assert_called_with(@cache, :write_multi_entries) do + @cache.fetch_multi "a", "b", "c" do |key| + key * 2 + end + end + end +end + +class CacheStoreWriteMultiInstrumentationTest < ActiveSupport::TestCase + setup do + @cache = ActiveSupport::Cache.lookup_store(:null_store) + end + + test "instrumentation" do + writes = { "a" => "aa", "b" => "bb" } + + events = with_instrumentation "write_multi" do + @cache.write_multi(writes) + end + + assert_equal %w[ cache_write_multi.active_support ], events.map(&:name) + assert_nil events[0].payload[:super_operation] + assert_equal({ "a" => "aa", "b" => "bb" }, events[0].payload[:key]) + end + + test "instrumentation with fetch_multi as super operation" do + skip "fetch_multi isn't instrumented yet" + + events = with_instrumentation "write_multi" do + @cache.fetch_multi("a", "b") { |key| key * 2 } + end + + assert_equal %w[ cache_write_multi.active_support ], events.map(&:name) + assert_nil events[0].payload[:super_operation] + assert !events[0].payload[:hit] + end + + private + def with_instrumentation(method) + event_name = "cache_#{method}.active_support" + + [].tap do |events| + ActiveSupport::Notifications.subscribe event_name do |*args| + events << ActiveSupport::Notifications::Event.new(*args) + end + yield + end + ensure + ActiveSupport::Notifications.unsubscribe event_name + end +end diff --git a/activesupport/test/cache/local_cache_middleware_test.rb b/activesupport/test/cache/local_cache_middleware_test.rb new file mode 100644 index 0000000000..352502fb43 --- /dev/null +++ b/activesupport/test/cache/local_cache_middleware_test.rb @@ -0,0 +1,61 @@ +require "abstract_unit" +require "active_support/cache" + +module ActiveSupport + module Cache + module Strategy + module LocalCache + class MiddlewareTest < ActiveSupport::TestCase + def test_local_cache_cleared_on_close + key = "super awesome key" + assert_nil LocalCacheRegistry.cache_for key + middleware = Middleware.new("<3", key).new(->(env) { + assert LocalCacheRegistry.cache_for(key), "should have a cache" + [200, {}, []] + }) + _, _, body = middleware.call({}) + assert LocalCacheRegistry.cache_for(key), "should still have a cache" + body.each {} + assert LocalCacheRegistry.cache_for(key), "should still have a cache" + body.close + assert_nil LocalCacheRegistry.cache_for(key) + end + + def test_local_cache_cleared_and_response_should_be_present_on_invalid_parameters_error + key = "super awesome key" + assert_nil LocalCacheRegistry.cache_for key + middleware = Middleware.new("<3", key).new(->(env) { + assert LocalCacheRegistry.cache_for(key), "should have a cache" + raise Rack::Utils::InvalidParameterError + }) + response = middleware.call({}) + assert response, "response should exist" + assert_nil LocalCacheRegistry.cache_for(key) + end + + def test_local_cache_cleared_on_exception + key = "super awesome key" + assert_nil LocalCacheRegistry.cache_for key + middleware = Middleware.new("<3", key).new(->(env) { + assert LocalCacheRegistry.cache_for(key), "should have a cache" + raise + }) + assert_raises(RuntimeError) { middleware.call({}) } + assert_nil LocalCacheRegistry.cache_for(key) + end + + def test_local_cache_cleared_on_throw + key = "super awesome key" + assert_nil LocalCacheRegistry.cache_for key + middleware = Middleware.new("<3", key).new(->(env) { + assert LocalCacheRegistry.cache_for(key), "should have a cache" + throw :warden + }) + assert_throws(:warden) { middleware.call({}) } + assert_nil LocalCacheRegistry.cache_for(key) + end + end + end + end + end +end diff --git a/activesupport/test/cache/stores/file_store_test.rb b/activesupport/test/cache/stores/file_store_test.rb new file mode 100644 index 0000000000..48b304fe6e --- /dev/null +++ b/activesupport/test/cache/stores/file_store_test.rb @@ -0,0 +1,128 @@ +require "abstract_unit" +require "active_support/cache" +require_relative "../behaviors" +require "pathname" + +class FileStoreTest < ActiveSupport::TestCase + def setup + Dir.mkdir(cache_dir) unless File.exist?(cache_dir) + @cache = ActiveSupport::Cache.lookup_store(:file_store, cache_dir, expires_in: 60) + @peek = ActiveSupport::Cache.lookup_store(:file_store, cache_dir, expires_in: 60) + @cache_with_pathname = ActiveSupport::Cache.lookup_store(:file_store, Pathname.new(cache_dir), expires_in: 60) + + @buffer = StringIO.new + @cache.logger = ActiveSupport::Logger.new(@buffer) + end + + def teardown + FileUtils.rm_r(cache_dir) + rescue Errno::ENOENT + end + + def cache_dir + File.join(Dir.pwd, "tmp_cache") + end + + include CacheStoreBehavior + include CacheStoreVersionBehavior + include LocalCacheBehavior + include CacheDeleteMatchedBehavior + include CacheIncrementDecrementBehavior + include AutoloadingCacheBehavior + + def test_clear + gitkeep = File.join(cache_dir, ".gitkeep") + keep = File.join(cache_dir, ".keep") + FileUtils.touch([gitkeep, keep]) + @cache.clear + assert File.exist?(gitkeep) + assert File.exist?(keep) + end + + def test_clear_without_cache_dir + FileUtils.rm_r(cache_dir) + @cache.clear + end + + def test_long_uri_encoded_keys + @cache.write("%" * 870, 1) + assert_equal 1, @cache.read("%" * 870) + end + + def test_key_transformation + key = @cache.send(:normalize_key, "views/index?id=1", {}) + assert_equal "views/index?id=1", @cache.send(:file_path_key, key) + end + + def test_key_transformation_with_pathname + FileUtils.touch(File.join(cache_dir, "foo")) + key = @cache_with_pathname.send(:normalize_key, "views/index?id=1", {}) + assert_equal "views/index?id=1", @cache_with_pathname.send(:file_path_key, key) + end + + # Test that generated cache keys are short enough to have Tempfile stuff added to them and + # remain valid + def test_filename_max_size + key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}" + path = @cache.send(:normalize_key, key, {}) + Dir::Tmpname.create(path) do |tmpname, n, opts| + assert File.basename(tmpname + ".lock").length <= 255, "Temp filename too long: #{File.basename(tmpname + '.lock').length}" + end + end + + # Because file systems have a maximum filename size, filenames > max size should be split in to directories + # If filename is 'AAAAB', where max size is 4, the returned path should be AAAA/B + def test_key_transformation_max_filename_size + key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}B" + path = @cache.send(:normalize_key, key, {}) + assert path.split("/").all? { |dir_name| dir_name.size <= ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE } + assert_equal "B", File.basename(path) + end + + # If nothing has been stored in the cache, there is a chance the cache directory does not yet exist + # Ensure delete_matched gracefully handles this case + def test_delete_matched_when_cache_directory_does_not_exist + assert_nothing_raised do + ActiveSupport::Cache::FileStore.new("/test/cache/directory").delete_matched(/does_not_exist/) + end + end + + def test_delete_does_not_delete_empty_parent_dir + sub_cache_dir = File.join(cache_dir, "subdir/") + sub_cache_store = ActiveSupport::Cache::FileStore.new(sub_cache_dir) + assert_nothing_raised do + assert sub_cache_store.write("foo", "bar") + assert sub_cache_store.delete("foo") + end + assert File.exist?(cache_dir), "Parent of top level cache dir was deleted!" + assert File.exist?(sub_cache_dir), "Top level cache dir was deleted!" + assert Dir.entries(sub_cache_dir).reject { |f| ActiveSupport::Cache::FileStore::EXCLUDED_DIRS.include?(f) }.empty? + end + + def test_log_exception_when_cache_read_fails + File.stub(:exist?, -> { raise StandardError.new("failed") }) do + @cache.send(:read_entry, "winston", {}) + assert @buffer.string.present? + end + end + + def test_cleanup_removes_all_expired_entries + time = Time.now + @cache.write("foo", "bar", expires_in: 10) + @cache.write("baz", "qux") + @cache.write("quux", "corge", expires_in: 20) + Time.stub(:now, time + 15) do + @cache.cleanup + assert_not @cache.exist?("foo") + assert @cache.exist?("baz") + assert @cache.exist?("quux") + end + end + + def test_write_with_unless_exist + assert_equal true, @cache.write(1, "aaaaaaaaaa") + assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) + @cache.write(1, nil) + assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) + end +end diff --git a/activesupport/test/cache/stores/mem_cache_store_test.rb b/activesupport/test/cache/stores/mem_cache_store_test.rb new file mode 100644 index 0000000000..2dd5264818 --- /dev/null +++ b/activesupport/test/cache/stores/mem_cache_store_test.rb @@ -0,0 +1,74 @@ +require "abstract_unit" +require "active_support/cache" +require_relative "../behaviors" +require "dalli" + +class MemCacheStoreTest < ActiveSupport::TestCase + begin + ss = Dalli::Client.new("localhost:11211").stats + raise Dalli::DalliError unless ss["localhost:11211"] + + MEMCACHE_UP = true + rescue Dalli::DalliError + $stderr.puts "Skipping memcached tests. Start memcached and try again." + MEMCACHE_UP = false + end + + def setup + skip "memcache server is not up" unless MEMCACHE_UP + + @cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, expires_in: 60) + @peek = ActiveSupport::Cache.lookup_store(:mem_cache_store) + @data = @cache.instance_variable_get(:@data) + @cache.clear + @cache.silence! + @cache.logger = ActiveSupport::Logger.new("/dev/null") + end + + include CacheStoreBehavior + include CacheStoreVersionBehavior + include LocalCacheBehavior + include CacheIncrementDecrementBehavior + include EncodedKeyCacheBehavior + include AutoloadingCacheBehavior + + def test_raw_values + cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) + cache.clear + cache.write("foo", 2) + assert_equal "2", cache.read("foo") + end + + def test_raw_values_with_marshal + cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) + cache.clear + cache.write("foo", Marshal.dump([])) + assert_equal [], cache.read("foo") + end + + def test_local_cache_raw_values + cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) + cache.clear + cache.with_local_cache do + cache.write("foo", 2) + assert_equal "2", cache.read("foo") + end + end + + def test_local_cache_raw_values_with_marshal + cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) + cache.clear + cache.with_local_cache do + cache.write("foo", Marshal.dump([])) + assert_equal [], cache.read("foo") + end + end + + def test_read_should_return_a_different_object_id_each_time_it_is_called + @cache.write("foo", "bar") + value = @cache.read("foo") + assert_not_equal value.object_id, @cache.read("foo").object_id + value << "bingo" + assert_not_equal value, @cache.read("foo") + end +end diff --git a/activesupport/test/cache/stores/memory_store_test.rb b/activesupport/test/cache/stores/memory_store_test.rb new file mode 100644 index 0000000000..3dd1646d56 --- /dev/null +++ b/activesupport/test/cache/stores/memory_store_test.rb @@ -0,0 +1,107 @@ +require "abstract_unit" +require "active_support/cache" +require_relative "../behaviors" + +class MemoryStoreTest < ActiveSupport::TestCase + def setup + @record_size = ActiveSupport::Cache.lookup_store(:memory_store).send(:cached_size, 1, ActiveSupport::Cache::Entry.new("aaaaaaaaaa")) + @cache = ActiveSupport::Cache.lookup_store(:memory_store, expires_in: 60, size: @record_size * 10 + 1) + end + + include CacheStoreBehavior + include CacheStoreVersionBehavior + include CacheDeleteMatchedBehavior + include CacheIncrementDecrementBehavior + + def test_prune_size + @cache.write(1, "aaaaaaaaaa") && sleep(0.001) + @cache.write(2, "bbbbbbbbbb") && sleep(0.001) + @cache.write(3, "cccccccccc") && sleep(0.001) + @cache.write(4, "dddddddddd") && sleep(0.001) + @cache.write(5, "eeeeeeeeee") && sleep(0.001) + @cache.read(2) && sleep(0.001) + @cache.read(4) + @cache.prune(@record_size * 3) + assert @cache.exist?(5) + assert @cache.exist?(4) + assert !@cache.exist?(3), "no entry" + assert @cache.exist?(2) + assert !@cache.exist?(1), "no entry" + end + + def test_prune_size_on_write + @cache.write(1, "aaaaaaaaaa") && sleep(0.001) + @cache.write(2, "bbbbbbbbbb") && sleep(0.001) + @cache.write(3, "cccccccccc") && sleep(0.001) + @cache.write(4, "dddddddddd") && sleep(0.001) + @cache.write(5, "eeeeeeeeee") && sleep(0.001) + @cache.write(6, "ffffffffff") && sleep(0.001) + @cache.write(7, "gggggggggg") && sleep(0.001) + @cache.write(8, "hhhhhhhhhh") && sleep(0.001) + @cache.write(9, "iiiiiiiiii") && sleep(0.001) + @cache.write(10, "kkkkkkkkkk") && sleep(0.001) + @cache.read(2) && sleep(0.001) + @cache.read(4) && sleep(0.001) + @cache.write(11, "llllllllll") + assert @cache.exist?(11) + assert @cache.exist?(10) + assert @cache.exist?(9) + assert @cache.exist?(8) + assert @cache.exist?(7) + assert !@cache.exist?(6), "no entry" + assert !@cache.exist?(5), "no entry" + assert @cache.exist?(4) + assert !@cache.exist?(3), "no entry" + assert @cache.exist?(2) + assert !@cache.exist?(1), "no entry" + end + + def test_prune_size_on_write_based_on_key_length + @cache.write(1, "aaaaaaaaaa") && sleep(0.001) + @cache.write(2, "bbbbbbbbbb") && sleep(0.001) + @cache.write(3, "cccccccccc") && sleep(0.001) + @cache.write(4, "dddddddddd") && sleep(0.001) + @cache.write(5, "eeeeeeeeee") && sleep(0.001) + @cache.write(6, "ffffffffff") && sleep(0.001) + @cache.write(7, "gggggggggg") && sleep(0.001) + @cache.write(8, "hhhhhhhhhh") && sleep(0.001) + @cache.write(9, "iiiiiiiiii") && sleep(0.001) + long_key = "*" * 2 * @record_size + @cache.write(long_key, "llllllllll") + assert @cache.exist?(long_key) + assert @cache.exist?(9) + assert @cache.exist?(8) + assert @cache.exist?(7) + assert @cache.exist?(6) + assert !@cache.exist?(5), "no entry" + assert !@cache.exist?(4), "no entry" + assert !@cache.exist?(3), "no entry" + assert !@cache.exist?(2), "no entry" + assert !@cache.exist?(1), "no entry" + end + + def test_pruning_is_capped_at_a_max_time + def @cache.delete_entry(*args) + sleep(0.01) + super + end + @cache.write(1, "aaaaaaaaaa") && sleep(0.001) + @cache.write(2, "bbbbbbbbbb") && sleep(0.001) + @cache.write(3, "cccccccccc") && sleep(0.001) + @cache.write(4, "dddddddddd") && sleep(0.001) + @cache.write(5, "eeeeeeeeee") && sleep(0.001) + @cache.prune(30, 0.001) + assert @cache.exist?(5) + assert @cache.exist?(4) + assert @cache.exist?(3) + assert @cache.exist?(2) + assert !@cache.exist?(1) + end + + def test_write_with_unless_exist + assert_equal true, @cache.write(1, "aaaaaaaaaa") + assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) + @cache.write(1, nil) + assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) + end +end diff --git a/activesupport/test/cache/stores/null_store_test.rb b/activesupport/test/cache/stores/null_store_test.rb new file mode 100644 index 0000000000..23c4e64ee4 --- /dev/null +++ b/activesupport/test/cache/stores/null_store_test.rb @@ -0,0 +1,57 @@ +require "abstract_unit" +require "active_support/cache" +require_relative "../behaviors" + +class NullStoreTest < ActiveSupport::TestCase + def setup + @cache = ActiveSupport::Cache.lookup_store(:null_store) + end + + def test_clear + @cache.clear + end + + def test_cleanup + @cache.cleanup + end + + def test_write + assert_equal true, @cache.write("name", "value") + end + + def test_read + @cache.write("name", "value") + assert_nil @cache.read("name") + end + + def test_delete + @cache.write("name", "value") + assert_equal false, @cache.delete("name") + end + + def test_increment + @cache.write("name", 1, raw: true) + assert_nil @cache.increment("name") + end + + def test_decrement + @cache.write("name", 1, raw: true) + assert_nil @cache.increment("name") + end + + def test_delete_matched + @cache.write("name", "value") + @cache.delete_matched(/name/) + end + + def test_local_store_strategy + @cache.with_local_cache do + @cache.write("name", "value") + assert_equal "value", @cache.read("name") + @cache.delete("name") + assert_nil @cache.read("name") + @cache.write("name", "value") + end + assert_nil @cache.read("name") + end +end diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb deleted file mode 100644 index f2f8c58111..0000000000 --- a/activesupport/test/caching_test.rb +++ /dev/null @@ -1,1359 +0,0 @@ -require "logger" -require "abstract_unit" -require "active_support/cache" -require "dependencies_test_helpers" - -require "pathname" - -module ActiveSupport - module Cache - module Strategy - module LocalCache - class MiddlewareTest < ActiveSupport::TestCase - def test_local_cache_cleared_on_close - key = "super awesome key" - assert_nil LocalCacheRegistry.cache_for key - middleware = Middleware.new("<3", key).new(->(env) { - assert LocalCacheRegistry.cache_for(key), "should have a cache" - [200, {}, []] - }) - _, _, body = middleware.call({}) - assert LocalCacheRegistry.cache_for(key), "should still have a cache" - body.each {} - assert LocalCacheRegistry.cache_for(key), "should still have a cache" - body.close - assert_nil LocalCacheRegistry.cache_for(key) - end - - def test_local_cache_cleared_and_response_should_be_present_on_invalid_parameters_error - key = "super awesome key" - assert_nil LocalCacheRegistry.cache_for key - middleware = Middleware.new("<3", key).new(->(env) { - assert LocalCacheRegistry.cache_for(key), "should have a cache" - raise Rack::Utils::InvalidParameterError - }) - response = middleware.call({}) - assert response, "response should exist" - assert_nil LocalCacheRegistry.cache_for(key) - end - - def test_local_cache_cleared_on_exception - key = "super awesome key" - assert_nil LocalCacheRegistry.cache_for key - middleware = Middleware.new("<3", key).new(->(env) { - assert LocalCacheRegistry.cache_for(key), "should have a cache" - raise - }) - assert_raises(RuntimeError) { middleware.call({}) } - assert_nil LocalCacheRegistry.cache_for(key) - end - - def test_local_cache_cleared_on_throw - key = "super awesome key" - assert_nil LocalCacheRegistry.cache_for key - middleware = Middleware.new("<3", key).new(->(env) { - assert LocalCacheRegistry.cache_for(key), "should have a cache" - throw :warden - }) - assert_throws(:warden) { middleware.call({}) } - assert_nil LocalCacheRegistry.cache_for(key) - end - end - end - end - end -end - -class CacheKeyTest < ActiveSupport::TestCase - def test_entry_legacy_optional_ivars - legacy = Class.new(ActiveSupport::Cache::Entry) do - def initialize(value, options = {}) - @value = value - @expires_in = nil - @created_at = nil - super - end - end - - entry = legacy.new "foo" - assert_equal "foo", entry.value - end - - def test_expand_cache_key - assert_equal "1/2/true", ActiveSupport::Cache.expand_cache_key([1, "2", true]) - assert_equal "name/1/2/true", ActiveSupport::Cache.expand_cache_key([1, "2", true], :name) - end - - def test_expand_cache_key_with_rails_cache_id - with_env("RAILS_CACHE_ID" => "c99") do - assert_equal "c99/foo", ActiveSupport::Cache.expand_cache_key(:foo) - assert_equal "c99/foo", ActiveSupport::Cache.expand_cache_key([:foo]) - assert_equal "c99/foo/bar", ActiveSupport::Cache.expand_cache_key([:foo, :bar]) - assert_equal "nm/c99/foo", ActiveSupport::Cache.expand_cache_key(:foo, :nm) - assert_equal "nm/c99/foo", ActiveSupport::Cache.expand_cache_key([:foo], :nm) - assert_equal "nm/c99/foo/bar", ActiveSupport::Cache.expand_cache_key([:foo, :bar], :nm) - end - end - - def test_expand_cache_key_with_rails_app_version - with_env("RAILS_APP_VERSION" => "rails3") do - assert_equal "rails3/foo", ActiveSupport::Cache.expand_cache_key(:foo) - end - end - - def test_expand_cache_key_rails_cache_id_should_win_over_rails_app_version - with_env("RAILS_CACHE_ID" => "c99", "RAILS_APP_VERSION" => "rails3") do - assert_equal "c99/foo", ActiveSupport::Cache.expand_cache_key(:foo) - end - end - - def test_expand_cache_key_respond_to_cache_key - key = "foo" - def key.cache_key - :foo_key - end - assert_equal "foo_key", ActiveSupport::Cache.expand_cache_key(key) - end - - def test_expand_cache_key_array_with_something_that_responds_to_cache_key - key = "foo" - def key.cache_key - :foo_key - end - assert_equal "foo_key", ActiveSupport::Cache.expand_cache_key([key]) - end - - def test_expand_cache_key_of_nil - assert_equal "", ActiveSupport::Cache.expand_cache_key(nil) - end - - def test_expand_cache_key_of_false - assert_equal "false", ActiveSupport::Cache.expand_cache_key(false) - end - - def test_expand_cache_key_of_true - assert_equal "true", ActiveSupport::Cache.expand_cache_key(true) - end - - def test_expand_cache_key_of_array_like_object - assert_equal "foo/bar/baz", ActiveSupport::Cache.expand_cache_key(%w{foo bar baz}.to_enum) - end - - private - - def with_env(kv) - old_values = {} - kv.each { |key, value| old_values[key], ENV[key] = ENV[key], value } - yield - ensure - old_values.each { |key, value| ENV[key] = value } - end -end - -class CacheStoreSettingTest < ActiveSupport::TestCase - def test_memory_store_gets_created_if_no_arguments_passed_to_lookup_store_method - store = ActiveSupport::Cache.lookup_store - assert_kind_of(ActiveSupport::Cache::MemoryStore, store) - end - - def test_memory_store - store = ActiveSupport::Cache.lookup_store :memory_store - assert_kind_of(ActiveSupport::Cache::MemoryStore, store) - end - - def test_file_fragment_cache_store - store = ActiveSupport::Cache.lookup_store :file_store, "/path/to/cache/directory" - assert_kind_of(ActiveSupport::Cache::FileStore, store) - assert_equal "/path/to/cache/directory", store.cache_path - end - - def test_mem_cache_fragment_cache_store - assert_called_with(Dalli::Client, :new, [%w[localhost], {}]) do - store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost" - assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) - end - end - - def test_mem_cache_fragment_cache_store_with_given_mem_cache - mem_cache = Dalli::Client.new - assert_not_called(Dalli::Client, :new) do - store = ActiveSupport::Cache.lookup_store :mem_cache_store, mem_cache - assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) - end - end - - def test_mem_cache_fragment_cache_store_with_not_dalli_client - assert_not_called(Dalli::Client, :new) do - memcache = Object.new - assert_raises(ArgumentError) do - ActiveSupport::Cache.lookup_store :mem_cache_store, memcache - end - end - end - - def test_mem_cache_fragment_cache_store_with_multiple_servers - assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], {}]) do - store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", "192.168.1.1" - assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) - end - end - - def test_mem_cache_fragment_cache_store_with_options - assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], { timeout: 10 }]) do - store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", "192.168.1.1", namespace: "foo", timeout: 10 - assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) - assert_equal "foo", store.options[:namespace] - end - end - - def test_object_assigned_fragment_cache_store - store = ActiveSupport::Cache.lookup_store ActiveSupport::Cache::FileStore.new("/path/to/cache/directory") - assert_kind_of(ActiveSupport::Cache::FileStore, store) - assert_equal "/path/to/cache/directory", store.cache_path - end -end - -class CacheStoreNamespaceTest < ActiveSupport::TestCase - def test_static_namespace - cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: "tester") - cache.write("foo", "bar") - assert_equal "bar", cache.read("foo") - assert_equal "bar", cache.instance_variable_get(:@data)["tester:foo"].value - end - - def test_proc_namespace - test_val = "tester" - proc = lambda { test_val } - cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: proc) - cache.write("foo", "bar") - assert_equal "bar", cache.read("foo") - assert_equal "bar", cache.instance_variable_get(:@data)["tester:foo"].value - end - - def test_delete_matched_key_start - cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: "tester") - cache.write("foo", "bar") - cache.write("fu", "baz") - cache.delete_matched(/^fo/) - assert !cache.exist?("foo") - assert cache.exist?("fu") - end - - def test_delete_matched_key - cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: "foo") - cache.write("foo", "bar") - cache.write("fu", "baz") - cache.delete_matched(/OO/i) - assert !cache.exist?("foo") - assert cache.exist?("fu") - end -end - -# Tests the base functionality that should be identical across all cache stores. -module CacheStoreBehavior - def test_should_read_and_write_strings - assert @cache.write("foo", "bar") - assert_equal "bar", @cache.read("foo") - end - - def test_should_overwrite - @cache.write("foo", "bar") - @cache.write("foo", "baz") - assert_equal "baz", @cache.read("foo") - end - - def test_fetch_without_cache_miss - @cache.write("foo", "bar") - assert_not_called(@cache, :write) do - assert_equal "bar", @cache.fetch("foo") { "baz" } - end - end - - def test_fetch_with_cache_miss - assert_called_with(@cache, :write, ["foo", "baz", @cache.options]) do - assert_equal "baz", @cache.fetch("foo") { "baz" } - end - end - - def test_fetch_with_cache_miss_passes_key_to_block - cache_miss = false - assert_equal 3, @cache.fetch("foo") { |key| cache_miss = true; key.length } - assert cache_miss - - cache_miss = false - assert_equal 3, @cache.fetch("foo") { |key| cache_miss = true; key.length } - assert !cache_miss - end - - def test_fetch_with_forced_cache_miss - @cache.write("foo", "bar") - assert_not_called(@cache, :read) do - assert_called_with(@cache, :write, ["foo", "bar", @cache.options.merge(force: true)]) do - @cache.fetch("foo", force: true) { "bar" } - end - end - end - - def test_fetch_with_cached_nil - @cache.write("foo", nil) - assert_not_called(@cache, :write) do - assert_nil @cache.fetch("foo") { "baz" } - end - end - - def test_fetch_with_forced_cache_miss_with_block - @cache.write("foo", "bar") - assert_equal "foo_bar", @cache.fetch("foo", force: true) { "foo_bar" } - end - - def test_fetch_with_forced_cache_miss_without_block - @cache.write("foo", "bar") - assert_raises(ArgumentError) do - @cache.fetch("foo", force: true) - end - - assert_equal "bar", @cache.read("foo") - end - - def test_should_read_and_write_hash - assert @cache.write("foo", a: "b") - assert_equal({ a: "b" }, @cache.read("foo")) - end - - def test_should_read_and_write_integer - assert @cache.write("foo", 1) - assert_equal 1, @cache.read("foo") - end - - def test_should_read_and_write_nil - assert @cache.write("foo", nil) - assert_nil @cache.read("foo") - end - - def test_should_read_and_write_false - assert @cache.write("foo", false) - assert_equal false, @cache.read("foo") - end - - def test_read_multi - @cache.write("foo", "bar") - @cache.write("fu", "baz") - @cache.write("fud", "biz") - assert_equal({ "foo" => "bar", "fu" => "baz" }, @cache.read_multi("foo", "fu")) - end - - def test_read_multi_with_expires - time = Time.now - @cache.write("foo", "bar", expires_in: 10) - @cache.write("fu", "baz") - @cache.write("fud", "biz") - Time.stub(:now, time + 11) do - assert_equal({ "fu" => "baz" }, @cache.read_multi("foo", "fu")) - end - end - - def test_fetch_multi - @cache.write("foo", "bar") - @cache.write("fud", "biz") - - values = @cache.fetch_multi("foo", "fu", "fud") { |value| value * 2 } - - assert_equal({ "foo" => "bar", "fu" => "fufu", "fud" => "biz" }, values) - assert_equal("fufu", @cache.read("fu")) - end - - def test_multi_with_objects - cache_struct = Struct.new(:cache_key, :title) - foo = cache_struct.new("foo", "FOO!") - bar = cache_struct.new("bar") - - @cache.write("bar", "BAM!") - - values = @cache.fetch_multi(foo, bar) { |object| object.title } - - assert_equal({ foo => "FOO!", bar => "BAM!" }, values) - end - - def test_fetch_multi_without_block - assert_raises(ArgumentError) do - @cache.fetch_multi("foo") - end - end - - def test_read_and_write_compressed_small_data - @cache.write("foo", "bar", compress: true) - assert_equal "bar", @cache.read("foo") - end - - def test_read_and_write_compressed_large_data - @cache.write("foo", "bar", compress: true, compress_threshold: 2) - assert_equal "bar", @cache.read("foo") - end - - def test_read_and_write_compressed_nil - @cache.write("foo", nil, compress: true) - assert_nil @cache.read("foo") - end - - def test_cache_key - obj = Object.new - def obj.cache_key - :foo - end - @cache.write(obj, "bar") - assert_equal "bar", @cache.read("foo") - end - - def test_param_as_cache_key - obj = Object.new - def obj.to_param - "foo" - end - @cache.write(obj, "bar") - assert_equal "bar", @cache.read("foo") - end - - def test_array_as_cache_key - @cache.write([:fu, "foo"], "bar") - assert_equal "bar", @cache.read("fu/foo") - end - - def test_hash_as_cache_key - @cache.write({ foo: 1, fu: 2 }, "bar") - assert_equal "bar", @cache.read("foo=1/fu=2") - end - - def test_keys_are_case_sensitive - @cache.write("foo", "bar") - assert_nil @cache.read("FOO") - end - - def test_exist - @cache.write("foo", "bar") - assert_equal true, @cache.exist?("foo") - assert_equal false, @cache.exist?("bar") - end - - def test_nil_exist - @cache.write("foo", nil) - assert @cache.exist?("foo") - end - - def test_delete - @cache.write("foo", "bar") - assert @cache.exist?("foo") - assert @cache.delete("foo") - assert !@cache.exist?("foo") - end - - def test_original_store_objects_should_not_be_immutable - bar = "bar" - @cache.write("foo", bar) - assert_nothing_raised { bar.gsub!(/.*/, "baz") } - end - - def test_expires_in - time = Time.local(2008, 4, 24) - - Time.stub(:now, time) do - @cache.write("foo", "bar") - assert_equal "bar", @cache.read("foo") - end - - Time.stub(:now, time + 30) do - assert_equal "bar", @cache.read("foo") - end - - Time.stub(:now, time + 61) do - assert_nil @cache.read("foo") - end - end - - def test_race_condition_protection_skipped_if_not_defined - @cache.write("foo", "bar") - time = @cache.send(:read_entry, @cache.send(:normalize_key, "foo", {}), {}).expires_at - - Time.stub(:now, Time.at(time)) do - result = @cache.fetch("foo") do - assert_nil @cache.read("foo") - "baz" - end - assert_equal "baz", result - end - end - - def test_race_condition_protection_is_limited - time = Time.now - @cache.write("foo", "bar", expires_in: 60) - Time.stub(:now, time + 71) do - result = @cache.fetch("foo", race_condition_ttl: 10) do - assert_nil @cache.read("foo") - "baz" - end - assert_equal "baz", result - end - end - - def test_race_condition_protection_is_safe - time = Time.now - @cache.write("foo", "bar", expires_in: 60) - Time.stub(:now, time + 61) do - begin - @cache.fetch("foo", race_condition_ttl: 10) do - assert_equal "bar", @cache.read("foo") - raise ArgumentError.new - end - rescue ArgumentError - end - assert_equal "bar", @cache.read("foo") - end - Time.stub(:now, time + 91) do - assert_nil @cache.read("foo") - end - end - - def test_race_condition_protection - time = Time.now - @cache.write("foo", "bar", expires_in: 60) - Time.stub(:now, time + 61) do - result = @cache.fetch("foo", race_condition_ttl: 10) do - assert_equal "bar", @cache.read("foo") - "baz" - end - assert_equal "baz", result - end - end - - def test_crazy_key_characters - crazy_key = "#/:*(<+=> )&$%@?;'\"\'`~-" - assert @cache.write(crazy_key, "1", raw: true) - assert_equal "1", @cache.read(crazy_key) - assert_equal "1", @cache.fetch(crazy_key) - assert @cache.delete(crazy_key) - assert_equal "2", @cache.fetch(crazy_key, raw: true) { "2" } - assert_equal 3, @cache.increment(crazy_key) - assert_equal 2, @cache.decrement(crazy_key) - end - - def test_really_long_keys - key = "" - 900.times { key << "x" } - assert @cache.write(key, "bar") - assert_equal "bar", @cache.read(key) - assert_equal "bar", @cache.fetch(key) - assert_nil @cache.read("#{key}x") - assert_equal({ key => "bar" }, @cache.read_multi(key)) - assert @cache.delete(key) - end - - def test_cache_hit_instrumentation - key = "test_key" - @events = [] - ActiveSupport::Notifications.subscribe "cache_read.active_support" do |*args| - @events << ActiveSupport::Notifications::Event.new(*args) - end - assert @cache.write(key, "1", raw: true) - assert @cache.fetch(key) {} - assert_equal 1, @events.length - assert_equal "cache_read.active_support", @events[0].name - assert_equal :fetch, @events[0].payload[:super_operation] - assert @events[0].payload[:hit] - ensure - ActiveSupport::Notifications.unsubscribe "cache_read.active_support" - end - - def test_cache_miss_instrumentation - @events = [] - ActiveSupport::Notifications.subscribe(/^cache_(.*)\.active_support$/) do |*args| - @events << ActiveSupport::Notifications::Event.new(*args) - end - assert_not @cache.fetch("bad_key") {} - assert_equal 3, @events.length - assert_equal "cache_read.active_support", @events[0].name - assert_equal "cache_generate.active_support", @events[1].name - assert_equal "cache_write.active_support", @events[2].name - assert_equal :fetch, @events[0].payload[:super_operation] - assert_not @events[0].payload[:hit] - ensure - ActiveSupport::Notifications.unsubscribe "cache_read.active_support" - end -end - -module CacheStoreVersionBehavior - ModelWithKeyAndVersion = Struct.new(:cache_key, :cache_version) - - def test_fetch_with_right_version_should_hit - @cache.fetch("foo", version: 1) { "bar" } - assert_equal "bar", @cache.read("foo", version: 1) - end - - def test_fetch_with_wrong_version_should_miss - @cache.fetch("foo", version: 1) { "bar" } - assert_nil @cache.read("foo", version: 2) - end - - def test_read_with_right_version_should_hit - @cache.write("foo", "bar", version: 1) - assert_equal "bar", @cache.read("foo", version: 1) - end - - def test_read_with_wrong_version_should_miss - @cache.write("foo", "bar", version: 1) - assert_nil @cache.read("foo", version: 2) - end - - def test_exist_with_right_version_should_be_true - @cache.write("foo", "bar", version: 1) - assert @cache.exist?("foo", version: 1) - end - - def test_exist_with_wrong_version_should_be_false - @cache.write("foo", "bar", version: 1) - assert !@cache.exist?("foo", version: 2) - end - - def test_reading_and_writing_with_model_supporting_cache_version - m1v1 = ModelWithKeyAndVersion.new("model/1", 1) - m1v2 = ModelWithKeyAndVersion.new("model/1", 2) - - @cache.write(m1v1, "bar") - assert_equal "bar", @cache.read(m1v1) - assert_nil @cache.read(m1v2) - end - - def test_reading_and_writing_with_model_supporting_cache_version_using_nested_key - m1v1 = ModelWithKeyAndVersion.new("model/1", 1) - m1v2 = ModelWithKeyAndVersion.new("model/1", 2) - - @cache.write([ "something", m1v1 ], "bar") - assert_equal "bar", @cache.read([ "something", m1v1 ]) - assert_nil @cache.read([ "something", m1v2 ]) - end - - def test_fetching_with_model_supporting_cache_version - m1v1 = ModelWithKeyAndVersion.new("model/1", 1) - m1v2 = ModelWithKeyAndVersion.new("model/1", 2) - - @cache.fetch(m1v1) { "bar" } - assert_equal "bar", @cache.fetch(m1v1) { "bu" } - assert_equal "bu", @cache.fetch(m1v2) { "bu" } - end - - def test_exist_with_model_supporting_cache_version - m1v1 = ModelWithKeyAndVersion.new("model/1", 1) - m1v2 = ModelWithKeyAndVersion.new("model/1", 2) - - @cache.write(m1v1, "bar") - assert @cache.exist?(m1v1) - assert_not @cache.fetch(m1v2) - end - - def test_fetch_multi_with_model_supporting_cache_version - m1v1 = ModelWithKeyAndVersion.new("model/1", 1) - m2v1 = ModelWithKeyAndVersion.new("model/2", 1) - m2v2 = ModelWithKeyAndVersion.new("model/2", 2) - - first_fetch_values = @cache.fetch_multi(m1v1, m2v1) { |m| m.cache_key } - second_fetch_values = @cache.fetch_multi(m1v1, m2v2) { |m| m.cache_key + " 2nd" } - - assert_equal({ m1v1 => "model/1", m2v1 => "model/2" }, first_fetch_values) - assert_equal({ m1v1 => "model/1", m2v2 => "model/2 2nd" }, second_fetch_values) - end - - def test_version_is_normalized - @cache.write("foo", "bar", version: 1) - assert_equal "bar", @cache.read("foo", version: "1") - end -end - -# https://rails.lighthouseapp.com/projects/8994/tickets/6225-memcachestore-cant-deal-with-umlauts-and-special-characters -# The error is caused by character encodings that can't be compared with ASCII-8BIT regular expressions and by special -# characters like the umlaut in UTF-8. -module EncodedKeyCacheBehavior - Encoding.list.each do |encoding| - define_method "test_#{encoding.name.underscore}_encoded_values" do - key = "foo".force_encoding(encoding) - assert @cache.write(key, "1", raw: true) - assert_equal "1", @cache.read(key) - assert_equal "1", @cache.fetch(key) - assert @cache.delete(key) - assert_equal "2", @cache.fetch(key, raw: true) { "2" } - assert_equal 3, @cache.increment(key) - assert_equal 2, @cache.decrement(key) - end - end - - def test_common_utf8_values - key = "\xC3\xBCmlaut".force_encoding(Encoding::UTF_8) - assert @cache.write(key, "1", raw: true) - assert_equal "1", @cache.read(key) - assert_equal "1", @cache.fetch(key) - assert @cache.delete(key) - assert_equal "2", @cache.fetch(key, raw: true) { "2" } - assert_equal 3, @cache.increment(key) - assert_equal 2, @cache.decrement(key) - end - - def test_retains_encoding - key = "\xC3\xBCmlaut".force_encoding(Encoding::UTF_8) - assert @cache.write(key, "1", raw: true) - assert_equal Encoding::UTF_8, key.encoding - end -end - -module CacheDeleteMatchedBehavior - def test_delete_matched - @cache.write("foo", "bar") - @cache.write("fu", "baz") - @cache.write("foo/bar", "baz") - @cache.write("fu/baz", "bar") - @cache.delete_matched(/oo/) - assert !@cache.exist?("foo") - assert @cache.exist?("fu") - assert !@cache.exist?("foo/bar") - assert @cache.exist?("fu/baz") - end -end - -module CacheIncrementDecrementBehavior - def test_increment - @cache.write("foo", 1, raw: true) - assert_equal 1, @cache.read("foo").to_i - assert_equal 2, @cache.increment("foo") - assert_equal 2, @cache.read("foo").to_i - assert_equal 3, @cache.increment("foo") - assert_equal 3, @cache.read("foo").to_i - assert_nil @cache.increment("bar") - end - - def test_decrement - @cache.write("foo", 3, raw: true) - assert_equal 3, @cache.read("foo").to_i - assert_equal 2, @cache.decrement("foo") - assert_equal 2, @cache.read("foo").to_i - assert_equal 1, @cache.decrement("foo") - assert_equal 1, @cache.read("foo").to_i - assert_nil @cache.decrement("bar") - end -end - -module LocalCacheBehavior - def test_local_writes_are_persistent_on_the_remote_cache - retval = @cache.with_local_cache do - @cache.write("foo", "bar") - end - assert retval - assert_equal "bar", @cache.read("foo") - end - - def test_clear_also_clears_local_cache - @cache.with_local_cache do - @cache.write("foo", "bar") - @cache.clear - assert_nil @cache.read("foo") - end - - assert_nil @cache.read("foo") - end - - def test_local_cache_of_write - @cache.with_local_cache do - @cache.write("foo", "bar") - @peek.delete("foo") - assert_equal "bar", @cache.read("foo") - end - end - - def test_local_cache_of_read - @cache.write("foo", "bar") - @cache.with_local_cache do - assert_equal "bar", @cache.read("foo") - end - end - - def test_local_cache_of_read_nil - @cache.with_local_cache do - assert_nil @cache.read("foo") - @cache.send(:bypass_local_cache) { @cache.write "foo", "bar" } - assert_nil @cache.read("foo") - end - end - - def test_local_cache_fetch - @cache.with_local_cache do - @cache.send(:local_cache).write "foo", "bar" - assert_equal "bar", @cache.send(:local_cache).fetch("foo") - end - end - - def test_local_cache_of_write_nil - @cache.with_local_cache do - assert @cache.write("foo", nil) - assert_nil @cache.read("foo") - @peek.write("foo", "bar") - assert_nil @cache.read("foo") - end - end - - def test_local_cache_of_write_with_unless_exist - @cache.with_local_cache do - @cache.write("foo", "bar") - @cache.write("foo", "baz", unless_exist: true) - assert_equal @peek.read("foo"), @cache.read("foo") - end - end - - def test_local_cache_of_delete - @cache.with_local_cache do - @cache.write("foo", "bar") - @cache.delete("foo") - assert_nil @cache.read("foo") - end - end - - def test_local_cache_of_exist - @cache.with_local_cache do - @cache.write("foo", "bar") - @peek.delete("foo") - assert @cache.exist?("foo") - end - end - - def test_local_cache_of_increment - @cache.with_local_cache do - @cache.write("foo", 1, raw: true) - @peek.write("foo", 2, raw: true) - @cache.increment("foo") - assert_equal 3, @cache.read("foo") - end - end - - def test_local_cache_of_decrement - @cache.with_local_cache do - @cache.write("foo", 1, raw: true) - @peek.write("foo", 3, raw: true) - @cache.decrement("foo") - assert_equal 2, @cache.read("foo") - end - end - - def test_middleware - app = lambda { |env| - result = @cache.write("foo", "bar") - assert_equal "bar", @cache.read("foo") # make sure 'foo' was written - assert result - [200, {}, []] - } - app = @cache.middleware.new(app) - app.call({}) - end -end - -module AutoloadingCacheBehavior - include DependenciesTestHelpers - def test_simple_autoloading - with_autoloading_fixtures do - @cache.write("foo", EM.new) - end - - remove_constants(:EM) - ActiveSupport::Dependencies.clear - - with_autoloading_fixtures do - assert_kind_of EM, @cache.read("foo") - end - - remove_constants(:EM) - ActiveSupport::Dependencies.clear - end - - def test_two_classes_autoloading - with_autoloading_fixtures do - @cache.write("foo", [EM.new, ClassFolder.new]) - end - - remove_constants(:EM, :ClassFolder) - ActiveSupport::Dependencies.clear - - with_autoloading_fixtures do - loaded = @cache.read("foo") - assert_kind_of Array, loaded - assert_equal 2, loaded.size - assert_kind_of EM, loaded[0] - assert_kind_of ClassFolder, loaded[1] - end - - remove_constants(:EM, :ClassFolder) - ActiveSupport::Dependencies.clear - end -end - -class FileStoreTest < ActiveSupport::TestCase - def setup - Dir.mkdir(cache_dir) unless File.exist?(cache_dir) - @cache = ActiveSupport::Cache.lookup_store(:file_store, cache_dir, expires_in: 60) - @peek = ActiveSupport::Cache.lookup_store(:file_store, cache_dir, expires_in: 60) - @cache_with_pathname = ActiveSupport::Cache.lookup_store(:file_store, Pathname.new(cache_dir), expires_in: 60) - - @buffer = StringIO.new - @cache.logger = ActiveSupport::Logger.new(@buffer) - end - - def teardown - FileUtils.rm_r(cache_dir) - rescue Errno::ENOENT - end - - def cache_dir - File.join(Dir.pwd, "tmp_cache") - end - - include CacheStoreBehavior - include CacheStoreVersionBehavior - include LocalCacheBehavior - include CacheDeleteMatchedBehavior - include CacheIncrementDecrementBehavior - include AutoloadingCacheBehavior - - def test_clear - gitkeep = File.join(cache_dir, ".gitkeep") - keep = File.join(cache_dir, ".keep") - FileUtils.touch([gitkeep, keep]) - @cache.clear - assert File.exist?(gitkeep) - assert File.exist?(keep) - end - - def test_clear_without_cache_dir - FileUtils.rm_r(cache_dir) - @cache.clear - end - - def test_long_uri_encoded_keys - @cache.write("%" * 870, 1) - assert_equal 1, @cache.read("%" * 870) - end - - def test_key_transformation - key = @cache.send(:normalize_key, "views/index?id=1", {}) - assert_equal "views/index?id=1", @cache.send(:file_path_key, key) - end - - def test_key_transformation_with_pathname - FileUtils.touch(File.join(cache_dir, "foo")) - key = @cache_with_pathname.send(:normalize_key, "views/index?id=1", {}) - assert_equal "views/index?id=1", @cache_with_pathname.send(:file_path_key, key) - end - - # Test that generated cache keys are short enough to have Tempfile stuff added to them and - # remain valid - def test_filename_max_size - key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}" - path = @cache.send(:normalize_key, key, {}) - Dir::Tmpname.create(path) do |tmpname, n, opts| - assert File.basename(tmpname + ".lock").length <= 255, "Temp filename too long: #{File.basename(tmpname + '.lock').length}" - end - end - - # Because file systems have a maximum filename size, filenames > max size should be split in to directories - # If filename is 'AAAAB', where max size is 4, the returned path should be AAAA/B - def test_key_transformation_max_filename_size - key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}B" - path = @cache.send(:normalize_key, key, {}) - assert path.split("/").all? { |dir_name| dir_name.size <= ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE } - assert_equal "B", File.basename(path) - end - - # If nothing has been stored in the cache, there is a chance the cache directory does not yet exist - # Ensure delete_matched gracefully handles this case - def test_delete_matched_when_cache_directory_does_not_exist - assert_nothing_raised do - ActiveSupport::Cache::FileStore.new("/test/cache/directory").delete_matched(/does_not_exist/) - end - end - - def test_delete_does_not_delete_empty_parent_dir - sub_cache_dir = File.join(cache_dir, "subdir/") - sub_cache_store = ActiveSupport::Cache::FileStore.new(sub_cache_dir) - assert_nothing_raised do - assert sub_cache_store.write("foo", "bar") - assert sub_cache_store.delete("foo") - end - assert File.exist?(cache_dir), "Parent of top level cache dir was deleted!" - assert File.exist?(sub_cache_dir), "Top level cache dir was deleted!" - assert Dir.entries(sub_cache_dir).reject { |f| ActiveSupport::Cache::FileStore::EXCLUDED_DIRS.include?(f) }.empty? - end - - def test_log_exception_when_cache_read_fails - File.stub(:exist?, -> { raise StandardError.new("failed") }) do - @cache.send(:read_entry, "winston", {}) - assert @buffer.string.present? - end - end - - def test_cleanup_removes_all_expired_entries - time = Time.now - @cache.write("foo", "bar", expires_in: 10) - @cache.write("baz", "qux") - @cache.write("quux", "corge", expires_in: 20) - Time.stub(:now, time + 15) do - @cache.cleanup - assert_not @cache.exist?("foo") - assert @cache.exist?("baz") - assert @cache.exist?("quux") - end - end - - def test_write_with_unless_exist - assert_equal true, @cache.write(1, "aaaaaaaaaa") - assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) - @cache.write(1, nil) - assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) - end -end - -class MemoryStoreTest < ActiveSupport::TestCase - def setup - @record_size = ActiveSupport::Cache.lookup_store(:memory_store).send(:cached_size, 1, ActiveSupport::Cache::Entry.new("aaaaaaaaaa")) - @cache = ActiveSupport::Cache.lookup_store(:memory_store, expires_in: 60, size: @record_size * 10 + 1) - end - - include CacheStoreBehavior - include CacheStoreVersionBehavior - include CacheDeleteMatchedBehavior - include CacheIncrementDecrementBehavior - - def test_prune_size - @cache.write(1, "aaaaaaaaaa") && sleep(0.001) - @cache.write(2, "bbbbbbbbbb") && sleep(0.001) - @cache.write(3, "cccccccccc") && sleep(0.001) - @cache.write(4, "dddddddddd") && sleep(0.001) - @cache.write(5, "eeeeeeeeee") && sleep(0.001) - @cache.read(2) && sleep(0.001) - @cache.read(4) - @cache.prune(@record_size * 3) - assert @cache.exist?(5) - assert @cache.exist?(4) - assert !@cache.exist?(3), "no entry" - assert @cache.exist?(2) - assert !@cache.exist?(1), "no entry" - end - - def test_prune_size_on_write - @cache.write(1, "aaaaaaaaaa") && sleep(0.001) - @cache.write(2, "bbbbbbbbbb") && sleep(0.001) - @cache.write(3, "cccccccccc") && sleep(0.001) - @cache.write(4, "dddddddddd") && sleep(0.001) - @cache.write(5, "eeeeeeeeee") && sleep(0.001) - @cache.write(6, "ffffffffff") && sleep(0.001) - @cache.write(7, "gggggggggg") && sleep(0.001) - @cache.write(8, "hhhhhhhhhh") && sleep(0.001) - @cache.write(9, "iiiiiiiiii") && sleep(0.001) - @cache.write(10, "kkkkkkkkkk") && sleep(0.001) - @cache.read(2) && sleep(0.001) - @cache.read(4) && sleep(0.001) - @cache.write(11, "llllllllll") - assert @cache.exist?(11) - assert @cache.exist?(10) - assert @cache.exist?(9) - assert @cache.exist?(8) - assert @cache.exist?(7) - assert !@cache.exist?(6), "no entry" - assert !@cache.exist?(5), "no entry" - assert @cache.exist?(4) - assert !@cache.exist?(3), "no entry" - assert @cache.exist?(2) - assert !@cache.exist?(1), "no entry" - end - - def test_prune_size_on_write_based_on_key_length - @cache.write(1, "aaaaaaaaaa") && sleep(0.001) - @cache.write(2, "bbbbbbbbbb") && sleep(0.001) - @cache.write(3, "cccccccccc") && sleep(0.001) - @cache.write(4, "dddddddddd") && sleep(0.001) - @cache.write(5, "eeeeeeeeee") && sleep(0.001) - @cache.write(6, "ffffffffff") && sleep(0.001) - @cache.write(7, "gggggggggg") && sleep(0.001) - @cache.write(8, "hhhhhhhhhh") && sleep(0.001) - @cache.write(9, "iiiiiiiiii") && sleep(0.001) - long_key = "*" * 2 * @record_size - @cache.write(long_key, "llllllllll") - assert @cache.exist?(long_key) - assert @cache.exist?(9) - assert @cache.exist?(8) - assert @cache.exist?(7) - assert @cache.exist?(6) - assert !@cache.exist?(5), "no entry" - assert !@cache.exist?(4), "no entry" - assert !@cache.exist?(3), "no entry" - assert !@cache.exist?(2), "no entry" - assert !@cache.exist?(1), "no entry" - end - - def test_pruning_is_capped_at_a_max_time - def @cache.delete_entry(*args) - sleep(0.01) - super - end - @cache.write(1, "aaaaaaaaaa") && sleep(0.001) - @cache.write(2, "bbbbbbbbbb") && sleep(0.001) - @cache.write(3, "cccccccccc") && sleep(0.001) - @cache.write(4, "dddddddddd") && sleep(0.001) - @cache.write(5, "eeeeeeeeee") && sleep(0.001) - @cache.prune(30, 0.001) - assert @cache.exist?(5) - assert @cache.exist?(4) - assert @cache.exist?(3) - assert @cache.exist?(2) - assert !@cache.exist?(1) - end - - def test_write_with_unless_exist - assert_equal true, @cache.write(1, "aaaaaaaaaa") - assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) - @cache.write(1, nil) - assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) - end -end - -class MemCacheStoreTest < ActiveSupport::TestCase - require "dalli" - - begin - ss = Dalli::Client.new("localhost:11211").stats - raise Dalli::DalliError unless ss["localhost:11211"] - - MEMCACHE_UP = true - rescue Dalli::DalliError - $stderr.puts "Skipping memcached tests. Start memcached and try again." - MEMCACHE_UP = false - end - - def setup - skip "memcache server is not up" unless MEMCACHE_UP - - @cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, expires_in: 60) - @peek = ActiveSupport::Cache.lookup_store(:mem_cache_store) - @data = @cache.instance_variable_get(:@data) - @cache.clear - @cache.silence! - @cache.logger = ActiveSupport::Logger.new("/dev/null") - end - - include CacheStoreBehavior - include CacheStoreVersionBehavior - include LocalCacheBehavior - include CacheIncrementDecrementBehavior - include EncodedKeyCacheBehavior - include AutoloadingCacheBehavior - - def test_raw_values - cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) - cache.clear - cache.write("foo", 2) - assert_equal "2", cache.read("foo") - end - - def test_raw_values_with_marshal - cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) - cache.clear - cache.write("foo", Marshal.dump([])) - assert_equal [], cache.read("foo") - end - - def test_local_cache_raw_values - cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) - cache.clear - cache.with_local_cache do - cache.write("foo", 2) - assert_equal "2", cache.read("foo") - end - end - - def test_local_cache_raw_values_with_marshal - cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) - cache.clear - cache.with_local_cache do - cache.write("foo", Marshal.dump([])) - assert_equal [], cache.read("foo") - end - end - - def test_read_should_return_a_different_object_id_each_time_it_is_called - @cache.write("foo", "bar") - value = @cache.read("foo") - assert_not_equal value.object_id, @cache.read("foo").object_id - value << "bingo" - assert_not_equal value, @cache.read("foo") - end -end - -class NullStoreTest < ActiveSupport::TestCase - def setup - @cache = ActiveSupport::Cache.lookup_store(:null_store) - end - - def test_clear - @cache.clear - end - - def test_cleanup - @cache.cleanup - end - - def test_write - assert_equal true, @cache.write("name", "value") - end - - def test_read - @cache.write("name", "value") - assert_nil @cache.read("name") - end - - def test_delete - @cache.write("name", "value") - assert_equal false, @cache.delete("name") - end - - def test_increment - @cache.write("name", 1, raw: true) - assert_nil @cache.increment("name") - end - - def test_decrement - @cache.write("name", 1, raw: true) - assert_nil @cache.increment("name") - end - - def test_delete_matched - @cache.write("name", "value") - @cache.delete_matched(/name/) - end - - def test_local_store_strategy - @cache.with_local_cache do - @cache.write("name", "value") - assert_equal "value", @cache.read("name") - @cache.delete("name") - assert_nil @cache.read("name") - @cache.write("name", "value") - end - assert_nil @cache.read("name") - end -end - -class CacheStoreLoggerTest < ActiveSupport::TestCase - def setup - @cache = ActiveSupport::Cache.lookup_store(:memory_store) - - @buffer = StringIO.new - @cache.logger = ActiveSupport::Logger.new(@buffer) - end - - def test_logging - @cache.fetch("foo") { "bar" } - assert @buffer.string.present? - end - - def test_log_with_string_namespace - @cache.fetch("foo", namespace: "string_namespace") { "bar" } - assert_match %r{string_namespace:foo}, @buffer.string - end - - def test_log_with_proc_namespace - proc = Proc.new do - "proc_namespace" - end - @cache.fetch("foo", namespace: proc) { "bar" } - assert_match %r{proc_namespace:foo}, @buffer.string - end - - def test_mute_logging - @cache.mute { @cache.fetch("foo") { "bar" } } - assert @buffer.string.blank? - end -end - -class CacheEntryTest < ActiveSupport::TestCase - def test_expired - entry = ActiveSupport::Cache::Entry.new("value") - assert !entry.expired?, "entry not expired" - entry = ActiveSupport::Cache::Entry.new("value", expires_in: 60) - assert !entry.expired?, "entry not expired" - Time.stub(:now, Time.now + 61) do - assert entry.expired?, "entry is expired" - end - end - - def test_compress_values - value = "value" * 100 - entry = ActiveSupport::Cache::Entry.new(value, compress: true, compress_threshold: 1) - assert_equal value, entry.value - assert(value.bytesize > entry.size, "value is compressed") - end - - def test_non_compress_values - value = "value" * 100 - entry = ActiveSupport::Cache::Entry.new(value) - assert_equal value, entry.value - assert_equal value.bytesize, entry.size - end -end - -class CacheStoreWriteMultiEntriesStoreProviderInterfaceTest < ActiveSupport::TestCase - setup do - @cache = ActiveSupport::Cache.lookup_store(:null_store) - end - - test "fetch_multi uses write_multi_entries store provider interface" do - assert_called_with(@cache, :write_multi_entries) do - @cache.fetch_multi "a", "b", "c" do |key| - key * 2 - end - end - end -end - -class CacheStoreWriteMultiInstrumentationTest < ActiveSupport::TestCase - setup do - @cache = ActiveSupport::Cache.lookup_store(:null_store) - end - - test "instrumentation" do - writes = { "a" => "aa", "b" => "bb" } - - events = with_instrumentation "write_multi" do - @cache.write_multi(writes) - end - - assert_equal %w[ cache_write_multi.active_support ], events.map(&:name) - assert_nil events[0].payload[:super_operation] - assert_equal({ "a" => "aa", "b" => "bb" }, events[0].payload[:key]) - end - - test "instrumentation with fetch_multi as super operation" do - skip "fetch_multi isn't instrumented yet" - - events = with_instrumentation "write_multi" do - @cache.fetch_multi("a", "b") { |key| key * 2 } - end - - assert_equal %w[ cache_write_multi.active_support ], events.map(&:name) - assert_nil events[0].payload[:super_operation] - assert !events[0].payload[:hit] - end - - private - def with_instrumentation(method) - event_name = "cache_#{method}.active_support" - - [].tap do |events| - ActiveSupport::Notifications.subscribe event_name do |*args| - events << ActiveSupport::Notifications::Event.new(*args) - end - yield - end - ensure - ActiveSupport::Notifications.unsubscribe event_name - end -end diff --git a/guides/source/active_record_postgresql.md b/guides/source/active_record_postgresql.md index 6d07291b07..041fdacbab 100644 --- a/guides/source/active_record_postgresql.md +++ b/guides/source/active_record_postgresql.md @@ -84,7 +84,7 @@ Book.where("array_length(ratings, 1) >= 3") ### Hstore * [type definition](http://www.postgresql.org/docs/current/static/hstore.html) -* [functions and operators](http://www.postgresql.org/docs/current/static/hstore.html#AEN167712) +* [functions and operators](http://www.postgresql.org/docs/current/static/hstore.html#AEN179902) NOTE: You need to enable the `hstore` extension to use hstore. @@ -285,7 +285,7 @@ SELECT n.nspname AS enum_schema, ### UUID * [type definition](http://www.postgresql.org/docs/current/static/datatype-uuid.html) -* [pgcrypto generator function](http://www.postgresql.org/docs/current/static/pgcrypto.html#AEN159361) +* [pgcrypto generator function](http://www.postgresql.org/docs/current/static/pgcrypto.html#AEN182570) * [uuid-ossp generator functions](http://www.postgresql.org/docs/current/static/uuid-ossp.html) NOTE: You need to enable the `pgcrypto` (only PostgreSQL >= 9.4) or `uuid-ossp` diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index e34af8aa8d..23f53ac084 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -755,6 +755,8 @@ NOTE: Defined in `active_support/core_ext/module/anonymous.rb`. ### Method Delegation +#### `delegate` + The macro `delegate` offers an easy way to forward methods. Let's imagine that users in some application have login information in the `User` model but name and other data in a separate `Profile` model: @@ -837,13 +839,32 @@ In the previous example the macro generates `avatar_size` rather than `size`. NOTE: Defined in `active_support/core_ext/module/delegation.rb` +#### `delegate_missing_to` + +Imagine you would like to delegate everything missing from the `User` object, +to the `Profile` one. The `delegate_missing_to` macro lets you implement this +in a breeze: + +```ruby +class User < ApplicationRecord + has_one :profile + + delegate_missing_to :profile +end +``` + +The target can be anything callable within the object, e.g. instance variables, +methods, constants, etc. Only the public methods of the target are delegated. + +NOTE: Defined in `active_support/core_ext/module/delegation.rb`. + ### Redefining Methods There are cases where you need to define a method with `define_method`, but don't know whether a method with that name already exists. If it does, a warning is issued if they are enabled. No big deal, but not clean either. The method `redefine_method` prevents such a potential warning, removing the existing method before if needed. -NOTE: Defined in `active_support/core_ext/module/remove_method.rb` +NOTE: Defined in `active_support/core_ext/module/remove_method.rb`. Extensions to `Class` --------------------- @@ -931,7 +952,7 @@ When `:instance_reader` is `false`, the instance predicate returns a `NoMethodEr If you do not want the instance predicate, pass `instance_predicate: false` and it will not be defined. -NOTE: Defined in `active_support/core_ext/class/attribute.rb` +NOTE: Defined in `active_support/core_ext/class/attribute.rb`. #### `cattr_reader`, `cattr_writer`, and `cattr_accessor` @@ -1827,7 +1848,7 @@ as well as adding or subtracting their results from a Time object. For example: (4.months + 5.years).from_now ``` -NOTE: Defined in `active_support/core_ext/numeric/time.rb` +NOTE: Defined in `active_support/core_ext/numeric/time.rb`. ### Formatting diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 5d774566dd..a02eebf263 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -572,20 +572,6 @@ would generate this HTML: The `body` param is required by Sprockets. -### Runtime Error Checking - -By default the asset pipeline will check for potential errors in development mode during -runtime. To disable this behavior you can set: - -```ruby -config.assets.raise_runtime_errors = false -``` - -When this option is true, the asset pipeline will check if all the assets loaded -in your application are included in the `config.assets.precompile` list. -If `config.assets.digest` is also true, the asset pipeline will require that -all requests for assets include digests. - ### Raise an Error When an Asset is Not Found If you are using sprockets-rails >= 3.2.0 you can configure what happens diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index 5c7d1f5365..bead931529 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -960,7 +960,7 @@ class Author < ApplicationRecord end ``` -NOTE: You only need to specify the :counter_cache option on the `belongs_to` +NOTE: You only need to specify the `:counter_cache` option on the `belongs_to` side of the association. Counter cache columns are added to the containing model's list of read-only attributes through `attr_readonly`. @@ -1831,7 +1831,7 @@ The `limit` method lets you restrict the total number of objects that will be fe class Author < ApplicationRecord has_many :recent_books, -> { order('published_at desc').limit(100) }, - class_name: "Book", + class_name: "Book" end ``` diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 1234e1f192..21b3ca0efa 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -157,8 +157,6 @@ defaults to `:debug` for all environments. The available log levels are: `:debug * `config.assets.enabled` a flag that controls whether the asset pipeline is enabled. It is set to `true` by default. -* `config.assets.raise_runtime_errors` Set this flag to `true` to enable additional runtime error checking. Recommended in `config/environments/development.rb` to minimize unexpected behavior when deploying to `production`. - * `config.assets.css_compressor` defines the CSS compressor to use. It is set by default by `sass-rails`. The unique alternative value at the moment is `:yui`, which uses the `yui-compressor` gem. * `config.assets.js_compressor` defines the JavaScript compressor to use. Possible values are `:closure`, `:uglifier` and `:yui` which require the use of the `closure-compiler`, `uglifier` or `yui-compressor` gems respectively. diff --git a/guides/source/security.md b/guides/source/security.md index f69a0c72b0..9b1f28a283 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -1060,6 +1060,7 @@ Additional Resources The security landscape shifts and it is important to keep up to date, because missing a new vulnerability can be catastrophic. You can find additional resources about (Rails) security here: -* Subscribe to the Rails security [mailing list](http://groups.google.com/group/rubyonrails-security) -* [Keep up to date on the other application layers](http://secunia.com/) (they have a weekly newsletter, too) -* A [good security blog](https://www.owasp.org) including the [Cross-Site scripting Cheat Sheet](https://www.owasp.org/index.php/DOM_based_XSS_Prevention_Cheat_Sheet) +* Subscribe to the Rails security [mailing list.](http://groups.google.com/group/rubyonrails-security) +* [Brakeman - Rails Security Scanner](http://brakemanscanner.org/)- To perform static security analysis for Rails applications. +* [Keep up to date on the other application layers.](http://secunia.com/) (they have a weekly newsletter, too) +* A [good security blog](https://www.owasp.org) including the [Cross-Site scripting Cheat Sheet.](https://www.owasp.org/index.php/DOM_based_XSS_Prevention_Cheat_Sheet) diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md index 290f2a509b..35e6aea4cf 100644 --- a/guides/source/working_with_javascript_in_rails.md +++ b/guides/source/working_with_javascript_in_rails.md @@ -376,6 +376,35 @@ browser to submit the form via normal means (i.e. non-AJAX submission) will be canceled and the form will not be submitted at all. This is useful for implementing your own AJAX file upload workaround. +### Rails-ujs event handlers + +Rails 5.1 introduced rails-ujs and dropped jQuery as a dependency. +As a result the Unobtrusive JavaScript (UJS) driver has been rewritten to operate without jQuery. +These introductions cause small changes to `custom events` fired during the request: + +NOTE: Signature of calls to UJS’s event handlers have changed. +Unlike the version with jqeury, all custom events return only one parameter: `event`. +In this parameter, there is an additional attribute `details` which contains an array of extra parameters. + +| Event name | Extra parameters (event.detail) | Fired | +|---------------------|---------------------------------|-------------------------------------------------------------| +| `ajax:before` | | Before the whole ajax business. | +| `ajax:beforeSend` | [xhr, options] | Before the request is sent. | +| `ajax:send` | [xhr] | When the request is sent. | +| `ajax:stopped` | | When the request is stopped. | +| `ajax:success` | [response, status, xhr] | After completion, if the response was a success. | +| `ajax:error` | [response, status, xhr] | After completion, if the response was an error. | +| `ajax:complete` | [xhr, status] | After the request has been completed, no matter the outcome.| + +Example usage: + +```html +document.body.addEventListener('ajax:success', function(event) { + var detail = event.detail; + var data = detail[0], status = detail[1], xhr = detail[2]; +}) +``` + Server-Side Concerns -------------------- diff --git a/railties/bin/test b/railties/bin/test index a7beb14b27..470ce93f10 100755 --- a/railties/bin/test +++ b/railties/bin/test @@ -1,4 +1,4 @@ #!/usr/bin/env ruby COMPONENT_ROOT = File.expand_path("..", __dir__) -require File.expand_path("../tools/test", COMPONENT_ROOT) +require_relative "../../tools/test" diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 4ffde6198a..fb635c6ae8 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -92,6 +92,10 @@ module Rails action_dispatch.use_authenticated_cookie_encryption = true end + if respond_to?(:active_support) + active_support.use_authenticated_message_encryption = true + end + else raise "Unknown version #{target_version.to_s.inspect}" end diff --git a/railties/lib/rails/commands/secrets/secrets_command.rb b/railties/lib/rails/commands/secrets/secrets_command.rb index 651411d444..5f077a5bcb 100644 --- a/railties/lib/rails/commands/secrets/secrets_command.rb +++ b/railties/lib/rails/commands/secrets/secrets_command.rb @@ -31,7 +31,7 @@ module Rails require_application_and_environment! Rails::Secrets.read_for_editing do |tmp_path| - system("\$EDITOR #{tmp_path}") + system("#{ENV["EDITOR"]} #{tmp_path}") end say "New secrets encrypted and saved." @@ -43,7 +43,7 @@ module Rails raise unless error.message =~ /secrets\.yml\.enc/ Rails::Secrets.read_template_for_editing do |tmp_path| - system("\$EDITOR #{tmp_path}") + system("#{ENV["EDITOR"]} #{tmp_path}") generator.skip_secrets_file { setup } end end diff --git a/railties/lib/rails/commands/test/test_command.rb b/railties/lib/rails/commands/test/test_command.rb index 65e16900ba..dce85cf12d 100644 --- a/railties/lib/rails/commands/test/test_command.rb +++ b/railties/lib/rails/commands/test/test_command.rb @@ -11,7 +11,7 @@ module Rails end def perform(*) - $LOAD_PATH << Rails::Command.root.join("test") + $LOAD_PATH << Rails::Command.root.join("test").to_s Minitest.run_via = :rails diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index 747d2e6253..64e2062aea 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -37,7 +37,7 @@ end group :development do <%- unless options.api? -%> - # Access an IRB console on exception pages or by using <%%= console %> anywhere in the code. + # Access an interactive console on exception pages or by calling 'console' anywhere in the code. <%- if options.dev? || options.edge? -%> gem 'web-console', github: 'rails/web-console' <%- else -%> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml index a21555e573..049de65f22 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml @@ -1,4 +1,4 @@ -# SQL Server (2012 or higher recommended) +# SQL Server (2012 or higher required) # # Install the adapters and driver # gem install tiny_tds @@ -12,7 +12,7 @@ default: &default adapter: sqlserver encoding: utf8 username: sa - password: <%= ENV['SA_PASSWORD'] %> + password: <%%= ENV['SA_PASSWORD'] %> host: localhost development: diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt index 900baa607a..3809936f9f 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt @@ -13,3 +13,7 @@ # Use AES 256 GCM authenticated encryption for encrypted cookies. # Existing cookies will be converted on read then written with the new scheme. # Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = true + +# Use AES-256-GCM authenticated encryption as default cipher for encrypting messages +# instead of AES-256-CBC, when use_authenticated_message_encryption is set to true. +# Rails.application.config.active_support.use_authenticated_message_encryption = true diff --git a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb index 7568af5b5e..6ad1f11781 100644 --- a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb +++ b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb @@ -1,4 +1,4 @@ -require File.expand_path('../config/environment', __dir__) +require_relative '../config/environment' require 'rails/test_help' class ActiveSupport::TestCase diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb index 32e8202e1c..c281fc42ca 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb @@ -1,4 +1,4 @@ -require File.expand_path("../<%= options[:dummy_path] -%>/config/environment.rb", __dir__) +require_relative "<%= File.join('..', options[:dummy_path], 'config/environment') -%>" <% unless options[:skip_active_record] -%> ActiveRecord::Migrator.migrations_paths = [File.expand_path("../<%= options[:dummy_path] -%>/db/migrate", __dir__)] <% if options[:mountable] -%> diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb index 3476bb0eb5..eca8335559 100644 --- a/railties/lib/rails/railtie.rb +++ b/railties/lib/rails/railtie.rb @@ -103,6 +103,9 @@ module Rails # end # end # + # Since filenames on the load path are shared across gems, be sure that files you load + # through a railtie have unique names. + # # == Application and Engine # # An engine is nothing more than a railtie with some initializers already set. And since diff --git a/railties/test/application/initializers/i18n_test.rb b/railties/test/application/initializers/i18n_test.rb index 206e42703b..cee198bd01 100644 --- a/railties/test/application/initializers/i18n_test.rb +++ b/railties/test/application/initializers/i18n_test.rb @@ -245,7 +245,7 @@ fr: end test "[shortcut] config.i18n.fallbacks = [{ :ca => :'es-ES' }] initializes fallbacks with a mapping ca => es-ES" do - I18n::Railtie.config.i18n.fallbacks.map = { ca: :'es-ES' } + I18n::Railtie.config.i18n.fallbacks = [{ ca: :'es-ES' }] load_app assert_fallbacks ca: [:ca, :"es-ES", :es, :en] end diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index af16a2641a..63aa079a12 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -63,7 +63,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_no_file "config/routes.rb" assert_no_file "app/assets/config/bukkits_manifest.js" assert_file "test/test_helper.rb" do |content| - assert_match(/require.+test\/dummy\/config\/environment/, content) + assert_match(/require_relative.+test\/dummy\/config\/environment/, content) assert_match(/ActiveRecord::Migrator\.migrations_paths.+test\/dummy\/db\/migrate/, content) assert_match(/Minitest\.backtrace_filter = Minitest::BacktraceFilter\.new/, content) assert_match(/Rails::TestUnitReporter\.executable = 'bin\/test'/, content) @@ -438,7 +438,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "spec/dummy/config/application.rb" assert_no_file "test/dummy" assert_file "test/test_helper.rb" do |content| - assert_match(/require.+spec\/dummy\/config\/environment/, content) + assert_match(/require_relative.+spec\/dummy\/config\/environment/, content) assert_match(/ActiveRecord::Migrator\.migrations_paths.+spec\/dummy\/db\/migrate/, content) end end @@ -449,7 +449,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "spec/fake/config/application.rb" assert_no_file "test/dummy" assert_file "test/test_helper.rb" do |content| - assert_match(/require.+spec\/fake\/config\/environment/, content) + assert_match(/require_relative.+spec\/fake\/config\/environment/, content) assert_match(/ActiveRecord::Migrator\.migrations_paths.+spec\/fake\/db\/migrate/, content) end end diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb index b784446535..e07627f36d 100644 --- a/railties/test/generators_test.rb +++ b/railties/test/generators_test.rb @@ -233,7 +233,7 @@ class GeneratorsTest < Rails::Generators::TestCase end def test_usage_with_embedded_ruby - require File.expand_path("fixtures/lib/generators/usage_template/usage_template_generator", __dir__) + require_relative "fixtures/lib/generators/usage_template/usage_template_generator" output = capture(:stdout) { Rails::Generators.invoke :usage_template, ["--help"] } assert_match(/:: 2 ::/, output) end |