activejob 5.2.3 → 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +112 -33
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +17 -10
  5. data/lib/active_job.rb +2 -1
  6. data/lib/active_job/arguments.rb +40 -28
  7. data/lib/active_job/base.rb +3 -1
  8. data/lib/active_job/callbacks.rb +4 -1
  9. data/lib/active_job/core.rb +38 -21
  10. data/lib/active_job/enqueuing.rb +26 -5
  11. data/lib/active_job/exceptions.rb +40 -17
  12. data/lib/active_job/execution.rb +1 -1
  13. data/lib/active_job/gem_version.rb +3 -3
  14. data/lib/active_job/logging.rb +40 -9
  15. data/lib/active_job/queue_adapter.rb +2 -0
  16. data/lib/active_job/queue_adapters.rb +8 -10
  17. data/lib/active_job/queue_adapters/async_adapter.rb +1 -1
  18. data/lib/active_job/queue_adapters/backburner_adapter.rb +2 -2
  19. data/lib/active_job/queue_adapters/inline_adapter.rb +1 -1
  20. data/lib/active_job/queue_adapters/test_adapter.rb +22 -8
  21. data/lib/active_job/queue_name.rb +21 -1
  22. data/lib/active_job/railtie.rb +16 -1
  23. data/lib/active_job/serializers.rb +63 -0
  24. data/lib/active_job/serializers/date_serializer.rb +21 -0
  25. data/lib/active_job/serializers/date_time_serializer.rb +21 -0
  26. data/lib/active_job/serializers/duration_serializer.rb +24 -0
  27. data/lib/active_job/serializers/object_serializer.rb +54 -0
  28. data/lib/active_job/serializers/symbol_serializer.rb +21 -0
  29. data/lib/active_job/serializers/time_serializer.rb +21 -0
  30. data/lib/active_job/serializers/time_with_zone_serializer.rb +21 -0
  31. data/lib/active_job/test_helper.rb +278 -57
  32. data/lib/active_job/timezones.rb +13 -0
  33. data/lib/active_job/translation.rb +1 -1
  34. data/lib/rails/generators/job/job_generator.rb +4 -0
  35. metadata +22 -11
  36. data/lib/active_job/queue_adapters/qu_adapter.rb +0 -46
@@ -31,7 +31,7 @@ module ActiveJob
31
31
  # jobs. Since jobs share a single thread pool, long-running jobs will block
32
32
  # short-lived jobs. Fine for dev/test; bad for production.
33
33
  class AsyncAdapter
34
- # See {Concurrent::ThreadPoolExecutor}[https://ruby-concurrency.github.io/concurrent-ruby/Concurrent/ThreadPoolExecutor.html] for executor options.
34
+ # See {Concurrent::ThreadPoolExecutor}[https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/ThreadPoolExecutor.html] for executor options.
35
35
  def initialize(**executor_options)
36
36
  @scheduler = Scheduler.new(**executor_options)
37
37
  end
@@ -16,12 +16,12 @@ module ActiveJob
16
16
  # Rails.application.config.active_job.queue_adapter = :backburner
17
17
  class BackburnerAdapter
18
18
  def enqueue(job) #:nodoc:
19
- Backburner::Worker.enqueue JobWrapper, [ job.serialize ], queue: job.queue_name
19
+ Backburner::Worker.enqueue(JobWrapper, [job.serialize], queue: job.queue_name, pri: job.priority)
20
20
  end
21
21
 
22
22
  def enqueue_at(job, timestamp) #:nodoc:
23
23
  delay = timestamp - Time.current.to_f
24
- Backburner::Worker.enqueue JobWrapper, [ job.serialize ], queue: job.queue_name, delay: delay
24
+ Backburner::Worker.enqueue(JobWrapper, [job.serialize], queue: job.queue_name, pri: job.priority, delay: delay)
25
25
  end
26
26
 
27
27
  class JobWrapper #:nodoc:
@@ -16,7 +16,7 @@ module ActiveJob
16
16
  end
17
17
 
18
18
  def enqueue_at(*) #:nodoc:
19
- raise NotImplementedError, "Use a queueing backend to enqueue jobs in the future. Read more at http://guides.rubyonrails.org/active_job_basics.html"
19
+ raise NotImplementedError, "Use a queueing backend to enqueue jobs in the future. Read more at https://guides.rubyonrails.org/active_job_basics.html"
20
20
  end
21
21
  end
22
22
  end
@@ -12,7 +12,7 @@ module ActiveJob
12
12
  #
13
13
  # Rails.application.config.active_job.queue_adapter = :test
14
14
  class TestAdapter
15
- attr_accessor(:perform_enqueued_jobs, :perform_enqueued_at_jobs, :filter, :reject)
15
+ attr_accessor(:perform_enqueued_jobs, :perform_enqueued_at_jobs, :filter, :reject, :queue)
16
16
  attr_writer(:enqueued_jobs, :performed_jobs)
17
17
 
18
18
  # Provides a store of all the enqueued jobs with the TestAdapter so you can check them.
@@ -29,14 +29,14 @@ module ActiveJob
29
29
  return if filtered?(job)
30
30
 
31
31
  job_data = job_to_hash(job)
