diff options
34 files changed, 287 insertions, 271 deletions
@@ -35,7 +35,7 @@ or to generate the body of an email. In Rails, View generation is handled by Act  You can read more about Action View in its [README](actionview/README.rdoc).  Active Record, Active Model, Action Pack, and Action View can each be used independently outside Rails. -In addition to them, Rails also comes with Action Mailer ([README](actionmailer/README.rdoc)), a library +In addition to that, Rails also comes with Action Mailer ([README](actionmailer/README.rdoc)), a library  to generate and send emails; Active Job ([README](activejob/README.md)), a  framework for declaring jobs and making them run on a variety of queueing  backends; Action Cable ([README](actioncable/README.md)), a framework to diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index e17189f9f9..25ec3cf5b6 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -144,17 +144,21 @@ module ActionController      end      # Returns true if another +Parameters+ object contains the same content and -    # permitted flag, or other Hash-like object contains the same content. This -    # override is in place so you can perform a comparison with `Hash`. -    def ==(other_hash) -      if other_hash.respond_to?(:permitted?) -        super +    # permitted flag. +    def ==(other) +      if other.respond_to?(:permitted?) +        self.permitted? == other.permitted? && self.parameters == other.parameters +      elsif other.is_a?(Hash) +        ActiveSupport::Deprecation.warn <<-WARNING.squish +          Comparing equality between `ActionController::Parameters` and a +          `Hash` is deprecated and will be removed in Rails 5.1. Please only do +          comparisons between instances of `ActionController::Parameters`. If +          you need to compare to a hash, first convert it using +          `ActionController::Parameters#new`. +        WARNING +        @parameters == other.with_indifferent_access        else -        if other_hash.is_a?(Hash) -          @parameters == other_hash.with_indifferent_access -        else -          @parameters == other_hash -        end +        @parameters == other        end      end @@ -597,6 +601,8 @@ module ActionController      end      protected +      attr_reader :parameters +        def permitted=(new_permitted)          @permitted = new_permitted        end diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index 7556f984f2..754ac144cc 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -381,19 +381,14 @@ class CollectionCacheController < ActionController::Base      render 'index'    end -  def index_explicit_render +  def index_explicit_render_in_controller      @customers = [Customer.new('david', 1)] -    render partial: 'customers/customer', collection: @customers +    render partial: 'customers/customer', collection: @customers, cached: true    end    def index_with_comment      @customers = [Customer.new('david', 1)] -    render partial: 'customers/commented_customer', collection: @customers, as: :customer -  end - -  def index_with_callable_cache_key -    @customers = [Customer.new('david', 1)] -    render @customers, cache: -> customer { 'cached_david' } +    render partial: 'customers/commented_customer', collection: @customers, as: :customer, cached: true    end  end @@ -404,7 +399,7 @@ class AutomaticCollectionCacheTest < ActionController::TestCase      @controller.perform_caching = true      @controller.partial_rendered_times = 0      @controller.cache_store = ActiveSupport::Cache::MemoryStore.new -    ActionView::PartialRenderer.collection_cache = @controller.cache_store +    ActionView::PartialRenderer.collection_cache = ActiveSupport::Cache::MemoryStore.new    end    def test_collection_fetches_cached_views @@ -427,7 +422,7 @@ class AutomaticCollectionCacheTest < ActionController::TestCase    end    def test_explicit_render_call_with_options -    get :index_explicit_render +    get :index_explicit_render_in_controller      assert_select ':root', "david, 1"    end @@ -440,12 +435,6 @@ class AutomaticCollectionCacheTest < ActionController::TestCase      assert_equal 1, @controller.partial_rendered_times    end -  def test_caching_with_callable_cache_key -    get :index_with_callable_cache_key -    assert_customer_cached 'cached_david', 'david, 1' -    assert_customer_cached 'david/1', 'david, 1' -  end -    private      def assert_customer_cached(key, content)        assert_match content, diff --git a/actionpack/test/controller/parameters/accessors_test.rb b/actionpack/test/controller/parameters/accessors_test.rb index 08b3d81bf0..4ef5bed30d 100644 --- a/actionpack/test/controller/parameters/accessors_test.rb +++ b/actionpack/test/controller/parameters/accessors_test.rb @@ -129,10 +129,51 @@ class ParametersAccessorsTest < ActiveSupport::TestCase      assert_not @params[:person].values_at(:name).first.permitted?    end -  test "equality with another hash works" do +  test "equality with a hash is deprecated" do      hash1 = { foo: :bar }      params1 = ActionController::Parameters.new(hash1) -    assert(params1 == hash1) +    assert_deprecated("will be removed in Rails 5.1") do +      assert(params1 == hash1) +    end +  end + +  test "is equal to Parameters instance with same params" do +    params1 = ActionController::Parameters.new(a: 1, b: 2) +    params2 = ActionController::Parameters.new(a: 1, b: 2) +    assert(params1 == params2) +  end + +  test "is equal to Parameters instance with same permitted params" do +    params1 = ActionController::Parameters.new(a: 1, b: 2).permit(:a) +    params2 = ActionController::Parameters.new(a: 1, b: 2).permit(:a) +    assert(params1 == params2) +  end + +  test "is equal to Parameters instance with same different source params, but same permitted params" do +    params1 = ActionController::Parameters.new(a: 1, b: 2).permit(:a) +    params2 = ActionController::Parameters.new(a: 1, c: 3).permit(:a) +    assert(params1 == params2) +    assert(params2 == params1) +  end + +  test 'is not equal to an unpermitted Parameters instance with same params' do +    params1 = ActionController::Parameters.new(a: 1).permit(:a) +    params2 = ActionController::Parameters.new(a: 1) +    assert(params1 != params2) +    assert(params2 != params1) +  end + +  test "is not equal to Parameters instance with different permitted params" do +    params1 = ActionController::Parameters.new(a: 1, b: 2).permit(:a, :b) +    params2 = ActionController::Parameters.new(a: 1, b: 2).permit(:a) +    assert(params1 != params2) +    assert(params2 != params1) +  end + +  test "equality with simple types works" do +    assert(@params != 'Hello') +    assert(@params != 42) +    assert(@params != false)    end    test "inspect shows both class name and parameters" do diff --git a/actionpack/test/fixtures/collection_cache/index.html.erb b/actionpack/test/fixtures/collection_cache/index.html.erb index 521b1450df..853e501ab4 100644 --- a/actionpack/test/fixtures/collection_cache/index.html.erb +++ b/actionpack/test/fixtures/collection_cache/index.html.erb @@ -1 +1 @@ -<%= render @customers %>
\ No newline at end of file +<%= render partial: 'customers/customer', collection: @customers, cached: true %> diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb index 401f398721..e842d8e31c 100644 --- a/actionview/lib/action_view/helpers/cache_helper.rb +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -126,44 +126,16 @@ module ActionView        #        # Now all you have to do is change that timestamp when the helper method changes.        # -      # === Automatic Collection Caching +      # === Collection Caching        # -      # When rendering collections such as: +      # When rendering a collection of objects that each use the same partial, a `cached` +      # option can be passed. +      # For collections rendered such:        # -      #   <%= render @notifications %> -      #   <%= render partial: 'notifications/notification', collection: @notifications %> +      #   <%= render partial: 'notifications/notification', collection: @notifications, cached: true %>        # -      # If the notifications/_notification partial starts with a cache call as: -      # -      #   <% cache notification do %> -      #     <%= notification.name %> -      #   <% end %> -      # -      # The collection can then automatically use any cached renders for that -      # template by reading them at once instead of one by one. -      # -      # See ActionView::Template::Handlers::ERB.resource_cache_call_pattern for -      # more information on what cache calls make a template eligible for this -      # collection caching. -      # -      # The automatic cache multi read can be turned off like so: -      # -      #   <%= render @notifications, cache: false %> -      # -      # === Explicit Collection Caching -      # -      # If the partial template doesn't start with a clean cache call as -      # mentioned above, you can still benefit from collection caching by -      # adding a special comment format anywhere in the template, like: -      # -      #   <%# Template Collection: notification %> -      #   <% my_helper_that_calls_cache(some_arg, notification) do %> -      #     <%= notification.name %> -      #   <% end %> -      # -      # The pattern used to match these is <tt>/# Template Collection: (\S+)/</tt>, -      # so it's important that you type it out just so. -      # You can only declare one collection in a partial template file. +      # The `cached: true` will make Action Views rendering issue a `read_multi` to +      # the cache store instead of reading from it for every partial.        def cache(name = {}, options = {}, &block)          if controller.respond_to?(:perform_caching) && controller.perform_caching            name_options = options.slice(:skip_digest, :virtual_path) diff --git a/actionview/lib/action_view/log_subscriber.rb b/actionview/lib/action_view/log_subscriber.rb index 9047dbdd85..aa38db2a3a 100644 --- a/actionview/lib/action_view/log_subscriber.rb +++ b/actionview/lib/action_view/log_subscriber.rb @@ -20,7 +20,15 @@ module ActionView        end      end      alias :render_partial :render_template -    alias :render_collection :render_template + +    def render_collection(event) +      identifier = event.payload[:identifier] || 'templates' + +      info do +        "  Rendered collection of #{from_rails_root(identifier)}" \ +        " #{render_count(event.payload)} (#{event.duration.round(1)}ms)" +      end +    end      def logger        ActionView::Base.logger @@ -38,6 +46,14 @@ module ActionView      def rails_root        @root ||= "#{Rails.root}/"      end + +    def render_count(payload) +      if payload[:cache_hits] +        "[#{payload[:cache_hits]} / #{payload[:count]} cache hits]" +      else +        "[#{payload[:count]} times]" +      end +    end    end  end diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb index aa77a77acf..23e672a95f 100644 --- a/actionview/lib/action_view/renderer/abstract_renderer.rb +++ b/actionview/lib/action_view/renderer/abstract_renderer.rb @@ -35,8 +35,12 @@ module ActionView        end      end -    def instrument(name, options={}) -      ActiveSupport::Notifications.instrument("render_#{name}.action_view", options){ yield } +    def instrument(name, **options) +      options[:identifier] ||= (@template && @template.identifier) || @path + +      ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload| +        yield payload +      end      end      def prepend_formats(formats) diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index a9bd257e76..514804b08e 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -294,7 +294,7 @@ module ActionView      def render(context, options, block)        setup(context, options, block) -      identifier = (@template = find_partial) ? @template.identifier : @path +      @template = find_partial        @lookup_context.rendered_format ||= begin          if @template && @template.formats.present? @@ -305,11 +305,9 @@ module ActionView        end        if @collection -        instrument(:collection, :identifier => identifier || "collection", :count => @collection.size) do -          render_collection -        end +        render_collection        else -        instrument(:partial, :identifier => identifier) do +        instrument(:partial) do            render_partial          end        end @@ -318,15 +316,17 @@ module ActionView      private      def render_collection -      return nil if @collection.blank? +      instrument(:collection, count: @collection.size) do |payload| +        return nil if @collection.blank? -      if @options.key?(:spacer_template) -        spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals) -      end +        if @options.key?(:spacer_template) +          spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals) +        end -      cache_collection_render do -        @template ? collection_with_template : collection_without_template -      end.join(spacer).html_safe +        cache_collection_render(payload) do +          @template ? collection_with_template : collection_without_template +        end.join(spacer).html_safe +      end      end      def render_partial @@ -428,8 +428,6 @@ module ActionView          layout = find_template(layout, @template_keys)        end -      collection_cache_eligible = automatic_cache_eligible? -        partial_iteration = PartialIteration.new(@collection.size)        locals[iteration] = partial_iteration @@ -438,11 +436,6 @@ module ActionView          locals[counter]   = partial_iteration.index          content = template.render(view, locals) - -        if collection_cache_eligible -          collection_cache_rendered_partial(content, object) -        end -          content = layout.render(view, locals) { content } if layout          partial_iteration.iterate!          content diff --git a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb index 4860f00243..f7deba94ce 100644 --- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb +++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/object/try' -  module ActionView    module CollectionCaching # :nodoc:      extend ActiveSupport::Concern @@ -11,40 +9,25 @@ module ActionView      end      private -      def cache_collection_render -        return yield unless cache_collection? +      def cache_collection_render(instrumentation_payload) +        return yield unless @options[:cached]          keyed_collection = collection_by_cache_keys -        cached_partials = collection_cache.read_multi(*keyed_collection.keys) +        cached_partials  = collection_cache.read_multi(*keyed_collection.keys) +        instrumentation_payload[:cache_hits] = cached_partials.size          @collection = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values          rendered_partials = @collection.empty? ? [] : yield          index = 0 -        keyed_collection.map do |cache_key, _| -          cached_partials.fetch(cache_key) do -            rendered_partials[index].tap { index += 1 } -          end +        fetch_or_cache_partial(cached_partials, order_by: keyed_collection.each_key) do +          rendered_partials[index].tap { index += 1 }          end        end -      def cache_collection? -        @options.fetch(:cache, automatic_cache_eligible?) -      end - -      def automatic_cache_eligible? -        @template && @template.eligible_for_collection_caching?(as: @options[:as]) -      end - -      def callable_cache_key? -        @options[:cache].respond_to?(:call) -      end -        def collection_by_cache_keys -        seed = callable_cache_key? ? @options[:cache] : ->(i) { i } -          @collection.each_with_object({}) do |item, hash| -          hash[expanded_cache_key(seed.call(item))] = item +          hash[expanded_cache_key(item)] = item          end        end @@ -53,10 +36,13 @@ module ActionView          key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0.        end -      def collection_cache_rendered_partial(rendered_partial, key_by) -        if callable_cache_key? -          key = expanded_cache_key(@options[:cache].call(key_by)) -          collection_cache.write(key, rendered_partial, @options[:cache_options]) +      def fetch_or_cache_partial(cached_partials, order_by:) +        order_by.map do |cache_key| +          cached_partials.fetch(cache_key) do +            yield.tap do |rendered_partial| +              collection_cache.write(cache_key, rendered_partial) +            end +          end          end        end    end diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb index 15fc2b71a3..169ee55fdc 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -130,7 +130,6 @@ module ActionView        @source            = source        @identifier        = identifier        @handler           = handler -      @cache_name        = extract_resource_cache_name        @compiled          = false        @original_encoding = nil        @locals            = details[:locals] || [] @@ -166,10 +165,6 @@ module ActionView        @type ||= Types[@formats.first] if @formats.first      end -    def eligible_for_collection_caching?(as: nil) -      @cache_name == (as || inferred_cache_name).to_s -    end -      # Receives a view object and return a template similar to self by using @virtual_path.      #      # This method is useful if you have a template object but it does not contain its source @@ -355,23 +350,5 @@ module ActionView            ActiveSupport::Notifications.instrument("#{action}.action_view".freeze, payload, &block)          end        end - -      EXPLICIT_COLLECTION = /# Template Collection: (?<resource_name>\w+)/ - -      def extract_resource_cache_name -        if match = @source.match(EXPLICIT_COLLECTION) || resource_cache_call_match -          match[:resource_name] -        end -      end - -      def resource_cache_call_match -        if @handler.respond_to?(:resource_cache_call_pattern) -          @source.match(@handler.resource_cache_call_pattern) -        end -      end - -      def inferred_cache_name -        @inferred_cache_name ||= @virtual_path.split('/'.freeze).last.sub('_'.freeze, ''.freeze) -      end    end  end diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb index 1f8459c24b..85a100ed4c 100644 --- a/actionview/lib/action_view/template/handlers/erb.rb +++ b/actionview/lib/action_view/template/handlers/erb.rb @@ -123,31 +123,6 @@ module ActionView            ).src          end -        # Returns Regexp to extract a cached resource's name from a cache call at the -        # first line of a template. -        # The extracted cache name is captured as :resource_name. -        # -        #   <% cache notification do %> # => notification -        # -        # The pattern should support templates with a beginning comment: -        # -        #   <%# Still extractable even though there's a comment %> -        #   <% cache notification do %> # => notification -        # -        # But fail to extract a name if a resource association is cached. -        # -        #   <% cache notification.event do %> # => nil -        def resource_cache_call_pattern -          /\A -            (?:<%\#.*%>)*                 # optional initial comment -            \s*                           # followed by optional spaces or newlines -            <%\s*cache[\(\s]              # followed by an ERB call to cache -            \s*                           # followed by optional spaces or newlines -            (?<resource_name>\w+)         # capture the cache call argument as :resource_name -            [\s\)]                        # followed by a space or close paren -          /xm -        end -        private          def valid_encoding(string, encoding) diff --git a/actionview/test/template/log_subscriber_test.rb b/actionview/test/template/log_subscriber_test.rb index 4776c18b0b..93a0701dcc 100644 --- a/actionview/test/template/log_subscriber_test.rb +++ b/actionview/test/template/log_subscriber_test.rb @@ -86,7 +86,7 @@ class AVLogSubscriberTest < ActiveSupport::TestCase        wait        assert_equal 1, @logger.logged(:info).size -      assert_match(/Rendered test\/_customer.erb/, @logger.logged(:info).last) +      assert_match(/Rendered collection of test\/_customer.erb \[2 times\]/, @logger.logged(:info).last)      end    end @@ -96,7 +96,7 @@ class AVLogSubscriberTest < ActiveSupport::TestCase        wait        assert_equal 1, @logger.logged(:info).size -      assert_match(/Rendered customers\/_customer\.html\.erb/, @logger.logged(:info).last) +      assert_match(/Rendered collection of customers\/_customer\.html\.erb \[2 times\]/, @logger.logged(:info).last)      end    end @@ -106,7 +106,21 @@ class AVLogSubscriberTest < ActiveSupport::TestCase        wait        assert_equal 1, @logger.logged(:info).size -      assert_match(/Rendered collection/, @logger.logged(:info).last) +      assert_match(/Rendered collection of templates/, @logger.logged(:info).last) +    end +  end + +  def test_render_collection_with_cached_set +    Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do +      def @view.view_cache_dependencies; []; end +      def @view.fragment_cache_key(*); 'ahoy `controller` dependency'; end + +      @view.render(partial: 'customers/customer', collection: [ Customer.new('david'), Customer.new('mary') ], cached: true, +        locals: { greeting: 'hi' }) +      wait + +      assert_equal 1, @logger.logged(:info).size +      assert_match(/Rendered collection of customers\/_customer\.html\.erb \[0 \/ 2 cache hits\]/, @logger.logged(:info).last)      end    end  end diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index bf811abdd0..3561114d90 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -628,56 +628,59 @@ class LazyViewRenderTest < ActiveSupport::TestCase    end  end -class CachedCollectionViewRenderTest < CachedViewRenderTest +class CachedCollectionViewRenderTest < ActiveSupport::TestCase    class CachedCustomer < Customer; end -  teardown do -    ActionView::PartialRenderer.collection_cache.clear -  end +  include RenderTestCases -  test "with custom key" do -    customer = Customer.new("david") -    key = cache_key([customer, 'key'], "test/_customer") +  # Ensure view path cache is primed +  setup do +    view_paths = ActionController::Base.view_paths +    assert_equal ActionView::OptimizedFileSystemResolver, view_paths.first.class -    ActionView::PartialRenderer.collection_cache.write(key, 'Hello') +    ActionView::PartialRenderer.collection_cache = ActiveSupport::Cache::MemoryStore.new -    assert_equal "Hello", -      @view.render(partial: "test/customer", collection: [customer], cache: ->(item) { [item, 'key'] }) +    setup_view(view_paths)    end -  test "with caching with custom key and rendering with different key" do -    customer = Customer.new("david") -    key = cache_key([customer, 'key'], "test/_customer") +  teardown do +    GC.start +    I18n.reload! +  end -    ActionView::PartialRenderer.collection_cache.write(key, 'Hello') +  test "collection caching does not cache by default" do +    customer = Customer.new("david", 1) +    key = cache_key(customer, "test/_customer") -    assert_equal "Hello: david", -      @view.render(partial: "test/customer", collection: [customer], cache: ->(item) { [item, 'another_key'] }) +    ActionView::PartialRenderer.collection_cache.write(key, 'Cached') + +    assert_not_equal "Cached", +      @view.render(partial: "test/customer", collection: [customer])    end -  test "automatic caching with inferred cache name" do -    customer = CachedCustomer.new("david") -    key = cache_key(customer, "test/_cached_customer") +  test "collection caching with partial that doesn't use fragment caching" do +    customer = Customer.new("david", 1) +    key = cache_key(customer, "test/_customer")      ActionView::PartialRenderer.collection_cache.write(key, 'Cached')      assert_equal "Cached", -      @view.render(partial: "test/cached_customer", collection: [customer]) +      @view.render(partial: "test/customer", collection: [customer], cached: true)    end -  test "automatic caching with as name" do -    customer = CachedCustomer.new("david") -    key = cache_key(customer, "test/_cached_customer_as") +  test "collection caching with cached true" do +    customer = CachedCustomer.new("david", 1) +    key = cache_key(customer, "test/_cached_customer")      ActionView::PartialRenderer.collection_cache.write(key, 'Cached')      assert_equal "Cached", -      @view.render(partial: "test/cached_customer_as", collection: [customer], as: :buyer) +      @view.render(partial: "test/cached_customer", collection: [customer], cached: true)    end    private -    def cache_key(names, virtual_path) +    def cache_key(*names, virtual_path)        digest = ActionView::Digestor.digest name: virtual_path, finder: @view.lookup_context, dependencies: [] -      @view.fragment_cache_key([ *Array(names), digest ]) +      @view.fragment_cache_key([ *names, digest ])      end  end diff --git a/actionview/test/template/template_test.rb b/actionview/test/template/template_test.rb index 921011b073..533c1c3219 100644 --- a/actionview/test/template/template_test.rb +++ b/actionview/test/template/template_test.rb @@ -192,38 +192,6 @@ class TestERBTemplate < ActiveSupport::TestCase      assert_match(/\xFC/, e.message)    end -  def test_not_eligible_for_collection_caching_without_cache_call -    [ -      "<%= 'Hello' %>", -      "<% cache_customer = 42 %>", -      "<% cache customer.name do %><% end %>", -      "<% my_cache customer do %><% end %>" -    ].each do |body| -      template = new_template(body, virtual_path: "test/foo/_customer") -      assert_not template.eligible_for_collection_caching?, "Template #{body.inspect} should not be eligible for collection caching" -    end -  end - -  def test_eligible_for_collection_caching_with_cache_call_or_explicit -    [ -      "<% cache customer do %><% end %>", -      "<% cache(customer) do %><% end %>", -      "<% cache( customer) do %><% end %>", -      "<% cache( customer ) do %><% end %>", -      "<%cache customer do %><% end %>", -      "<%   cache   customer    do %><% end %>", -      "  <% cache customer do %>\n<% end %>\n", -      "<%# comment %><% cache customer do %><% end %>", -      "<%# comment %>\n<% cache customer do %><% end %>", -      "<%# comment\n line 2\n line 3 %>\n<% cache customer do %><% end %>", -      "<%# comment 1 %>\n<%# comment 2 %>\n<% cache customer do %><% end %>", -      "<%# comment 1 %>\n<%# Template Collection: customer %>\n<% my_cache customer do %><% end %>" -    ].each do |body| -      template = new_template(body, virtual_path: "test/foo/_customer") -      assert template.eligible_for_collection_caching?, "Template #{body.inspect} should be eligible for collection caching" -    end -  end -    def with_external_encoding(encoding)      old = Encoding.default_external      Encoding::Converter.new old, encoding if old != encoding diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index 1d2888a818..21339b628e 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -167,6 +167,13 @@ module ActiveModel      def should_validate?(record) # :nodoc:        !record.persisted? || record.changed? || record.marked_for_destruction?      end + +    # Always validate the record if the attribute is a virtual attribute. +    # We have no way of knowing that the record was changed if the attribute +    # does not exist in the database. +    def unknown_attribute?(record, attribute) # :nodoc: +      !record.attributes.include?(attribute.to_s) +    end    end    # +BlockValidator+ is a special +EachValidator+ which receives a block on initialization diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 641e9195c6..c18403865f 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,11 @@ +*   Ensure that mutations of the array returned from `ActiveRecord::Relation#to_a` +    do not affect the original relation, by returning a duplicate array each time. + +    This brings the behavior in line with `CollectionProxy#to_a`, which was +    already more careful. + +    *Matthew Draper* +  *   Fixed `where` for polymorphic associations when passed an array containing different types.      Fixes #17011. diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index b888148841..5fbd79d118 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -76,6 +76,9 @@ module ActiveRecord::Associations::Builder # :nodoc:            left_model.retrieve_connection          end +        def self.primary_key +          false +        end        }        join_model.name                = "HABTM_#{association_name.to_s.camelize}" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index 6aa264d766..6f2e03b370 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -118,7 +118,7 @@ module ActiveRecord          alias :exec_update :exec_delete          def sql_for_insert(sql, pk, id_value, sequence_name, binds) # :nodoc: -          unless pk +          if pk.nil?              # Extract the table from the insert sql. Yuck.              table_ref = extract_table_ref_from_insert_sql(sql)              pk = primary_key(table_ref) if table_ref diff --git a/activerecord/lib/active_record/validations/absence.rb b/activerecord/lib/active_record/validations/absence.rb index 2e19e6dc5c..376d743c92 100644 --- a/activerecord/lib/active_record/validations/absence.rb +++ b/activerecord/lib/active_record/validations/absence.rb @@ -2,7 +2,7 @@ module ActiveRecord    module Validations      class AbsenceValidator < ActiveModel::Validations::AbsenceValidator # :nodoc:        def validate_each(record, attribute, association_or_value) -        return unless should_validate?(record) +        return unless should_validate?(record) || unknown_attribute?(record, attribute)          if record.class._reflect_on_association(attribute)            association_or_value = Array.wrap(association_or_value).reject(&:marked_for_destruction?)          end diff --git a/activerecord/lib/active_record/validations/length.rb b/activerecord/lib/active_record/validations/length.rb index 69e048eef1..fe34e4875c 100644 --- a/activerecord/lib/active_record/validations/length.rb +++ b/activerecord/lib/active_record/validations/length.rb @@ -2,7 +2,7 @@ module ActiveRecord    module Validations      class LengthValidator < ActiveModel::Validations::LengthValidator # :nodoc:        def validate_each(record, attribute, association_or_value) -        return unless should_validate?(record) || associations_are_dirty?(record) +        return unless should_validate?(record) || unknown_attribute?(record, attribute) || associations_are_dirty?(record)          if association_or_value.respond_to?(:loaded?) && association_or_value.loaded?            association_or_value = association_or_value.target.reject(&:marked_for_destruction?)          end diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index 7e85ed43ac..e34d2d70ab 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -2,7 +2,7 @@ module ActiveRecord    module Validations      class PresenceValidator < ActiveModel::Validations::PresenceValidator # :nodoc:        def validate_each(record, attribute, association_or_value) -        return unless should_validate?(record) +        return unless should_validate?(record) || unknown_attribute?(record, attribute)          if record.class._reflect_on_association(attribute)            association_or_value = Array.wrap(association_or_value).reject(&:marked_for_destruction?)          end diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index 5c4586da19..9096cbc0ab 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -146,6 +146,19 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase      assert_equal 1, country.treaties.count    end +  def test_join_table_composite_primary_key_should_not_warn +    country = Country.new(:name => 'India') +    country.country_id = 'c1' +    country.save! + +    treaty = Treaty.new(:name => 'peace') +    treaty.treaty_id = 't1' +    warning = capture(:stderr) do +      country.treaties << treaty +    end +    assert_no_match(/WARNING: Rails does not support composite primary key\./, warning) +  end +    def test_has_and_belongs_to_many      david = Developer.find(1) diff --git a/activerecord/test/cases/validations/absence_validation_test.rb b/activerecord/test/cases/validations/absence_validation_test.rb index dd43ee358c..180acbcb6a 100644 --- a/activerecord/test/cases/validations/absence_validation_test.rb +++ b/activerecord/test/cases/validations/absence_validation_test.rb @@ -72,4 +72,18 @@ class AbsenceValidationTest < ActiveRecord::TestCase        assert man.valid?      end    end + +  def test_validates_absence_of_virtual_attribute_on_model +    repair_validations(Interest) do +      Interest.send(:attr_accessor, :token) +      Interest.validates_absence_of(:token) + +      interest = Interest.create!(topic: 'Thought Leadering') +      assert interest.valid? + +      interest.token = 'tl' + +      assert interest.invalid? +    end +  end  end diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb index c5d8f8895c..624aeaaddb 100644 --- a/activerecord/test/cases/validations/length_validation_test.rb +++ b/activerecord/test/cases/validations/length_validation_test.rb @@ -74,4 +74,20 @@ class LengthValidationTest < ActiveRecord::TestCase      assert owner.valid?      assert pet.valid?    end + +  def test_validates_presence_of_virtual_attribute_on_model +    repair_validations(Pet) do +      Pet.send(:attr_accessor, :nickname) +      Pet.validates_length_of(:name, minimum: 1) +      Pet.validates_length_of(:nickname, minimum: 1) + +      pet = Pet.create!(name: 'Fancy Pants', nickname: 'Fancy') + +      assert pet.valid? + +      pet.nickname = '' + +      assert pet.invalid? +    end +  end  end diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb index 6f8ad06ab6..691f10a635 100644 --- a/activerecord/test/cases/validations/presence_validation_test.rb +++ b/activerecord/test/cases/validations/presence_validation_test.rb @@ -80,4 +80,19 @@ class PresenceValidationTest < ActiveRecord::TestCase        assert man.valid?      end    end + +  def test_validates_presence_of_virtual_attribute_on_model +    repair_validations(Interest) do +      Interest.send(:attr_accessor, :abbreviation) +      Interest.validates_presence_of(:topic) +      Interest.validates_presence_of(:abbreviation) + +      interest = Interest.create!(topic: 'Thought Leadering', abbreviation: 'tl') +      assert interest.valid? + +      interest.abbreviation = '' + +      assert interest.invalid? +    end +  end  end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index b9e0706d60..2a8996f35c 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -929,7 +929,7 @@ ActiveRecord::Schema.define do      t.string :treaty_id      t.string :name    end -  create_table :countries_treaties, force: true, id: false do |t| +  create_table :countries_treaties, force: true, primary_key: [:country_id, :treaty_id] do |t|      t.string :country_id, null: false      t.string :treaty_id, null: false    end diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index f4c324803c..db8d279cff 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,7 @@ +*   Make `benchmark('something', silence: true)` actually work + +    *DHH* +  *   Add `#on_weekday?` method to `Date`, `Time`, and `DateTime`.      `#on_weekday?` returns `true` if the receiving date/time does not fall on a Saturday diff --git a/activesupport/lib/active_support/benchmarkable.rb b/activesupport/lib/active_support/benchmarkable.rb index 805b7a714f..3988b147ac 100644 --- a/activesupport/lib/active_support/benchmarkable.rb +++ b/activesupport/lib/active_support/benchmarkable.rb @@ -38,7 +38,7 @@ module ActiveSupport          options[:level] ||= :info          result = nil -        ms = Benchmark.ms { result = options[:silence] ? silence { yield } : yield } +        ms = Benchmark.ms { result = options[:silence] ? logger.silence { yield } : yield }          logger.send(options[:level], '%s (%.1fms)' % [ message, ms ])          result        else diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 610105f41c..1c63e8a93f 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -333,21 +333,19 @@ module ActiveSupport          options = names.extract_options!          options = merged_options(options) -        instrument_multi(:read, names, options) do |payload| -          results = {} -          names.each do |name| -            key = normalize_key(name, options) -            entry = read_entry(key, options) -            if entry -              if entry.expired? -                delete_entry(key, options) -              else -                results[name] = entry.value -              end +        results = {} +        names.each do |name| +          key = normalize_key(name, options) +          entry = read_entry(key, options) +          if entry +            if entry.expired? +              delete_entry(key, options) +            else +              results[name] = entry.value              end            end -          results          end +        results        end        # Fetches data from the cache, using the given keys. If there is data in @@ -555,17 +553,6 @@ module ActiveSupport            ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) }          end -        def instrument_multi(operation, keys, options = nil) -          log do -            formatted_keys = keys.map { |k| "- #{k}" }.join("\n") -            "Caches multi #{operation}:\n#{formatted_keys}#{options.blank? ? "" : " (#{options.inspect})"}" -          end - -          payload = { key: keys } -          payload.merge!(options) if options.is_a?(Hash) -          ActiveSupport::Notifications.instrument("cache_#{operation}_multi.active_support", payload) { yield(payload) } -        end -          def log            return unless logger && logger.debug? && !silence?            logger.debug(yield) diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb index 174913365a..2ca4b51efa 100644 --- a/activesupport/lib/active_support/cache/mem_cache_store.rb +++ b/activesupport/lib/active_support/cache/mem_cache_store.rb @@ -96,16 +96,14 @@ module ActiveSupport          options = names.extract_options!          options = merged_options(options) -        instrument_multi(:read, names, options) do -          keys_to_names = Hash[names.map{|name| [normalize_key(name, options), name]}] -          raw_values = @data.get_multi(keys_to_names.keys, :raw => true) -          values = {} -          raw_values.each do |key, value| -            entry = deserialize_entry(value) -            values[keys_to_names[key]] = entry.value unless entry.expired? -          end -          values +        keys_to_names = Hash[names.map{|name| [normalize_key(name, options), name]}] +        raw_values = @data.get_multi(keys_to_names.keys, :raw => true) +        values = {} +        raw_values.each do |key, value| +          entry = deserialize_entry(value) +          values[keys_to_names[key]] = entry.value unless entry.expired?          end +        values        end        # Increment a cached value. This method uses the memcached incr atomic diff --git a/activesupport/test/benchmarkable_test.rb b/activesupport/test/benchmarkable_test.rb index 04d4f5e503..5af041f458 100644 --- a/activesupport/test/benchmarkable_test.rb +++ b/activesupport/test/benchmarkable_test.rb @@ -41,6 +41,20 @@ class BenchmarkableTest < ActiveSupport::TestCase      assert_last_logged 'test_run'    end +  def test_with_silence +    assert_difference 'buffer.count', +2 do +      benchmark('test_run') do +        logger.info "SOMETHING" +      end +    end + +    assert_difference 'buffer.count', +1 do +      benchmark('test_run', silence: true) do +        logger.info "NOTHING" +      end +    end +  end +    def test_within_level      logger.level = ActiveSupport::Logger::DEBUG      benchmark('included_debug_run', :level => :debug) { } diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index 4a299429f3..39fa3cedbe 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -1151,15 +1151,6 @@ class CacheStoreLoggerTest < ActiveSupport::TestCase      @cache.mute { @cache.fetch('foo') { 'bar' } }      assert @buffer.string.blank?    end - -  def test_multi_read_loggin -    @cache.write 'hello', 'goodbye' -    @cache.write 'world', 'earth' - -    @cache.read_multi('hello', 'world') - -    assert_match "Caches multi read:\n- hello\n- world", @buffer.string -  end  end  class CacheEntryTest < ActiveSupport::TestCase diff --git a/railties/test/application/per_request_digest_cache_test.rb b/railties/test/application/per_request_digest_cache_test.rb index 210646c7c0..dfe3fc9354 100644 --- a/railties/test/application/per_request_digest_cache_test.rb +++ b/railties/test/application/per_request_digest_cache_test.rb @@ -29,6 +29,8 @@ class PerRequestDigestCacheTest < ActiveSupport::TestCase      app_file 'app/controllers/customers_controller.rb', <<-RUBY        class CustomersController < ApplicationController +        self.perform_caching = true +          def index            render [ Customer.new('david', 1), Customer.new('dingus', 2) ]          end  | 
