require"active_job/base"require"active_job/arguments"moduleRSpecmoduleRailsmoduleMatchers# Namespace for various implementations of ActiveJob features## @api privatemoduleActiveJob# rubocop: disable Metrics/ClassLength# @privateclassBase<RSpec::Rails::Matchers::BaseMatcherdefinitialize@args=[]@queue=nil@priority=nil@at=nil@block=proc{}set_expected_number(:exactly,1)enddefwith(*args,&block)@args=args@block=blockifblock.present?selfenddefon_queue(queue)@queue=queue.to_sselfenddefat_priority(priority)@priority=priority.to_iselfenddefat(time_or_date)casetime_or_datewhenTimethen@at=Time.at(time_or_date.to_f)else@at=time_or_dateendselfenddefexactly(count)set_expected_number(:exactly,count)selfenddefat_least(count)set_expected_number(:at_least,count)selfenddefat_most(count)set_expected_number(:at_most,count)selfenddeftimesselfenddefonceexactly(:once)enddeftwiceexactly(:twice)enddefthriceexactly(:thrice)enddeffailure_messagereturn@failure_messageifdefined?(@failure_message)"expected to #{self.class::FAILURE_MESSAGE_EXPECTATION_ACTION}#{base_message}".tapdo|msg|if@unmatching_jobs.any?msg<<"\nQueued jobs:"@unmatching_jobs.eachdo|job|msg<<"\n#{base_job_message(job)}"endendendenddeffailure_message_when_negated"expected not to #{self.class::FAILURE_MESSAGE_EXPECTATION_ACTION}#{base_message}"enddefmessage_expectation_modifiercase@expectation_typewhen:exactlythen"exactly"when:at_mostthen"at most"when:at_leastthen"at least"endenddefsupports_block_expectations?trueendprivatedefcheck(jobs)@matching_jobs,@unmatching_jobs=jobs.partitiondo|job|ifmatches_constraints?(job)args=deserialize_arguments(job)@block.call(*args)trueelsefalseendendif(signature_mismatch=detect_args_signature_mismatch(@matching_jobs))@failure_message=signature_mismatchreturnfalseend@matching_jobs_count=@matching_jobs.sizecase@expectation_typewhen:exactlythen@expected_number==@matching_jobs_countwhen:at_mostthen@expected_number>=@matching_jobs_countwhen:at_leastthen@expected_number<=@matching_jobs_countendenddefbase_message"#{message_expectation_modifier}#{@expected_number} jobs,".tapdo|msg|msg<<" with #{@args},"if@args.any?msg<<" on queue #{@queue},"if@queuemsg<<" at #{@at.inspect},"if@atmsg<<" with priority #{@priority},"if@prioritymsg<<" but #{self.class::MESSAGE_EXPECTATION_ACTION}#{@matching_jobs_count}"endenddefbase_job_message(job)msg_parts=[]msg_parts<<"with #{deserialize_arguments(job)}"ifjob[:args].any?msg_parts<<"on queue #{job[:queue]}"ifjob[:queue]msg_parts<<"at #{Time.at(job[:at])}"ifjob[:at]msg_parts<<ifjob[:priority]"with priority #{job[:priority]}"else"with no priority specified"end"#{job[:job].name} job".tapdo|msg|msg<<" #{msg_parts.join(', ')}"ifmsg_parts.any?endenddefmatches_constraints?(job)job_matches?(job)&&arguments_match?(job)&&queue_match?(job)&&at_match?(job)&&priority_match?(job)enddefjob_matches?(job)@job?@job==job[:job]:trueenddefarguments_match?(job)if@args.any?args=serialize_and_deserialize_arguments(@args)deserialized_args=deserialize_arguments(job)RSpec::Mocks::ArgumentListMatcher.new(*args).args_match?(*deserialized_args)elsetrueendenddefdetect_args_signature_mismatch(jobs)jobs.eachdo|job|args=deserialize_arguments(job)if(signature_mismatch=check_args_signature_mismatch(job.fetch(:job),:perform,args))returnsignature_mismatchendendnilenddefcheck_args_signature_mismatch(job_class,job_method,args)signature=Support::MethodSignature.new(job_class.public_instance_method(job_method))verifier=Support::StrictSignatureVerifier.new(signature,args)unlessverifier.valid?"Incorrect arguments passed to #{job_class.name}: #{verifier.error_message}"endenddefqueue_match?(job)returntrueunless@queue@queue==job[:queue]enddefpriority_match?(job)returntrueunless@priority@priority==job[:priority]enddefat_match?(job)returntrueunless@atreturnjob[:at].nil?if@at==:no_waitreturnfalseunlessjob[:at]scheduled_at=Time.at(job[:at])values_match?(@at,scheduled_at)||check_for_inprecise_value(scheduled_at)enddefcheck_for_inprecise_value(scheduled_at)returnunlessTime===@at&&values_match?(@at.change(usec: 0),scheduled_at)RSpec.warn_with((<<-WARNING).gsub(/^\s+\|/,'').chomp)
|[WARNING] Your expected `at(...)` value does not match the job scheduled_at value
|unless microseconds are removed. This precision error often occurs when checking
|values against `Time.current` / `Time.now` which have usec precision, but Rails
|uses `n.seconds.from_now` internally which has a usec count of `0`.
|
|Use `change(usec: 0)` to correct these values. For example:
|
|`Time.current.change(usec: 0)`
|
|Note: RSpec cannot do this for you because jobs can be scheduled with usec
|precision and we do not know whether it is on purpose or not.
|
|
WARNINGfalseenddefset_expected_number(relativity,count)@expectation_type=relativity@expected_number=casecountwhen:oncethen1when:twicethen2when:thricethen3elseInteger(count)endenddefserialize_and_deserialize_arguments(args)serialized=::ActiveJob::Arguments.serialize(args)::ActiveJob::Arguments.deserialize(serialized)rescue::ActiveJob::SerializationErrorargsenddefdeserialize_arguments(job)::ActiveJob::Arguments.deserialize(job[:args])rescue::ActiveJob::DeserializationErrorjob[:args]enddefqueue_adapter::ActiveJob::Base.queue_adapterendend# rubocop: enable Metrics/ClassLength# @privateclassHaveEnqueuedJob<BaseFAILURE_MESSAGE_EXPECTATION_ACTION='enqueue'.freezeMESSAGE_EXPECTATION_ACTION='enqueued'.freezedefinitialize(job)super()@job=jobenddefmatches?(proc)raiseArgumentError,"have_enqueued_job and enqueue_job only support block expectations"unlessProc===procoriginal_enqueued_jobs=Set.new(queue_adapter.enqueued_jobs)proc.callenqueued_jobs=Set.new(queue_adapter.enqueued_jobs)check(enqueued_jobs-original_enqueued_jobs)enddefdoes_not_match?(proc)set_expected_number(:at_least,1)!matches?(proc)endend# @privateclassHaveBeenEnqueued<BaseFAILURE_MESSAGE_EXPECTATION_ACTION='enqueue'.freezeMESSAGE_EXPECTATION_ACTION='enqueued'.freezedefmatches?(job)@job=jobcheck(queue_adapter.enqueued_jobs)enddefdoes_not_match?(proc)set_expected_number(:at_least,1)!matches?(proc)endend# @privateclassHavePerformedJob<BaseFAILURE_MESSAGE_EXPECTATION_ACTION='perform'.freezeMESSAGE_EXPECTATION_ACTION='performed'.freezedefinitialize(job)super()@job=jobenddefmatches?(proc)raiseArgumentError,"have_performed_job only supports block expectations"unlessProc===procoriginal_performed_jobs_count=queue_adapter.performed_jobs.countproc.callin_block_jobs=queue_adapter.performed_jobs.drop(original_performed_jobs_count)check(in_block_jobs)endend# @privateclassHaveBeenPerformed<BaseFAILURE_MESSAGE_EXPECTATION_ACTION='perform'.freezeMESSAGE_EXPECTATION_ACTION='performed'.freezedefmatches?(job)@job=jobcheck(queue_adapter.performed_jobs)endendend# @api public# Passes if a job has been enqueued inside block. May chain at_least, at_most or exactly to specify a number of times.## @example# expect {# HeavyLiftingJob.perform_later# }.to have_enqueued_job## # Using alias# expect {# HeavyLiftingJob.perform_later# }.to enqueue_job## expect {# HelloJob.perform_later# HeavyLiftingJob.perform_later# }.to have_enqueued_job(HelloJob).exactly(:once)## expect {# 3.times { HelloJob.perform_later }# }.to have_enqueued_job(HelloJob).at_least(2).times## expect {# HelloJob.perform_later# }.to have_enqueued_job(HelloJob).at_most(:twice)## expect {# HelloJob.perform_later# HeavyLiftingJob.perform_later# }.to have_enqueued_job(HelloJob).and have_enqueued_job(HeavyLiftingJob)## expect {# HelloJob.set(wait_until: Date.tomorrow.noon, queue: "low").perform_later(42)# }.to have_enqueued_job.with(42).on_queue("low").at(Date.tomorrow.noon)## expect {# HelloJob.set(queue: "low").perform_later(42)# }.to have_enqueued_job.with(42).on_queue("low").at(:no_wait)## expect {# HelloJob.perform_later('rspec_rails', 'rails', 42)# }.to have_enqueued_job.with { |from, to, times|# # Perform more complex argument matching using dynamic arguments# expect(from).to include "_#{to}"# }defhave_enqueued_job(job=nil)check_active_job_adapterActiveJob::HaveEnqueuedJob.new(job)endalias_method:enqueue_job,:have_enqueued_job# @api public# Passes if a job has been enqueued. May chain at_least, at_most or exactly to specify a number of times.## @example# before { ActiveJob::Base.queue_adapter.enqueued_jobs.clear }## HeavyLiftingJob.perform_later# expect(HeavyLiftingJob).to have_been_enqueued## HelloJob.perform_later# HeavyLiftingJob.perform_later# expect(HeavyLiftingJob).to have_been_enqueued.exactly(:once)## 3.times { HelloJob.perform_later }# expect(HelloJob).to have_been_enqueued.at_least(2).times## HelloJob.perform_later# expect(HelloJob).to enqueue_job(HelloJob).at_most(:twice)## HelloJob.perform_later# HeavyLiftingJob.perform_later# expect(HelloJob).to have_been_enqueued# expect(HeavyLiftingJob).to have_been_enqueued## HelloJob.set(wait_until: Date.tomorrow.noon, queue: "low").perform_later(42)# expect(HelloJob).to have_been_enqueued.with(42).on_queue("low").at(Date.tomorrow.noon)## HelloJob.set(queue: "low").perform_later(42)# expect(HelloJob).to have_been_enqueued.with(42).on_queue("low").at(:no_wait)defhave_been_enqueuedcheck_active_job_adapterActiveJob::HaveBeenEnqueued.newend# @api public# Passes if a job has been performed inside block. May chain at_least, at_most or exactly to specify a number of times.## @example# expect {# perform_enqueued_jobs { HeavyLiftingJob.perform_later }# }.to have_performed_job## expect {# perform_enqueued_jobs {# HelloJob.perform_later# HeavyLiftingJob.perform_later# }# }.to have_performed_job(HelloJob).exactly(:once)## expect {# perform_enqueued_jobs { 3.times { HelloJob.perform_later } }# }.to have_performed_job(HelloJob).at_least(2).times## expect {# perform_enqueued_jobs { HelloJob.perform_later }# }.to have_performed_job(HelloJob).at_most(:twice)## expect {# perform_enqueued_jobs {# HelloJob.perform_later# HeavyLiftingJob.perform_later# }# }.to have_performed_job(HelloJob).and have_performed_job(HeavyLiftingJob)## expect {# perform_enqueued_jobs {# HelloJob.set(wait_until: Date.tomorrow.noon, queue: "low").perform_later(42)# }# }.to have_performed_job.with(42).on_queue("low").at(Date.tomorrow.noon)defhave_performed_job(job=nil)check_active_job_adapterActiveJob::HavePerformedJob.new(job)endalias_method:perform_job,:have_performed_job# @api public# Passes if a job has been performed. May chain at_least, at_most or exactly to specify a number of times.## @example# before do# ActiveJob::Base.queue_adapter.performed_jobs.clear# ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true# ActiveJob::Base.queue_adapter.perform_enqueued_at_jobs = true# end## HeavyLiftingJob.perform_later# expect(HeavyLiftingJob).to have_been_performed## HelloJob.perform_later# HeavyLiftingJob.perform_later# expect(HeavyLiftingJob).to have_been_performed.exactly(:once)## 3.times { HelloJob.perform_later }# expect(HelloJob).to have_been_performed.at_least(2).times## HelloJob.perform_later# HeavyLiftingJob.perform_later# expect(HelloJob).to have_been_performed# expect(HeavyLiftingJob).to have_been_performed## HelloJob.set(wait_until: Date.tomorrow.noon, queue: "low").perform_later(42)# expect(HelloJob).to have_been_performed.with(42).on_queue("low").at(Date.tomorrow.noon)defhave_been_performedcheck_active_job_adapterActiveJob::HaveBeenPerformed.newendprivate# @privatedefcheck_active_job_adapterreturnif::ActiveJob::QueueAdapters::TestAdapter===::ActiveJob::Base.queue_adapterraiseStandardError,"To use ActiveJob matchers set `ActiveJob::Base.queue_adapter = :test`"endendendend