Add all matcher [DONE] #491

Merged
merged 1 commit into from Apr 17, 2014

Conversation

Projects
None yet
4 participants
@yelled3
Contributor

yelled3 commented Mar 6, 2014

#483

Changelog

  • Add support for all matcher, to allow you to match matcher on a collection.
    • expect(stoplight.color).to ~eq("blue")
    • expect([1, 3, 5]).to all( be_odd.and be_an(Integer) )
      (Adam Farhi)

as @myronmarston mentioned;
waiting for 3.1 release

lib/rspec/matchers.rb
+ #
+ # expect([3,3,3]).to all.eq(3)
+ # expect([1,2,3]).to all.eq(3) # fails
+ # expect([1,2,3]).not_to all.eq(5)

This comment has been minimized.

@myronmarston

myronmarston Mar 6, 2014

Member

Shouldn't these examples be formatted like all(eq 3) rather than all.eq(3)? The matcher is an argument, not a a chained method.

Also, please put spaces in your arrays: [1, 2, 3], not [1,2,3]. Spaceshelpmakeiteasiertoread.

@myronmarston

myronmarston Mar 6, 2014

Member

Shouldn't these examples be formatted like all(eq 3) rather than all.eq(3)? The matcher is an argument, not a a chained method.

Also, please put spaces in your arrays: [1, 2, 3], not [1,2,3]. Spaceshelpmakeiteasiertoread.

This comment has been minimized.

@JonRowe

JonRowe Mar 6, 2014

Member

Yeah these should be methods accepting matchers rather than a fluent interface.

@JonRowe

JonRowe Mar 6, 2014

Member

Yeah these should be methods accepting matchers rather than a fluent interface.

lib/rspec/matchers.rb
+ # expect([3,3,3]).to all.eq(3).and be_an(Integer)
+ def all(*expected)
+ BuiltIn::All.new(*expected)
+ end

This comment has been minimized.

@myronmarston

myronmarston Mar 6, 2014

Member

Why do you accept a splat here? Does it make sense to accept multiple matchers?

Hmm, actually, maybe it does:

expect([1, 3, 5]).to all(be_a(Fixnum), be_odd)

Maybe show an example like this?

@myronmarston

myronmarston Mar 6, 2014

Member

Why do you accept a splat here? Does it make sense to accept multiple matchers?

Hmm, actually, maybe it does:

expect([1, 3, 5]).to all(be_a(Fixnum), be_odd)

Maybe show an example like this?

This comment has been minimized.

@JonRowe

JonRowe Mar 6, 2014

Member

Yeah I think this needs to be a splat, but should be documented as such

@JonRowe

JonRowe Mar 6, 2014

Member

Yeah I think this needs to be a splat, but should be documented as such

This comment has been minimized.

@yelled3

yelled3 Mar 7, 2014

Contributor

funny what can happen from a typo :-)

my initial intent was to pass a single matcher to all, which will be asserted for each object in an enumerable. Since we can use composable matchers - there's no need to pass more then one matcher as an argument to all.

that being said, doing the reverse is also interesting;
single object with multiple expectations, as a shorthand version of chaining matchers with and:

expect(item).to pass_all( be_active, be_valid, be_an(Item))

# which would be same as doing:
expect(item).to be_active.and be_valid.and be_an(Item)

@myronmarston @JonRowe WDYT?

@yelled3

yelled3 Mar 7, 2014

Contributor

funny what can happen from a typo :-)

my initial intent was to pass a single matcher to all, which will be asserted for each object in an enumerable. Since we can use composable matchers - there's no need to pass more then one matcher as an argument to all.

that being said, doing the reverse is also interesting;
single object with multiple expectations, as a shorthand version of chaining matchers with and:

expect(item).to pass_all( be_active, be_valid, be_an(Item))

# which would be same as doing:
expect(item).to be_active.and be_valid.and be_an(Item)

@myronmarston @JonRowe WDYT?

This comment has been minimized.

@myronmarston

myronmarston Mar 7, 2014

Member

Since we can use composable matchers - there's no need to pass more then one matcher as an argument to all.

I don't understand what you mean. If the user wants every item in a collection to match against more than one matcher, how else would they do it? You have a line above like this:

expect([3,3,3]).to all.eq(3).and be_an(Integer)

...which I think you actually meant to be this:

expect([3,3,3]).to all(eq(3)).and be_an(Integer)

...but that won't pass. The and be_an(Integer) part is the equivalent of:

expect([3,3,3]).to be_an(Integer)

...but the target of the expectation is an array, not an integer, so it'll fail.

If we want to support allowing multiple matchers to match against all elements in a collection, I think all needs to accept multiple matchers as arguments; I can't see any other way for it to work, and I think it would surprise me if it didn't support it.

Anyhow, I don't think pass_all reads well at all, and I see no advantage over the chained and form. You can certainly define it as a custom matcher if you like it, though :).

@myronmarston

myronmarston Mar 7, 2014

Member

Since we can use composable matchers - there's no need to pass more then one matcher as an argument to all.

I don't understand what you mean. If the user wants every item in a collection to match against more than one matcher, how else would they do it? You have a line above like this:

expect([3,3,3]).to all.eq(3).and be_an(Integer)

...which I think you actually meant to be this:

expect([3,3,3]).to all(eq(3)).and be_an(Integer)

...but that won't pass. The and be_an(Integer) part is the equivalent of:

expect([3,3,3]).to be_an(Integer)

...but the target of the expectation is an array, not an integer, so it'll fail.

If we want to support allowing multiple matchers to match against all elements in a collection, I think all needs to accept multiple matchers as arguments; I can't see any other way for it to work, and I think it would surprise me if it didn't support it.

Anyhow, I don't think pass_all reads well at all, and I see no advantage over the chained and form. You can certainly define it as a custom matcher if you like it, though :).

This comment has been minimized.

@yelled3

yelled3 Mar 7, 2014

Contributor

firstly, you are corrent - it should be

expect([3,3,3]).to all(...)

maybe I'm missing something, but I thought doing:

expect([3,3,3]).to all(
   eq(3).and be_an(Integer)
)

both matchers (eq and be_a) are grouped into a single Compound matcher which is then passed to the all matcher as a single argument.

I thought it would work just the same as:

Compound matcher expressions can also be passed as an argument to another matcher:
expect(["food", "drink"]).to include(
  a_string_starting_with("f").and ending_with("d")
)
@yelled3

yelled3 Mar 7, 2014

Contributor

firstly, you are corrent - it should be

expect([3,3,3]).to all(...)

maybe I'm missing something, but I thought doing:

expect([3,3,3]).to all(
   eq(3).and be_an(Integer)
)

both matchers (eq and be_a) are grouped into a single Compound matcher which is then passed to the all matcher as a single argument.

I thought it would work just the same as:

Compound matcher expressions can also be passed as an argument to another matcher:
expect(["food", "drink"]).to include(
  a_string_starting_with("f").and ending_with("d")
)

