From 1ca6039a1fe934fb4db0080c8fe5bb3c38cc1638 Mon Sep 17 00:00:00 2001 From: stakx Date: Tue, 29 Dec 2020 22:31:06 +0100 Subject: [PATCH 01/21] Define `Await` operator & show its usage in test --- src/Moq/AwaitOperator.cs | 26 ++++++++++++++++++++ tests/Moq.Tests/AwaitOperatorFixture.cs | 32 +++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 src/Moq/AwaitOperator.cs create mode 100644 tests/Moq.Tests/AwaitOperatorFixture.cs diff --git a/src/Moq/AwaitOperator.cs b/src/Moq/AwaitOperator.cs new file mode 100644 index 000000000..0a7e1a66c --- /dev/null +++ b/src/Moq/AwaitOperator.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors. +// All rights reserved. Licensed under the BSD 3-Clause License; see License.txt. + +using System.Threading.Tasks; + +namespace Moq +{ + /// + /// Provides the Await group of methods that can serve as a substitute + /// for the keyword in setup expressions. Make Await + /// available in your code by importing this type's static members: + /// + /// using static Moq.AwaitOperator; + /// + /// + public static class AwaitOperator + { + /// + /// Continues to set up what should happen when the given task completes. + /// + public static TResult Await(Task task) + { + return default(TResult); + } + } +} diff --git a/tests/Moq.Tests/AwaitOperatorFixture.cs b/tests/Moq.Tests/AwaitOperatorFixture.cs new file mode 100644 index 000000000..832c34705 --- /dev/null +++ b/tests/Moq.Tests/AwaitOperatorFixture.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors. +// All rights reserved. Licensed under the BSD 3-Clause License; see License.txt. + +using System.Threading.Tasks; + +using Xunit; + +using static Moq.AwaitOperator; + +namespace Moq.Tests +{ + public class AwaitOperatorFixture + { + [Fact] + public async Task Returns__on_awaited_Task() + { + var expectedName = "Alice"; + + var mock = new Mock(); + mock.Setup(m => Await(m.GetNameTaskAsync())).Returns(expectedName); + + var actualName = await mock.Object.GetNameTaskAsync(); + + Assert.Equal(expectedName, actualName); + } + + public interface IPerson + { + Task GetNameTaskAsync(); + } + } +} From 079236958374fe230c3cb41d5135e4318975d854 Mon Sep 17 00:00:00 2001 From: stakx Date: Tue, 29 Dec 2020 23:00:58 +0100 Subject: [PATCH 02/21] `IAwaitableHandler`s wrap & unwrap awaitables --- src/Moq/Async/AwaitableHandler.cs | 28 ++++++++ src/Moq/Async/IAwaitableHandler.cs | 18 +++++ src/Moq/Async/TaskOfHandler.cs | 61 ++++++++++++++++ .../Async/AwaitableHandlerFixture.cs | 28 ++++++++ tests/Moq.Tests/Async/TaskOfHandlerFixture.cs | 70 +++++++++++++++++++ 5 files changed, 205 insertions(+) create mode 100644 src/Moq/Async/AwaitableHandler.cs create mode 100644 src/Moq/Async/IAwaitableHandler.cs create mode 100644 src/Moq/Async/TaskOfHandler.cs create mode 100644 tests/Moq.Tests/Async/AwaitableHandlerFixture.cs create mode 100644 tests/Moq.Tests/Async/TaskOfHandlerFixture.cs diff --git a/src/Moq/Async/AwaitableHandler.cs b/src/Moq/Async/AwaitableHandler.cs new file mode 100644 index 000000000..317033be5 --- /dev/null +++ b/src/Moq/Async/AwaitableHandler.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors. +// All rights reserved. Licensed under the BSD 3-Clause License; see License.txt. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Moq.Async +{ + internal static class AwaitableHandler + { + private static readonly Dictionary> factories; + + static AwaitableHandler() + { + AwaitableHandler.factories = new Dictionary>() + { + [typeof(Task<>)] = type => new TaskOfHandler(type.GetGenericArguments()[0]), + }; + } + + public static IAwaitableHandler TryGet(Type type) + { + var typeDefinition = type.IsConstructedGenericType ? type.GetGenericTypeDefinition() : type; + return AwaitableHandler.factories.TryGetValue(typeDefinition, out var factory) ? factory.Invoke(type) : null; + } + } +} diff --git a/src/Moq/Async/IAwaitableHandler.cs b/src/Moq/Async/IAwaitableHandler.cs new file mode 100644 index 000000000..5360c4552 --- /dev/null +++ b/src/Moq/Async/IAwaitableHandler.cs @@ -0,0 +1,18 @@ +// Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors. +// All rights reserved. Licensed under the BSD 3-Clause License; see License.txt. + +using System; + +namespace Moq.Async +{ + internal interface IAwaitableHandler + { + Type ResultType { get; } + + object CreateCompleted(object result); + + object CreateFaulted(Exception exception); + + bool TryGetResult(object awaitable, out object result); + } +} diff --git a/src/Moq/Async/TaskOfHandler.cs b/src/Moq/Async/TaskOfHandler.cs new file mode 100644 index 000000000..6e3182c27 --- /dev/null +++ b/src/Moq/Async/TaskOfHandler.cs @@ -0,0 +1,61 @@ +// Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors. +// All rights reserved. Licensed under the BSD 3-Clause License; see License.txt. + +using System; +using System.Threading.Tasks; + +namespace Moq.Async +{ + internal sealed class TaskOfHandler : IAwaitableHandler + { + private readonly Type resultType; + private readonly Type tcsType; + + public TaskOfHandler(Type resultType) + { + this.resultType = resultType; + this.tcsType = typeof(TaskCompletionSource<>).MakeGenericType(this.resultType); + } + + public Type ResultType => this.resultType; + + public object CreateCompleted(object result) + { + var tcs = Activator.CreateInstance(this.tcsType); + this.tcsType.GetMethod("SetResult").Invoke(tcs, new object[] { result }); + var task = this.tcsType.GetProperty("Task").GetValue(tcs); + return task; + } + + public object CreateFaulted(Exception exception) + { + var tcs = Activator.CreateInstance(this.tcsType); + this.tcsType.GetMethod("SetException", new Type[] { typeof(Exception) }).Invoke(tcs, new object[] { exception }); + var task = this.tcsType.GetProperty("Task").GetValue(tcs); + return task; + } + + public bool TryGetResult(object task, out object result) + { + if (task != null) + { + var type = task.GetType(); + var isCompleted = (bool)type.GetProperty("IsCompleted").GetValue(task); + if (isCompleted) + { + try + { + result = type.GetProperty("Result").GetValue(task); + return true; + } + catch + { + } + } + } + + result = null; + return false; + } + } +} diff --git a/tests/Moq.Tests/Async/AwaitableHandlerFixture.cs b/tests/Moq.Tests/Async/AwaitableHandlerFixture.cs new file mode 100644 index 000000000..bc826cd09 --- /dev/null +++ b/tests/Moq.Tests/Async/AwaitableHandlerFixture.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors. +// All rights reserved. Licensed under the BSD 3-Clause License; see License.txt. + +using System; +using System.Threading.Tasks; + +using Moq.Async; + +using Xunit; + +namespace Moq.Tests.Async +{ + public class AwaitableHandlerFixture + { + [Theory] + [InlineData(typeof(AttributeTargets))] + [InlineData(typeof(Func))] + [InlineData(typeof(IAsyncResult))] + [InlineData(typeof(int))] + [InlineData(typeof(object))] + public void TryGet__for_Task_of_X__returns_matching_TaskOfHandler(Type resultType) + { + var handler = AwaitableHandler.TryGet(typeof(Task<>).MakeGenericType(resultType)); + Assert.IsType(handler); + Assert.Equal(resultType, handler.ResultType); + } + } +} diff --git a/tests/Moq.Tests/Async/TaskOfHandlerFixture.cs b/tests/Moq.Tests/Async/TaskOfHandlerFixture.cs new file mode 100644 index 000000000..120d3316e --- /dev/null +++ b/tests/Moq.Tests/Async/TaskOfHandlerFixture.cs @@ -0,0 +1,70 @@ +// Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors. +// All rights reserved. Licensed under the BSD 3-Clause License; see License.txt. + +using System; +using System.Threading.Tasks; + +using Moq.Async; + +using Xunit; + +namespace Moq.Tests.Async +{ + public class TaskOfHandlerFixture + { + [Fact] + public async Task CreateCompleted__returns_completed_task__with_correct_result() + { + var expectedResult = 42; + + var handler = new TaskOfHandler(typeof(int)); + var task = (Task)handler.CreateCompleted(expectedResult); + + var actualResult = await task; + + Assert.Equal(expectedResult, actualResult); + } + + [Fact] + public async Task CreateFaulted__returns_faulted_task__that_throws_correct_exception() + { + var expectedException = new Exception(); + + var handler = new TaskOfHandler(typeof(int)); + var task = (Task)handler.CreateFaulted(expectedException); + + var actualException = await Assert.ThrowsAsync(async () => await task); + + Assert.Same(expectedException, actualException); + } + + [Fact] + public void TryGetResult__can_extract_result__from_completed_Task() + { + var expectedResult = 42; + + var handler = new TaskOfHandler(typeof(int)); + var task = Task.FromResult(expectedResult); + + Assert.True(handler.TryGetResult(task, out var actualResult)); + Assert.Equal(expectedResult, actualResult); + } + + [Fact] + public void TryGetResult__cannot_extract_result__from_faulted_Task() + { + var handler = new TaskOfHandler(typeof(int)); + var task = Task.FromException(new Exception()); + + Assert.False(handler.TryGetResult(task, out _)); + } + + [Fact] + public void TryGetResult__cannot_extract_result__from_null() + { + var handler = new TaskOfHandler(typeof(int)); + + Assert.False(handler.TryGetResult(null, out _)); + } + } +} From b3b010944d4b73a145bec28a4adf770db4adddf6 Mon Sep 17 00:00:00 2001 From: stakx Date: Wed, 30 Dec 2020 11:18:06 +0100 Subject: [PATCH 03/21] Awaited setups lift invocation results to awaitables --- src/Moq/ExpressionExtensions.cs | 21 +++++++++++++++++++++ src/Moq/Invocation.cs | 8 ++++++++ src/Moq/InvocationShape.cs | 2 ++ src/Moq/Setup.cs | 20 +++++++++++++++++++- 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/Moq/ExpressionExtensions.cs b/src/Moq/ExpressionExtensions.cs index 256f74cd4..5fa3fd046 100644 --- a/src/Moq/ExpressionExtensions.cs +++ b/src/Moq/ExpressionExtensions.cs @@ -10,6 +10,7 @@ using System.Reflection; using System.Text; +using Moq.Async; using Moq.Properties; using Moq.Protected; @@ -63,6 +64,21 @@ internal static TDelegate CompileUsingExpressionCompiler(this Express return ExpressionCompiler.Instance.Compile(expression); } + public static bool IsAwait(this MethodCallExpression expression, out IAwaitableHandler awaitableHandler) + { + if (expression.Method.DeclaringType == typeof(AwaitOperator)) + { + var awaitableType = expression.Method.GetParameters().Single().ParameterType; + awaitableHandler = AwaitableHandler.TryGet(awaitableType); + } + else + { + awaitableHandler = null; + } + + return awaitableHandler != null; + } + public static bool IsMatch(this Expression expression, out Match match) { if (expression is MatchExpression matchExpression) @@ -222,6 +238,11 @@ void Split(Expression e, out Expression r /* remainder */, out InvocationShape p method, arguments); } + else if (methodCallExpression.IsAwait(out var awaitableHandler)) + { + Split(methodCallExpression.Arguments.Single(), out r, out p); + p.AwaitableHandler = awaitableHandler; + } else { Debug.Assert(methodCallExpression.Method.IsExtensionMethod()); diff --git a/src/Moq/Invocation.cs b/src/Moq/Invocation.cs index 64b7cbc73..3127d2e62 100644 --- a/src/Moq/Invocation.cs +++ b/src/Moq/Invocation.cs @@ -7,6 +7,8 @@ using System.Reflection; using System.Text; +using Moq.Async; + namespace Moq { internal abstract class Invocation : IInvocation @@ -89,6 +91,12 @@ public Exception Exception } } + public void ConvertResultUsing(IAwaitableHandler awaitableHandler) + { + this.result = this.result is ExceptionResult r ? awaitableHandler.CreateFaulted(r.Exception) + : awaitableHandler.CreateCompleted(this.result); + } + public bool IsVerified => this.verified; /// diff --git a/src/Moq/InvocationShape.cs b/src/Moq/InvocationShape.cs index 11fdec7ee..5b932a2d3 100644 --- a/src/Moq/InvocationShape.cs +++ b/src/Moq/InvocationShape.cs @@ -8,6 +8,7 @@ using System.Linq.Expressions; using System.Reflection; +using Moq.Async; using Moq.Expressions.Visitors; using E = System.Linq.Expressions.Expression; @@ -63,6 +64,7 @@ public static InvocationShape CreateFrom(Invocation invocation) public readonly LambdaExpression Expression; public readonly MethodInfo Method; public readonly IReadOnlyList Arguments; + public IAwaitableHandler AwaitableHandler; private readonly IMatcher[] argumentMatchers; private MethodInfo methodImplementation; diff --git a/src/Moq/Setup.cs b/src/Moq/Setup.cs index c9911ab62..d4a66b928 100644 --- a/src/Moq/Setup.cs +++ b/src/Moq/Setup.cs @@ -63,7 +63,25 @@ public void Execute(Invocation invocation) this.Condition?.SetupEvaluatedSuccessfully(); this.expectation.SetupEvaluatedSuccessfully(invocation); - this.ExecuteCore(invocation); + if (this.expectation.AwaitableHandler == null) + { + this.ExecuteCore(invocation); + } + else + { + try + { + this.ExecuteCore(invocation); + } + catch (Exception ex) + { + invocation.Exception = ex; + } + finally + { + invocation.ConvertResultUsing(this.expectation.AwaitableHandler); + } + } } protected abstract void ExecuteCore(Invocation invocation); From a8082a431cd685d2a474e8caa2ae58febddfd2c3 Mon Sep 17 00:00:00 2001 From: stakx Date: Tue, 29 Dec 2020 23:53:44 +0100 Subject: [PATCH 04/21] Test `.Callback`, `.Throws`, and recursive setups --- tests/Moq.Tests/AwaitOperatorFixture.cs | 77 +++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/tests/Moq.Tests/AwaitOperatorFixture.cs b/tests/Moq.Tests/AwaitOperatorFixture.cs index 832c34705..dc94d3a88 100644 --- a/tests/Moq.Tests/AwaitOperatorFixture.cs +++ b/tests/Moq.Tests/AwaitOperatorFixture.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors. // All rights reserved. Licensed under the BSD 3-Clause License; see License.txt. +using System; using System.Threading.Tasks; using Xunit; @@ -11,6 +12,21 @@ namespace Moq.Tests { public class AwaitOperatorFixture { + [Fact] + public async Task Callback__on_awaited_Task() + { + var invoked = false; + + var mock = new Mock(); + mock.Setup(m => Await(m.GetNameTaskAsync())).Callback(() => invoked = true); + + Assert.False(invoked); + + await mock.Object.GetNameTaskAsync(); + + Assert.True(invoked); + } + [Fact] public async Task Returns__on_awaited_Task() { @@ -24,9 +40,70 @@ public async Task Returns__on_awaited_Task() Assert.Equal(expectedName, actualName); } + [Fact] + public async Task Throws__on_awaited_Task() + { + var expectedException = new Exception(); + + var mock = new Mock(); + mock.Setup(m => Await(m.GetNameTaskAsync())).Throws(expectedException); + + var task = mock.Object.GetNameTaskAsync(); + var actualException = await Assert.ThrowsAsync(async () => await task); + + Assert.Same(expectedException, actualException); + } + + [Fact] + public async Task Callback__on_property__of_awaited_Task() + { + var invoked = false; + + var mock = new Mock(); + mock.Setup(m => Await(m.GetFriendTaskAsync()).Name).Callback(() => invoked = true); + + var friend = await mock.Object.GetFriendTaskAsync(); + + Assert.False(invoked); + + _ = friend.Name; + + Assert.True(invoked); + } + + [Fact] + public async Task Returns__on_property__of_awaited_Task() + { + var expectedName = "Alice"; + + var mock = new Mock(); + mock.Setup(m => Await(m.GetFriendTaskAsync()).Name).Returns(expectedName); + + var friend = await mock.Object.GetFriendTaskAsync(); + var actualName = friend.Name; + + Assert.Equal(expectedName, actualName); + } + + [Fact] + public async Task Throws__on_property__of_awaited_Task() + { + var expectedException = new Exception(); + + var mock = new Mock(); + mock.Setup(m => Await(m.GetFriendTaskAsync()).Name).Throws(expectedException); + + var friend = await mock.Object.GetFriendTaskAsync(); + var actualException = Assert.Throws(() => friend.Name); + + Assert.Same(expectedException, actualException); + } + public interface IPerson { + string Name { get; } Task GetNameTaskAsync(); + Task GetFriendTaskAsync(); } } } From 40d013013b3f067331bff8a4c4dd248999c1086f Mon Sep 17 00:00:00 2001 From: stakx Date: Wed, 30 Dec 2020 12:12:42 +0100 Subject: [PATCH 05/21] Only lift result when necessary Some other parts of Moq (e.g. the `ReturnBase` behavior or default value providers) already produce awaitables that needn't be lifted. --- src/Moq/Invocation.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Moq/Invocation.cs b/src/Moq/Invocation.cs index 3127d2e62..26ee62132 100644 --- a/src/Moq/Invocation.cs +++ b/src/Moq/Invocation.cs @@ -93,8 +93,14 @@ public Exception Exception public void ConvertResultUsing(IAwaitableHandler awaitableHandler) { - this.result = this.result is ExceptionResult r ? awaitableHandler.CreateFaulted(r.Exception) - : awaitableHandler.CreateCompleted(this.result); + if (this.result is ExceptionResult r) + { + this.result = awaitableHandler.CreateFaulted(r.Exception); + } + else if (this.result == null || !this.method.ReturnType.IsAssignableFrom(this.result.GetType())) + { + this.result = awaitableHandler.CreateCompleted(this.result); + } } public bool IsVerified => this.verified; From 282520759b38a3a60bc466b568fc1938c7d4e841 Mon Sep 17 00:00:00 2001 From: stakx Date: Wed, 30 Dec 2020 00:02:13 +0100 Subject: [PATCH 06/21] Duplicate tests for `ValueTask<>` --- src/Moq/AwaitOperator.cs | 8 +++ tests/Moq.Tests/AwaitOperatorFixture.cs | 89 +++++++++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/src/Moq/AwaitOperator.cs b/src/Moq/AwaitOperator.cs index 0a7e1a66c..b1f13d79f 100644 --- a/src/Moq/AwaitOperator.cs +++ b/src/Moq/AwaitOperator.cs @@ -22,5 +22,13 @@ public static TResult Await(Task task) { return default(TResult); } + + /// + /// Continues to set up what should happen when the given task completes. + /// + public static TResult Await(ValueTask task) + { + return default(TResult); + } } } diff --git a/tests/Moq.Tests/AwaitOperatorFixture.cs b/tests/Moq.Tests/AwaitOperatorFixture.cs index dc94d3a88..5daa6599c 100644 --- a/tests/Moq.Tests/AwaitOperatorFixture.cs +++ b/tests/Moq.Tests/AwaitOperatorFixture.cs @@ -27,6 +27,21 @@ public async Task Callback__on_awaited_Task() Assert.True(invoked); } + [Fact] + public async Task Callback__on_awaited_ValueTask() + { + var invoked = false; + + var mock = new Mock(); + mock.Setup(m => Await(m.GetNameValueTaskAsync())).Callback(() => invoked = true); + + Assert.False(invoked); + + await mock.Object.GetNameValueTaskAsync(); + + Assert.True(invoked); + } + [Fact] public async Task Returns__on_awaited_Task() { @@ -40,6 +55,19 @@ public async Task Returns__on_awaited_Task() Assert.Equal(expectedName, actualName); } + [Fact] + public async Task Returns__on_awaited_ValueTask() + { + var expectedName = "Alice"; + + var mock = new Mock(); + mock.Setup(m => Await(m.GetNameValueTaskAsync())).Returns(expectedName); + + var actualName = await mock.Object.GetNameValueTaskAsync(); + + Assert.Equal(expectedName, actualName); + } + [Fact] public async Task Throws__on_awaited_Task() { @@ -54,6 +82,20 @@ public async Task Throws__on_awaited_Task() Assert.Same(expectedException, actualException); } + [Fact] + public async Task Throws__on_awaited_ValueTask() + { + var expectedException = new Exception(); + + var mock = new Mock(); + mock.Setup(m => Await(m.GetNameValueTaskAsync())).Throws(expectedException); + + var task = mock.Object.GetNameValueTaskAsync(); + var actualException = await Assert.ThrowsAsync(async () => await task); + + Assert.Same(expectedException, actualException); + } + [Fact] public async Task Callback__on_property__of_awaited_Task() { @@ -71,6 +113,23 @@ public async Task Callback__on_property__of_awaited_Task() Assert.True(invoked); } + [Fact] + public async Task Callback__on_property__of_awaited_ValueTask() + { + var invoked = false; + + var mock = new Mock(); + mock.Setup(m => Await(m.GetFriendValueTaskAsync()).Name).Callback(() => invoked = true); + + var friend = await mock.Object.GetFriendValueTaskAsync(); + + Assert.False(invoked); + + _ = friend.Name; + + Assert.True(invoked); + } + [Fact] public async Task Returns__on_property__of_awaited_Task() { @@ -85,6 +144,20 @@ public async Task Returns__on_property__of_awaited_Task() Assert.Equal(expectedName, actualName); } + [Fact] + public async Task Returns__on_property__of_awaited_ValueTask() + { + var expectedName = "Alice"; + + var mock = new Mock(); + mock.Setup(m => Await(m.GetFriendValueTaskAsync()).Name).Returns(expectedName); + + var friend = await mock.Object.GetFriendValueTaskAsync(); + var actualName = friend.Name; + + Assert.Equal(expectedName, actualName); + } + [Fact] public async Task Throws__on_property__of_awaited_Task() { @@ -99,11 +172,27 @@ public async Task Throws__on_property__of_awaited_Task() Assert.Same(expectedException, actualException); } + [Fact] + public async Task Throws__on_property__of_awaited_ValueTask() + { + var expectedException = new Exception(); + + var mock = new Mock(); + mock.Setup(m => Await(m.GetFriendValueTaskAsync()).Name).Throws(expectedException); + + var friend = await mock.Object.GetFriendValueTaskAsync(); + var actualException = Assert.Throws(() => friend.Name); + + Assert.Same(expectedException, actualException); + } + public interface IPerson { string Name { get; } Task GetNameTaskAsync(); + ValueTask GetNameValueTaskAsync(); Task GetFriendTaskAsync(); + ValueTask GetFriendValueTaskAsync(); } } } From b96023942d764febdc7306fa14da48ec4ca166a1 Mon Sep 17 00:00:00 2001 From: stakx Date: Wed, 30 Dec 2020 00:17:46 +0100 Subject: [PATCH 07/21] Add `IAwaitableHandler` for `ValueTask<>` --- src/Moq/Async/AwaitableHandler.cs | 1 + src/Moq/Async/ValueTaskOfHandler.cs | 70 +++++++++++++++++++ .../Async/AwaitableHandlerFixture.cs | 13 ++++ .../Async/ValueTaskOfHandlerFixture.cs | 70 +++++++++++++++++++ 4 files changed, 154 insertions(+) create mode 100644 src/Moq/Async/ValueTaskOfHandler.cs create mode 100644 tests/Moq.Tests/Async/ValueTaskOfHandlerFixture.cs diff --git a/src/Moq/Async/AwaitableHandler.cs b/src/Moq/Async/AwaitableHandler.cs index 317033be5..1c9e5b65d 100644 --- a/src/Moq/Async/AwaitableHandler.cs +++ b/src/Moq/Async/AwaitableHandler.cs @@ -16,6 +16,7 @@ static AwaitableHandler() AwaitableHandler.factories = new Dictionary>() { [typeof(Task<>)] = type => new TaskOfHandler(type.GetGenericArguments()[0]), + [typeof(ValueTask<>)] = type => new ValueTaskOfHandler(type, type.GetGenericArguments()[0]), }; } diff --git a/src/Moq/Async/ValueTaskOfHandler.cs b/src/Moq/Async/ValueTaskOfHandler.cs new file mode 100644 index 000000000..3ad4a97e8 --- /dev/null +++ b/src/Moq/Async/ValueTaskOfHandler.cs @@ -0,0 +1,70 @@ +// Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors. +// All rights reserved. Licensed under the BSD 3-Clause License; see License.txt. + +using System; +using System.Threading.Tasks; + +namespace Moq.Async +{ + internal sealed class ValueTaskOfHandler : IAwaitableHandler + { + private readonly Type resultType; + private readonly Type taskType; + private readonly Type tcsType; + + public ValueTaskOfHandler(Type taskType, Type resultType) + { + this.resultType = resultType; + this.taskType = taskType; + this.tcsType = typeof(TaskCompletionSource<>).MakeGenericType(this.resultType); + } + + public Type ResultType => this.resultType; + + public object CreateCompleted(object result) + { + // `Activator.CreateInstance` could throw an `AmbiguousMatchException` in this use case, + // so we're explicitly selecting and calling the constructor we want to use: + var ctor = this.taskType.GetConstructor(new[] { resultType }); + var valueTask = ctor.Invoke(new object[] { result }); + return valueTask; + + } + + public object CreateFaulted(Exception exception) + { + var tcs = Activator.CreateInstance(this.tcsType); + this.tcsType.GetMethod("SetException", new Type[] { typeof(Exception) }).Invoke(tcs, new object[] { exception }); + var task = this.tcsType.GetProperty("Task").GetValue(tcs); + + // `Activator.CreateInstance` could throw an `AmbiguousMatchException` in this use case, + // so we're explicitly selecting and calling the constructor we want to use: + var ctor = this.taskType.GetConstructor(new[] { task.GetType() }); + var valueTask = ctor.Invoke(new object[] { task }); + return valueTask; + } + + public bool TryGetResult(object valueTask, out object result) + { + if (valueTask != null) + { + var type = valueTask.GetType(); + var isCompleted = (bool)type.GetProperty("IsCompleted").GetValue(valueTask); + if (isCompleted) + { + try + { + result = type.GetProperty("Result").GetValue(valueTask); + return true; + } + catch + { + } + } + } + + result = null; + return false; + } + } +} diff --git a/tests/Moq.Tests/Async/AwaitableHandlerFixture.cs b/tests/Moq.Tests/Async/AwaitableHandlerFixture.cs index bc826cd09..cca23004a 100644 --- a/tests/Moq.Tests/Async/AwaitableHandlerFixture.cs +++ b/tests/Moq.Tests/Async/AwaitableHandlerFixture.cs @@ -24,5 +24,18 @@ public void TryGet__for_Task_of_X__returns_matching_TaskOfHandler(Type resultTyp Assert.IsType(handler); Assert.Equal(resultType, handler.ResultType); } + + [Theory] + [InlineData(typeof(AttributeTargets))] + [InlineData(typeof(Func))] + [InlineData(typeof(IAsyncResult))] + [InlineData(typeof(int))] + [InlineData(typeof(object))] + public void TryGet__for_ValueTask_of_X__returns_matching_ValueTaskOfHandler(Type resultType) + { + var handler = AwaitableHandler.TryGet(typeof(ValueTask<>).MakeGenericType(resultType)); + Assert.IsType(handler); + Assert.Equal(resultType, handler.ResultType); + } } } diff --git a/tests/Moq.Tests/Async/ValueTaskOfHandlerFixture.cs b/tests/Moq.Tests/Async/ValueTaskOfHandlerFixture.cs new file mode 100644 index 000000000..21f353598 --- /dev/null +++ b/tests/Moq.Tests/Async/ValueTaskOfHandlerFixture.cs @@ -0,0 +1,70 @@ +// Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors. +// All rights reserved. Licensed under the BSD 3-Clause License; see License.txt. + +using System; +using System.Threading.Tasks; + +using Moq.Async; + +using Xunit; + +namespace Moq.Tests.Async +{ + public class ValueTaskOfHandlerFixture + { + [Fact] + public async Task CreateCompleted__returns_completed_task__with_correct_result() + { + var expectedResult = 42; + + var handler = new ValueTaskOfHandler(typeof(ValueTask), typeof(int)); + var task = (ValueTask)handler.CreateCompleted(expectedResult); + + var actualResult = await task; + + Assert.Equal(expectedResult, actualResult); + } + + [Fact] + public async Task CreateFaulted__returns_faulted_task__that_throws_correct_exception() + { + var expectedException = new Exception(); + + var handler = new ValueTaskOfHandler(typeof(ValueTask), typeof(int)); + var task = (ValueTask)handler.CreateFaulted(expectedException); + + var actualException = await Assert.ThrowsAsync(async () => await task); + + Assert.Same(expectedException, actualException); + } + + [Fact] + public void TryGetResult__can_extract_result__from_completed_Task() + { + var expectedResult = 42; + + var handler = new ValueTaskOfHandler(typeof(ValueTask), typeof(int)); + var task = new ValueTask(expectedResult); + + Assert.True(handler.TryGetResult(task, out var actualResult)); + Assert.Equal(expectedResult, actualResult); + } + + [Fact] + public void TryGetResult__cannot_extract_result__from_faulted_Task() + { + var handler = new ValueTaskOfHandler(typeof(ValueTask), typeof(int)); + var task = new ValueTask(Task.FromException(new Exception())); + + Assert.False(handler.TryGetResult(task, out _)); + } + + [Fact] + public void TryGetResult__cannot_extract_result__from_null() + { + var handler = new ValueTaskOfHandler(typeof(ValueTask), typeof(int)); + + Assert.False(handler.TryGetResult(null, out _)); + } + } +} From cfb3f8fcaf38f5837730b652c01a4296c5c76150 Mon Sep 17 00:00:00 2001 From: stakx Date: Wed, 30 Dec 2020 00:26:29 +0100 Subject: [PATCH 08/21] Add tests for non-generic `Task` and `ValueTask` --- src/Moq/AwaitOperator.cs | 14 ++++++ tests/Moq.Tests/AwaitOperatorFixture.cs | 67 +++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/src/Moq/AwaitOperator.cs b/src/Moq/AwaitOperator.cs index b1f13d79f..decc0bba8 100644 --- a/src/Moq/AwaitOperator.cs +++ b/src/Moq/AwaitOperator.cs @@ -15,6 +15,20 @@ namespace Moq /// public static class AwaitOperator { + /// + /// Continues to set up what should happen when the given task completes. + /// + public static void Await(Task task) + { + } + + /// + /// Continues to set up what should happen when the given task completes. + /// + public static void Await(ValueTask task) + { + } + /// /// Continues to set up what should happen when the given task completes. /// diff --git a/tests/Moq.Tests/AwaitOperatorFixture.cs b/tests/Moq.Tests/AwaitOperatorFixture.cs index 5daa6599c..cdd80229a 100644 --- a/tests/Moq.Tests/AwaitOperatorFixture.cs +++ b/tests/Moq.Tests/AwaitOperatorFixture.cs @@ -12,6 +12,36 @@ namespace Moq.Tests { public class AwaitOperatorFixture { + [Fact] + public async Task Callback__on_awaited_non_generic_Task() + { + var invoked = false; + + var mock = new Mock(); + mock.Setup(m => Await(m.DoSomethingTaskAsync())).Callback(() => invoked = true); + + Assert.False(invoked); + + await mock.Object.DoSomethingTaskAsync(); + + Assert.True(invoked); + } + + [Fact] + public async Task Callback__on_awaited_non_generic_ValueTask() + { + var invoked = false; + + var mock = new Mock(); + mock.Setup(m => Await(m.DoSomethingValueTaskAsync())).Callback(() => invoked = true); + + Assert.False(invoked); + + await mock.Object.DoSomethingValueTaskAsync(); + + Assert.True(invoked); + } + [Fact] public async Task Callback__on_awaited_Task() { @@ -96,6 +126,40 @@ public async Task Throws__on_awaited_ValueTask() Assert.Same(expectedException, actualException); } + [Fact] + public async Task Callback__on_awaited_Task__of_property() + { + var invoked = false; + + var mock = new Mock(); + mock.Setup(m => Await(m.Friend.DoSomethingTaskAsync())).Callback(() => invoked = true); + + var friend = mock.Object.Friend; + + Assert.False(invoked); + + await friend.DoSomethingTaskAsync(); + + Assert.True(invoked); + } + + [Fact] + public async Task Callback__on_awaited_ValueTask__of_property() + { + var invoked = false; + + var mock = new Mock(); + mock.Setup(m => Await(m.Friend.DoSomethingValueTaskAsync())).Callback(() => invoked = true); + + var friend = mock.Object.Friend; + + Assert.False(invoked); + + await friend.DoSomethingValueTaskAsync(); + + Assert.True(invoked); + } + [Fact] public async Task Callback__on_property__of_awaited_Task() { @@ -188,11 +252,14 @@ public async Task Throws__on_property__of_awaited_ValueTask() public interface IPerson { + IPerson Friend { get; } string Name { get; } Task GetNameTaskAsync(); ValueTask GetNameValueTaskAsync(); Task GetFriendTaskAsync(); ValueTask GetFriendValueTaskAsync(); + Task DoSomethingTaskAsync(); + ValueTask DoSomethingValueTaskAsync(); } } } From 84acfdfec05f49c128492d316a9c4afd5ef22be2 Mon Sep 17 00:00:00 2001 From: stakx Date: Wed, 30 Dec 2020 01:46:18 +0100 Subject: [PATCH 09/21] Add `IAwaitableHandler` for `Task` and `ValueTask` --- src/Moq/Async/AwaitableHandler.cs | 2 ++ src/Moq/Async/TaskHandler.cs | 41 +++++++++++++++++++++++++++++++ src/Moq/Async/ValueTaskHandler.cs | 39 +++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 src/Moq/Async/TaskHandler.cs create mode 100644 src/Moq/Async/ValueTaskHandler.cs diff --git a/src/Moq/Async/AwaitableHandler.cs b/src/Moq/Async/AwaitableHandler.cs index 1c9e5b65d..fa5dd6825 100644 --- a/src/Moq/Async/AwaitableHandler.cs +++ b/src/Moq/Async/AwaitableHandler.cs @@ -15,7 +15,9 @@ static AwaitableHandler() { AwaitableHandler.factories = new Dictionary>() { + [typeof(Task)] = type => TaskHandler.Instance, [typeof(Task<>)] = type => new TaskOfHandler(type.GetGenericArguments()[0]), + [typeof(ValueTask)] = type => ValueTaskHandler.Instance, [typeof(ValueTask<>)] = type => new ValueTaskOfHandler(type, type.GetGenericArguments()[0]), }; } diff --git a/src/Moq/Async/TaskHandler.cs b/src/Moq/Async/TaskHandler.cs new file mode 100644 index 000000000..efded48eb --- /dev/null +++ b/src/Moq/Async/TaskHandler.cs @@ -0,0 +1,41 @@ +// Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors. +// All rights reserved. Licensed under the BSD 3-Clause License; see License.txt. + +using System; +using System.Threading.Tasks; + +namespace Moq.Async +{ + internal sealed class TaskHandler : IAwaitableHandler + { + public static readonly TaskHandler Instance = new TaskHandler(); + + private TaskHandler() + { + } + + Type IAwaitableHandler.ResultType => typeof(void); + + public object CreateCompleted() + { + var tcs = new TaskCompletionSource(); + tcs.SetResult(true); + return tcs.Task; + } + + object IAwaitableHandler.CreateCompleted(object _) => this.CreateCompleted(); + + public object CreateFaulted(Exception exception) + { + var tcs = new TaskCompletionSource(); + tcs.SetException(exception); + return tcs.Task; + } + + bool IAwaitableHandler.TryGetResult(object task, out object result) + { + result = null; + return false; + } + } +} diff --git a/src/Moq/Async/ValueTaskHandler.cs b/src/Moq/Async/ValueTaskHandler.cs new file mode 100644 index 000000000..ea2ee0123 --- /dev/null +++ b/src/Moq/Async/ValueTaskHandler.cs @@ -0,0 +1,39 @@ +// Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors. +// All rights reserved. Licensed under the BSD 3-Clause License; see License.txt. + +using System; +using System.Threading.Tasks; + +namespace Moq.Async +{ + internal sealed class ValueTaskHandler : IAwaitableHandler + { + public static readonly ValueTaskHandler Instance = new ValueTaskHandler(); + + private ValueTaskHandler() + { + } + + Type IAwaitableHandler.ResultType => typeof(void); + + public object CreateCompleted() + { + return new ValueTask(); + } + + object IAwaitableHandler.CreateCompleted(object _) => this.CreateCompleted(); + + public object CreateFaulted(Exception exception) + { + var tcs = new TaskCompletionSource(); + tcs.SetException(exception); + return new ValueTask(tcs.Task); + } + + bool IAwaitableHandler.TryGetResult(object task, out object result) + { + result = null; + return false; + } + } +} From 88c44383dbc147bed5a58c55f7b3c73fc41df666 Mon Sep 17 00:00:00 2001 From: stakx Date: Wed, 30 Dec 2020 01:41:28 +0100 Subject: [PATCH 10/21] Add tests for `SetupSequence`'s `Pass`, `Returns`, and `Throws` --- src/Moq/Language/Flow/SetupSequencePhrase.cs | 1 + tests/Moq.Tests/AwaitOperatorFixture.cs | 126 +++++++++++++++++++ 2 files changed, 127 insertions(+) diff --git a/src/Moq/Language/Flow/SetupSequencePhrase.cs b/src/Moq/Language/Flow/SetupSequencePhrase.cs index b4428e69b..d19744a5a 100644 --- a/src/Moq/Language/Flow/SetupSequencePhrase.cs +++ b/src/Moq/Language/Flow/SetupSequencePhrase.cs @@ -4,6 +4,7 @@ using System; using System.ComponentModel; +using Moq.Async; using Moq.Behaviors; namespace Moq.Language.Flow diff --git a/tests/Moq.Tests/AwaitOperatorFixture.cs b/tests/Moq.Tests/AwaitOperatorFixture.cs index cdd80229a..4217bbb20 100644 --- a/tests/Moq.Tests/AwaitOperatorFixture.cs +++ b/tests/Moq.Tests/AwaitOperatorFixture.cs @@ -250,6 +250,132 @@ public async Task Throws__on_property__of_awaited_ValueTask() Assert.Same(expectedException, actualException); } + [Fact] + public async Task SetupSequence_Pass__on_awaited_non_generic_Task() + { + var mock = new Mock(); + mock.SetupSequence(m => Await(m.DoSomethingTaskAsync())).Pass().Pass(); + + var firstTask = mock.Object.DoSomethingTaskAsync(); + await firstTask; + + var secondTask = mock.Object.DoSomethingTaskAsync(); + await secondTask; + + Assert.NotSame(firstTask, secondTask); + } + + [Fact] + public async Task SetupSequence_Pass__on_awaited_non_generic_ValueTask() + { + var mock = new Mock(); + mock.SetupSequence(m => Await(m.DoSomethingValueTaskAsync())).Pass().Pass(); + + var firstTask = mock.Object.DoSomethingValueTaskAsync(); + await firstTask; + + var secondTask = mock.Object.DoSomethingValueTaskAsync(); + await secondTask; + + Assert.NotSame(firstTask, secondTask); + } + + [Fact] + public async Task SetupSequence_Returns__on_awaited_Task() + { + var expectedFirstName = "Alice"; + var expectedSecondName = "Betty"; + + var mock = new Mock(); + mock.SetupSequence(m => Await(m.GetNameTaskAsync())).Returns(expectedFirstName).Returns(expectedSecondName); + + var actualFirstName = await mock.Object.GetNameTaskAsync(); + var actualSecondName = await mock.Object.GetNameTaskAsync(); + + Assert.Equal(expectedFirstName, actualFirstName); + Assert.Equal(expectedSecondName, actualSecondName); + } + + [Fact] + public async Task SetupSequence_Returns__on_awaited_ValueTask() + { + var expectedFirstName = "Alice"; + var expectedSecondName = "Betty"; + + var mock = new Mock(); + mock.SetupSequence(m => Await(m.GetNameValueTaskAsync())).Returns(expectedFirstName).Returns(expectedSecondName); + + var actualFirstName = await mock.Object.GetNameValueTaskAsync(); + var actualSecondName = await mock.Object.GetNameValueTaskAsync(); + + Assert.Equal(expectedFirstName, actualFirstName); + Assert.Equal(expectedSecondName, actualSecondName); + } + + [Fact] + public async Task SetupSequence_Throws__on_awaited_non_generic_Task() + { + var expectedFirstException = new Exception(); + var expectedSecondException = new Exception(); + + var mock = new Mock(); + mock.SetupSequence(m => Await(m.DoSomethingTaskAsync())).Throws(expectedFirstException).Throws(expectedSecondException); + + var actualFirstException = await Assert.ThrowsAsync(async () => await mock.Object.DoSomethingTaskAsync()); + var actualSecondException = await Assert.ThrowsAsync(async () => await mock.Object.DoSomethingTaskAsync()); + + Assert.Same(expectedFirstException, actualFirstException); + Assert.Same(expectedSecondException, actualSecondException); + } + + [Fact] + public async Task SetupSequence_Throws__on_awaited_non_generic_ValueTask() + { + var expectedFirstException = new Exception(); + var expectedSecondException = new Exception(); + + var mock = new Mock(); + mock.SetupSequence(m => Await(m.DoSomethingValueTaskAsync())).Throws(expectedFirstException).Throws(expectedSecondException); + + var actualFirstException = await Assert.ThrowsAsync(async () => await mock.Object.DoSomethingValueTaskAsync()); + var actualSecondException = await Assert.ThrowsAsync(async () => await mock.Object.DoSomethingValueTaskAsync()); + + Assert.Same(expectedFirstException, actualFirstException); + Assert.Same(expectedSecondException, actualSecondException); + } + + [Fact] + public async Task SetupSequence_Throws__on_awaited_Task() + { + var expectedFirstException = new Exception(); + var expectedSecondException = new Exception(); + + var mock = new Mock(); + mock.SetupSequence(m => Await(m.GetNameTaskAsync())).Throws(expectedFirstException).Throws(expectedSecondException); + + var actualFirstException = await Assert.ThrowsAsync(async () => await mock.Object.GetNameTaskAsync()); + var actualSecondException = await Assert.ThrowsAsync(async () => await mock.Object.GetNameTaskAsync()); + + Assert.Same(expectedFirstException, actualFirstException); + Assert.Same(expectedSecondException, actualSecondException); + } + + [Fact] + public async Task SetupSequence_Throws__on_awaited_ValueTask() + { + var expectedFirstException = new Exception(); + var expectedSecondException = new Exception(); + + var mock = new Mock(); + mock.SetupSequence(m => Await(m.GetNameValueTaskAsync())).Throws(expectedFirstException).Throws(expectedSecondException); + + var actualFirstException = await Assert.ThrowsAsync(async () => await mock.Object.GetNameValueTaskAsync()); + var actualSecondException = await Assert.ThrowsAsync(async () => await mock.Object.GetNameValueTaskAsync()); + + Assert.Same(expectedFirstException, actualFirstException); + Assert.Same(expectedSecondException, actualSecondException); + } + public interface IPerson { IPerson Friend { get; } From ef77bbd3e60adca495f60f3bd14afb01264cbc35 Mon Sep 17 00:00:00 2001 From: stakx Date: Wed, 30 Dec 2020 12:40:36 +0100 Subject: [PATCH 11/21] Add test for `Await` in `Mock.Of` --- tests/Moq.Tests/AwaitOperatorFixture.cs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/Moq.Tests/AwaitOperatorFixture.cs b/tests/Moq.Tests/AwaitOperatorFixture.cs index 4217bbb20..82b0aba7f 100644 --- a/tests/Moq.Tests/AwaitOperatorFixture.cs +++ b/tests/Moq.Tests/AwaitOperatorFixture.cs @@ -376,6 +376,27 @@ public async Task SetupSequence_Throws__on_awaited_ValueTask() Assert.Same(expectedSecondException, actualSecondException); } + [Fact] + public async Task Mock_Of() + { + var expectedName = "Alice"; + var expectedSameName = "also Alice"; + + var mock = Mock.Of(m => + Await(m.GetFriendTaskAsync()).Name == expectedName && + Await(m.Friend.GetNameValueTaskAsync()) == expectedSameName); + + var friend = await mock.GetFriendTaskAsync(); + var actualName = friend.Name; + + Assert.Equal(expectedName, actualName); + + friend = mock.Friend; + actualName = await friend.GetNameValueTaskAsync(); + + Assert.Equal(expectedSameName, actualName); + } + public interface IPerson { IPerson Friend { get; } From a0ffad9f8c7fabba9de2a970f14f88cc7abc6804 Mon Sep 17 00:00:00 2001 From: stakx Date: Wed, 30 Dec 2020 02:00:03 +0100 Subject: [PATCH 12/21] `Await` should support 'await anything' --- tests/Moq.Tests/AwaitOperatorFixture.cs | 128 ++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/tests/Moq.Tests/AwaitOperatorFixture.cs b/tests/Moq.Tests/AwaitOperatorFixture.cs index 82b0aba7f..fffdffafd 100644 --- a/tests/Moq.Tests/AwaitOperatorFixture.cs +++ b/tests/Moq.Tests/AwaitOperatorFixture.cs @@ -2,11 +2,13 @@ // All rights reserved. Licensed under the BSD 3-Clause License; see License.txt. using System; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Xunit; using static Moq.AwaitOperator; +using static Moq.Tests.AwaitOperatorFixture.AwaitSomeOperator; namespace Moq.Tests { @@ -72,6 +74,21 @@ public async Task Callback__on_awaited_ValueTask() Assert.True(invoked); } + [Fact] + public async Task Callback__on_awaited_custom_awaitable() + { + var invoked = false; + + var mock = new Mock(); + mock.Setup(m => Await(m.GetNameSomeAsync())).Callback(() => invoked = true); + + Assert.False(invoked); + + await mock.Object.GetNameSomeAsync(); + + Assert.True(invoked); + } + [Fact] public async Task Returns__on_awaited_Task() { @@ -98,6 +115,19 @@ public async Task Returns__on_awaited_ValueTask() Assert.Equal(expectedName, actualName); } + [Fact] + public async Task Returns__on_awaited_custom_awaitable() + { + var expectedName = "Alice"; + + var mock = new Mock(); + mock.Setup(m => Await(m.GetNameSomeAsync())).Returns(expectedName); + + var actualName = await mock.Object.GetNameSomeAsync(); + + Assert.Equal(expectedName, actualName); + } + [Fact] public async Task Throws__on_awaited_Task() { @@ -126,6 +156,20 @@ public async Task Throws__on_awaited_ValueTask() Assert.Same(expectedException, actualException); } + [Fact] + public async Task Throws__on_awaited_custom_awaitable() + { + var expectedException = new Exception(); + + var mock = new Mock(); + mock.Setup(m => Await(m.GetNameSomeAsync())).Throws(expectedException); + + var task = mock.Object.GetNameSomeAsync(); + var actualException = await Assert.ThrowsAsync(async () => await task); + + Assert.Same(expectedException, actualException); + } + [Fact] public async Task Callback__on_awaited_Task__of_property() { @@ -160,6 +204,23 @@ public async Task Callback__on_awaited_ValueTask__of_property() Assert.True(invoked); } + [Fact] + public async Task Callback__on_awaited_custom_awaitable__of_property() + { + var invoked = false; + + var mock = new Mock(); + mock.Setup(m => Await(m.Friend.GetNameSomeAsync())).Callback(() => invoked = true); + + var friend = mock.Object.Friend; + + Assert.False(invoked); + + await friend.GetNameSomeAsync(); + + Assert.True(invoked); + } + [Fact] public async Task Callback__on_property__of_awaited_Task() { @@ -194,6 +255,23 @@ public async Task Callback__on_property__of_awaited_ValueTask() Assert.True(invoked); } + [Fact] + public async Task Callback__on_property__of_awaited_custom_awaitable() + { + var invoked = false; + + var mock = new Mock(); + mock.Setup(m => Await(m.GetFriendSomeAsync()).Name).Callback(() => invoked = true); + + var friend = await mock.Object.GetFriendSomeAsync(); + + Assert.False(invoked); + + _ = friend.Name; + + Assert.True(invoked); + } + [Fact] public async Task Returns__on_property__of_awaited_Task() { @@ -222,6 +300,20 @@ public async Task Returns__on_property__of_awaited_ValueTask() Assert.Equal(expectedName, actualName); } + [Fact] + public async Task Returns__on_property__of_awaited_custom_awaitable() + { + var expectedName = "Alice"; + + var mock = new Mock(); + mock.Setup(m => Await(m.GetFriendSomeAsync()).Name).Returns(expectedName); + + var friend = await mock.Object.GetFriendSomeAsync(); + var actualName = friend.Name; + + Assert.Equal(expectedName, actualName); + } + [Fact] public async Task Throws__on_property__of_awaited_Task() { @@ -250,6 +342,20 @@ public async Task Throws__on_property__of_awaited_ValueTask() Assert.Same(expectedException, actualException); } + [Fact] + public async Task Throws__on_property__of_awaited_custom_awaitable() + { + var expectedException = new Exception(); + + var mock = new Mock(); + mock.Setup(m => Await(m.GetFriendSomeAsync()).Name).Throws(expectedException); + + var friend = await mock.Object.GetFriendSomeAsync(); + var actualException = Assert.Throws(() => friend.Name); + + Assert.Same(expectedException, actualException); + } + [Fact] public async Task SetupSequence_Pass__on_awaited_non_generic_Task() { @@ -401,12 +507,34 @@ public interface IPerson { IPerson Friend { get; } string Name { get; } + Some GetNameSomeAsync(); Task GetNameTaskAsync(); ValueTask GetNameValueTaskAsync(); + Some GetFriendSomeAsync(); Task GetFriendTaskAsync(); ValueTask GetFriendValueTaskAsync(); Task DoSomethingTaskAsync(); ValueTask DoSomethingValueTaskAsync(); } + + public class Some + { + private readonly Task task; + + public Some(TResult result) + { + this.task = Task.FromResult(result); + } + + public TaskAwaiter GetAwaiter() => this.task.GetAwaiter(); + } + + public static class AwaitSomeOperator + { + public static TResult Await(Some some) + { + return default(TResult); + } + } } } From 2d9a7bb28195f35838f5ae24a0f524d8d7291f02 Mon Sep 17 00:00:00 2001 From: stakx Date: Wed, 30 Dec 2020 13:38:45 +0100 Subject: [PATCH 13/21] Enable custom `IAwaitHandler`s & `Await` methods * `IAwaitableHandler` needs to become public so that it can be implemented for user-defined awaitable types. * `Await` methods for user-defined awaitable types may be defined any- where. They are required to be static and have exactly one parameter. That parameter gets used to look up a suitable `IAwaitableHandler`. * Handlers are registered with `AwaitableHandler.Register`. (For this reason `AwaitableHandler` is also made public.) --- src/Moq/Async/AwaitableHandler.cs | 34 ++++++++++++++++- src/Moq/Async/IAwaitableHandler.cs | 35 ++++++++++++++++- src/Moq/ExpressionExtensions.cs | 4 +- tests/Moq.Tests/AwaitOperatorFixture.cs | 51 +++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 5 deletions(-) diff --git a/src/Moq/Async/AwaitableHandler.cs b/src/Moq/Async/AwaitableHandler.cs index fa5dd6825..b8e64fa51 100644 --- a/src/Moq/Async/AwaitableHandler.cs +++ b/src/Moq/Async/AwaitableHandler.cs @@ -3,11 +3,16 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Threading.Tasks; namespace Moq.Async { - internal static class AwaitableHandler + /// + /// Provides methods for registering custom awaitable type handlers (). + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static class AwaitableHandler { private static readonly Dictionary> factories; @@ -22,7 +27,32 @@ static AwaitableHandler() }; } - public static IAwaitableHandler TryGet(Type type) + /// + /// Registers an factory function for the given awaitable type. + /// This allows Moq to properly recognize the awaitable type. + /// + /// + /// As an example, given an -able type TaskLike<TResult> + /// and a corresponding implementation TaskLikeHandler, + /// call this method as follows: + /// + /// AwaitableHandler.Register(typeof(TaskLike<>), type => new TaskLikeHandler(resultType: type.GetGenericArguments().Single())); + /// + /// + /// + /// The awaitable type for which to register the given function. + /// You can specify an open generic type definition, e. g. typeof(TaskLike<>), + /// to cover all concrete type instantiations. + /// + /// + /// The factory function that, given the of a concrete awaitable type, + /// will produce a suitable for it. + public static void Register(Type typeDefinition, Func factory) + { + AwaitableHandler.factories[typeDefinition] = factory; + } + + internal static IAwaitableHandler TryGet(Type type) { var typeDefinition = type.IsConstructedGenericType ? type.GetGenericTypeDefinition() : type; return AwaitableHandler.factories.TryGetValue(typeDefinition, out var factory) ? factory.Invoke(type) : null; diff --git a/src/Moq/Async/IAwaitableHandler.cs b/src/Moq/Async/IAwaitableHandler.cs index 5360c4552..27f27911d 100644 --- a/src/Moq/Async/IAwaitableHandler.cs +++ b/src/Moq/Async/IAwaitableHandler.cs @@ -2,17 +2,50 @@ // All rights reserved. Licensed under the BSD 3-Clause License; see License.txt. using System; +using System.ComponentModel; namespace Moq.Async { - internal interface IAwaitableHandler + /// + /// Converts return values and exceptions to and from instances of a particular awaitable type. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public interface IAwaitableHandler { + /// + /// Gets the type of result value represented by instances of this handler's awaitable type. + /// + /// + /// If this awaitable type does not have any result values, this property should return + /// (). + /// Type ResultType { get; } + /// + /// Converts the given result value to a successfully completed awaitable. + /// + /// + /// If this awaitable types does not have any result values, may be ignored. + /// object CreateCompleted(object result); + /// + /// Converts the given exception to a faulted awaitable. + /// object CreateFaulted(Exception exception); + /// + /// Attempts to extract the result value from the given awaitable. + /// This will succeed only for a successfully completed awaitable that has a result value. + /// + /// The awaitable from which a result value should be extracted. + /// + /// If successful, this parameter is set to the extracted result value. + /// + /// + /// if extraction of a result value succeeded; + /// otherwise, . + /// bool TryGetResult(object awaitable, out object result); } } diff --git a/src/Moq/ExpressionExtensions.cs b/src/Moq/ExpressionExtensions.cs index 5fa3fd046..70ea443d6 100644 --- a/src/Moq/ExpressionExtensions.cs +++ b/src/Moq/ExpressionExtensions.cs @@ -66,9 +66,9 @@ internal static TDelegate CompileUsingExpressionCompiler(this Express public static bool IsAwait(this MethodCallExpression expression, out IAwaitableHandler awaitableHandler) { - if (expression.Method.DeclaringType == typeof(AwaitOperator)) + if (expression.Method.IsStatic && expression.Arguments.Count == 1) { - var awaitableType = expression.Method.GetParameters().Single().ParameterType; + var awaitableType = expression.Method.GetParameters()[0].ParameterType; awaitableHandler = AwaitableHandler.TryGet(awaitableType); } else diff --git a/tests/Moq.Tests/AwaitOperatorFixture.cs b/tests/Moq.Tests/AwaitOperatorFixture.cs index fffdffafd..10043eac8 100644 --- a/tests/Moq.Tests/AwaitOperatorFixture.cs +++ b/tests/Moq.Tests/AwaitOperatorFixture.cs @@ -5,6 +5,8 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; +using Moq.Async; + using Xunit; using static Moq.AwaitOperator; @@ -14,6 +16,13 @@ namespace Moq.Tests { public class AwaitOperatorFixture { + public AwaitOperatorFixture() + { + AwaitableHandler.Register( + typeof(Some<>), + type => new SomeOfHandler(resultType: type.GetGenericArguments()[0])); + } + [Fact] public async Task Callback__on_awaited_non_generic_Task() { @@ -526,6 +535,11 @@ public Some(TResult result) this.task = Task.FromResult(result); } + public Some(Exception exception) + { + this.task = Task.FromException(exception); + } + public TaskAwaiter GetAwaiter() => this.task.GetAwaiter(); } @@ -536,5 +550,42 @@ public static TResult Await(Some some) return default(TResult); } } + + private sealed class SomeOfHandler : IAwaitableHandler + { + private readonly Type resultType; + + public SomeOfHandler(Type resultType) + { + this.resultType = resultType; + } + + public Type ResultType => this.resultType; + + public object CreateCompleted(object result) + { + var someType = typeof(Some<>).MakeGenericType(this.resultType); + var ctor = someType.GetConstructor(new Type[] { this.resultType }); + var some = ctor.Invoke(new object[] { result }); + return some; + } + + public object CreateFaulted(Exception exception) + { + var someType = typeof(Some<>).MakeGenericType(this.resultType); + var ctor = someType.GetConstructor(new Type[] { typeof(Exception) }); + var some = ctor.Invoke(new object[] { exception }); + return some; + } + + public bool TryGetResult(object some, out object result) + { + var type = some.GetType(); + var awaiter = type.GetMethod("GetAwaiter").Invoke(some, null); + var awaiterType = awaiter.GetType(); + result = awaiterType.GetMethod("GetResult").Invoke(awaiter, null); + return true; + } + } } } From f2146b2a5b209bef05c954877120ed679ca290b5 Mon Sep 17 00:00:00 2001 From: stakx Date: Wed, 30 Dec 2020 14:23:25 +0100 Subject: [PATCH 14/21] Create abstract base class `AwaitableHandler` ... by combining the existing `IAwaitableHandler` & `AwaitableHandler`. --- src/Moq/Async/AwaitableHandler.cs | 80 ++++++++++++++++++++++--- src/Moq/Async/IAwaitableHandler.cs | 51 ---------------- src/Moq/Async/TaskHandler.cs | 10 ++-- src/Moq/Async/TaskOfHandler.cs | 10 ++-- src/Moq/Async/ValueTaskHandler.cs | 10 ++-- src/Moq/Async/ValueTaskOfHandler.cs | 10 ++-- src/Moq/ExpressionExtensions.cs | 2 +- src/Moq/Invocation.cs | 2 +- src/Moq/InvocationShape.cs | 2 +- tests/Moq.Tests/AwaitOperatorFixture.cs | 17 ++---- 10 files changed, 98 insertions(+), 96 deletions(-) delete mode 100644 src/Moq/Async/IAwaitableHandler.cs diff --git a/src/Moq/Async/AwaitableHandler.cs b/src/Moq/Async/AwaitableHandler.cs index b8e64fa51..2fbbc1cef 100644 --- a/src/Moq/Async/AwaitableHandler.cs +++ b/src/Moq/Async/AwaitableHandler.cs @@ -9,16 +9,16 @@ namespace Moq.Async { /// - /// Provides methods for registering custom awaitable type handlers (). + /// Converts return values and exceptions to and from instances of a particular awaitable type. /// [EditorBrowsable(EditorBrowsableState.Advanced)] - public static class AwaitableHandler + public abstract class AwaitableHandler { - private static readonly Dictionary> factories; + private static readonly Dictionary> factories; static AwaitableHandler() { - AwaitableHandler.factories = new Dictionary>() + AwaitableHandler.factories = new Dictionary>() { [typeof(Task)] = type => TaskHandler.Instance, [typeof(Task<>)] = type => new TaskOfHandler(type.GetGenericArguments()[0]), @@ -28,12 +28,12 @@ static AwaitableHandler() } /// - /// Registers an factory function for the given awaitable type. + /// Registers an factory function for the given awaitable type. /// This allows Moq to properly recognize the awaitable type. /// /// /// As an example, given an -able type TaskLike<TResult> - /// and a corresponding implementation TaskLikeHandler, + /// and a corresponding implementation TaskLikeHandler, /// call this method as follows: /// /// AwaitableHandler.Register(typeof(TaskLike<>), type => new TaskLikeHandler(resultType: type.GetGenericArguments().Single())); @@ -46,16 +46,78 @@ static AwaitableHandler() /// /// /// The factory function that, given the of a concrete awaitable type, - /// will produce a suitable for it. - public static void Register(Type typeDefinition, Func factory) + /// will produce a suitable for it. + public static void Register(Type typeDefinition, Func factory) { AwaitableHandler.factories[typeDefinition] = factory; } - internal static IAwaitableHandler TryGet(Type type) + internal static AwaitableHandler TryGet(Type type) { var typeDefinition = type.IsConstructedGenericType ? type.GetGenericTypeDefinition() : type; return AwaitableHandler.factories.TryGetValue(typeDefinition, out var factory) ? factory.Invoke(type) : null; } + + /// + /// Gets the type of result value represented by instances of this handler's awaitable type. + /// + /// + /// If this awaitable type does not have any result values, this property should return + /// (). + /// + public abstract Type ResultType { get; } + + /// + /// Converts the given result value to a successfully completed awaitable. + /// + /// + /// If this awaitable types does not have any result values, may be ignored. + /// + public abstract object CreateCompleted(object result); + + /// + /// Converts the given exception to a faulted awaitable. + /// + public abstract object CreateFaulted(Exception exception); + + /// + /// Attempts to extract the result value from the given awaitable. + /// This will succeed only for a successfully completed awaitable that has a result value. + /// + /// The awaitable from which a result value should be extracted. + /// + /// If successful, this parameter is set to the extracted result value. + /// + /// + /// if extraction of a result value succeeded; + /// otherwise, . + /// + public virtual bool TryGetResult(object awaitable, out object result) + { + result = null; + + if (awaitable != null) + { + var awaitableType = awaitable.GetType(); + var awaiter = awaitableType.GetMethod("GetAwaiter")?.Invoke(awaitable, null); + if (awaiter != null) + { + var awaiterType = awaiter.GetType(); + var isCompleted = awaiterType.GetProperty("IsCompleted")?.GetValue(awaiter); + if (object.Equals(isCompleted, true)) + { + try + { + result = awaiterType.GetMethod("GetResult")?.Invoke(awaiter, null); + } + catch + { + } + } + } + } + + return result != null; + } } } diff --git a/src/Moq/Async/IAwaitableHandler.cs b/src/Moq/Async/IAwaitableHandler.cs deleted file mode 100644 index 27f27911d..000000000 --- a/src/Moq/Async/IAwaitableHandler.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors. -// All rights reserved. Licensed under the BSD 3-Clause License; see License.txt. - -using System; -using System.ComponentModel; - -namespace Moq.Async -{ - /// - /// Converts return values and exceptions to and from instances of a particular awaitable type. - /// - [EditorBrowsable(EditorBrowsableState.Advanced)] - public interface IAwaitableHandler - { - /// - /// Gets the type of result value represented by instances of this handler's awaitable type. - /// - /// - /// If this awaitable type does not have any result values, this property should return - /// (). - /// - Type ResultType { get; } - - /// - /// Converts the given result value to a successfully completed awaitable. - /// - /// - /// If this awaitable types does not have any result values, may be ignored. - /// - object CreateCompleted(object result); - - /// - /// Converts the given exception to a faulted awaitable. - /// - object CreateFaulted(Exception exception); - - /// - /// Attempts to extract the result value from the given awaitable. - /// This will succeed only for a successfully completed awaitable that has a result value. - /// - /// The awaitable from which a result value should be extracted. - /// - /// If successful, this parameter is set to the extracted result value. - /// - /// - /// if extraction of a result value succeeded; - /// otherwise, . - /// - bool TryGetResult(object awaitable, out object result); - } -} diff --git a/src/Moq/Async/TaskHandler.cs b/src/Moq/Async/TaskHandler.cs index efded48eb..f8d2b15de 100644 --- a/src/Moq/Async/TaskHandler.cs +++ b/src/Moq/Async/TaskHandler.cs @@ -6,7 +6,7 @@ namespace Moq.Async { - internal sealed class TaskHandler : IAwaitableHandler + internal sealed class TaskHandler : AwaitableHandler { public static readonly TaskHandler Instance = new TaskHandler(); @@ -14,7 +14,7 @@ private TaskHandler() { } - Type IAwaitableHandler.ResultType => typeof(void); + public override Type ResultType => typeof(void); public object CreateCompleted() { @@ -23,16 +23,16 @@ public object CreateCompleted() return tcs.Task; } - object IAwaitableHandler.CreateCompleted(object _) => this.CreateCompleted(); + public override object CreateCompleted(object _) => this.CreateCompleted(); - public object CreateFaulted(Exception exception) + public override object CreateFaulted(Exception exception) { var tcs = new TaskCompletionSource(); tcs.SetException(exception); return tcs.Task; } - bool IAwaitableHandler.TryGetResult(object task, out object result) + public override bool TryGetResult(object task, out object result) { result = null; return false; diff --git a/src/Moq/Async/TaskOfHandler.cs b/src/Moq/Async/TaskOfHandler.cs index 6e3182c27..093b172b1 100644 --- a/src/Moq/Async/TaskOfHandler.cs +++ b/src/Moq/Async/TaskOfHandler.cs @@ -6,7 +6,7 @@ namespace Moq.Async { - internal sealed class TaskOfHandler : IAwaitableHandler + internal sealed class TaskOfHandler : AwaitableHandler { private readonly Type resultType; private readonly Type tcsType; @@ -17,9 +17,9 @@ public TaskOfHandler(Type resultType) this.tcsType = typeof(TaskCompletionSource<>).MakeGenericType(this.resultType); } - public Type ResultType => this.resultType; + public override Type ResultType => this.resultType; - public object CreateCompleted(object result) + public override object CreateCompleted(object result) { var tcs = Activator.CreateInstance(this.tcsType); this.tcsType.GetMethod("SetResult").Invoke(tcs, new object[] { result }); @@ -27,7 +27,7 @@ public object CreateCompleted(object result) return task; } - public object CreateFaulted(Exception exception) + public override object CreateFaulted(Exception exception) { var tcs = Activator.CreateInstance(this.tcsType); this.tcsType.GetMethod("SetException", new Type[] { typeof(Exception) }).Invoke(tcs, new object[] { exception }); @@ -35,7 +35,7 @@ public object CreateFaulted(Exception exception) return task; } - public bool TryGetResult(object task, out object result) + public override bool TryGetResult(object task, out object result) { if (task != null) { diff --git a/src/Moq/Async/ValueTaskHandler.cs b/src/Moq/Async/ValueTaskHandler.cs index ea2ee0123..f3043219e 100644 --- a/src/Moq/Async/ValueTaskHandler.cs +++ b/src/Moq/Async/ValueTaskHandler.cs @@ -6,7 +6,7 @@ namespace Moq.Async { - internal sealed class ValueTaskHandler : IAwaitableHandler + internal sealed class ValueTaskHandler : AwaitableHandler { public static readonly ValueTaskHandler Instance = new ValueTaskHandler(); @@ -14,23 +14,23 @@ private ValueTaskHandler() { } - Type IAwaitableHandler.ResultType => typeof(void); + public override Type ResultType => typeof(void); public object CreateCompleted() { return new ValueTask(); } - object IAwaitableHandler.CreateCompleted(object _) => this.CreateCompleted(); + public override object CreateCompleted(object _) => this.CreateCompleted(); - public object CreateFaulted(Exception exception) + public override object CreateFaulted(Exception exception) { var tcs = new TaskCompletionSource(); tcs.SetException(exception); return new ValueTask(tcs.Task); } - bool IAwaitableHandler.TryGetResult(object task, out object result) + public override bool TryGetResult(object task, out object result) { result = null; return false; diff --git a/src/Moq/Async/ValueTaskOfHandler.cs b/src/Moq/Async/ValueTaskOfHandler.cs index 3ad4a97e8..65481f636 100644 --- a/src/Moq/Async/ValueTaskOfHandler.cs +++ b/src/Moq/Async/ValueTaskOfHandler.cs @@ -6,7 +6,7 @@ namespace Moq.Async { - internal sealed class ValueTaskOfHandler : IAwaitableHandler + internal sealed class ValueTaskOfHandler : AwaitableHandler { private readonly Type resultType; private readonly Type taskType; @@ -19,9 +19,9 @@ public ValueTaskOfHandler(Type taskType, Type resultType) this.tcsType = typeof(TaskCompletionSource<>).MakeGenericType(this.resultType); } - public Type ResultType => this.resultType; + public override Type ResultType => this.resultType; - public object CreateCompleted(object result) + public override object CreateCompleted(object result) { // `Activator.CreateInstance` could throw an `AmbiguousMatchException` in this use case, // so we're explicitly selecting and calling the constructor we want to use: @@ -31,7 +31,7 @@ public object CreateCompleted(object result) } - public object CreateFaulted(Exception exception) + public override object CreateFaulted(Exception exception) { var tcs = Activator.CreateInstance(this.tcsType); this.tcsType.GetMethod("SetException", new Type[] { typeof(Exception) }).Invoke(tcs, new object[] { exception }); @@ -44,7 +44,7 @@ public object CreateFaulted(Exception exception) return valueTask; } - public bool TryGetResult(object valueTask, out object result) + public override bool TryGetResult(object valueTask, out object result) { if (valueTask != null) { diff --git a/src/Moq/ExpressionExtensions.cs b/src/Moq/ExpressionExtensions.cs index 70ea443d6..af4646296 100644 --- a/src/Moq/ExpressionExtensions.cs +++ b/src/Moq/ExpressionExtensions.cs @@ -64,7 +64,7 @@ internal static TDelegate CompileUsingExpressionCompiler(this Express return ExpressionCompiler.Instance.Compile(expression); } - public static bool IsAwait(this MethodCallExpression expression, out IAwaitableHandler awaitableHandler) + public static bool IsAwait(this MethodCallExpression expression, out AwaitableHandler awaitableHandler) { if (expression.Method.IsStatic && expression.Arguments.Count == 1) { diff --git a/src/Moq/Invocation.cs b/src/Moq/Invocation.cs index 26ee62132..99232ca4a 100644 --- a/src/Moq/Invocation.cs +++ b/src/Moq/Invocation.cs @@ -91,7 +91,7 @@ public Exception Exception } } - public void ConvertResultUsing(IAwaitableHandler awaitableHandler) + public void ConvertResultUsing(AwaitableHandler awaitableHandler) { if (this.result is ExceptionResult r) { diff --git a/src/Moq/InvocationShape.cs b/src/Moq/InvocationShape.cs index 5b932a2d3..bfe775d5e 100644 --- a/src/Moq/InvocationShape.cs +++ b/src/Moq/InvocationShape.cs @@ -64,7 +64,7 @@ public static InvocationShape CreateFrom(Invocation invocation) public readonly LambdaExpression Expression; public readonly MethodInfo Method; public readonly IReadOnlyList Arguments; - public IAwaitableHandler AwaitableHandler; + public AwaitableHandler AwaitableHandler; private readonly IMatcher[] argumentMatchers; private MethodInfo methodImplementation; diff --git a/tests/Moq.Tests/AwaitOperatorFixture.cs b/tests/Moq.Tests/AwaitOperatorFixture.cs index 10043eac8..c06748ed3 100644 --- a/tests/Moq.Tests/AwaitOperatorFixture.cs +++ b/tests/Moq.Tests/AwaitOperatorFixture.cs @@ -551,7 +551,7 @@ public static TResult Await(Some some) } } - private sealed class SomeOfHandler : IAwaitableHandler + private sealed class SomeOfHandler : AwaitableHandler { private readonly Type resultType; @@ -560,9 +560,9 @@ public SomeOfHandler(Type resultType) this.resultType = resultType; } - public Type ResultType => this.resultType; + public override Type ResultType => this.resultType; - public object CreateCompleted(object result) + public override object CreateCompleted(object result) { var someType = typeof(Some<>).MakeGenericType(this.resultType); var ctor = someType.GetConstructor(new Type[] { this.resultType }); @@ -570,22 +570,13 @@ public object CreateCompleted(object result) return some; } - public object CreateFaulted(Exception exception) + public override object CreateFaulted(Exception exception) { var someType = typeof(Some<>).MakeGenericType(this.resultType); var ctor = someType.GetConstructor(new Type[] { typeof(Exception) }); var some = ctor.Invoke(new object[] { exception }); return some; } - - public bool TryGetResult(object some, out object result) - { - var type = some.GetType(); - var awaiter = type.GetMethod("GetAwaiter").Invoke(some, null); - var awaiterType = awaiter.GetType(); - result = awaiterType.GetMethod("GetResult").Invoke(awaiter, null); - return true; - } } } } From 058eb0ac01d50443225699f18c8bf04af04441c6 Mon Sep 17 00:00:00 2001 From: stakx Date: Wed, 30 Dec 2020 14:45:13 +0100 Subject: [PATCH 15/21] Delegate to `AwaitableHandler`s where appropriate This makes both default value providers and "inner mock" discovery work seamlessly for all known awaitable types. Also reduces code duplication a little. --- src/Moq/InnerMockSetup.cs | 2 +- .../LookupOrFallbackDefaultValueProvider.cs | 58 ++++++++----------- src/Moq/Mock.cs | 4 +- src/Moq/MockException.cs | 2 +- src/Moq/Setup.cs | 2 +- src/Moq/Unwrap.cs | 42 ++++---------- 6 files changed, 40 insertions(+), 70 deletions(-) diff --git a/src/Moq/InnerMockSetup.cs b/src/Moq/InnerMockSetup.cs index 21241153d..256a511cd 100644 --- a/src/Moq/InnerMockSetup.cs +++ b/src/Moq/InnerMockSetup.cs @@ -13,7 +13,7 @@ internal sealed class InnerMockSetup : SetupWithOutParameterSupport public InnerMockSetup(Expression originalExpression, Mock mock, InvocationShape expectation, object returnValue) : base(originalExpression, mock, expectation) { - Debug.Assert(Unwrap.ResultIfCompletedTask(returnValue) is IMocked); + Debug.Assert(Unwrap.ResultIfCompletedAwaitable(returnValue) is IMocked); this.returnValue = returnValue; diff --git a/src/Moq/LookupOrFallbackDefaultValueProvider.cs b/src/Moq/LookupOrFallbackDefaultValueProvider.cs index 2e16c5e7f..0f4db26b6 100644 --- a/src/Moq/LookupOrFallbackDefaultValueProvider.cs +++ b/src/Moq/LookupOrFallbackDefaultValueProvider.cs @@ -9,6 +9,8 @@ using System.Reflection; using System.Threading.Tasks; +using Moq.Async; + namespace Moq { /// @@ -41,9 +43,6 @@ protected LookupOrFallbackDefaultValueProvider() { this.factories = new Dictionary>() { - [typeof(Task)] = CreateTask, - [typeof(Task<>)] = CreateTaskOf, - [typeof(ValueTask<>)] = CreateValueTaskOf, ["System.ValueTuple`1"] = CreateValueTupleOf, ["System.ValueTuple`2"] = CreateValueTupleOf, ["System.ValueTuple`3"] = CreateValueTupleOf, @@ -64,8 +63,11 @@ protected void Deregister(Type factoryKey) { Debug.Assert(factoryKey != null); - this.factories.Remove(factoryKey); - this.factories.Remove(factoryKey.FullName); + // NOTE: In order to be able to unregister the default handling for awaitable types, + // we need a way (below) to know when to delegate to `AwaitableHandler`, and when not to. + // This is why we only reset the dictionary entry instead of removing it. + this.factories[factoryKey] = null; + this.factories[factoryKey.FullName] = null; } /// @@ -122,9 +124,22 @@ protected internal sealed override object GetDefaultValue(Type type, Mock mock) : type; Func factory; - return this.factories.TryGetValue(handlerKey , out factory) ? factory.Invoke(type, mock) - : this.factories.TryGetValue(handlerKey.FullName, out factory) ? factory.Invoke(type, mock) - : this.GetFallbackDefaultValue(type, mock); + + if (this.factories.TryGetValue(handlerKey, out factory) || this.factories.TryGetValue(handlerKey.FullName, out factory)) + { + if (factory != null) // This prevents delegation to `AwaitableHandler` for deregistered awaitables; see note above. + { + return factory.Invoke(type, mock); + } + } + else if (AwaitableHandler.TryGet(type) is { } awaitableHandler) + { + var resultType = awaitableHandler.ResultType; + var result = resultType != typeof(void) ? this.GetDefaultValue(resultType, mock) : default; + return awaitableHandler.CreateCompleted(result); + } + + return this.GetFallbackDefaultValue(type, mock); } /// @@ -142,33 +157,6 @@ protected virtual object GetFallbackDefaultValue(Type type, Mock mock) return type.GetDefaultValue(); } - private static object CreateTask(Type type, Mock mock) - { - return Task.FromResult(false); - } - - private object CreateTaskOf(Type type, Mock mock) - { - var resultType = type.GetGenericArguments()[0]; - var result = this.GetDefaultValue(resultType, mock); - - var tcsType = typeof(TaskCompletionSource<>).MakeGenericType(resultType); - var tcs = Activator.CreateInstance(tcsType); - tcsType.GetMethod("SetResult").Invoke(tcs, new[] { result }); - return tcsType.GetProperty("Task").GetValue(tcs, null); - } - - private object CreateValueTaskOf(Type type, Mock mock) - { - var resultType = type.GetGenericArguments()[0]; - var result = this.GetDefaultValue(resultType, mock); - - // `Activator.CreateInstance` could throw an `AmbiguousMatchException` in this use case, - // so we're explicitly selecting and calling the constructor we want to use: - var valueTaskCtor = type.GetConstructor(new[] { resultType }); - return valueTaskCtor.Invoke(new object[] { result }); - } - private object CreateValueTupleOf(Type type, Mock mock) { var itemTypes = type.GetGenericArguments(); diff --git a/src/Moq/Mock.cs b/src/Moq/Mock.cs index 97b18ef19..9bb1b0c7c 100644 --- a/src/Moq/Mock.cs +++ b/src/Moq/Mock.cs @@ -488,7 +488,7 @@ private static int GetMatchingInvocationCount( // Intermediate parts of a fluent expression do not contribute to the // total count themselves. The matching invocation count of the rightmost // expression gets "forwarded" towards the left: - if (Unwrap.ResultIfCompletedTask(matchingInvocation.ReturnValue) is IMocked mocked) + if (Unwrap.ResultIfCompletedAwaitable(matchingInvocation.ReturnValue) is IMocked mocked) { count += Mock.GetMatchingInvocationCount(mocked.Mock, remainingParts, visitedInnerMocks, invocationsToBeMarkedAsVerified); } @@ -792,7 +792,7 @@ internal object GetDefaultValue(MethodInfo method, out Mock candidateInnerMock, } var result = (useAlternateProvider ?? this.DefaultValueProvider).GetDefaultReturnValue(method, this); - var unwrappedResult = Unwrap.ResultIfCompletedTask(result); + var unwrappedResult = Unwrap.ResultIfCompletedAwaitable(result); candidateInnerMock = (unwrappedResult as IMocked)?.Mock; return result; diff --git a/src/Moq/MockException.cs b/src/Moq/MockException.cs index f30ab9b33..8d61dc63d 100644 --- a/src/Moq/MockException.cs +++ b/src/Moq/MockException.cs @@ -105,7 +105,7 @@ internal static MockException NoMatchingCalls( { message.Append($" {invocation}"); - if (invocation.Method.ReturnType != typeof(void) && Unwrap.ResultIfCompletedTask(invocation.ReturnValue) is IMocked mocked) + if (invocation.Method.ReturnType != typeof(void) && Unwrap.ResultIfCompletedAwaitable(invocation.ReturnValue) is IMocked mocked) { var innerMock = mocked.Mock; mocks.Enqueue(innerMock); diff --git a/src/Moq/Setup.cs b/src/Moq/Setup.cs index d4a66b928..b5be60eb7 100644 --- a/src/Moq/Setup.cs +++ b/src/Moq/Setup.cs @@ -34,7 +34,7 @@ protected Setup(Expression originalExpression, Mock mock, InvocationShape expect public LambdaExpression Expression => this.expectation.Expression; public Mock InnerMock => this.TryGetReturnValue(out var returnValue) - && Unwrap.ResultIfCompletedTask(returnValue) is IMocked mocked ? mocked.Mock : null; + && Unwrap.ResultIfCompletedAwaitable(returnValue) is IMocked mocked ? mocked.Mock : null; public bool IsConditional => this.Condition != null; diff --git a/src/Moq/Unwrap.cs b/src/Moq/Unwrap.cs index 6b0886fa5..7c3d08705 100644 --- a/src/Moq/Unwrap.cs +++ b/src/Moq/Unwrap.cs @@ -1,47 +1,29 @@ // Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors. // All rights reserved. Licensed under the BSD 3-Clause License; see License.txt. -using System.Threading.Tasks; +using Moq.Async; namespace Moq { internal static class Unwrap { /// - /// Recursively unwraps the result of completed or instances. - /// If the given value is not a task, the value itself is returned. + /// Recursively unwraps the result of successfully completed awaitable objects. + /// If the given value is not a successfully completed awaitable, the value itself is returned. /// /// The value to be unwrapped. - public static object ResultIfCompletedTask(object obj) + public static object ResultIfCompletedAwaitable(object obj) { - if (obj != null) + if (obj != null + && AwaitableHandler.TryGet(obj.GetType()) is { } awaitableHandler + && awaitableHandler.TryGetResult(obj, out var innerObj)) { - var objType = obj.GetType(); - if (objType.IsGenericType) - { - var genericTypeDefinition = objType.GetGenericTypeDefinition(); - if (genericTypeDefinition == typeof(Task<>) || genericTypeDefinition == typeof(ValueTask<>)) - { - var isCompleted = (bool)objType.GetProperty("IsCompleted").GetValue(obj, null); - if (isCompleted) - { - try - { - var innerObj = objType.GetProperty("Result").GetValue(obj, null); - return Unwrap.ResultIfCompletedTask(innerObj); - } - catch - { - // We end up here when the task has completed, but not successfully; - // e.g. when an exception was thrown. There's no return value to unwrap, - // so fall through. - } - } - } - } + return Unwrap.ResultIfCompletedAwaitable(innerObj); + } + else + { + return obj; } - - return obj; } } } From 8a9012229441ce806660a1f2564826e9519c4e42 Mon Sep 17 00:00:00 2001 From: stakx Date: Wed, 30 Dec 2020 18:03:36 +0100 Subject: [PATCH 16/21] Add tests for `SetupSet` --- tests/Moq.Tests/AwaitOperatorFixture.cs | 59 ++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/tests/Moq.Tests/AwaitOperatorFixture.cs b/tests/Moq.Tests/AwaitOperatorFixture.cs index c06748ed3..179baf180 100644 --- a/tests/Moq.Tests/AwaitOperatorFixture.cs +++ b/tests/Moq.Tests/AwaitOperatorFixture.cs @@ -281,6 +281,63 @@ public async Task Callback__on_property__of_awaited_custom_awaitable() Assert.True(invoked); } + [Fact] + public async Task Callback__on_property_set__of_awaited_Task() + { + var expectedName = "Alice"; + + string invokedWithName = "not yet invoked"; + + var mock = new Mock(); + mock.SetupSet(m => Await(m.GetFriendTaskAsync()).Name = It.IsAny()).Callback((string name) => invokedWithName = name); + + var friend = await mock.Object.GetFriendTaskAsync(); + + Assert.Equal("not yet invoked", invokedWithName); + + friend.Name = expectedName; + + Assert.Equal(expectedName, invokedWithName); + } + + [Fact] + public async Task Callback__on_property_set__of_awaited_ValueTask() + { + var expectedName = "Alice"; + + string invokedWithName = "not yet invoked"; + + var mock = new Mock(); + mock.SetupSet(m => Await(m.GetFriendValueTaskAsync()).Name = It.IsAny()).Callback((string name) => invokedWithName = name); + + var friend = await mock.Object.GetFriendValueTaskAsync(); + + Assert.Equal("not yet invoked", invokedWithName); + + friend.Name = expectedName; + + Assert.Equal(expectedName, invokedWithName); + } + + [Fact] + public async Task Callback__on_property_set__of_awaited_custom_awaitable() + { + var expectedName = "Alice"; + + string invokedWithName = "not yet invoked"; + + var mock = new Mock(); + mock.SetupSet(m => Await(m.GetFriendSomeAsync()).Name = It.IsAny()).Callback((string name) => invokedWithName = name); + + var friend = await mock.Object.GetFriendSomeAsync(); + + Assert.Equal("not yet invoked", invokedWithName); + + friend.Name = expectedName; + + Assert.Equal(expectedName, invokedWithName); + } + [Fact] public async Task Returns__on_property__of_awaited_Task() { @@ -515,7 +572,7 @@ public async Task Mock_Of() public interface IPerson { IPerson Friend { get; } - string Name { get; } + string Name { get; set; } Some GetNameSomeAsync(); Task GetNameTaskAsync(); ValueTask GetNameValueTaskAsync(); From 35832e69d1c8e29e099e8509aabfb1899e9e2d74 Mon Sep 17 00:00:00 2001 From: stakx Date: Wed, 30 Dec 2020 18:58:58 +0100 Subject: [PATCH 17/21] Infer awaits during delegate-to-expression reconstruction --- src/Moq/ActionObserver.cs | 23 ++++++++++- src/Moq/Async/AwaitExpression.cs | 41 +++++++++++++++++++ src/Moq/AwaitOperator.cs | 4 +- src/Moq/ExpressionComparer.cs | 14 ++++++- src/Moq/ExpressionExtensions.cs | 39 ++++++++++++++---- ...tringBuilderExtensions.AppendExpression.cs | 10 +++++ tests/Moq.Tests/AwaitOperatorFixture.cs | 2 +- 7 files changed, 118 insertions(+), 15 deletions(-) create mode 100644 src/Moq/Async/AwaitExpression.cs diff --git a/src/Moq/ActionObserver.cs b/src/Moq/ActionObserver.cs index e35c5f6f0..0f61d745e 100644 --- a/src/Moq/ActionObserver.cs +++ b/src/Moq/ActionObserver.cs @@ -9,6 +9,7 @@ using System.Linq.Expressions; using System.Reflection; +using Moq.Async; using Moq.Expressions.Visitors; using Moq.Internals; using Moq.Properties; @@ -60,6 +61,19 @@ public override Expression> ReconstructExpression(Action action, var invocation = recorder.Invocation; if (invocation != null) { + var resultType = invocation.Method.DeclaringType; + if (resultType.IsAssignableFrom(body.Type) == false) + { + if (AwaitableHandler.TryGet(body.Type) is { } awaitableHandler + && awaitableHandler.ResultType.IsAssignableFrom(resultType)) + { + // We are here because the current invocation cannot be chained onto the previous one, + // however it *can* be chained if we assume that there was an `Await` call around the + // former invocation that we simply don't see because static methods aren't recorded. + // In this case, we make things work by wrapping an `await` around the left invocation: + body = new AwaitExpression(body, awaitableHandler); + } + } body = Expression.Call(body, invocation.Method, GetArgumentExpressions(invocation, recorder.Matches.ToArray())); } else @@ -227,7 +241,7 @@ private sealed class Recorder : IInterceptor private int creationTimestamp; private Invocation invocation; private int invocationTimestamp; - private IProxy returnValue; + private object returnValue; public Recorder(MatcherObserver matcherObserver) { @@ -248,7 +262,7 @@ public IEnumerable Matches } } - public Recorder Next => this.returnValue?.Interceptor as Recorder; + public Recorder Next => (Unwrap.ResultIfCompletedAwaitable(this.returnValue) as IProxy)?.Interceptor as Recorder; public void Intercept(Invocation invocation) { @@ -277,6 +291,11 @@ public void Intercept(Invocation invocation) { this.returnValue = null; } + else if (AwaitableHandler.TryGet(returnType) is { } awaitableHandler) + { + var result = CreateProxy(awaitableHandler.ResultType, null, this.matcherObserver, out _); + this.returnValue = awaitableHandler.CreateCompleted(result); + } else if (returnType.IsMockable()) { this.returnValue = CreateProxy(returnType, null, this.matcherObserver, out _); diff --git a/src/Moq/Async/AwaitExpression.cs b/src/Moq/Async/AwaitExpression.cs new file mode 100644 index 000000000..1fff5e1c3 --- /dev/null +++ b/src/Moq/Async/AwaitExpression.cs @@ -0,0 +1,41 @@ +// Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors. +// All rights reserved. Licensed under the BSD 3-Clause License; see License.txt. + +using System; +using System.Linq.Expressions; + +namespace Moq.Async +{ + /// + /// Represents an expression. + /// + /// + /// When reconstructing an expression tree from a delegate, will insert + /// nodes of this kind as a substitute for inferred calls to Await methods. (Being static methods, + /// they are not recorded / seen by and therefore their identity gets "lost".) + /// + internal sealed class AwaitExpression : Expression + { + public readonly AwaitableHandler AwaitableHandler; + public readonly Expression Operand; + + public AwaitExpression(Expression operand, AwaitableHandler awaitableHandler) + { + this.AwaitableHandler = awaitableHandler; + this.Operand = operand; + } + + public override bool CanReduce => false; + + public override ExpressionType NodeType => ExpressionType.Extension; + + public override Type Type => this.AwaitableHandler.ResultType; + + public override string ToString() + { + return $"(await {this.Operand})"; + } + + protected override Expression VisitChildren(ExpressionVisitor visitor) => this; + } +} diff --git a/src/Moq/AwaitOperator.cs b/src/Moq/AwaitOperator.cs index decc0bba8..261e8f326 100644 --- a/src/Moq/AwaitOperator.cs +++ b/src/Moq/AwaitOperator.cs @@ -34,7 +34,7 @@ public static void Await(ValueTask task) /// public static TResult Await(Task task) { - return default(TResult); + return task.Result; } /// @@ -42,7 +42,7 @@ public static TResult Await(Task task) /// public static TResult Await(ValueTask task) { - return default(TResult); + return task.Result; } } } diff --git a/src/Moq/ExpressionComparer.cs b/src/Moq/ExpressionComparer.cs index a86e68574..fa78ebf20 100644 --- a/src/Moq/ExpressionComparer.cs +++ b/src/Moq/ExpressionComparer.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Linq.Expressions; +using Moq.Async; using Moq.Expressions.Visitors; namespace Moq @@ -265,11 +266,20 @@ private bool EqualsUnary(UnaryExpression x, UnaryExpression y) private bool EqualsExtension(Expression x, Expression y) { - // For now, we only care about our own `MatchExpression` extension; + // For now, we only care about our own extensions; // if we wanted to be more thorough, we'd try to reduce `x` and `y`, // then compare the reduced nodes. - return x.IsMatch(out var xm) && y.IsMatch(out var ym) && object.Equals(xm, ym); + if (x.IsMatch(out var xm)) + { + return y.IsMatch(out var ym) && object.Equals(xm, ym); + } + else if (x is AwaitExpression xa) + { + return y is AwaitExpression ya && this.Equals(xa, ya); + } + + return false; } } } diff --git a/src/Moq/ExpressionExtensions.cs b/src/Moq/ExpressionExtensions.cs index af4646296..df8b82110 100644 --- a/src/Moq/ExpressionExtensions.cs +++ b/src/Moq/ExpressionExtensions.cs @@ -64,16 +64,31 @@ internal static TDelegate CompileUsingExpressionCompiler(this Express return ExpressionCompiler.Instance.Compile(expression); } - public static bool IsAwait(this MethodCallExpression expression, out AwaitableHandler awaitableHandler) + public static bool IsAwait(this Expression expression, out AwaitableHandler awaitableHandler) { - if (expression.Method.IsStatic && expression.Arguments.Count == 1) - { - var awaitableType = expression.Method.GetParameters()[0].ParameterType; - awaitableHandler = AwaitableHandler.TryGet(awaitableType); - } - else + awaitableHandler = null; + + switch (expression) { - awaitableHandler = null; + case AwaitExpression awaitExpression: + { + awaitableHandler = awaitExpression.AwaitableHandler; + break; + } + + case MethodCallExpression methodCallExpression: + { + if (methodCallExpression.Method.IsStatic && methodCallExpression.Arguments.Count == 1) + { + var awaitableType = methodCallExpression.Method.GetParameters()[0].ParameterType; + awaitableHandler = AwaitableHandler.TryGet(awaitableType); + } + else + { + awaitableHandler = null; + } + break; + } } return awaitableHandler != null; @@ -111,6 +126,7 @@ public static bool CanSplit(this Expression e) case ExpressionType.Call: case ExpressionType.Index: + case ExpressionType.Extension when (e is AwaitExpression): { return true; } @@ -262,6 +278,13 @@ void Split(Expression e, out Expression r /* remainder */, out InvocationShape p return; } + case ExpressionType.Extension when (e is AwaitExpression awaitExpression): + { + Split(awaitExpression.Operand, out r, out p); + p.AwaitableHandler = awaitExpression.AwaitableHandler; + return; + } + case ExpressionType.Index: // indexer query { var indexExpression = (IndexExpression)e; diff --git a/src/Moq/StringBuilderExtensions.AppendExpression.cs b/src/Moq/StringBuilderExtensions.AppendExpression.cs index 174741e16..36c6986c9 100644 --- a/src/Moq/StringBuilderExtensions.AppendExpression.cs +++ b/src/Moq/StringBuilderExtensions.AppendExpression.cs @@ -8,6 +8,7 @@ using System.Linq.Expressions; using System.Text; +using Moq.Async; using Moq.Properties; namespace Moq @@ -106,6 +107,10 @@ public static StringBuilder AppendExpression(this StringBuilder builder, Express { return builder.AppendExpression(me); } + else if (expression is AwaitExpression ae) + { + return builder.AppendExpression(ae); + } goto default; default: @@ -432,5 +437,10 @@ private static StringBuilder AppendExpression(this StringBuilder builder, MatchE { return builder.AppendExpression(expression.Match.RenderExpression); } + + private static StringBuilder AppendExpression(this StringBuilder builder, AwaitExpression expression) + { + return builder.Append("(await ").AppendExpression(expression.Operand).Append(")"); + } } } diff --git a/tests/Moq.Tests/AwaitOperatorFixture.cs b/tests/Moq.Tests/AwaitOperatorFixture.cs index 179baf180..6ab9d4f45 100644 --- a/tests/Moq.Tests/AwaitOperatorFixture.cs +++ b/tests/Moq.Tests/AwaitOperatorFixture.cs @@ -604,7 +604,7 @@ public static class AwaitSomeOperator { public static TResult Await(Some some) { - return default(TResult); + return some.GetAwaiter().GetResult(); } } From 2b7cb4855c311ab8bf36861af6ac0b73c7700525 Mon Sep 17 00:00:00 2001 From: stakx Date: Wed, 30 Dec 2020 19:06:29 +0100 Subject: [PATCH 18/21] Add one more test & clean up another --- tests/Moq.Tests/AwaitOperatorFixture.cs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/Moq.Tests/AwaitOperatorFixture.cs b/tests/Moq.Tests/AwaitOperatorFixture.cs index 6ab9d4f45..5e63ec77d 100644 --- a/tests/Moq.Tests/AwaitOperatorFixture.cs +++ b/tests/Moq.Tests/AwaitOperatorFixture.cs @@ -338,6 +338,28 @@ public async Task Callback__on_property_set__of_awaited_custom_awaitable() Assert.Equal(expectedName, invokedWithName); } + [Fact] + public async Task Callback__on_property_set__of_awaited_Task__of_property__of_awaited_ValueTask__of_awaited_custom_awaitable_type() + { + var expectedName = "Alice"; + + string invokedWithName = "not yet invoked"; + + var mock = new Mock(); + mock.SetupSet(m => Await(Await(Await(m.GetFriendSomeAsync()).GetFriendValueTaskAsync()).Friend.GetFriendTaskAsync()).Name = It.IsAny()).Callback((string name) => invokedWithName = name); + + var friend = await mock.Object.GetFriendSomeAsync(); + var friendOfFriend = await friend.GetFriendValueTaskAsync(); + var friendOfFriendOfFriend = friendOfFriend.Friend; + var friendOfFriendOfFriendOfFriend = await friendOfFriendOfFriend.GetFriendTaskAsync(); + + Assert.Equal("not yet invoked", invokedWithName); + + friendOfFriendOfFriendOfFriend.Name = expectedName; + + Assert.Equal(expectedName, invokedWithName); + } + [Fact] public async Task Returns__on_property__of_awaited_Task() { @@ -448,8 +470,6 @@ public async Task SetupSequence_Pass__on_awaited_non_generic_ValueTask() var secondTask = mock.Object.DoSomethingValueTaskAsync(); await secondTask; - - Assert.NotSame(firstTask, secondTask); } [Fact] From 7bd534e07241917a9500d79cefb12e6e24f695a0 Mon Sep 17 00:00:00 2001 From: stakx Date: Wed, 30 Dec 2020 20:42:25 +0100 Subject: [PATCH 19/21] Resolve a (very serious!) CodeFactor issue --- src/Moq/Async/ValueTaskOfHandler.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Moq/Async/ValueTaskOfHandler.cs b/src/Moq/Async/ValueTaskOfHandler.cs index 65481f636..6d63bdf6c 100644 --- a/src/Moq/Async/ValueTaskOfHandler.cs +++ b/src/Moq/Async/ValueTaskOfHandler.cs @@ -28,7 +28,6 @@ public override object CreateCompleted(object result) var ctor = this.taskType.GetConstructor(new[] { resultType }); var valueTask = ctor.Invoke(new object[] { result }); return valueTask; - } public override object CreateFaulted(Exception exception) From 8528b9a4e6c830ebeb655f494841c595fb2b7aad Mon Sep 17 00:00:00 2001 From: stakx Date: Wed, 30 Dec 2020 20:59:16 +0100 Subject: [PATCH 20/21] Ensure any await is visible in setup expressions --- src/Moq/ExpressionExtensions.cs | 7 +++++++ tests/Moq.Tests/AwaitOperatorFixture.cs | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/Moq/ExpressionExtensions.cs b/src/Moq/ExpressionExtensions.cs index df8b82110..6bc7abcfe 100644 --- a/src/Moq/ExpressionExtensions.cs +++ b/src/Moq/ExpressionExtensions.cs @@ -257,6 +257,13 @@ void Split(Expression e, out Expression r /* remainder */, out InvocationShape p else if (methodCallExpression.IsAwait(out var awaitableHandler)) { Split(methodCallExpression.Arguments.Single(), out r, out p); + p = new InvocationShape( + expression: + Expression.Lambda( + new AwaitExpression(methodCallExpression.Arguments.Single(), awaitableHandler), + p.Expression.Parameters), + method: p.Method, + arguments: p.Arguments); p.AwaitableHandler = awaitableHandler; } else diff --git a/tests/Moq.Tests/AwaitOperatorFixture.cs b/tests/Moq.Tests/AwaitOperatorFixture.cs index 5e63ec77d..bc4d849a7 100644 --- a/tests/Moq.Tests/AwaitOperatorFixture.cs +++ b/tests/Moq.Tests/AwaitOperatorFixture.cs @@ -2,6 +2,7 @@ // All rights reserved. Licensed under the BSD 3-Clause License; see License.txt. using System; +using System.Linq; using System.Runtime.CompilerServices; using System.Threading.Tasks; @@ -589,6 +590,29 @@ public async Task Mock_Of() Assert.Equal(expectedSameName, actualName); } + [Fact] + public void Await__is_represented__in_setup_Expression() + { + var mock = new Mock(); + mock.Setup(m => Await(m.DoSomethingTaskAsync())); + var setup = mock.Setups.Last(); + Assert.Contains("aWAiT", setup.Expression.ToStringFixed(), StringComparison.OrdinalIgnoreCase); + // ^^^^^ + // NOTE: Depending on the exact scenario, the `Await` method name may get converted to + // a faux `await` keyword (e.g. with `SetupSet`). Right now, we don't care too much about + // that slight loss of accuracy, as long as a clue for the occurred await remains. + } + + [Fact] + public void Await__is_represented__in_setup_OriginalExpression() + { + // NOTE: The note in the test above applies here, too. + var mock = new Mock(); + mock.Setup(m => Await(m.DoSomethingTaskAsync())); + var setup = mock.Setups.Last(); + Assert.Contains("await", setup.OriginalExpression.ToStringFixed(), StringComparison.OrdinalIgnoreCase); + } + public interface IPerson { IPerson Friend { get; } From c99687818125e56f7f556860bcd405786bd776f3 Mon Sep 17 00:00:00 2001 From: stakx Date: Wed, 30 Dec 2020 21:19:44 +0100 Subject: [PATCH 21/21] Update the changelog --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d54f77ea..87f7aff93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,28 @@ The format is loosely based on [Keep a Changelog](http://keepachangelog.com/en/1 ## Unreleased +#### Added + +* "Await anything". Async methods can now be set up using the `Await` methods that act as a substitute for the `await` keyword (which the compilers don't allow inside setup expressions). This also enables method chaining across async calls in setup expressions. Should work almost anywhere. + + ```csharp + using static Moq.AwaitOperator; + + mock.SetupGet(m => Await(m.GetByIdAsync(...)).Items) + .Returns(new[] { 1, 2 }); + + var x = await mock.Object.GetByIdAsync(...); + var items = x.Items; // [ 1, 2 ] + ``` + + or, using `Mock.Of`: + + ```csharp + Mock.Of(m => Await(m.GetByIdAsync(...)).Items == new[] { 1, 2 })); + ``` + + See [details in the pull request description](https://github.com/moq/moq4/pull/1123), including an example of how to enable custom awaitable types. (@stakx, #1123) + #### Changed * Attempts to mark conditionals setup as verifiable are once again allowed; it turns out that forbidding it (as was done in #997 for version 4.14.0) is in fact a regression. (@stakx, #1121)