Skip to content

Commit e905038

Browse files
committed
docs(mocks): document arity-constant relationship + returning-async snapshot (#6254)
Addresses PR review feedback: - Document the two-level gating between MaxTypedParams (controls whether a non-async method gets a typed wrapper at all) and MaxDelegateParams (the BCL Func<>/Action<> ceiling enforced at the typed-overload emit sites for every wrapper). Comment added at the ShouldGenerateTypedWrapper async short-circuit and the MaxTypedParams declaration. Comment-only; generator output unchanged. - Add a snapshot for the returning-async (Task<T>) path at the 17-param boundary. This exercises the value-returning unified-class emit site (distinct from the void-Task path already covered) and confirms the typed overloads are omitted while the untyped Returns(int)/ReturnsAsync(Task<int>) surface is kept.
1 parent e2ba5fc commit e905038

3 files changed

Lines changed: 299 additions & 1 deletion

File tree

TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1322,6 +1322,35 @@ void M()
13221322
return VerifyGeneratorOutput(source);
13231323
}
13241324

1325+
[Test]
1326+
public Task Returning_Async_Method_With_More_Params_Than_Func_Action_Arity()
1327+
{
1328+
// Issue #6254, returning-async (Task<T>) variant. This routes through the value-returning
1329+
// unified-class emit path (distinct from the void-Task path above), which has its own
1330+
// arity gate. At 17 params — one past the BCL Func<>/Action<> limit — the typed overloads
1331+
// must be omitted while the untyped Returns/ReturnsAsync surface is retained.
1332+
var source = """
1333+
using System.Threading.Tasks;
1334+
using TUnit.Mocks;
1335+
1336+
public interface ILongReturningSignature
1337+
{
1338+
Task<int> Sum(int _1, int _2, int _3, int _4, int _5, int _6, int _7, int _8, int _9,
1339+
int _10, int _11, int _12, int _13, int _14, int _15, int _16, int _17);
1340+
}
1341+
1342+
public class TestUsage
1343+
{
1344+
void M()
1345+
{
1346+
var mock = Mock.Of<ILongReturningSignature>();
1347+
}
1348+
}
1349+
""";
1350+
1351+
return VerifyGeneratorOutput(source);
1352+
}
1353+
13251354
[Test]
13261355
public Task GenerateMock_Attribute_With_Concrete_Class()
13271356
{
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
// <auto-generated/>
2+
#pragma warning disable
3+
#nullable enable
4+
5+
public sealed class ILongReturningSignatureMock : global::TUnit.Mocks.Mock<global::ILongReturningSignature>, global::ILongReturningSignature
6+
{
7+
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
8+
internal ILongReturningSignatureMock(global::ILongReturningSignature mockObject, global::TUnit.Mocks.MockEngine<global::ILongReturningSignature> engine)
9+
: base(mockObject, engine) { }
10+
11+
global::System.Threading.Tasks.Task<int> global::ILongReturningSignature.Sum(int _1, int _2, int _3, int _4, int _5, int _6, int _7, int _8, int _9, int _10, int _11, int _12, int _13, int _14, int _15, int _16, int _17) => Object.Sum(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17);
12+
}
13+
14+
15+
// ===== FILE SEPARATOR =====
16+
17+
// <auto-generated/>
18+
#pragma warning disable
19+
#nullable enable
20+
21+
file sealed class ILongReturningSignatureMockImpl : global::ILongReturningSignature, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject
22+
{
23+
private readonly global::TUnit.Mocks.MockEngine<global::ILongReturningSignature> _engine;
24+
25+
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
26+
global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; }
27+
28+
internal ILongReturningSignatureMockImpl(global::TUnit.Mocks.MockEngine<global::ILongReturningSignature> engine)
29+
{
30+
_engine = engine;
31+
}
32+
33+
public global::System.Threading.Tasks.Task<int> Sum(int _1, int _2, int _3, int _4, int _5, int _6, int _7, int _8, int _9, int _10, int _11, int _12, int _13, int _14, int _15, int _16, int _17)
34+
{
35+
try
36+
{
37+
var __result = _engine.HandleCallWithReturn<int>(0, "Sum", new object?[] { _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17 }, default);
38+
if (global::TUnit.Mocks.Setup.RawReturnContext.TryConsume(out var __rawAsync))
39+
{
40+
if (__rawAsync is global::System.Threading.Tasks.Task<int> __typedAsync) return __typedAsync;
41+
throw new global::System.InvalidOperationException($"ReturnsAsync: expected global::System.Threading.Tasks.Task<int> but got {__rawAsync?.GetType().Name ?? "null"}");
42+
}
43+
return global::System.Threading.Tasks.Task.FromResult<int>(__result);
44+
}
45+
catch (global::System.Exception __ex)
46+
{
47+
return global::System.Threading.Tasks.Task.FromException<int>(__ex);
48+
}
49+
}
50+
51+
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
52+
public void RaiseEvent(string eventName, object? args)
53+
{
54+
throw new global::System.InvalidOperationException($"No event named '{eventName}' exists on this mock.");
55+
}
56+
}
57+
58+
internal static class ILongReturningSignatureMockFactory
59+
{
60+
[global::System.Runtime.CompilerServices.ModuleInitializer]
61+
internal static void Register()
62+
{
63+
global::TUnit.Mocks.MockRegistry.RegisterFactory<global::ILongReturningSignature>(Create);
64+
}
65+
66+
internal static global::TUnit.Mocks.Mock<global::ILongReturningSignature> CreateAutoMock(global::TUnit.Mocks.MockBehavior behavior)
67+
{
68+
var engine = new global::TUnit.Mocks.MockEngine<global::ILongReturningSignature>(behavior);
69+
var impl = new ILongReturningSignatureMockImpl(engine);
70+
engine.Raisable = impl;
71+
var mock = new ILongReturningSignatureMock(impl, engine);
72+
return mock;
73+
}
74+
75+
internal static global::TUnit.Mocks.Mock<global::ILongReturningSignature> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)
76+
{
77+
if (constructorArgs.Length > 0) throw new global::System.ArgumentException($"Interface mock 'global::ILongReturningSignature' does not support constructor arguments, but {constructorArgs.Length} were provided.");
78+
var engine = new global::TUnit.Mocks.MockEngine<global::ILongReturningSignature>(behavior);
79+
var impl = new ILongReturningSignatureMockImpl(engine);
80+
engine.Raisable = impl;
81+
var mock = new ILongReturningSignatureMock(impl, engine);
82+
return mock;
83+
}
84+
}
85+
86+
87+
// ===== FILE SEPARATOR =====
88+
89+
// <auto-generated/>
90+
#pragma warning disable
91+
#nullable enable
92+
93+
public static class ILongReturningSignature_MockMemberExtensions
94+
{
95+
public static ILongReturningSignature_Sum_M0_MockCall Sum(this global::TUnit.Mocks.Mock<global::ILongReturningSignature> mock, global::TUnit.Mocks.Arguments.Arg<int> _1, global::TUnit.Mocks.Arguments.Arg<int> _2, global::TUnit.Mocks.Arguments.Arg<int> _3, global::TUnit.Mocks.Arguments.Arg<int> _4, global::TUnit.Mocks.Arguments.Arg<int> _5, global::TUnit.Mocks.Arguments.Arg<int> _6, global::TUnit.Mocks.Arguments.Arg<int> _7, global::TUnit.Mocks.Arguments.Arg<int> _8, global::TUnit.Mocks.Arguments.Arg<int> _9, global::TUnit.Mocks.Arguments.Arg<int> _10, global::TUnit.Mocks.Arguments.Arg<int> _11, global::TUnit.Mocks.Arguments.Arg<int> _12, global::TUnit.Mocks.Arguments.Arg<int> _13, global::TUnit.Mocks.Arguments.Arg<int> _14, global::TUnit.Mocks.Arguments.Arg<int> _15, global::TUnit.Mocks.Arguments.Arg<int> _16, global::TUnit.Mocks.Arguments.Arg<int> _17)
96+
{
97+
var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { _1.Matcher, _2.Matcher, _3.Matcher, _4.Matcher, _5.Matcher, _6.Matcher, _7.Matcher, _8.Matcher, _9.Matcher, _10.Matcher, _11.Matcher, _12.Matcher, _13.Matcher, _14.Matcher, _15.Matcher, _16.Matcher, _17.Matcher };
98+
return new ILongReturningSignature_Sum_M0_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "Sum", matchers);
99+
}
100+
101+
/// <summary>Configure the mock setup for <c>Sum</c> with every argument matched as <c>Any&lt;T&gt;()</c>.</summary>
102+
public static ILongReturningSignature_Sum_M0_MockCall Sum(this global::TUnit.Mocks.Mock<global::ILongReturningSignature> mock, global::TUnit.Mocks.Arguments.AnyArgs _)
103+
{
104+
var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { global::TUnit.Mocks.Matchers.AnyMatcher<int>.Instance, global::TUnit.Mocks.Matchers.AnyMatcher<int>.Instance, global::TUnit.Mocks.Matchers.AnyMatcher<int>.Instance, global::TUnit.Mocks.Matchers.AnyMatcher<int>.Instance, global::TUnit.Mocks.Matchers.AnyMatcher<int>.Instance, global::TUnit.Mocks.Matchers.AnyMatcher<int>.Instance, global::TUnit.Mocks.Matchers.AnyMatcher<int>.Instance, global::TUnit.Mocks.Matchers.AnyMatcher<int>.Instance, global::TUnit.Mocks.Matchers.AnyMatcher<int>.Instance, global::TUnit.Mocks.Matchers.AnyMatcher<int>.Instance, global::TUnit.Mocks.Matchers.AnyMatcher<int>.Instance, global::TUnit.Mocks.Matchers.AnyMatcher<int>.Instance, global::TUnit.Mocks.Matchers.AnyMatcher<int>.Instance, global::TUnit.Mocks.Matchers.AnyMatcher<int>.Instance, global::TUnit.Mocks.Matchers.AnyMatcher<int>.Instance, global::TUnit.Mocks.Matchers.AnyMatcher<int>.Instance, global::TUnit.Mocks.Matchers.AnyMatcher<int>.Instance };
105+
return new ILongReturningSignature_Sum_M0_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "Sum", matchers);
106+
}
107+
108+
#if NET9_0_OR_GREATER
109+
[global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)]
110+
public static void Reset(this global::TUnit.Mocks.Mock<global::ILongReturningSignature> mock)
111+
=> global::TUnit.Mocks.Mock.Reset(mock);
112+
113+
[global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)]
114+
public static void VerifyAll(this global::TUnit.Mocks.Mock<global::ILongReturningSignature> mock)
115+
=> global::TUnit.Mocks.Mock.VerifyAll(mock);
116+
117+
[global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)]
118+
public static void VerifyNoOtherCalls(this global::TUnit.Mocks.Mock<global::ILongReturningSignature> mock)
119+
=> global::TUnit.Mocks.Mock.VerifyNoOtherCalls(mock);
120+
121+
[global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)]
122+
public static void SetupAllProperties(this global::TUnit.Mocks.Mock<global::ILongReturningSignature> mock)
123+
=> global::TUnit.Mocks.Mock.SetupAllProperties(mock);
124+
125+
[global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)]
126+
public static global::TUnit.Mocks.Diagnostics.MockDiagnostics GetDiagnostics(this global::TUnit.Mocks.Mock<global::ILongReturningSignature> mock)
127+
=> global::TUnit.Mocks.Mock.GetDiagnostics(mock);
128+
129+
[global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)]
130+
public static void SetState(this global::TUnit.Mocks.Mock<global::ILongReturningSignature> mock, string? stateName)
131+
=> global::TUnit.Mocks.Mock.SetState(mock, stateName);
132+
133+
[global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)]
134+
public static void InState(this global::TUnit.Mocks.Mock<global::ILongReturningSignature> mock, string stateName, global::System.Action<global::TUnit.Mocks.Mock<global::ILongReturningSignature>> configure)
135+
=> global::TUnit.Mocks.Mock.InState(mock, stateName, configure);
136+
137+
extension(global::TUnit.Mocks.Mock<global::ILongReturningSignature> mock)
138+
{
139+
[global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)]
140+
public global::System.Collections.Generic.IReadOnlyList<global::TUnit.Mocks.Verification.CallRecord> Invocations => global::TUnit.Mocks.Mock.Invocations(mock);
141+
142+
[global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)]
143+
public global::TUnit.Mocks.MockBehavior Behavior => global::TUnit.Mocks.Mock.Behavior(mock);
144+
145+
[global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)]
146+
public global::TUnit.Mocks.IDefaultValueProvider? DefaultValueProvider
147+
{
148+
get => global::TUnit.Mocks.Mock.GetDefaultValueProvider(mock);
149+
set => global::TUnit.Mocks.Mock.SetDefaultValueProvider(mock, value);
150+
}
151+
}
152+
#endif
153+
}
154+
155+
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
156+
public sealed class ILongReturningSignature_Sum_M0_MockCall : global::TUnit.Mocks.Verification.ICallVerification
157+
{
158+
private readonly global::TUnit.Mocks.IMockEngineAccess _engine;
159+
private readonly int _memberId;
160+
private readonly string _memberName;
161+
private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers;
162+
private global::TUnit.Mocks.Setup.MethodSetupBuilder<int>? _builder;
163+
164+
internal ILongReturningSignature_Sum_M0_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers)
165+
{
166+
_engine = engine;
167+
_memberId = memberId;
168+
_memberName = memberName;
169+
_matchers = matchers;
170+
}
171+
172+
private global::TUnit.Mocks.Setup.MethodSetupBuilder<int> EnsureSetup()
173+
{
174+
var existing = global::System.Threading.Volatile.Read(ref _builder);
175+
if (existing is not null) return existing;
176+
return EnsureSetupSlow();
177+
}
178+
179+
[global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
180+
private global::TUnit.Mocks.Setup.MethodSetupBuilder<int> EnsureSetupSlow()
181+
{
182+
var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName);
183+
var fresh = new global::TUnit.Mocks.Setup.MethodSetupBuilder<int>(setup);
184+
var prev = global::System.Threading.Interlocked.CompareExchange(ref _builder, fresh, null);
185+
if (prev is not null) return prev;
186+
// AddSetup runs only on the CAS winner. Setup is sequential in practice,
187+
// so a concurrent loser observing the builder before registration is benign.
188+
_engine.AddSetup(setup);
189+
return fresh;
190+
}
191+
192+
/// <inheritdoc />
193+
public ILongReturningSignature_Sum_M0_MockCall Returns(int value) { EnsureSetup().Returns(value); return this; }
194+
/// <inheritdoc />
195+
public ILongReturningSignature_Sum_M0_MockCall Returns(global::System.Func<int> factory) { EnsureSetup().Returns(factory); return this; }
196+
/// <inheritdoc />
197+
public ILongReturningSignature_Sum_M0_MockCall ReturnsSequentially(params int[] values) { EnsureSetup().ReturnsSequentially(values); return this; }
198+
/// <inheritdoc />
199+
public ILongReturningSignature_Sum_M0_MockCall Throws<TException>() where TException : global::System.Exception, new() { EnsureSetup().Throws<TException>(); return this; }
200+
/// <inheritdoc />
201+
public ILongReturningSignature_Sum_M0_MockCall Throws(global::System.Exception exception) { EnsureSetup().Throws(exception); return this; }
202+
/// <inheritdoc />
203+
public ILongReturningSignature_Sum_M0_MockCall Callback(global::System.Action callback) { EnsureSetup().Callback(callback); return this; }
204+
/// <inheritdoc />
205+
public ILongReturningSignature_Sum_M0_MockCall TransitionsTo(string stateName) { EnsureSetup().TransitionsTo(stateName); return this; }
206+
/// <inheritdoc />
207+
public ILongReturningSignature_Sum_M0_MockCall Then() { EnsureSetup().Then(); return this; }
208+
209+
/// <summary>Return a pre-built Task directly (e.g., from a TaskCompletionSource).</summary>
210+
public ILongReturningSignature_Sum_M0_MockCall ReturnsAsync(global::System.Threading.Tasks.Task<int> task) { EnsureSetup().ReturnsRaw(task); return this; }
211+
/// <summary>Return a pre-built Task from a factory, invoked on each call.</summary>
212+
public ILongReturningSignature_Sum_M0_MockCall ReturnsAsync(global::System.Func<global::System.Threading.Tasks.Task<int>> taskFactory) { EnsureSetup().ReturnsRaw(() => (object?)taskFactory()); return this; }
213+
214+
// ICallVerification
215+
/// <inheritdoc />
216+
public void WasCalled() => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled();
217+
/// <inheritdoc />
218+
public void WasCalled(global::TUnit.Mocks.Times times) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times);
219+
/// <inheritdoc />
220+
public void WasCalled(global::TUnit.Mocks.Times times, string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times, message);
221+
/// <inheritdoc />
222+
public void WasCalled(string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(message);
223+
/// <inheritdoc />
224+
public void WasNeverCalled() => _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled();
225+
/// <inheritdoc />
226+
public void WasNeverCalled(string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(message);
227+
}
228+
229+
230+
// ===== FILE SEPARATOR =====
231+
232+
// <auto-generated/>
233+
#pragma warning disable
234+
#nullable enable
235+
236+
namespace TUnit.Mocks
237+
{
238+
public static class ILongReturningSignature_MockStaticExtension
239+
{
240+
extension(global::ILongReturningSignature _)
241+
{
242+
public static global::ILongReturningSignatureMock Mock()
243+
{
244+
return (global::ILongReturningSignatureMock)global::ILongReturningSignatureMockFactory.CreateAutoMock(global::TUnit.Mocks.Mock.DefaultBehavior);
245+
}
246+
247+
public static global::ILongReturningSignatureMock Mock(global::TUnit.Mocks.MockBehavior behavior)
248+
{
249+
return (global::ILongReturningSignatureMock)global::ILongReturningSignatureMockFactory.CreateAutoMock(behavior);
250+
}
251+
}
252+
}
253+
}
254+
255+
256+
// ===== FILE SEPARATOR =====
257+
258+
// <auto-generated/>
259+
#pragma warning disable
260+
#nullable enable
261+
262+
namespace TUnit.Mocks.Generated;

TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ namespace TUnit.Mocks.SourceGenerator.Builders;
1515
/// </summary>
1616
internal static class MockMembersBuilder
1717
{
18+
// Non-async methods only get a typed wrapper (and thus typed parameter overloads) up to this
19+
// many matchable params; past it they fall back to the untyped setup surface. Must stay
20+
// <= MaxDelegateParams so the typed overloads it permits never exceed the BCL delegate arity.
1821
private const int MaxTypedParams = 8;
1922
private const int MaxFuncOverloadParams = 4;
2023

@@ -268,7 +271,11 @@ private static void EmitOutParamDefaults(CodeWriter writer, MockMemberModel meth
268271

269272
private static bool ShouldGenerateTypedWrapper(MockMemberModel method, MockTypeModel model, bool hasEvents)
270273
{
271-
// Async methods need a typed wrapper for the generated ReturnsAsync() method
274+
// Async methods need a typed wrapper for the generated ReturnsAsync() method, so they are
275+
// emitted regardless of parameter count (bypassing the MaxTypedParams ceiling below). The
276+
// typed parameter overloads inside the wrapper are still bounded by MaxDelegateParams at
277+
// their emit sites (see CanEmitTypedParamOverloads), which is what keeps Func<>/Action<>
278+
// within the BCL arity limit for high-parameter-count methods.
272279
if (method.IsAsync) return true;
273280

274281
// Exclude out params and ref struct params (can't be boxed or used as type args)

0 commit comments

Comments
 (0)