This comment has been minimized.

@myronmarston

myronmarston Mar 7, 2014

Member

You're right, that will work just fine. I was thrown by the chaining you were originally doing.

@myronmarston

myronmarston Mar 7, 2014

Member

You're right, that will work just fine. I was thrown by the chaining you were originally doing.

lib/rspec/matchers.rb
+ #
+ # @example
+ #
+ # expect([3,3,3]).to all.eq(3).and be_an(Integer)

This comment has been minimized.

@myronmarston

myronmarston Mar 6, 2014

Member

and chaining like this won't work -- it'll match be_an(Integer) against the entire actual object (the array), rather than matching against individual items. If you want to support matching multiple matchers against all items, you can have it accept multiple matchers as I've shown below.

@myronmarston

myronmarston Mar 6, 2014

Member

and chaining like this won't work -- it'll match be_an(Integer) against the entire actual object (the array), rather than matching against individual items. If you want to support matching multiple matchers against all items, you can have it accept multiple matchers as I've shown below.

lib/rspec/matchers/built_in/all.rb
+
+ private
+
+ #def perform_match(predicate, hash_subset_predicate)

This comment has been minimized.

@JonRowe

JonRowe Mar 6, 2014

Member

Obviously all this will need removing ;)

@JonRowe

JonRowe Mar 6, 2014

Member

Obviously all this will need removing ;)

This comment has been minimized.

@yelled3

yelled3 Mar 7, 2014

Contributor

yeah, sorry about that - copying bolierplate for a built in matcher :-)

@yelled3

yelled3 Mar 7, 2014

Contributor

yeah, sorry about that - copying bolierplate for a built in matcher :-)

@JonRowe

This comment has been minimized.

Show comment
Hide comment
@JonRowe

JonRowe Mar 6, 2014

Member

Good start! @yelled3

Member

JonRowe commented Mar 6, 2014

Good start! @yelled3

spec/rspec/matchers/built_in/all_spec.rb
+ it 'returns the index of the failed object' do
+ expect {
+ expect(invalid_collection).to all( eq('A') )
+ }.to fail_with(dedent <<-EOS)

This comment has been minimized.

@yelled3

yelled3 Mar 11, 2014

Contributor

please ignore this format for now - still not sure what's the optimal error message...
@myronmarston @JonRowe any suggestions?

I was thinking about something like:

Failure/Error: expect(invalid_collection).to all( eq('A') )
       expected ["A", "B", "A", "C", "A"] to all eq "A"

       object at index "1" failed: 
       expected: "A"
            got: "B"

       (compared using ==)


       object at index "3" failed: 
       expected: "A"
            got: "C"

       (compared using ==)
@yelled3

yelled3 Mar 11, 2014

Contributor

please ignore this format for now - still not sure what's the optimal error message...
@myronmarston @JonRowe any suggestions?

I was thinking about something like:

Failure/Error: expect(invalid_collection).to all( eq('A') )
       expected ["A", "B", "A", "C", "A"] to all eq "A"

       object at index "1" failed: 
       expected: "A"
            got: "B"

       (compared using ==)


       object at index "3" failed: 
       expected: "A"
            got: "C"

       (compared using ==)

This comment has been minimized.

@myronmarston

myronmarston Mar 11, 2014

Member

Hmmm. More thought is needed. You mind if we defer this until 3.1? I want to stay focused on the rspec-core changes that are still needed for 3.0.

@myronmarston

myronmarston Mar 11, 2014

Member

Hmmm. More thought is needed. You mind if we defer this until 3.1? I want to stay focused on the rspec-core changes that are still needed for 3.0.

This comment has been minimized.

@JonRowe

JonRowe Mar 11, 2014

Member

Yeah I think that's wise, I think that error message is a bit verbose, how about expressing a diff of failing items?

@JonRowe

JonRowe Mar 11, 2014

Member

Yeah I think that's wise, I think that error message is a bit verbose, how about expressing a diff of failing items?

spec/rspec/matchers/built_in/all_spec.rb
+ it 'returns the index of the failed object' do
+ expect {
+ expect(['A', 'A', 'A', 'C', 'A']).to all(inner_matcher)
+ }.to fail_with(dedent <<-EOS)

This comment has been minimized.

@yelled3

yelled3 Mar 14, 2014

Contributor

writing these kind of assertions was a real pain, since the output has a very unreadable diff.

I actually found myself doing:

it 'shows a proper diff for failure message' do
        matcher = all(eq('A'))
        matcher.matches?(['A', 'A', 'A', 'C', 'A'])
        expect(matcher.failure_message).to eq(dedent <<-EOS)
          |expected ["A", "A", "A", "C", "A"] to all eq "A"
          |
          |object at index "3" failed to match:
          |expected: "A"
          |     got: "C"
          |
          |(compared using ==)
          |
        EOS
do

anyone else had the same issue?

@yelled3

yelled3 Mar 14, 2014

Contributor

writing these kind of assertions was a real pain, since the output has a very unreadable diff.

I actually found myself doing:

it 'shows a proper diff for failure message' do
        matcher = all(eq('A'))
        matcher.matches?(['A', 'A', 'A', 'C', 'A'])
        expect(matcher.failure_message).to eq(dedent <<-EOS)
          |expected ["A", "A", "A", "C", "A"] to all eq "A"
          |
          |object at index "3" failed to match:
          |expected: "A"
          |     got: "C"
          |
          |(compared using ==)
          |
        EOS
do

anyone else had the same issue?

This comment has been minimized.

@myronmarston

myronmarston Mar 15, 2014

Member

It's not ideal but I think it's important that we use the matchers as users will. Got a suggestion for how to maintain that and improve the output?

@myronmarston

myronmarston Mar 15, 2014

Member

It's not ideal but I think it's important that we use the matchers as users will. Got a suggestion for how to maintain that and improve the output?

@yelled3

This comment has been minimized.

Show comment
Hide comment
@yelled3

yelled3 Mar 14, 2014

Contributor

@JonRowe you're welcome to a final check :-)

Contributor

yelled3 commented Mar 14, 2014

@JonRowe you're welcome to a final check :-)

@samphippen

This comment has been minimized.

Show comment
Hide comment
@samphippen

samphippen Mar 14, 2014

Member

@yelled3 I like the idea behind this matcher and thanks for your contribution. My issue isn't with the code you've written here, but with the grammar of the matcher and how it reads:

I expect foo to all equal bar doesn't seem like a well formed sentence to me. I have some alternatives but I'm not in love with any of them:

expect(foo).to have_all_values_that(eq(3))
expect(foo).to contain_all_values_that(eq(3))
expect(foo).to all_values_that(eq(3))
expect(foo).to contain_values_that_all(eq(3))

and so on. Does anyone have thoughts around this?

Member

samphippen commented Mar 14, 2014

