Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Comparing changes

Choose two branches to see what's changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
base fork: rspec/rspec-expectations
...
head fork: rspec/rspec-expectations
Checking mergeability… Don't worry, you can still create the pull request.
  • 11 commits
  • 8 files changed
  • 0 commit comments
  • 3 contributors
Commits on Nov 13, 2012
@alindeman alindeman Fixes changes link 5baacca
Commits on Dec 04, 2012
@alindeman alindeman OperatorMatcher considers ancestor chain
* Related #191
e5e5a74
Commits on Dec 05, 2012
@myronmarston myronmarston Merge pull request #192 from alindeman/operator_registry_considers_an…
…cestors

OperatorMatcher considers ancestor chain
abefb76
Commits on Dec 06, 2012
@myronmarston myronmarston Provide a clear failure message when using `{}.should =~ {}`.
The match_array matcher (delegated to by `=~` for enumerables) is
not meant to be used for hashes, but the `should =~` syntax doesn't
make that obvious. Previously, you would get a failure like:

     Failure/Error: actual.should =~ actual
       expected: {:foo=>"bar"}
            got: {:foo=>"bar"} (using =~)

...which is pretty confusing. Our `match_array` matcher already
includes handling for invalid arguments (such as hashes) to return
a clear failure message, but it wasn't being used for an expression
like `{}.should =~ {}` because it was only registered as an operator
matcher for `Array`. This changes it so that it is registered as an
operator matcher for any Enumerable. This improves the failure message
for enumerable types like hash and set, but on its own, it could cause
breakage for things like 1.8 strings that are Enumerable but also
define a reasonable `=~`.  The fix here changes the operator matcher
delegation logic so that it only delegates to the registered matcher
if the object has the generic Kernel implementation of the operator.
If it has a more specific implementation, we assume the user actually
wants to match using the given operator itself.

Fixes #191.
c7a91af
@myronmarston myronmarston Merge pull request #193 from rspec/hash_match_array_failure
Provide a clear failure message when using `{}.should =~ {}`.
cf52fe6
@stevenharman stevenharman Cleanup whitespace b36e3ca
@stevenharman stevenharman Allow custom matchers to use #match built in
Addresses problem with dispatching the #match matcher from within a
custom matcher. (Wow... what a mouthful) See Issue #188.
6442118
@stevenharman stevenharman Add explaination about why we alias #match c956506
Commits on Dec 07, 2012
@myronmarston myronmarston Merge pull request #194 from stevenharman/issue_188_custom_matchers_w…
…ith_match

