rspec-sidekiq 3.0.3 → 4.1.0
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.
- checksums.yaml +5 -5
- data/CHANGES.md +43 -0
- data/LICENSE +12 -0
- data/README.md +217 -84
- data/lib/rspec/sidekiq/batch.rb +30 -3
- data/lib/rspec/sidekiq/configuration.rb +13 -2
- data/lib/rspec/sidekiq/matchers/base.rb +262 -0
- data/lib/rspec/sidekiq/matchers/be_delayed.rb +17 -3
- data/lib/rspec/sidekiq/matchers/enqueue_sidekiq_job.rb +99 -0
- data/lib/rspec/sidekiq/matchers/have_enqueued_sidekiq_job.rb +25 -0
- data/lib/rspec/sidekiq/matchers.rb +13 -8
- data/lib/rspec/sidekiq/sidekiq.rb +1 -1
- data/lib/rspec/sidekiq/version.rb +1 -1
- data/lib/rspec-sidekiq.rb +2 -0
- metadata +130 -83
- data/.gitattributes +0 -22
- data/.gitignore +0 -2
- data/.rspec +0 -4
- data/.simplecov +0 -5
- data/Gemfile +0 -9
- data/lib/rspec/sidekiq/matchers/have_enqueued_job.rb +0 -183
- data/rspec-sidekiq.gemspec +0 -37
- data/spec/rspec/sidekiq/batch_spec.rb +0 -77
- data/spec/rspec/sidekiq/helpers/retries_exhausted_spec.rb +0 -40
- data/spec/rspec/sidekiq/matchers/be_delayed_spec.rb +0 -238
- data/spec/rspec/sidekiq/matchers/be_expired_in_spec.rb +0 -57
- data/spec/rspec/sidekiq/matchers/be_processed_in_spec.rb +0 -114
- data/spec/rspec/sidekiq/matchers/be_retryable_spec.rb +0 -129
- data/spec/rspec/sidekiq/matchers/be_unique_spec.rb +0 -115
- data/spec/rspec/sidekiq/matchers/have_enqueued_job_spec.rb +0 -228
- data/spec/rspec/sidekiq/matchers/save_backtrace_spec.rb +0 -136
- data/spec/rspec/sidekiq/sidekiq_spec.rb +0 -15
- data/spec/spec_helper.rb +0 -29
- data/spec/support/factories.rb +0 -33
- data/spec/support/init.rb +0 -6
- data/spec/support/test_action_mailer.rb +0 -6
- data/spec/support/test_job.rb +0 -6
- data/spec/support/test_resource.rb +0 -16
- data/spec/support/test_worker.rb +0 -8
- data/spec/support/test_worker_alternative.rb +0 -8
@@ -0,0 +1,262 @@
|
|
1
|
+
module RSpec
|
2
|
+
module Sidekiq
|
3
|
+
module Matchers
|
4
|
+
# @api private
|
5
|
+
class JobOptionParser
|
6
|
+
attr_reader :job
|
7
|
+
|
8
|
+
def initialize(job)
|
9
|
+
@job = job
|
10
|
+
end
|
11
|
+
|
12
|
+
def matches?(options)
|
13
|
+
with_context(**options)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def at_evaluator(value)
|
19
|
+
return value.nil? if job["at"].to_s.empty?
|
20
|
+
value == Time.at(job["at"]).to_i
|
21
|
+
end
|
22
|
+
|
23
|
+
def with_context(**expected_context)
|
24
|
+
expected_context.all? do |key, value|
|
25
|
+
if key == "at"
|
26
|
+
# send to custom evaluator
|
27
|
+
at_evaluator(value)
|
28
|
+
else
|
29
|
+
job.context.has_key?(key) && job.context[key] == value
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# @api private
|
36
|
+
class JobArguments
|
37
|
+
include RSpec::Mocks::ArgumentMatchers
|
38
|
+
|
39
|
+
def initialize(job)
|
40
|
+
self.job = job
|
41
|
+
end
|
42
|
+
attr_accessor :job
|
43
|
+
|
44
|
+
def matches?(expected_args)
|
45
|
+
matcher = RSpec::Mocks::ArgumentListMatcher.new(*expected_args)
|
46
|
+
|
47
|
+
matcher.args_match?(*unwrapped_arguments)
|
48
|
+
end
|
49
|
+
|
50
|
+
def unwrapped_arguments
|
51
|
+
args = job["args"]
|
52
|
+
|
53
|
+
return deserialized_active_job_args if active_job?
|
54
|
+
|
55
|
+
args
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def active_job?
|
61
|
+
job["class"] == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
|
62
|
+
end
|
63
|
+
|
64
|
+
def deserialized_active_job_args
|
65
|
+
active_job_args = ActiveJob::Arguments.deserialize(active_job_original_args)
|
66
|
+
|
67
|
+
# ActiveJob 7 (aj7) changed deserialization structure, adding passed arguments
|
68
|
+
# in an aj-specific hash under the :args key
|
69
|
+
aj7_args_hash = active_job_args.detect { |arg| arg.respond_to?(:key) && arg.key?(:args) }
|
70
|
+
|
71
|
+
return active_job_args if aj7_args_hash.nil?
|
72
|
+
|
73
|
+
active_job_args.delete(aj7_args_hash)
|
74
|
+
active_job_args.concat(aj7_args_hash[:args])
|
75
|
+
end
|
76
|
+
|
77
|
+
def active_job_original_args
|
78
|
+
active_job_args = job["args"].detect { |arg| arg.is_a?(Hash) && arg.key?("arguments") }
|
79
|
+
active_job_args ||= {}
|
80
|
+
active_job_args["arguments"] || []
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
class EnqueuedJob
|
85
|
+
extend Forwardable
|
86
|
+
attr_reader :job
|
87
|
+
delegate :[] => :@job
|
88
|
+
|
89
|
+
def initialize(job)
|
90
|
+
@job = job
|
91
|
+
end
|
92
|
+
|
93
|
+
def jid
|
94
|
+
job["jid"]
|
95
|
+
end
|
96
|
+
|
97
|
+
def args
|
98
|
+
@args ||= JobArguments.new(job).unwrapped_arguments
|
99
|
+
end
|
100
|
+
|
101
|
+
def context
|
102
|
+
@context ||= job.except("args")
|
103
|
+
end
|
104
|
+
|
105
|
+
def ==(other)
|
106
|
+
super(other) unless other.is_a?(EnqueuedJob)
|
107
|
+
|
108
|
+
jid == other.jid
|
109
|
+
end
|
110
|
+
alias_method :eql?, :==
|
111
|
+
end
|
112
|
+
|
113
|
+
class EnqueuedJobs
|
114
|
+
include Enumerable
|
115
|
+
attr_reader :jobs
|
116
|
+
|
117
|
+
def initialize(klass)
|
118
|
+
@jobs = unwrap_jobs(klass.jobs).map { |job| EnqueuedJob.new(job) }
|
119
|
+
end
|
120
|
+
|
121
|
+
def includes?(arguments, options)
|
122
|
+
!!jobs.find { |job| matches?(job, arguments, options) }
|
123
|
+
end
|
124
|
+
|
125
|
+
def each(&block)
|
126
|
+
jobs.each(&block)
|
127
|
+
end
|
128
|
+
|
129
|
+
def minus!(other)
|
130
|
+
self unless other.is_a?(EnqueuedJobs)
|
131
|
+
|
132
|
+
@jobs -= other.jobs
|
133
|
+
|
134
|
+
self
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def matches?(job, arguments, options)
|
140
|
+
arguments_matches?(job, arguments) &&
|
141
|
+
options_matches?(job, options)
|
142
|
+
end
|
143
|
+
|
144
|
+
def arguments_matches?(job, arguments)
|
145
|
+
job_arguments = JobArguments.new(job)
|
146
|
+
|
147
|
+
job_arguments.matches?(arguments)
|
148
|
+
end
|
149
|
+
|
150
|
+
def options_matches?(job, options)
|
151
|
+
parser = JobOptionParser.new(job)
|
152
|
+
|
153
|
+
parser.matches?(options)
|
154
|
+
end
|
155
|
+
|
156
|
+
def unwrap_jobs(jobs)
|
157
|
+
return jobs if jobs.is_a?(Array)
|
158
|
+
jobs.values.flatten
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# @api private
|
163
|
+
class Base
|
164
|
+
include RSpec::Mocks::ArgumentMatchers
|
165
|
+
include RSpec::Matchers::Composable
|
166
|
+
|
167
|
+
attr_reader :expected_arguments, :expected_options, :klass, :actual_jobs
|
168
|
+
|
169
|
+
def initialize
|
170
|
+
@expected_arguments = [any_args]
|
171
|
+
@expected_options = {}
|
172
|
+
end
|
173
|
+
|
174
|
+
def with(*expected_arguments)
|
175
|
+
@expected_arguments = normalize_arguments(expected_arguments)
|
176
|
+
self
|
177
|
+
end
|
178
|
+
|
179
|
+
def at(timestamp)
|
180
|
+
@expected_options["at"] = timestamp.to_time.to_i
|
181
|
+
self
|
182
|
+
end
|
183
|
+
|
184
|
+
def in(interval)
|
185
|
+
@expected_options["at"] = (Time.now.to_f + interval.to_f).to_i
|
186
|
+
self
|
187
|
+
end
|
188
|
+
|
189
|
+
def immediately
|
190
|
+
@expected_options["at"] = nil
|
191
|
+
self
|
192
|
+
end
|
193
|
+
|
194
|
+
def on(queue)
|
195
|
+
@expected_options["queue"] = queue
|
196
|
+
self
|
197
|
+
end
|
198
|
+
|
199
|
+
def description
|
200
|
+
"have an enqueued #{klass} job with arguments #{expected_arguments}"
|
201
|
+
end
|
202
|
+
|
203
|
+
def failure_message
|
204
|
+
message = ["expected to have an enqueued #{klass} job"]
|
205
|
+
if expected_arguments
|
206
|
+
message << " with arguments:"
|
207
|
+
message << " -#{formatted(expected_arguments)}"
|
208
|
+
end
|
209
|
+
|
210
|
+
if expected_options.any?
|
211
|
+
message << " with context:"
|
212
|
+
message << " -#{formatted(expected_options)}"
|
213
|
+
end
|
214
|
+
|
215
|
+
if actual_jobs.any?
|
216
|
+
message << "but have enqueued only jobs"
|
217
|
+
if expected_arguments
|
218
|
+
job_messages = actual_jobs.map do |job|
|
219
|
+
base = " -JID:#{job.jid} with arguments:"
|
220
|
+
base << "\n -#{formatted(job.args)}"
|
221
|
+
if expected_options.any?
|
222
|
+
base << "\n with context: #{formatted(job.context)}"
|
223
|
+
end
|
224
|
+
|
225
|
+
base
|
226
|
+
end
|
227
|
+
|
228
|
+
message << job_messages.join("\n")
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
message.join("\n")
|
233
|
+
end
|
234
|
+
|
235
|
+
def failure_message_when_negated
|
236
|
+
message = ["expected not to have an enqueued #{klass} job"]
|
237
|
+
message << " arguments: #{expected_arguments}" if expected_arguments.any?
|
238
|
+
message << " options: #{expected_options}" if expected_options.any?
|
239
|
+
message.join("\n")
|
240
|
+
end
|
241
|
+
|
242
|
+
def formatted(thing)
|
243
|
+
RSpec::Support::ObjectFormatter.format(thing)
|
244
|
+
end
|
245
|
+
|
246
|
+
def normalize_arguments(args)
|
247
|
+
if args.is_a?(Array)
|
248
|
+
args.map{ |x| normalize_arguments(x) }
|
249
|
+
elsif args.is_a?(Hash)
|
250
|
+
args.each_with_object({}) do |(key, value), hash|
|
251
|
+
hash[key.to_s] = normalize_arguments(value)
|
252
|
+
end
|
253
|
+
elsif args.is_a?(Symbol)
|
254
|
+
args.to_s
|
255
|
+
else
|
256
|
+
args
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
@@ -1,12 +1,18 @@
|
|
1
1
|
module RSpec
|
2
2
|
module Sidekiq
|
3
3
|
module Matchers
|
4
|
+
include RSpec::Mocks::ArgumentMatchers
|
5
|
+
|
4
6
|
def be_delayed(*expected_arguments)
|
5
7
|
BeDelayed.new(*expected_arguments)
|
6
8
|
end
|
7
9
|
|
8
10
|
class BeDelayed
|
9
11
|
def initialize(*expected_arguments)
|
12
|
+
raise <<~MSG if RSpec::Sidekiq.configuration.sidekiq_gte_7?
|
13
|
+
Use of the be_delayed matcher with Sidekiq 7+ is not possible. Try refactoring to a Sidekiq Job with `perform_at` or `perform_in` and the `have_enqueued_sidekiq_job` matcher
|
14
|
+
MSG
|
15
|
+
|
10
16
|
@expected_arguments = expected_arguments
|
11
17
|
end
|
12
18
|
|
@@ -34,7 +40,7 @@ module RSpec
|
|
34
40
|
find_job @expected_method, @expected_arguments do |job|
|
35
41
|
if @expected_interval
|
36
42
|
created_enqueued_at = job['enqueued_at'] || job['created_at']
|
37
|
-
return job['at'].to_i == created_enqueued_at.
|
43
|
+
return job['at'].to_i == Time.at(created_enqueued_at.to_f + @expected_interval.to_f).to_i
|
38
44
|
elsif @expected_time
|
39
45
|
return job['at'].to_i == @expected_time.to_i
|
40
46
|
else
|
@@ -58,8 +64,16 @@ module RSpec
|
|
58
64
|
|
59
65
|
def find_job(method, arguments, &block)
|
60
66
|
job = (::Sidekiq::Extensions::DelayedClass.jobs + ::Sidekiq::Extensions::DelayedModel.jobs + ::Sidekiq::Extensions::DelayedMailer.jobs).find do |job|
|
61
|
-
|
62
|
-
|
67
|
+
arg = job['args'].first
|
68
|
+
yaml = begin
|
69
|
+
YAML.load(arg, aliases: true) # Psych 4 required syntax
|
70
|
+
rescue ArgumentError
|
71
|
+
YAML.load(arg) # Pysch < 4 syntax
|
72
|
+
end
|
73
|
+
|
74
|
+
@expected_method_receiver == yaml[0] &&
|
75
|
+
method.name == yaml[1] &&
|
76
|
+
(arguments.empty? || RSpec::Mocks::ArgumentListMatcher.new(*arguments).args_match?(*yaml[2]))
|
63
77
|
end
|
64
78
|
|
65
79
|
yield job if block && job
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module RSpec
|
2
|
+
module Sidekiq
|
3
|
+
module Matchers
|
4
|
+
# @api private
|
5
|
+
class EnqueueSidekiqJob < Base
|
6
|
+
attr_reader :original_jobs # Plus that from Base
|
7
|
+
|
8
|
+
def initialize(job_class)
|
9
|
+
super()
|
10
|
+
default = if RSpec::Sidekiq.configuration.sidekiq_gte_7?
|
11
|
+
::Sidekiq::Job
|
12
|
+
else
|
13
|
+
::Sidekiq::Worker
|
14
|
+
end
|
15
|
+
|
16
|
+
@klass = job_class || default
|
17
|
+
end
|
18
|
+
|
19
|
+
def matches?(proc)
|
20
|
+
raise ArgumentError, "Only block syntax supported for enqueue_sidekiq_job" unless Proc === proc
|
21
|
+
|
22
|
+
@original_jobs = EnqueuedJobs.new(@klass)
|
23
|
+
proc.call
|
24
|
+
@actual_jobs = EnqueuedJobs.new(@klass).minus!(original_jobs)
|
25
|
+
|
26
|
+
if @actual_jobs.none?
|
27
|
+
return false
|
28
|
+
end
|
29
|
+
|
30
|
+
@actual_jobs.includes?(expected_arguments, expected_options)
|
31
|
+
end
|
32
|
+
|
33
|
+
def failure_message
|
34
|
+
if @actual_jobs.none?
|
35
|
+
"expected to enqueue a job but enqueued 0"
|
36
|
+
else
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def failure_message_when_negated
|
42
|
+
messages = ["expected not to enqueue a #{@klass} job but enqueued #{actual_jobs.count}"]
|
43
|
+
|
44
|
+
messages << " with arguments #{formatted(expected_arguments)}" if expected_arguments
|
45
|
+
messages << " with context #{formatted(expected_options)}" if expected_options
|
46
|
+
|
47
|
+
messages.join("\n")
|
48
|
+
end
|
49
|
+
|
50
|
+
def supports_block_expectations?
|
51
|
+
true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# @api public
|
56
|
+
#
|
57
|
+
# Passes if a Job is enqueued as the result of a block. Chainable `with`
|
58
|
+
# for arguments, `on` for queue, `at` for queued for a specific time, and
|
59
|
+
# `in` for a specific interval delay to being queued, `immediately` for
|
60
|
+
# queued without delay.
|
61
|
+
#
|
62
|
+
# @example
|
63
|
+
#
|
64
|
+
# expect { AwesomeJob.perform_async }.to enqueue_sidekiq_job
|
65
|
+
#
|
66
|
+
# # A specific job class
|
67
|
+
# expect { AwesomeJob.perform_async }.to enqueue_sidekiq_job(AwesomeJob)
|
68
|
+
#
|
69
|
+
# # with specific arguments
|
70
|
+
# expect { AwesomeJob.perform_async "Awesome!" }.to enqueue_sidekiq_job.with("Awesome!")
|
71
|
+
#
|
72
|
+
# # On a specific queue
|
73
|
+
# expect { AwesomeJob.set(queue: "high").perform_async }.to enqueue_sidekiq_job.on("high")
|
74
|
+
#
|
75
|
+
# # At a specific datetime
|
76
|
+
# specific_time = 1.hour.from_now
|
77
|
+
# expect { AwesomeJob.perform_at(specific_time) }.to enqueue_sidekiq_job.at(specific_time)
|
78
|
+
#
|
79
|
+
# # In a specific interval (be mindful of freezing or managing time here)
|
80
|
+
# freeze_time do
|
81
|
+
# expect { AwesomeJob.perform_in(1.hour) }.to enqueue_sidekiq_job.in(1.hour)
|
82
|
+
# end
|
83
|
+
#
|
84
|
+
# # Without any delay
|
85
|
+
# expect { AwesomeJob.perform_async }.to enqueue_sidekiq_job.immediately
|
86
|
+
# expect { AwesomeJob.perform_at(1.hour.ago) }.to enqueue_sidekiq_job.immediately
|
87
|
+
#
|
88
|
+
# ## Composable
|
89
|
+
#
|
90
|
+
# expect do
|
91
|
+
# AwesomeJob.perform_async
|
92
|
+
# OtherJob.perform_async
|
93
|
+
# end.to enqueue_sidekiq_job(AwesomeJob).and enqueue_sidekiq_job(OtherJob)
|
94
|
+
def enqueue_sidekiq_job(job_class = nil)
|
95
|
+
EnqueueSidekiqJob.new(job_class)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module RSpec
|
2
|
+
module Sidekiq
|
3
|
+
module Matchers
|
4
|
+
def have_enqueued_sidekiq_job(*expected_arguments)
|
5
|
+
HaveEnqueuedSidekiqJob.new expected_arguments
|
6
|
+
end
|
7
|
+
|
8
|
+
# @api private
|
9
|
+
class HaveEnqueuedSidekiqJob < Base
|
10
|
+
def initialize(expected_arguments)
|
11
|
+
super()
|
12
|
+
@expected_arguments = normalize_arguments(expected_arguments)
|
13
|
+
end
|
14
|
+
|
15
|
+
def matches?(job_class)
|
16
|
+
@klass = job_class
|
17
|
+
|
18
|
+
@actual_jobs = EnqueuedJobs.new(klass)
|
19
|
+
|
20
|
+
actual_jobs.includes?(expected_arguments, expected_options)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -1,11 +1,16 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
1
|
+
require "rspec/core"
|
2
|
+
require "rspec/mocks/argument_list_matcher"
|
3
|
+
require "rspec/mocks/argument_matchers"
|
4
|
+
|
5
|
+
require "rspec/sidekiq/matchers/base"
|
6
|
+
require "rspec/sidekiq/matchers/be_delayed"
|
7
|
+
require "rspec/sidekiq/matchers/be_expired_in"
|
8
|
+
require "rspec/sidekiq/matchers/be_processed_in"
|
9
|
+
require "rspec/sidekiq/matchers/be_retryable"
|
10
|
+
require "rspec/sidekiq/matchers/be_unique"
|
11
|
+
require "rspec/sidekiq/matchers/have_enqueued_sidekiq_job"
|
12
|
+
require "rspec/sidekiq/matchers/save_backtrace"
|
13
|
+
require "rspec/sidekiq/matchers/enqueue_sidekiq_job"
|
9
14
|
|
10
15
|
RSpec.configure do |config|
|
11
16
|
config.include RSpec::Sidekiq::Matchers
|
@@ -14,7 +14,7 @@ end
|
|
14
14
|
|
15
15
|
RSpec.configure do |config|
|
16
16
|
config.before(:suite) do
|
17
|
-
message = '[rspec-sidekiq] WARNING! Sidekiq will *NOT* process jobs in this environment. See https://github.com/
|
17
|
+
message = '[rspec-sidekiq] WARNING! Sidekiq will *NOT* process jobs in this environment. See https://github.com/wspurgin/rspec-sidekiq/wiki/FAQ-&-Troubleshooting'
|
18
18
|
message = "\e[33m#{message}\e[0m" if RSpec::Sidekiq.configuration.enable_terminal_colours
|
19
19
|
puts message if RSpec::Sidekiq.configuration.warn_when_jobs_not_processed_by_sidekiq
|
20
20
|
end
|