32
- enqueue_or_perform(perform_enqueued_jobs, job, job_data)
32
+ perform_or_enqueue(perform_enqueued_jobs, job, job_data)
33
33
  end
34
34
 
35
35
  def enqueue_at(job, timestamp) #:nodoc:
36
36
  return if filtered?(job)
37
37
 
38
38
  job_data = job_to_hash(job, at: timestamp)
39
- enqueue_or_perform(perform_enqueued_at_jobs, job, job_data)
39
+ perform_or_enqueue(perform_enqueued_at_jobs, job, job_data)
40
40
  end
41
41
 
42
42
  private
@@ -44,7 +44,7 @@ module ActiveJob
44
44
  { job: job.class, args: job.serialize.fetch("arguments"), queue: job.queue_name }.merge!(extras)
45
45
  end
46
46
 
47
- def enqueue_or_perform(perform, job, job_data)
47
+ def perform_or_enqueue(perform, job, job_data)
48
48
  if perform
49
49
  performed_jobs << job_data
50
50
  Base.execute job.serialize
@@ -54,14 +54,28 @@ module ActiveJob
54
54
  end
55
55
 
56
56
  def filtered?(job)
57
+ filtered_queue?(job) || filtered_job_class?(job)
58
+ end
59
+
60
+ def filtered_queue?(job)
61
+ if queue
62
+ job.queue_name != queue.to_s
63
+ end
64
+ end
65
+
66
+ def filtered_job_class?(job)
57
67
  if filter
58
- !Array(filter).include?(job.class)
68
+ !filter_as_proc(filter).call(job)
59
69
  elsif reject
60
- Array(reject).include?(job.class)
61
- else
62
- false
70
+ filter_as_proc(reject).call(job)
63
71
  end
64
72
  end
73
+
74
+ def filter_as_proc(filter)
75
+ return filter if filter.is_a?(Proc)
76
+
77
+ ->(job) { Array(filter).include?(job.class) }
78
+ end
65
79
  end
66
80
  end
67
81
  end
@@ -18,6 +18,26 @@ module ActiveJob
18
18
  # post.to_feed!
19
19
  # end
20
20
  # end
21
+ #
22
+ # Can be given a block that will evaluate in the context of the job
23
+ # allowing +self.arguments+ to be accessed so that a dynamic queue name
24
+ # can be applied:
25
+ #
26
+ # class PublishToFeedJob < ApplicationJob
27
+ # queue_as do
28
+ # post = self.arguments.first
29
+ #
30
+ # if post.paid?
31
+ # :paid_feeds
32
+ # else
33
+ # :feeds
34
+ # end
35
+ # end
36
+ #
37
+ # def perform(post)
38
+ # post.to_feed!
39
+ # end
40
+ # end
21
41
  def queue_as(part_name = nil, &block)
22
42
  if block_given?
23
43
  self.queue_name = block
@@ -34,7 +54,7 @@ module ActiveJob
34
54
  end
35
55
 
36
56
  included do
37
- class_attribute :queue_name, instance_accessor: false, default: default_queue_name
57
+ class_attribute :queue_name, instance_accessor: false, default: -> { self.class.default_queue_name }
38
58
  class_attribute :queue_name_delimiter, instance_accessor: false, default: "_"
39
59
  end
40
60
 
@@ -7,17 +7,32 @@ module ActiveJob
7
7
  # = Active Job Railtie
8
8
  class Railtie < Rails::Railtie # :nodoc:
9
9
  config.active_job = ActiveSupport::OrderedOptions.new
10
+ config.active_job.custom_serializers = []
10
11
 
11
12
  initializer "active_job.logger" do
12
13
  ActiveSupport.on_load(:active_job) { self.logger = ::Rails.logger }
13
14
  end
14
15
 
16
+ initializer "active_job.custom_serializers" do |app|
17
+ config.after_initialize do
18
+ custom_serializers = app.config.active_job.delete(:custom_serializers)
19
+ ActiveJob::Serializers.add_serializers custom_serializers
20
+ end
21
+ end
22
+
15
23
  initializer "active_job.set_configs" do |app|
16
24
  options = app.config.active_job
17
25
  options.queue_adapter ||= :async
18
26
 
19
27
  ActiveSupport.on_load(:active_job) do
20
- options.each { |k, v| send("#{k}=", v) }
28
+ options.each do |k, v|
29
+ k = "#{k}="
30
+ send(k, v) if respond_to? k
31
+ end
32
+ end
33
+
34
+ ActiveSupport.on_load(:action_dispatch_integration_test) do
35
+ include ActiveJob::TestHelper
21
36
  end
22
37
  end
23
38
 
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module ActiveJob
6
+ # The <tt>ActiveJob::Serializers</tt> module is used to store a list of known serializers
7
+ # and to add new ones. It also has helpers to serialize/deserialize objects.
8
+ module Serializers # :nodoc:
9
+ extend ActiveSupport::Autoload
10
+
11
+ autoload :ObjectSerializer
12
+ autoload :SymbolSerializer
13
+ autoload :DurationSerializer
14
+ autoload :DateTimeSerializer
15
+ autoload :DateSerializer
16
+ autoload :TimeWithZoneSerializer
17
+ autoload :TimeSerializer
18
+
19
+ mattr_accessor :_additional_serializers
20
+ self._additional_serializers = Set.new
21
+
22
+ class << self
23
+ # Returns serialized representative of the passed object.
24
+ # Will look up through all known serializers.
25
+ # Raises <tt>ActiveJob::SerializationError</tt> if it can't find a proper serializer.
26
+ def serialize(argument)
27
+ serializer = serializers.detect { |s| s.serialize?(argument) }
28
+ raise SerializationError.new("Unsupported argument type: #{argument.class.name}") unless serializer
29
+ serializer.serialize(argument)
30
+ end
31
+
32
+ # Returns deserialized object.
33
+ # Will look up through all known serializers.
34
+ # If no serializer found will raise <tt>ArgumentError</tt>.
35
+ def deserialize(argument)
36
+ serializer_name = argument[Arguments::OBJECT_SERIALIZER_KEY]
37
+ raise ArgumentError, "Serializer name is not present in the argument: #{argument.inspect}" unless serializer_name
38
+
39
+ serializer = serializer_name.safe_constantize
40
+ raise ArgumentError, "Serializer #{serializer_name} is not known" unless serializer
41
+
42
+ serializer.deserialize(argument)
43
+ end
44
+
45
+ # Returns list of known serializers.
46
+ def serializers
47
+ self._additional_serializers
48
+ end
49
+
50
+ # Adds new serializers to a list of known serializers.
51
+ def add_serializers(*new_serializers)
52
+ self._additional_serializers += new_serializers.flatten
53
+ end
54
+ end
55
+
56
+ add_serializers SymbolSerializer,
57
+ DurationSerializer,
58
+ DateTimeSerializer,
59
+ DateSerializer,
60
+ TimeWithZoneSerializer,
61
+ TimeSerializer
62
+ end
63
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module Serializers
5
+ class DateSerializer < ObjectSerializer # :nodoc:
6
+ def serialize(date)
7
+ super("value" => date.iso8601)
8
+ end
9
+
10
+ def deserialize(hash)
11
+ Date.iso8601(hash["value"])
12
+ end
13
+
14
+ private
15
+
16
+ def klass
17
+ Date
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module Serializers
5
+ class DateTimeSerializer < ObjectSerializer # :nodoc:
6
+ def serialize(time)
7
+ super("value" => time.iso8601)
8
+ end
9
+
10
+ def deserialize(hash)
11
+ DateTime.iso8601(hash["value"])
12
+ end
13
+
14
+ private
15
+
16
+ def klass
17
+ DateTime
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module Serializers
5
+ class DurationSerializer < ObjectSerializer # :nodoc:
6
+ def serialize(duration)
7
+ super("value" => duration.value, "parts" => Arguments.serialize(duration.parts))
8
+ end
9
+
10
+ def deserialize(hash)
11
+ value = hash["value"]
12
+ parts = Arguments.deserialize(hash["parts"])
13
+
14
+ klass.new(value, parts)
15
+ end
16
+
17
+ private
18
+
19
+ def klass
20
+ ActiveSupport::Duration
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module Serializers
5
+ # Base class for serializing and deserializing custom objects.
6
+ #
7
+ # Example:
8
+ #
9
+ # class MoneySerializer < ActiveJob::Serializers::ObjectSerializer
10
+ # def serialize(money)
11
+ # super("amount" => money.amount, "currency" => money.currency)
12
+ # end
13
+ #
14
+ # def deserialize(hash)
15
+ # Money.new(hash["amount"], hash["currency"])
16
+ # end
17
+ #
18
+ # private
19
+ #
20
+ # def klass
21
+ # Money
22
+ # end
23
+ # end
24
+ class ObjectSerializer
25
+ include Singleton
26
+
27
+ class << self
28
+ delegate :serialize?, :serialize, :deserialize, to: :instance
29
+ end
30
+
31
+ # Determines if an argument should be serialized by a serializer.
32
+ def serialize?(argument)
33
+ argument.is_a?(klass)
34
+ end
35
+
36
+ # Serializes an argument to a JSON primitive type.
37
+ def serialize(hash)
38
+ { Arguments::OBJECT_SERIALIZER_KEY => self.class.name }.merge!(hash)
39
+ end
40
+
41
+ # Deserializes an argument from a JSON primitive type.
42
+ def deserialize(_argument)
43
+ raise NotImplementedError
44
+ end
45
+
46
+ private
47
+
48
+ # The class of the object that will be serialized.
49
+ def klass # :doc:
50
+ raise NotImplementedError
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module Serializers
5
+ class SymbolSerializer < ObjectSerializer # :nodoc:
6
+ def serialize(argument)
7
+ super("value" => argument.to_s)
8
+ end
9
+
10
+ def deserialize(argument)
11
+ argument["value"].to_sym
12
+ end
13
+
14
+ private
15
+
16
+ def klass
17
+ Symbol
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module Serializers
5
+ class TimeSerializer < ObjectSerializer # :nodoc:
6
+ def serialize(time)
7
+ super("value" => time.iso8601)
8
+ end
9
+
10
+ def deserialize(hash)
11
+ Time.iso8601(hash["value"])
12
+ end
13
+
14
+ private
15
+
16
+ def klass
17
+ Time
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module Serializers
5
+ class TimeWithZoneSerializer < ObjectSerializer # :nodoc:
6
+ def serialize(time)
7
+ super("value" => time.iso8601)
8
+ end
9
+
10
+ def deserialize(hash)
11
+ Time.iso8601(hash["value"]).in_time_zone
12
+ end
13
+
14
+ private
15
+
16
+ def klass
17
+ ActiveSupport::TimeWithZone
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/core_ext/class/subclasses"
4
- require "active_support/core_ext/hash/keys"
5
4
 
