diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f78e0b26..7fd1e8544 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,11 +12,11 @@ jobs: strategy: fail-fast: false matrix: - ruby: [2.5, 2.6, 2.7, jruby, jruby-head, ruby-head] + ruby: [2.5, 2.6, 2.7, 3.0, jruby, jruby-head, ruby-head] rails_version: - - '5.2.0' + - '5.2.5' - '6.0.0' - - '6.1.0.rc2' + - '6.1.0' - 'edge' include: # diff --git a/Gemfile b/Gemfile index b83acb5bc..0bd41d117 100644 --- a/Gemfile +++ b/Gemfile @@ -27,7 +27,7 @@ platforms :jruby do elsif ENV['RAILS_VERSION'] gem 'railties', "~> #{ENV['RAILS_VERSION']}" else - gem 'railties', ['>= 3.0', '< 6.2'] + gem 'railties', ['>= 3.0', '< 7.1'] end end @@ -43,8 +43,8 @@ group :test do gem 'actionmailer', "~> #{ENV['RAILS_VERSION']}" gem 'activerecord', "~> #{ENV['RAILS_VERSION']}" else - gem 'actionmailer', ['>= 3.0', '< 6.2'] - gem 'activerecord', ['>= 3.0', '< 6.2'] + gem 'actionmailer', ['>= 3.0', '< 7.1'] + gem 'activerecord', ['>= 3.0', '< 7.1'] end gem 'rspec', '>= 3' diff --git a/delayed_job.gemspec b/delayed_job.gemspec index c9b60a781..23eec33e6 100644 --- a/delayed_job.gemspec +++ b/delayed_job.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |spec| - spec.add_dependency 'activesupport', ['>= 3.0', '< 6.2'] + spec.add_dependency 'activesupport', ['>= 3.0', '< 7.1'] spec.authors = ['Brandon Keepers', 'Brian Ryckbost', 'Chris Gaffney', 'David Genord II', 'Erik Michaels-Ober', 'Matt Griffin', 'Steve Richert', 'Tobias Lütke'] spec.description = 'Delayed_job (or DJ) encapsulates the common pattern of asynchronously executing longer tasks in the background. It is a direct extraction from Shopify where the job table is responsible for a multitude of core tasks.' spec.email = ['brian@collectiveidea.com'] diff --git a/lib/delayed/performable_mailer.rb b/lib/delayed/performable_mailer.rb index 8535c452d..7c5d6d7b0 100644 --- a/lib/delayed/performable_mailer.rb +++ b/lib/delayed/performable_mailer.rb @@ -3,7 +3,7 @@ module Delayed class PerformableMailer < PerformableMethod def perform - mailer = object.send(method_name, *args) + mailer = super mailer.respond_to?(:deliver_now) ? mailer.deliver_now : mailer.deliver end end diff --git a/lib/delayed/performable_method.rb b/lib/delayed/performable_method.rb index 96a28b056..9f64a6167 100644 --- a/lib/delayed/performable_method.rb +++ b/lib/delayed/performable_method.rb @@ -22,18 +22,43 @@ def display_name end end - def perform - object.send(method_name, *args) if object + # required to support named parameters in RUBY 3.0 + # Otherwise the following error is thrown + # ArgumentError: + # wrong number of arguments (given 1, expected 0; required keywords: + if RUBY_VERSION >= '3.0' + def perform + return unless object + + if args_is_a_hash? + object.send(method_name, **args.first) + else + object.send(method_name, *args) + end + end + + def args_is_a_hash? + args.size == 1 && args.first.is_a?(Hash) + end + else + def perform + object.send(method_name, *args) if object + end end def method(sym) object.method(sym) end - - # rubocop:disable MethodMissing - def method_missing(symbol, *args) - object.send(symbol, *args) - end + method_def = [] + location = caller_locations(1, 1).first + file = location.path + line = location.lineno + definition = RUBY_VERSION >= '3.0' ? '...' : '*args, &block' + method_def << + "def method_missing(#{definition})" \ + " object.send(#{definition})" \ + 'end' + module_eval(method_def.join(';'), file, line) # rubocop:enable MethodMissing def respond_to?(symbol, include_private = false) diff --git a/spec/performable_method_spec.rb b/spec/performable_method_spec.rb index a4d700322..17179c676 100644 --- a/spec/performable_method_spec.rb +++ b/spec/performable_method_spec.rb @@ -1,4 +1,5 @@ require 'helper' +require 'action_controller/metal/strong_parameters' if ActionPack::VERSION::MAJOR >= 5 describe Delayed::PerformableMethod do describe 'perform' do @@ -22,6 +23,106 @@ end end + describe 'perform with hash object' do + before do + @method = Delayed::PerformableMethod.new('foo', :count, [{:o => true}]) + end + + it 'calls the method on the object' do + expect(@method.object).to receive(:count).with(:o => true) + @method.perform + end + end + + describe 'perform with many hash objects' do + before do + @method = Delayed::PerformableMethod.new('foo', :count, [{:o => true}, {:o2 => true}]) + end + + it 'calls the method on the object' do + expect(@method.object).to receive(:count).with({:o => true}, :o2 => true) + @method.perform + end + end + + if ActionPack::VERSION::MAJOR >= 5 + describe 'perform with params object' do + before do + @params = ActionController::Parameters.new(:person => { + :name => 'Francesco', + :age => 22, + :role => 'admin' + }) + + @method = Delayed::PerformableMethod.new('foo', :count, [@params]) + end + + it 'calls the method on the object' do + expect(@method.object).to receive(:count).with(@params) + @method.perform + end + end + + describe 'perform with sample object and params object' do + before do + @params = ActionController::Parameters.new(:person => { + :name => 'Francesco', + :age => 22, + :role => 'admin' + }) + + klass = Class.new do + def test_method(_o1, _o2) + true + end + end + + @method = Delayed::PerformableMethod.new(klass.new, :test_method, ['o', @params]) + end + + it 'calls the method on the object' do + expect(@method.object).to receive(:test_method).with('o', @params) + @method.perform + end + + it 'calls the method on the object (real)' do + expect(@method.perform).to be true + end + end + end + + describe 'perform with sample object and hash object' do + before do + @method = Delayed::PerformableMethod.new('foo', :count, ['o', {:o => true}]) + end + + it 'calls the method on the object' do + expect(@method.object).to receive(:count).with('o', :o => true) + @method.perform + end + end + + describe 'perform with hash to named parameters' do + before do + klass = Class.new do + def test_method(name:, any:) + true if name && any + end + end + + @method = Delayed::PerformableMethod.new(klass.new, :test_method, [{:name => 'name', :any => 'any'}]) + end + + it 'calls the method on the object' do + expect(@method.object).to receive(:test_method).with(:name => 'name', :any => 'any') + @method.perform + end + + it 'calls the method on the object (real)' do + expect(@method.perform).to be true + end + end + it "raises a NoMethodError if target method doesn't exist" do expect do Delayed::PerformableMethod.new(Object, :method_that_does_not_exist, [])