-
-
Notifications
You must be signed in to change notification settings - Fork 358
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
base: main
Are you sure you want to change the base?
Prevent error formatting from causing recursive errors #1275
Conversation
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.
There was a problem hiding this 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 :)
@@ -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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 stillallowed_to_fail
here, so this hits the same stack overflowsafe_invoke
, on the other hand, stops the error, but increments the count so this error message ends up beingexpected: 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.
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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
@jamesdabbs-procore would you be able to finish this one off and close it out? |
Ah, sorry. Lost this thread. Definitely, although it might be sometime this weekend. |
@jamesdabbs-procore ping! |
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
@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 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 |
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? |
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? |
This comes from tracking down rspec/rspec-rails#2049 wherein
overflows. There
Devise
has defined aninspect
method which callsemail
, so when the second call toemail
errors, the error generator'sinspect
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!