@yelled3 I like the idea behind this matcher and thanks for your contribution. My issue isn't with the code you've written here, but with the grammar of the matcher and how it reads:

I expect foo to all equal bar doesn't seem like a well formed sentence to me. I have some alternatives but I'm not in love with any of them:

expect(foo).to have_all_values_that(eq(3))
expect(foo).to contain_all_values_that(eq(3))
expect(foo).to all_values_that(eq(3))
expect(foo).to contain_values_that_all(eq(3))

and so on. Does anyone have thoughts around this?

@yelled3

This comment has been minimized.

Show comment
Hide comment
@yelled3

yelled3 Mar 15, 2014

Contributor

Hi @samphippen - I see no harm in adding aliases to all;

I don't think the word values is suitable here. collections have objects of some kind. values is very specific...
how about:

expect(items).to all(be_active)
expect(items).to all(be_active).items # add chaining ,like - have(3).items

expect.all(items).to be_active # not sure if possible, but very readable
expect(array).to have_only_objects_that(eq(3)) # too long? :-)
Contributor

yelled3 commented Mar 15, 2014

Hi @samphippen - I see no harm in adding aliases to all;

I don't think the word values is suitable here. collections have objects of some kind. values is very specific...
how about:

expect(items).to all(be_active)
expect(items).to all(be_active).items # add chaining ,like - have(3).items

expect.all(items).to be_active # not sure if possible, but very readable
expect(array).to have_only_objects_that(eq(3)) # too long? :-)
lib/rspec/matchers.rb
+ #
+ # expect([3, 3, 3]).to all.eq(3)
+ # expect([1, 2, 3]).to all.eq(3) # fails
+ # expect([1, 2, 3]).not_to all.eq(5)

This comment has been minimized.

@myronmarston

myronmarston Mar 15, 2014

Member

These examples are wrong; you need to pass eq as an arg, not chain it.

@myronmarston

myronmarston Mar 15, 2014

Member

These examples are wrong; you need to pass eq as an arg, not chain it.

This comment has been minimized.

@myronmarston

myronmarston Mar 15, 2014

Member

Also, this is kind of a lame example; maybe this is a bit better?

expect([1, 3, 5]).to all be_odd
@myronmarston

myronmarston Mar 15, 2014

Member

Also, this is kind of a lame example; maybe this is a bit better?

expect([1, 3, 5]).to all be_odd
lib/rspec/matchers.rb
+ # expect([1, 2, 3]).to all.eq(3) # fails
+ # expect([1, 2, 3]).not_to all.eq(5)
+ #
+ # You can also use this with composable matchers

This comment has been minimized.

@myronmarston

myronmarston Mar 15, 2014

Member

We've adopted the term "compound matchers" for the and/or chaining -- mind using that here.

@myronmarston

myronmarston Mar 15, 2014

Member

We've adopted the term "compound matchers" for the and/or chaining -- mind using that here.

lib/rspec/matchers/built_in/all.rb
+ def matches?(actual)
+ @actual = actual
+ perform_match
+ end

This comment has been minimized.

@myronmarston

myronmarston Mar 15, 2014

Member

matches? in BaseMatcher is very similar to this. You can delete this matches? implementation and rename perform_match to match(expected, actual) and it'll work.

@myronmarston

myronmarston Mar 15, 2014

Member

matches? in BaseMatcher is very similar to this. You can delete this matches? implementation and rename perform_match to match(expected, actual) and it'll work.

lib/rspec/matchers/built_in/all.rb
+
+ def index_failed_objects(is_negated)
+ actual.each_with_index do |actual_item, index|
+ cloned_matcher = matcher.clone

This comment has been minimized.

@myronmarston

myronmarston Mar 15, 2014

Member

Why do we clone the matcher?

@myronmarston

myronmarston Mar 15, 2014

Member

Why do we clone the matcher?

This comment has been minimized.

@yelled3

yelled3 Mar 15, 2014

Contributor

my bad - I thought it was needed in order to match multiple values

@yelled3

yelled3 Mar 15, 2014

Contributor

my bad - I thought it was needed in order to match multiple values

+ end
+ end
+ end
+ end

This comment has been minimized.

@myronmarston

myronmarston Mar 15, 2014

Member

I'm not sure if this is what we want to do for the negative case. A plain reading of expect(x).to_not all( matcher ) is !x.all? { |item| matcher === item } (that is, it'll pass if at least one of the items does not match), but you have implemented x.none? { |item| matcher === item }. This is what we've used elsewhere (e.g. for expect(obj).not_to respond_to(:foo, :bar), it uses the none? semantics because the intent there is that the object does not respond to any of the messages), but I don't know if that's what users would expect here. I'm not sure which semantic makes the most sense or is what users would expect. Really, I can see an argument for either. Given that, I'd rather disallow the negative form for now, and potentially reenable it after there's been a community discussion.

@myronmarston

myronmarston Mar 15, 2014

Member

I'm not sure if this is what we want to do for the negative case. A plain reading of expect(x).to_not all( matcher ) is !x.all? { |item| matcher === item } (that is, it'll pass if at least one of the items does not match), but you have implemented x.none? { |item| matcher === item }. This is what we've used elsewhere (e.g. for expect(obj).not_to respond_to(:foo, :bar), it uses the none? semantics because the intent there is that the object does not respond to any of the messages), but I don't know if that's what users would expect here. I'm not sure which semantic makes the most sense or is what users would expect. Really, I can see an argument for either. Given that, I'd rather disallow the negative form for now, and potentially reenable it after there's been a community discussion.

This comment has been minimized.

@JonRowe

JonRowe Mar 15, 2014

Member

I think the former (!x.all?) makes more sense from both a grammatical perspective, and a boolean algebra perspective.

@JonRowe

JonRowe Mar 15, 2014

Member

I think the former (!x.all?) makes more sense from both a grammatical perspective, and a boolean algebra perspective.

This comment has been minimized.

@yelled3

yelled3 Mar 15, 2014

Contributor

putting aside the grammar aspect, I think the x.none? is more useful.
say you have:

# check that all boxes are full
expect(boxes).to all( be_full )

# check that all boxes are not full
expect(boxes).to all( be_not_full ) # requires me to add method for - 'not_full?'

# so I would rather do
expect(boxes).to_not all( be_full)

in other usecases I may want to check !x.all?
like @myronmarston said: that is, it'll pass if at least one of the items does not match

but this can already be done by:

 # check that the boxes include at least a single full box
 expect(boxes).to include( be_full )

# check that the boxes doesn't include a full box
 expect(boxes).to_not include( be_full )

maybe we can add an alias to all which will make the negative form more readable... any suggestions?

@yelled3

yelled3 Mar 15, 2014

Contributor

putting aside the grammar aspect, I think the x.none? is more useful.
say you have:

# check that all boxes are full
expect(boxes).to all( be_full )

# check that all boxes are not full
expect(boxes).to all( be_not_full ) # requires me to add method for - 'not_full?'

