123456789_123456789_123456789_123456789_123456789_

Class: RSpec::Matchers::BuiltIn::Compound::NestedEvaluator Private

Relationships & Source Files
Inherits: Object
Defined in: rspec-expectations/lib/rspec/matchers/built_in/compound.rb

Overview

Normally, we evaluate the matching sequentially. For an expression like ‘expect(x).to foo.and bar`, this becomes:

expect(x).to foo
expect(x).to bar

For block expectations, we need to nest them instead, so that ‘expect { x }.to foo.and bar` becomes:

expect {
  expect { x }.to foo
}.to bar

This is necessary so that the ‘expect` block is only executed once.

Class Method Summary

Instance Method Summary

Constructor Details

.new(actual, matcher_1, matcher_2) ⇒ NestedEvaluator

[ GitHub ]

  
# File 'rspec-expectations/lib/rspec/matchers/built_in/compound.rb', line 157

def initialize(actual, matcher_1, matcher_2)
  @actual        = actual
  @matcher_1     = matcher_1
  @matcher_2     = matcher_2
  @match_results = {}

  inner, outer = order_block_matchers

  @match_results[outer] = outer.matches?(Proc.new do |*args|
    @match_results[inner] = inner.matches?(inner_matcher_block(args))
  end)
end

Class Method Details

.matcher_expects_call_stack_jump?(matcher) ⇒ Boolean (private)

[ GitHub ]

  
# File 'rspec-expectations/lib/rspec/matchers/built_in/compound.rb', line 233

def self.matcher_expects_call_stack_jump?(matcher)
  matcher.expects_call_stack_jump?
rescue NoMethodError
  false
end

Instance Method Details

#inner_matcher_block(outer_args) (private)

Some block matchers (such as ‘yield_xyz`) pass args to the expect block. When such a matcher is used as the outer matcher, we need to forward the the args on to the expect block.

[ GitHub ]

  
# File 'rspec-expectations/lib/rspec/matchers/built_in/compound.rb', line 184

def inner_matcher_block(outer_args)
  return @actual if outer_args.empty?

  Proc.new do |*inner_args|
    unless inner_args.empty?
      raise ArgumentError, "(#{@matcher_1.description}) and " \
        "(#{@matcher_2.description}) cannot be combined in a compound expectation " \
        "since both matchers pass arguments to the block."
    end

    @actual.call(*outer_args)
  end
end

#matcher_matches?(matcher) ⇒ Boolean

[ GitHub ]

  
# File 'rspec-expectations/lib/rspec/matchers/built_in/compound.rb', line 170

def matcher_matches?(matcher)
  @match_results.fetch(matcher) do
    raise ArgumentError, "Your #{matcher.description} has no match " \
     "results, this can occur when an unexpected call stack or " \
     "local jump occurs. Perhaps one of your matchers needs to " \
     "declare `expects_call_stack_jump?` as `true`?"
  end
end

#order_block_matchers (private)

For a matcher like ‘raise_error` or throw_symbol, where the block will jump up the call stack, we need to order things so that it is the inner matcher. For example, we need it to be this:

expect {
  expect {
    x += 1
    raise "boom"
  }.to raise_error("boom")
}.to change { x }.by(1)

…rather than:

expect {
  expect {
    x += 1
    raise "boom"
  }.to change { x }.by(1)
}.to raise_error("boom")

In the latter case, the after-block logic in the ‘change` matcher would never get executed because the `raise “boom”` line would jump to the rescue in the raise_error logic, so only the former case will work properly.

This method figures out which matcher should be the inner matcher and which should be the outer matcher.

Raises:

  • (ArgumentError)
[ GitHub ]

  
# File 'rspec-expectations/lib/rspec/matchers/built_in/compound.rb', line 224

def order_block_matchers
  return @matcher_1, @matcher_2 unless self.class.matcher_expects_call_stack_jump?(@matcher_2)
  return @matcher_2, @matcher_1 unless self.class.matcher_expects_call_stack_jump?(@matcher_1)

  raise ArgumentError, "(#{@matcher_1.description}) and " \
    "(#{@matcher_2.description}) cannot be combined in a compound expectation " \
    "because they both expect a call stack jump."
end