diff options
-rw-r--r-- | actioncable/lib/action_cable/channel/test_case.rb | 8 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/mapper.rb | 14 | ||||
-rw-r--r-- | actionpack/test/dispatch/routing_test.rb | 15 | ||||
-rw-r--r-- | activejob/CHANGELOG.md | 6 | ||||
-rw-r--r-- | activejob/lib/active_job/test_helper.rb | 50 | ||||
-rw-r--r-- | activejob/test/cases/test_helper_test.rb | 46 | ||||
-rw-r--r-- | activerecord/lib/active_record/associations.rb | 2 | ||||
-rw-r--r-- | activerecord/lib/active_record/attribute_methods.rb | 4 | ||||
-rw-r--r-- | activerecord/lib/active_record/relation.rb | 23 | ||||
-rw-r--r-- | activerecord/lib/arel.rb | 5 | ||||
-rw-r--r-- | activerecord/lib/arel/factory_methods.rb | 4 | ||||
-rw-r--r-- | activerecord/lib/arel/nodes/node.rb | 10 | ||||
-rw-r--r-- | activerecord/lib/arel/visitors/to_sql.rb | 4 |
13 files changed, 156 insertions, 35 deletions
diff --git a/actioncable/lib/action_cable/channel/test_case.rb b/actioncable/lib/action_cable/channel/test_case.rb index 88d7c7092b..a26051d687 100644 --- a/actioncable/lib/action_cable/channel/test_case.rb +++ b/actioncable/lib/action_cable/channel/test_case.rb @@ -215,13 +215,9 @@ module ActionCable # Subsribe to the channel under test. Optionally pass subscription parameters as a Hash. def subscribe(params = {}) @connection ||= stub_connection - # NOTE: Rails < 5.0.1 calls subscribe_to_channel during #initialize. - # We have to stub before it - @subscription = self.class.channel_class.allocate + @subscription = self.class.channel_class.new(connection, CHANNEL_IDENTIFIER, params.with_indifferent_access) @subscription.singleton_class.include(ChannelStub) - @subscription.send(:initialize, connection, CHANNEL_IDENTIFIER, params.with_indifferent_access) - # Call subscribe_to_channel if it's public (Rails 5.0.1+) - @subscription.subscribe_to_channel if ActionCable.gem_version >= Gem::Version.new("5.0.1") + @subscription.subscribe_to_channel @subscription end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 9b378d9031..8b5a39dcaf 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -50,7 +50,19 @@ module ActionDispatch private def constraint_args(constraint, request) - constraint.arity == 1 ? [request] : [request.path_parameters, request] + arity = if constraint.respond_to?(:arity) + constraint.arity + else + constraint.method(:call).arity + end + + if arity < 1 + [] + elsif arity == 1 + [request] + else + [request.path_parameters, request] + end end end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index fdfa21fccc..affc2d8497 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -115,6 +115,21 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal 301, status end + def test_accepts_a_constraint_object_responding_to_call + constraint = Class.new do + def call(*); true; end + def matches?(*); false; end + end + + draw do + get "/", to: "home#show", constraints: constraint.new + end + + assert_nothing_raised do + get "/" + end + end + def test_namespace_with_controller_segment assert_raise(ArgumentError) do draw do diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md index 57a62e963d..af5c197bac 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -1,3 +1,9 @@ +* Allow `assert_enqueued_with`/`assert_performed_with` methods to accept + a proc for the `args` argument. This is useful to check if only a subset of arguments + matches your expectations. + + *Edouard Chin* + * `ActionDispatch::IntegrationTest` includes `ActiveJob::TestHelper` module by default. *Ricardo Díaz* diff --git a/activejob/lib/active_job/test_helper.rb b/activejob/lib/active_job/test_helper.rb index 9efc8c0c12..e7a9b16277 100644 --- a/activejob/lib/active_job/test_helper.rb +++ b/activejob/lib/active_job/test_helper.rb @@ -331,6 +331,22 @@ module ActiveJob # assert_enqueued_with(job: MyJob, at: Date.tomorrow.noon) # end # + # + # The +args+ argument also accepts a proc which will get passed the actual + # job's arguments. Your proc needs to returns a boolean value determining if + # the job's arguments matches your expectation. This is useful to check only + # for a subset of arguments. + # + # def test_assert_enqueued_with + # expected_args = ->(job_args) do + # assert job_args.first.key?(:foo) + # end + # + # MyJob.perform_later(foo: 'bar', other_arg: 'No need to check in the test') + # assert_enqueued_with(job: MyJob, args: expected_args, queue: 'low') + # end + # + # # If a block is passed, that block should cause the job to be # enqueued with the given arguments. # @@ -359,7 +375,14 @@ module ActiveJob matching_job = jobs.find do |enqueued_job| deserialized_job = deserialize_args_for_assertion(enqueued_job) - expected_args.all? { |key, value| value == deserialized_job[key] } + + expected_args.all? do |key, value| + if value.respond_to?(:call) + value.call(deserialized_job[key]) + else + value == deserialized_job[key] + end + end end assert matching_job, "No enqueued job found with #{expected}" @@ -382,6 +405,22 @@ module ActiveJob # assert_performed_with(job: MyJob, at: Date.tomorrow.noon) # end # + # The +args+ argument also accepts a proc which will get passed the actual + # job's arguments. Your proc needs to returns a boolean value determining if + # the job's arguments matches your expectation. This is useful to check only + # for a subset of arguments. + # + # def test_assert_performed_with + # expected_args = ->(job_args) do + # assert job_args.first.key?(:foo) + # end + # MyJob.perform_later(foo: 'bar', other_arg: 'No need to check in the test') + # + # perform_enqueued_jobs + # + # assert_performed_with(job: MyJob, args: expected_args, queue: 'high') + # end + # # If a block is passed, that block performs all of the jobs that were # enqueued throughout the duration of the block and asserts that # the job has been performed with the given arguments in the block. @@ -411,7 +450,14 @@ module ActiveJob matching_job = jobs.find do |enqueued_job| deserialized_job = deserialize_args_for_assertion(enqueued_job) - expected_args.all? { |key, value| value == deserialized_job[key] } + + expected_args.all? do |key, value| + if value.respond_to?(:call) + value.call(deserialized_job[key]) + else + value == deserialized_job[key] + end + end end assert matching_job, "No performed job found with #{expected}" diff --git a/activejob/test/cases/test_helper_test.rb b/activejob/test/cases/test_helper_test.rb index 018c40c28f..83c71ab1c4 100644 --- a/activejob/test/cases/test_helper_test.rb +++ b/activejob/test/cases/test_helper_test.rb @@ -538,6 +538,29 @@ class EnqueuedJobsTest < ActiveJob::TestCase end end + def test_assert_enqueued_with_selective_args + args = ->(job_args) do + assert_equal 1, job_args.first[:argument1] + assert job_args.first[:argument2].key?(:b) + end + + assert_enqueued_with(job: MultipleKwargsJob, args: args) do + MultipleKwargsJob.perform_later(argument2: { b: 2, a: 1 }, argument1: 1) + end + end + + def test_assert_enqueued_with_selective_args_fails + args = ->(job_args) do + false + end + + assert_raise ActiveSupport::TestCase::Assertion do + assert_enqueued_with(job: MultipleKwargsJob, args: args) do + MultipleKwargsJob.perform_later(argument2: { b: 2, a: 1 }, argument1: 1) + end + end + end + def test_assert_enqueued_with_with_no_block_args assert_raise ArgumentError do NestedJob.set(wait_until: Date.tomorrow.noon).perform_later @@ -1579,6 +1602,29 @@ class PerformedJobsTest < ActiveJob::TestCase end end + def test_assert_performed_with_selective_args + args = ->(job_args) do + assert_equal 1, job_args.first[:argument1] + assert job_args.first[:argument2].key?(:b) + end + + assert_performed_with(job: MultipleKwargsJob, args: args) do + MultipleKwargsJob.perform_later(argument2: { b: 2, a: 1 }, argument1: 1) + end + end + + def test_assert_performed_with_selective_args_fails + args = ->(job_args) do + false + end + + assert_raise ActiveSupport::TestCase::Assertion do + assert_performed_with(job: MultipleKwargsJob, args: args) do + MultipleKwargsJob.perform_later(argument2: { b: 2, a: 1 }, argument1: 1) + end + end + end + def test_assert_performed_with_with_global_id_args ricardo = Person.new(9) assert_performed_with(job: HelloJob, args: [ricardo]) do diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index b1778732dd..ab1e7ad269 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1582,7 +1582,7 @@ module ActiveRecord # association will use "taggable_type" as the default <tt>:foreign_type</tt>. # [:primary_key] # Specify the method that returns the primary key of associated object used for the association. - # By default this is id. + # By default this is +id+. # [:dependent] # If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 701f19a6ae..42eba27b2a 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -194,9 +194,7 @@ module ActiveRecord def disallow_raw_sql!(args, permit: COLUMN_NAME) # :nodoc: unexpected = args.reject do |arg| - arg.kind_of?(Arel::Node) || - arg.is_a?(Arel::Nodes::SqlLiteral) || - arg.is_a?(Arel::Attributes::Attribute) || + Arel.arel_node?(arg) || arg.to_s.split(/\s*,\s*/).all? { |part| permit.match?(part) } end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 771ca952e1..db06bd9e26 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -379,19 +379,22 @@ module ActiveRecord def update_counters(counters) # :nodoc: touch = counters.delete(:touch) - updates = counters.map do |counter_name, value| - operator = value < 0 ? "-" : "+" - quoted_column = connection.quote_table_name_for_assignment(table.name, counter_name) - "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}" + updates = {} + counters.each do |counter_name, value| + attr = arel_attribute(counter_name) + bind = predicate_builder.build_bind_attribute(attr.name, value.abs) + expr = table.coalesce(Arel::Nodes::UnqualifiedColumn.new(attr), 0) + expr = value < 0 ? expr - bind : expr + bind + updates[counter_name] = expr.expr end if touch names = touch if touch != true touch_updates = klass.touch_attributes_with_time(*names) - updates << klass.sanitize_sql_for_assignment(touch_updates, table.name) unless touch_updates.empty? + updates.merge!(touch_updates) unless touch_updates.empty? end - update_all updates.join(", ") + update_all updates end # Touches all records in the current relation without instantiating records first with the updated_at/on attributes @@ -632,9 +635,11 @@ module ActiveRecord def _substitute_values(values) values.map do |name, value| attr = arel_attribute(name) - type = klass.type_for_attribute(attr.name) - bind = predicate_builder.build_bind_attribute(attr.name, type.cast(value)) - [attr, bind] + unless Arel.arel_node?(value) + type = klass.type_for_attribute(attr.name) + value = predicate_builder.build_bind_attribute(attr.name, type.cast(value)) + end + [attr, value] end end diff --git a/activerecord/lib/arel.rb b/activerecord/lib/arel.rb index 7d04e1cac6..dab785738e 100644 --- a/activerecord/lib/arel.rb +++ b/activerecord/lib/arel.rb @@ -35,6 +35,11 @@ module Arel # :nodoc: all def self.star sql "*" end + + def self.arel_node?(value) + value.is_a?(Arel::Node) || value.is_a?(Arel::Attribute) || value.is_a?(Arel::Nodes::SqlLiteral) + end + ## Convenience Alias Node = Arel::Nodes::Node end diff --git a/activerecord/lib/arel/factory_methods.rb b/activerecord/lib/arel/factory_methods.rb index b828bc274e..83ec23e403 100644 --- a/activerecord/lib/arel/factory_methods.rb +++ b/activerecord/lib/arel/factory_methods.rb @@ -41,5 +41,9 @@ module Arel # :nodoc: all def lower(column) Nodes::NamedFunction.new "LOWER", [Nodes.build_quoted(column)] end + + def coalesce(*exprs) + Nodes::NamedFunction.new "COALESCE", exprs + end end end diff --git a/activerecord/lib/arel/nodes/node.rb b/activerecord/lib/arel/nodes/node.rb index 2b9b1e9828..8086102bde 100644 --- a/activerecord/lib/arel/nodes/node.rb +++ b/activerecord/lib/arel/nodes/node.rb @@ -8,16 +8,6 @@ module Arel # :nodoc: all include Arel::FactoryMethods include Enumerable - if $DEBUG - def _caller - @caller - end - - def initialize - @caller = caller.dup - end - end - ### # Factory method to create a Nodes::Not node that has the recipient of # the caller as a child. diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb index 81ca63f261..1ed41ff577 100644 --- a/activerecord/lib/arel/visitors/to_sql.rb +++ b/activerecord/lib/arel/visitors/to_sql.rb @@ -637,7 +637,7 @@ module Arel # :nodoc: all def visit_Arel_Nodes_Assignment(o, collector) case o.right - when Arel::Nodes::UnqualifiedColumn, Arel::Attributes::Attribute, Arel::Nodes::BindParam + when Arel::Nodes::Node, Arel::Attributes::Attribute collector = visit o.left, collector collector << " = " visit o.right, collector @@ -732,8 +732,6 @@ module Arel # :nodoc: all end alias :visit_Arel_Nodes_SqlLiteral :literal - alias :visit_Bignum :literal - alias :visit_Fixnum :literal alias :visit_Integer :literal def quoted(o, a) |