# so I would rather do
expect(boxes).to_not all( be_full)

in other usecases I may want to check !x.all?
like @myronmarston said: that is, it'll pass if at least one of the items does not match

but this can already be done by:

 # check that the boxes include at least a single full box
 expect(boxes).to include( be_full )

# check that the boxes doesn't include a full box
 expect(boxes).to_not include( be_full )

maybe we can add an alias to all which will make the negative form more readable... any suggestions?

This comment has been minimized.

@myronmarston

myronmarston Mar 15, 2014

Member

My suggestion:

  • Add a separate all_not matcher. This is very explicit that every item in the list must not match the matcher:
expect(list).to all_not( be_full )
  • Make the negated from of all raise an error telling people to use negated include or all_not, depending on what they want.

@JonRowe, what do you think of that plan?

@myronmarston

myronmarston Mar 15, 2014

Member

My suggestion:

  • Add a separate all_not matcher. This is very explicit that every item in the list must not match the matcher:
expect(list).to all_not( be_full )
  • Make the negated from of all raise an error telling people to use negated include or all_not, depending on what they want.

@JonRowe, what do you think of that plan?

This comment has been minimized.

@yelled3

yelled3 Mar 15, 2014

Contributor

sounds solid :-)

@yelled3

yelled3 Mar 15, 2014

Contributor

sounds solid :-)

This comment has been minimized.

@myronmarston

myronmarston Mar 15, 2014

Member

Also, the all_not matcher should not support negation. Double negation would be even more confusing :).

@myronmarston

myronmarston Mar 15, 2014

Member

Also, the all_not matcher should not support negation. Double negation would be even more confusing :).

This comment has been minimized.

@myronmarston

myronmarston Mar 15, 2014

Member

Actually, this circles us back to #493: if we can make a general-purpose way to negate any matcher, then you could just say:

expect(list).to all( not be_full )

...or whatever. There's still the problem that not is a keyword, though. But before adding a separate matcher for negation, I'd like to see if we can find a way to provide general purpose negation.

@myronmarston

myronmarston Mar 15, 2014

Member

Actually, this circles us back to #493: if we can make a general-purpose way to negate any matcher, then you could just say:

expect(list).to all( not be_full )

...or whatever. There's still the problem that not is a keyword, though. But before adding a separate matcher for negation, I'd like to see if we can find a way to provide general purpose negation.

This comment has been minimized.

@JonRowe

JonRowe Apr 9, 2014

Member

(I missed this but I like this plan)

@JonRowe

JonRowe Apr 9, 2014

Member

(I missed this but I like this plan)

@myronmarston

This comment has been minimized.

Show comment
Hide comment
@myronmarston

myronmarston Mar 15, 2014

Member

@samphippen -- I think all reads better than any of your suggestions. It actually doesn't really bother me.

@yelled3 -- this'll need a cuke, as well.

Member

myronmarston commented Mar 15, 2014

@samphippen -- I think all reads better than any of your suggestions. It actually doesn't really bother me.

@yelled3 -- this'll need a cuke, as well.

lib/rspec/matchers/built_in/all.rb
+ improve_hash_formatting "all#{to_sentence(described_items)}"
+ end
+
+ private

This comment has been minimized.

@JonRowe

JonRowe Mar 15, 2014

Member

This should be dedented by 2 spaces, we aline our privates with class/end

@JonRowe

JonRowe Mar 15, 2014

Member

This should be dedented by 2 spaces, we aline our privates with class/end

+ end
+
+ end
+end

This comment has been minimized.

@JonRowe

JonRowe Mar 15, 2014

Member

Being picky I know but this needs a \n on the end of the line (not a whole blank link, just this lines ending)

@JonRowe

JonRowe Mar 15, 2014

Member

Being picky I know but this needs a \n on the end of the line (not a whole blank link, just this lines ending)

lib/rspec/matchers/built_in/all.rb
+ message.start_with?("\n") ? message : "\n#{message}"
+ end
+
+ def initialize_copy(other)

This comment has been minimized.

@yelled3

yelled3 Apr 8, 2014

Contributor

@myronmarston added clone support + specs

@yelled3

yelled3 Apr 8, 2014

Contributor

@myronmarston added clone support + specs

@yelled3

This comment has been minimized.

Show comment
Hide comment
@yelled3

yelled3 Apr 8, 2014

Contributor

@myronmarston @JonRowe getting some sporadic CI failures:

Installing ffi (1.9.3)
Gem::RemoteFetcher::FetchError: Errno::ETIMEDOUT: Connection timed out - connect(2) for "rubygems.org" port 443 (https://rubygems.org/gems/childprocess-0.5.2.gem)
An error occurred while installing childprocess (0.5.2), and Bundler cannot
continue.
Make sure that `gem install childprocess -v '0.5.2'` succeeds before bundling.
The command "script/run_build" exited with 5.

https://travis-ci.org/rspec/rspec-expectations/jobs/22533536

any idea?

Contributor

yelled3 commented Apr 8, 2014

@myronmarston @JonRowe getting some sporadic CI failures:

Installing ffi (1.9.3)
Gem::RemoteFetcher::FetchError: Errno::ETIMEDOUT: Connection timed out - connect(2) for "rubygems.org" port 443 (https://rubygems.org/gems/childprocess-0.5.2.gem)
An error occurred while installing childprocess (0.5.2), and Bundler cannot
continue.
Make sure that `gem install childprocess -v '0.5.2'` succeeds before bundling.
The command "script/run_build" exited with 5.

https://travis-ci.org/rspec/rspec-expectations/jobs/22533536

any idea?

@yelled3

This comment has been minimized.

Show comment
Hide comment
@yelled3

yelled3 Apr 8, 2014

Contributor

Aside from CI issues (see above), and squashing to a single commit, I think this is ready for a final review.
@myronmarston @JonRowe @samphippen /cc

Contributor

yelled3 commented Apr 8, 2014

Aside from CI issues (see above), and squashing to a single commit, I think this is ready for a final review.
@myronmarston @JonRowe @samphippen /cc

@JonRowe

This comment has been minimized.

Show comment
Hide comment
@JonRowe

JonRowe Apr 9, 2014

Member

I rebooted the build ;)

Member

JonRowe commented Apr 9, 2014

I rebooted the build ;)

lib/rspec/matchers/built_in/all.rb
+ def match(_, actual)
+ index_failed_objects
+ actual.all?{ |actual_item| matcher.matches?(actual_item) }
+ end

This comment has been minimized.

@myronmarston

myronmarston Apr 9, 2014

Member

This implementation loops over the actual list twice (once to index, once to see if the it matches all items) and the matcher is matched against every item twice. There's no guarantee that matcher.matches? will be a fast operation, so it's best to avoid matching multiple times. I think you can implement this in one pass over the list like so:

def match(_, actual)
  index_failed_objects
  failed_objects.empty?
end
@myronmarston

