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

Prevent error formatting from causing recursive errors #1275

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

jamesdabbs-procore
Copy link

This comes from tracking down rspec/rspec-rails#2049 wherein

it 'should show only 2 times' do
  expect(@student).to     receive(:email).once.and_call_original
  @student.email
  @student.email
end

overflows. There Devise has defined an inspect method which calls email, so when the second call to email errors, the error generator's inspect call goes into a loop.

I've added a spec that I think describes the desired behavior, but which is currently failing against master.

The included lib change fixes the spec, but this is my first time digging into the RSpec codebase, and it's entirely possible there's a better place to make that change.

Please let me know if I've mis-understood the desired behavior, or if there's anything else you'd like to see changed. Thanks!

as initially reported in rspec/rspec-rails#2049

The issue occurs when a class defines an `inspect` method which depends on
methods that have been stubbed to e.g. `receive(...).at_most(n).times`.
Failures try to call `inspect` which fails which calls `inspect` which ...
The error generator may make subsequent calls back in to the erroring
method. This prevents those calls from cascading errors or incrementing
the call count.
Copy link
Member

@JonRowe JonRowe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch, a few tweaks :)

spec/rspec/mocks/partial_double_spec.rb Outdated Show resolved Hide resolved
spec/rspec/mocks/partial_double_spec.rb Outdated Show resolved Hide resolved
spec/rspec/mocks/partial_double_spec.rb Outdated Show resolved Hide resolved
lib/rspec/mocks/message_expectation.rb Outdated Show resolved Hide resolved
@@ -424,7 +425,11 @@ def safe_invoke(parent_stub, *args, &block)
end

def invoke(parent_stub, *args, &block)
invoke_incrementing_actual_calls_by(1, true, parent_stub, *args, &block)
if @raised
invoke_incrementing_actual_calls_by(0, false, parent_stub, *args, &block)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to do this at all.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely sure what you're saying here. Are you saying this could just be?:

def invoke(...)
  return if @failed 
  invoke_incrementing_actual_calls_by(1, true, ...)
end

If so, that breaks the new spec as written b/c inspect is expecting to receive the correct name post-failure. I also considered extracting an "invoke directly" from around here for clarity, or unsetting the method proxy once it failed, but those seemed like larger changes. If any of those approaches sound better, or if I'm misunderstanding the comment, please let me know.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well wouldn't invoke_without_incrementing_received_count be a better choice?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So after digging a bit, I'm not entirely sure what's best here.

  • invoke_without_incrementing_received_count is still allowed_to_fail here, so this hits the same stack overflow
  • safe_invoke, on the other hand, stops the error, but increments the count so this error message ends up being expected: 1 time with any arguments\n received: 3 times with any arguments instead, which is a little counter-intuitive for end users.

It seems like invoke_incrementing_actual_calls_by(0, false, ...) is what needs to happen here, unless you're envisioning larger changes elsewhere.

Copy link
Member

@JonRowe JonRowe Nov 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your patience here, I'd rather the code expressed what it does, if we have to change invoke_without_incrementing_received_count so we can send the false thats ok, we might just need two methods, personally I think the whole matcher implementation needs refactoring a bit breaking apart the methods so we don't have to pass these counts and a boolean flag...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally! Thanks for the feedback. I should have a little bit of downtime over the Thanksgiving weekend, so I'll take a look and aim to have something up next week.

following comments from CR
@fables-tales
Copy link
Member

@jamesdabbs-procore would you be able to finish this one off and close it out?

@jamesdabbs-procore
Copy link
Author

Ah, sorry. Lost this thread. Definitely, although it might be sometime this weekend.

@pirj
Copy link
Member

pirj commented Nov 15, 2019

@jamesdabbs-procore ping!

@jamesdabbs-procore
Copy link
Author

jamesdabbs-procore commented Nov 18, 2019

Oh, sorry. I posted a threaded response earlier - #1275 (comment) - and could use some feedback there. (I'm not entirely sure what change is being requested.)

@@ -556,6 +561,7 @@ def invoke_incrementing_actual_calls_by(increment, allowed_to_fail, parent_stub,
args.unshift(orig_object) if yield_receiver_to_implementation_block?

if negative? || (allowed_to_fail && (@exactly || @at_most) && (@actual_received_count == @expected_received_count))
@failed = true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to remove this, and replace it with an @invoking check further down.

e.g

@invoking = true
if implementation.present?
  implementation.call(*args, &block)
elsif parent_stub
  parent_stub.invoke(nil, *args, &block)
end
@invoking = false

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think thats safer and I think theres a risk of false positives as is, as it won't record further invocations?

If the error generator sends a message that we've expected `exactly` or `at_most` some number of times, we get a recursive error and stack overflow. This ensures that if we re-`invoke` while already `invoke`ing, we don't increment the count or allow errors to bubble up.
@jamesdabbs-procore
Copy link
Author

@JonRowe - I think that last implements the changes you were requesting. I also don't love the proliferation of flags / method-name combinations. Are the existing safe_invoke and invoke_without_incrementing_received_count intended to be part of the publicly supported API here? (Is invoke itself?) If not, we could standardize on something like

def invoke(parent_stub, *args, increment: 1, allow_failure: !@invoking, &block)

and update the existing callers.

What were you thinking in terms of refactoring the existing invoke?

@benoittgt
Copy link
Member

benoittgt commented Dec 13, 2019

I find the current implementation pretty clean and readable.

I don't think we want this to be accessible to the public API. Do you see a reason to have it public? I am not against your proposal but it is little bit more complicated. :)

Could you rebase master for the CI?

@JonRowe
Copy link
Member

JonRowe commented Dec 14, 2019

Ok I'm happy to merge this once CI is green, if I feel that strongly about the method names I'll refactor it myself later 😂, can you give it a rebase?

@JonRowe JonRowe changed the base branch from master to main August 2, 2020 02:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants