-
Notifications
You must be signed in to change notification settings - Fork 265
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
Add extensibility point for custom auto providers #259
Add extensibility point for custom auto providers #259
Conversation
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.
Alex, Thank you for the PR! I left few comments after quick review. But I need to dig deeper into this.
} | ||
|
||
[Test] | ||
public void First_added_handler_has_precedence() |
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 it's better to be consistent with the existing API e.g. Returns - last specification overrides previous. @dtchepak WDYT?
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.
It might be not very intuitive if we reverse order. That is not a regular "Returns", rather that is a pipeline member. I'd expect that registered handlers are executed one by one, rather than in opposite order.
But if you feel that it might be confusing - NP, I'll change that.
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.
Hmm, I don't have strong opinion here. If we treat it as a pipeline then yes, "first handler wins" sounds reasonable.
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 are right. If we're dealing with handlers then it's better to traverse from first handler to last added one.
})); | ||
|
||
//act | ||
source.MethodWithArgs(Arg.Any<string>(), Arg.Is("42")); |
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.
Missed "specification" part ie Returns("")
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.
Yep! Added.
ConcurrentQueue<ResultForCallSpec> _results; | ||
|
||
public CallResults(ICallInfoFactory callInfoFactory) | ||
public CallResults(ICallInfoFactory callInfoFactory, bool skipVoidMethodsForPerformance) |
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 sure that we need skipVoidMethodsForPerformance
. How will it be used?
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 added it to make this code reusable from AutoFixture side. I'll need my own cache for already resolved results, so CallResults
(more precisely, CallResultsCache
wrapper) is ideal for that. However, AutoFixture sets results for the ref
and out
parameters, while method might be void
. CallResults
will just ignore such methods :(
I've decided to not change this behavior by default, because that could lead to performance degradation.
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 would remove this "hack" and implement that logic on AutoFixture side. Is it possible?
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.
Of course, I can do that, just don't like idea to copy-paste the whole class to change a single place. If later you fix some bug in this class, AF will not be updated :(
What if I rename ReturnsVoidFrom method to bool SkipVoidMethod()
and make it protected virtual
? Will you accept such idea? 😊
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.
Changed API. See in new commits. Do you still have concerns about how it looks?
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.
(Not related to this one but) In any case, code duplication is much better than the leaky conventional abstraction that just few people know about 😉
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 still have some confusion regarding the "by design" CallResults
usage. I see your point (it's for returning value only), however I'd consider the following sample as fully valid usage:
mock.MethodWithOut(out spec).Returns(c =>
{
c[0] = "outParamValue";
return true;
}
Therefore, for me it looks that usage of CallResults
(internally) for setting ref
and out
parameters if you have return value is correct, while if you don't have return value - it's wrong. I'd say such agreement is a bit weird.
In any case, I appreciate your time, your comment and I understand that you are a custodian of this project and it's your duty to keep the code clear. Therefore, I've merged your PR. After all, that is very tiny class, so it should be easy to replicate it without fear :)
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.
Yes, that's perfectly valid usage and Returns
API allows it. But you cannot call it for void method, only WhenDo
and CallActions
work for them.
We will come up to the best solution I'm sure 😉
PS I want to take a part in the NSub-AutoFixture development, 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.
But you cannot call it for void method, only WhenDo and CallActions work for them.
Yep, I noticed that. For me it looked like just an API limitation, but it seems to be a part of god's plan :) Deeply inside I still don't agree with it and for me there is no difference (out
is essentially the same return
, introduced to pass more than one value), but I understand your point and accept it 😯.
I'll mention you and will ask for your review when I implement integration. Let me know if I misunderstood you. Hope, that is not because you noticed that I'm going to make some horrible decisions (though, likely, that is an exact reason given all the discussion above) 😞 😉
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.
Deeply inside I still don't agree with it and for me there is no difference (out is essentially the same return
I agree with you on that. Yes, out
is a "return". NSub's API respects that. But it's internals were designed in a different way. And I really don't want to leak that exception tiny via protected method to a 3rd party library.
I'll mention you and will ask for your review when I implement integration.
Yes please. That's not about horrible decisions 😆
private readonly List<ICallHandler> _handlers = new List<ICallHandler>(); | ||
|
||
public IEnumerable<ICallHandler> Handlers => _handlers; | ||
public IDictionary<object, object> HandlerDataStorage { get; } = new ConcurrentDictionary<object, object>(); |
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 it's better to stick to C# 5.0 and initialize the property from the constructor.
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.
Is that a strong requirement for the whole project? I noticed that both your CI handled that well. That is a compiler sugar, so produced binary is compatible with .NET 3.5.
I'll gladly change that (and a few other places where I used get-only properties). However, I am just curious about reasons to not use C# 6 features.
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.
No, it's not a strong requirement, it's just my preference to stick to one style. I think I just missed the point when we all switched to C# 6 😊 I call @dtchepak here
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 don't have strong feelings about this either way. Consistency is good, progress is good. ¯_(ツ)_/¯ :)
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.
@alexandrnikitin So, it's up to you for now. I'm fine with C# 6 features :)
@@ -0,0 +1,9 @@ | |||
namespace NSubstitute.Core | |||
{ | |||
public interface ICallResultsCache |
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.
What's the reason for introducing ICallResultsCache
? It's the same as ICallResults
but without Clear()
and it just wraps the implementation of CallResults
.
Ah I see:
AutoValue feature uses its own cache for known results, rather than register its result globally. Current behavior leads to "increased priority" for auto-values, if setup is done after the first invocation. See this test for more details. Also, see this discussion.
There's enum value AutoValueBehaviour.UseValueForSubsequentCalls
that we explicitly use when we specify call for some reason 😕
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.
Yes, that is just a tiny wrapper on top of ICallResults
and ICallSpecificationFactory
. Rather than have 2 dependencies each time, I decided to unite them to logical unit. It could happen that such kind of objects is required in other places as well (e.g. in ReturnFromAndConfigureDynamicCall
), so passing 2 dependencies will pollute consumers.
Do you want me to remove it and use both ICallResults
and ICallSpecificationFactory
separately?
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 it's better to remove it as that wrapper and its purpose isn't obvious. What do you think?
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, I see it's purpose - to be a standalone registry to track "known" call results. It has both Add
and Get
API. To be fair, it isn't just a pure wrapper - it has some logic which combines two APIs.
But given that we can easily substitute ICallResultsCache
by (ICallResults
, ICallSpecificationFactory
) pair - I wouldn't persist too much and will agree to remove it if you feel it's better for further support and such instances just add overhead.
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 created a PR to address the ICallResultsCache. Please take a look. https://github.com/Zvirja/NSubstitute/pull/1/files
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.
Merged. Actually yes, you were right, wrapper it too tiny so we could live without it :)
public interface ICustomHandlers | ||
{ | ||
IEnumerable<ICallHandler> Handlers { get; } | ||
IDictionary<object, object> HandlerDataStorage { get; } |
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.
What's the purpose behind HandlerDataStorage
? Do you think it's needed?
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.
Current implementation is that each CallHandler
is stateless and substitute state is present in the state
object. Handlers are recreated each time the route is composed.
My thoughts were to have place in state
where each call handler could store it's permanent data per substitute (e.g. cached results). Later it could read it from storage when call is dispatched. Alternatively. each handler will need to track it's own dictionary to map substitute to state. That is much harder (and costs in term of memory/performance) than to have a place inside state
where it could put its data.
Later I changed implementation and now single instance of custom handler is created per substitute. However, that is rather a "weak agreement" that we will not create handler more than once per substitute.
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.
It looks premature to me. Will it be used in AutoFixture? If no then I think it's better to remove it.
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, yes, AutoFixture will store its "known" results there - each subsequent call to same method should return the same result. That results are per-substitute, so it's an ideal place to store them.
Do you see any other place where AF can store cached results for substitute calls? I don't see, unfortunately :( Keeping a custom map (subsitute object
to AF state
) looks a horrible idea.
Another way is to guarantee that we always create a single instance of CallHandler
per substitute. But is that OK to make such guarantee a part of CallHandler
design? That would be the only ICallHandler
we make such guarantee for..
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 I'd prefer to have each custom call handler manage its own state if that's feasible.
AutoFixture details aside, I was imagining we could do something like sub.GetCallRouter().RegisterHandler(new MyHandler(myState))
, so a handler would automatically be associated with a single substitute. Don't know if this is practical, but was my initial line of thinking about this.
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.
@dtchepak Many thanks for your example. That is was I was missing. Now it looks obvious, but when I designed API - I didn't see that 😕
Indeed, we could use closure in factory method to associate context with single substitute:
var afState = new AFState();
sub.GetCallRouter().RegisterHandler(subState => new AFHandler(subState, afState));
@alexandrnikitin I'd remove this storage a bit later, it isn't required now.
@@ -15,7 +15,7 @@ public ICallSpecification CreateFrom(ICall call, MatchArgs matchArgs) | |||
{ | |||
var methodInfo = call.GetMethodInfo(); | |||
var argumentSpecs = call.GetArgumentSpecifications(); | |||
var arguments = call.GetArguments(); | |||
var arguments = call.GetOriginalArguments(); |
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.
This lines scares me. I have to think about consequences of that change 😟
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.
That's the necessary change, unfortunately. Otherwise, no way for user to configure results using the Returns()
extension if my handler fills the ref
arguments.
If you see any issues with that - let me know. So far no tests failed :)
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.
@dtchepak While Alexandr is checking that, do you see from your side any problems that could arise because of this change? The idea is that we always use original call arguments for specifications, rather than potentially modified args. So if user changes any argument value (e.g. in When/Do
handler), that would not affect call specification.
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.
Hi @zvirja, can you show an example test that will fail if this line stayed as call.GetArguments()
?
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.
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.
@zvirja This has always been a problem with out/ref in cases where we set the values from within a Returns
. Does your changes fix that problem overall, or just for the custom handlers? (I can check it myself, but thought you might have the info at hand :) )
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.
It changes behavior for all cases, including Returns
and When/Do
handlers as well - it doesn't matter much whether you modify args via ICall
directly or CallInfo
.
I've created a bug in AF in past which describes this case (currently AF works via Returns
mechanism).
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.
@alexandrnikitin @dtchepak After I investigated that more, I realized that already applied changes are not enough to fully cover this sub-feature. Thank you for pointing me to this direction. If we use original arguments for specification composition, we should also ensure that we take original arguments for matching the calls against the specification.
Therefore, I was forced to add this commit. Please see the added tests - all them fail without the introduced changes.
Sorry Alexandr, now you will need to think about even more set of changes 😉 But we definitely need them. Current behavior of NSubstitute is weak in that area.
P.S. I like how this PR evolves and becomes more mature and consistent.
Thank you very much for your time and feedback! I've put my replies and will continue to track each "local conversation" :) |
@alexandrnikitin @dtchepak Did you have any chance to review my answers? I'm a bit confused - you answered me very quickly, I replied and you stopped to reply.. Is everything fine? :) If you just don't have free time - fine, I'll wait. |
@zvirja I'm so sorry. It's just a matter of time (read as will/motivation) from my side. I added some comments. I still need time to wrap my head around |
@alexandrnikitin Many thanks for reply. Got it - will just wait in future till you feel enough desire to review all this mess 😉 |
@zvirja, thanks for this work and for your patience with us. Just for some context @alexandrnikitin has been out of .net for a while so it really is a wonderful thing he still makes time to help out NSubstitute at all 👍 😄 . Alexandr, feel free to merge this when you're happy with it, or if you run out of time to allocate to this let me know and I'll pick it up as soon as I can. (I'll run it over a few big test suites I have to reduce the chances of undetected breaking changes.) |
48e11d7
to
26994b8
Compare
I second David, Thank you for your work and patience, Alex. This would not have happened without you. Everything looks good to me. I'm ready to merge it. Alex, could you please squash it by yourself. (GitHub UI feature puts the latest author there I think, I want it to be you not me) I didn't want to sound rude when I wrote about time and motivation. One always have a choice whether it's a family or a couch :) |
More precise set of changes: - Ensure that specification evaluates original argument values, because they could be modified during call dispatch. - Ensure that original arguments are used to check whether current call meets particular specification. - Use own results cache for AutoValues route handler. That is needed to ensure that AutoValues priority isn't increased. Otherwise, once handler invoked for the first time, it's "memory" will have same priority as user configured results. As outcome, custom handlers, returns for type, etc will not be invoked.
f91205b
to
db8c2e0
Compare
@alexandrnikitin Squashed, thanks for that hint. Now there is a chance for me to get a "Contributor" badge for NSubstitute project (I believe GitHub looks at commit history) 😸 Thank you guys for your collaboration. I was really impressed by how many resources you are ready to invest to each individual PR despite that you are busy with other tasks. Such experience really inspires to fire another PRs if needed. Will be looking forward to @dtchepak checks and if there are some troubles - let's discuss that. |
Thanks so much for all this work @zvirja and @alexandrnikitin! 👍 👏 |
@dtchepak Do you have any approximate ETA's when this change will be released, so I can pull it from nuget and start working on feature in AutoFixture project? :) |
@zvirja you don't need to wait for the release. You can use NuGet local feeds. Just build the NSub package using |
I'm unable to build it locally. I've tried to do that on 2 different PCs, but each time FAKE fails with the same error:
Of course, git is installed and added to Do you know why that happens and how to overcome it? I've tried to google the solution, but no luck so far :( That's why I'm still looking forward to @dtchepak release :) |
@zvirja What if you run |
>__> That helps. That is something I had to try myself 😖 P.S. Not it started to work even after I reverted changes and triggered |
This PR creates extensibility point for external parties to specify custom default values. This is a merge of #156 and #234.
Changes overview:
Added custom handlers call action to
RecordReplay
route.I haven't created any extension method for this feature. That is because I assume this feature to be used rarely and extension method will pollute the completion menu. Moreover, given that very low-level API is provided, it doesn't seem to be a feature for "broad masses" :) Let me know if you have different opinion.
For now, API is following:
SubstitutionContext.Current.GetCallRouterFor(source).RegisterCustomCallHandler(factory)
.Introduced
OriginalArguments
to theICall
interface and use them for specifications. This way if call handler (or user inWhen/Do
statement) modify argument values (e.g. forref
parameters), that doesn't affect rules. Check the following tests: test1, test2.AutoValue feature uses its own cache for known results, rather than register its result globally. Current behavior leads to "increased priority" for auto-values, if setup is done after the first invocation. See this test for more details. Also, see this discussion.