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

Reimplement SetupSequence to make it thread-safe #476

Merged
merged 5 commits into from
Oct 21, 2017

Conversation

stakx
Copy link
Contributor

@stakx stakx commented Oct 6, 2017

This is an early draft and shouldn't be merged just yet.

It does appear to resolve #467, however since it's a complete reimplementation of SetupSequence which works in a completely different way than the previous implementation, it needs much more thorough testing of edge cases, as well as a code cleanup.

This resolves #467.

@stakx stakx self-assigned this Oct 6, 2017
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.

This is a draft and needs more work.

{
// keeping the fluent API separate from `SequenceMethodCall` saves us from having to
// define a generic variant `SequenceMethodCallReturn<TResult>`, which would be much more
// work that having a generic fluent API counterpart `SequenceMethodCall<TResult>`.
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 demonstrates a pattern that should probably be applied more in Moq. Moq's "plumbing" classes (e.g. MethodCall, MethodCallReturn<TResult>, Mock) should be made largely non-generic and kept strictly separate from the generic classes that are part of the fluent API. These latter types only flow the context of a fluent API "phrase" (i.e. a chain of method calls starting with mock.Setup… and ending with the end-of-line semicolon). That way, we could probably get rid of some code duplication.


public ISetupSequentialAction Throws<TException>()
where TException : Exception, new()
=> this.Throws(new TException());
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Perhaps delayed instantiation should be preferred over newing up exception objects way in advance?

Source/Mock.cs Outdated
@@ -768,7 +768,7 @@ private static Expression GetPropertyExpression(Type mockType, PropertyInfo prop
/// Gets the interceptor target for the given expression and root mock,
/// building the intermediate hierarchy of mock objects if necessary.
/// </summary>
private static Interceptor GetInterceptor(Expression fluentExpression, Mock mock)
internal static Interceptor GetInterceptor(Expression fluentExpression, Mock mock)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Making this and the following helper methods internal was a quick hack to see if the reimplementation was feasible at all. This needs to be cleaned up. Do these methods actually have to be located in the Mock class?

using Moq.Language;
using Moq.Language.Flow;
using System.Linq;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

(Detail:) Sort usings properly.

@@ -0,0 +1,65 @@
using System;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

(Detail:) Add missing license text to all new files!

{
// we get here if there are more invocations than configured responses.
// if the setup method does not have a return value, we don't need to do anything;
// if it does have a return value, we produce the default value.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Verify (via additional unit tests) that the new implementation acts the same way as the old implementation when the configured responses have been exhausted by too many invocations.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The new implementation acts differently; see explanation in #467 (comment). I personally favour the new behaviour as it brings SetupSequence more in line with how other Setup methods work: unless a setup is a conditional setup (.When(...)), then it overrides any previous setup for the same invocation.

}
else if (call.Method.ReturnType.GetTypeInfo().IsValueType)
{
call.ReturnValue = Activator.CreateInstance(call.Method.ReturnType);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

AFAIK other parts of Moq do this in a more complicated fashion, because of nullable value types, COM types, etc. Check whether this is sufficient.

}

[Fact]
public async Task Test()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Give this test a more sensible name.

for (int i = 1; i <= length; i++)
{
seqSetup = seqSetup
.Returns(i);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unnecessary line break, reformat.

var orderedInvocations = invocationsQueue.OrderBy(x => x).ToArray();

// The assertion prints the real invocations, you will see duplicates there!
Assert.Equal(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@icalvo's original repro code used MSTest's CollectionAssert.AreEqual. Make sure that this is indeed xUnit.NET's way of comparing two sequences.

@stakx stakx force-pushed the setupsequence-thread-safety branch 2 times, most recently from d43a177 to 5646a64 Compare October 9, 2017 18:28
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.

Minor points from previous issue have been addressed. Need to add additional unit tests now that try to detect changes in behaviour.

/// </summary>
[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "By Design")]
public ISetupSequentialResult<TResult> SetupSequence<TResult>(
Expression<Func<T, TResult>> expression)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unnecessary line break, remove.

Source/Mock.cs Outdated
@@ -667,6 +669,44 @@ private static Expression GetValueExpression(object value, Type type)
return Expression.Convert(Expression.Constant(value), type);
}

[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "By Design")]
internal static SetupSequencePhrase<TResult> SetupSequence<T, TResult>(Mock<T> mock, Expression<Func<T, TResult>> expression)
where T : class
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Does this method really have to be generic? Why not make the parameters be of types Mock and LambdaExpression? The public method in Mock<T> can make sure that everything is appropriately typed.

Source/Mock.cs Outdated
}

internal static SetupSequencePhrase SetupSequence<T>(Mock<T> mock, Expression<Action<T>> expression)
where T : class
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same here: Does this method really have to be generic? Why not make the parameters be of types Mock and LambdaExpression? The public method in Mock<T> can make sure that everything is appropriately typed.

public static ISetupSequentialResult<TResult> SetupSequence<TMock, TResult>(
this Mock<TMock> mock,
Expression<Func<TMock, TResult>> expression)
where TMock : class
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TMock should be just T, but changing a generic type parameter's name is possibly a (minor) breaking change. Leave as is for now.

[Obsolete("Please use instance method Mock<T>.SetupSequence instead.")]
[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "By Design")]
public static ISetupSequentialAction SetupSequence<TMock>(this Mock<TMock> mock, Expression<Action<TMock>> expression)
where TMock : class
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same here: TMock should be just T, but changing a generic type parameter's name is possibly a (minor) breaking change. Leave as is for now.

@stakx stakx force-pushed the setupsequence-thread-safety branch 5 times, most recently from 5b366a9 to 3e5564e Compare October 19, 2017 17:03
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.

All points have been addressed, this is good to go, except that we might want to add new verbs for SetupSequence such that the default response for an exhausted sequence can be defined.


public override void Execute(ICallContext call)
{
base.Execute(call);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. Added a unit test that ensures Verify can correctly check a sequence's invocation count.

{
// we get here if there are more invocations than configured responses.
// if the setup method does not have a return value, we don't need to do anything;
// if it does have a return value, we produce the default value.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The new implementation acts differently; see explanation in #467 (comment). I personally favour the new behaviour as it brings SetupSequence more in line with how other Setup methods work: unless a setup is a conditional setup (.When(...)), then it overrides any previous setup for the same invocation.

@stakx stakx changed the title Reimplement the whole machinery around SetupSequence to make it thread-safe (Draft) Reimplement SetupSequence to make it thread-safe Oct 20, 2017
@stakx stakx force-pushed the setupsequence-thread-safety branch from 3e5564e to 6bedd9f Compare October 21, 2017 15:11
@stakx stakx force-pushed the setupsequence-thread-safety branch from 6bedd9f to 9139dfc Compare October 21, 2017 19:21
@stakx stakx force-pushed the setupsequence-thread-safety branch from 9139dfc to c87cdcf Compare October 21, 2017 19:29
@stakx stakx merged commit 50ba5bf into devlooped:develop Oct 21, 2017
@stakx stakx deleted the setupsequence-thread-safety branch October 21, 2017 19:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant