Skip to content

Commit

Permalink
Merge pull request #373 from rspec/change-matcher-protocol
Browse files Browse the repository at this point in the history
Change matcher protocol
  • Loading branch information
myronmarston committed Dec 3, 2013
2 parents 1e8b1aa + a0f0149 commit b9e21b9
Show file tree
Hide file tree
Showing 38 changed files with 521 additions and 253 deletions.
16 changes: 15 additions & 1 deletion Changelog.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
### 3.0.0.beta2 Development
[full changelog](http://github.com/rspec/rspec-expectations/compare/v3.0.0.beta1...v3.0.0.beta2)

Ehancements:
Enhancements:

* Simplify the failure message of the `be` matcher when matching against:
`true`, `false` and `nil`. (Sam Phippen)
* Update matcher protocol and custom matcher DSL to better align
with the newer `expect` syntax. If you want your matchers to
maintain compatibility with multiple versions of RSpec, you can
alias the new names to the old. (Myron Marston)
* `failure_message_for_should` => `failure_message`
* `failure_message_for_should_not` => `failure_message_when_negated`
* `match_for_should` => `match`
* `match_for_should_not` => `match_when_negated`

Breaking Changes for 3.0.0:

Expand All @@ -17,6 +25,12 @@ Bug Fixes:

* Fix wrong matcher descriptions with falsey expected value (yujinakayama)

Deprecations:

* Retain support for RSpec 2 matcher protocol (e.g. for matchers
in 3rd party extension gems like `shoulda`), but it will print
a deprecation warning. (Myron Marston)

### 3.0.0.beta1 / 2013-11-07
[full changelog](http://github.com/rspec/rspec-expectations/compare/v2.99.0.beta1...v3.0.0.beta1)

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# RSpec Expectations [![Build Status](https://secure.travis-ci.org/rspec/rspec-expectations.png?branch=master)](http://travis-ci.org/rspec/rspec-expectations) [![Code Climate](https://codeclimate.com/github/rspec/rspec-expectations.png)](https://codeclimate.com/github/rspec/rspec-expectations) [![Coverage Status](https://coveralls.io/repos/rspec/rspec-expectations/badge.png?branch=master)](https://coveralls.io/r/rspec/rspec-expectations?branch=master)
# RSpec Expectations [![Build Status](https://secure.travis-ci.org/rspec/rspec-expectations.png?branch=master)](http://travis-ci.org/rspec/rspec-expectations) [![Code Climate](https://codeclimate.com/github/rspec/rspec-expectations.png)](https://codeclimate.com/github/rspec/rspec-expectations)

RSpec::Expectations lets you express expected outcomes on an object in an
example.
Expand Down
4 changes: 2 additions & 2 deletions features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ Note: You can also use `expect(..).to_not` instead of `expect(..).not_to`.
A Matcher is any object that responds to the following methods:

matches?(actual)
failure_message_for_should
failure_message

These methods are also part of the matcher protocol, but are optional:

does_not_match?(actual)
failure_message_for_should_not
failure_message_when_negated
description

RSpec ships with a number of built-in matchers and a DSL for writing custom
Expand Down
12 changes: 6 additions & 6 deletions features/custom_matchers/define_matcher.feature
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Feature: define matcher
And the output should contain "expected 9 to be a multiple of 4"
And the output should contain "expected 9 not to be a multiple of 3"

Scenario: overriding the failure_message_for_should
Scenario: overriding the failure_message
Given a file named "matcher_with_failure_message_spec.rb" with:
"""ruby
require 'rspec/expectations'
Expand All @@ -55,7 +55,7 @@ Feature: define matcher
match do |actual|
actual % expected == 0
end
failure_message_for_should do |actual|
failure_message do |actual|
"expected that #{actual} would be a multiple of #{expected}"
end
end
Expand All @@ -70,7 +70,7 @@ Feature: define matcher
And the stdout should contain "1 example, 1 failure"
And the stdout should contain "expected that 9 would be a multiple of 4"

Scenario: overriding the failure_message_for_should_not
Scenario: overriding the failure_message_when_negated
Given a file named "matcher_with_failure_for_message_spec.rb" with:
"""ruby
require 'rspec/expectations'
Expand All @@ -79,7 +79,7 @@ Feature: define matcher
match do |actual|
actual % expected == 0
end
failure_message_for_should_not do |actual|
failure_message_when_negated do |actual|
"expected that #{actual} would not be a multiple of #{expected}"
end
end
Expand Down Expand Up @@ -261,11 +261,11 @@ Feature: define matcher
Given a file named "matcher_with_separate_should_not_logic_spec.rb" with:
"""ruby
RSpec::Matchers.define :contain do |*expected|
match_for_should do |actual|
match do |actual|
expected.all? { |e| actual.include?(e) }
end
match_for_should_not do |actual|
match_when_negated do |actual|
expected.none? { |e| actual.include?(e) }
end
end
Expand Down
37 changes: 23 additions & 14 deletions lib/rspec/expectations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,40 @@
require 'rspec/expectations/diff_presenter'

module RSpec
# RSpec::Expectations adds two instance methods to every object:
# RSpec::Expectations provides a simple, readable API to express
# the expected outcomes in a code example. To express an expected
# outcome, wrap an object or block in `expect`, call `to` or `to_not`
# (aliased as `not_to`) and pass it a matcher object:
#
# should(matcher=nil)
# should_not(matcher=nil)
# expect(order.total).to eq(Money.new(5.55, :USD))
# expect(list).to include(user)
# expect(message).not_to match(/foo/)
# expect { do_something }.to raise_error
#
# Both methods take an optional matcher object (See
# [RSpec::Matchers](../RSpec/Matchers)). When `should` is invoked with a
# matcher, it turns around and calls `matcher.matches?(self)`. For example,
# The last form (the block form) is needed to match against ruby constructs
# that are not objects, but can only be observed when executing a block
# of code. This includes raising errors, throwing symbols, yielding,
# and changing values.
#
# When `expect(...).to` is invoked with a matcher, it turns around
# and calls `matcher.matches?(<object wrapped by expect>)`. For example,
# in the expression:
#
# order.total.should eq(Money.new(5.55, :USD))
# expect(order.total).to eq(Money.new(5.55, :USD))
#
# the `should` method invokes the equivalent of `eq.matches?(order.total)`. If
# `matches?` returns true, the expectation is met and execution continues. If
# `false`, then the spec fails with the message returned by
# `eq.failure_message_for_should`.
# ...`eq(Money.new(5.55, :USD))` returns a matcher object, and it results
# in the equivalent of `eq.matches?(order.total)`. If `matches?` returns
# `true`, the expectation is met and execution continues. If `false`, then
# the spec fails with the message returned by `eq.failure_message`.
#
# Given the expression:
#
# order.entries.should_not include(entry)
# expet(order.entries).not_to include(entry)
#
# the `should_not` method invokes the equivalent of
# ...the `not_to` method (also available as `to_not`) invokes the equivalent of
# `include.matches?(order.entries)`, but it interprets `false` as success, and
# `true` as a failure, using the message generated by
# `eq.failure_message_for_should_not`.
# `eq.failure_message_when_negated`.
#
# rspec-expectations ships with a standard set of useful matchers, and writing
# your own matchers is quite simple.
Expand Down
140 changes: 113 additions & 27 deletions lib/rspec/expectations/handler.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module RSpec
module Expectations

# @api private
class ExpectationHandler
def self.check_message(msg)
unless msg.nil? || msg.respond_to?(:to_str) || msg.respond_to?(:call)
Expand All @@ -11,24 +12,25 @@ def self.check_message(msg)
].join
end
end
end

class PositiveExpectationHandler < ExpectationHandler
# Returns an RSpec-3+ compatible matcher, wrapping a legacy one
# in an adapter if necessary.
#
# @api private
def self.modern_matcher_from(matcher)
LegacyMacherAdapter::RSpec2.wrap(matcher) ||
LegacyMacherAdapter::RSpec1.wrap(matcher) || matcher
end

def self.handle_matcher(actual, matcher, message=nil, &block)
def self.handle_matcher(matcher, message, failure_message_method)
check_message(message)
::RSpec::Matchers.last_should = :should
matcher = modern_matcher_from(matcher)
::RSpec::Matchers.last_matcher = matcher
return ::RSpec::Matchers::BuiltIn::PositiveOperatorMatcher.new(actual) if matcher.nil?

match = matcher.matches?(actual, &block)
return match if match
yield

message = message.call if message.respond_to?(:call)

message ||= matcher.respond_to?(:failure_message_for_should) ?
matcher.failure_message_for_should :
matcher.failure_message
message ||= matcher.__send__(failure_message_method)

if matcher.respond_to?(:diffable?) && matcher.diffable?
::RSpec::Expectations.fail_with message, matcher.expected, matcher.actual
Expand All @@ -38,28 +40,112 @@ def self.handle_matcher(actual, matcher, message=nil, &block)
end
end

# @api private
class PositiveExpectationHandler < ExpectationHandler
def self.handle_matcher(actual, matcher, message=nil, &block)
super(matcher, message, :failure_message) do
::RSpec::Matchers.last_should = :should
return ::RSpec::Matchers::BuiltIn::PositiveOperatorMatcher.new(actual) if matcher.nil?

match = matcher.matches?(actual, &block)
return match if match
end
end
end

# @api private
class NegativeExpectationHandler < ExpectationHandler
def self.handle_matcher(actual, matcher, message=nil, &block)
check_message(message)
::RSpec::Matchers.last_should = :should_not
::RSpec::Matchers.last_matcher = matcher
return ::RSpec::Matchers::BuiltIn::NegativeOperatorMatcher.new(actual) if matcher.nil?
super(matcher, message, :failure_message_when_negated) do
::RSpec::Matchers.last_should = :should_not
return ::RSpec::Matchers::BuiltIn::NegativeOperatorMatcher.new(actual) if matcher.nil?

match = matcher.respond_to?(:does_not_match?) ?
!matcher.does_not_match?(actual, &block) :
matcher.matches?(actual, &block)
return match unless match
match = if matcher.respond_to?(:does_not_match?)
!matcher.does_not_match?(actual, &block)
else
matcher.matches?(actual, &block)
end
return match unless match
end
end
end

message = message.call if message.respond_to?(:call)
# Wraps a matcher written against one of the legacy protocols in
# order to present the current protocol.
#
# @api private
class LegacyMacherAdapter < defined?(::BasicObject) ? ::BasicObject : ::Object
attr_reader :matcher

message ||= matcher.respond_to?(:failure_message_for_should_not) ?
matcher.failure_message_for_should_not :
matcher.negative_failure_message
def initialize(matcher)
@matcher = matcher

if matcher.respond_to?(:diffable?) && matcher.diffable?
::RSpec::Expectations.fail_with message, matcher.expected, matcher.actual
else
::RSpec::Expectations.fail_with message
::RSpec.warn_deprecation(<<-EOS.gsub(/^\s+\|/, ''))
|--------------------------------------------------------------------------
|#{matcher.class.name || matcher.inspect} implements a legacy RSpec matcher
|protocol. For the current protocol you should expose the failure messages
|via the `failure_message` and `failure_message_when_negated` methods.
|(Used from #{CallerFilter.first_non_rspec_line})
|--------------------------------------------------------------------------
EOS
end

def method_missing(name, *args, &block)
@matcher.__send__(name, *args, &block)
end

def respond_to?(name, *args)
super || @matcher.respond_to?(name, *args)
end

def self.wrap(matcher)
new(matcher) if interface_matches?(matcher)
end

# Starting in RSpec 1.2 (and continuing through all 2.x releases),
# the failure message protocol was:
# * `failure_message_for_should`
# * `failure_message_for_should_not`
# @api private
class RSpec2 < self
def failure_message
matcher.failure_message_for_should
end

def failure_message_when_negated
matcher.failure_message_for_should_not
end

def self.interface_matches?(matcher)
(
!matcher.respond_to?(:failure_message) &&
matcher.respond_to?(:failure_message_for_should)
) || (
!matcher.respond_to?(:failure_message_when_negated) &&
matcher.respond_to?(:failure_message_for_should_not)
)
end
end

# Before RSpec 1.2, the failure message protocol was:
# * `failure_message`
# * `negative_failure_message`
# @api private
class RSpec1 < self
def failure_message
matcher.failure_message
end

def failure_message_when_negated
matcher.negative_failure_message
end

# Note: `failure_message` is part of the RSpec 3 protocol
# (paired with `failure_message_when_negated`), so we don't check
# for `failure_message` here.
def self.interface_matches?(matcher)
!matcher.respond_to?(:failure_message_when_negated) &&
matcher.respond_to?(:negative_failure_message)
end
end
end
Expand Down
Loading

0 comments on commit b9e21b9

Please sign in to comment.