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

Remove Interceptor.calls, ExpressionKey, and SetupKind #509

Merged
merged 5 commits into from
Oct 29, 2017

Conversation

stakx
Copy link
Contributor

@stakx stakx commented Oct 29, 2017

All of these were used so far to detect when one setup overrides an earlier, equivalent setup. This PR proposes their removal.

  1. Instead of performing the checks against Interceptor.calls, they can run directly against InterceptorContext.OrderedCalls (of which the former is a partial view).

  2. Instead of comparing setups via their string representation (built for ExpressionKey), they can be compared by directly comparing their expression trees using the already existing ExpressionComparer.

  3. SetupKind becomes redundant after (2).

The `calls` dictionary in `Interceptor` was used, up until now, to de-
tect equivalent unconditional setups such that for any two such set-
ups, the one added later would replace the former.

Unfortunately, maintaining `calls` comes at a cost:

 * There are two data structures keeping track of registered setups:
   `calls` (a partial view), and `InterceptorContext.OrderedCalls`
   (the full view). It's often difficult to remember which one is used
   for what purpose(s).

 * For every setup being registered, the setup expression is converted
   into a lookup key. This involves formatting the expression tree to
   a string representaion, which is fairly costly.

It turns out that if we just use `InterceptorContext.OrderedCalls` in-
stead of `calls`, and if we always insert new setups at the *front* of
that collection, then it doesn't matter much if it contains duplicate
setups; when finding a matching setup for an invocation, the setup
first encountered will win over those at a later (earlier) position.

This means that the invocation->setup matching operation becomes more
efficient, too.

One thing however becomes less efficient: mass verification triggered
via `mock.Verify[All]`. While we no longer care about duplicate setups
during setup, we still need to ensure that verification does not in-
clude duplicate setups, so the lookup key generation is still needed
here.
`ExpressionKey` was used, up until now, as a representation for an
expression tree so that those could be compared quickly for equality.
Such comparisons are needed to detect duplicate / overridden setups.

Turns out that just comparing expression trees directly, using
`ExpressionComparer` (which is already in Moq's codebase) instead of
first turning them into strings, is actually approx. 1.33x faster.

This commit therefore removes `ExpressionKey` in favor of `Expression-
Comparer`.
Copy link
Contributor Author

@stakx stakx left a comment

Choose a reason for hiding this comment

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

No more changes required, this is good to go.


namespace Moq.Matchers
namespace Moq
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We no longer use ExpressionComparer just together with matchers, so it's moved into the main namespace.

@@ -142,7 +138,7 @@ internal IEnumerable<IProxyCall> OrderedCalls
{
lock (orderedCalls)
{
return orderedCalls.ToList();
return orderedCalls.ToArray();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Micro-optimization. Since callers are only going to read the returned collection, choose a type with as little overhead as possible.

{
throw new MockVerificationException(failures.ToArray());
continue;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Conditional setups were already excluded from Mock.Verify[All] prior to this PR, but that wasn't as easy to see as it is now (because previously, conditional setups only got added to OrderedCalls, but not to calls, and mass verification happened against the latter).

{
verifiedSetupsForMethod = new List<Expression>();
verifiedSetupsPerMethod.Add(setup.Method, verifiedSetupsForMethod);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's another occurrence of this code pattern somewhere else in Moq's codebase, perhaps consider creating some kind of MultiMap / MultiDictionary ADT so this can be abstracted away.

@@ -69,22 +69,26 @@ public InterceptionAction HandleIntercept(ICallContext invocation, InterceptorCo
return InterceptionAction.Continue;
}

IProxyCall lastMatchingSetup = null;
IProxyCall lastMatchingSetupForSameMethod = null;
IProxyCall matchedSetup = null;
foreach (var setup in ctx.OrderedCalls)
{
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since InterceptorContext.OrderedCalls, now being a Stack<IProxyCall> instead of a List<IProxyCall>, is now in reverse order (last setup added gets enumerated first), we no longer search for the last matching setup, but for the first. This makes it possible to break out of the loop earlier.

@@ -406,6 +406,7 @@ private static bool AreSameMethod(Expression left, Expression right)
private static string FormatSetupsInfo(IEnumerable<IProxyCall> setups)
{
var expressionSetups = setups
.Reverse()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is needed for continuity (so setups are shown in exception messages in the order in which they were registered). Minor performance penalty here, but since it only happens in the error case, that's OK.


public MockVerificationException(IProxyCall[] failedSetups)
public MockVerificationException(IEnumerable<IProxyCall> failedSetups)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This change is to avoid an unnecessary call to .ToArray() in the calling code.

@stakx stakx merged commit 0efcefa into devlooped:develop Oct 29, 2017
@stakx stakx deleted the expressionkey-removal branch October 29, 2017 22:21
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.

None yet

1 participant