Allow custom matchers to use built-in #match
7fe9ec4
Commits on Dec 16, 2012
@myronmarston myronmarston Run travis builds against ruby 2.0. 2e14988
@myronmarston myronmarston 2.12.1 release. a71fe2f
View
5 .travis.yml
@@ -9,3 +9,8 @@ rvm:
- ree
- rbx-18mode
- rbx-19mode
+ - 2.0.0
+matrix:
+ allow_failures:
+ - rvm: 2.0.0
+
View
14 Changelog.md
@@ -1,5 +1,17 @@
+### 2.12.1 / 2012-12-15
+[full changelog](http://github.com/rspec/rspec-expectations/compare/v2.12.0...v2.12.1)
+
+Bug fixes
+
+* Improve the failure message for an expression like
+ `{}.should =~ {}`. (Myron Marston and Andy Lindeman)
+* Provide a `match_regex` alias so that custom matchers
+ built using the matcher DSL can use it (since `match`
+ is a different method in that context).
+ (Steven Harman)
+
### 2.12.0 / 2012-11-12
-[full changelog](http://github.com/rspec/rspec-expectations/compare/v2.11.3...2.12.0)
+[full changelog](http://github.com/rspec/rspec-expectations/compare/v2.11.3...v2.12.0)
Enhancements
View
28 features/custom_matchers/define_matcher.feature
@@ -338,3 +338,31 @@ Feature: define matcher
| 4 examples, 2 failures |
| expected 9 to be a multiple of 2 |
| expected 9 not to be a multiple of 3 |
+
+ Scenario: matching against a regular expression
+ Given a file named "regular_expression_matcher_spec.rb" with:
+ """ruby
+ # Due to Ruby's method dispatch mechanism, use the `#match_regex` alias
+ # rather than the `#match` matcher when defining custom matchers via the
+ # DSL.
+
+ RSpec::Matchers.define :be_valid_us_zipcode do
+ match do |actual|
+ expect(actual).to match_regex(/\A\d{5}(-\d{4})?\z/)
+ end
+ end
+
+ describe "30316" do
+ it { should be_valid_us_zipcode }
+ end
+
+ describe "30316-0001" do
+ it { should be_valid_us_zipcode }
+ end
+
+ describe "1000-61303" do
+ it { should_not be_valid_us_zipcode }
+ end
+ """
+ When I run `rspec regular_expression_matcher_spec.rb`
+ Then the stdout should contain "3 examples, 0 failures"
View
2  lib/rspec/expectations/version.rb
@@ -2,7 +2,7 @@ module RSpec
module Expectations
# @private
module Version
- STRING = '2.12.0'
+ STRING = '2.12.1'
end
end
end
View
28 lib/rspec/matchers.rb
@@ -214,7 +214,7 @@ def be_nil
#
# Given true, false, or nil, will pass if actual value is true, false or
# nil (respectively). Given no args means the caller should satisfy an if
- # condition (to be or not to be).
+ # condition (to be or not to be).
#
# Predicates are any Ruby method that ends in a "?" and returns true or
# false. Given be_ followed by arbitrary_predicate (without the "?"),
@@ -246,7 +246,7 @@ def be_a(klass)
def be_an_instance_of(expected)
BuiltIn::BeAnInstanceOf.new(expected)
end
-
+
alias_method :be_instance_of, :be_an_instance_of
# Passes if actual.kind_of?(expected)
@@ -288,20 +288,20 @@ def be_within(delta)
# @example
#
# lambda {
- # team.add_player(player)
+ # team.add_player(player)
# }.should change(roster, :count)
#
# lambda {
- # team.add_player(player)
+ # team.add_player(player)
# }.should change(roster, :count).by(1)
#
# lambda {
- # team.add_player(player)
+ # team.add_player(player)
# }.should change(roster, :count).by_at_least(1)
#
# lambda {
# team.add_player(player)
- # }.should change(roster, :count).by_at_most(1)
+ # }.should change(roster, :count).by_at_most(1)
#
# string = "string"
# lambda {
@@ -311,7 +311,7 @@ def be_within(delta)
# lambda {
# person.happy_birthday
# }.should change(person, :birthday).from(32).to(33)
- #
+ #
# lambda {
# employee.develop_great_new_social_networking_app
# }.should change(employee, :title).from("Mail Clerk").to("CEO")
@@ -491,10 +491,20 @@ def include(*expected)
#
# email.should match(/^([^\s]+)((?:[-a-z0-9]+\.)+[a-z]{2,})$/i)
# email.should match("@example.com")
+ # zipcode.should match_regex(/\A\d{5}(-\d{4})?\z/)
+ # zipcode.should match_regex("90210")
+ #
+ # @note Due to Ruby's method dispatch mechanism, using the `#match` matcher
+ # within a custom matcher defined via the matcher DSL
+ # (`RSpec::Matcher.define`) will result Ruby calling the wrong `#match`
+ # method and raising an `ArgumentError`. Instead, use the aliased
+ # `#match_regex` method.
def match(expected)
BuiltIn::Match.new(expected)
end
+ alias_method :match_regex, :match
+
# With no args, matches if any error is raised.
# With a named error, matches only if that specific error is raised.
# With a named error and messsage specified as a String, matches only if both match.
@@ -523,7 +533,7 @@ def raise_error(error=Exception, message=nil, &block)
# provided. Names can be Strings or Symbols.
#
# @example
- #
+ #
def respond_to(*names)
BuiltIn::RespondTo.new(*names)
end
@@ -684,6 +694,6 @@ def match_array(array)
BuiltIn::MatchArray.new(array)
end
- OperatorMatcher.register(Array, '=~', BuiltIn::MatchArray)
+ OperatorMatcher.register(Enumerable, '=~', BuiltIn::MatchArray)
end
end
View
29 lib/rspec/matchers/operator_matcher.rb
@@ -11,8 +11,17 @@ def register(klass, operator, matcher)
registry[klass][operator] = matcher
end
+ def unregister(klass, operator)
+ registry[klass] && registry[klass].delete(operator)
+ end
+
def get(klass, operator)
- registry[klass] && registry[klass][operator]
+ klass.ancestors.each { |ancestor|
+ matcher = registry[ancestor] && registry[ancestor][operator]
+ return matcher if matcher
+ }
+
+ nil
end
end
@@ -22,7 +31,7 @@ def initialize(actual)
def self.use_custom_matcher_or_delegate(operator)
define_method(operator) do |expected|
- if matcher = OperatorMatcher.get(@actual.class, operator)
+ if uses_generic_implementation_of?(operator) && matcher = OperatorMatcher.get(@actual.class, operator)
@actual.send(::RSpec::Matchers.last_should, matcher.new(expected))
else
eval_match(@actual, operator, expected)
@@ -53,6 +62,22 @@ def description
private
+ if Method.method_defined?(:owner) # 1.8.6 lacks Method#owner :-(
+ def uses_generic_implementation_of?(op)
+ @actual.method(op).owner == ::Kernel
+ end
+ else
+ def uses_generic_implementation_of?(op)
+ # This is a bit of a hack, but:
+ #
+ # {}.method(:=~).to_s # => "#<Method: Hash(Kernel)#=~>"
+ #
+ # In the absence of Method#owner, this is the best we
+ # can do to see if the method comes from Kernel.
+ @actual.method(op).to_s.include?('(Kernel)')
+ end
+ end
+
def eval_match(actual, operator, expected)
::RSpec::Matchers.last_matcher = self
@operator, @expected = operator, expected
View
15 spec/rspec/matchers/match_array_spec.rb
@@ -113,6 +113,17 @@ def ==(other)
MESSAGE
end
+ context "when the array defines a `=~` method" do
+ it 'delegates to that method rather than using the match_array matcher' do
+ array = []
+ def array.=~(other)
+ other == :foo
+ end
+
+ array.should =~ :foo
+ expect { array.should =~ :bar }.to fail_with(/expected: :bar/)
+ end
+ end
end
describe "should_not =~ [:with, :multiple, :args]" do
@@ -135,4 +146,8 @@ def ==(other)
it "fails with a string and the expected error message is given" do
expect { "I like turtles".should match_array([1,2,3]) }.to fail_with(/expected an array/)
end
+
+ it 'fails with a clear message when given a hash using the `should =~` syntax' do
+ expect { {}.should =~ {} }.to fail_with(/expected an array/)
+ end
end
View
23 spec/rspec/matchers/operator_matcher_spec.rb
@@ -196,6 +196,29 @@
end
+describe "OperatorMatcher registry" do
+ let(:custom_klass) { Class.new }
+ let(:custom_subklass) { Class.new(custom_klass) }
+
+ after {
+ RSpec::Matchers::OperatorMatcher.unregister(custom_klass, "=~")
+ }
+
+ it "allows operator matchers to be registered for types" do
+ RSpec::Matchers::OperatorMatcher.register(custom_klass, "=~", RSpec::Matchers::BuiltIn::Match)
+ expect(RSpec::Matchers::OperatorMatcher.get(custom_klass, "=~")).to eq(RSpec::Matchers::BuiltIn::Match)
+ end
+
+ it "considers ancestors when finding an operator matcher" do
+ RSpec::Matchers::OperatorMatcher.register(custom_klass, "=~", RSpec::Matchers::BuiltIn::Match)
+ expect(RSpec::Matchers::OperatorMatcher.get(custom_subklass, "=~")).to eq(RSpec::Matchers::BuiltIn::Match)
+ end
+
+ it "returns nil if there is no matcher registered for a type" do
+ expect(RSpec::Matchers::OperatorMatcher.get(custom_klass, "=~")).to be_nil
+ end
+end
+
describe RSpec::Matchers::BuiltIn::PositiveOperatorMatcher do
it "works when the target has implemented #send" do

No commit comments for this range

Something went wrong with that request. Please try again.