Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Octocat-spinner-32-eaf2f5

Cannot retrieve contributors at this time

file 511 lines (455 sloc) 15.785 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510
module RSpec
  module Mocks

    class MessageExpectation
      # @private
      attr_reader :message
      attr_writer :expected_received_count, :method_block, :expected_from, :argument_expectation
      protected :expected_received_count=, :method_block=, :expected_from=
      attr_accessor :error_generator
      protected :error_generator, :error_generator=

      # @private
      def initialize(error_generator, expectation_ordering, expected_from, message, method_block, expected_received_count=1, opts={}, &implementation)
        @error_generator = error_generator
        @error_generator.opts = opts
        @expected_from = expected_from
        @message = message
        @method_block = method_block
        @return_block = nil
        @actual_received_count = 0
        @expected_received_count = expected_received_count
        @argument_expectation = ArgumentExpectation.new(ArgumentMatchers::AnyArgsMatcher.new)
        @consecutive = false
        @exception_to_raise = nil
        @args_to_throw = []
        @order_group = expectation_ordering
        @at_least = nil
        @at_most = nil
        @exactly = nil
        @args_to_yield = []
        @failed_fast = nil
        @args_to_yield_were_cloned = false
        @return_block = implementation
        @eval_context = nil
      end

      # @private
      def build_child(expected_from, method_block, expected_received_count, opts={})
        child = clone
        child.expected_from = expected_from
        child.method_block = method_block
        child.expected_received_count = expected_received_count
        child.clear_actual_received_count!
        new_gen = error_generator.clone
        new_gen.opts = opts
        child.error_generator = new_gen
        child.clone_args_to_yield(*@args_to_yield)
        child.argument_expectation = ArgumentExpectation.new(ArgumentMatchers::AnyArgsMatcher.new)
        child
      end

      # @private
      def expected_args
        @argument_expectation.args
      end

      def at_least_once?
        @at_least && @expected_received_count == 1
      end

      # @overload and_return(value)
      # @overload and_return(first_value, second_value)
      # @overload and_return(&block)
      #
      # Tells the object to return a value when it receives the message. Given
      # more than one value, the first value is returned the first time the
      # message is received, the second value is returned the next time, etc,
      # etc.
      #
      # If the message is received more times than there are values, the last
      # value is received for every subsequent call.
      #
      # The block format is still supported, but is unofficially deprecated in
      # favor of just passing a block to the stub method.
      #
      # @example
      #
      # counter.stub(:count).and_return(1)
      # counter.count # => 1
      # counter.count # => 1
      #
      # counter.stub(:count).and_return(1,2,3)
      # counter.count # => 1
      # counter.count # => 2
      # counter.count # => 3
      # counter.count # => 3
      # counter.count # => 3
      # # etc
      #
      # # Supported, but ...
      # counter.stub(:count).and_return { 1 }
      # counter.count # => 1
      #
      # # ... this is prefered
      # counter.stub(:count) { 1 }
      # counter.count # => 1
      def and_return(*values, &return_block)
        Kernel::raise AmbiguousReturnError unless @method_block.nil?
        case values.size
        when 0 then value = nil
        when 1 then value = values[0]
        else
          value = values
          @consecutive = true
          @expected_received_count = values.size if !ignoring_args? &&
            @expected_received_count < values.size
        end
        @return_block = block_given? ? return_block : lambda { value }
      end

      # @overload and_raise
      # @overload and_raise(ExceptionClass)
      # @overload and_raise(exception_instance)
      #
      # Tells the object to raise an exception when the message is received.
      #
      # @note
      #
      # When you pass an exception class, the MessageExpectation will raise
      # an instance of it, creating it with `new`. If the exception class
      # initializer requires any parameters, you must pass in an instance and
      # not the class.
      #
      # @example
      #
      # car.stub(:go).and_raise
      # car.stub(:go).and_raise(OutOfGas)
      # car.stub(:go).and_raise(OutOfGas.new(2, :oz))
      def and_raise(exception=Exception)
        @exception_to_raise = exception
      end

      # @overload and_throw(symbol)
      # @overload and_throw(symbol, object)
      #
      # Tells the object to throw a symbol (with the object if that form is
      # used) when the message is received.
      #
      # @example
      #
      # car.stub(:go).and_throw(:out_of_gas)
      # car.stub(:go).and_throw(:out_of_gas, :level => 0.1)
      def and_throw(symbol, object = nil)
        @args_to_throw << symbol
        @args_to_throw << object if object
      end

      # Tells the object to yield one or more args to a block when the message
      # is received.
      #
      # @example
      #
      # stream.stub(:open).and_yield(StringIO.new)
      def and_yield(*args, &block)
        if @args_to_yield_were_cloned
          @args_to_yield.clear
          @args_to_yield_were_cloned = false
        end

        if block
          @eval_context = Object.new
          @eval_context.extend RSpec::Mocks::InstanceExec
          yield @eval_context
        end

        @args_to_yield << args
        self
      end

      # @private
      def matches?(message, *args)
        @message == message and @argument_expectation.args_match?(*args)
      end

      # @private
      def invoke(*args, &block)
        if @expected_received_count == 0
          @failed_fast = true
          @actual_received_count += 1
          @error_generator.raise_expectation_error(@message, @expected_received_count, @actual_received_count, *args)
        end

        @order_group.handle_order_constraint self

        begin
          begin
            raise(@exception_to_raise) unless @exception_to_raise.nil?
          rescue ArgumentError => e
            raise e.exception(<<-MESSAGE)
'and_raise' can only accept an Exception class if an instance can be constructed with no arguments.
#{@exception_to_raise.to_s}'s initialize method requires #{@exception_to_raise.instance_method(:initialize).arity} arguments, so you have to supply an instance instead.
MESSAGE
          end
          Kernel::throw(*@args_to_throw) unless @args_to_throw.empty?

          default_return_val = if !@method_block.nil?
                                 invoke_method_block(*args, &block)
                               elsif !@args_to_yield.empty? || @eval_context
                                 invoke_with_yield(&block)
                               else
                                 nil
                               end

          if @consecutive
            invoke_consecutive_return_block(*args, &block)
          elsif @return_block
            invoke_return_block(*args, &block)
          else
            default_return_val
          end
        ensure
          @actual_received_count += 1
        end
      end

      # @private
      def called_max_times?
        @expected_received_count != :any && @expected_received_count > 0 &&
          @actual_received_count >= @expected_received_count
      end

      # @private
      def matches_name_but_not_args(message, *args)
        @message == message and not @argument_expectation.args_match?(*args)
      end

      # @private
      def verify_messages_received
        generate_error unless expected_messages_received? || failed_fast?
      rescue RSpec::Mocks::MockExpectationError => error
        error.backtrace.insert(0, @expected_from)
        Kernel::raise error
      end

      # @private
      def expected_messages_received?
        ignoring_args? || matches_exact_count? || matches_at_least_count? || matches_at_most_count?
      end

      # @private
      def ignoring_args?
        @expected_received_count == :any
      end

      # @private
      def matches_at_least_count?
        @at_least && @actual_received_count >= @expected_received_count
      end

      # @private
      def matches_at_most_count?
        @at_most && @actual_received_count <= @expected_received_count
      end

      # @private
      def matches_exact_count?
        @expected_received_count == @actual_received_count
      end

      # @private
      def similar_messages
        @similar_messages ||= []
      end

      # @private
      def advise(*args)
        similar_messages << args
      end

      # @private
      def generate_error
        if similar_messages.empty?
          @error_generator.raise_expectation_error(@message, @expected_received_count, @actual_received_count, *@argument_expectation.args)
        else
          @error_generator.raise_similar_message_args_error(self, *@similar_messages)
        end
      end

      def raise_out_of_order_error
        @error_generator.raise_out_of_order_error @message
      end

      # Constrains a stub or message expectation to invocations with specific
      # arguments.
      #
      # With a stub, if the message might be received with other args as well,
      # you should stub a default value first, and then stub or mock the same
      # message using `with` to constrain to specific arguments.
      #
      # A message expectation will fail if the message is received with different
      # arguments.
      #
      # @example
      #
      # cart.stub(:add) { :failure }
      # cart.stub(:add).with(Book.new(:isbn => 1934356379)) { :success }
      # cart.add(Book.new(:isbn => 1234567890))
      # # => :failure
      # cart.add(Book.new(:isbn => 1934356379))
      # # => :success
      #
      # cart.should_receive(:add).with(Book.new(:isbn => 1934356379)) { :success }
      # cart.add(Book.new(:isbn => 1234567890))
      # # => failed expectation
      # cart.add(Book.new(:isbn => 1934356379))
      # # => passes
      def with(*args, &block)
        @return_block = block if block_given? unless args.empty?
        @argument_expectation = ArgumentExpectation.new(*args, &block)
        self
      end

      # Constrain a message expectation to be received a specific number of
      # times.
      #
      # @example
      #
      # dealer.should_recieve(:deal_card).exactly(10).times
      def exactly(n, &block)
        @method_block = block if block
        set_expected_received_count :exactly, n
        self
      end

      # Constrain a message expectation to be received at least a specific
      # number of times.
      #
      # @example
      #
      # dealer.should_recieve(:deal_card).at_least(9).times
      def at_least(n, &block)
        @method_block = block if block
        set_expected_received_count :at_least, n
        self
      end

      # Constrain a message expectation to be received at most a specific
      # number of times.
      #
      # @example
      #
      # dealer.should_recieve(:deal_card).at_most(10).times
      def at_most(n, &block)
        @method_block = block if block
        set_expected_received_count :at_most, n
        self
      end

      # Syntactic sugar for `exactly`, `at_least` and `at_most`
      #
      # @example
      #
      # dealer.should_recieve(:deal_card).exactly(10).times
      # dealer.should_recieve(:deal_card).at_least(10).times
      # dealer.should_recieve(:deal_card).at_most(10).times
      def times(&block)
        @method_block = block if block
        self
      end


      # Allows an expected message to be received any number of times.
      def any_number_of_times(&block)
        @method_block = block if block
        @expected_received_count = :any
        self
      end

      # Expect a message not to be received at all.
      #
      # @example
      #
      # car.should_receive(:stop).never
      def never
        @expected_received_count = 0
        self
      end

      # Expect a message to be received exactly one time.
      #
      # @example
      #
      # car.should_receive(:go).once
      def once(&block)
        @method_block = block if block
        set_expected_received_count :exactly, 1
        self
      end

      # Expect a message to be received exactly two times.
      #
      # @example
      #
      # car.should_receive(:go).twice
      def twice(&block)
        @method_block = block if block
        set_expected_received_count :exactly, 2
        self
      end

      # Expect messages to be received in a specific order.
      #
      # @example
      #
      # api.should_receive(:prepare).ordered
      # api.should_receive(:run).ordered
      # api.should_receive(:finish).ordered
      def ordered(&block)
        @method_block = block if block
        @order_group.register(self)
        @ordered = true
        self
      end

      # @private
      def negative_expectation_for?(message)
        return false
      end

      # @private
      def actual_received_count_matters?
        @at_least || @at_most || @exactly
      end

      # @private
      def increase_actual_received_count!
        @actual_received_count += 1
      end

      protected

      def invoke_method_block(*args, &block)
        begin
          @method_block.call(*args, &block)
        rescue => detail
          @error_generator.raise_block_failed_error(@message, detail.message)
        end
      end

      def invoke_with_yield(&block)
        if block.nil?
          @error_generator.raise_missing_block_error @args_to_yield
        end
        value = nil
        @args_to_yield.each do |args_to_yield_this_time|
          if block.arity > -1 && args_to_yield_this_time.length != block.arity
            @error_generator.raise_wrong_arity_error args_to_yield_this_time, block.arity
          end
          value = eval_block(*args_to_yield_this_time, &block)
        end
        value
      end

      def eval_block(*args, &block)
        if @eval_context
          @eval_context.instance_exec(*args, &block)
        else
          block.call(*args)
        end
      end

      def invoke_consecutive_return_block(*args, &block)
        value = invoke_return_block(*args, &block)
        index = [@actual_received_count, value.size-1].min
        value[index]
      end

      def invoke_return_block(*args, &block)
        @return_block.arity == 0 ? @return_block.call(&block) : @return_block.call(*args, &block)
      end

      def clone_args_to_yield(*args)
        @args_to_yield = args.clone
        @args_to_yield_were_cloned = true
      end

      def failed_fast?
        @failed_fast
      end

      def set_expected_received_count(relativity, n)
        @at_least = (relativity == :at_least)
        @at_most = (relativity == :at_most)
        @exactly = (relativity == :exactly)
        @expected_received_count = case n
                                   when Numeric then n
                                   when :once then 1
                                   when :twice then 2
                                   end
      end

      def clear_actual_received_count!
        @actual_received_count = 0
      end
    end

    # @private
    class NegativeMessageExpectation < MessageExpectation
      # @private
      def initialize(error_generator, expectation_ordering, expected_from, message, method_block)
        super(error_generator, expectation_ordering, expected_from, message, method_block, 0)
      end

      # @private
      def negative_expectation_for?(message)
        return @message == message
      end
    end
  end
end
Something went wrong with that request. Please try again.