-
-
Notifications
You must be signed in to change notification settings - Fork 802
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
"Different number of parameters" error when passing to Returns a dynamically compiled Expression #652
Comments
@MaMazav, thanks for reporting this, and for your detailed preliminary analysis. Weird, I have never encountered (or noticed) this additional What appears to be missing in I'd like to look more closely at this. Also, it has always bothered me a bit that there appear to be so many different ways how Moq validates method signatures. But I'm not sure that can be simplified further.
|
Methods compiled from expression trees can have an additional first parameter of type `Sytem.Runtime.CompilerServices.Closure` which apparently captures all closed-over variables. This additional parameter can trigger a parameter count mismatch error in `MethodCallReturn.ValidateNumberOfCallbackParameters`. The test code is taken from: devlooped#652 (comment)
(Sorry in advance if I'm talking about already discussed issue). I think that the problem is deeper than the Closure argument and touches the general strategy of argument check in Returns. I tried to fix the issue by changing the MethodCallReturn path to use the HasCompatibleParameterList method, but then some tests failed. However the failing tests expose the fact that there was a difference between the Callback limitations and the Returns limitation.
Although this line seems reasonable, because the It.Any causes the valueFunction to get a T argument for sure, it's only in mistake. For example, I tried to add the following line in the caller method (HelperSetupTest, line 3510):
And it was not caught by the argument check (but crashed later when tried to call the method). Moreover, when tried to replace the above method with Callback instead of Returns, the argument check caught the problem immediately:
So first we need to answer the question - is this inconsistency between Returns and Callback reasonable? And if so, why? |
Yes, I noticed that too when we implemented stronger callback checks going from 4.7.x to 4.8.0. I think there are actual use cases for the more lenient handling of
It doesn't matter a lot whether it's reasonable or not. This inconsistency has been in Moq for a long time; changing anything about it will invariably break things for users. This issue and the feedback we got for 4.8.0 (where we made At this point, I think it would be wise not to do anything at all about it and just live with that inconsistency until Moq 5 is published. Being a new major version, it can afford breaking changes more easily. |
OK. As I said there is the option to add another method and mark the old problematic Returns overloading as deprecated, but I see your point. EDIT: You can see the suggested solution here: idigra@dac25d6 |
I don't know whether this is even possible. If you wanted to catch this error at compile-time, you would need a
I suspect that (1) is an unsolvable problem given C#'s type system. And possibly as a result of that, we have
Moq knows too many ways of comparing method signatures already, so this sounds interesting. I've tried to merge these different strategies in the past, and have failed. There are some small differences between them and IIRC, there's a reason for that. That being said, feel free to work on this and submit a PR. Just bear in mind two things: (1) No new API methods (differently-named variants on existing methods). The API should stay straight-forward, and not offer you a bunch of similarly-named methods that do slightly different things. (2) Breaking changes should be avoided. |
@idigra: I'll be merging the PR that solves the immediate issue reported here, so this present issue will get closed. Please don't let this irritate you. We can keep on discussing the additional issues you've raised either here, or in a new dedicated issue if you prefer. |
Regards your answer about early catch of signature inconsistency - I didn't talk about compile time check. I talked about checking when installing the moq method (on call to Returns) rather than deferring it to a strange exception when the actual method is called, as it happens today in the one liner code example I supplied and you quoted. This early check is possible for sure, as it happens in the Callback method. Regards my suggestion - you probably missed my late edit from the previous post in which I added the code suggestion. It meets your two requirements: idigra@dac25d6 |
I see; my bad. This reminds me of #445, #519, and #520. If you decide that you'd like to work on this, please study these first so we won't end up repeating history. :) |
@idigra - I'm sorry, I'm getting a little confused. I think we're discussing two things at the same time: (1) Catching errors early. (2) Using |
Yes, we are talking about (2). I left the idea of catching errors early when you raised the concern about breaking change and API changes. This change will make the checks more permissive rather than catching problems earlier. Until now I didn't consider performance at all. I guessed that the fact that HasCompatibleParameterList used in Callback means that we are OK with its performance. Anyway, we should ask my previous question also about performance: Is there a performance requirements difference between Callback and Returns which allows us using the more stable and less performant HasCompatibleParameterList method only in Callback? |
I still don't get it; sorry. Your change, as you said, makes I totally agree that fail-early would be preferable if the setup is faulty and there's no chance for the actual mocked method calls to succeed. Because of that I think returning early and skipping important checks in the Performance isn't super crucial in these methods, but if we degrade it (however slightly) only to skip cheap checks that shouldn't be skipped, then that's when I complain. And finally, more to the point: Why is |
Let me go and take a close look at the validation methods in question. I'm obviously missing something here. I'll be back. |
As I said, I gave up about failing early when you wanted to avoid any breaking change. I'm not sure I understand why Callback and Returns doesn't share same validation requirements. E.g. the usecase of the It.Any with int callback (as in the failing test above) is reasonable also for Callback. But I guess your intuitions are more sharp than mine after seeing more Moq usecases than me. |
What I don't understand is why you put so much trust in that one method. It may be just as faulty, or faulty ways of its own, as other validation logic. Who says it captures any more corner cases just because it might have caught this one here?
Neither do I. But Moq's been like that for longer than I have been with Moq. :) I suspect this divergence started out as a simple accident / undiscovered bug, and now time has essentially frozen it into a de-facto specification because user code has likely come to rely on this behavior by now. That's probably all there is to it. This should be straightened out with Moq 5. |
BTW if you are worried about
Copied from my answer in #655: IMO Checking the standard .NET Invoke method is a more stable way to eliminate such manipulations than implementing a heuristics and extend it on each case we encounter (as was really proved in the Closure bug).
If no real reason to have divergence except of historical ones and it's going to be merged in Moq 5, I don't see a reason why not doing another step to this direction. It will not catch false positive due to the avoidance of breaking change, but at least it will eliminate false negatives which the Closure bug might be only one example among others. BTW, regards the performance concern - I can move the permissive check to after the existing logic instead of before. That way performance will not degrade for all existing working scenarios. However if that solution is going to be pushed into Moq 5 (is it? what did you think to be the correct merged strategy?), I think it will be better to have this performance degradation ASAP to get feedback if it's bad (as long as it's not a functional-breaking change, but only performance change). |
That would invalidate the whole idea behind your PR, and be rather pointless, too: If I understand your PR correctly, then its intent is to avoid false negatives triggered by the existing logic (which you suspect to be too strict in cases like the one reported at the beginning of this issue, as well as possibly other cases that we haven't discovered yet). So if you move your proposed check to after the current logic, then it can no longer prevent false negatives. Also, at the end of the current logic (i.e. at the end of the method), it's rather pointless to check whether we should return from the method, because that'll happen anyway. You'd only be making the method more costly. You wrote in your PR:
I am still struggling to understand your faith in What this boils down to is that you seem to trust At the same time as it doesn't have any known benefits, adding an additional Adding that additional check just because it might prevent a problem in a hypothetical, unknown case doesn't feel right to me. If Show me a scenario where |
OK. I see your point. I hope that all cases are really covered. I'll abandon my PR.
By saying "after the current logic" I didn't mean to cut-and-paste the code lines to the end of the method, but to do the new logic after we discovered that we are in one of the two problematic cases. Conceptually, think about that like I move the new code lines to before the two throw statements (not talking now about avoiding code duplication that could be solved e.g. by additional method).
My faith is based on the implementation of both ways. I just trust more on comparing the Invoke's argument which the CLR guarantees to be correct than manual way that heuristically tries to cover more and more cases. |
I sort of get your drift, but I'm afraid I still don't get it precisely. The CLR "guaranteeing" anything doesn't have anything to do with what happens inside Did you know, for example, that |
Thanks, I just verified and it works now. |
When trying to mock the following interface:
By the following code:
While setting callbacks using Callback method succeeds for method compiled on runtime, trying to mock using Returns method fail with "System.ArgumentException: 'Invalid callback. Setup on method with 1 parameter(s) cannot invoke callback with different number of parameters (2).'".
Preliminary investigation:
All compiled method has a "redundant" first argument of type System.Runtime.CompilerServices.Closure. In general, overcoming this redundant parameter is by calling to DynamicInvoke instead of simple Invoke, thus if I understand correctly it should not be a problem in a Moq (some answers on the web talk about that e.g. https://stackoverflow.com/questions/7935306/compiling-a-lambda-expression-results-in-delegate-with-closure-argument/7940617#7940617).
So seems that the problem is only the error method. And indeed, seems that the argument checks differ between the Callback and the Returns method internal implementations: While the former (in file src/Moq/MethodCall.cs, method SetCallbackWithArguments) calls extension method HasCompatibleParameterList to check correctness, the latter (in file src/Moq/MethodCallReturn.cs, method ValidateNumberOfCallbackParameters) have a different check of arguments which seems less stable.
(A side note, this problem was seen by another user: https://stackoverflow.com/questions/49641537/incorrect-number-of-arguments-supplied-for-call-to-method-expression-callmetho).
The text was updated successfully, but these errors were encountered: