-
Notifications
You must be signed in to change notification settings - Fork 255
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鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce proxy id to simplify identification #379
Conversation
UPDATE. I've changed ID to make it shorter. Now it looks like following:
It should allow to save space in the different error messages and looks nicer. |
@dtchepak Previously you asked to ping you explicitly if you don't reply for a while. Have you noticed this PR (and a few other ones nearby)? 馃槉 |
Arghh I didn't get an email/notification! Sorry! Thanks for pinging! |
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.
Looks good! Seems like a very neat solution to this. Have you done any quick measures on perf differences with this extra interception? Might be worth running a quick comparison and making sure it doesn't add much overhead on construction or interception.
I wonder if we could simplify this but still meet the immediate need by switching to Java-style Object.toString which just shoves the hashCode on the end? So for Substitute.For<IFoo>()
we'd get Substitute.IFoo@{hashCode}
(or hashcode in hex). This may help avoid people getting confused trying to assign meaning to the sequence number generated (as per the IFoo#1
/IBar#2
comment).
I was initially expecting this to be implemented via the CallRouter
mixed-in via ProxyGenerationOptions
. If this logic could go in CallRouter it might give us a good hook to support explicitly tagging substitutes later on? (like this example, where we might want to say sub1.Name("good"); sub2.Name("bad")
or similar.)
Sorry for the rambling comment, I just wanted to jot down some notes during my initial look at the PR. I'd appreciate your thoughts on any of this stuff.
var id = Interlocked.Increment(ref _idCounter); | ||
var shortTypeName = primaryProxyType.GetNonMangledTypeName(); | ||
|
||
return string.Format(CultureInfo.InvariantCulture, "Substitute.{0}#{1}", shortTypeName, id); |
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 wonder if this format is slightly misleading as it looks like the number refers to the specific type?
e.g. Substitute.For<IFoo>(), Substitute.For<IBar>
gives IFoo#1
and IBar#2
.
I don't think I can come up with anything better (Substitute#{1} [{0}]
?), but if you've got any ideas around this I'd love to hear them.
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 wonder if this format is slightly misleading as it looks like the number refers to the specific type?
Fixed by using the pipe - it brings much better semantics and it's clear that values are not related.
I don't think I can come up with anything better (Substitute#{1} [{0}]?)
My goal was to make a substitute id as concise as possible. Some test frameworks have limits on the number of characters they show in e.g. error messages. Therefore the shorter the ID is, the more information will fit into the screen/message. Additionally, it's better to not have spaces in the ID - we never know how they will be formatted).
Personally, I feel that Substitute.type#id
is the best tradeoff between readability and length.
I wonder if we could simplify this but still meet the immediate need by switching to Java-style Object.toString which just shoves the hashCode on the end? So for Substitute.For() we'd get Substitute.IFoo@{hashCode} (or hashcode in hex). This may help avoid people getting confused trying to assign meaning to the sequence number generated (as per the IFoo#1/IBar#2 comment).
Just realized that you are completely right. This counter might look strange to people, as it's global across all the tests and they might expect that counter is local to the proxy type. If we stick to the hash code, we'll likely always have large number, so it will be clear to user that numbers are non-predictable. I've changed the code to fix that.
Now about the format. I see a few options:
Substitute.IInterface#007877b1
- padded hexSubstitute.IInterface#13b9a
- non padded hexSubstitute.IInterface#59207139
- non-padded decimal
I suggest us to stick to hex, as hash code might be negative and will look ugly in decimal: Substitute.IInterface#-7894961
. Also the perfectionist inside me votes for padding 馃槄
As for the delimiter, I see the following options:
Substitute.IInterface#007877b1
Substitute.IInterface:007877b1
Substitute.IInterface|007877b1
Substitute.IInterface`007877b1
Substitute.IInterface@007877b1
Substitute.IInterface/007877b1
Substitute.IInterface\007877b1
Substitute.IInterface_007877b1
Substitute.IInterface-007877b1
Substitute.IInterface.007877b1
Substitute.IInterface$007877b1
Substitute.IInterface!007877b1
Substitute.IInterface%007877b1
Substitute.IInterface^007877b1
Substitute.IInterface*007877b1
Substitute.IInterface:#007877b1
Substitute.IInterface[007877b1]
Personally, I like the first 4 options, but decided to proceed with pipe as it clearly divide parts. Here how the message looks with pipes:
Expected: some item equal to <Substitute.IAdviseSink|03dc30f2>
But was: < <Substitute.IAdviseSink|03624f36>, <Substitute.IAdviseSink|03ac360b> >
Notice, we cannot use proxy.GetType().Name
to get ID, because it will always return ObjectProxy
no matter what we mock. It's less useful than the primary type name.
} | ||
} | ||
|
||
private void VerifyNoConstructorArgumentsGivenForInterface(object[] constructorArguments) | ||
private class ProxyIdGenerator |
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 sure it's worthwhile, but we could conceivably reuse SequenceNumberGenerator
(with a new ctor that lets the count start from 0
) here and inline the method so we lose the additional class. 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.
Removed the counter at all in favor of GetHashCode()
.
In order to simplify proxy identification we override the ToString() method to return an unique proxy id. We don't override the ToString() method if proxied type provides its own implementation.
@dtchepak See my replies in the comments as well.
I thought that, but later realized that it's a bad idea. If we decide to move call through the
I want
The first point might be achieved by adding the extra conditions inside the The feature with explicit naming will be still possible even with current architecture, as it's possible to pass Given that I prefer to have a separate interceptor as the best guarantee of isolation. I believe that overhead is negligible.
I'll follow up on that a bit later, when I have more free time. |
Thanks @zvirja, yet another awesome PR :) |
@dtchepak Here are the performance testing results: Code public class NSubstituteBenchmark
{
private ClassWithIntMethod _classProxy;
private IInterfaceWithIntMethod _interfaceProxy;
public NSubstituteBenchmark()
{
this._classProxy = Substitute.For<ClassWithIntMethod>();
this._interfaceProxy = Substitute.For<IInterfaceWithIntMethod>();
}
[Benchmark]
public ClassWithIntMethod CreateClassProxy() => Substitute.For<ClassWithIntMethod>();
[Benchmark]
public IInterfaceWithIntMethod CreateInterfaceProxy() => Substitute.For<IInterfaceWithIntMethod>();
[Benchmark]
public string InvokeToStringInterfaceProxy() => _interfaceProxy.ToString();
[Benchmark]
public string InvokeToStringClassProxy() => _classProxy.ToString();
[Benchmark]
public int InvokeRegularMethodInterfaceProxy() => _interfaceProxy.GetInt();
[Benchmark]
public int InvokeRegularMethodClassProxy() => _classProxy.GetInt();
public interface IInterfaceWithIntMethod
{
int GetInt();
}
public abstract class ClassWithIntMethod
{
public abstract int GetInt();
}
} Environment:
Before:
After:
As you can see, there is almost no difference in performance and overhead from extra interceptor is negligible. The only sensible difference is in the Also you can see how quick the |
Closes #44
In this PR I've implemented the proxy id feature. which allows to recognize the particular instance of the proxy. The ID looks like following:
Where
PrimaryProxyType
- the short type name of the primary proxy type,#42
- auto-incremented field (global counter, not per type).To achieve the desired behavior I've changed the following:
System.Object
. That is required to intercept theToString()
method, otherwise Castle doesn't hook it.ProxyIdInterceptor
to return proxy id whenToString()
is called. Given that our interceptor participates earlier than forwarding one, calls to theToString()
are transparent for the NSubstutite core.ToString()
if type has its own implementation.I've tested the code and it works fine in VS debugger - proxy ID is shown for the debugger view:
Due to some reasons Rider doesn't want to evaluate
ToString()
, however we can still see the proxy ID:Hope one day they will improve their evaluator and it will invoke the complex
ToString()
method as well.Of course, everything works fine with assertions, as they invoke
ToString()
method:and error:
I don't like a bit that now we are always forced to create proxy for the class. However, that should not affect the performance too much and the benefit seems to worth it.
@alexandrnikitin @dtchepak Please take a look and let me know your opinion. Thanks 馃尮