6
5
  module ActiveJob
7
6
  # Provides helper methods for testing Active Job
@@ -52,7 +51,7 @@ module ActiveJob
52
51
  queue_adapter_changed_jobs.each { |klass| klass.disable_test_adapter }
53
52
  end
54
53
 
55
- # Specifies the queue adapter to use with all active job test helpers.
54
+ # Specifies the queue adapter to use with all Active Job test helpers.
56
55
  #
57
56
  # Returns an instance of the queue adapter and defaults to
58
57
  # <tt>ActiveJob::QueueAdapters::TestAdapter</tt>.
@@ -75,7 +74,7 @@ module ActiveJob
75
74
  # assert_enqueued_jobs 2
76
75
  # end
77
76
  #
78
- # If a block is passed, that block will cause the specified number of
77
+ # If a block is passed, asserts that the block will cause the specified number of
79
78
  # jobs to be enqueued.
80
79
  #
81
80
  # def test_jobs_again
@@ -89,7 +88,7 @@ module ActiveJob
89
88
  # end
90
89
  # end
91
90
  #
92
- # The number of times a specific job was enqueued can be asserted.
91
+ # Asserts the number of times a specific job was enqueued by passing +:only+ option.
93
92
  #
94
93
  # def test_logging_job
95
94
  # assert_enqueued_jobs 1, only: LoggingJob do
@@ -98,7 +97,7 @@ module ActiveJob
98
97
  # end
99
98
  # end
100
99
  #
101
- # The number of times a job except specific class was enqueued can be asserted.
100
+ # Asserts the number of times a job except specific class was enqueued by passing +:except+ option.
102
101
  #
103
102
  # def test_logging_job
104
103
  # assert_enqueued_jobs 1, except: HelloJob do
@@ -107,7 +106,10 @@ module ActiveJob
107
106
  # end
108
107
  # end
109
108
  #
110
- # The number of times a job is enqueued to a specific queue can also be asserted.
109
+ # +:only+ and +:except+ options accepts Class, Array of Class or Proc. When passed a Proc,
110
+ # a hash containing the job's class and it's argument are passed as argument.
111
+ #
112
+ # Asserts the number of times a job is enqueued to a specific queue by passing +:queue+ option.
111
113
  #
112
114
  # def test_logging_job
113
115
  # assert_enqueued_jobs 2, queue: 'default' do
@@ -117,14 +119,18 @@ module ActiveJob
117
119
  # end
118
120
  def assert_enqueued_jobs(number, only: nil, except: nil, queue: nil)
119
121
  if block_given?
120
- original_count = enqueued_jobs_size(only: only, except: except, queue: queue)
122
+ original_count = enqueued_jobs_with(only: only, except: except, queue: queue)
123
+
121
124
  yield
122
- new_count = enqueued_jobs_size(only: only, except: except, queue: queue)
123
- assert_equal number, new_count - original_count, "#{number} jobs expected, but #{new_count - original_count} were enqueued"
125
+
126
+ new_count = enqueued_jobs_with(only: only, except: except, queue: queue)
127
+
128
+ actual_count = new_count - original_count
124
129
  else
125
- actual_count = enqueued_jobs_size(only: only, except: except, queue: queue)
126
- assert_equal number, actual_count, "#{number} jobs expected, but #{actual_count} were enqueued"
130
+ actual_count = enqueued_jobs_with(only: only, except: except, queue: queue)
127
131
  end
132
+
133
+ assert_equal number, actual_count, "#{number} jobs expected, but #{actual_count} were enqueued"
128
134
  end
129
135
 
130
136
  # Asserts that no jobs have been enqueued.
@@ -135,7 +141,7 @@ module ActiveJob
135
141
  # assert_enqueued_jobs 1
136
142
  # end
137
143
  #
138
- # If a block is passed, that block should not cause any job to be enqueued.
144
+ # If a block is passed, asserts that the block will not cause any job to be enqueued.
139
145
  #
140
146
  # def test_jobs_again
141
147
  # assert_no_enqueued_jobs do
@@ -143,7 +149,7 @@ module ActiveJob
143
149
  # end
144
150
  # end
145
151
  #
146
- # It can be asserted that no jobs of a specific kind are enqueued:
152
+ # Asserts that no jobs of a specific kind are enqueued by passing +:only+ option.
147
153
  #
148
154
  # def test_no_logging
149
155
  # assert_no_enqueued_jobs only: LoggingJob do
@@ -151,7 +157,7 @@ module ActiveJob
151
157
  # end
152
158
  # end
153
159
  #
154
- # It can be asserted that no jobs except specific class are enqueued:
160
+ # Asserts that no jobs except specific class are enqueued by passing +:except+ option.
155
161
  #
156
162
  # def test_no_logging
157
163
  # assert_no_enqueued_jobs except: HelloJob do
@@ -159,16 +165,27 @@ module ActiveJob
159
165
  # end
160
166
  # end
161
167
  #
168
+ # +:only+ and +:except+ options accepts Class, Array of Class or Proc. When passed a Proc,
169
+ # a hash containing the job's class and it's argument are passed as argument.
170
+ #
171
+ # Asserts that no jobs are enqueued to a specific queue by passing +:queue+ option
172
+ #
173
+ # def test_no_logging
174
+ # assert_no_enqueued_jobs queue: 'default' do
175
+ # LoggingJob.set(queue: :some_queue).perform_later
176
+ # end
177
+ # end
178
+ #
162
179
  # Note: This assertion is simply a shortcut for:
163
180
  #
164
181
  # assert_enqueued_jobs 0, &block
165
- def assert_no_enqueued_jobs(only: nil, except: nil, &block)
166
- assert_enqueued_jobs 0, only: only, except: except, &block
182
+ def assert_no_enqueued_jobs(only: nil, except: nil, queue: nil, &block)
183
+ assert_enqueued_jobs 0, only: only, except: except, queue: queue, &block
167
184
  end
168
185
 
169
186
  # Asserts that the number of performed jobs matches the given number.
170
187
  # If no block is passed, <tt>perform_enqueued_jobs</tt>
171
- # must be called around the job call.
188
+ # must be called around or after the job call.
172
189
  #
173
190
  # def test_jobs
174
191
  # assert_performed_jobs 0
@@ -178,13 +195,14 @@ module ActiveJob
178
195
  # end
179
196
  # assert_performed_jobs 1
180
197
  #
181
- # perform_enqueued_jobs do
182
- # HelloJob.perform_later('yves')
183
- # assert_performed_jobs 2
184
- # end
198
+ # HelloJob.perform_later('yves')
199
+ #
200
+ # perform_enqueued_jobs
201
+ #
202
+ # assert_performed_jobs 2
185
203
  # end
186
204
  #
187
- # If a block is passed, that block should cause the specified number of
205
+ # If a block is passed, asserts that the block will cause the specified number of
188
206
  # jobs to be performed.
189
207
  #
190
208
  # def test_jobs_again
@@ -198,7 +216,7 @@ module ActiveJob
198
216
  # end
199
217
  # end
200
218
  #
201
- # The block form supports filtering. If the :only option is specified,
219
+ # This method also supports filtering. If the +:only+ option is specified,
202
220
  # then only the listed job(s) will be performed.
203
221
  #
204
222
  # def test_hello_job
@@ -208,7 +226,7 @@ module ActiveJob
208
226
  # end
209
227
  # end
210
228
  #
211
- # Also if the :except option is specified,
229
+ # Also if the +:except+ option is specified,
212
230
  # then the job(s) except specific class will be performed.
213
231
  #
214
232
  # def test_hello_job
@@ -229,17 +247,42 @@ module ActiveJob
229
247
  # end
230
248
  # end
231
249
  # end
232
- def assert_performed_jobs(number, only: nil, except: nil)
250
+ #
251
+ # A proc may also be specified. When passed a Proc, the job's instance will be passed as argument.
252
+ #
253
+ # def test_hello_and_logging_jobs
254
+ # assert_nothing_raised do
255
+ # assert_performed_jobs(1, only: ->(job) { job.is_a?(HelloJob) }) do
256
+ # HelloJob.perform_later('jeremy')
257
+ # LoggingJob.perform_later('stewie')
258
+ # RescueJob.perform_later('david')
259
+ # end
260
+ # end
261
+ # end
262
+ #
263
+ # If the +:queue+ option is specified,
264
+ # then only the job(s) enqueued to a specific queue will be performed.
265
+ #
266
+ # def test_assert_performed_jobs_with_queue_option
267
+ # assert_performed_jobs 1, queue: :some_queue do
268
+ # HelloJob.set(queue: :some_queue).perform_later("jeremy")
269
+ # HelloJob.set(queue: :other_queue).perform_later("bogdan")
270
+ # end
271
+ # end
272
+ def assert_performed_jobs(number, only: nil, except: nil, queue: nil, &block)
233
273
  if block_given?
234
274
  original_count = performed_jobs.size
235
- perform_enqueued_jobs(only: only, except: except) { yield }
275
+
276
+ perform_enqueued_jobs(only: only, except: except, queue: queue, &block)
277
+
236
278
  new_count = performed_jobs.size
237
- assert_equal number, new_count - original_count,
238
- "#{number} jobs expected, but #{new_count - original_count} were performed"
279
+
280
+ performed_jobs_size = new_count - original_count
239
281
  else
240
- performed_jobs_size = performed_jobs.size
241
- assert_equal number, performed_jobs_size, "#{number} jobs expected, but #{performed_jobs_size} were performed"
282
+ performed_jobs_size = performed_jobs_with(only: only, except: except, queue: queue)
242
283
  end