myronmarston Apr 9, 2014

Member

This implementation loops over the actual list twice (once to index, once to see if the it matches all items) and the matcher is matched against every item twice. There's no guarantee that matcher.matches? will be a fast operation, so it's best to avoid matching multiple times. I think you can implement this in one pass over the list like so:

def match(_, actual)
  index_failed_objects
  failed_objects.empty?
end

This comment has been minimized.

@yelled3

yelled3 Apr 9, 2014

Contributor

good point 👍
although slightly less readable, then

actual.all?{ |actual_item| matcher.matches?(actual_item) }
@yelled3

yelled3 Apr 9, 2014

Contributor

good point 👍
although slightly less readable, then

actual.all?{ |actual_item| matcher.matches?(actual_item) }
lib/rspec/matchers/built_in/all.rb
+
+ def index_failed_objects
+ actual.each_with_index do |actual_item, index|
+ matches = matcher.matches?(actual_item)

This comment has been minimized.

@myronmarston

myronmarston Apr 9, 2014

Member

This should use values_match?(actual_item, matcher) rather than matcher.matches?. That includes some extra internal flexibility for matching nested data structures and also takes care of cloning the matchers before using them in order to avoid issues with memoization.

@myronmarston

myronmarston Apr 9, 2014

Member

This should use values_match?(actual_item, matcher) rather than matcher.matches?. That includes some extra internal flexibility for matching nested data structures and also takes care of cloning the matchers before using them in order to avoid issues with memoization.

This comment has been minimized.

@myronmarston

myronmarston Apr 9, 2014

Member

(Notice that if you look at any of the other composable matchers, they all use values_match?).

@myronmarston

myronmarston Apr 9, 2014

Member

(Notice that if you look at any of the other composable matchers, they all use values_match?).

This comment has been minimized.

@yelled3

yelled3 Apr 9, 2014

Contributor

the thing is, I still need to call matcher.matches?(actual_item) in order for matcher.failure_message to work. since values_match? generate a failure message for the cloned matcher...
maybe the values_match? should return both the result and the cloned matcher?

result, cloned_matcher = values_match?(matcher, actual_item)

ideas?

@yelled3

yelled3 Apr 9, 2014

Contributor

the thing is, I still need to call matcher.matches?(actual_item) in order for matcher.failure_message to work. since values_match? generate a failure message for the cloned matcher...
maybe the values_match? should return both the result and the cloned matcher?

result, cloned_matcher = values_match?(matcher, actual_item)

ideas?

lib/rspec/matchers/built_in/all.rb
+
+ # @private
+ def does_not_match?(actual)
+ raise NotImplementedError, '`expect { }.not_to all( matcher )` is not supported'

This comment has been minimized.

@myronmarston

myronmarston Apr 9, 2014

Member
  • This matcher doesn't operate on a block...it operates on a collection. The expression should be expect(...), not expect { ... }.
  • Once we merge the negation PR, it would be nice for this to mention that you can do: expect(...).to all( ~matcher ).
@myronmarston

myronmarston Apr 9, 2014

Member
  • This matcher doesn't operate on a block...it operates on a collection. The expression should be expect(...), not expect { ... }.
  • Once we merge the negation PR, it would be nice for this to mention that you can do: expect(...).to all( ~matcher ).

This comment has been minimized.

@yelled3

yelled3 Apr 9, 2014

Contributor

done

@yelled3

yelled3 Apr 9, 2014

Contributor

done

spec/rspec/matchers/built_in/all_spec.rb
+ let(:collection) { ['A', 'A', 'A'] }
+ let(:invalid_collection) { ['A', 'B', 'A', 'C', 'A'] }
+
+ let(:inner_matcher) { eq('A') }

This comment has been minimized.

@myronmarston

myronmarston Apr 9, 2014

Member

I'd prefer to see these values used in line in the specs below rather than making let declarations for them. The let declarations force you to scroll back and forth to understand the full expression, and there's nothing special about these values that makes them automatically useful for all examples below. The duplication in any examples that happen to re-use these same values is purely incidental and isn't a DRY violation as these don't represent a piece of knowledge.

@myronmarston

myronmarston Apr 9, 2014

Member

I'd prefer to see these values used in line in the specs below rather than making let declarations for them. The let declarations force you to scroll back and forth to understand the full expression, and there's nothing special about these values that makes them automatically useful for all examples below. The duplication in any examples that happen to re-use these same values is purely incidental and isn't a DRY violation as these don't represent a piece of knowledge.

This comment has been minimized.

@yelled3

yelled3 Apr 9, 2014

Contributor

np

@yelled3

yelled3 Apr 9, 2014

Contributor

np

spec/rspec/matchers/built_in/all_spec.rb
+
+ let(:inner_matcher) { eq('A') }
+
+ describe :description do

This comment has been minimized.

@myronmarston

myronmarston Apr 9, 2014

Member

describe :description makes the subject within this group refer to the :description symbol. (In RSpec 3, anything you are describing -- besides a string -- becomes the subject for that group). You aren't using subject but it's best to stick to string descriptions here, I think.

@myronmarston

myronmarston Apr 9, 2014

Member

describe :description makes the subject within this group refer to the :description symbol. (In RSpec 3, anything you are describing -- besides a string -- becomes the subject for that group). You aren't using subject but it's best to stick to string descriptions here, I think.

This comment has been minimized.

@yelled3

yelled3 Apr 9, 2014

Contributor

noted

@yelled3

yelled3 Apr 9, 2014

Contributor

noted

spec/rspec/matchers/built_in/all_spec.rb
+ describe :description do
+ it 'provides a description' do
+ matcher = all(inner_matcher)
+ matcher.matches?(collection)

This comment has been minimized.

@myronmarston

myronmarston Apr 9, 2014

Member

Is it actually necessary to call this before checking the description?

@myronmarston

myronmarston Apr 9, 2014

Member

Is it actually necessary to call this before checking the description?

This comment has been minimized.

@yelled3

yelled3 Apr 9, 2014

Contributor

nope :-)

@yelled3

yelled3 Apr 9, 2014

Contributor

nope :-)

spec/rspec/matchers/built_in/all_spec.rb
+ }.to fail_with(dedent <<-EOS)
+ |expected ["A", "A", "A", "C", "A"] to all eq "A"
+ |
+ |object at index "3" failed to match:

This comment has been minimized.

@myronmarston

myronmarston Apr 9, 2014

Member

Hmm, I just noticed that the index is presented as a string, but it's actually an integer right? Why are there quotes around it?

@myronmarston

myronmarston Apr 9, 2014

Member

Hmm, I just noticed that the index is presented as a string, but it's actually an integer right? Why are there quotes around it?

This comment has been minimized.

@yelled3

yelled3 Apr 9, 2014

Contributor

you're correct - I did this purely for readability.

object at index "3" failed to match:

vs

object at index 3 failed to match:

would you prefer another message format?

@yelled3

