rspec-sidekiq 3.0.3 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGES.md +43 -0
  3. data/LICENSE +12 -0
  4. data/README.md +217 -84
  5. data/lib/rspec/sidekiq/batch.rb +30 -3
  6. data/lib/rspec/sidekiq/configuration.rb +13 -2
  7. data/lib/rspec/sidekiq/matchers/base.rb +262 -0
  8. data/lib/rspec/sidekiq/matchers/be_delayed.rb +17 -3
  9. data/lib/rspec/sidekiq/matchers/enqueue_sidekiq_job.rb +99 -0
  10. data/lib/rspec/sidekiq/matchers/have_enqueued_sidekiq_job.rb +25 -0
  11. data/lib/rspec/sidekiq/matchers.rb +13 -8
  12. data/lib/rspec/sidekiq/sidekiq.rb +1 -1
  13. data/lib/rspec/sidekiq/version.rb +1 -1
  14. data/lib/rspec-sidekiq.rb +2 -0
  15. metadata +130 -83
  16. data/.gitattributes +0 -22
  17. data/.gitignore +0 -2
  18. data/.rspec +0 -4
  19. data/.simplecov +0 -5
  20. data/Gemfile +0 -9
  21. data/lib/rspec/sidekiq/matchers/have_enqueued_job.rb +0 -183
  22. data/rspec-sidekiq.gemspec +0 -37
  23. data/spec/rspec/sidekiq/batch_spec.rb +0 -77
  24. data/spec/rspec/sidekiq/helpers/retries_exhausted_spec.rb +0 -40
  25. data/spec/rspec/sidekiq/matchers/be_delayed_spec.rb +0 -238
  26. data/spec/rspec/sidekiq/matchers/be_expired_in_spec.rb +0 -57
  27. data/spec/rspec/sidekiq/matchers/be_processed_in_spec.rb +0 -114
  28. data/spec/rspec/sidekiq/matchers/be_retryable_spec.rb +0 -129
  29. data/spec/rspec/sidekiq/matchers/be_unique_spec.rb +0 -115
  30. data/spec/rspec/sidekiq/matchers/have_enqueued_job_spec.rb +0 -228
  31. data/spec/rspec/sidekiq/matchers/save_backtrace_spec.rb +0 -136
  32. data/spec/rspec/sidekiq/sidekiq_spec.rb +0 -15
  33. data/spec/spec_helper.rb +0 -29
  34. data/spec/support/factories.rb +0 -33
  35. data/spec/support/init.rb +0 -6
  36. data/spec/support/test_action_mailer.rb +0 -6
  37. data/spec/support/test_job.rb +0 -6
  38. data/spec/support/test_resource.rb +0 -16
  39. data/spec/support/test_worker.rb +0 -8
  40. 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.to_i + @expected_interval
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
- yaml = YAML.load(job['args'].first)
62
- @expected_method_receiver == yaml[0] && @expected_method.name == yaml[1] && (@expected_arguments <=> yaml[2]) == 0
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 'rspec/core'
2
- require 'rspec/sidekiq/matchers/be_delayed'
3
- require 'rspec/sidekiq/matchers/be_expired_in'
4
- require 'rspec/sidekiq/matchers/be_processed_in'
5
- require 'rspec/sidekiq/matchers/be_retryable'
6
- require 'rspec/sidekiq/matchers/be_unique'
7
- require 'rspec/sidekiq/matchers/have_enqueued_job'
8
- require 'rspec/sidekiq/matchers/save_backtrace'
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/philostler/rspec-sidekiq/wiki/FAQ-&-Troubleshooting'
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
@@ -1,5 +1,5 @@
1
1
  module RSpec
2
2
  module Sidekiq
3
- VERSION = '3.0.3'
3
+ VERSION = "4.1.0"
4
4
  end
5
5
  end
data/lib/rspec-sidekiq.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'forwardable'
2
+
1
3
  require 'sidekiq'
2
4
  require 'sidekiq/testing'
3
5