diff --git a/src/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs b/src/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs index 7d735a8ecc..8c4bd062d9 100644 --- a/src/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs +++ b/src/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs @@ -718,6 +718,21 @@ private static void GenerateTypedReturnsAsyncOverload(CodeWriter writer, List (object?)factory({castArgs}));"); writer.AppendLine("return this;"); } + + // Returns alias, so an `async (a, b) => ...` lambda binds here rather than failing against + // the synchronous typed overload with CS4010. Deprioritised — and therefore net9.0+ only — + // for the same reasons as the parameterless alias; see EmitReturnsAsyncOverloads. + // See issue #6495. + writer.AppendLine(); + writer.AppendLine("#if NET9_0_OR_GREATER"); + writer.AppendLine("/// Configure a typed computed async return value using the actual method parameters. The returned task is handed back as-is, so an async factory stays pending until it completes."); + writer.AppendLine(PriorityMinusOneAttribute); + using (writer.Block($"public {wrapperName} Returns({funcType} factory)")) + { + writer.AppendLine($"EnsureSetup().ReturnsRaw(args => (object?)factory({castArgs}));"); + writer.AppendLine("return this;"); + } + writer.AppendLine("#endif"); } private static void GenerateTypedCallbackOverload(CodeWriter writer, List nonOutParams, @@ -1778,6 +1793,26 @@ private static void EmitReturnsAsyncOverloads(CodeWriter writer, string wrapperN writer.AppendLine($"public {wrapperName} ReturnsAsync({taskType} task) {{ EnsureSetup().ReturnsRaw(task); return this; }}"); writer.AppendLine($"/// Return a pre-built {taskLabel} from a factory, invoked on each call."); writer.AppendLine($"public {wrapperName} ReturnsAsync(global::System.Func<{taskType}> taskFactory) {{ EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; }}"); + // Same factory, spelled Returns — so an `async () => ...` lambda binds without the caller + // having to know about ReturnsAsync (the synchronous Func overload rejects it with + // CS4010). The returned task is handed back as-is, so it stays pending. See issue #6495. + // + // Deprioritised against the synchronous Returns(Func) sibling: when T is a reference + // type, a lambda whose body pins nothing — Returns(() => null), Returns(() => throw ...) — + // converts equally well to Func and Func>, which would be CS0121. The priority + // breaks that tie back to the pre-existing synchronous meaning. A genuine async lambda is + // unaffected: it is not convertible to Func at all, so it is the only candidate. + // + // That makes the alias inseparable from the attribute, which only reaches the consumer's + // compilation on net9.0+ — TUnit.Mocks polyfills it internally for its own build, so a + // net8.0 consumer would hit CS0246. Emitting the overload there without the priority would + // hand them the ambiguity instead, so the whole alias is net9.0+ (matching the framework + // polyfills below); net8.0 keeps ReturnsAsync, which already returns the task as-is. + writer.AppendLine("#if NET9_0_OR_GREATER"); + writer.AppendLine($"/// Return a {taskLabel} from a factory, invoked on each call. The {taskLabel} is returned as-is, so an async factory stays pending until it completes."); + writer.AppendLine(PriorityMinusOneAttribute); + writer.AppendLine($"public {wrapperName} Returns(global::System.Func<{taskType}> taskFactory) {{ EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; }}"); + writer.AppendLine("#endif"); } private static void EmitEnsureSetup(CodeWriter writer, string builderType, bool hasTypeArguments) diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_FluentUI_Shape_Nullable_Warnings.verified.txt b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_FluentUI_Shape_Nullable_Warnings.verified.txt index ace9dea357..f3d011b8a9 100644 --- a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_FluentUI_Shape_Nullable_Warnings.verified.txt +++ b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_FluentUI_Shape_Nullable_Warnings.verified.txt @@ -202,6 +202,11 @@ public sealed class IDialogReference_GetReturnValueAsync_M0_MockCall : global public IDialogReference_GetReturnValueAsync_M0_MockCall ReturnsAsync(global::System.Threading.Tasks.Task task) { EnsureSetup().ReturnsRaw(task); return this; } /// Return a pre-built Task from a factory, invoked on each call. public IDialogReference_GetReturnValueAsync_M0_MockCall ReturnsAsync(global::System.Func> taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #if NET9_0_OR_GREATER + /// Return a Task from a factory, invoked on each call. The Task is returned as-is, so an async factory stays pending until it completes. + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public IDialogReference_GetReturnValueAsync_M0_MockCall Returns(global::System.Func> taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #endif // ICallVerification /// @@ -587,6 +592,11 @@ public sealed class IDialogService_UpdateDialogAsync_M0_MockCall : global public IDialogService_UpdateDialogAsync_M0_MockCall ReturnsAsync(global::System.Threading.Tasks.Task task) { EnsureSetup().ReturnsRaw(task); return this; } /// Return a pre-built Task from a factory, invoked on each call. public IDialogService_UpdateDialogAsync_M0_MockCall ReturnsAsync(global::System.Func> taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #if NET9_0_OR_GREATER + /// Return a Task from a factory, invoked on each call. The Task is returned as-is, so an async factory stays pending until it completes. + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public IDialogService_UpdateDialogAsync_M0_MockCall Returns(global::System.Func> taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #endif /// Configure a typed computed return value using the actual method parameters. public IDialogService_UpdateDialogAsync_M0_MockCall Returns(global::System.Func, global::IDialogReference?> factory) @@ -602,6 +612,16 @@ public sealed class IDialogService_UpdateDialogAsync_M0_MockCall : global return this; } + #if NET9_0_OR_GREATER + /// Configure a typed computed async return value using the actual method parameters. The returned task is handed back as-is, so an async factory stays pending until it completes. + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public IDialogService_UpdateDialogAsync_M0_MockCall Returns(global::System.Func, global::System.Threading.Tasks.Task> factory) + { + EnsureSetup().ReturnsRaw(args => (object?)factory((string)args[0]!, (global::DialogParameters)args[1]!)); + return this; + } + #endif + /// Execute a typed callback using the actual method parameters. public IDialogService_UpdateDialogAsync_M0_MockCall Callback(global::System.Action> callback) { @@ -694,6 +714,11 @@ public sealed class IDialogService_ShowDialogAsync_M1_MockCall : global public IDialogService_ShowDialogAsync_M1_MockCall ReturnsAsync(global::System.Threading.Tasks.Task task) { EnsureSetup().ReturnsRaw(task); return this; } /// Return a pre-built Task from a factory, invoked on each call. public IDialogService_ShowDialogAsync_M1_MockCall ReturnsAsync(global::System.Func> taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #if NET9_0_OR_GREATER + /// Return a Task from a factory, invoked on each call. The Task is returned as-is, so an async factory stays pending until it completes. + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public IDialogService_ShowDialogAsync_M1_MockCall Returns(global::System.Func> taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #endif /// Configure a typed computed return value using the actual method parameters. public IDialogService_ShowDialogAsync_M1_MockCall Returns(global::System.Func factory) @@ -709,6 +734,16 @@ public sealed class IDialogService_ShowDialogAsync_M1_MockCall : global return this; } + #if NET9_0_OR_GREATER + /// Configure a typed computed async return value using the actual method parameters. The returned task is handed back as-is, so an async factory stays pending until it completes. + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public IDialogService_ShowDialogAsync_M1_MockCall Returns(global::System.Func> factory) + { + EnsureSetup().ReturnsRaw(args => (object?)factory((object)args[0]!, (global::DialogParameters)args[1]!)); + return this; + } + #endif + /// Execute a typed callback using the actual method parameters. public IDialogService_ShowDialogAsync_M1_MockCall Callback(global::System.Action callback) { diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Async_Methods.verified.txt b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Async_Methods.verified.txt index 04014b7250..3ec08fa6e4 100644 --- a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Async_Methods.verified.txt +++ b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Async_Methods.verified.txt @@ -299,6 +299,11 @@ public sealed class IAsyncService_GetValueAsync_M0_MockCall : global::TUnit.Mock public IAsyncService_GetValueAsync_M0_MockCall ReturnsAsync(global::System.Threading.Tasks.Task task) { EnsureSetup().ReturnsRaw(task); return this; } /// Return a pre-built Task from a factory, invoked on each call. public IAsyncService_GetValueAsync_M0_MockCall ReturnsAsync(global::System.Func> taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #if NET9_0_OR_GREATER + /// Return a Task from a factory, invoked on each call. The Task is returned as-is, so an async factory stays pending until it completes. + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public IAsyncService_GetValueAsync_M0_MockCall Returns(global::System.Func> taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #endif /// Configure a typed computed return value using the actual method parameters. public IAsyncService_GetValueAsync_M0_MockCall Returns(global::System.Func factory) @@ -314,6 +319,16 @@ public sealed class IAsyncService_GetValueAsync_M0_MockCall : global::TUnit.Mock return this; } + #if NET9_0_OR_GREATER + /// Configure a typed computed async return value using the actual method parameters. The returned task is handed back as-is, so an async factory stays pending until it completes. + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public IAsyncService_GetValueAsync_M0_MockCall Returns(global::System.Func> factory) + { + EnsureSetup().ReturnsRaw(args => (object?)factory((string)args[0]!)); + return this; + } + #endif + /// Execute a typed callback using the actual method parameters. public IAsyncService_GetValueAsync_M0_MockCall Callback(global::System.Action callback) { @@ -398,6 +413,11 @@ public sealed class IAsyncService_DoWorkAsync_M1_MockCall : global::TUnit.Mocks. public IAsyncService_DoWorkAsync_M1_MockCall ReturnsAsync(global::System.Threading.Tasks.Task task) { EnsureSetup().ReturnsRaw(task); return this; } /// Return a pre-built Task from a factory, invoked on each call. public IAsyncService_DoWorkAsync_M1_MockCall ReturnsAsync(global::System.Func taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #if NET9_0_OR_GREATER + /// Return a Task from a factory, invoked on each call. The Task is returned as-is, so an async factory stays pending until it completes. + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public IAsyncService_DoWorkAsync_M1_MockCall Returns(global::System.Func taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #endif // ICallVerification /// @@ -474,6 +494,11 @@ public sealed class IAsyncService_ComputeAsync_M2_MockCall : global::TUnit.Mocks public IAsyncService_ComputeAsync_M2_MockCall ReturnsAsync(global::System.Threading.Tasks.ValueTask task) { EnsureSetup().ReturnsRaw(task); return this; } /// Return a pre-built ValueTask from a factory, invoked on each call. public IAsyncService_ComputeAsync_M2_MockCall ReturnsAsync(global::System.Func> taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #if NET9_0_OR_GREATER + /// Return a ValueTask from a factory, invoked on each call. The ValueTask is returned as-is, so an async factory stays pending until it completes. + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public IAsyncService_ComputeAsync_M2_MockCall Returns(global::System.Func> taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #endif /// Configure a typed computed return value using the actual method parameters. public IAsyncService_ComputeAsync_M2_MockCall Returns(global::System.Func factory) @@ -489,6 +514,16 @@ public sealed class IAsyncService_ComputeAsync_M2_MockCall : global::TUnit.Mocks return this; } + #if NET9_0_OR_GREATER + /// Configure a typed computed async return value using the actual method parameters. The returned task is handed back as-is, so an async factory stays pending until it completes. + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public IAsyncService_ComputeAsync_M2_MockCall Returns(global::System.Func> factory) + { + EnsureSetup().ReturnsRaw(args => (object?)factory((int)args[0]!)); + return this; + } + #endif + /// Execute a typed callback using the actual method parameters. public IAsyncService_ComputeAsync_M2_MockCall Callback(global::System.Action callback) { @@ -575,6 +610,11 @@ public sealed class IAsyncService_InitializeAsync_M3_MockCall : global::TUnit.Mo public IAsyncService_InitializeAsync_M3_MockCall ReturnsAsync(global::System.Threading.Tasks.ValueTask task) { EnsureSetup().ReturnsRaw(task); return this; } /// Return a pre-built ValueTask from a factory, invoked on each call. public IAsyncService_InitializeAsync_M3_MockCall ReturnsAsync(global::System.Func taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #if NET9_0_OR_GREATER + /// Return a ValueTask from a factory, invoked on each call. The ValueTask is returned as-is, so an async factory stays pending until it completes. + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public IAsyncService_InitializeAsync_M3_MockCall Returns(global::System.Func taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #endif /// Execute a typed callback using the actual method parameters. public IAsyncService_InitializeAsync_M3_MockCall Callback(global::System.Action callback) diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Mixed_Members.verified.txt b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Mixed_Members.verified.txt index 17444abbbf..816b9e02e8 100644 --- a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Mixed_Members.verified.txt +++ b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Mixed_Members.verified.txt @@ -313,6 +313,11 @@ public sealed class IService_GetAsync_M3_MockCall : global::TUnit.Mocks.Verifica public IService_GetAsync_M3_MockCall ReturnsAsync(global::System.Threading.Tasks.Task task) { EnsureSetup().ReturnsRaw(task); return this; } /// Return a pre-built Task from a factory, invoked on each call. public IService_GetAsync_M3_MockCall ReturnsAsync(global::System.Func> taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #if NET9_0_OR_GREATER + /// Return a Task from a factory, invoked on each call. The Task is returned as-is, so an async factory stays pending until it completes. + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public IService_GetAsync_M3_MockCall Returns(global::System.Func> taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #endif /// Configure a typed computed return value using the actual method parameters. public IService_GetAsync_M3_MockCall Returns(global::System.Func factory) @@ -328,6 +333,16 @@ public sealed class IService_GetAsync_M3_MockCall : global::TUnit.Mocks.Verifica return this; } + #if NET9_0_OR_GREATER + /// Configure a typed computed async return value using the actual method parameters. The returned task is handed back as-is, so an async factory stays pending until it completes. + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public IService_GetAsync_M3_MockCall Returns(global::System.Func> factory) + { + EnsureSetup().ReturnsRaw(args => (object?)factory((int)args[0]!)); + return this; + } + #endif + /// Execute a typed callback using the actual method parameters. public IService_GetAsync_M3_MockCall Callback(global::System.Action callback) { diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Nullable_Reference_Type_Parameters.verified.txt b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Nullable_Reference_Type_Parameters.verified.txt index bdd52e332d..9215c1a3db 100644 --- a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Nullable_Reference_Type_Parameters.verified.txt +++ b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Nullable_Reference_Type_Parameters.verified.txt @@ -613,6 +613,11 @@ public sealed class IFoo_GetAsync_M3_MockCall : global::TUnit.Mocks.Verification public IFoo_GetAsync_M3_MockCall ReturnsAsync(global::System.Threading.Tasks.Task task) { EnsureSetup().ReturnsRaw(task); return this; } /// Return a pre-built Task from a factory, invoked on each call. public IFoo_GetAsync_M3_MockCall ReturnsAsync(global::System.Func> taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #if NET9_0_OR_GREATER + /// Return a Task from a factory, invoked on each call. The Task is returned as-is, so an async factory stays pending until it completes. + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public IFoo_GetAsync_M3_MockCall Returns(global::System.Func> taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #endif /// Configure a typed computed return value using the actual method parameters. public IFoo_GetAsync_M3_MockCall Returns(global::System.Func factory) @@ -628,6 +633,16 @@ public sealed class IFoo_GetAsync_M3_MockCall : global::TUnit.Mocks.Verification return this; } + #if NET9_0_OR_GREATER + /// Configure a typed computed async return value using the actual method parameters. The returned task is handed back as-is, so an async factory stays pending until it completes. + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public IFoo_GetAsync_M3_MockCall Returns(global::System.Func> factory) + { + EnsureSetup().ReturnsRaw(args => (object?)factory((string?)args[0])); + return this; + } + #endif + /// Execute a typed callback using the actual method parameters. public IFoo_GetAsync_M3_MockCall Callback(global::System.Action callback) { diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Obsolete_Members.verified.txt b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Obsolete_Members.verified.txt index 900901f66b..aaef917862 100644 --- a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Obsolete_Members.verified.txt +++ b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Obsolete_Members.verified.txt @@ -701,6 +701,11 @@ public sealed class IDialogService_ShowPanel_M1_MockCall : global::TUnit. public IDialogService_ShowPanel_M1_MockCall ReturnsAsync(global::System.Threading.Tasks.Task task) { EnsureSetup().ReturnsRaw(task); return this; } /// Return a pre-built Task from a factory, invoked on each call. public IDialogService_ShowPanel_M1_MockCall ReturnsAsync(global::System.Func> taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #if NET9_0_OR_GREATER + /// Return a Task from a factory, invoked on each call. The Task is returned as-is, so an async factory stays pending until it completes. + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public IDialogService_ShowPanel_M1_MockCall Returns(global::System.Func> taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #endif /// Configure a typed computed return value using the actual method parameters. public IDialogService_ShowPanel_M1_MockCall Returns(global::System.Func factory) @@ -716,6 +721,16 @@ public sealed class IDialogService_ShowPanel_M1_MockCall : global::TUnit. return this; } + #if NET9_0_OR_GREATER + /// Configure a typed computed async return value using the actual method parameters. The returned task is handed back as-is, so an async factory stays pending until it completes. + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public IDialogService_ShowPanel_M1_MockCall Returns(global::System.Func> factory) + { + EnsureSetup().ReturnsRaw(args => (object?)factory((TData?)args[0])); + return this; + } + #endif + /// Execute a typed callback using the actual method parameters. public IDialogService_ShowPanel_M1_MockCall Callback(global::System.Action callback) { diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Unconstrained_Nullable_Generic.verified.txt b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Unconstrained_Nullable_Generic.verified.txt index 17a7694609..c5c344d7af 100644 --- a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Unconstrained_Nullable_Generic.verified.txt +++ b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Unconstrained_Nullable_Generic.verified.txt @@ -228,6 +228,11 @@ public sealed class IFoo_DoSomethingAsync_M0_MockCall : global::TUnit.Mocks.V public IFoo_DoSomethingAsync_M0_MockCall ReturnsAsync(global::System.Threading.Tasks.Task task) { EnsureSetup().ReturnsRaw(task); return this; } /// Return a pre-built Task from a factory, invoked on each call. public IFoo_DoSomethingAsync_M0_MockCall ReturnsAsync(global::System.Func> taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #if NET9_0_OR_GREATER + /// Return a Task from a factory, invoked on each call. The Task is returned as-is, so an async factory stays pending until it completes. + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public IFoo_DoSomethingAsync_M0_MockCall Returns(global::System.Func> taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #endif // ICallVerification /// diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Method_With_More_Params_Than_Func_Action_Arity.verified.txt b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Method_With_More_Params_Than_Func_Action_Arity.verified.txt index 789f61b4af..fb82684d60 100644 --- a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Method_With_More_Params_Than_Func_Action_Arity.verified.txt +++ b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Method_With_More_Params_Than_Func_Action_Arity.verified.txt @@ -204,6 +204,11 @@ public sealed class ILongMethodSignatures_SomeMethod_M0_MockCall : global::TUnit public ILongMethodSignatures_SomeMethod_M0_MockCall ReturnsAsync(global::System.Threading.Tasks.Task task) { EnsureSetup().ReturnsRaw(task); return this; } /// Return a pre-built Task from a factory, invoked on each call. public ILongMethodSignatures_SomeMethod_M0_MockCall ReturnsAsync(global::System.Func taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #if NET9_0_OR_GREATER + /// Return a Task from a factory, invoked on each call. The Task is returned as-is, so an async factory stays pending until it completes. + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public ILongMethodSignatures_SomeMethod_M0_MockCall Returns(global::System.Func taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #endif // ICallVerification /// diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Returning_Async_Method_With_More_Params_Than_Func_Action_Arity.verified.txt b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Returning_Async_Method_With_More_Params_Than_Func_Action_Arity.verified.txt index 9f0ad9042b..2e68036166 100644 --- a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Returning_Async_Method_With_More_Params_Than_Func_Action_Arity.verified.txt +++ b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Returning_Async_Method_With_More_Params_Than_Func_Action_Arity.verified.txt @@ -207,6 +207,11 @@ public sealed class ILongReturningSignature_Sum_M0_MockCall : global::TUnit.Mock public ILongReturningSignature_Sum_M0_MockCall ReturnsAsync(global::System.Threading.Tasks.Task task) { EnsureSetup().ReturnsRaw(task); return this; } /// Return a pre-built Task from a factory, invoked on each call. public ILongReturningSignature_Sum_M0_MockCall ReturnsAsync(global::System.Func> taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #if NET9_0_OR_GREATER + /// Return a Task from a factory, invoked on each call. The Task is returned as-is, so an async factory stays pending until it completes. + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public ILongReturningSignature_Sum_M0_MockCall Returns(global::System.Func> taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; } + #endif // ICallVerification /// diff --git a/tests/TUnit.Mocks.Tests/Issue6495Tests.cs b/tests/TUnit.Mocks.Tests/Issue6495Tests.cs new file mode 100644 index 0000000000..eb3bc4c51f --- /dev/null +++ b/tests/TUnit.Mocks.Tests/Issue6495Tests.cs @@ -0,0 +1,198 @@ +using System.Diagnostics; +using TUnit.Mocks; + +namespace TUnit.Mocks.Tests; + +// Regression: https://github.com/thomhurst/TUnit/issues/6495 +// `.Returns()` on an async member only accepted a synchronous Func, so an `async () => ...` +// lambda failed with CS4010 and the only workaround (Thread.Sleep) blocked the caller before the +// task was ever handed back — making it impossible to race a mocked call against a timeout. + +public interface ISlowService +{ + Task GetValueAsync(); + + Task GetValueAsync(int seed); + + ValueTask GetValueValueTaskAsync(); + + Task DoWorkAsync(); +} + +// A reference-typed async result: null and throw-expression lambda bodies convert to both T and +// Task, so the two Returns factory overloads must not become an ambiguity (CS0121). +public interface IReferenceResultService +{ + Task GetNameAsync(); + + Task GetNameAsync(int id); +} + +public class Issue6495Tests +{ + private const int NeverInTestTimeMs = 30_000; + +#if NET9_0_OR_GREATER + // The async-factory Returns overload carries [OverloadResolutionPriority], which only reaches + // a consumer's compilation on net9.0+ — so the overload, and these tests, are net9.0+ only. + // net8.0 consumers keep ReturnsAsync, covered by ReturnsAsync_Factory_Is_Unchanged below. + + [Test] + public async Task Returns_Async_Lambda_Stays_Pending_So_A_Timeout_Can_Win() + { + var mock = ISlowService.Mock(); + mock.GetValueAsync().Returns(async () => + { + await Task.Delay(NeverInTestTimeMs); + return 42; + }); + + var call = mock.Object.GetValueAsync(); + var timeout = Task.Delay(50); + + // The mocked call must come back incomplete, or the timeout branch could never win. + await Assert.That(call.IsCompleted).IsFalse(); + await Assert.That(await Task.WhenAny(call, timeout)).IsSameReferenceAs(timeout); + } + + [Test] + public async Task Returns_Async_Lambda_Still_Produces_The_Value() + { + var mock = ISlowService.Mock(); + mock.GetValueAsync().Returns(async () => + { + await Task.Yield(); + return 42; + }); + + await Assert.That(await mock.Object.GetValueAsync()).IsEqualTo(42); + } + + [Test] + public async Task Returns_Async_Lambda_Is_Invoked_Per_Call() + { + var calls = 0; + var mock = ISlowService.Mock(); + mock.GetValueAsync().Returns(async () => + { + await Task.Yield(); + return ++calls; + }); + + await Assert.That(await mock.Object.GetValueAsync()).IsEqualTo(1); + await Assert.That(await mock.Object.GetValueAsync()).IsEqualTo(2); + } + + [Test] + public async Task Returns_Async_Lambda_With_Parameters() + { + var mock = ISlowService.Mock(); + mock.GetValueAsync(Arg.Any()).Returns(async seed => + { + await Task.Yield(); + return seed * 2; + }); + + await Assert.That(await mock.Object.GetValueAsync(21)).IsEqualTo(42); + } + + [Test] + public async Task Returns_Async_Lambda_On_ValueTask_Member() + { + var mock = ISlowService.Mock(); + mock.GetValueValueTaskAsync().Returns(async () => + { + await Task.Yield(); + return 7; + }); + + await Assert.That(await mock.Object.GetValueValueTaskAsync()).IsEqualTo(7); + } + + [Test] + public async Task Returns_Async_Lambda_On_Task_Returning_Member_Stays_Pending() + { + var mock = ISlowService.Mock(); + mock.DoWorkAsync().Returns(async () => await Task.Delay(NeverInTestTimeMs)); + + var call = mock.Object.DoWorkAsync(); + + await Assert.That(call.IsCompleted).IsFalse(); + } + +#endif + + [Test] + public async Task Synchronous_Returns_Factory_Still_Works() + { + var mock = ISlowService.Mock(); + mock.GetValueAsync().Returns(() => 42); + + await Assert.That(await mock.Object.GetValueAsync()).IsEqualTo(42); + } + + [Test] + public async Task Null_Returning_Lambda_Still_Binds_To_The_Synchronous_Factory() + { + // `() => null` converts to both Func and Func>. The async overload + // is deprioritised, so this keeps its pre-existing meaning: null is the *value*, and the + // member still returns a completed task. + var mock = IReferenceResultService.Mock(); + mock.GetNameAsync().Returns(() => null); + + var call = mock.Object.GetNameAsync(); + + await Assert.That(call.IsCompleted).IsTrue(); + await Assert.That(await call).IsNull(); + } + + [Test] + public async Task Throwing_Lambda_Still_Binds_To_The_Synchronous_Factory() + { + var mock = IReferenceResultService.Mock(); + mock.GetNameAsync().Returns(() => throw new InvalidOperationException("boom")); + + await Assert.That(async () => await mock.Object.GetNameAsync()) + .Throws(); + } + + [Test] + public async Task Null_Returning_Typed_Lambda_Still_Binds_To_The_Synchronous_Factory() + { + var mock = IReferenceResultService.Mock(); + mock.GetNameAsync(Arg.Any()).Returns(id => null); + + await Assert.That(await mock.Object.GetNameAsync(1)).IsNull(); + } + +#if NET9_0_OR_GREATER + [Test] + public async Task Async_Lambda_Still_Binds_On_A_Reference_Typed_Result() + { + var mock = IReferenceResultService.Mock(); + mock.GetNameAsync().Returns(async () => + { + await Task.Delay(NeverInTestTimeMs); + return "late"; + }); + + var call = mock.Object.GetNameAsync(); + + await Assert.That(call.IsCompleted).IsFalse(); + } + +#endif + + [Test] + public async Task ReturnsAsync_Factory_Is_Unchanged() + { + var mock = ISlowService.Mock(); + mock.GetValueAsync().ReturnsAsync(async () => + { + await Task.Yield(); + return 42; + }); + + await Assert.That(await mock.Object.GetValueAsync()).IsEqualTo(42); + } +}