284
+
285
+ assert_equal number, performed_jobs_size, "#{number} jobs expected, but #{performed_jobs_size} were performed"
243
286
  end
244
287
 
245
288
  # Asserts that no jobs have been performed.
@@ -253,7 +296,7 @@ module ActiveJob
253
296
  # end
254
297
  # end
255
298
  #
256
- # If a block is passed, that block should not cause any job to be performed.
299
+ # If a block is passed, asserts that the block will not cause any job to be performed.
257
300
  #
258
301
  # def test_jobs_again
259
302
  # assert_no_performed_jobs do
@@ -261,7 +304,7 @@ module ActiveJob
261
304
  # end
262
305
  # end
263
306
  #
264
- # The block form supports filtering. If the :only option is specified,
307
+ # The block form supports filtering. If the +:only+ option is specified,
265
308
  # then only the listed job(s) will not be performed.
266
309
  #
267
310
  # def test_no_logging
@@ -270,7 +313,7 @@ module ActiveJob
270
313
  # end
271
314
  # end
272
315
  #
273
- # Also if the :except option is specified,
316
+ # Also if the +:except+ option is specified,
274
317
  # then the job(s) except specific class will not be performed.
275
318
  #
276
319
  # def test_no_logging
@@ -279,14 +322,53 @@ module ActiveJob
279
322
  # end
280
323
  # end
281
324
  #
325
+ # +:only+ and +:except+ options accepts Class, Array of Class or Proc. When passed a Proc,
326
+ # an instance of the job will be passed as argument.
327
+ #
328
+ # If the +:queue+ option is specified,
329
+ # then only the job(s) enqueued to a specific queue will not be performed.
330
+ #
331
+ # def test_assert_no_performed_jobs_with_queue_option
332
+ # assert_no_performed_jobs queue: :some_queue do
333
+ # HelloJob.set(queue: :other_queue).perform_later("jeremy")
334
+ # end
335
+ # end
336
+ #
282
337
  # Note: This assertion is simply a shortcut for:
283
338
  #
284
339
  # assert_performed_jobs 0, &block
285
- def assert_no_performed_jobs(only: nil, except: nil, &block)
286
- assert_performed_jobs 0, only: only, except: except, &block
340
+ def assert_no_performed_jobs(only: nil, except: nil, queue: nil, &block)
341
+ assert_performed_jobs 0, only: only, except: except, queue: queue, &block
287
342
  end
288
343
 
289
- # Asserts that the job passed in the block has been enqueued with the given arguments.
344
+ # Asserts that the job has been enqueued with the given arguments.
345
+ #
346
+ # def test_assert_enqueued_with
347
+ # MyJob.perform_later(1,2,3)
348
+ # assert_enqueued_with(job: MyJob, args: [1,2,3], queue: 'low')
349
+ #
350
+ # MyJob.set(wait_until: Date.tomorrow.noon).perform_later
351
+ # assert_enqueued_with(job: MyJob, at: Date.tomorrow.noon)
352
+ # end
353
+ #
354
+ #
355
+ # The +args+ argument also accepts a proc which will get passed the actual
356
+ # job's arguments. Your proc needs to return a boolean value determining if
357
+ # the job's arguments matches your expectation. This is useful to check only
358
+ # for a subset of arguments.
359
+ #
360
+ # def test_assert_enqueued_with
361
+ # expected_args = ->(job_args) do
362
+ # assert job_args.first.key?(:foo)
363
+ # end
364
+ #
365
+ # MyJob.perform_later(foo: 'bar', other_arg: 'No need to check in the test')
366
+ # assert_enqueued_with(job: MyJob, args: expected_args, queue: 'low')
367
+ # end
368
+ #
369
+ #
370
+ # If a block is passed, asserts that the block will cause the job to be
371
+ # enqueued with the given arguments.
290
372
  #
291
373
  # def test_assert_enqueued_with
292
374
  # assert_enqueued_with(job: MyJob, args: [1,2,3], queue: 'low') do
@@ -298,20 +380,70 @@ module ActiveJob
298
380
  # end
299
381
  # end
300
382
  def assert_enqueued_with(job: nil, args: nil, at: nil, queue: nil)
301
- original_enqueued_jobs_count = enqueued_jobs.count
302
383
  expected = { job: job, args: args, at: at, queue: queue }.compact
303
384
  expected_args = prepare_args_for_assertion(expected)
304
- yield
305
- in_block_jobs = enqueued_jobs.drop(original_enqueued_jobs_count)
306
- matching_job = in_block_jobs.find do |in_block_job|
307
- deserialized_job = deserialize_args_for_assertion(in_block_job)
308
- expected_args.all? { |key, value| value == deserialized_job[key] }
385
+
386
+ if block_given?
387
+ original_enqueued_jobs_count = enqueued_jobs.count
388
+
389
+ yield
390
+
391
+ jobs = enqueued_jobs.drop(original_enqueued_jobs_count)
392
+ else
393
+ jobs = enqueued_jobs
309
394
  end
