class RSpec::Rails::Matchers::ActiveJob::Base
@private
rubocop: disable Metrics/ClassLength
def arguments_match?(job)
def arguments_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) else true end end
def at(time_or_date)
def at(time_or_date) case time_or_date when Time then @at = Time.at(time_or_date.to_f) else @at = time_or_date end self end
def at_least(count)
def at_least(count) set_expected_number(:at_least, count) self end
def at_match?(job)
def at_match?(job) return true unless @at return job[:at].nil? if @at == :no_wait return false unless job[:at] scheduled_at = Time.at(job[:at]) values_match?(@at, scheduled_at) || check_for_inprecise_value(scheduled_at) end
def at_most(count)
def at_most(count) set_expected_number(:at_most, count) self end
def at_priority(priority)
def at_priority(priority) @priority = priority.to_i self end
def base_job_message(job)
def base_job_message(job) msg_parts = [] msg_parts << "with #{deserialize_arguments(job)}" if job[:args].any? msg_parts << "on queue #{job[:queue]}" if job[:queue] msg_parts << "at #{Time.at(job[:at])}" if job[:at] msg_parts << if job[:priority] "with priority #{job[:priority]}" else "with no priority specified" end "#{job[:job].name} job".tap do |msg| msg << " #{msg_parts.join(', ')}" if msg_parts.any? end end
def base_message
def base_message "#{message_expectation_modifier} #{@expected_number} jobs,".tap do |msg| msg << " with #{@args}," if @args.any? msg << " on queue #{@queue}," if @queue msg << " at #{@at.inspect}," if @at msg << " with priority #{@priority}," if @priority msg << " but #{self.class::MESSAGE_EXPECTATION_ACTION} #{@matching_jobs_count}" end end
def check(jobs)
def check(jobs) @matching_jobs, @unmatching_jobs = jobs.partition do |job| if matches_constraints?(job) args = deserialize_arguments(job) @block.call(*args) true else false end end if (signature_mismatch = detect_args_signature_mismatch(@matching_jobs)) @failure_message = signature_mismatch return false end @matching_jobs_count = @matching_jobs.size case @expectation_type when :exactly then @expected_number == @matching_jobs_count when :at_most then @expected_number >= @matching_jobs_count when :at_least then @expected_number <= @matching_jobs_count end end
def check_args_signature_mismatch(job_class, job_method, args)
def check_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) unless verifier.valid? "Incorrect arguments passed to #{job_class.name}: #{verifier.error_message}" end end
def check_for_inprecise_value(scheduled_at)
def check_for_inprecise_value(scheduled_at) return unless Time === @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. | | WARNING false end
def deserialize_arguments(job)
def deserialize_arguments(job) ::ActiveJob::Arguments.deserialize(job[:args]) rescue ::ActiveJob::DeserializationError job[:args] end
def detect_args_signature_mismatch(jobs)
def detect_args_signature_mismatch(jobs) return if skip_signature_verification? jobs.each do |job| args = deserialize_arguments(job) if (signature_mismatch = check_args_signature_mismatch(job.fetch(:job), :perform, args)) return signature_mismatch end end nil end
def exactly(count)
def exactly(count) set_expected_number(:exactly, count) self end
def failure_message
def failure_message return @failure_message if defined?(@failure_message) "expected to #{self.class::FAILURE_MESSAGE_EXPECTATION_ACTION} #{base_message}".tap do |msg| if @unmatching_jobs.any? msg << "\nQueued jobs:" @unmatching_jobs.each do |job| msg << "\n #{base_job_message(job)}" end end end end
def failure_message_when_negated
def failure_message_when_negated "expected not to #{self.class::FAILURE_MESSAGE_EXPECTATION_ACTION} #{base_message}" end
def initialize
def initialize @args = [] @queue = nil @priority = nil @at = nil @block = proc { } set_expected_number(:exactly, 1) end
def job_matches?(job)
def job_matches?(job) @job ? @job == job[:job] : true end
def matches_constraints?(job)
def matches_constraints?(job) job_matches?(job) && arguments_match?(job) && queue_match?(job) && at_match?(job) && priority_match?(job) end
def message_expectation_modifier
def message_expectation_modifier case @expectation_type when :exactly then "exactly" when :at_most then "at most" when :at_least then "at least" end end
def on_queue(queue)
def on_queue(queue) @queue = queue.to_s self end
def once
def once exactly(:once) end
def priority_match?(job)
def priority_match?(job) return true unless @priority @priority == job[:priority] end
def queue_adapter
def queue_adapter ::ActiveJob::Base.queue_adapter end
def queue_match?(job)
def queue_match?(job) return true unless @queue @queue == job[:queue] end
def serialize_and_deserialize_arguments(args)
def serialize_and_deserialize_arguments(args) serialized = ::ActiveJob::Arguments.serialize(args) ::ActiveJob::Arguments.deserialize(serialized) rescue ::ActiveJob::SerializationError args end
def set_expected_number(relativity, count)
def set_expected_number(relativity, count) @expectation_type = relativity @expected_number = case count when :once then 1 when :twice then 2 when :thrice then 3 else Integer(count) end end
def skip_signature_verification?
def skip_signature_verification? return true unless defined?(::RSpec::Mocks) && (::RSpec::Mocks.respond_to?(:configuration)) !RSpec::Mocks.configuration.verify_partial_doubles? || RSpec::Mocks.configuration.temporarily_suppress_partial_double_verification end
def supports_block_expectations?
def supports_block_expectations? true end
def thrice
def thrice exactly(:thrice) end
def times
def times self end
def twice
def twice exactly(:twice) end
def with(*args, &block)
def with(*args, &block) @args = args @block = block if block.present? self end