diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_FluentUI_Shape_Nullable_Warnings.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_FluentUI_Shape_Nullable_Warnings.verified.txt index 5542924cb6..a35b30151c 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_FluentUI_Shape_Nullable_Warnings.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_FluentUI_Shape_Nullable_Warnings.verified.txt @@ -34,7 +34,7 @@ file sealed class IDialogReferenceMockImpl : global::IDialogReference, global::T { try { - var __result = _engine.HandleCallWithReturn(0, "GetReturnValueAsync", global::System.Array.Empty(), default); + var __result = _engine.HandleCallWithReturn(0, "GetReturnValueAsync", global::System.Array.Empty(), default, global::TUnit.Mocks.TypeArguments.Of.Value); if (global::TUnit.Mocks.Setup.RawReturnContext.TryConsume(out var __rawAsync)) { if (__rawAsync is global::System.Threading.Tasks.Task __typedAsync) return __typedAsync; @@ -95,7 +95,7 @@ public static class IDialogReference_MockMemberExtensions public static global::TUnit.Mocks.MockMethodCall GetReturnValueAsync(this global::TUnit.Mocks.Mock mock) { var matchers = global::System.Array.Empty(); - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "GetReturnValueAsync", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "GetReturnValueAsync", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } #if NET9_0_OR_GREATER @@ -238,7 +238,7 @@ file sealed class IDialogServiceMockImpl : global::IDialogService, global::TUnit { try { - var __result = _engine.HandleCallWithReturn>(0, "UpdateDialogAsync", id, parameters, default, static __behavior => global::IDialogReferenceMockFactory.CreateAutoMock(__behavior)); + var __result = _engine.HandleCallWithReturn(0, "UpdateDialogAsync", new object?[] { id, parameters }, default, static __behavior => global::IDialogReferenceMockFactory.CreateAutoMock(__behavior), global::TUnit.Mocks.TypeArguments.Of.Value); if (global::TUnit.Mocks.Setup.RawReturnContext.TryConsume(out var __rawAsync)) { if (__rawAsync is global::System.Threading.Tasks.Task __typedAsync) return __typedAsync; @@ -256,7 +256,7 @@ file sealed class IDialogServiceMockImpl : global::IDialogService, global::TUnit { try { - var __result = _engine.HandleCallWithReturn(1, "ShowDialogAsync", data, parameters, default!, static __behavior => global::IDialogReferenceMockFactory.CreateAutoMock(__behavior)); + var __result = _engine.HandleCallWithReturn(1, "ShowDialogAsync", new object?[] { data, parameters }, default!, static __behavior => global::IDialogReferenceMockFactory.CreateAutoMock(__behavior), global::TUnit.Mocks.TypeArguments.Of.Value); if (global::TUnit.Mocks.Setup.RawReturnContext.TryConsume(out var __rawAsync)) { if (__rawAsync is global::System.Threading.Tasks.Task __typedAsync) return __typedAsync; @@ -347,21 +347,21 @@ public static class IDialogService_MockMemberExtensions public static global::TUnit.Mocks.MockMethodCall UpdateDialogAsync(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg id, global::TUnit.Mocks.Arguments.Arg> parameters) where TData : class { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { id.Matcher, parameters.Matcher }; - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "UpdateDialogAsync", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "UpdateDialogAsync", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.MockMethodCall UpdateDialogAsync(this global::TUnit.Mocks.Mock mock, global::System.Func id, global::TUnit.Mocks.Arguments.Arg> parameters) where TData : class { global::TUnit.Mocks.Arguments.Arg __fa_id = id; var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_id.Matcher, parameters.Matcher }; - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "UpdateDialogAsync", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "UpdateDialogAsync", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.MockMethodCall UpdateDialogAsync(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg id, global::System.Func, bool> parameters) where TData : class { global::TUnit.Mocks.Arguments.Arg> __fa_parameters = parameters; var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { id.Matcher, __fa_parameters.Matcher }; - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "UpdateDialogAsync", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "UpdateDialogAsync", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.MockMethodCall UpdateDialogAsync(this global::TUnit.Mocks.Mock mock, global::System.Func id, global::System.Func, bool> parameters) where TData : class @@ -369,27 +369,27 @@ public static class IDialogService_MockMemberExtensions global::TUnit.Mocks.Arguments.Arg __fa_id = id; global::TUnit.Mocks.Arguments.Arg> __fa_parameters = parameters; var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_id.Matcher, __fa_parameters.Matcher }; - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "UpdateDialogAsync", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "UpdateDialogAsync", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.MockMethodCall ShowDialogAsync(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg data, global::TUnit.Mocks.Arguments.Arg parameters) where TDialog : global::IDialogContentComponent { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { data.Matcher, parameters.Matcher }; - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "ShowDialogAsync", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "ShowDialogAsync", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.MockMethodCall ShowDialogAsync(this global::TUnit.Mocks.Mock mock, global::System.Func data, global::TUnit.Mocks.Arguments.Arg parameters) where TDialog : global::IDialogContentComponent { global::TUnit.Mocks.Arguments.Arg __fa_data = data; var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_data.Matcher, parameters.Matcher }; - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "ShowDialogAsync", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "ShowDialogAsync", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.MockMethodCall ShowDialogAsync(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg data, global::System.Func parameters) where TDialog : global::IDialogContentComponent { global::TUnit.Mocks.Arguments.Arg __fa_parameters = parameters; var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { data.Matcher, __fa_parameters.Matcher }; - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "ShowDialogAsync", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "ShowDialogAsync", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.MockMethodCall ShowDialogAsync(this global::TUnit.Mocks.Mock mock, global::System.Func data, global::System.Func parameters) where TDialog : global::IDialogContentComponent @@ -397,7 +397,7 @@ public static class IDialogService_MockMemberExtensions global::TUnit.Mocks.Arguments.Arg __fa_data = data; global::TUnit.Mocks.Arguments.Arg __fa_parameters = parameters; var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_data.Matcher, __fa_parameters.Matcher }; - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "ShowDialogAsync", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "ShowDialogAsync", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static void RaiseOnShow(this global::TUnit.Mocks.Mock mock, global::IDialogReference arg1, global::System.Type? arg2, global::DialogParameters arg3, object arg4) diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Generic_Method_Constraints_On_Explicit_Impl.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Generic_Method_Constraints_On_Explicit_Impl.verified.txt index a3664f9148..fc53db6070 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Generic_Method_Constraints_On_Explicit_Impl.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Generic_Method_Constraints_On_Explicit_Impl.verified.txt @@ -42,32 +42,32 @@ file sealed class IConstrainedMockImpl : global::IConstrained, global::TUnit.Moc public T GetNotnull(string key) where T : notnull { - return _engine.HandleCallWithReturn(0, "GetNotnull", key, default!); + return _engine.HandleCallWithReturn(0, "GetNotnull", new object?[] { key }, default!, global::TUnit.Mocks.TypeArguments.Of.Value); } public T GetNew() where T : new() { - return _engine.HandleCallWithReturn(1, "GetNew", global::System.Array.Empty(), default!); + return _engine.HandleCallWithReturn(1, "GetNew", global::System.Array.Empty(), default!, global::TUnit.Mocks.TypeArguments.Of.Value); } public T GetUnmanaged() where T : struct, unmanaged { - return _engine.HandleCallWithReturn(2, "GetUnmanaged", global::System.Array.Empty(), default); + return _engine.HandleCallWithReturn(2, "GetUnmanaged", global::System.Array.Empty(), default, global::TUnit.Mocks.TypeArguments.Of.Value); } public T GetDisposable() where T : global::System.IDisposable { - return _engine.HandleCallWithReturn(3, "GetDisposable", global::System.Array.Empty(), default!); + return _engine.HandleCallWithReturn(3, "GetDisposable", global::System.Array.Empty(), default!, global::TUnit.Mocks.TypeArguments.Of.Value); } public T GetClassNew() where T : class, global::System.IDisposable, new() { - return _engine.HandleCallWithReturn(4, "GetClassNew", global::System.Array.Empty(), default!); + return _engine.HandleCallWithReturn(4, "GetClassNew", global::System.Array.Empty(), default!, global::TUnit.Mocks.TypeArguments.Of.Value); } public T GetStructDisposable() where T : struct, global::System.IDisposable { - return _engine.HandleCallWithReturn(5, "GetStructDisposable", global::System.Array.Empty(), default); + return _engine.HandleCallWithReturn(5, "GetStructDisposable", global::System.Array.Empty(), default, global::TUnit.Mocks.TypeArguments.Of.Value); } [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] @@ -117,44 +117,44 @@ public static class IConstrained_MockMemberExtensions public static global::TUnit.Mocks.MockMethodCall GetNotnull(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg key) where T : notnull { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { key.Matcher }; - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "GetNotnull", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "GetNotnull", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.MockMethodCall GetNotnull(this global::TUnit.Mocks.Mock mock, global::System.Func key) where T : notnull { global::TUnit.Mocks.Arguments.Arg __fa_key = key; var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_key.Matcher }; - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "GetNotnull", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "GetNotnull", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.MockMethodCall GetNew(this global::TUnit.Mocks.Mock mock) where T : new() { var matchers = global::System.Array.Empty(); - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "GetNew", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "GetNew", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.MockMethodCall GetUnmanaged(this global::TUnit.Mocks.Mock mock) where T : struct, unmanaged { var matchers = global::System.Array.Empty(); - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "GetUnmanaged", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "GetUnmanaged", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.MockMethodCall GetDisposable(this global::TUnit.Mocks.Mock mock) where T : global::System.IDisposable { var matchers = global::System.Array.Empty(); - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 3, "GetDisposable", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 3, "GetDisposable", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.MockMethodCall GetClassNew(this global::TUnit.Mocks.Mock mock) where T : class, global::System.IDisposable, new() { var matchers = global::System.Array.Empty(); - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 4, "GetClassNew", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 4, "GetClassNew", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.MockMethodCall GetStructDisposable(this global::TUnit.Mocks.Mock mock) where T : struct, global::System.IDisposable { var matchers = global::System.Array.Empty(); - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 5, "GetStructDisposable", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 5, "GetStructDisposable", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } #if NET9_0_OR_GREATER diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Generic_Methods.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Generic_Methods.verified.txt index caa923f50f..7bc89051f5 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Generic_Methods.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Generic_Methods.verified.txt @@ -39,17 +39,17 @@ file sealed class IRepositoryMockImpl : global::IRepository, global::TUnit.Mocks public T GetById(int id) where T : class { - return _engine.HandleCallWithReturn(0, "GetById", id, default!); + return _engine.HandleCallWithReturn(0, "GetById", new object?[] { id }, default!, global::TUnit.Mocks.TypeArguments.Of.Value); } public void Save(T entity) where T : class, new() { - _engine.HandleCall(1, "Save", entity); + _engine.HandleCall(1, "Save", new object?[] { entity }, global::TUnit.Mocks.TypeArguments.Of.Value); } public TResult Transform(TInput input) where TInput : notnull where TResult : struct { - return _engine.HandleCallWithReturn(2, "Transform", input, default); + return _engine.HandleCallWithReturn(2, "Transform", new object?[] { input }, default, global::TUnit.Mocks.TypeArguments.Of.Value); } [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] @@ -99,40 +99,40 @@ public static class IRepository_MockMemberExtensions public static global::TUnit.Mocks.MockMethodCall GetById(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg id) where T : class { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { id.Matcher }; - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "GetById", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "GetById", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.MockMethodCall GetById(this global::TUnit.Mocks.Mock mock, global::System.Func id) where T : class { global::TUnit.Mocks.Arguments.Arg __fa_id = id; var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_id.Matcher }; - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "GetById", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "GetById", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.VoidMockMethodCall Save(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg entity) where T : class, new() { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { entity.Matcher }; - return new global::TUnit.Mocks.VoidMockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Save", matchers); + return new global::TUnit.Mocks.VoidMockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Save", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.VoidMockMethodCall Save(this global::TUnit.Mocks.Mock mock, global::System.Func entity) where T : class, new() { global::TUnit.Mocks.Arguments.Arg __fa_entity = entity; var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_entity.Matcher }; - return new global::TUnit.Mocks.VoidMockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Save", matchers); + return new global::TUnit.Mocks.VoidMockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Save", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.MockMethodCall Transform(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg input) where TInput : notnull where TResult : struct { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { input.Matcher }; - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "Transform", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "Transform", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.MockMethodCall Transform(this global::TUnit.Mocks.Mock mock, global::System.Func input) where TInput : notnull where TResult : struct { global::TUnit.Mocks.Arguments.Arg __fa_input = input; var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_input.Matcher }; - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "Transform", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "Transform", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } #if NET9_0_OR_GREATER diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Obsolete_Members.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Obsolete_Members.verified.txt index d4a22c151d..84a7f6d2e5 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Obsolete_Members.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Obsolete_Members.verified.txt @@ -321,7 +321,7 @@ file sealed class IDialogServiceMockImpl : global::IDialogService, global::TUnit { try { - var __result = _engine.HandleCallWithReturn(1, "ShowPanel", data, default); + var __result = _engine.HandleCallWithReturn(1, "ShowPanel", new object?[] { data }, default, global::TUnit.Mocks.TypeArguments.Of.Value); if (global::TUnit.Mocks.Setup.RawReturnContext.TryConsume(out var __rawAsync)) { if (__rawAsync is global::System.Threading.Tasks.Task __typedAsync) return __typedAsync; @@ -469,14 +469,14 @@ public static class IDialogService_MockMemberExtensions public static global::TUnit.Mocks.MockMethodCall ShowPanel(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg data) where TData : class { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { data.Matcher }; - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "ShowPanel", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "ShowPanel", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.MockMethodCall ShowPanel(this global::TUnit.Mocks.Mock mock, global::System.Func data) where TData : class { global::TUnit.Mocks.Arguments.Arg __fa_data = data; var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_data.Matcher }; - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "ShowPanel", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "ShowPanel", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static IDialogService_WithTrickyChars_M8_MockCall WithTrickyChars(this global::TUnit.Mocks.Mock mock) diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Unconstrained_Nullable_Generic.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Unconstrained_Nullable_Generic.verified.txt index 1ce7aa8f33..da9b7b3ec1 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Unconstrained_Nullable_Generic.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Unconstrained_Nullable_Generic.verified.txt @@ -38,7 +38,7 @@ file sealed class IFooMockImpl : global::IFoo, global::TUnit.Mocks.IRaisable, gl { try { - var __result = _engine.HandleCallWithReturn(0, "DoSomethingAsync", global::System.Array.Empty(), default); + var __result = _engine.HandleCallWithReturn(0, "DoSomethingAsync", global::System.Array.Empty(), default, global::TUnit.Mocks.TypeArguments.Of.Value); if (global::TUnit.Mocks.Setup.RawReturnContext.TryConsume(out var __rawAsync)) { if (__rawAsync is global::System.Threading.Tasks.Task __typedAsync) return __typedAsync; @@ -54,12 +54,12 @@ file sealed class IFooMockImpl : global::IFoo, global::TUnit.Mocks.IRaisable, gl public T? GetValue() { - return _engine.HandleCallWithReturn(1, "GetValue", global::System.Array.Empty(), default); + return _engine.HandleCallWithReturn(1, "GetValue", global::System.Array.Empty(), default, global::TUnit.Mocks.TypeArguments.Of.Value); } public (T?, string) GetPair() { - return _engine.HandleCallWithReturn<(T?, string)>(2, "GetPair", global::System.Array.Empty(), default); + return _engine.HandleCallWithReturn<(T?, string)>(2, "GetPair", global::System.Array.Empty(), default, global::TUnit.Mocks.TypeArguments.Of.Value); } [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] @@ -109,19 +109,19 @@ public static class IFoo_MockMemberExtensions public static global::TUnit.Mocks.MockMethodCall DoSomethingAsync(this global::TUnit.Mocks.Mock mock) { var matchers = global::System.Array.Empty(); - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "DoSomethingAsync", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "DoSomethingAsync", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.MockMethodCall GetValue(this global::TUnit.Mocks.Mock mock) { var matchers = global::System.Array.Empty(); - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "GetValue", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "GetValue", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.MockMethodCall<(T?, string)> GetPair(this global::TUnit.Mocks.Mock mock) { var matchers = global::System.Array.Empty(); - return new global::TUnit.Mocks.MockMethodCall<(T?, string)>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "GetPair", matchers); + return new global::TUnit.Mocks.MockMethodCall<(T?, string)>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "GetPair", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } #if NET9_0_OR_GREATER diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_With_Generic_Constrained_Virtual_Methods.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_With_Generic_Constrained_Virtual_Methods.verified.txt index 58660c9c71..6d8ed797e7 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_With_Generic_Constrained_Virtual_Methods.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_With_Generic_Constrained_Virtual_Methods.verified.txt @@ -44,7 +44,7 @@ file sealed class BaseServiceMockImpl : global::BaseService, global::TUnit.Mocks public override global::System.Collections.Generic.IEnumerable GetAll() { - return _engine.HandleCallWithReturn>(3, "GetAll", global::System.Array.Empty(), global::System.Array.Empty()); + return _engine.HandleCallWithReturn>(3, "GetAll", global::System.Array.Empty(), global::System.Array.Empty(), global::TUnit.Mocks.TypeArguments.Of.Value); } [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] @@ -84,46 +84,46 @@ public static class BaseService_MockMemberExtensions public static global::TUnit.Mocks.MockMethodCall GetById(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg id) where T : class { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { id.Matcher }; - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "GetById", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "GetById", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.MockMethodCall GetById(this global::TUnit.Mocks.Mock mock, global::System.Func id) where T : class { global::TUnit.Mocks.Arguments.Arg __fa_id = id; var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_id.Matcher }; - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "GetById", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "GetById", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.VoidMockMethodCall Save(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg entity) where T : class, new() { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { entity.Matcher }; - return new global::TUnit.Mocks.VoidMockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Save", matchers); + return new global::TUnit.Mocks.VoidMockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Save", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.VoidMockMethodCall Save(this global::TUnit.Mocks.Mock mock, global::System.Func entity) where T : class, new() { global::TUnit.Mocks.Arguments.Arg __fa_entity = entity; var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_entity.Matcher }; - return new global::TUnit.Mocks.VoidMockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Save", matchers); + return new global::TUnit.Mocks.VoidMockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Save", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.MockMethodCall Transform(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg input) where TInput : notnull where TResult : struct { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { input.Matcher }; - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "Transform", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "Transform", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.MockMethodCall Transform(this global::TUnit.Mocks.Mock mock, global::System.Func input) where TInput : notnull where TResult : struct { global::TUnit.Mocks.Arguments.Arg __fa_input = input; var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_input.Matcher }; - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "Transform", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "Transform", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.MockMethodCall> GetAll(this global::TUnit.Mocks.Mock mock) where T : class { var matchers = global::System.Array.Empty(); - return new global::TUnit.Mocks.MockMethodCall>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 3, "GetAll", matchers); + return new global::TUnit.Mocks.MockMethodCall>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 3, "GetAll", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } #if NET9_0_OR_GREATER diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Wrap_Mock_With_Generic_Constrained_Virtual_Methods.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Wrap_Mock_With_Generic_Constrained_Virtual_Methods.verified.txt index e1f13a7971..e28c040824 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Wrap_Mock_With_Generic_Constrained_Virtual_Methods.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Wrap_Mock_With_Generic_Constrained_Virtual_Methods.verified.txt @@ -73,27 +73,27 @@ public static class Repository_MockMemberExtensions public static global::TUnit.Mocks.MockMethodCall Get(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg id) where T : class { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { id.Matcher }; - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "Get", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "Get", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.MockMethodCall Get(this global::TUnit.Mocks.Mock mock, global::System.Func id) where T : class { global::TUnit.Mocks.Arguments.Arg __fa_id = id; var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_id.Matcher }; - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "Get", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "Get", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.VoidMockMethodCall Store(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg item) where T : class, new() { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { item.Matcher }; - return new global::TUnit.Mocks.VoidMockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Store", matchers); + return new global::TUnit.Mocks.VoidMockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Store", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } public static global::TUnit.Mocks.VoidMockMethodCall Store(this global::TUnit.Mocks.Mock mock, global::System.Func item) where T : class, new() { global::TUnit.Mocks.Arguments.Arg __fa_item = item; var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_item.Matcher }; - return new global::TUnit.Mocks.VoidMockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Store", matchers); + return new global::TUnit.Mocks.VoidMockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Store", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } #if NET9_0_OR_GREATER diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs index 20f139f6ca..4be7366aca 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs @@ -744,6 +744,15 @@ private static void GenerateEngineDispatchBody(CodeWriter writer, MockMemberMode } var (isTyped, typeArgs, argsList) = GetTypedDispatchInfo(method); + // Generic methods always dispatch through the object?[] + type-arguments fallback so their concrete + // type arguments reach the engine (typed dispatch can't carry them). Force the fallback path + // here so argsArray is materialized; the emit helpers then select the type-arg overloads. + if (method.IsGenericMethod) + { + isTyped = false; + typeArgs = null; + argsList = null; + } var argsArray = isTyped ? null : EmitArgsArrayVariable(writer, method); var autoMockFactory = GetAutoMockFactoryLambda(method); @@ -752,7 +761,7 @@ private static void GenerateEngineDispatchBody(CodeWriter writer, MockMemberMode if (method.IsVoid && !method.IsAsync) { // Pure void method - writer.AppendLine($"{EmitHandleCall(isTyped, typeArgs, argsList, argsArray, method.MemberId, method.Name)};"); + writer.AppendLine($"{EmitHandleCall(isTyped, typeArgs, argsList, argsArray, method.MemberId, method.Name, method)};"); EmitOutRefReadback(writer, method, model); } else if (method.IsVoid && method.IsAsync) @@ -760,7 +769,7 @@ private static void GenerateEngineDispatchBody(CodeWriter writer, MockMemberMode // Async void method (Task or ValueTask with no generic arg) using (writer.Block("try")) { - writer.AppendLine($"{EmitHandleCall(isTyped, typeArgs, argsList, argsArray, method.MemberId, method.Name)};"); + writer.AppendLine($"{EmitHandleCall(isTyped, typeArgs, argsList, argsArray, method.MemberId, method.Name, method)};"); EmitOutRefReadback(writer, method, model); EmitRawReturnCheck(writer, method); if (method.IsValueTask) @@ -793,11 +802,11 @@ private static void GenerateEngineDispatchBody(CodeWriter writer, MockMemberMode { if (method.IsReturnTypeStaticAbstractInterface) { - writer.AppendLine($"var __result = ({method.UnwrappedReturnType}){EmitHandleCallWithReturn(isTyped, typeArgs, argsList, argsArray, unwrappedArg, method.MemberId, method.Name, unwrappedDefault)}!;"); + writer.AppendLine($"var __result = ({method.UnwrappedReturnType}){EmitHandleCallWithReturn(isTyped, typeArgs, argsList, argsArray, unwrappedArg, method.MemberId, method.Name, unwrappedDefault, method: method)}!;"); } else { - writer.AppendLine($"var __result = {EmitHandleCallWithReturn(isTyped, typeArgs, argsList, argsArray, unwrappedArg, method.MemberId, method.Name, unwrappedDefault, autoMockFactory)};"); + writer.AppendLine($"var __result = {EmitHandleCallWithReturn(isTyped, typeArgs, argsList, argsArray, unwrappedArg, method.MemberId, method.Name, unwrappedDefault, autoMockFactory, method)};"); } EmitOutRefReadback(writer, method, model); EmitRawReturnCheck(writer, method); @@ -827,7 +836,7 @@ private static void GenerateEngineDispatchBody(CodeWriter writer, MockMemberMode // Synchronous method returning a ref struct — can't use HandleCallWithReturn because // ref structs can't be generic type arguments. Use void dispatch for call tracking, // callbacks, and throws. - writer.AppendLine($"{EmitHandleCall(isTyped, typeArgs, argsList, argsArray, method.MemberId, method.Name)};"); + writer.AppendLine($"{EmitHandleCall(isTyped, typeArgs, argsList, argsArray, method.MemberId, method.Name, method)};"); if (method.SpanReturnElementType is not null) { // Span return: read back out/ref params AND extract return value from OutRefContext index -1 @@ -845,13 +854,13 @@ private static void GenerateEngineDispatchBody(CodeWriter writer, MockMemberMode // as a generic type argument. Use object? and cast. if (hasOutRef) { - writer.AppendLine($"var __result = ({method.ReturnType}){EmitHandleCallWithReturn(isTyped, typeArgs, argsList, argsArray, "object?", method.MemberId, method.Name, "null")}!;"); + writer.AppendLine($"var __result = ({method.ReturnType}){EmitHandleCallWithReturn(isTyped, typeArgs, argsList, argsArray, "object?", method.MemberId, method.Name, "null", method: method)}!;"); EmitOutRefReadback(writer, method, model); writer.AppendLine("return __result;"); } else { - writer.AppendLine($"return ({method.ReturnType}){EmitHandleCallWithReturn(isTyped, typeArgs, argsList, argsArray, "object?", method.MemberId, method.Name, "null")}!;"); + writer.AppendLine($"return ({method.ReturnType}){EmitHandleCallWithReturn(isTyped, typeArgs, argsList, argsArray, "object?", method.MemberId, method.Name, "null", method: method)}!;"); } } else @@ -859,13 +868,13 @@ private static void GenerateEngineDispatchBody(CodeWriter writer, MockMemberMode // Synchronous method with return value — need to read back out/ref before returning if (hasOutRef) { - writer.AppendLine($"var __result = {EmitHandleCallWithReturn(isTyped, typeArgs, argsList, argsArray, method.ReturnType, method.MemberId, method.Name, method.SmartDefault, autoMockFactory)};"); + writer.AppendLine($"var __result = {EmitHandleCallWithReturn(isTyped, typeArgs, argsList, argsArray, method.ReturnType, method.MemberId, method.Name, method.SmartDefault, autoMockFactory, method)};"); EmitOutRefReadback(writer, method, model); writer.AppendLine("return __result;"); } else { - writer.AppendLine($"return {EmitHandleCallWithReturn(isTyped, typeArgs, argsList, argsArray, method.ReturnType, method.MemberId, method.Name, method.SmartDefault, autoMockFactory)};"); + writer.AppendLine($"return {EmitHandleCallWithReturn(isTyped, typeArgs, argsList, argsArray, method.ReturnType, method.MemberId, method.Name, method.SmartDefault, autoMockFactory, method)};"); } } } @@ -1375,16 +1384,50 @@ private static (bool IsTyped, string? TypeArgs, string? ArgsList) GetTypedDispat } /// Emits a HandleCall or TryHandleCall invocation, choosing typed or fallback path. - private static string EmitHandleCall(bool isTyped, string? typeArgs, string? argsList, string? argsArray, int memberId, string memberName) - => isTyped + /// + /// A generic method always uses the object?[] fallback overload that also takes the method's + /// concrete type arguments, so the engine can discriminate setups/calls by type argument. + /// + private static string EmitHandleCall(bool isTyped, string? typeArgs, string? argsList, string? argsArray, int memberId, string memberName, MockMemberModel? method = null) + { + if (method is { IsGenericMethod: true }) + { + return $"_engine.HandleCall({memberId}, \"{memberName}\", {argsArray}, {TypeArgumentsArrayLiteral(method)})"; + } + return isTyped ? $"_engine.HandleCall<{typeArgs}>({memberId}, \"{memberName}\", {argsList})" : $"_engine.HandleCall({memberId}, \"{memberName}\", {argsArray})"; + } /// Emits a HandleCallWithReturn invocation, choosing typed or fallback path. - private static string EmitHandleCallWithReturn(bool isTyped, string? typeArgs, string? argsList, string? argsArray, string returnTypeArg, int memberId, string memberName, string defaultValue, string? autoMockFactory = null) - => isTyped + /// See for the generic-method type-argument path. + private static string EmitHandleCallWithReturn(bool isTyped, string? typeArgs, string? argsList, string? argsArray, string returnTypeArg, int memberId, string memberName, string defaultValue, string? autoMockFactory = null, MockMemberModel? method = null) + { + if (method is { IsGenericMethod: true }) + { + // Generic methods dispatch through the object?[] overload that carries the method's type + // arguments; an auto-mock factory (for an auto-mockable return type) is preserved before it. + return $"_engine.HandleCallWithReturn<{returnTypeArg}>({memberId}, \"{memberName}\", {argsArray}, {defaultValue}{FormatAutoMockFactoryArgument(autoMockFactory)}, {TypeArgumentsArrayLiteral(method)})"; + } + return isTyped ? $"_engine.HandleCallWithReturn<{returnTypeArg}, {typeArgs}>({memberId}, \"{memberName}\", {argsList}, {defaultValue}{FormatAutoMockFactoryArgument(autoMockFactory)})" : $"_engine.HandleCallWithReturn<{returnTypeArg}>({memberId}, \"{memberName}\", {argsArray}, {defaultValue}{FormatAutoMockFactoryArgument(autoMockFactory)})"; + } + + /// + /// Emits the type-argument array for a generic method. For the common 1–4 type-parameter cases it + /// references the per-closed-type cache (TypeArguments.Of<T>.Value) to avoid a per-call + /// allocation; higher arities fall back to a per-call ImmutableArray.Create(...). + /// + internal static string TypeArgumentsArrayLiteral(MockMemberModel method) + { + var typeParams = method.TypeParameters; + if (typeParams.Length is >= 1 and <= 4) + { + return $"global::TUnit.Mocks.TypeArguments.Of<{string.Join(", ", typeParams.Select(tp => tp.Name))}>.Value"; + } + return $"global::System.Collections.Immutable.ImmutableArray.Create({string.Join(", ", typeParams.Select(tp => $"typeof({tp.Name})"))})"; + } /// Emits a TryHandleCall condition, choosing typed or fallback path. private static string EmitTryHandleCall(bool isTyped, string? typeArgs, string? argsList, string? argsArray, int memberId, string memberName) diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs index b62905d5f2..d5853a98e2 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs @@ -969,6 +969,14 @@ private static void EmitMemberMethodBody(CodeWriter writer, MockMemberModel meth private static void EmitReturnConstruction(CodeWriter writer, MockMemberModel method, MockTypeModel model, string safeName, bool useTypedWrapper, string setupReturnType) { + // For a generic method, pass the configured type arguments so the setup/verification can + // discriminate calls by type argument. Non-generic methods omit the argument (overload with + // the trailing type-arguments parameter is not selected). The typed wrapper is never generated + // for generic methods. + var typeArgs = method.IsGenericMethod + ? $", {MockImplBuilder.TypeArgumentsArrayLiteral(method)}" + : ""; + if (useTypedWrapper) { var wrapperName = MockImplBuilder.GetGeneratedTypeName(GetWrapperName(safeName, method), model); @@ -976,15 +984,15 @@ private static void EmitReturnConstruction(CodeWriter writer, MockMemberModel me } else if (method.IsVoid || method.IsRefStructReturn) { - writer.AppendLine($"return new global::TUnit.Mocks.VoidMockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), {method.MemberId}, \"{method.Name}\", matchers);"); + writer.AppendLine($"return new global::TUnit.Mocks.VoidMockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), {method.MemberId}, \"{method.Name}\", matchers{typeArgs});"); } else if (method.IsReturnTypeStaticAbstractInterface) { - writer.AppendLine($"return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), {method.MemberId}, \"{method.Name}\", matchers);"); + writer.AppendLine($"return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), {method.MemberId}, \"{method.Name}\", matchers{typeArgs});"); } else { - writer.AppendLine($"return new global::TUnit.Mocks.MockMethodCall<{setupReturnType}>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), {method.MemberId}, \"{method.Name}\", matchers);"); + writer.AppendLine($"return new global::TUnit.Mocks.MockMethodCall<{setupReturnType}>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), {method.MemberId}, \"{method.Name}\", matchers{typeArgs});"); } } diff --git a/TUnit.Mocks.Tests/GenericTests.cs b/TUnit.Mocks.Tests/GenericTests.cs index 48a9708f56..59ab866844 100644 --- a/TUnit.Mocks.Tests/GenericTests.cs +++ b/TUnit.Mocks.Tests/GenericTests.cs @@ -1,5 +1,6 @@ using TUnit.Mocks; using TUnit.Mocks.Arguments; +using TUnit.Mocks.Exceptions; namespace TUnit.Mocks.Tests; @@ -13,6 +14,37 @@ public interface IRepository TResult Transform(TInput input) where TInput : class where TResult : class; } +/// +/// Generic method whose type parameter does not appear in the parameter list, so calls can only be +/// distinguished by their type argument. Mirrors discussion #4981. +/// +public interface IGenericGreeter +{ + string Greet() where T : class; +} + +/// +/// Methods with two type parameters, used to verify discrimination is sensitive to the full, +/// ordered list of type arguments — not just the first. +/// +public interface IMultiGeneric +{ + // Zero parameters: the only discriminator is the (T1, T2) type-argument pair. + string Describe() where T1 : class where T2 : class; + + // TOutput appears only as a type parameter; the argument matcher is on TInput. + void Handle(TInput input) where TInput : class where TOutput : class; +} + +/// +/// A struct-constrained generic method, to verify the wildcard works for +/// where T : struct parameters (and is not dead code). +/// +public interface IValueDescriber +{ + string Describe() where T : struct; +} + public class Customer { public int Id { get; set; } @@ -24,6 +56,9 @@ public class Order public int OrderId { get; set; } } +public class Class1 { } +public class Class2 { } + /// /// US7 Integration Tests: Generic method support in mock generation. /// @@ -145,4 +180,201 @@ public async Task Generic_Method_With_Two_Type_Parameters() await Assert.That(result).IsNotNull(); await Assert.That(result.OrderId).IsEqualTo(10); } + + [Test] + public async Task Generic_Method_ZeroArgs_Distinguished_By_Type_Argument() + { + // Regression for discussion #4981: with no parameters, only the type argument distinguishes + // the two setups. Before the fix both setups collided and the last one always won. + var mock = IGenericGreeter.Mock(); + mock.Greet().Returns("Hello!"); + mock.Greet().Returns("Goodbye!"); + + IGenericGreeter greeter = mock.Object; + + await Assert.That(greeter.Greet()).IsEqualTo("Hello!"); + await Assert.That(greeter.Greet()).IsEqualTo("Goodbye!"); + } + + [Test] + public async Task Generic_Method_Wildcard_AnyType_Matches_Any_Type_Argument() + { + var mock = IGenericGreeter.Mock(); + mock.Greet().Returns("any"); + + IGenericGreeter greeter = mock.Object; + + await Assert.That(greeter.Greet()).IsEqualTo("any"); + await Assert.That(greeter.Greet()).IsEqualTo("any"); + } + + [Test] + public async Task Generic_Method_Exact_Type_Setup_Wins_Over_Wildcard() + { + // A later, more specific setup is matched first (last-wins iteration), while other type + // arguments still fall through to the wildcard. + var mock = IGenericGreeter.Mock(); + mock.Greet().Returns("any"); + mock.Greet().Returns("specific"); + + IGenericGreeter greeter = mock.Object; + + await Assert.That(greeter.Greet()).IsEqualTo("specific"); + await Assert.That(greeter.Greet()).IsEqualTo("any"); + } + + [Test] + public void Generic_Void_Method_Verify_Discriminates_By_Type_Argument() + { + var mock = IRepository.Mock(); + IRepository repo = mock.Object; + + repo.Save(new Customer()); + repo.Save(new Customer()); + repo.Save(new Order()); + + mock.Save(Any()).WasCalled(Times.Exactly(2)); + mock.Save(Any()).WasCalled(Times.Once); + mock.Save(Any()).WasCalled(Times.Exactly(3)); + } + + [Test] + public async Task Generic_Method_Two_Type_Params_Distinguished_By_Order() + { + // The two setups share the same (empty) argument list and the same type-argument *set* — + // only the order differs. Each must resolve independently. + var mock = IMultiGeneric.Mock(); + mock.Describe().Returns("1-2"); + mock.Describe().Returns("2-1"); + + IMultiGeneric sut = mock.Object; + + await Assert.That(sut.Describe()).IsEqualTo("1-2"); + await Assert.That(sut.Describe()).IsEqualTo("2-1"); + } + + [Test] + public async Task Generic_Method_Two_Type_Params_Partial_Wildcard() + { + // Wildcard the first type parameter, pin the second. Only the second must match exactly. + var mock = IMultiGeneric.Mock(); + mock.Describe().Returns("any-2"); + + IMultiGeneric sut = mock.Object; + + await Assert.That(sut.Describe()).IsEqualTo("any-2"); // first wildcard, second matches + await Assert.That(sut.Describe()).IsEqualTo("any-2"); // first wildcard, second matches + // Second type arg (Class1) != Class2 → no setup matches → unconfigured default (empty string) + await Assert.That(sut.Describe()).IsEqualTo(string.Empty); + } + + [Test] + public async Task Generic_Method_Two_Type_Params_Exact_Wins_Over_Partial_Wildcard() + { + var mock = IMultiGeneric.Mock(); + mock.Describe().Returns("any-2"); + mock.Describe().Returns("1-2"); // more specific, added later → matched first + + IMultiGeneric sut = mock.Object; + + await Assert.That(sut.Describe()).IsEqualTo("1-2"); + await Assert.That(sut.Describe()).IsEqualTo("any-2"); + } + + [Test] + public void Generic_Void_Method_Two_Type_Params_Verify_Discriminates() + { + var mock = IMultiGeneric.Mock(); + IMultiGeneric sut = mock.Object; + + sut.Handle(new Customer()); + sut.Handle(new Order()); + + mock.Handle(Any()).WasCalled(Times.Once); + mock.Handle(Any()).WasCalled(Times.Once); + mock.Handle(Any()).WasNeverCalled(); // pair never called + mock.Handle(Any()).WasCalled(Times.Exactly(2)); // both wildcards + mock.Handle(Any()).WasCalled(Times.Once); // only Handle + } + + [Test] + public async Task Generic_Verification_Failure_Message_Includes_Type_Argument() + { + var mock = IRepository.Mock(); + mock.Object.Save(new Order()); // Save, never Save + + var ex = Assert.Throws(() => + mock.Save(Any()).WasCalled(Times.Once)); + + // The failure should name the type argument so the developer can tell which setup was expected. + await Assert.That(ex!.ExpectedCall).Contains("Save"); + } + + [Test] + public async Task Generic_Method_Struct_Constraint_AnyValueType_Wildcard() + { + // AnyValueType is a struct, so it satisfies `where T : struct` and acts as a wildcard there. + var mock = IValueDescriber.Mock(); + mock.Describe().Returns("any-value"); + + IValueDescriber sut = mock.Object; + + await Assert.That(sut.Describe()).IsEqualTo("any-value"); + await Assert.That(sut.Describe()).IsEqualTo("any-value"); + } + + [Test] + public async Task Generic_Method_Struct_Constraint_Distinguished_By_Type_Argument() + { + var mock = IValueDescriber.Mock(); + mock.Describe().Returns("int"); + mock.Describe().Returns("long"); + + IValueDescriber sut = mock.Object; + + await Assert.That(sut.Describe()).IsEqualTo("int"); + await Assert.That(sut.Describe()).IsEqualTo("long"); + } + + [Test] + public void Ordered_Verification_Discriminates_By_Type_Argument() + { + var mock = IGenericGreeter.Mock(); + IGenericGreeter greeter = mock.Object; + + greeter.Greet(); + greeter.Greet(); + + // Passes only if each expectation matches the call with its own type argument. + Mock.VerifyInOrder(() => + { + mock.Greet().WasCalled(); + mock.Greet().WasCalled(); + }); + } + + [Test] + public async Task Ordered_Verification_Fails_When_Type_Arguments_Out_Of_Order() + { + var mock = IGenericGreeter.Mock(); + IGenericGreeter greeter = mock.Object; + + greeter.Greet(); + greeter.Greet(); + + // Reversed type-argument order must fail. Before type arguments were threaded through + // ordered verification, both expectations matched both calls and this passed regardless + // of which type was actually called first. + var ex = Assert.Throws(() => + Mock.VerifyInOrder(() => + { + mock.Greet().WasCalled(); + mock.Greet().WasCalled(); + })); + + await Assert.That(ex!.Message).Contains("Ordered verification failed"); + // The failure message names the expected calls with their type arguments. + await Assert.That(ex.Message).Contains("Greet"); + await Assert.That(ex.Message).Contains("Greet"); + } } diff --git a/TUnit.Mocks/Arguments/AnyType.cs b/TUnit.Mocks/Arguments/AnyType.cs new file mode 100644 index 0000000000..2525571171 --- /dev/null +++ b/TUnit.Mocks/Arguments/AnyType.cs @@ -0,0 +1,29 @@ +namespace TUnit.Mocks.Arguments; + +/// +/// Wildcard marker for a generic method's reference-type argument. Use it as the type argument of a +/// mock setup or verification on a generic method to match calls regardless of the concrete type +/// argument supplied at the call site: +/// +/// mock.Get<AnyType>(Arg.Any<int>()).Returns(value); // matches Get<Customer>, Get<Order>, ... +/// +/// Satisfies a where T : class, where T : class, new(), or unconstrained type parameter +/// (it has a public parameterless constructor). For a where T : struct parameter use +/// . Type parameters constrained to a specific base class or interface +/// support exact-type matching only. +/// +public sealed class AnyType +{ +} + +/// +/// Wildcard marker for a generic method's value-type argument. The struct counterpart of +/// , for matching a where T : struct type parameter regardless of the +/// concrete value type supplied at the call site. +/// A type parameter with constraints beyond struct (e.g. where T : struct, IComparable) +/// supports exact-type matching only, since does not implement any +/// interfaces and cannot be supplied as its type argument. +/// +public struct AnyValueType +{ +} diff --git a/TUnit.Mocks/ITypeArgumentVerificationFactory.cs b/TUnit.Mocks/ITypeArgumentVerificationFactory.cs new file mode 100644 index 0000000000..7f440d5eff --- /dev/null +++ b/TUnit.Mocks/ITypeArgumentVerificationFactory.cs @@ -0,0 +1,37 @@ +using System.Collections.Immutable; +using TUnit.Mocks.Arguments; +using TUnit.Mocks.Verification; + +namespace TUnit.Mocks; + +/// +/// Internal extension of the engine's verification surface that also filters by a generic method's +/// type arguments. Kept off the public interface so adding generic +/// type-argument support does not break external implementers (a default interface method is not an +/// option while the library targets netstandard2.0). The only implementer is +/// ; generic mock-call wrappers reach it via a type test and fall back to +/// the public, non-filtering surface for any custom engine. +/// +internal interface ITypeArgumentVerificationFactory +{ + /// + /// Creates a call verification builder that filters recorded calls by the supplied type arguments + /// (concrete types or / wildcards). + /// + ICallVerification CreateVerification(int memberId, string memberName, IArgumentMatcher[] matchers, ImmutableArray typeArguments); +} + +/// +/// Shared verification-builder routing for and +/// : non-generic calls use the public engine surface unchanged; +/// generic calls route through (always implemented by +/// MockEngine). A custom implementation falls back to the public +/// surface, losing type-argument filtering but never throwing. +/// +internal static class MockCallVerification +{ + public static ICallVerification Create(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers, ImmutableArray typeArguments) + => !typeArguments.IsDefault && engine is ITypeArgumentVerificationFactory typeArgFactory + ? typeArgFactory.CreateVerification(memberId, memberName, matchers, typeArguments) + : engine.CreateVerification(memberId, memberName, matchers); +} diff --git a/TUnit.Mocks/MockEngine.cs b/TUnit.Mocks/MockEngine.cs index b5497a6953..40d75b8569 100644 --- a/TUnit.Mocks/MockEngine.cs +++ b/TUnit.Mocks/MockEngine.cs @@ -4,6 +4,7 @@ using TUnit.Mocks.Setup.Behaviors; using TUnit.Mocks.Verification; using System.Collections.Concurrent; +using System.Collections.Immutable; using System.Runtime.CompilerServices; using System.Threading; using System.ComponentModel; @@ -21,7 +22,7 @@ internal static class MockCallSequence } [EditorBrowsable(EditorBrowsableState.Never)] -public sealed partial class MockEngine : IMockEngineAccess where T : class +public sealed partial class MockEngine : IMockEngineAccess, ITypeArgumentVerificationFactory where T : class { // Single lock for both setup and call mutations — reduces allocation by one Lock object. // Contention is acceptable since setup and call recording rarely overlap in typical usage. @@ -224,13 +225,27 @@ private void EnsureSetupArrayCapacity(int memberId) ICallVerification IMockEngineAccess.CreateVerification(int memberId, string memberName, IArgumentMatcher[] matchers) => new CallVerificationBuilder(this, memberId, memberName, matchers); + ICallVerification ITypeArgumentVerificationFactory.CreateVerification(int memberId, string memberName, IArgumentMatcher[] matchers, ImmutableArray typeArguments) + => new CallVerificationBuilder(this, memberId, memberName, matchers, typeArguments); + /// /// Handles a void method call. Records the call and executes matching setup behavior. /// public void HandleCall(int memberId, string memberName, object?[] args) + => HandleCallCore(memberId, memberName, args, default); + + /// + /// Handles a void generic-method call. are the concrete closed + /// type arguments, used to discriminate setups and recorded calls by type argument. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public void HandleCall(int memberId, string memberName, object?[] args, ImmutableArray typeArguments) + => HandleCallCore(memberId, memberName, args, typeArguments); + + private void HandleCallCore(int memberId, string memberName, object?[] args, ImmutableArray typeArguments) { RawReturnContext.Clear(); - var callRecord = RecordCall(memberId, memberName, args); + var callRecord = RecordCall(memberId, memberName, args, typeArguments); // Auto-track property setters: store value keyed by property name if (AutoTrackProperties && memberName.StartsWith("set_", StringComparison.Ordinal) && args.Length > 0) @@ -238,7 +253,7 @@ public void HandleCall(int memberId, string memberName, object?[] args) AutoTrackValues[memberName[4..]] = args[0]; } - var (setupFound, behavior, matchedSetup) = FindMatchingSetup(memberId, args); + var (setupFound, behavior, matchedSetup) = FindMatchingSetup(memberId, args, typeArguments); if (behavior is not null) { @@ -282,15 +297,34 @@ public void HandleCall(int memberId, string memberName, object?[] args) /// or returns default/throws for strict mode. /// public TReturn HandleCallWithReturn(int memberId, string memberName, object?[] args, TReturn defaultValue) - => HandleCallWithReturn(memberId, memberName, args, defaultValue, null); + => HandleCallWithReturnCore(memberId, memberName, args, defaultValue, null, default); [EditorBrowsable(EditorBrowsableState.Never)] public TReturn HandleCallWithReturn(int memberId, string memberName, object?[] args, TReturn defaultValue, Func? autoMockFactory) + => HandleCallWithReturnCore(memberId, memberName, args, defaultValue, autoMockFactory, default); + + /// + /// Handles a generic-method call with a return value. are the + /// concrete closed type arguments, used to discriminate setups and recorded calls by type argument. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public TReturn HandleCallWithReturn(int memberId, string memberName, object?[] args, TReturn defaultValue, ImmutableArray typeArguments) + => HandleCallWithReturnCore(memberId, memberName, args, defaultValue, null, typeArguments); + + /// + /// Generic-method call with a return value that may also auto-mock its return type. Combines the + /// and paths. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public TReturn HandleCallWithReturn(int memberId, string memberName, object?[] args, TReturn defaultValue, Func? autoMockFactory, ImmutableArray typeArguments) + => HandleCallWithReturnCore(memberId, memberName, args, defaultValue, autoMockFactory, typeArguments); + + private TReturn HandleCallWithReturnCore(int memberId, string memberName, object?[] args, TReturn defaultValue, Func? autoMockFactory, ImmutableArray typeArguments) { RawReturnContext.Clear(); - var callRecord = RecordCall(memberId, memberName, args); + var callRecord = RecordCall(memberId, memberName, args, typeArguments); - var (setupFound, behavior, matchedSetup) = FindMatchingSetup(memberId, args); + var (setupFound, behavior, matchedSetup) = FindMatchingSetup(memberId, args, typeArguments); if (behavior is not null) { @@ -752,6 +786,9 @@ private void CollectCallRecords(List target, Func? private CallRecord RecordCall(int memberId, string memberName, object?[] args) => StoreCallRecord(new CallRecord(memberId, memberName, args, MockCallSequence.Next())); + private CallRecord RecordCall(int memberId, string memberName, object?[] args, ImmutableArray typeArguments) + => StoreCallRecord(new CallRecord(memberId, memberName, args, MockCallSequence.Next(), typeArguments)); + private CallRecord RecordCall(int memberId, string memberName, IArgumentStore store) => StoreCallRecord(new CallRecord(memberId, memberName, store, MockCallSequence.Next())); @@ -875,7 +912,14 @@ private void RebuildStaleSnapshots() } } + // Non-generic two-arg overload kept distinct (no optional parameter) so it wins overload + // resolution against the generic FindMatchingSetup(int, T1) for object?[] call sites. + // Adding a defaulted third parameter here would let the generic overload bind instead, + // silently treating the args array as a single typed argument. private (bool SetupFound, IBehavior? Behavior, MethodSetup? Setup) FindMatchingSetup(int memberId, object?[] args) + => FindMatchingSetup(memberId, args, default(ImmutableArray)); + + private (bool SetupFound, IBehavior? Behavior, MethodSetup? Setup) FindMatchingSetup(int memberId, object?[] args, ImmutableArray typeArguments) { // Rebuild snapshots if setup phase just ended (batches all ToArray work into one pass) if (_hasStaleSetups) @@ -887,7 +931,7 @@ private void RebuildStaleSnapshots() // to prevent concurrent invocations from consuming the same state transition if (_hasStatefulSetups) { - return FindMatchingSetupLocked(memberId, args); + return FindMatchingSetupLocked(memberId, args, typeArguments); } var snapshot = _setupsByMemberId; @@ -907,7 +951,7 @@ private void RebuildStaleSnapshots() { var setup = setups[i]; - if (setup.Matches(args)) + if (setup.Matches(args) && setup.TypeArgumentsMatch(typeArguments)) { setup.IncrementInvokeCount(); setup.ApplyCaptures(args); @@ -918,7 +962,9 @@ private void RebuildStaleSnapshots() return (false, null, null); } - private (bool SetupFound, IBehavior? Behavior, MethodSetup? Setup) FindMatchingSetupLocked(int memberId, object?[] args) + // FindMatchingSetupLocked has no generic sibling, so a defaulted parameter is safe here + // (unlike FindMatchingSetup, which competes with FindMatchingSetup). + private (bool SetupFound, IBehavior? Behavior, MethodSetup? Setup) FindMatchingSetupLocked(int memberId, object?[] args, ImmutableArray typeArguments = default) { lock (Lock) { @@ -942,7 +988,7 @@ private void RebuildStaleSnapshots() continue; } - if (setup.Matches(args)) + if (setup.Matches(args) && setup.TypeArgumentsMatch(typeArguments)) { setup.IncrementInvokeCount(); setup.ApplyCaptures(args); @@ -954,6 +1000,14 @@ private void RebuildStaleSnapshots() return (false, null, null); } + // The typed FindMatchingSetup family deliberately omits a TypeArgumentsMatch check. + // Typed dispatch never carries call-side type arguments, and per TypeArgumentMatching.Matches a + // default call side matches any setup — so the check would be a constant `true`. Two paths land + // here: non-generic methods (their setups never carry type arguments), and virtual/partial/wrap + // generic methods, which use the documented graceful-degradation path where setups are matched by + // arguments only, regardless of configured type arguments. Interface generic methods never use + // typed dispatch (see MockMembersBuilder.ShouldGenerateTypedWrapper) and are fully discriminated + // via the object?[] overloads above. private (bool SetupFound, IBehavior? Behavior, MethodSetup? Setup) FindMatchingSetup(int memberId, T1 arg1) { if (_hasStaleSetups) RebuildStaleSnapshots(); diff --git a/TUnit.Mocks/MockMethodCall.cs b/TUnit.Mocks/MockMethodCall.cs index 2cf0972255..b3d77b153e 100644 --- a/TUnit.Mocks/MockMethodCall.cs +++ b/TUnit.Mocks/MockMethodCall.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using System.ComponentModel; using TUnit.Mocks.Arguments; using TUnit.Mocks.Setup; @@ -20,23 +21,33 @@ public sealed class MockMethodCall : IMethodSetup, ISetupChain private readonly int _memberId; private readonly string _memberName; private readonly IArgumentMatcher[] _matchers; + private readonly ImmutableArray _typeArguments; private MethodSetupBuilder? _builder; private bool _builderInitialized; private object? _builderLock; + // Kept as a distinct overload (not a single optional-parameter ctor) to preserve the original + // public binary signature for backward compatibility. [EditorBrowsable(EditorBrowsableState.Never)] public MockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers) + : this(engine, memberId, memberName, matchers, default) + { + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public MockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers, ImmutableArray typeArguments) { _engine = engine; _memberId = memberId; _memberName = memberName; _matchers = matchers; + _typeArguments = typeArguments; } private MethodSetupBuilder EnsureSetup() => LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => { - var setup = new MethodSetup(_memberId, _matchers, _memberName); + var setup = new MethodSetup(_memberId, _matchers, _memberName, _typeArguments); _engine.AddSetup(setup); return new MethodSetupBuilder(setup); })!; @@ -109,33 +120,36 @@ public IMethodSetup Then() // ICallVerification implementation + private ICallVerification CreateVerification() + => MockCallVerification.Create(_engine, _memberId, _memberName, _matchers, _typeArguments); + public void WasCalled(Times times) { - _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times); + CreateVerification().WasCalled(times); } public void WasCalled(Times times, string? message) { - _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times, message); + CreateVerification().WasCalled(times, message); } public void WasNeverCalled() { - _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(); + CreateVerification().WasNeverCalled(); } public void WasNeverCalled(string? message) { - _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(message); + CreateVerification().WasNeverCalled(message); } public void WasCalled() { - _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(); + CreateVerification().WasCalled(); } public void WasCalled(string? message) { - _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(message); + CreateVerification().WasCalled(message); } } diff --git a/TUnit.Mocks/Setup/MethodSetup.cs b/TUnit.Mocks/Setup/MethodSetup.cs index 54dc147bca..9ce416d6c9 100644 --- a/TUnit.Mocks/Setup/MethodSetup.cs +++ b/TUnit.Mocks/Setup/MethodSetup.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using System.ComponentModel; using System.Runtime.CompilerServices; using TUnit.Mocks.Arguments; @@ -78,13 +79,36 @@ public string? TransitionTarget [EditorBrowsable(EditorBrowsableState.Never)] public string MemberName { get; } + /// + /// For a generic method, the configured type arguments (concrete types, or + /// / wildcards). Default for a + /// non-generic method, in which case the setup matches regardless of call-site type arguments. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public ImmutableArray TypeArguments { get; } + public MethodSetup(int memberId, IArgumentMatcher[] matchers, string memberName = "") + : this(memberId, matchers, memberName, default) + { + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public MethodSetup(int memberId, IArgumentMatcher[] matchers, string memberName, ImmutableArray typeArguments) { MemberId = memberId; _matchers = matchers; MemberName = memberName; + TypeArguments = typeArguments; } + /// + /// True if this setup's configured type arguments match a call made with + /// . Non-generic setups (default ) match any call. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool TypeArgumentsMatch(ImmutableArray callTypeArgs) + => TypeArgumentMatching.Matches(TypeArguments, callTypeArgs); + private RareState EnsureRareState() { var existing = Volatile.Read(ref _rareState); diff --git a/TUnit.Mocks/TypeArgumentMatching.cs b/TUnit.Mocks/TypeArgumentMatching.cs new file mode 100644 index 0000000000..cb062ddfc0 --- /dev/null +++ b/TUnit.Mocks/TypeArgumentMatching.cs @@ -0,0 +1,66 @@ +using System.Collections.Immutable; +using TUnit.Mocks.Arguments; + +namespace TUnit.Mocks; + +/// +/// Shared matching logic for a generic method's type arguments. Setups and verifications record the +/// type arguments they were configured with (or / +/// wildcards); recorded calls record the concrete closed types. A non-generic member carries a +/// default and always matches. +/// +internal static class TypeArgumentMatching +{ + /// True if is a recognized "match any type argument" wildcard. + public static bool IsAnyMarker(Type type) + => type == typeof(AnyType) || type == typeof(AnyValueType); + + /// + /// Determines whether a setup configured with matches a call + /// made with . A default setup array (non-generic method, + /// or generic method whose setup did not specify type args) matches any call. A default + /// call array (the dispatch path did not supply type arguments — e.g. partial/wrap virtual + /// methods) is not discriminated, so it matches any setup. Otherwise the arity must match and + /// each slot must be equal or a wildcard marker. + /// + public static bool Matches(ImmutableArray setupTypeArgs, ImmutableArray callTypeArgs) + { + if (setupTypeArgs.IsDefault || callTypeArgs.IsDefault) + { + return true; + } + + if (callTypeArgs.Length != setupTypeArgs.Length) + { + return false; + } + + for (var i = 0; i < setupTypeArgs.Length; i++) + { + var slot = setupTypeArgs[i]; + if (!IsAnyMarker(slot) && slot != callTypeArgs[i]) + { + return false; + } + } + + return true; + } + + /// + /// Formats type arguments as <T1, T2> for failure messages; empty string for a + /// non-generic member (default or empty array). + /// + public static string FormatForDiagnostics(ImmutableArray typeArguments) + => typeArguments.IsDefaultOrEmpty + ? "" + : "<" + string.Join(", ", typeArguments.Select(FriendlyTypeName)) + ">"; + + /// Short type name for diagnostics, without the CLR arity suffix (e.g. List, not List`1). + private static string FriendlyTypeName(Type type) + { + var name = type.Name; + var tick = name.IndexOf('`'); + return tick < 0 ? name : name.Substring(0, tick); + } +} diff --git a/TUnit.Mocks/TypeArguments.cs b/TUnit.Mocks/TypeArguments.cs new file mode 100644 index 0000000000..1f6354a3b0 --- /dev/null +++ b/TUnit.Mocks/TypeArguments.cs @@ -0,0 +1,39 @@ +using System.Collections.Immutable; +using System.ComponentModel; + +namespace TUnit.Mocks; + +/// +/// Per-closed-type cache of a generic method's type-argument array, so a generic-method dispatch does +/// not allocate a fresh array on every call. Generated code references +/// TypeArguments.Of<T>.Value for the common 1–4 type-argument cases; higher arities fall +/// back to a per-call literal. Each Value is shared and +/// immutable. Public for generated code access. Not intended for direct use. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class TypeArguments +{ + [EditorBrowsable(EditorBrowsableState.Never)] + public static class Of + { + public static readonly ImmutableArray Value = ImmutableArray.Create(typeof(T)); + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public static class Of + { + public static readonly ImmutableArray Value = ImmutableArray.Create(typeof(T1), typeof(T2)); + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public static class Of + { + public static readonly ImmutableArray Value = ImmutableArray.Create(typeof(T1), typeof(T2), typeof(T3)); + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public static class Of + { + public static readonly ImmutableArray Value = ImmutableArray.Create(typeof(T1), typeof(T2), typeof(T3), typeof(T4)); + } +} diff --git a/TUnit.Mocks/Verification/CallRecord.cs b/TUnit.Mocks/Verification/CallRecord.cs index 3741aa43ad..a32d958ffd 100644 --- a/TUnit.Mocks/Verification/CallRecord.cs +++ b/TUnit.Mocks/Verification/CallRecord.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using System.ComponentModel; using TUnit.Mocks.Arguments; @@ -17,11 +18,21 @@ public sealed class CallRecord /// [EditorBrowsable(EditorBrowsableState.Never)] public CallRecord(int memberId, string memberName, object?[] arguments, long sequenceNumber) + : this(memberId, memberName, arguments, sequenceNumber, default) + { + } + + /// + /// Creates a call record with pre-boxed arguments and generic-method type arguments. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public CallRecord(int memberId, string memberName, object?[] arguments, long sequenceNumber, ImmutableArray typeArguments) { MemberId = memberId; MemberName = memberName; _arguments = arguments; SequenceNumber = sequenceNumber; + TypeArguments = typeArguments; } /// @@ -45,6 +56,12 @@ public CallRecord(int memberId, string memberName, IArgumentStore store, long se /// The global sequence number for cross-mock ordering. public long SequenceNumber { get; } + /// + /// For a generic method, the concrete closed type arguments the call was made with; default for a + /// non-generic member. Used by verification to discriminate calls by type argument. + /// + public ImmutableArray TypeArguments { get; } + /// /// The arguments passed to the call. Lazily materialized from the argument store if one was provided. /// diff --git a/TUnit.Mocks/Verification/CallVerificationBuilder.cs b/TUnit.Mocks/Verification/CallVerificationBuilder.cs index eb79f33c41..74c6266c47 100644 --- a/TUnit.Mocks/Verification/CallVerificationBuilder.cs +++ b/TUnit.Mocks/Verification/CallVerificationBuilder.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using System.ComponentModel; using TUnit.Mocks.Arguments; using TUnit.Mocks.Exceptions; @@ -15,13 +16,20 @@ public sealed class CallVerificationBuilder : ICallVerification where T : cla private readonly int _memberId; private readonly string _memberName; private readonly IArgumentMatcher[] _matchers; + private readonly ImmutableArray _typeArguments; public CallVerificationBuilder(MockEngine engine, int memberId, string memberName, IArgumentMatcher[] matchers) + : this(engine, memberId, memberName, matchers, default) + { + } + + public CallVerificationBuilder(MockEngine engine, int memberId, string memberName, IArgumentMatcher[] matchers, ImmutableArray typeArguments) { _engine = engine; _memberId = memberId; _memberName = memberName; _matchers = matchers; + _typeArguments = typeArguments; } /// @@ -42,15 +50,17 @@ public void WasCalled(Times times, string? message) } var allCalls = _engine.GetAllCalls(); - OrderedVerification.RecordExpectation(_memberId, _memberName, _matchers, times, allCalls); + OrderedVerification.RecordExpectation(_memberId, _memberName, _matchers, _typeArguments, times, allCalls); return; } - // Fast path: when no argument matchers, use the per-member call counter directly. + // Fast path: when no argument matchers (and no type-argument filter), use the per-member + // call counter directly. A type-argument filter forces the per-record path below, since the + // member counter aggregates calls across all type arguments. // Note: the count is read lock-free, then MarkCallsVerified acquires the lock. // Calls recorded between these two steps will be marked verified but weren't counted. // This is safe because verification should only run after all calls have completed. - if (_matchers.Length == 0) + if (_matchers.Length == 0 && _typeArguments.IsDefault) { var totalCount = _engine.GetCallCountFor(_memberId); if (!times.Matches(totalCount)) @@ -113,7 +123,7 @@ private int CountMatchingBuffer(CallRecordBuffer buffer) var count = 0; for (int i = 0; i < bufferCount; i++) { - if (MatchesArguments(items[i]!.Arguments)) + if (MatchesCall(items[i]!)) { count++; } @@ -127,13 +137,17 @@ private void MarkMatchingBuffer(CallRecordBuffer buffer) for (int i = 0; i < bufferCount; i++) { var record = items[i]!; - if (MatchesArguments(record.Arguments)) + if (MatchesCall(record)) { record.IsVerified = true; } } } + private bool MatchesCall(CallRecord record) + => MatchesArguments(record.Arguments) + && TypeArgumentMatching.Matches(_typeArguments, record.TypeArguments); + private bool MatchesArguments(object?[] arguments) { if (_matchers.Length != arguments.Length) @@ -154,6 +168,7 @@ private bool MatchesArguments(object?[] arguments) private string FormatExpectedCall() { var argDescriptions = string.Join(", ", _matchers.Select(m => m.Describe())); - return $"{_memberName}({argDescriptions})"; + var typeArgs = TypeArgumentMatching.FormatForDiagnostics(_typeArguments); + return $"{_memberName}{typeArgs}({argDescriptions})"; } } diff --git a/TUnit.Mocks/Verification/OrderedVerification.cs b/TUnit.Mocks/Verification/OrderedVerification.cs index f4def956ca..0b312e6b77 100644 --- a/TUnit.Mocks/Verification/OrderedVerification.cs +++ b/TUnit.Mocks/Verification/OrderedVerification.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using TUnit.Mocks.Arguments; using TUnit.Mocks.Exceptions; @@ -21,9 +22,9 @@ public static class OrderedVerification /// Records an expectation during ordered verification collection. /// Called by and overloads. /// - internal static void RecordExpectation(int memberId, string memberName, IArgumentMatcher[] matchers, Times times, IReadOnlyList allCalls) + internal static void RecordExpectation(int memberId, string memberName, IArgumentMatcher[] matchers, ImmutableArray typeArguments, Times times, IReadOnlyList allCalls) { - _expectations.Value?.Add(new OrderedCallExpectation(memberId, memberName, matchers, times, allCalls)); + _expectations.Value?.Add(new OrderedCallExpectation(memberId, memberName, matchers, typeArguments, times, allCalls)); } /// @@ -163,7 +164,9 @@ private static List FindMatchingCalls(OrderedCallExpectation expecta var result = new List(); foreach (var call in expectation.AllCalls) { - if (call.MemberId == expectation.MemberId && MatchesArguments(call.Arguments, expectation.Matchers)) + if (call.MemberId == expectation.MemberId + && MatchesArguments(call.Arguments, expectation.Matchers) + && TypeArgumentMatching.Matches(expectation.TypeArguments, call.TypeArguments)) { result.Add(call); } @@ -195,13 +198,14 @@ private static bool MatchesArguments(object?[] arguments, IArgumentMatcher[] mat private static string FormatExpectedCall(OrderedCallExpectation expectation) { + var typeArgs = TypeArgumentMatching.FormatForDiagnostics(expectation.TypeArguments); if (expectation.Matchers.Length == 0) { - return $"{expectation.MemberName}()"; + return $"{expectation.MemberName}{typeArgs}()"; } var argDescriptions = string.Join(", ", expectation.Matchers.Select(m => m.Describe())); - return $"{expectation.MemberName}({argDescriptions})"; + return $"{expectation.MemberName}{typeArgs}({argDescriptions})"; } } @@ -212,6 +216,7 @@ internal sealed record OrderedCallExpectation( int MemberId, string MemberName, IArgumentMatcher[] Matchers, + ImmutableArray TypeArguments, Times Times, IReadOnlyList AllCalls ); diff --git a/TUnit.Mocks/VoidMockMethodCall.cs b/TUnit.Mocks/VoidMockMethodCall.cs index 9508d448d3..43d774d53f 100644 --- a/TUnit.Mocks/VoidMockMethodCall.cs +++ b/TUnit.Mocks/VoidMockMethodCall.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using System.ComponentModel; using TUnit.Mocks.Arguments; using TUnit.Mocks.Setup; @@ -22,23 +23,39 @@ public sealed class VoidMockMethodCall : IVoidMethodSetup, IVoidSetupChain, ICal private readonly int _memberId; private readonly string _memberName; private readonly IArgumentMatcher[] _matchers; + private readonly ImmutableArray _typeArguments; private VoidMethodSetupBuilder? _builder; private bool _builderInitialized; private object? _builderLock; + // Kept as distinct overloads (not single optional-parameter ctors) to preserve the original + // public/internal binary signatures for backward compatibility. [EditorBrowsable(EditorBrowsableState.Never)] public VoidMockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers) - : this(engine, memberId, memberName, matchers, eagerRegister: true) + : this(engine, memberId, memberName, matchers, eagerRegister: true, typeArguments: default) + { + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public VoidMockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers, ImmutableArray typeArguments) + : this(engine, memberId, memberName, matchers, eagerRegister: true, typeArguments) { } [EditorBrowsable(EditorBrowsableState.Never)] internal VoidMockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers, bool eagerRegister) + : this(engine, memberId, memberName, matchers, eagerRegister, typeArguments: default) + { + } + + [EditorBrowsable(EditorBrowsableState.Never)] + internal VoidMockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers, bool eagerRegister, ImmutableArray typeArguments) { _engine = engine; _memberId = memberId; _memberName = memberName; _matchers = matchers; + _typeArguments = typeArguments; if (eagerRegister) { _ = EnsureSetup(); @@ -48,7 +65,7 @@ internal VoidMockMethodCall(IMockEngineAccess engine, int memberId, string membe private VoidMethodSetupBuilder EnsureSetup() => LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => { - var setup = new MethodSetup(_memberId, _matchers, _memberName); + var setup = new MethodSetup(_memberId, _matchers, _memberName, _typeArguments); _engine.AddSetup(setup); return new VoidMethodSetupBuilder(setup); })!; @@ -109,33 +126,36 @@ public IVoidMethodSetup Then() // ICallVerification implementation + private ICallVerification CreateVerification() + => MockCallVerification.Create(_engine, _memberId, _memberName, _matchers, _typeArguments); + public void WasCalled(Times times) { - _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times); + CreateVerification().WasCalled(times); } public void WasCalled(Times times, string? message) { - _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times, message); + CreateVerification().WasCalled(times, message); } public void WasNeverCalled() { - _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(); + CreateVerification().WasNeverCalled(); } public void WasNeverCalled(string? message) { - _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(message); + CreateVerification().WasNeverCalled(message); } public void WasCalled() { - _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(); + CreateVerification().WasCalled(); } public void WasCalled(string? message) { - _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(message); + CreateVerification().WasCalled(message); } }