Skip to content
This repository was archived by the owner on Nov 30, 2024. It is now read-only.

and_yield not using default block argument when no argument given. #714

Closed
wants to merge 1 commit into from

Conversation

tom025
Copy link
Contributor

@tom025 tom025 commented Jun 13, 2014

Using ruby 2.1.2

default_arg = Object.new
obj = Object.new

allow(obj).to receive(:a_message).and_yield
expect(default_arg).to receive(:bar)

obj.a_message do |receiver=default_arg|
  receiver.bar
end

The above code fails with:

 Failure/Error: obj.a_message do |receiver=default_arg|
        #<Object:0x007fe3ac1ce408> yielded || to block with arity of 1

The block has a arity of 0 or 1 not 1. So I would expect that when no argument
is given to and_yield it would use the default block argument.

I have a supplied a test that exposes this but I'm not sure how to fix this.

Using ruby 2.1.2

```ruby
default_arg = Object.new
obj = Object.new

allow(obj).to receive(:a_message).and_yield
expect(default_arg).to receive(:bar)

obj.a_message do |receiver=default_arg|
  receiver.bar
end
```

The above code fails with:

```
 Failure/Error: obj.a_message do |receiver=default_arg|
        #<Object:0x007fe3ac1ce408> yielded || to block with arity of 1
```

The block has a arity of 0 or 1 not 1. So I would expect that when no argument
is given to `and_yield` it would use the default block argument.
@myronmarston
Copy link
Member

Thanks, I'll look into this.

@myronmarston myronmarston self-assigned this Jun 16, 2014
@JonRowe
Copy link
Member

JonRowe commented Jun 17, 2014

(I verified this failed as described FWIW)

@myronmarston
Copy link
Member

So the problem is that ruby provides no way to differentiate between a block arg that has a default and a block arg that does not:

irb(main):001:0> Proc.new { |a| }.parameters
=> [[:opt, :a]]
irb(main):002:0> Proc.new { |a=1| }.parameters
=> [[:opt, :a]]

This is because procs/blocks have an implicit default of nil for every argument:

irb(main):003:0> Proc.new { |a| puts a.inspect }.call
nil
=> nil

...so { |a| } is really { |a=nil| }.

Lambdas, on the other hand, do differentiate:

irb(main):004:0> lambda { |a| }.parameters
=> [[:req, :a]]
irb(main):005:0> lambda { |a=1| }.parameters
=> [[:opt, :a]]

and_yield treats all block args as required args in order to provide an error when the arities don't match up:

irb(main):001:0> require 'rspec/mocks/standalone'
=> true
irb(main):002:0> dbl = double
=> #<RSpec::Mocks::Double:0x3fe0e3ce1784 @name=nil>
irb(main):003:0> allow(dbl).to receive(:foo).and_yield(1, 2); nil
=> nil
irb(main):004:0> dbl.foo { |a| }
RSpec::Mocks::MockExpectationError: Double yielded |1, 2| to block with arity of 1
    from /Users/myron/code/rspec-dev/repos/rspec-mocks/lib/rspec/mocks/error_generator.rb:206:in `__raise'
    from /Users/myron/code/rspec-dev/repos/rspec-mocks/lib/rspec/mocks/error_generator.rb:160:in `raise_wrong_arity_error'
    from /Users/myron/code/rspec-dev/repos/rspec-mocks/lib/rspec/mocks/message_expectation.rb:548:in `block in call'
    from /Users/myron/code/rspec-dev/repos/rspec-mocks/lib/rspec/mocks/message_expectation.rb:546:in `each'
    from /Users/myron/code/rspec-dev/repos/rspec-mocks/lib/rspec/mocks/message_expectation.rb:546:in `call'
    from /Users/myron/code/rspec-dev/repos/rspec-mocks/lib/rspec/mocks/message_expectation.rb:581:in `block in call'
    from /Users/myron/code/rspec-dev/repos/rspec-mocks/lib/rspec/mocks/message_expectation.rb:580:in `map'
    from /Users/myron/code/rspec-dev/repos/rspec-mocks/lib/rspec/mocks/message_expectation.rb:580:in `call'
    from /Users/myron/code/rspec-dev/repos/rspec-mocks/lib/rspec/mocks/message_expectation.rb:484:in `invoke_incrementing_actual_calls_by'
    from /Users/myron/code/rspec-dev/repos/rspec-mocks/lib/rspec/mocks/message_expectation.rb:213:in `invoke'
    from /Users/myron/code/rspec-dev/repos/rspec-mocks/lib/rspec/mocks/proxy.rb:163:in `message_received'
    from /Users/myron/code/rspec-dev/repos/rspec-mocks/lib/rspec/mocks/method_double.rb:73:in `proxy_method_invoked'
    from /Users/myron/code/rspec-dev/repos/rspec-mocks/lib/rspec/mocks/method_double.rb:60:in `block (2 levels) in define_proxy_method'
    from (irb):4
    from /Users/myron/.rubies/ruby-2.1.1-p76/bin/irb:11:in `<main>'