395
+
396
+ matching_job = jobs.find do |enqueued_job|
397
+ deserialized_job = deserialize_args_for_assertion(enqueued_job)
398
+
399
+ expected_args.all? do |key, value|
400
+ if value.respond_to?(:call)
401
+ value.call(deserialized_job[key])
402
+ else
403
+ value == deserialized_job[key]
404
+ end
405
+ end
406
+ end
407
+
310
408
  assert matching_job, "No enqueued job found with #{expected}"
311
409
  instantiate_job(matching_job)
312
410
  end
313
411
 
314
- # Asserts that the job passed in the block has been performed with the given arguments.
412
+ # Asserts that the job has been performed with the given arguments.
413
+ #
414
+ # def test_assert_performed_with
415
+ # MyJob.perform_later(1,2,3)
416
+ #
417
+ # perform_enqueued_jobs
418
+ #
419
+ # assert_performed_with(job: MyJob, args: [1,2,3], queue: 'high')
420
+ #
421
+ # MyJob.set(wait_until: Date.tomorrow.noon).perform_later
422
+ #
423
+ # perform_enqueued_jobs
424
+ #
425
+ # assert_performed_with(job: MyJob, at: Date.tomorrow.noon)
426
+ # end
427
+ #
428
+ # The +args+ argument also accepts a proc which will get passed the actual
429
+ # job's arguments. Your proc needs to return a boolean value determining if
430
+ # the job's arguments matches your expectation. This is useful to check only
431
+ # for a subset of arguments.
432
+ #
433
+ # def test_assert_performed_with
434
+ # expected_args = ->(job_args) do
435
+ # assert job_args.first.key?(:foo)
436
+ # end
437
+ # MyJob.perform_later(foo: 'bar', other_arg: 'No need to check in the test')
438
+ #
439
+ # perform_enqueued_jobs
440
+ #
441
+ # assert_performed_with(job: MyJob, args: expected_args, queue: 'high')
442
+ # end
443
+ #
444
+ # If a block is passed, that block performs all of the jobs that were
445
+ # enqueued throughout the duration of the block and asserts that
446
+ # the job has been performed with the given arguments in the block.
315
447
  #
316
448
  # def test_assert_performed_with
317
449
  # assert_performed_with(job: MyJob, args: [1,2,3], queue: 'high') do
@@ -322,21 +454,39 @@ module ActiveJob
322
454
  # MyJob.set(wait_until: Date.tomorrow.noon).perform_later
323
455
  # end
324
456
  # end
325
- def assert_performed_with(job: nil, args: nil, at: nil, queue: nil)
326
- original_performed_jobs_count = performed_jobs.count
457
+ def assert_performed_with(job: nil, args: nil, at: nil, queue: nil, &block)
327
458
  expected = { job: job, args: args, at: at, queue: queue }.compact
328
459
  expected_args = prepare_args_for_assertion(expected)
329
- perform_enqueued_jobs { yield }
330
- in_block_jobs = performed_jobs.drop(original_performed_jobs_count)
331
- matching_job = in_block_jobs.find do |in_block_job|
332
- deserialized_job = deserialize_args_for_assertion(in_block_job)
333
- expected_args.all? { |key, value| value == deserialized_job[key] }
460
+
461
+ if block_given?
462
+ original_performed_jobs_count = performed_jobs.count
463
+
464
+ perform_enqueued_jobs(&block)
465
+
466
+ jobs = performed_jobs.drop(original_performed_jobs_count)
467
+ else
468
+ jobs = performed_jobs
334
469
  end
470
+
471
+ matching_job = jobs.find do |enqueued_job|
472
+ deserialized_job = deserialize_args_for_assertion(enqueued_job)
473
+
474
+ expected_args.all? do |key, value|
475
+ if value.respond_to?(:call)
476
+ value.call(deserialized_job[key])
477
+ else
478
+ value == deserialized_job[key]
479
+ end
480
+ end
481
+ end
482
+
335
483
  assert matching_job, "No performed job found with #{expected}"
336
484
  instantiate_job(matching_job)
337
485
  end
338
486
 
339
- # Performs all enqueued jobs in the duration of the block.
487
+ # Performs all enqueued jobs. If a block is given, performs all of the jobs
488
+ # that were enqueued throughout the duration of the block. If a block is
489
+ # not given, performs all of the enqueued jobs up to this point in the test.
340
490
  #
341
491
  # def test_perform_enqueued_jobs
342
492
  # perform_enqueued_jobs do
@@ -345,6 +495,14 @@ module ActiveJob
345
495
  # assert_performed_jobs 1
346
496
  # end
347
497
  #
498
+ # def test_perform_enqueued_jobs_without_block
499
+ # MyJob.perform_later(1, 2, 3)
500
+ #
501
+ # perform_enqueued_jobs
502
+ #
503
+ # assert_performed_jobs 1
504
+ # end
505
+ #
348
506
  # This method also supports filtering. If the +:only+ option is specified,
349
507
  # then only the listed job(s) will be performed.
350
508
  #
@@ -367,24 +525,45 @@ module ActiveJob
367
525
  # assert_performed_jobs 1
368
526
  # end
369
527
  #
