From 047bf9565a77ae4da63ff5331bd1e81d8a7d6a7c Mon Sep 17 00:00:00 2001 From: stakx Date: Fri, 1 Jan 2021 10:44:43 +0100 Subject: [PATCH 1/6] Add tests for `task.Result` setups --- tests/Moq.Tests/SetupTaskResultFixture.cs | 303 ++++++++++++++++++++++ 1 file changed, 303 insertions(+) create mode 100644 tests/Moq.Tests/SetupTaskResultFixture.cs diff --git a/tests/Moq.Tests/SetupTaskResultFixture.cs b/tests/Moq.Tests/SetupTaskResultFixture.cs new file mode 100644 index 000000000..819563a40 --- /dev/null +++ b/tests/Moq.Tests/SetupTaskResultFixture.cs @@ -0,0 +1,303 @@ +// 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; + +namespace Moq.Tests +{ + public class SetupTaskResultFixture + { + private readonly Exception Exception = new Exception("bad"); + private readonly Exception SecondException = new Exception("very bad"); + + private readonly IPerson Friend = Mock.Of(p => p.Name == "Alice"); + private readonly string NameOfFriend = "Alice"; + private readonly string SecondNameOfFriend = "Alicia"; + + private readonly IPerson SecondFriend = Mock.Of(p => p.Name == "Betty"); + + public interface IPerson + { + string Name { get; set; } + Task GetNameTaskAsync(); + ValueTask GetNameValueTaskAsync(); + + IPerson Friend { get; set; } + Task GetFriendTaskAsync(); + ValueTask GetFriendValueTaskAsync(); + } + + [Fact] + public async Task Setup__task_Result__creates_a_single_setup() + { + var person = new Mock() { DefaultValue = DefaultValue.Mock }; + person.Setup(p => p.GetFriendTaskAsync().Result); + var friend = Mock.Get(await person.Object.GetFriendTaskAsync()); + Assert.Single(person.Setups); + Assert.Empty(friend.Setups); + } + + [Fact] + public async Task Mock_Of__completed_Task() + { + var person = Mock.Of(p => p.GetFriendTaskAsync().Result == Friend); + var friend = await person.GetFriendTaskAsync(); + Assert.Same(Friend, friend); + } + + [Fact] + public async Task Mock_Of__completed_ValueTask() + { + var person = Mock.Of(p => p.GetFriendValueTaskAsync().Result == Friend); + var friend = await person.GetFriendValueTaskAsync(); + Assert.Same(Friend, friend); + } + + [Fact] + public async Task Mock_Of__property_of__completed_Task() + { + var person = Mock.Of(p => p.GetFriendTaskAsync().Result.Name == NameOfFriend); + var friend = await person.GetFriendTaskAsync(); + Assert.Equal(NameOfFriend, friend.Name); + } + + [Fact] + public async Task Mock_Of__property_of__completed_ValueTask() + { + var person = Mock.Of(p => p.GetFriendValueTaskAsync().Result.Name == NameOfFriend); + var friend = await person.GetFriendValueTaskAsync(); + Assert.Equal(NameOfFriend, friend.Name); + } + + [Fact] + public async Task Mock_Of__properties_of__completed_Task() + { + var person = Mock.Of(p => p.GetFriendTaskAsync().Result.Name == NameOfFriend + && p.GetFriendTaskAsync().Result.Friend == SecondFriend); + var friend = await person.GetFriendTaskAsync(); + Assert.Equal(NameOfFriend, friend.Name); + Assert.Same(SecondFriend, friend.Friend); + } + + [Fact] + public async Task Mock_Of__properties_of__completed_ValueTask() + { + var person = Mock.Of(p => p.GetFriendValueTaskAsync().Result.Name == NameOfFriend + && p.GetFriendValueTaskAsync().Result.Friend == SecondFriend); + var friend = await person.GetFriendValueTaskAsync(); + Assert.Equal(NameOfFriend, friend.Name); + Assert.Same(SecondFriend, friend.Friend); + } + + [Fact] + public async Task Setup__completed_Task__Returns() + { + var person = new Mock(); + person.Setup(p => p.GetFriendTaskAsync().Result).Returns(Friend); + var friend = await person.Object.GetFriendTaskAsync(); + Assert.Same(Friend, friend); + } + + [Fact] + public async Task Setup__completed_Task__Throws() + { + var person = new Mock(); + person.Setup(p => p.GetFriendTaskAsync().Result).Throws(Exception); + var exception = await Assert.ThrowsAsync(async () => await person.Object.GetFriendTaskAsync()); + Assert.Same(Exception, exception); + } + + [Fact] + public async Task Setup__completed_ValueTask__Returns() + { + var person = new Mock(); + person.Setup(p => p.GetFriendValueTaskAsync().Result).Returns(Friend); + var friend = await person.Object.GetFriendValueTaskAsync(); + Assert.Same(Friend, friend); + } + + [Fact] + public async Task Setup__completed_ValueTask__Throws() + { + var person = new Mock(); + person.Setup(p => p.GetFriendValueTaskAsync().Result).Throws(Exception); + var exception = await Assert.ThrowsAsync(async () => await person.Object.GetFriendValueTaskAsync()); + Assert.Same(Exception, exception); + } + + [Fact] + public async Task SetupGet__property_of__completed_Task__Returns() + { + var person = new Mock(); + person.SetupGet(p => p.GetFriendTaskAsync().Result.Name).Returns(NameOfFriend); + var friend = await person.Object.GetFriendTaskAsync(); + Assert.Equal(NameOfFriend, friend.Name); + } + + [Fact] + public async Task SetupGet__property_of__completed_Task__Throws() + { + var person = new Mock(); + person.SetupGet(p => p.GetFriendTaskAsync().Result.Name).Throws(Exception); + var friend = await person.Object.GetFriendTaskAsync(); + var exception = Assert.Throws(() => friend.Name); + Assert.Same(Exception, exception); + } + + [Fact] + public async Task SetupGet__property_of__completed_ValueTask__Returns() + { + var person = new Mock(); + person.Setup(m => m.GetFriendValueTaskAsync().Result.Name).Returns(NameOfFriend); + var friend = await person.Object.GetFriendValueTaskAsync(); + Assert.Equal(NameOfFriend, friend.Name); + } + + [Fact] + public async Task SetupGet__property_of__completed_ValueTask__Throws() + { + var person = new Mock(); + person.SetupGet(p => p.GetFriendValueTaskAsync().Result.Name).Throws(Exception); + var friend = await person.Object.GetFriendValueTaskAsync(); + var exception = Assert.Throws(() => friend.Name); + Assert.Same(Exception, exception); + } + + [Fact] + public async Task SetupSequence__completed_Task__Returns() + { + var person = new Mock(); + person.SetupSequence(p => p.GetFriendTaskAsync().Result).Returns(Friend).Returns(SecondFriend); + var friend = await person.Object.GetFriendTaskAsync(); + var secondFriend = await person.Object.GetFriendTaskAsync(); + Assert.Same(Friend, friend); + Assert.Same(SecondFriend, secondFriend); + } + + [Fact] + public async Task SetupSequence__completed_Task__Throws() + { + var person = new Mock(); + person.SetupSequence(p => p.GetFriendTaskAsync().Result).Throws(Exception).Throws(SecondException); + var exception = await Assert.ThrowsAsync(async () => await person.Object.GetFriendTaskAsync()); + var secondException = await Assert.ThrowsAsync(async () => await person.Object.GetFriendTaskAsync()); + Assert.Same(Exception, exception); + Assert.Same(SecondException, secondException); + } + + [Fact] + public async Task SetupSequence__completed_ValueTask__Returns() + { + var person = new Mock(); + person.SetupSequence(p => p.GetFriendValueTaskAsync().Result).Returns(Friend).Returns(SecondFriend); + var friend = await person.Object.GetFriendValueTaskAsync(); + var secondFriend = await person.Object.GetFriendValueTaskAsync(); + Assert.Same(Friend, friend); + Assert.Same(SecondFriend, secondFriend); + } + + [Fact] + public async Task SetupSequence__completed_ValueTask__Throws() + { + var person = new Mock(); + person.SetupSequence(p => p.GetFriendValueTaskAsync().Result).Throws(Exception).Throws(SecondException); + var exception = await Assert.ThrowsAsync(async () => await person.Object.GetFriendValueTaskAsync()); + var secondException = await Assert.ThrowsAsync(async () => await person.Object.GetFriendValueTaskAsync()); + Assert.Same(Exception, exception); + Assert.Same(SecondException, secondException); + } + + [Fact] + public async Task SetupSequence__property_of__completed_Task__Returns() + { + var person = new Mock(); + person.SetupSequence(p => p.GetFriendTaskAsync().Result.Name).Returns(NameOfFriend).Returns(SecondNameOfFriend); + var friend = await person.Object.GetFriendTaskAsync(); + Assert.Equal(NameOfFriend, friend.Name); + Assert.Equal(SecondNameOfFriend, friend.Name); + } + + [Fact] + public async Task SetupSequence__property_of__completed_Task__Throws() + { + var person = new Mock(); + person.SetupSequence(p => p.GetFriendTaskAsync().Result.Name).Throws(Exception).Throws(SecondException); + var friend = await person.Object.GetFriendTaskAsync(); + var exception = Assert.Throws(() => friend.Name); + var secondException = Assert.Throws(() => friend.Name); + Assert.Same(Exception, exception); + Assert.Same(SecondException, secondException); + } + + [Fact] + public async Task SetupSequence__property_of__completed_ValueTask__Returns() + { + var person = new Mock(); + person.SetupSequence(p => p.GetFriendValueTaskAsync().Result.Name).Returns(NameOfFriend).Returns(SecondNameOfFriend); + var friend = await person.Object.GetFriendValueTaskAsync(); + Assert.Equal(NameOfFriend, friend.Name); + Assert.Equal(SecondNameOfFriend, friend.Name); + } + + [Fact] + public async Task SetupSequence__property_of__completed_ValueTask__Throws() + { + var person = new Mock(); + person.SetupSequence(p => p.GetFriendValueTaskAsync().Result.Name).Throws(Exception).Throws(SecondException); + var friend = await person.Object.GetFriendValueTaskAsync(); + var exception = Assert.Throws(() => friend.Name); + var secondException = Assert.Throws(() => friend.Name); + Assert.Same(Exception, exception); + Assert.Same(SecondException, secondException); + } + + [Fact] + public async Task SetupSet__property_of__completed_Task__Callback() + { + string setToValue = null; + var person = new Mock(); + person.SetupSet(p => p.GetFriendTaskAsync().Result.Name = It.IsAny()).Callback((string value) => setToValue = value); + var friend = await person.Object.GetFriendTaskAsync(); + Assert.Null(setToValue); + friend.Name = NameOfFriend; + Assert.Equal(NameOfFriend, setToValue); + } + + [Fact] + public async Task SetupSet__property_of__completed_ValueTask__Callback() + { + string setToValue = null; + var person = new Mock(); + person.SetupSet(p => p.GetFriendValueTaskAsync().Result.Name = It.IsAny()).Callback((string value) => setToValue = value); + var friend = await person.Object.GetFriendValueTaskAsync(); + Assert.Null(setToValue); + friend.Name = NameOfFriend; + Assert.Equal(NameOfFriend, setToValue); + } + + [Fact] + public async Task SetupSet__property_of__completed_Task__Throws() + { + string setToValue = null; + var person = new Mock(); + person.SetupSet(p => p.GetFriendTaskAsync().Result.Name = It.IsAny()).Throws(Exception); + var friend = await person.Object.GetFriendTaskAsync(); + var exception = Assert.Throws(() => friend.Name = NameOfFriend); + Assert.Same(Exception, exception); + } + + [Fact] + public async Task SetupSet__property_of__completed_ValueTask__Throws() + { + string setToValue = null; + var person = new Mock(); + person.SetupSet(p => p.GetFriendValueTaskAsync().Result.Name = It.IsAny()).Throws(Exception); + var friend = await person.Object.GetFriendValueTaskAsync(); + var exception = Assert.Throws(() => friend.Name = NameOfFriend); + Assert.Same(Exception, exception); + } + } +} From 5802db8aa7ca35cabd148e96dcd7249d836a793a Mon Sep 17 00:00:00 2001 From: stakx Date: Fri, 1 Jan 2021 10:49:23 +0100 Subject: [PATCH 2/6] Support faulted awaitables in `IAwaitableFactory` --- src/Moq/Async/AwaitableFactory`1.cs | 21 +++++++++++++++++++++ src/Moq/Async/AwaitableFactory`2.cs | 21 +++++++++++++++++++++ src/Moq/Async/IAwaitableFactory.cs | 5 +++++ src/Moq/Async/TaskFactory.cs | 17 +++++++++++++++++ src/Moq/Async/TaskFactory`1.cs | 16 ++++++++++++++++ src/Moq/Async/ValueTaskFactory.cs | 16 ++++++++++++++++ src/Moq/Async/ValueTaskFactory`1.cs | 16 ++++++++++++++++ 7 files changed, 112 insertions(+) diff --git a/src/Moq/Async/AwaitableFactory`1.cs b/src/Moq/Async/AwaitableFactory`1.cs index d71541d68..eb45d228a 100644 --- a/src/Moq/Async/AwaitableFactory`1.cs +++ b/src/Moq/Async/AwaitableFactory`1.cs @@ -2,7 +2,9 @@ // All rights reserved. Licensed under the BSD 3-Clause License; see License.txt. using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; namespace Moq.Async { @@ -23,6 +25,25 @@ object IAwaitableFactory.CreateCompleted(object result) return this.CreateCompleted(); } + public abstract TAwaitable CreateFaulted(Exception exception); + + object IAwaitableFactory.CreateFaulted(Exception exception) + { + Debug.Assert(exception != null); + + return this.CreateFaulted(exception); + } + + public abstract TAwaitable CreateFaulted(IEnumerable exceptions); + + object IAwaitableFactory.CreateFaulted(IEnumerable exceptions) + { + Debug.Assert(exceptions != null); + Debug.Assert(exceptions.Any()); + + return this.CreateFaulted(exceptions); + } + bool IAwaitableFactory.TryGetResult(object awaitable, out object result) { Debug.Assert(awaitable is TAwaitable); diff --git a/src/Moq/Async/AwaitableFactory`2.cs b/src/Moq/Async/AwaitableFactory`2.cs index f1042b534..4cac4c456 100644 --- a/src/Moq/Async/AwaitableFactory`2.cs +++ b/src/Moq/Async/AwaitableFactory`2.cs @@ -2,7 +2,9 @@ // All rights reserved. Licensed under the BSD 3-Clause License; see License.txt. using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; namespace Moq.Async { @@ -23,6 +25,25 @@ object IAwaitableFactory.CreateCompleted(object result) return this.CreateCompleted((TResult)result); } + public abstract TAwaitable CreateFaulted(Exception exception); + + object IAwaitableFactory.CreateFaulted(Exception exception) + { + Debug.Assert(exception != null); + + return this.CreateFaulted(exception); + } + + public abstract TAwaitable CreateFaulted(IEnumerable exceptions); + + object IAwaitableFactory.CreateFaulted(IEnumerable exceptions) + { + Debug.Assert(exceptions != null); + Debug.Assert(exceptions.Any()); + + return this.CreateFaulted(exceptions); + } + public abstract bool TryGetResult(TAwaitable awaitable, out TResult result); bool IAwaitableFactory.TryGetResult(object awaitable, out object result) diff --git a/src/Moq/Async/IAwaitableFactory.cs b/src/Moq/Async/IAwaitableFactory.cs index f31c8656b..1c38ca940 100644 --- a/src/Moq/Async/IAwaitableFactory.cs +++ b/src/Moq/Async/IAwaitableFactory.cs @@ -2,6 +2,7 @@ // All rights reserved. Licensed under the BSD 3-Clause License; see License.txt. using System; +using System.Collections.Generic; namespace Moq.Async { @@ -11,6 +12,10 @@ internal interface IAwaitableFactory object CreateCompleted(object result = null); + object CreateFaulted(Exception exception); + + object CreateFaulted(IEnumerable exceptions); + bool TryGetResult(object awaitable, out object result); } } diff --git a/src/Moq/Async/TaskFactory.cs b/src/Moq/Async/TaskFactory.cs index 1ada0969e..874751c6d 100644 --- a/src/Moq/Async/TaskFactory.cs +++ b/src/Moq/Async/TaskFactory.cs @@ -1,6 +1,9 @@ // 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.Reflection; using System.Threading.Tasks; namespace Moq.Async @@ -17,5 +20,19 @@ public override Task CreateCompleted() { return Task.FromResult(default); } + + public override Task CreateFaulted(Exception exception) + { + var tcs = new TaskCompletionSource(); + tcs.SetException(exception); + return tcs.Task; + } + + public override Task CreateFaulted(IEnumerable exceptions) + { + var tcs = new TaskCompletionSource(); + tcs.SetException(exceptions); + return tcs.Task; + } } } diff --git a/src/Moq/Async/TaskFactory`1.cs b/src/Moq/Async/TaskFactory`1.cs index ccb4316b8..41d5dde6a 100644 --- a/src/Moq/Async/TaskFactory`1.cs +++ b/src/Moq/Async/TaskFactory`1.cs @@ -1,6 +1,8 @@ // 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 @@ -12,6 +14,20 @@ public override Task CreateCompleted(TResult result) return Task.FromResult(result); } + public override Task CreateFaulted(Exception exception) + { + var tcs = new TaskCompletionSource(); + tcs.SetException(exception); + return tcs.Task; + } + + public override Task CreateFaulted(IEnumerable exceptions) + { + var tcs = new TaskCompletionSource(); + tcs.SetException(exceptions); + return tcs.Task; + } + public override bool TryGetResult(Task task, out TResult result) { if (task.Status == TaskStatus.RanToCompletion) diff --git a/src/Moq/Async/ValueTaskFactory.cs b/src/Moq/Async/ValueTaskFactory.cs index fbd3fb1f2..d8775ff14 100644 --- a/src/Moq/Async/ValueTaskFactory.cs +++ b/src/Moq/Async/ValueTaskFactory.cs @@ -1,6 +1,8 @@ // 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 @@ -17,5 +19,19 @@ public override ValueTask CreateCompleted() { return default; } + + public override ValueTask CreateFaulted(Exception exception) + { + var tcs = new TaskCompletionSource(); + tcs.SetException(exception); + return new ValueTask(tcs.Task); + } + + public override ValueTask CreateFaulted(IEnumerable exceptions) + { + var tcs = new TaskCompletionSource(); + tcs.SetException(exceptions); + return new ValueTask(tcs.Task); + } } } diff --git a/src/Moq/Async/ValueTaskFactory`1.cs b/src/Moq/Async/ValueTaskFactory`1.cs index 265ef4813..213921bf7 100644 --- a/src/Moq/Async/ValueTaskFactory`1.cs +++ b/src/Moq/Async/ValueTaskFactory`1.cs @@ -1,6 +1,8 @@ // 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 @@ -12,6 +14,20 @@ public override ValueTask CreateCompleted(TResult result) return new ValueTask(result); } + public override ValueTask CreateFaulted(Exception exception) + { + var tcs = new TaskCompletionSource(); + tcs.SetException(exception); + return new ValueTask(tcs.Task); + } + + public override ValueTask CreateFaulted(IEnumerable exceptions) + { + var tcs = new TaskCompletionSource(); + tcs.SetException(exceptions); + return new ValueTask(tcs.Task); + } + public override bool TryGetResult(ValueTask valueTask, out TResult result) { if (valueTask.IsCompletedSuccessfully) From 187902c6d131a639bb089552566583327d1d0771 Mon Sep 17 00:00:00 2001 From: stakx Date: Fri, 1 Jan 2021 10:55:47 +0100 Subject: [PATCH 3/6] Enable `task.Result` for expression-based setup methods --- src/Moq/ExpressionExtensions.cs | 20 ++++++++++++++++++++ src/Moq/Invocation.cs | 14 ++++++++++++++ src/Moq/InvocationShape.cs | 15 ++++++++++++++- src/Moq/Setup.cs | 20 +++++++++++++++++++- 4 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/Moq/ExpressionExtensions.cs b/src/Moq/ExpressionExtensions.cs index 256f74cd4..d17eb5169 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; @@ -294,6 +295,16 @@ void Split(Expression e, out Expression r /* remainder */, out InvocationShape p { var memberAccessExpression = (MemberExpression)e; Debug.Assert(memberAccessExpression.Member is PropertyInfo); + + if (IsResult(memberAccessExpression.Member, out var awaitableFactory)) + { + Split(memberAccessExpression.Expression, out r, out p); + p.AddResultExpression( + awaitable => Expression.MakeMemberAccess(awaitable, memberAccessExpression.Member), + awaitableFactory); + return; + } + r = memberAccessExpression.Expression; var parameter = Expression.Parameter(r.Type, r is ParameterExpression ope ? ope.Name : ParameterName); var property = memberAccessExpression.GetReboundProperty(); @@ -327,6 +338,15 @@ void Split(Expression e, out Expression r /* remainder */, out InvocationShape p throw new InvalidOperationException(); // this should be unreachable } } + + bool IsResult(MemberInfo member, out IAwaitableFactory awaitableFactory) + { + var instanceType = member.DeclaringType; + awaitableFactory = AwaitableFactory.TryGet(instanceType); + var returnType = member switch { PropertyInfo p => p.PropertyType, + _ => null }; + return awaitableFactory != null && object.Equals(returnType, awaitableFactory.ResultType); + } } internal static PropertyInfo GetReboundProperty(this MemberExpression expression) diff --git a/src/Moq/Invocation.cs b/src/Moq/Invocation.cs index 64b7cbc73..b859d11f0 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,18 @@ public Exception Exception } } + public void ConvertResultToAwaitable(IAwaitableFactory awaitableFactory) + { + if (this.result is ExceptionResult r) + { + this.result = awaitableFactory.CreateFaulted(r.Exception); + } + else if (this.result != null && !this.method.ReturnType.IsAssignableFrom(this.result.GetType())) + { + this.result = awaitableFactory.CreateCompleted(this.result); + } + } + public bool IsVerified => this.verified; /// diff --git a/src/Moq/InvocationShape.cs b/src/Moq/InvocationShape.cs index 11fdec7ee..2c9a78768 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; @@ -60,11 +61,12 @@ public static InvocationShape CreateFrom(Invocation invocation) private static readonly Expression[] noArguments = new Expression[0]; private static readonly IMatcher[] noArgumentMatchers = new IMatcher[0]; - public readonly LambdaExpression Expression; + public LambdaExpression Expression; public readonly MethodInfo Method; public readonly IReadOnlyList Arguments; private readonly IMatcher[] argumentMatchers; + private IAwaitableFactory awaitableFactory; private MethodInfo methodImplementation; private Expression[] partiallyEvaluatedArguments; #if DEBUG @@ -98,6 +100,17 @@ public InvocationShape(LambdaExpression expression, MethodInfo method, IReadOnly this.exactGenericTypeArguments = exactGenericTypeArguments; } + public void AddResultExpression(Func add, IAwaitableFactory awaitableFactory) + { + this.Expression = E.Lambda(add(this.Expression.Body), this.Expression.Parameters); + this.awaitableFactory = awaitableFactory; + } + + public bool HasResultExpression(out IAwaitableFactory awaitableFactory) + { + return (awaitableFactory = this.awaitableFactory) != null; + } + public void Deconstruct(out LambdaExpression expression, out MethodInfo method, out IReadOnlyList arguments) { expression = this.Expression; diff --git a/src/Moq/Setup.cs b/src/Moq/Setup.cs index 66c20f595..6a72d7949 100644 --- a/src/Moq/Setup.cs +++ b/src/Moq/Setup.cs @@ -65,7 +65,25 @@ public void Execute(Invocation invocation) this.Condition?.SetupEvaluatedSuccessfully(); this.expectation.SetupEvaluatedSuccessfully(invocation); - this.ExecuteCore(invocation); + if (this.expectation.HasResultExpression(out var awaitableFactory)) + { + try + { + this.ExecuteCore(invocation); + } + catch (Exception exception) + { + invocation.Exception = exception; + } + finally + { + invocation.ConvertResultToAwaitable(awaitableFactory); + } + } + else + { + this.ExecuteCore(invocation); + } } protected abstract void ExecuteCore(Invocation invocation); From 42521c47313d86b4d06c617e1bdcda22046fdcaa Mon Sep 17 00:00:00 2001 From: stakx Date: Fri, 1 Jan 2021 11:27:50 +0100 Subject: [PATCH 4/6] Add ability in `IAwaitableFactory` to create result expression --- src/Moq/Async/AwaitExpression.cs | 40 +++++++++++++++++++++++++++++ src/Moq/Async/AwaitableFactory`1.cs | 6 +++++ src/Moq/Async/AwaitableFactory`2.cs | 3 +++ src/Moq/Async/IAwaitableFactory.cs | 3 +++ src/Moq/Async/TaskFactory.cs | 1 + src/Moq/Async/TaskFactory`1.cs | 8 ++++++ src/Moq/Async/ValueTaskFactory`1.cs | 8 ++++++ 7 files changed, 69 insertions(+) create mode 100644 src/Moq/Async/AwaitExpression.cs diff --git a/src/Moq/Async/AwaitExpression.cs b/src/Moq/Async/AwaitExpression.cs new file mode 100644 index 000000000..126cee898 --- /dev/null +++ b/src/Moq/Async/AwaitExpression.cs @@ -0,0 +1,40 @@ +// 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.Diagnostics; +using System.Linq.Expressions; + +namespace Moq.Async +{ + internal sealed class AwaitExpression : Expression + { + private readonly IAwaitableFactory awaitableFactory; + private readonly Expression operand; + + public AwaitExpression(Expression operand, IAwaitableFactory awaitableFactory) + { + Debug.Assert(awaitableFactory != null); + Debug.Assert(operand != null); + + this.awaitableFactory = awaitableFactory; + this.operand = operand; + } + + public override bool CanReduce => false; + + public override ExpressionType NodeType => ExpressionType.Extension; + + public Expression Operand => this.operand; + + public override Type Type => this.awaitableFactory.ResultType; + + public override string ToString() + { + return this.awaitableFactory.ResultType == typeof(void) ? $"await {this.operand}" + : $"(await {this.operand})"; + } + + protected override Expression VisitChildren(ExpressionVisitor visitor) => this; + } +} diff --git a/src/Moq/Async/AwaitableFactory`1.cs b/src/Moq/Async/AwaitableFactory`1.cs index eb45d228a..c3048d013 100644 --- a/src/Moq/Async/AwaitableFactory`1.cs +++ b/src/Moq/Async/AwaitableFactory`1.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Linq.Expressions; namespace Moq.Async { @@ -44,6 +45,11 @@ object IAwaitableFactory.CreateFaulted(IEnumerable exceptions) return this.CreateFaulted(exceptions); } + Expression IAwaitableFactory.CreateResultExpression(Expression awaitableExpression) + { + return new AwaitExpression(awaitableExpression, this); + } + bool IAwaitableFactory.TryGetResult(object awaitable, out object result) { Debug.Assert(awaitable is TAwaitable); diff --git a/src/Moq/Async/AwaitableFactory`2.cs b/src/Moq/Async/AwaitableFactory`2.cs index 4cac4c456..5c5cdae85 100644 --- a/src/Moq/Async/AwaitableFactory`2.cs +++ b/src/Moq/Async/AwaitableFactory`2.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Linq.Expressions; namespace Moq.Async { @@ -46,6 +47,8 @@ object IAwaitableFactory.CreateFaulted(IEnumerable exceptions) public abstract bool TryGetResult(TAwaitable awaitable, out TResult result); + public abstract Expression CreateResultExpression(Expression awaitableExpression); + bool IAwaitableFactory.TryGetResult(object awaitable, out object result) { Debug.Assert(awaitable is TAwaitable); diff --git a/src/Moq/Async/IAwaitableFactory.cs b/src/Moq/Async/IAwaitableFactory.cs index 1c38ca940..fb87828bb 100644 --- a/src/Moq/Async/IAwaitableFactory.cs +++ b/src/Moq/Async/IAwaitableFactory.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq.Expressions; namespace Moq.Async { @@ -16,6 +17,8 @@ internal interface IAwaitableFactory object CreateFaulted(IEnumerable exceptions); + Expression CreateResultExpression(Expression awaitableExpression); + bool TryGetResult(object awaitable, out object result); } } diff --git a/src/Moq/Async/TaskFactory.cs b/src/Moq/Async/TaskFactory.cs index 874751c6d..c396b2671 100644 --- a/src/Moq/Async/TaskFactory.cs +++ b/src/Moq/Async/TaskFactory.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; diff --git a/src/Moq/Async/TaskFactory`1.cs b/src/Moq/Async/TaskFactory`1.cs index 41d5dde6a..2f19662f0 100644 --- a/src/Moq/Async/TaskFactory`1.cs +++ b/src/Moq/Async/TaskFactory`1.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq.Expressions; using System.Threading.Tasks; namespace Moq.Async @@ -28,6 +29,13 @@ public override Task CreateFaulted(IEnumerable exceptions) return tcs.Task; } + public override Expression CreateResultExpression(Expression awaitableExpression) + { + return Expression.MakeMemberAccess( + awaitableExpression, + typeof(Task).GetProperty(nameof(Task.Result))); + } + public override bool TryGetResult(Task task, out TResult result) { if (task.Status == TaskStatus.RanToCompletion) diff --git a/src/Moq/Async/ValueTaskFactory`1.cs b/src/Moq/Async/ValueTaskFactory`1.cs index 213921bf7..69db7d841 100644 --- a/src/Moq/Async/ValueTaskFactory`1.cs +++ b/src/Moq/Async/ValueTaskFactory`1.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq.Expressions; using System.Threading.Tasks; namespace Moq.Async @@ -28,6 +29,13 @@ public override ValueTask CreateFaulted(IEnumerable exceptio return new ValueTask(tcs.Task); } + public override Expression CreateResultExpression(Expression awaitableExpression) + { + return Expression.MakeMemberAccess( + awaitableExpression, + typeof(ValueTask).GetProperty(nameof(ValueTask.Result))); + } + public override bool TryGetResult(ValueTask valueTask, out TResult result) { if (valueTask.IsCompletedSuccessfully) From 66bcb21e5f09158ed0b6117c50c1181aa50ee089 Mon Sep 17 00:00:00 2001 From: stakx Date: Fri, 1 Jan 2021 11:28:41 +0100 Subject: [PATCH 5/6] Enable `task.Result` in delegate-based setup methods --- src/Moq/ActionObserver.cs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/Moq/ActionObserver.cs b/src/Moq/ActionObserver.cs index e35c5f6f0..3f5af5763 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 (AwaitableFactory.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 a `.Result` query on the + // former invocation that we don't see because non-virtual members aren't recorded. + // In this case, we make things work by adding back the missing `.Result`: + body = awaitableHandler.CreateResultExpression(body); + } + } 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 => (Awaitable.TryGetResultRecursive(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 (AwaitableFactory.TryGet(returnType) is { } awaitableFactory) + { + var result = CreateProxy(awaitableFactory.ResultType, null, this.matcherObserver, out _); + this.returnValue = awaitableFactory.CreateCompleted(result); + } else if (returnType.IsMockable()) { this.returnValue = CreateProxy(returnType, null, this.matcherObserver, out _); From 6f6a89dd4d20446f62332de9013232f48b92be52 Mon Sep 17 00:00:00 2001 From: stakx Date: Fri, 1 Jan 2021 12:04:08 +0100 Subject: [PATCH 6/6] Update the changelog --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d54f77ea..c2a57990f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,38 @@ The format is loosely based on [Keep a Changelog](http://keepachangelog.com/en/1 ## Unreleased +#### Added + +* Ability to directly set up the `.Result` of tasks and value tasks, which makes setup expressions more uniform by rendering dedicated async verbs like `.ReturnsAsync`, `.ThrowsAsync`, etc. unnecessary: + + ```diff + -mock.Setup(x => x.GetFooAsync()).ReturnsAsync(foo) + +mock.Setup(x => x.GetFooAsync().Result).Returns(foo) + ``` + + This is useful in places where there currently aren't any such async verbs at all: + + ```diff + -Mock.Of(x => x.GetFooAsync() == Task.FromResult(foo)) + +Mock.Of(x => x.GetFooAsync().Result == foo) + ``` + + This also allows recursive setups / method chaining across async calls inside a single setup expression: + + ```diff + -mock.Setup(x => x.GetFooAsync()).ReturnsAsync(Mock.Of(f => f.Bar == bar)) + +mock.Setup(x => x.GetFooAsync().Result.Bar).Returns(bar) + ``` + + or, with only `Mock.Of`: + + ```diff + -Mock.Of(x => x.GetFooAsync() == Task.FromResult(Mock.Of(f => f.Bar == bar))) + +Mock.Of(x => x.GetFooAsync().Result.Bar == bar) + ``` + + This should work in all principal setup methods (`Mock.Of`, `mock.Setup…`, `mock.Verify…`). Support in `mock.Protected()` and for custom awaitable types may be added in the future. (@stakx, #1125) + #### 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)