rspec-sidekiq 3.0.3 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGES.md +28 -0
  3. data/LICENSE +12 -0
  4. data/README.md +200 -79
  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 +257 -0
  8. data/lib/rspec/sidekiq/matchers/be_delayed.rb +17 -3
  9. data/lib/rspec/sidekiq/matchers/enqueue_sidekiq_job.rb +87 -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. metadata +129 -82
  15. data/.gitattributes +0 -22
  16. data/.gitignore +0 -2
  17. data/.rspec +0 -4
  18. data/.simplecov +0 -5
  19. data/Gemfile +0 -9
  20. data/lib/rspec/sidekiq/matchers/have_enqueued_job.rb +0 -183
  21. data/rspec-sidekiq.gemspec +0 -37
  22. data/spec/rspec/sidekiq/batch_spec.rb +0 -77
  23. data/spec/rspec/sidekiq/helpers/retries_exhausted_spec.rb +0 -40
  24. data/spec/rspec/sidekiq/matchers/be_delayed_spec.rb +0 -238
  25. data/spec/rspec/sidekiq/matchers/be_expired_in_spec.rb +0 -57
  26. data/spec/rspec/sidekiq/matchers/be_processed_in_spec.rb +0 -114
  27. data/spec/rspec/sidekiq/matchers/be_retryable_spec.rb +0 -129
  28. data/spec/rspec/sidekiq/matchers/be_unique_spec.rb +0 -115
  29. data/spec/rspec/sidekiq/matchers/have_enqueued_job_spec.rb +0 -228
  30. data/spec/rspec/sidekiq/matchers/save_backtrace_spec.rb +0 -136
  31. data/spec/rspec/sidekiq/sidekiq_spec.rb +0 -15
  32. data/spec/spec_helper.rb +0 -29
  33. data/spec/support/factories.rb +0 -33
  34. data/spec/support/init.rb +0 -6
  35. data/spec/support/test_action_mailer.rb +0 -6
  36. data/spec/support/test_job.rb +0 -6
  37. data/spec/support/test_resource.rb +0 -16
  38. data/spec/support/test_worker.rb +0 -8
  39. data/spec/support/test_worker_alternative.rb +0 -8
@@ -0,0 +1,257 @@
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 false 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
+
166
+ attr_reader :expected_arguments, :expected_options, :klass, :actual_jobs
167
+
168
+ def initialize
169
+ @expected_arguments = [any_args]
170
+ @expected_options = {}
171
+ end
172
+
173
+ def with(*expected_arguments)
174
+ @expected_arguments = expected_arguments
175
+ self
176
+ end
177
+
178
+ def at(timestamp)
179
+ @expected_options["at"] = timestamp.to_time.to_i
180
+ self
181
+ end
182
+
183
+ def in(interval)
184
+ @expected_options["at"] = (Time.now.to_f + interval.to_f).to_i
185
+ self
186
+ end
187
+
188
+ def on(queue)
189
+ @expected_options["queue"] = queue
190
+ self
191
+ end
192
+
193
+ def description
194
+ "have an enqueued #{klass} job with arguments #{expected_arguments}"
195
+ end
196
+
197
+ def failure_message
198
+ message = ["expected to have an enqueued #{klass} job"]
199
+ if expected_arguments
200
+ message << " with arguments:"
201
+ message << " -#{formatted(expected_arguments)}"
202
+ end
203
+
204
+ if expected_options.any?
205
+ message << " with context:"
206
+ message << " -#{formatted(expected_options)}"
207
+ end
208
+
209
+ if actual_jobs.any?
210
+ message << "but have enqueued only jobs"
211
+ if expected_arguments
212
+ job_messages = actual_jobs.map do |job|
213
+ base = " -JID:#{job.jid} with arguments:"
214
+ base << "\n -#{formatted(job.args)}"
215
+ if expected_options.any?
216
+ base << "\n with context: #{formatted(job.context)}"
217
+ end
218
+
219
+ base
220
+ end
221
+
222
+ message << job_messages.join("\n")
223
+ end
224
+ end
225
+
226
+ message.join("\n")
227
+ end
228
+
229
+ def failure_message_when_negated
230
+ message = ["expected not to have an enqueued #{klass} job"]
231
+ message << " arguments: #{expected_arguments}" if expected_arguments.any?
232
+ message << " options: #{expected_options}" if expected_options.any?
233
+ message.join("\n")
234
+ end
235
+
236
+ def formatted(thing)
237
+ RSpec::Support::ObjectFormatter.format(thing)
238
+ end
239
+
240
+ def jsonified_expected_arguments
241
+ # We would just cast-to-parse-json, but we need to support
242
+ # RSpec matcher args like #kind_of
243
+ @jsonified_expected_arguments ||= begin
244
+ expected_arguments.map do |arg|
245
+ case arg.class
246
+ when Symbol then arg.to_s
247
+ when Hash then JSON.parse(arg.to_json)
248
+ else
249
+ arg
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end
255
+ end
256
+ end
257
+ 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,87 @@
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
60
+ #
61
+ # @example
62
+ #
63
+ # expect { AwesomeJob.perform_async }.to enqueue_sidekiq_job
64
+ #
65
+ # # A specific job class
66
+ # expect { AwesomeJob.perform_async }.to enqueue_sidekiq_job(AwesomeJob)
67
+ #
68
+ # # with specific arguments
69
+ # expect { AwesomeJob.perform_async "Awesome!" }.to enqueue_sidekiq_job.with("Awesome!")
70
+ #
71
+ # # On a specific queue
72
+ # expect { AwesomeJob.set(queue: "high").perform_async }.to enqueue_sidekiq_job.on("high")
73
+ #
74
+ # # At a specific datetime
75
+ # specific_time = 1.hour.from_now
76
+ # expect { AwesomeJob.perform_at(specific_time) }.to enqueue_sidekiq_job.at(specific_time)
77
+ #
78
+ # # In a specific interval (be mindful of freezing or managing time here)
79
+ # freeze_time do
80
+ # expect { AwesomeJob.perform_in(1.hour) }.to enqueue_sidekiq_job.in(1.hour)
81
+ # end
82
+ def enqueue_sidekiq_job(job_class = nil)
83
+ EnqueueSidekiqJob.new(job_class)
84
+ end
85
+ end
86
+ end
87
+ 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 = 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?(jsonified_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.0.0"
4
4
  end
5
5
  end