Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change matcher protocol #373

Merged
merged 16 commits into from
Dec 3, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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