370
- def perform_enqueued_jobs(only: nil, except: nil)
528
+ # +:only+ and +:except+ options accepts Class, Array of Class or Proc. When passed a Proc,
529
+ # an instance of the job will be passed as argument.
530
+ #
531
+ # If the +:queue+ option is specified,
532
+ # then only the job(s) enqueued to a specific queue will be performed.
533
+ #
534
+ # def test_perform_enqueued_jobs_with_queue
535
+ # perform_enqueued_jobs queue: :some_queue do
536
+ # MyJob.set(queue: :some_queue).perform_later(1, 2, 3) # will be performed
537
+ # HelloJob.set(queue: :other_queue).perform_later(1, 2, 3) # will not be performed
538
+ # end
539
+ # assert_performed_jobs 1
540
+ # end
541
+ #
542
+ def perform_enqueued_jobs(only: nil, except: nil, queue: nil)
543
+ return flush_enqueued_jobs(only: only, except: except, queue: queue) unless block_given?
544
+
371
545
  validate_option(only: only, except: except)
546
+
372
547
  old_perform_enqueued_jobs = queue_adapter.perform_enqueued_jobs
373
548
  old_perform_enqueued_at_jobs = queue_adapter.perform_enqueued_at_jobs
374
549
  old_filter = queue_adapter.filter
375
550
  old_reject = queue_adapter.reject
551
+ old_queue = queue_adapter.queue
376
552
 
377
553
  begin
378
554
  queue_adapter.perform_enqueued_jobs = true
379
555
  queue_adapter.perform_enqueued_at_jobs = true
380
556
  queue_adapter.filter = only
381
557
  queue_adapter.reject = except
558
+ queue_adapter.queue = queue
559
+
382
560
  yield
383
561
  ensure
384
562
  queue_adapter.perform_enqueued_jobs = old_perform_enqueued_jobs
385
563
  queue_adapter.perform_enqueued_at_jobs = old_perform_enqueued_at_jobs
386
564
  queue_adapter.filter = old_filter
387
565
  queue_adapter.reject = old_reject
566
+ queue_adapter.queue = old_queue
388
567
  end
389
568
  end
390
569
 
@@ -406,30 +585,72 @@ module ActiveJob
406
585
  performed_jobs.clear
407
586
  end
408
587
 
409
- def enqueued_jobs_size(only: nil, except: nil, queue: nil)
588
+ def jobs_with(jobs, only: nil, except: nil, queue: nil)
410
589
  validate_option(only: only, except: except)
411
- enqueued_jobs.count do |job|
590
+
591
+ jobs.count do |job|
412
592
  job_class = job.fetch(:job)
593
+
413
594
  if only
414
- next false unless Array(only).include?(job_class)
595
+ next false unless filter_as_proc(only).call(job)
415
596
  elsif except
416
- next false if Array(except).include?(job_class)
597
+ next false if filter_as_proc(except).call(job)
417
598
  end
599
+
418
600
  if queue
419
601
  next false unless queue.to_s == job.fetch(:queue, job_class.queue_name)
420
602
  end
603
+
604
+ yield job if block_given?
605
+
421
606
  true
422
607
  end
423
608
  end
424
609
 
610
+ def filter_as_proc(filter)
611
+ return filter if filter.is_a?(Proc)
612
+
613
+ ->(job) { Array(filter).include?(job.fetch(:job)) }
614
+ end
615
+
616
+ def enqueued_jobs_with(only: nil, except: nil, queue: nil, &block)
617
+ jobs_with(enqueued_jobs, only: only, except: except, queue: queue, &block)
618
+ end
619
+
620
+ def performed_jobs_with(only: nil, except: nil, queue: nil, &block)
621
+ jobs_with(performed_jobs, only: only, except: except, queue: queue, &block)
622
+ end
623
+
624
+ def flush_enqueued_jobs(only: nil, except: nil, queue: nil)
625
+ enqueued_jobs_with(only: only, except: except, queue: queue) do |payload|
626
+ instantiate_job(payload).perform_now
627
+ queue_adapter.performed_jobs << payload
628
+ end
629
+ end
630
+
425
631
  def prepare_args_for_assertion(args)
426
632
  args.dup.tap do |arguments|
427
- arguments[:at] = arguments[:at].to_f if arguments[:at]
633
+ arguments[:at] = round_time_arguments(arguments[:at]) if arguments[:at]
634
+ arguments[:args] = round_time_arguments(arguments[:args]) if arguments[:args]
635
+ end
636
+ end
637
+
638
+ def round_time_arguments(argument)
639
+ case argument
640
+ when Time, ActiveSupport::TimeWithZone, DateTime
641
+ argument.change(usec: 0)
642
+ when Hash
643
+ argument.transform_values { |value| round_time_arguments(value) }
644
+ when Array
645
+ argument.map { |element| round_time_arguments(element) }
646
+ else
647
+ argument
428
648
  end
429
649
  end
430
650
 
431
651
  def deserialize_args_for_assertion(job)
432
652
  job.dup.tap do |new_job|
653
+ new_job[:at] = round_time_arguments(Time.at(new_job[:at])) if new_job[:at]
433
654
  new_job[:args] = ActiveJob::Arguments.deserialize(new_job[:args]) if new_job[:args]
434
655
  end
435
656
  end