...since this generally represents a mistake. I believe it's an old feature (from rspec-1, before my involvement with the project) that probably arose at a time when ruby didn't even allow blocks to have default arguments, so your use case wasn't on the radar.

Given the history here, how things currently work, and the fact that ruby provides no way to differentiate args with user-provided defaults vs not...I'm not sure what to do here. Maybe we should consider dropping the error entirely or making it opt-in via a config option, but I'd want more community feedback before going that route. For now you can work around this by passing your own implementation as a block. For example, here's a passing version of the failing spec from this PR:

it "yields the default argument when the argument is not given" do
  default_arg = Object.new
  obj = Object.new

  allow(obj).to receive(:a_message) { |&blk| blk.call }
  expect(default_arg).to receive(:bar)

  obj.a_message do |receiver=default_arg|
    receiver.bar
  end
end

@myronmarston
Copy link
Member

Hmm, maybe we can leverage something like this: https://github.com/schneems/proc_to_lambda

@JonRowe
Copy link
Member

JonRowe commented Jun 17, 2014

Convert it to a lambda internally? Would that work?

@myronmarston
Copy link
Member

It appears to:

irb(main):001:0> module ProcToLambda
irb(main):002:1>   def to_lambda
irb(main):003:2>     ProcToLambda.to_lambda(self)
irb(main):004:2>   end
irb(main):005:1>
irb(main):006:1*   def self.to_lambda(block)
irb(main):007:2>     if RUBY_ENGINE && RUBY_ENGINE == "jruby"
irb(main):008:3>       return lambda(&block)
irb(main):009:3>     else
irb(main):010:3*       obj = Object.new
irb(main):011:3>       obj.define_singleton_method(:_, &block)
irb(main):012:3>       return obj.method(:_).to_proc
irb(main):013:3>     end
irb(main):014:2>   end
irb(main):015:1> end
=> :to_lambda
irb(main):016:0> ProcToLambda.to_lambda(Proc.new { |a| }).parameters
=> [[:req, :a]]

I don't think I'd want to call the converted lambda (there appears to be some issues with self - http://stackoverflow.com/questions/2946603/ruby-convert-proc-to-lambda), but for the purposes of seeing what the parameters are and what's required vs optional, converting to a lambda before calling parameters seems to work like we want. I'll take a stab at this...

@JonRowe
Copy link
Member

JonRowe commented Jun 17, 2014

We'd only have to convert it to check the param counts right?

@myronmarston
Copy link
Member

We'd only have to convert it to check the param counts right?

Basically...that's what I was trying to say.

@myronmarston
Copy link
Member

I've got a fix in rspec/rspec-support#83. #719 is building against that branch.

myronmarston added a commit to rspec/rspec-support that referenced this pull request Jun 19, 2014
myronmarston added a commit to rspec/rspec-support that referenced this pull request Jun 19, 2014
@tom025
Copy link
Contributor Author

tom025 commented Jun 19, 2014

Thanks for looking into this and for coming up with a solution so quickly!

On Thursday, 19 June 2014, Myron Marston [email protected] wrote:

I've got a fix in rspec/rspec-support#83
rspec/rspec-support#83. #719
#719 is building against that
branch.


Reply to this email directly or view it on GitHub
#714 (comment).

myronmarston added a commit to rspec/rspec-support that referenced this pull request Jun 19, 2014
myronmarston added a commit to rspec/rspec-support that referenced this pull request Jun 20, 2014
There's a very odd ruby bug that makes this unsafe to use, as its
usage mutates the behavior of the block:

https://bugs.ruby-lang.org/issues/9967

Unfortunately, this un-fixes rspec/rspec-mocks#714, but we've got to do it.
@myronmarston
Copy link
Member

@tom025 -- unfortunately, there's a very odd bug in ruby that's forcing us to revert the fix we did for this:

https://bugs.ruby-lang.org/issues/9967

I plan to cut rspec-support 3.0.2 with that reversion later today. So we're back to square one with this :(. Any ideas?

@myronmarston myronmarston reopened this Jun 20, 2014
myronmarston added a commit to rspec/rspec-support that referenced this pull request Jun 21, 2014
There's a very odd ruby bug that makes this unsafe to use, as its
usage mutates the behavior of the block:

https://bugs.ruby-lang.org/issues/9967

Unfortunately, this un-fixes rspec/rspec-mocks#714, but we've got to do it.
yujinakayama pushed a commit to yujinakayama/rspec-monorepo that referenced this pull request Oct 6, 2021
yujinakayama pushed a commit to yujinakayama/rspec-monorepo that referenced this pull request Oct 6, 2021
There's a very odd ruby bug that makes this unsafe to use, as its
usage mutates the behavior of the block:

https://bugs.ruby-lang.org/issues/9967

Unfortunately, this un-fixes rspec/rspec-mocks#714, but we've got to do it.

---
This commit was imported from rspec/rspec-support@17c5b46.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants