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.
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