yelled3 Apr 9, 2014

Contributor

you're correct - I did this purely for readability.

object at index "3" failed to match:

vs

object at index 3 failed to match:

would you prefer another message format?

features/built_in_matchers/all.feature
+
+ ```ruby
+ expect([1, 3, 5]).to all( be_odd.and be_an(Integer) )
+ expect([1, 3, 4, 6]).to all( be_odd.or be_even )

This comment has been minimized.

@myronmarston

myronmarston Apr 9, 2014

Member

These are both kinda tautologies...be_odd implies it's an integer, so be_an(Integer) doesn't add value, and be_odd.or be_even is obviously always true of an array of integers. Would be good to use something a bit more realistic...maybe be_odd.and be > 0?

@myronmarston

myronmarston Apr 9, 2014

Member

These are both kinda tautologies...be_odd implies it's an integer, so be_an(Integer) doesn't add value, and be_odd.or be_even is obviously always true of an array of integers. Would be good to use something a bit more realistic...maybe be_odd.and be > 0?

This comment has been minimized.

@yelled3

yelled3 Apr 9, 2014

Contributor

agreed, changed to:

expect([1, 3, 5]).to all( be_odd.and be < 10 )
expect([1, 3, 22]).to all( be_odd.or be < 10 )
@yelled3

yelled3 Apr 9, 2014

Contributor

agreed, changed to:

expect([1, 3, 5]).to all( be_odd.and be < 10 )
expect([1, 3, 22]).to all( be_odd.or be < 10 )
lib/rspec/matchers.rb
+ # @example
+ #
+ # expect([1, 3, 5]).to all( be_odd.and be_an(Integer) )
+ def all(*expected)

This comment has been minimized.

@myronmarston

myronmarston Apr 9, 2014

Member

You know, we talked about this before but now I've flipped a bit...given that you can do compound expressions, there doesn't seem to be a need to accept a splat of expected matchers, so I think I'd rather go with this only supporting one for now. If an argument is made in the future for supporting the splat for a reason we haven't thought of we can consider opening it up, but I prefer starting with the smaller interface as it's easier to maintain smaller interfaces :).

@myronmarston

myronmarston Apr 9, 2014

Member

You know, we talked about this before but now I've flipped a bit...given that you can do compound expressions, there doesn't seem to be a need to accept a splat of expected matchers, so I think I'd rather go with this only supporting one for now. If an argument is made in the future for supporting the splat for a reason we haven't thought of we can consider opening it up, but I prefer starting with the smaller interface as it's easier to maintain smaller interfaces :).

This comment has been minimized.

@yelled3

yelled3 Apr 9, 2014

Contributor

I'm cool with starting small :-)

@yelled3

yelled3 Apr 9, 2014

Contributor

I'm cool with starting small :-)

spec/rspec/matchers/built_in/all_spec.rb
+
+ context 'when composed matcher is given' do
+
+ let(:inner_matcher) { be_between(2, 5).or be_between(6, 9) }

This comment has been minimized.

@myronmarston

myronmarston Apr 9, 2014

Member

Same here...I'd prefer to see this let inlined.

@myronmarston

myronmarston Apr 9, 2014

Member

Same here...I'd prefer to see this let inlined.

spec/rspec/matchers/built_in/all_spec.rb
+ |expected: "A"
+ | got: "C"
+ |
+ |(compared using ==)

This comment has been minimized.

@myronmarston

myronmarston Apr 9, 2014

Member

Seeing this full output (which is quite good -- nice work!) I'm noticing a few things that could be improved:

  • Given that the individual failure messages are essentially sub-expressions, I think it would be good for them to be indented -- one indentation level for the object at... message, and one additional indentation level for the message under that.
  • It would be nice to not have two blank lines between items -- it seems like to much space, IMO.

Putting this together with my feedback above about the integer indexes, I'm imagining output like this:

expected ["A", "B", "A", "C", "A"] to all eq "A"

  object at index 1 failed to match:
    expected: "A"
         got: "B"

    (compared using ==)

  object at index 3 failed to match:
    expected: "A"
         got: "C"

    (compared using ==)

You can take a look at the compound matchers for ideas of how we've done this kind of formatting elsewhere:

def indent_multiline_message(message)
message.lines.map do |line|
line =~ /\S/ ? ' ' + line : line
end.join
end
def compound_failure_message
message_1 = matcher_1.failure_message
message_2 = matcher_2.failure_message
if multiline?(message_1) || multiline?(message_2)
multiline_message(message_1, message_2)
else
singleline_message(message_1, message_2)
end
end
def multiline_message(message_1, message_2)
[
indent_multiline_message(message_1.sub(/\n+\z/, '')),
"...#{conjunction}:",
indent_multiline_message(message_2.sub(/\A\n+/, ''))
].join("\n\n")
end

@myronmarston

myronmarston Apr 9, 2014

Member

Seeing this full output (which is quite good -- nice work!) I'm noticing a few things that could be improved:

  • Given that the individual failure messages are essentially sub-expressions, I think it would be good for them to be indented -- one indentation level for the object at... message, and one additional indentation level for the message under that.
  • It would be nice to not have two blank lines between items -- it seems like to much space, IMO.

Putting this together with my feedback above about the integer indexes, I'm imagining output like this:

expected ["A", "B", "A", "C", "A"] to all eq "A"

  object at index 1 failed to match:
    expected: "A"
         got: "B"

    (compared using ==)

  object at index 3 failed to match:
    expected: "A"
         got: "C"

    (compared using ==)

You can take a look at the compound matchers for ideas of how we've done this kind of formatting elsewhere:

def indent_multiline_message(message)
message.lines.map do |line|
line =~ /\S/ ? ' ' + line : line
end.join
end
def compound_failure_message
message_1 = matcher_1.failure_message
message_2 = matcher_2.failure_message
if multiline?(message_1) || multiline?(message_2)
multiline_message(message_1, message_2)
else
singleline_message(message_1, message_2)
end
end
def multiline_message(message_1, message_2)
[
indent_multiline_message(message_1.sub(/\n+\z/, '')),
"...#{conjunction}:",
indent_multiline_message(message_2.sub(/\A\n+/, ''))
].join("\n\n")
end

This comment has been minimized.

@yelled3

yelled3 Apr 9, 2014

Contributor

I think it would be good for them to be indented

good idea - it reads much better 👍

It would be nice to not have two blank lines between items

could you please explain?
the example you gave have the same amount of blank lines.

@yelled3

yelled3 Apr 9, 2014

Contributor

I think it would be good for them to be indented

good idea - it reads much better 👍

It would be nice to not have two blank lines between items

could you please explain?
the example you gave have the same amount of blank lines.

This comment has been minimized.

@yelled3

This comment has been minimized.

Show comment
Hide comment
@yelled3

yelled3 Apr 9, 2014

Contributor

@JonRowe thanks for helping with the CI :-)

Contributor

yelled3 commented Apr 9, 2014

@JonRowe thanks for helping with the CI :-)

lib/rspec/matchers/built_in/all.rb
+ def index_failed_objects
+ actual.each_with_index do |actual_item, index|
+ unless values_match?(matcher, actual_item)
+ matcher.matches?(actual_item) # this is needed in order to generate the 'matcher.failure_message'

This comment has been minimized.

@yelled3

yelled3 Apr 9, 2014

Contributor

@myronmarston this the issue I mentioned:
#491 (comment)

@yelled3

yelled3 Apr 9, 2014

Contributor

@myronmarston this the issue I mentioned:
#491 (comment)

This comment has been minimized.

@myronmarston

myronmarston Apr 9, 2014

Member

Right...that wasn't thinking about the need for the message. I think we should switch back to matchers.matches? to get the message and only have it called once. However, once you do that, it's important that you clone the matcher yourself before calling matches? on it to avoid bugs with matchers that use internal memoization based on the actual value. It'd be good to have a failing spec for that before you fix it -- you can see an example of a spec like that in composable_spec.rb.

@myronmarston

myronmarston Apr 9, 2014

Member

Right...that wasn't thinking about the need for the message. I think we should switch back to matchers.matches? to get the message and only have it called once. However, once you do that, it's important that you clone the matcher yourself before calling matches? on it to avoid bugs with matchers that use internal memoization based on the actual value. It'd be good to have a failing spec for that before you fix it -- you can see an example of a spec like that in composable_spec.rb.

This comment has been minimized.

@myronmarston

myronmarston Apr 9, 2014

Member

Also, it'd be good to have a spec that forces you to use clone rather than dup by passing all a DSL-defined custom matcher. If we were going to use values_match? we wouldn't need these specs as that handles it internally but given we have to handle those cases here we should have specs to enforce that.

@myronmarston

myronmarston Apr 9, 2014

Member

Also, it'd be good to have a spec that forces you to use clone rather than dup by passing all a DSL-defined custom matcher. If we were going to use values_match? we wouldn't need these specs as that handles it internally but given we have to handle those cases here we should have specs to enforce that.

This comment has been minimized.

yelled3 pushed a commit to yelled3/rspec-expectations that referenced this pull request Apr 9, 2014

@yelled3

This comment has been minimized.

Show comment
Hide comment
@yelled3

yelled3 Apr 9, 2014

Contributor

@myronmarston @JonRowe all squashed :-)
hope it didn't remove any of the comments.

Contributor

yelled3 commented Apr 9, 2014

@myronmarston @JonRowe all squashed :-)
hope it didn't remove any of the comments.

spec/rspec/matchers/built_in/all_spec.rb
+ end
+ end
+
+ include_examples "making a copy", [:clone, :dup]

This comment has been minimized.

@myronmarston

myronmarston Apr 9, 2014

Member

It seems silly to make a shared example group, have it support an internal loop and then only include it once. I'd rather make it not support an internal loop and than use it twice (once with a :clone arg, once with a :dup arg).

@myronmarston

myronmarston Apr 9, 2014

Member

It seems silly to make a shared example group, have it support an internal loop and then only include it once. I'd rather make it not support an internal loop and than use it twice (once with a :clone arg, once with a :dup arg).

This comment has been minimized.

@yelled3

yelled3 Apr 10, 2014

Contributor

sure, np

@yelled3

yelled3 Apr 10, 2014

Contributor

sure, np

spec/rspec/matchers/built_in/all_spec.rb
+ expect(copied_matcher.matcher.expected).to eq(3)
+ end
+
+ it "copies custom matchers properly so they can work even though they have singleton behavior" do

This comment has been minimized.

@myronmarston

myronmarston Apr 9, 2014

Member

This docstring mentions custom matchers but the code below does not use any as far as I can tell. This is important, because DSL-defined custom matchers define singleton methods in their implementation and will only work when cloned, not when dupd -- as your specs stand, I think that these would pass if you used dup in your initialize_copy method rather than clone.

@myronmarston

myronmarston Apr 9, 2014

Member

This docstring mentions custom matchers but the code below does not use any as far as I can tell. This is important, because DSL-defined custom matchers define singleton methods in their implementation and will only work when cloned, not when dupd -- as your specs stand, I think that these would pass if you used dup in your initialize_copy method rather than clone.

lib/rspec/matchers/built_in/all.rb
+ def does_not_match?(actual)
+ raise NotImplementedError,
+ '`expect().not_to all( matcher )` is not supported.
+ Please use the negation matcher: `expect().to all( ~matcher )`'

This comment has been minimized.

@myronmarston

myronmarston Apr 9, 2014

Member

We'll need to make sure that the negation and this go out in the same release (otherwise this message will be confusing). (This is mostly a note for myself or whoever merges this).

@myronmarston

myronmarston Apr 9, 2014

Member

We'll need to make sure that the negation and this go out in the same release (otherwise this message will be confusing). (This is mostly a note for myself or whoever merges this).

This comment has been minimized.

@myronmarston

myronmarston Apr 16, 2014

Member

I think this PR is ready to merge except for the mention of negation via ~ (which isn't in master yet and we may go a different direction on) and being squashed. @yelled3, can you remove this sentence and squash you're commits?

@myronmarston

myronmarston Apr 16, 2014

Member

I think this PR is ready to merge except for the mention of negation via ~ (which isn't in master yet and we may go a different direction on) and being squashed. @yelled3, can you remove this sentence and squash you're commits?

@myronmarston

This comment has been minimized.

Show comment
Hide comment
@myronmarston

myronmarston Apr 9, 2014

Member

you're correct - I did this purely for readability.

object at index "3" failed to match:
vs

object at index 3 failed to match:
would you prefer another message format?

I prefer the raw integer (3), as I said.

agreed, changed to:

expect([1, 3, 22]).to all( be_odd.or be < 10 )

This is confusing -- the expectation doesn't pass (22 is neither odd nor less than 10). Maybe change 3 to 4 and 22 to 21? Then it'll pass.

Member

myronmarston commented Apr 9, 2014

you're correct - I did this purely for readability.

object at index "3" failed to match:
vs

object at index 3 failed to match:
would you prefer another message format?

I prefer the raw integer (3), as I said.

agreed, changed to:

expect([1, 3, 22]).to all( be_odd.or be < 10 )

This is confusing -- the expectation doesn't pass (22 is neither odd nor less than 10). Maybe change 3 to 4 and 22 to 21? Then it'll pass.

@myronmarston

This comment has been minimized.

Show comment
Hide comment
@myronmarston

myronmarston Apr 9, 2014

Member

could you please explain?
the example you gave have the same amount of blank lines.

When I wrote the comment the spec showed 2 blank lines between (compared using ==) and object at index "3" failed to match:. Looks like you've fixed it now, though.

Member

myronmarston commented Apr 9, 2014

could you please explain?
the example you gave have the same amount of blank lines.

When I wrote the comment the spec showed 2 blank lines between (compared using ==) and object at index "3" failed to match:. Looks like you've fixed it now, though.

spec/rspec/matchers/built_in/all_spec.rb
+ EOS
+ end
+
+ it 'returns the index of the failed object' do

This comment has been minimized.

@myronmarston

myronmarston Apr 9, 2014

Member

This doc string is the same as above. I'd change it to it "returns the indexes of all failed objects, not just the first one" (if your implementation used all?, for example, it would stop after the first failure).

@myronmarston

myronmarston Apr 9, 2014

Member

This doc string is the same as above. I'd change it to it "returns the indexes of all failed objects, not just the first one" (if your implementation used all?, for example, it would stop after the first failure).

@yelled3

This comment has been minimized.

Show comment
Hide comment
@yelled3

yelled3 Apr 10, 2014

Contributor

@myronmarston take a look at the last batch of fixes

Contributor

yelled3 commented Apr 10, 2014

@myronmarston take a look at the last batch of fixes

spec/rspec/matchers/built_in/all_spec.rb
+ include_examples 'making a copy', :clone
+ include_examples 'making a copy', :dup
+
+ RSpec::Matchers.define :have_string_length do |expected|

This comment has been minimized.

lib/rspec/matchers/built_in/all.rb
+ attr_reader :matcher, :failed_objects
+
+ def initialize(matcher)
+ @matcher = matcher.clone

This comment has been minimized.

@myronmarston

myronmarston Apr 10, 2014

Member

Why are you cloning the matcher here? It doesn't need to be cloned here. But it does need to be cloned below (I'll post a comment where that is needed).

@myronmarston

myronmarston Apr 10, 2014

Member

Why are you cloning the matcher here? It doesn't need to be cloned here. But it does need to be cloned below (I'll post a comment where that is needed).

+ end
+
+ def index_failed_objects
+ actual.each_with_index do |actual_item, index|

This comment has been minimized.

@myronmarston

myronmarston Apr 10, 2014

Member

The matcher needs to be cloned before calling matches? on it on the line below, because the matcher may memoize based on the actual value given to it, so we need to ensure that it's a fresh instance of the matcher for each item we match against.

Note that your tests are not written to properly catch this, so please get your test right first (e.g. it should fail in an expected way) before fixing the bug.

@myronmarston

myronmarston Apr 10, 2014

Member

The matcher needs to be cloned before calling matches? on it on the line below, because the matcher may memoize based on the actual value given to it, so we need to ensure that it's a fresh instance of the matcher for each item we match against.

Note that your tests are not written to properly catch this, so please get your test right first (e.g. it should fail in an expected way) before fixing the bug.

This comment has been minimized.

@yelled3

yelled3 Apr 16, 2014

Contributor

tested and fixed:
yelled3@4a15c70

@yelled3

yelled3 Apr 16, 2014

Contributor

tested and fixed:
yelled3@4a15c70

spec/rspec/matchers/built_in/all_spec.rb
+
+ RSpec::Matchers.define :custom_include do |*args|
+ match { |actual| expect(actual).to include(*args) }
+ end

This comment has been minimized.

@myronmarston

myronmarston Apr 10, 2014

Member

Redefining this matcher generates warnings:

/Users/myron/code/rspec-dev/repos/rspec-expectations/lib/rspec/matchers/dsl.rb:8: warning: method redefined; discarding old custom_include

The other custom matchers below (have_string_length and be_a_clone) have the same problem. They were defined in-line before because they were only used in one spot. At this point, we should move them into spec/support/matchers.rb.

@myronmarston

myronmarston Apr 10, 2014

Member

Redefining this matcher generates warnings:

/Users/myron/code/rspec-dev/repos/rspec-expectations/lib/rspec/matchers/dsl.rb:8: warning: method redefined; discarding old custom_include

The other custom matchers below (have_string_length and be_a_clone) have the same problem. They were defined in-line before because they were only used in one spot. At this point, we should move them into spec/support/matchers.rb.

This comment has been minimized.

@yelled3

yelled3 Apr 16, 2014

Contributor

thanks

@yelled3

yelled3 Apr 16, 2014

Contributor

thanks

spec/rspec/matchers/built_in/all_spec.rb
+ context "when using a matcher instance that memoizes state multiple times in a composed expression" do
+ it "works properly in spite of the memoization" do
+ expect(["foo", "bar", "xyz"]).to all( have_string_length(3) )
+ end

This comment has been minimized.

@myronmarston

myronmarston Apr 10, 2014

Member

This example will never fail. The value that the matcher memoizes is the string length, but they all have the same string length so wouldn't ever cause them to fail. Instead I think you need to set a negative expectation:

expect {
  expect(["foo", "bar", "a"]).to all( have_string_length(3) )
}.to fail

In this case, if the string length is memoized the all expectation would pass but it should fail.

@myronmarston

myronmarston Apr 10, 2014

Member

This example will never fail. The value that the matcher memoizes is the string length, but they all have the same string length so wouldn't ever cause them to fail. Instead I think you need to set a negative expectation:

expect {
  expect(["foo", "bar", "a"]).to all( have_string_length(3) )
}.to fail

In this case, if the string length is memoized the all expectation would pass but it should fail.

This comment has been minimized.

@yelled3

yelled3 Apr 16, 2014

Contributor

cheers - used your example :-)

@yelled3

yelled3 Apr 16, 2014

Contributor

cheers - used your example :-)

spec/rspec/matchers/built_in/all_spec.rb
+ end
+ end
+
+ describe "cloning data structures containing matchers" do

This comment has been minimized.

@myronmarston

myronmarston Apr 10, 2014

Member

The all matcher isn't involved in cloning data structures containing matchers. That's only something composable has to deal with since values_matches? supports being given an array or hash with nested matchers. all doesn't support that (and your specs below don't create any data structures containing matchers...)

@myronmarston

myronmarston Apr 10, 2014

Member

The all matcher isn't involved in cloning data structures containing matchers. That's only something composable has to deal with since values_matches? supports being given an array or hash with nested matchers. all doesn't support that (and your specs below don't create any data structures containing matchers...)

This comment has been minimized.

@yelled3

yelled3 Apr 16, 2014

Contributor

removed

@yelled3

yelled3 Apr 16, 2014

Contributor

removed

@yelled3

This comment has been minimized.

Show comment
Hide comment
@yelled3

yelled3 Apr 17, 2014

Contributor

@myronmarston

  • removed the ~ message
  • updated changelog
  • all squashed
Contributor

yelled3 commented Apr 17, 2014

@myronmarston

  • removed the ~ message
  • updated changelog
  • all squashed

myronmarston added a commit that referenced this pull request Apr 17, 2014

@myronmarston myronmarston merged commit 43ffa79 into rspec:master Apr 17, 2014

1 check passed

continuous-integration/travis-ci The Travis CI build passed
Details
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment