From 94127777071a098d5337be4c3fea4c8fcf58d308 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Tue, 2 Jun 2026 20:45:08 +0100 Subject: [PATCH 01/10] feat(mocks): discriminate generic-method mocks by type argument TUnit.Mocks recorded only (memberId, arg-matchers) as the match key, so a generic method's type arguments were dropped at the runtime boundary. Setups that differ only by type argument (e.g. Greet() vs Greet(), discussion #4981) collided and the last one always won. Thread an optional Type[]? typeArguments (closed typeof(T) at the call site) through setup registration, call dispatch, and verification: - MethodSetup/CallRecord carry the type arguments; MockEngine gains object[]+Type[] HandleCall/HandleCallWithReturn overloads and a TypeArgumentsMatch gate in FindMatchingSetup. CallVerificationBuilder filters recorded calls by type argument. - The source generator emits `new Type[] { typeof(T), ... }` for generic methods, routing them through the fallback dispatch (typed dispatch can't carry type args) while preserving any auto-mock factory. - Add AnyType/AnyValueType wildcard markers (TUnit.Mocks.Arguments) so a setup can match any type argument. Base/interface-constrained type params support exact matching only; partial/wrap virtual methods record no type args and are not discriminated (documented graceful degradation). Non-generic methods carry null and behave exactly as before. Note: FindMatchingSetup keeps a no-default (int, object?[]) overload distinct from the type-arg overload so it still wins resolution over generic FindMatchingSetup. Adds regression + wildcard + verification tests; updates generic-method snapshots. --- ...entUI_Shape_Nullable_Warnings.verified.txt | 24 +++---- ..._Constraints_On_Explicit_Impl.verified.txt | 26 +++---- ...nterface_With_Generic_Methods.verified.txt | 18 ++--- ...terface_With_Obsolete_Members.verified.txt | 6 +- ...nconstrained_Nullable_Generic.verified.txt | 12 ++-- ...c_Constrained_Virtual_Methods.verified.txt | 16 ++--- ...c_Constrained_Virtual_Methods.verified.txt | 8 +-- .../Builders/MockImplBuilder.cs | 58 ++++++++++++---- .../Builders/MockMembersBuilder.cs | 13 +++- TUnit.Mocks.Tests/GenericTests.cs | 69 +++++++++++++++++++ TUnit.Mocks/Arguments/AnyType.cs | 26 +++++++ TUnit.Mocks/IMockEngineAccess.cs | 7 ++ TUnit.Mocks/MockEngine.cs | 63 ++++++++++++++--- TUnit.Mocks/MockMethodCall.cs | 22 ++++-- TUnit.Mocks/Setup/MethodSetup.cs | 23 +++++++ TUnit.Mocks/TypeArgumentMatching.cs | 48 +++++++++++++ TUnit.Mocks/Verification/CallRecord.cs | 16 +++++ .../Verification/CallVerificationBuilder.cs | 21 ++++-- TUnit.Mocks/VoidMockMethodCall.cs | 30 +++++--- 19 files changed, 407 insertions(+), 99 deletions(-) create mode 100644 TUnit.Mocks/Arguments/AnyType.cs create mode 100644 TUnit.Mocks/TypeArgumentMatching.cs 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..8161140df1 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, new global::System.Type[] { typeof(T) }); 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, new global::System.Type[] { typeof(T) }); } #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), new global::System.Type[] { typeof(TData) }); 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), new global::System.Type[] { typeof(TDialog) }); 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, new global::System.Type[] { typeof(TData) }); } 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, new global::System.Type[] { typeof(TData) }); } 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, new global::System.Type[] { typeof(TData) }); } 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, new global::System.Type[] { typeof(TData) }); } 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, new global::System.Type[] { typeof(TDialog) }); } 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, new global::System.Type[] { typeof(TDialog) }); } 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, new global::System.Type[] { typeof(TDialog) }); } 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, new global::System.Type[] { typeof(TDialog) }); } 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..99d0c34ecc 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!, new global::System.Type[] { typeof(T) }); } 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!, new global::System.Type[] { typeof(T) }); } 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, new global::System.Type[] { typeof(T) }); } 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!, new global::System.Type[] { typeof(T) }); } 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!, new global::System.Type[] { typeof(T) }); } 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, new global::System.Type[] { typeof(T) }); } [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, new global::System.Type[] { typeof(T) }); } 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, new global::System.Type[] { typeof(T) }); } 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, new global::System.Type[] { typeof(T) }); } 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, new global::System.Type[] { typeof(T) }); } 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, new global::System.Type[] { typeof(T) }); } 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, new global::System.Type[] { typeof(T) }); } 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, new global::System.Type[] { typeof(T) }); } #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..1db00d454e 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!, new global::System.Type[] { typeof(T) }); } public void Save(T entity) where T : class, new() { - _engine.HandleCall(1, "Save", entity); + _engine.HandleCall(1, "Save", new object?[] { entity }, new global::System.Type[] { typeof(T) }); } 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, new global::System.Type[] { typeof(TInput), typeof(TResult) }); } [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, new global::System.Type[] { typeof(T) }); } 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, new global::System.Type[] { typeof(T) }); } 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, new global::System.Type[] { typeof(T) }); } 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, new global::System.Type[] { typeof(T) }); } 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, new global::System.Type[] { typeof(TInput), typeof(TResult) }); } 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, new global::System.Type[] { typeof(TInput), typeof(TResult) }); } #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..b25bd02303 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, new global::System.Type[] { typeof(TData) }); 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, new global::System.Type[] { typeof(TData) }); } 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, new global::System.Type[] { typeof(TData) }); } 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..5d53f32efb 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, new global::System.Type[] { typeof(T) }); 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, new global::System.Type[] { typeof(T) }); } 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, new global::System.Type[] { typeof(T) }); } [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, new global::System.Type[] { typeof(T) }); } 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, new global::System.Type[] { typeof(T) }); } 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, new global::System.Type[] { typeof(T) }); } #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..13ca882832 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(), new global::System.Type[] { typeof(T) }); } [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, new global::System.Type[] { typeof(T) }); } 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, new global::System.Type[] { typeof(T) }); } 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, new global::System.Type[] { typeof(T) }); } 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, new global::System.Type[] { typeof(T) }); } 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, new global::System.Type[] { typeof(TInput), typeof(TResult) }); } 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, new global::System.Type[] { typeof(TInput), typeof(TResult) }); } 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, new global::System.Type[] { typeof(T) }); } #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..e86adce3e1 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, new global::System.Type[] { typeof(T) }); } 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, new global::System.Type[] { typeof(T) }); } 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, new global::System.Type[] { typeof(T) }); } 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, new global::System.Type[] { typeof(T) }); } #if NET9_0_OR_GREATER diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs index 20f139f6ca..4e62a70ce8 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[] 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,39 @@ 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 new global::System.Type[] { typeof(T), ... } for a generic method's type parameters. + private static string TypeArgumentsArrayLiteral(MockMemberModel method) + => $"new global::System.Type[] {{ {string.Join(", ", method.TypeParameters.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..0f836147aa 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs @@ -969,6 +969,13 @@ 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[] is not selected). The typed wrapper is never generated for generic methods. + var typeArgs = method.IsGenericMethod + ? $", new global::System.Type[] {{ {string.Join(", ", method.TypeParameters.Select(tp => $"typeof({tp.Name})"))} }}" + : ""; + if (useTypedWrapper) { var wrapperName = MockImplBuilder.GetGeneratedTypeName(GetWrapperName(safeName, method), model); @@ -976,15 +983,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..dfd774d628 100644 --- a/TUnit.Mocks.Tests/GenericTests.cs +++ b/TUnit.Mocks.Tests/GenericTests.cs @@ -13,6 +13,15 @@ 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; +} + public class Customer { public int Id { get; set; } @@ -24,6 +33,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 +157,61 @@ 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)); + } } diff --git a/TUnit.Mocks/Arguments/AnyType.cs b/TUnit.Mocks/Arguments/AnyType.cs new file mode 100644 index 0000000000..c8eb3cf4d6 --- /dev/null +++ b/TUnit.Mocks/Arguments/AnyType.cs @@ -0,0 +1,26 @@ +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. +/// +public struct AnyValueType +{ +} diff --git a/TUnit.Mocks/IMockEngineAccess.cs b/TUnit.Mocks/IMockEngineAccess.cs index 6089835ddb..d36c348f7e 100644 --- a/TUnit.Mocks/IMockEngineAccess.cs +++ b/TUnit.Mocks/IMockEngineAccess.cs @@ -24,6 +24,13 @@ public interface IMockEngineAccess /// Creates a call verification builder for the specified member. ICallVerification CreateVerification(int memberId, string memberName, IArgumentMatcher[] matchers); + /// + /// Creates a call verification builder for a generic-method member, filtering recorded calls by + /// the supplied type arguments (concrete types or / + /// wildcards). + /// + ICallVerification CreateVerification(int memberId, string memberName, IArgumentMatcher[] matchers, Type[]? typeArguments); + /// Registers a callback that fires when a handler subscribes to the named event. void OnSubscribe(string eventName, Action callback); diff --git a/TUnit.Mocks/MockEngine.cs b/TUnit.Mocks/MockEngine.cs index b5497a6953..fa0cb12fc3 100644 --- a/TUnit.Mocks/MockEngine.cs +++ b/TUnit.Mocks/MockEngine.cs @@ -224,13 +224,27 @@ private void EnsureSetupArrayCapacity(int memberId) ICallVerification IMockEngineAccess.CreateVerification(int memberId, string memberName, IArgumentMatcher[] matchers) => new CallVerificationBuilder(this, memberId, memberName, matchers); + ICallVerification IMockEngineAccess.CreateVerification(int memberId, string memberName, IArgumentMatcher[] matchers, Type[]? 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, null); + + /// + /// 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, Type[] typeArguments) + => HandleCallCore(memberId, memberName, args, typeArguments); + + private void HandleCallCore(int memberId, string memberName, object?[] args, Type[]? 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 +252,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 +296,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, null); [EditorBrowsable(EditorBrowsableState.Never)] public TReturn HandleCallWithReturn(int memberId, string memberName, object?[] args, TReturn defaultValue, Func? autoMockFactory) + => HandleCallWithReturnCore(memberId, memberName, args, defaultValue, autoMockFactory, null); + + /// + /// 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, Type[] 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, Type[] typeArguments) + => HandleCallWithReturnCore(memberId, memberName, args, defaultValue, autoMockFactory, typeArguments); + + private TReturn HandleCallWithReturnCore(int memberId, string memberName, object?[] args, TReturn defaultValue, Func? autoMockFactory, Type[]? 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 +785,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, Type[]? 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 +911,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, (Type[]?)null); + + private (bool SetupFound, IBehavior? Behavior, MethodSetup? Setup) FindMatchingSetup(int memberId, object?[] args, Type[]? typeArguments) { // Rebuild snapshots if setup phase just ended (batches all ToArray work into one pass) if (_hasStaleSetups) @@ -887,7 +930,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 +950,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 +961,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, Type[]? typeArguments = null) { lock (Lock) { @@ -942,7 +987,7 @@ private void RebuildStaleSnapshots() continue; } - if (setup.Matches(args)) + if (setup.Matches(args) && setup.TypeArgumentsMatch(typeArguments)) { setup.IncrementInvokeCount(); setup.ApplyCaptures(args); diff --git a/TUnit.Mocks/MockMethodCall.cs b/TUnit.Mocks/MockMethodCall.cs index 2cf0972255..5a8793138b 100644 --- a/TUnit.Mocks/MockMethodCall.cs +++ b/TUnit.Mocks/MockMethodCall.cs @@ -20,23 +20,31 @@ public sealed class MockMethodCall : IMethodSetup, ISetupChain private readonly int _memberId; private readonly string _memberName; private readonly IArgumentMatcher[] _matchers; + private readonly Type[]? _typeArguments; private MethodSetupBuilder? _builder; private bool _builderInitialized; private object? _builderLock; [EditorBrowsable(EditorBrowsableState.Never)] public MockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers) + : this(engine, memberId, memberName, matchers, null) + { + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public MockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers, Type[]? 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); })!; @@ -111,31 +119,31 @@ public IMethodSetup Then() public void WasCalled(Times times) { - _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times); + _engine.CreateVerification(_memberId, _memberName, _matchers, _typeArguments).WasCalled(times); } public void WasCalled(Times times, string? message) { - _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times, message); + _engine.CreateVerification(_memberId, _memberName, _matchers, _typeArguments).WasCalled(times, message); } public void WasNeverCalled() { - _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(); + _engine.CreateVerification(_memberId, _memberName, _matchers, _typeArguments).WasNeverCalled(); } public void WasNeverCalled(string? message) { - _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(message); + _engine.CreateVerification(_memberId, _memberName, _matchers, _typeArguments).WasNeverCalled(message); } public void WasCalled() { - _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(); + _engine.CreateVerification(_memberId, _memberName, _matchers, _typeArguments).WasCalled(); } public void WasCalled(string? message) { - _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(message); + _engine.CreateVerification(_memberId, _memberName, _matchers, _typeArguments).WasCalled(message); } } diff --git a/TUnit.Mocks/Setup/MethodSetup.cs b/TUnit.Mocks/Setup/MethodSetup.cs index 54dc147bca..2f6f265970 100644 --- a/TUnit.Mocks/Setup/MethodSetup.cs +++ b/TUnit.Mocks/Setup/MethodSetup.cs @@ -78,13 +78,36 @@ public string? TransitionTarget [EditorBrowsable(EditorBrowsableState.Never)] public string MemberName { get; } + /// + /// For a generic method, the configured type arguments (concrete types, or + /// / wildcards). Null for a + /// non-generic method, in which case the setup matches regardless of call-site type arguments. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public Type[]? TypeArguments { get; } + public MethodSetup(int memberId, IArgumentMatcher[] matchers, string memberName = "") + : this(memberId, matchers, memberName, null) + { + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public MethodSetup(int memberId, IArgumentMatcher[] matchers, string memberName, Type[]? 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 (null ) match any call. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool TypeArgumentsMatch(Type[]? 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..feca8af3e1 --- /dev/null +++ b/TUnit.Mocks/TypeArgumentMatching.cs @@ -0,0 +1,48 @@ +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 +/// 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 setup (non-generic method, + /// or generic method whose setup did not specify type args) matches any call. A + /// call (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(Type[]? setupTypeArgs, Type[]? callTypeArgs) + { + if (setupTypeArgs is null || callTypeArgs is null) + { + 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; + } +} diff --git a/TUnit.Mocks/Verification/CallRecord.cs b/TUnit.Mocks/Verification/CallRecord.cs index 3741aa43ad..c421d4396a 100644 --- a/TUnit.Mocks/Verification/CallRecord.cs +++ b/TUnit.Mocks/Verification/CallRecord.cs @@ -17,11 +17,21 @@ public sealed class CallRecord /// [EditorBrowsable(EditorBrowsableState.Never)] public CallRecord(int memberId, string memberName, object?[] arguments, long sequenceNumber) + : this(memberId, memberName, arguments, sequenceNumber, null) + { + } + + /// + /// 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, Type[]? typeArguments) { MemberId = memberId; MemberName = memberName; _arguments = arguments; SequenceNumber = sequenceNumber; + TypeArguments = typeArguments; } /// @@ -45,6 +55,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; null for a + /// non-generic member. Used by verification to discriminate calls by type argument. + /// + public Type[]? 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..a3004f3ac5 100644 --- a/TUnit.Mocks/Verification/CallVerificationBuilder.cs +++ b/TUnit.Mocks/Verification/CallVerificationBuilder.cs @@ -15,13 +15,20 @@ public sealed class CallVerificationBuilder : ICallVerification where T : cla private readonly int _memberId; private readonly string _memberName; private readonly IArgumentMatcher[] _matchers; + private readonly Type[]? _typeArguments; public CallVerificationBuilder(MockEngine engine, int memberId, string memberName, IArgumentMatcher[] matchers) + : this(engine, memberId, memberName, matchers, null) + { + } + + public CallVerificationBuilder(MockEngine engine, int memberId, string memberName, IArgumentMatcher[] matchers, Type[]? typeArguments) { _engine = engine; _memberId = memberId; _memberName = memberName; _matchers = matchers; + _typeArguments = typeArguments; } /// @@ -46,11 +53,13 @@ public void WasCalled(Times times, string? message) 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 is null) { var totalCount = _engine.GetCallCountFor(_memberId); if (!times.Matches(totalCount)) @@ -113,7 +122,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 +136,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) diff --git a/TUnit.Mocks/VoidMockMethodCall.cs b/TUnit.Mocks/VoidMockMethodCall.cs index 9508d448d3..162a67b5d0 100644 --- a/TUnit.Mocks/VoidMockMethodCall.cs +++ b/TUnit.Mocks/VoidMockMethodCall.cs @@ -22,23 +22,37 @@ public sealed class VoidMockMethodCall : IVoidMethodSetup, IVoidSetupChain, ICal private readonly int _memberId; private readonly string _memberName; private readonly IArgumentMatcher[] _matchers; + private readonly Type[]? _typeArguments; private VoidMethodSetupBuilder? _builder; private bool _builderInitialized; private object? _builderLock; [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: null) + { + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public VoidMockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers, Type[]? 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: null) + { + } + + [EditorBrowsable(EditorBrowsableState.Never)] + internal VoidMockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers, bool eagerRegister, Type[]? typeArguments) { _engine = engine; _memberId = memberId; _memberName = memberName; _matchers = matchers; + _typeArguments = typeArguments; if (eagerRegister) { _ = EnsureSetup(); @@ -48,7 +62,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); })!; @@ -111,31 +125,31 @@ public IVoidMethodSetup Then() public void WasCalled(Times times) { - _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times); + _engine.CreateVerification(_memberId, _memberName, _matchers, _typeArguments).WasCalled(times); } public void WasCalled(Times times, string? message) { - _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times, message); + _engine.CreateVerification(_memberId, _memberName, _matchers, _typeArguments).WasCalled(times, message); } public void WasNeverCalled() { - _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(); + _engine.CreateVerification(_memberId, _memberName, _matchers, _typeArguments).WasNeverCalled(); } public void WasNeverCalled(string? message) { - _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(message); + _engine.CreateVerification(_memberId, _memberName, _matchers, _typeArguments).WasNeverCalled(message); } public void WasCalled() { - _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(); + _engine.CreateVerification(_memberId, _memberName, _matchers, _typeArguments).WasCalled(); } public void WasCalled(string? message) { - _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(message); + _engine.CreateVerification(_memberId, _memberName, _matchers, _typeArguments).WasCalled(message); } } From afff36782b1b67a81db1f4575e08cd46cf1e3487 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Tue, 2 Jun 2026 20:48:49 +0100 Subject: [PATCH 02/10] test(mocks): add multi-type-parameter generic-method tests Cover discrimination by the full ordered type-argument list (T1,T2 order sensitivity), partial wildcards (one AnyType + one concrete), exact-wins-over- partial-wildcard, and multi-type-param verification including wildcard counts. --- TUnit.Mocks.Tests/GenericTests.cs | 72 +++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/TUnit.Mocks.Tests/GenericTests.cs b/TUnit.Mocks.Tests/GenericTests.cs index dfd774d628..484da965d0 100644 --- a/TUnit.Mocks.Tests/GenericTests.cs +++ b/TUnit.Mocks.Tests/GenericTests.cs @@ -22,6 +22,19 @@ 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; +} + public class Customer { public int Id { get; set; } @@ -214,4 +227,63 @@ public void Generic_Void_Method_Verify_Discriminates_By_Type_Argument() 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 + } } From 8251cee81904805c444505efde068e7da82f5545 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Tue, 2 Jun 2026 20:56:04 +0100 Subject: [PATCH 03/10] refactor(mocks): dedup type-args literal + collapse redundant ctors - Reuse MockImplBuilder.TypeArgumentsArrayLiteral from MockMembersBuilder instead of re-building the 'new Type[] { typeof(T), ... }' string inline. - Collapse MockMethodCall (2->1) and VoidMockMethodCall (4->2) constructors using optional parameters; overload resolution and call sites unchanged. No behavior or generated-output change (snapshots unchanged). --- .../Builders/MockImplBuilder.cs | 2 +- .../Builders/MockMembersBuilder.cs | 2 +- TUnit.Mocks/MockMethodCall.cs | 8 +------- TUnit.Mocks/VoidMockMethodCall.cs | 16 ++-------------- 4 files changed, 5 insertions(+), 23 deletions(-) diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs index 4e62a70ce8..76714fc15c 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs @@ -1415,7 +1415,7 @@ private static string EmitHandleCallWithReturn(bool isTyped, string? typeArgs, s } /// Emits new global::System.Type[] { typeof(T), ... } for a generic method's type parameters. - private static string TypeArgumentsArrayLiteral(MockMemberModel method) + internal static string TypeArgumentsArrayLiteral(MockMemberModel method) => $"new global::System.Type[] {{ {string.Join(", ", method.TypeParameters.Select(tp => $"typeof({tp.Name})"))} }}"; /// Emits a TryHandleCall condition, choosing typed or fallback path. diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs index 0f836147aa..851e2b39ca 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs @@ -973,7 +973,7 @@ private static void EmitReturnConstruction(CodeWriter writer, MockMemberModel me // discriminate calls by type argument. Non-generic methods omit the argument (overload with // the trailing Type[] is not selected). The typed wrapper is never generated for generic methods. var typeArgs = method.IsGenericMethod - ? $", new global::System.Type[] {{ {string.Join(", ", method.TypeParameters.Select(tp => $"typeof({tp.Name})"))} }}" + ? $", {MockImplBuilder.TypeArgumentsArrayLiteral(method)}" : ""; if (useTypedWrapper) diff --git a/TUnit.Mocks/MockMethodCall.cs b/TUnit.Mocks/MockMethodCall.cs index 5a8793138b..7cb2b30ccd 100644 --- a/TUnit.Mocks/MockMethodCall.cs +++ b/TUnit.Mocks/MockMethodCall.cs @@ -26,13 +26,7 @@ public sealed class MockMethodCall : IMethodSetup, ISetupChain private object? _builderLock; [EditorBrowsable(EditorBrowsableState.Never)] - public MockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers) - : this(engine, memberId, memberName, matchers, null) - { - } - - [EditorBrowsable(EditorBrowsableState.Never)] - public MockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers, Type[]? typeArguments) + public MockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers, Type[]? typeArguments = null) { _engine = engine; _memberId = memberId; diff --git a/TUnit.Mocks/VoidMockMethodCall.cs b/TUnit.Mocks/VoidMockMethodCall.cs index 162a67b5d0..c8ab2f9334 100644 --- a/TUnit.Mocks/VoidMockMethodCall.cs +++ b/TUnit.Mocks/VoidMockMethodCall.cs @@ -28,25 +28,13 @@ public sealed class VoidMockMethodCall : IVoidMethodSetup, IVoidSetupChain, ICal private object? _builderLock; [EditorBrowsable(EditorBrowsableState.Never)] - public VoidMockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers) - : this(engine, memberId, memberName, matchers, eagerRegister: true, typeArguments: null) - { - } - - [EditorBrowsable(EditorBrowsableState.Never)] - public VoidMockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers, Type[]? typeArguments) + public VoidMockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers, Type[]? typeArguments = null) : 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: null) - { - } - - [EditorBrowsable(EditorBrowsableState.Never)] - internal VoidMockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers, bool eagerRegister, Type[]? typeArguments) + internal VoidMockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers, bool eagerRegister, Type[]? typeArguments = null) { _engine = engine; _memberId = memberId; From e62b3870b0c92bc7c8a50595259310af6ed72302 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:04:04 +0100 Subject: [PATCH 04/10] perf(mocks): cache generic type-arg arrays + show type args in verify failures Review feedback on PR #6153: - Cache a generic method's type-argument array per closed instantiation (TypeArguments.Of.Value) for the common 1-2 type-param cases, so dispatch no longer allocates a new Type[] on every generic call. Higher arities still emit a per-call literal. (Codacy perf finding #1.) - Include type arguments in MockVerificationException's expected-call text, so a failed Greet() verification reads 'Greet(...)' not 'Greet(...)'. Adds a regression test. (Review finding #5.) --- ...entUI_Shape_Nullable_Warnings.verified.txt | 24 ++++++++--------- ..._Constraints_On_Explicit_Impl.verified.txt | 26 +++++++++--------- ...nterface_With_Generic_Methods.verified.txt | 18 ++++++------- ...terface_With_Obsolete_Members.verified.txt | 6 ++--- ...nconstrained_Nullable_Generic.verified.txt | 12 ++++----- ...c_Constrained_Virtual_Methods.verified.txt | 16 +++++------ ...c_Constrained_Virtual_Methods.verified.txt | 8 +++--- .../Builders/MockImplBuilder.cs | 15 +++++++++-- TUnit.Mocks.Tests/GenericTests.cs | 14 ++++++++++ TUnit.Mocks/TypeArguments.cs | 27 +++++++++++++++++++ .../Verification/CallVerificationBuilder.cs | 5 +++- 11 files changed, 113 insertions(+), 58 deletions(-) create mode 100644 TUnit.Mocks/TypeArguments.cs 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 8161140df1..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, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(T) }); + 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", new object?[] { id, parameters }, default, static __behavior => global::IDialogReferenceMockFactory.CreateAutoMock(__behavior), new global::System.Type[] { typeof(TData) }); + 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", new object?[] { data, parameters }, default!, static __behavior => global::IDialogReferenceMockFactory.CreateAutoMock(__behavior), new global::System.Type[] { typeof(TDialog) }); + 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, new global::System.Type[] { typeof(TData) }); + 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, new global::System.Type[] { typeof(TData) }); + 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, new global::System.Type[] { typeof(TData) }); + 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, new global::System.Type[] { typeof(TData) }); + 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, new global::System.Type[] { typeof(TDialog) }); + 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, new global::System.Type[] { typeof(TDialog) }); + 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, new global::System.Type[] { typeof(TDialog) }); + 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, new global::System.Type[] { typeof(TDialog) }); + 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 99d0c34ecc..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", new object?[] { key }, default!, new global::System.Type[] { typeof(T) }); + 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!, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(T) }); + 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!, new global::System.Type[] { typeof(T) }); + 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!, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(T) }); + 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 1db00d454e..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", new object?[] { id }, default!, new global::System.Type[] { typeof(T) }); + 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", new object?[] { entity }, new global::System.Type[] { typeof(T) }); + _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", new object?[] { input }, default, new global::System.Type[] { typeof(TInput), typeof(TResult) }); + 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, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(TInput), typeof(TResult) }); + 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, new global::System.Type[] { typeof(TInput), typeof(TResult) }); + 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 b25bd02303..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", new object?[] { data }, default, new global::System.Type[] { typeof(TData) }); + 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, new global::System.Type[] { typeof(TData) }); + 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, new global::System.Type[] { typeof(TData) }); + 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 5d53f32efb..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, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(T) }); + 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 13ca882832..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(), new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(TInput), typeof(TResult) }); + 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, new global::System.Type[] { typeof(TInput), typeof(TResult) }); + 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, new global::System.Type[] { typeof(T) }); + 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 e86adce3e1..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, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(T) }); + 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, new global::System.Type[] { typeof(T) }); + 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 76714fc15c..8182ea8b22 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs @@ -1414,9 +1414,20 @@ private static string EmitHandleCallWithReturn(bool isTyped, string? typeArgs, s : $"_engine.HandleCallWithReturn<{returnTypeArg}>({memberId}, \"{memberName}\", {argsArray}, {defaultValue}{FormatAutoMockFactoryArgument(autoMockFactory)})"; } - /// Emits new global::System.Type[] { typeof(T), ... } for a generic method's type parameters. + /// + /// Emits the type-argument array for a generic method. For the common 1–2 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 fresh new global::System.Type[] { typeof(T), ... }. + /// internal static string TypeArgumentsArrayLiteral(MockMemberModel method) - => $"new global::System.Type[] {{ {string.Join(", ", method.TypeParameters.Select(tp => $"typeof({tp.Name})"))} }}"; + { + var typeParams = method.TypeParameters; + if (typeParams.Length is 1 or 2) + { + return $"global::TUnit.Mocks.TypeArguments.Of<{string.Join(", ", typeParams.Select(tp => tp.Name))}>.Value"; + } + return $"new global::System.Type[] {{ {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.Tests/GenericTests.cs b/TUnit.Mocks.Tests/GenericTests.cs index 484da965d0..91b82669c2 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; @@ -286,4 +287,17 @@ public void Generic_Void_Method_Two_Type_Params_Verify_Discriminates() 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"); + } } diff --git a/TUnit.Mocks/TypeArguments.cs b/TUnit.Mocks/TypeArguments.cs new file mode 100644 index 0000000000..c554d6d41c --- /dev/null +++ b/TUnit.Mocks/TypeArguments.cs @@ -0,0 +1,27 @@ +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 [] on every call. The arrays are immutable in practice +/// (only read by matching/verification), so sharing one instance per closed generic instantiation is +/// safe. Generated code references TypeArguments.Of<T>.Value for the common 1–2 type-arg +/// cases; higher arities fall back to a per-call new Type[] literal. 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 Type[] Value = { typeof(T) }; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public static class Of + { + public static readonly Type[] Value = { typeof(T1), typeof(T2) }; + } +} diff --git a/TUnit.Mocks/Verification/CallVerificationBuilder.cs b/TUnit.Mocks/Verification/CallVerificationBuilder.cs index a3004f3ac5..02195fa42f 100644 --- a/TUnit.Mocks/Verification/CallVerificationBuilder.cs +++ b/TUnit.Mocks/Verification/CallVerificationBuilder.cs @@ -167,6 +167,9 @@ private bool MatchesArguments(object?[] arguments) private string FormatExpectedCall() { var argDescriptions = string.Join(", ", _matchers.Select(m => m.Describe())); - return $"{_memberName}({argDescriptions})"; + var typeArgs = _typeArguments is { Length: > 0 } ta + ? "<" + string.Join(", ", ta.Select(t => t.Name)) + ">" + : ""; + return $"{_memberName}{typeArgs}({argDescriptions})"; } } From 463a2e00f8391d39e594a4d46ba6bf19a5ef394c Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:15:40 +0100 Subject: [PATCH 05/10] perf+dx(mocks): extend type-arg cache to arity 3-4, friendlier verify names, AnyValueType test Follow-up to PR #6153 review: - Extend TypeArguments.Of<> cache to 3 and 4 type parameters (was 1-2), so 3-4 type-param generic methods also avoid per-call Type[] allocation. Doc the shared arrays as read-only / never-mutate. - Strip the CLR arity suffix in verification failure messages (List, not List`1). - Add tests proving AnyValueType works as a wildcard for 'where T : struct' parameters (not dead code) and that struct-constrained methods discriminate by exact type argument. --- .../Builders/MockImplBuilder.cs | 2 +- TUnit.Mocks.Tests/GenericTests.cs | 35 +++++++++++++++++++ TUnit.Mocks/TypeArguments.cs | 25 ++++++++++--- .../Verification/CallVerificationBuilder.cs | 10 +++++- 4 files changed, 65 insertions(+), 7 deletions(-) diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs index 8182ea8b22..805303b21e 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs @@ -1422,7 +1422,7 @@ private static string EmitHandleCallWithReturn(bool isTyped, string? typeArgs, s internal static string TypeArgumentsArrayLiteral(MockMemberModel method) { var typeParams = method.TypeParameters; - if (typeParams.Length is 1 or 2) + if (typeParams.Length is >= 1 and <= 4) { return $"global::TUnit.Mocks.TypeArguments.Of<{string.Join(", ", typeParams.Select(tp => tp.Name))}>.Value"; } diff --git a/TUnit.Mocks.Tests/GenericTests.cs b/TUnit.Mocks.Tests/GenericTests.cs index 91b82669c2..e09604cf5e 100644 --- a/TUnit.Mocks.Tests/GenericTests.cs +++ b/TUnit.Mocks.Tests/GenericTests.cs @@ -36,6 +36,15 @@ public interface IMultiGeneric 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; } @@ -300,4 +309,30 @@ public async Task Generic_Verification_Failure_Message_Includes_Type_Argument() // 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"); + } } diff --git a/TUnit.Mocks/TypeArguments.cs b/TUnit.Mocks/TypeArguments.cs index c554d6d41c..1c6c1436e4 100644 --- a/TUnit.Mocks/TypeArguments.cs +++ b/TUnit.Mocks/TypeArguments.cs @@ -4,11 +4,10 @@ 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 [] on every call. The arrays are immutable in practice -/// (only read by matching/verification), so sharing one instance per closed generic instantiation is -/// safe. Generated code references TypeArguments.Of<T>.Value for the common 1–2 type-arg -/// cases; higher arities fall back to a per-call new Type[] literal. Public for generated code -/// access. Not intended for direct use. +/// not allocate a fresh [] 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 new Type[] literal. Public for generated code access. Not intended for +/// direct use. /// [EditorBrowsable(EditorBrowsableState.Never)] public static class TypeArguments @@ -16,12 +15,28 @@ public static class TypeArguments [EditorBrowsable(EditorBrowsableState.Never)] public static class Of { + /// Shared, read-only — must never be mutated (matching only reads it). public static readonly Type[] Value = { typeof(T) }; } [EditorBrowsable(EditorBrowsableState.Never)] public static class Of { + /// Shared, read-only — must never be mutated (matching only reads it). public static readonly Type[] Value = { typeof(T1), typeof(T2) }; } + + [EditorBrowsable(EditorBrowsableState.Never)] + public static class Of + { + /// Shared, read-only — must never be mutated (matching only reads it). + public static readonly Type[] Value = { typeof(T1), typeof(T2), typeof(T3) }; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public static class Of + { + /// Shared, read-only — must never be mutated (matching only reads it). + public static readonly Type[] Value = { typeof(T1), typeof(T2), typeof(T3), typeof(T4) }; + } } diff --git a/TUnit.Mocks/Verification/CallVerificationBuilder.cs b/TUnit.Mocks/Verification/CallVerificationBuilder.cs index 02195fa42f..aceb98839f 100644 --- a/TUnit.Mocks/Verification/CallVerificationBuilder.cs +++ b/TUnit.Mocks/Verification/CallVerificationBuilder.cs @@ -168,8 +168,16 @@ private string FormatExpectedCall() { var argDescriptions = string.Join(", ", _matchers.Select(m => m.Describe())); var typeArgs = _typeArguments is { Length: > 0 } ta - ? "<" + string.Join(", ", ta.Select(t => t.Name)) + ">" + ? "<" + string.Join(", ", ta.Select(FriendlyTypeName)) + ">" : ""; return $"{_memberName}{typeArgs}({argDescriptions})"; } + + /// 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); + } } From 55531137b39b069ab9e2cafde9f533a03cfe2b9a Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:50:01 +0100 Subject: [PATCH 06/10] fix(mocks): keep generic type-arg support fully backwards compatible Earlier commits in this PR introduced two breaking changes to pre-existing public types. Reverted while keeping the feature: - IMockEngineAccess: removed the added CreateVerification overload (a source break for any external implementer of this public interface). Generic type-argument verification now routes through a new internal ITypeArgumentVerificationFactory, implemented only by MockEngine and reached via an internal cast. Non-generic verification stays on the unchanged public surface. - MockMethodCall / VoidMockMethodCall: restored the original constructor signatures that the simplify pass had collapsed into optional-parameter ctors (a binary break), adding the type-argument overloads alongside instead of replacing them. The public API delta for the whole PR is now additions-only: new constructor overloads plus the new public types AnyType, AnyValueType and TypeArguments. No generated-code or snapshot changes. 1013 integration, 62 snapshot and 30 analyzer tests pass; runtime builds on all TFMs including netstandard2.0. --- TUnit.Mocks/IMockEngineAccess.cs | 7 ---- .../ITypeArgumentVerificationFactory.cs | 19 ++++++++++ TUnit.Mocks/MockEngine.cs | 4 +- TUnit.Mocks/MockMethodCall.cs | 29 +++++++++++---- TUnit.Mocks/VoidMockMethodCall.cs | 37 +++++++++++++++---- 5 files changed, 72 insertions(+), 24 deletions(-) create mode 100644 TUnit.Mocks/ITypeArgumentVerificationFactory.cs diff --git a/TUnit.Mocks/IMockEngineAccess.cs b/TUnit.Mocks/IMockEngineAccess.cs index d36c348f7e..6089835ddb 100644 --- a/TUnit.Mocks/IMockEngineAccess.cs +++ b/TUnit.Mocks/IMockEngineAccess.cs @@ -24,13 +24,6 @@ public interface IMockEngineAccess /// Creates a call verification builder for the specified member. ICallVerification CreateVerification(int memberId, string memberName, IArgumentMatcher[] matchers); - /// - /// Creates a call verification builder for a generic-method member, filtering recorded calls by - /// the supplied type arguments (concrete types or / - /// wildcards). - /// - ICallVerification CreateVerification(int memberId, string memberName, IArgumentMatcher[] matchers, Type[]? typeArguments); - /// Registers a callback that fires when a handler subscribes to the named event. void OnSubscribe(string eventName, Action callback); diff --git a/TUnit.Mocks/ITypeArgumentVerificationFactory.cs b/TUnit.Mocks/ITypeArgumentVerificationFactory.cs new file mode 100644 index 0000000000..6ea1122b06 --- /dev/null +++ b/TUnit.Mocks/ITypeArgumentVerificationFactory.cs @@ -0,0 +1,19 @@ +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; the only implementer is +/// , and generic mock-call wrappers reach it via an internal cast. +/// +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, Type[]? typeArguments); +} diff --git a/TUnit.Mocks/MockEngine.cs b/TUnit.Mocks/MockEngine.cs index fa0cb12fc3..babe75791f 100644 --- a/TUnit.Mocks/MockEngine.cs +++ b/TUnit.Mocks/MockEngine.cs @@ -21,7 +21,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,7 +224,7 @@ private void EnsureSetupArrayCapacity(int memberId) ICallVerification IMockEngineAccess.CreateVerification(int memberId, string memberName, IArgumentMatcher[] matchers) => new CallVerificationBuilder(this, memberId, memberName, matchers); - ICallVerification IMockEngineAccess.CreateVerification(int memberId, string memberName, IArgumentMatcher[] matchers, Type[]? typeArguments) + ICallVerification ITypeArgumentVerificationFactory.CreateVerification(int memberId, string memberName, IArgumentMatcher[] matchers, Type[]? typeArguments) => new CallVerificationBuilder(this, memberId, memberName, matchers, typeArguments); /// diff --git a/TUnit.Mocks/MockMethodCall.cs b/TUnit.Mocks/MockMethodCall.cs index 7cb2b30ccd..fb9d458ebf 100644 --- a/TUnit.Mocks/MockMethodCall.cs +++ b/TUnit.Mocks/MockMethodCall.cs @@ -25,8 +25,16 @@ public sealed class MockMethodCall : IMethodSetup, ISetupChain 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, Type[]? typeArguments = null) + public MockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers) + : this(engine, memberId, memberName, matchers, null) + { + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public MockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers, Type[]? typeArguments) { _engine = engine; _memberId = memberId; @@ -111,33 +119,40 @@ public IMethodSetup Then() // ICallVerification implementation + // Non-generic calls use the public engine surface unchanged; generic calls route through the + // internal type-argument-aware factory (the engine is always a MockEngine, which implements it). + private ICallVerification CreateVerification() + => _typeArguments is null + ? _engine.CreateVerification(_memberId, _memberName, _matchers) + : ((ITypeArgumentVerificationFactory)_engine).CreateVerification(_memberId, _memberName, _matchers, _typeArguments); + public void WasCalled(Times times) { - _engine.CreateVerification(_memberId, _memberName, _matchers, _typeArguments).WasCalled(times); + CreateVerification().WasCalled(times); } public void WasCalled(Times times, string? message) { - _engine.CreateVerification(_memberId, _memberName, _matchers, _typeArguments).WasCalled(times, message); + CreateVerification().WasCalled(times, message); } public void WasNeverCalled() { - _engine.CreateVerification(_memberId, _memberName, _matchers, _typeArguments).WasNeverCalled(); + CreateVerification().WasNeverCalled(); } public void WasNeverCalled(string? message) { - _engine.CreateVerification(_memberId, _memberName, _matchers, _typeArguments).WasNeverCalled(message); + CreateVerification().WasNeverCalled(message); } public void WasCalled() { - _engine.CreateVerification(_memberId, _memberName, _matchers, _typeArguments).WasCalled(); + CreateVerification().WasCalled(); } public void WasCalled(string? message) { - _engine.CreateVerification(_memberId, _memberName, _matchers, _typeArguments).WasCalled(message); + CreateVerification().WasCalled(message); } } diff --git a/TUnit.Mocks/VoidMockMethodCall.cs b/TUnit.Mocks/VoidMockMethodCall.cs index c8ab2f9334..a8f2ab41fd 100644 --- a/TUnit.Mocks/VoidMockMethodCall.cs +++ b/TUnit.Mocks/VoidMockMethodCall.cs @@ -27,14 +27,28 @@ public sealed class VoidMockMethodCall : IVoidMethodSetup, IVoidSetupChain, ICal 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, Type[]? typeArguments = null) + public VoidMockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers) + : this(engine, memberId, memberName, matchers, eagerRegister: true, typeArguments: null) + { + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public VoidMockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers, Type[]? typeArguments) : this(engine, memberId, memberName, matchers, eagerRegister: true, typeArguments) { } [EditorBrowsable(EditorBrowsableState.Never)] - internal VoidMockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers, bool eagerRegister, Type[]? typeArguments = null) + internal VoidMockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers, bool eagerRegister) + : this(engine, memberId, memberName, matchers, eagerRegister, typeArguments: null) + { + } + + [EditorBrowsable(EditorBrowsableState.Never)] + internal VoidMockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers, bool eagerRegister, Type[]? typeArguments) { _engine = engine; _memberId = memberId; @@ -111,33 +125,40 @@ public IVoidMethodSetup Then() // ICallVerification implementation + // Non-generic calls use the public engine surface unchanged; generic calls route through the + // internal type-argument-aware factory (the engine is always a MockEngine, which implements it). + private ICallVerification CreateVerification() + => _typeArguments is null + ? _engine.CreateVerification(_memberId, _memberName, _matchers) + : ((ITypeArgumentVerificationFactory)_engine).CreateVerification(_memberId, _memberName, _matchers, _typeArguments); + public void WasCalled(Times times) { - _engine.CreateVerification(_memberId, _memberName, _matchers, _typeArguments).WasCalled(times); + CreateVerification().WasCalled(times); } public void WasCalled(Times times, string? message) { - _engine.CreateVerification(_memberId, _memberName, _matchers, _typeArguments).WasCalled(times, message); + CreateVerification().WasCalled(times, message); } public void WasNeverCalled() { - _engine.CreateVerification(_memberId, _memberName, _matchers, _typeArguments).WasNeverCalled(); + CreateVerification().WasNeverCalled(); } public void WasNeverCalled(string? message) { - _engine.CreateVerification(_memberId, _memberName, _matchers, _typeArguments).WasNeverCalled(message); + CreateVerification().WasNeverCalled(message); } public void WasCalled() { - _engine.CreateVerification(_memberId, _memberName, _matchers, _typeArguments).WasCalled(); + CreateVerification().WasCalled(); } public void WasCalled(string? message) { - _engine.CreateVerification(_memberId, _memberName, _matchers, _typeArguments).WasCalled(message); + CreateVerification().WasCalled(message); } } From d95bfee8faef81a62bac0318e0c9d90fd1ba8410 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Tue, 2 Jun 2026 22:17:48 +0100 Subject: [PATCH 07/10] docs(mocks): consolidate repeated read-only note in TypeArguments Fold the identical per-field 'shared, read-only' comment on the four Of<>.Value fields into the class-level doc. Comment-only. --- TUnit.Mocks/TypeArguments.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/TUnit.Mocks/TypeArguments.cs b/TUnit.Mocks/TypeArguments.cs index 1c6c1436e4..1e6002c46f 100644 --- a/TUnit.Mocks/TypeArguments.cs +++ b/TUnit.Mocks/TypeArguments.cs @@ -6,7 +6,8 @@ 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 [] 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 new Type[] literal. Public for generated code access. Not intended for +/// back to a per-call new Type[] literal. Each Value is shared and read-only — it must +/// never be mutated (matching only reads it). Public for generated code access. Not intended for /// direct use. /// [EditorBrowsable(EditorBrowsableState.Never)] @@ -15,28 +16,24 @@ public static class TypeArguments [EditorBrowsable(EditorBrowsableState.Never)] public static class Of { - /// Shared, read-only — must never be mutated (matching only reads it). public static readonly Type[] Value = { typeof(T) }; } [EditorBrowsable(EditorBrowsableState.Never)] public static class Of { - /// Shared, read-only — must never be mutated (matching only reads it). public static readonly Type[] Value = { typeof(T1), typeof(T2) }; } [EditorBrowsable(EditorBrowsableState.Never)] public static class Of { - /// Shared, read-only — must never be mutated (matching only reads it). public static readonly Type[] Value = { typeof(T1), typeof(T2), typeof(T3) }; } [EditorBrowsable(EditorBrowsableState.Never)] public static class Of { - /// Shared, read-only — must never be mutated (matching only reads it). public static readonly Type[] Value = { typeof(T1), typeof(T2), typeof(T3), typeof(T4) }; } } From 44f3ea14a2699c854792cf57a0023c2efc2a9d61 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 3 Jun 2026 20:24:42 +0100 Subject: [PATCH 08/10] fix(mocks): degrade gracefully instead of casting when engine lacks type-arg verification A custom IMockEngineAccess implementation passed to a generic MockMethodCall/ VoidMockMethodCall previously hit a hard InvalidCastException at verification time. Replace the cast with a type test that falls back to the public, non-filtering verification surface. A default interface method on IMockEngineAccess is not viable while the library targets netstandard2.0. --- TUnit.Mocks/ITypeArgumentVerificationFactory.cs | 6 ++++-- TUnit.Mocks/MockMethodCall.cs | 10 ++++++---- TUnit.Mocks/VoidMockMethodCall.cs | 10 ++++++---- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/TUnit.Mocks/ITypeArgumentVerificationFactory.cs b/TUnit.Mocks/ITypeArgumentVerificationFactory.cs index 6ea1122b06..17f45487b2 100644 --- a/TUnit.Mocks/ITypeArgumentVerificationFactory.cs +++ b/TUnit.Mocks/ITypeArgumentVerificationFactory.cs @@ -6,8 +6,10 @@ 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; the only implementer is -/// , and generic mock-call wrappers reach it via an internal cast. +/// 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 { diff --git a/TUnit.Mocks/MockMethodCall.cs b/TUnit.Mocks/MockMethodCall.cs index fb9d458ebf..f77725cb60 100644 --- a/TUnit.Mocks/MockMethodCall.cs +++ b/TUnit.Mocks/MockMethodCall.cs @@ -120,11 +120,13 @@ public IMethodSetup Then() // ICallVerification implementation // Non-generic calls use the public engine surface unchanged; generic calls route through the - // internal type-argument-aware factory (the engine is always a MockEngine, which implements it). + // internal type-argument-aware factory (always implemented by MockEngine). A custom + // IMockEngineAccess implementation falls back to the public surface, losing type-argument + // filtering but never throwing. private ICallVerification CreateVerification() - => _typeArguments is null - ? _engine.CreateVerification(_memberId, _memberName, _matchers) - : ((ITypeArgumentVerificationFactory)_engine).CreateVerification(_memberId, _memberName, _matchers, _typeArguments); + => _typeArguments is not null && _engine is ITypeArgumentVerificationFactory typeArgFactory + ? typeArgFactory.CreateVerification(_memberId, _memberName, _matchers, _typeArguments) + : _engine.CreateVerification(_memberId, _memberName, _matchers); public void WasCalled(Times times) { diff --git a/TUnit.Mocks/VoidMockMethodCall.cs b/TUnit.Mocks/VoidMockMethodCall.cs index a8f2ab41fd..e8ecab33bc 100644 --- a/TUnit.Mocks/VoidMockMethodCall.cs +++ b/TUnit.Mocks/VoidMockMethodCall.cs @@ -126,11 +126,13 @@ public IVoidMethodSetup Then() // ICallVerification implementation // Non-generic calls use the public engine surface unchanged; generic calls route through the - // internal type-argument-aware factory (the engine is always a MockEngine, which implements it). + // internal type-argument-aware factory (always implemented by MockEngine). A custom + // IMockEngineAccess implementation falls back to the public surface, losing type-argument + // filtering but never throwing. private ICallVerification CreateVerification() - => _typeArguments is null - ? _engine.CreateVerification(_memberId, _memberName, _matchers) - : ((ITypeArgumentVerificationFactory)_engine).CreateVerification(_memberId, _memberName, _matchers, _typeArguments); + => _typeArguments is not null && _engine is ITypeArgumentVerificationFactory typeArgFactory + ? typeArgFactory.CreateVerification(_memberId, _memberName, _matchers, _typeArguments) + : _engine.CreateVerification(_memberId, _memberName, _matchers); public void WasCalled(Times times) { From 0afbe71bb172fd6d29c8707b957c459b5cef7edc Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 3 Jun 2026 21:24:27 +0100 Subject: [PATCH 09/10] fix(mocks): thread type args through ordered verification + immutable type-arg arrays Round 7 review fixes: - OrderedVerification now records and matches a generic expectation's type arguments, so VerifyInOrder discriminates Greet() from Greet() like unordered verification does. Failure messages include the type arguments. Regression tests added. - Type-argument arrays are now ImmutableArray end-to-end (TypeArguments.Of.Value, MethodSetup, CallRecord, engine dispatch, verification), closing the mutable-shared-array corruption risk while keeping array-speed indexed access on the hot matching path. The arity-5+ generator fallback emits ImmutableArray.Create. All affected signatures were introduced on this branch, so no released surface changes. - CreateVerification routing deduped into MockCallVerification.Create, shared by MockMethodCall and VoidMockMethodCall. - Documented why the typed FindMatchingSetup family omits a TypeArgumentsMatch check: typed dispatch never carries call-side type arguments, so the check would be a constant true (non-generic setups have none; virtual/partial generic methods use the documented degradation path). --- .../Builders/MockImplBuilder.cs | 8 ++-- .../Builders/MockMembersBuilder.cs | 3 +- TUnit.Mocks.Tests/GenericTests.cs | 42 +++++++++++++++++++ .../ITypeArgumentVerificationFactory.cs | 18 +++++++- TUnit.Mocks/MockEngine.cs | 35 ++++++++++------ TUnit.Mocks/MockMethodCall.cs | 15 +++---- TUnit.Mocks/Setup/MethodSetup.cs | 13 +++--- TUnit.Mocks/TypeArgumentMatching.cs | 36 ++++++++++++---- TUnit.Mocks/TypeArguments.cs | 16 +++---- TUnit.Mocks/Verification/CallRecord.cs | 9 ++-- .../Verification/CallVerificationBuilder.cs | 23 ++++------ .../Verification/OrderedVerification.cs | 15 ++++--- TUnit.Mocks/VoidMockMethodCall.cs | 19 ++++----- 13 files changed, 163 insertions(+), 89 deletions(-) diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs index 805303b21e..4be7366aca 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs @@ -744,7 +744,7 @@ private static void GenerateEngineDispatchBody(CodeWriter writer, MockMemberMode } var (isTyped, typeArgs, argsList) = GetTypedDispatchInfo(method); - // Generic methods always dispatch through the object?[] + Type[] fallback so their concrete + // 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) @@ -1415,9 +1415,9 @@ private static string EmitHandleCallWithReturn(bool isTyped, string? typeArgs, s } /// - /// Emits the type-argument array for a generic method. For the common 1–2 type-parameter cases it + /// 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 fresh new global::System.Type[] { typeof(T), ... }. + /// allocation; higher arities fall back to a per-call ImmutableArray.Create(...). /// internal static string TypeArgumentsArrayLiteral(MockMemberModel method) { @@ -1426,7 +1426,7 @@ internal static string TypeArgumentsArrayLiteral(MockMemberModel method) { return $"global::TUnit.Mocks.TypeArguments.Of<{string.Join(", ", typeParams.Select(tp => tp.Name))}>.Value"; } - return $"new global::System.Type[] {{ {string.Join(", ", typeParams.Select(tp => $"typeof({tp.Name})"))} }}"; + return $"global::System.Collections.Immutable.ImmutableArray.Create({string.Join(", ", typeParams.Select(tp => $"typeof({tp.Name})"))})"; } /// Emits a TryHandleCall condition, choosing typed or fallback path. diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs index 851e2b39ca..d5853a98e2 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs @@ -971,7 +971,8 @@ private static void EmitReturnConstruction(CodeWriter writer, MockMemberModel me { // 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[] is not selected). The typed wrapper is never generated for generic methods. + // the trailing type-arguments parameter is not selected). The typed wrapper is never generated + // for generic methods. var typeArgs = method.IsGenericMethod ? $", {MockImplBuilder.TypeArgumentsArrayLiteral(method)}" : ""; diff --git a/TUnit.Mocks.Tests/GenericTests.cs b/TUnit.Mocks.Tests/GenericTests.cs index e09604cf5e..59ab866844 100644 --- a/TUnit.Mocks.Tests/GenericTests.cs +++ b/TUnit.Mocks.Tests/GenericTests.cs @@ -335,4 +335,46 @@ public async Task Generic_Method_Struct_Constraint_Distinguished_By_Type_Argumen 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/ITypeArgumentVerificationFactory.cs b/TUnit.Mocks/ITypeArgumentVerificationFactory.cs index 17f45487b2..7f440d5eff 100644 --- a/TUnit.Mocks/ITypeArgumentVerificationFactory.cs +++ b/TUnit.Mocks/ITypeArgumentVerificationFactory.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using TUnit.Mocks.Arguments; using TUnit.Mocks.Verification; @@ -17,5 +18,20 @@ 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, Type[]? typeArguments); + 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 babe75791f..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; @@ -224,24 +225,24 @@ 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, Type[]? typeArguments) + 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, null); + => 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, Type[] typeArguments) + 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, Type[]? typeArguments) + private void HandleCallCore(int memberId, string memberName, object?[] args, ImmutableArray typeArguments) { RawReturnContext.Clear(); var callRecord = RecordCall(memberId, memberName, args, typeArguments); @@ -296,18 +297,18 @@ private void HandleCallCore(int memberId, string memberName, object?[] args, Typ /// or returns default/throws for strict mode. /// public TReturn HandleCallWithReturn(int memberId, string memberName, object?[] args, TReturn defaultValue) - => HandleCallWithReturnCore(memberId, memberName, args, defaultValue, null, 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, null); + => 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, Type[] typeArguments) + public TReturn HandleCallWithReturn(int memberId, string memberName, object?[] args, TReturn defaultValue, ImmutableArray typeArguments) => HandleCallWithReturnCore(memberId, memberName, args, defaultValue, null, typeArguments); /// @@ -315,10 +316,10 @@ public TReturn HandleCallWithReturn(int memberId, string memberName, ob /// and paths. /// [EditorBrowsable(EditorBrowsableState.Never)] - public TReturn HandleCallWithReturn(int memberId, string memberName, object?[] args, TReturn defaultValue, Func? autoMockFactory, Type[] typeArguments) + 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, Type[]? typeArguments) + private TReturn HandleCallWithReturnCore(int memberId, string memberName, object?[] args, TReturn defaultValue, Func? autoMockFactory, ImmutableArray typeArguments) { RawReturnContext.Clear(); var callRecord = RecordCall(memberId, memberName, args, typeArguments); @@ -785,7 +786,7 @@ 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, Type[]? typeArguments) + 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) @@ -916,9 +917,9 @@ private void RebuildStaleSnapshots() // 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, (Type[]?)null); + => FindMatchingSetup(memberId, args, default(ImmutableArray)); - private (bool SetupFound, IBehavior? Behavior, MethodSetup? Setup) FindMatchingSetup(int memberId, object?[] args, Type[]? typeArguments) + 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) @@ -963,7 +964,7 @@ private void RebuildStaleSnapshots() // 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, Type[]? typeArguments = null) + private (bool SetupFound, IBehavior? Behavior, MethodSetup? Setup) FindMatchingSetupLocked(int memberId, object?[] args, ImmutableArray typeArguments = default) { lock (Lock) { @@ -999,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 f77725cb60..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,7 +21,7 @@ public sealed class MockMethodCall : IMethodSetup, ISetupChain private readonly int _memberId; private readonly string _memberName; private readonly IArgumentMatcher[] _matchers; - private readonly Type[]? _typeArguments; + private readonly ImmutableArray _typeArguments; private MethodSetupBuilder? _builder; private bool _builderInitialized; private object? _builderLock; @@ -29,12 +30,12 @@ public sealed class MockMethodCall : IMethodSetup, ISetupChain // public binary signature for backward compatibility. [EditorBrowsable(EditorBrowsableState.Never)] public MockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers) - : this(engine, memberId, memberName, matchers, null) + : this(engine, memberId, memberName, matchers, default) { } [EditorBrowsable(EditorBrowsableState.Never)] - public MockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers, Type[]? typeArguments) + public MockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers, ImmutableArray typeArguments) { _engine = engine; _memberId = memberId; @@ -119,14 +120,8 @@ public IMethodSetup Then() // ICallVerification implementation - // Non-generic calls use the public engine surface unchanged; generic calls route through the - // internal type-argument-aware factory (always implemented by MockEngine). A custom - // IMockEngineAccess implementation falls back to the public surface, losing type-argument - // filtering but never throwing. private ICallVerification CreateVerification() - => _typeArguments is not null && _engine is ITypeArgumentVerificationFactory typeArgFactory - ? typeArgFactory.CreateVerification(_memberId, _memberName, _matchers, _typeArguments) - : _engine.CreateVerification(_memberId, _memberName, _matchers); + => MockCallVerification.Create(_engine, _memberId, _memberName, _matchers, _typeArguments); public void WasCalled(Times times) { diff --git a/TUnit.Mocks/Setup/MethodSetup.cs b/TUnit.Mocks/Setup/MethodSetup.cs index 2f6f265970..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; @@ -80,19 +81,19 @@ public string? TransitionTarget /// /// For a generic method, the configured type arguments (concrete types, or - /// / wildcards). Null for a + /// / wildcards). Default for a /// non-generic method, in which case the setup matches regardless of call-site type arguments. /// [EditorBrowsable(EditorBrowsableState.Never)] - public Type[]? TypeArguments { get; } + public ImmutableArray TypeArguments { get; } public MethodSetup(int memberId, IArgumentMatcher[] matchers, string memberName = "") - : this(memberId, matchers, memberName, null) + : this(memberId, matchers, memberName, default) { } [EditorBrowsable(EditorBrowsableState.Never)] - public MethodSetup(int memberId, IArgumentMatcher[] matchers, string memberName, Type[]? typeArguments) + public MethodSetup(int memberId, IArgumentMatcher[] matchers, string memberName, ImmutableArray typeArguments) { MemberId = memberId; _matchers = matchers; @@ -102,10 +103,10 @@ public MethodSetup(int memberId, IArgumentMatcher[] matchers, string memberName, /// /// True if this setup's configured type arguments match a call made with - /// . Non-generic setups (null ) match any call. + /// . Non-generic setups (default ) match any call. /// [EditorBrowsable(EditorBrowsableState.Never)] - public bool TypeArgumentsMatch(Type[]? callTypeArgs) + public bool TypeArgumentsMatch(ImmutableArray callTypeArgs) => TypeArgumentMatching.Matches(TypeArguments, callTypeArgs); private RareState EnsureRareState() diff --git a/TUnit.Mocks/TypeArgumentMatching.cs b/TUnit.Mocks/TypeArgumentMatching.cs index feca8af3e1..cb062ddfc0 100644 --- a/TUnit.Mocks/TypeArgumentMatching.cs +++ b/TUnit.Mocks/TypeArgumentMatching.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using TUnit.Mocks.Arguments; namespace TUnit.Mocks; @@ -5,8 +6,8 @@ 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 -/// and always matches. +/// wildcards); recorded calls record the concrete closed types. A non-generic member carries a +/// default and always matches. /// internal static class TypeArgumentMatching { @@ -16,15 +17,15 @@ public static bool IsAnyMarker(Type type) /// /// Determines whether a setup configured with matches a call - /// made with . A setup (non-generic method, - /// or generic method whose setup did not specify type args) matches any call. A - /// call (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. + /// 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(Type[]? setupTypeArgs, Type[]? callTypeArgs) + public static bool Matches(ImmutableArray setupTypeArgs, ImmutableArray callTypeArgs) { - if (setupTypeArgs is null || callTypeArgs is null) + if (setupTypeArgs.IsDefault || callTypeArgs.IsDefault) { return true; } @@ -45,4 +46,21 @@ public static bool Matches(Type[]? setupTypeArgs, Type[]? callTypeArgs) 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 index 1e6002c46f..1f6354a3b0 100644 --- a/TUnit.Mocks/TypeArguments.cs +++ b/TUnit.Mocks/TypeArguments.cs @@ -1,14 +1,14 @@ +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 [] on every call. Generated code references +/// 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 new Type[] literal. Each Value is shared and read-only — it must -/// never be mutated (matching only reads it). Public for generated code access. Not intended for -/// direct use. +/// 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 @@ -16,24 +16,24 @@ public static class TypeArguments [EditorBrowsable(EditorBrowsableState.Never)] public static class Of { - public static readonly Type[] Value = { typeof(T) }; + public static readonly ImmutableArray Value = ImmutableArray.Create(typeof(T)); } [EditorBrowsable(EditorBrowsableState.Never)] public static class Of { - public static readonly Type[] Value = { typeof(T1), typeof(T2) }; + public static readonly ImmutableArray Value = ImmutableArray.Create(typeof(T1), typeof(T2)); } [EditorBrowsable(EditorBrowsableState.Never)] public static class Of { - public static readonly Type[] Value = { typeof(T1), typeof(T2), typeof(T3) }; + public static readonly ImmutableArray Value = ImmutableArray.Create(typeof(T1), typeof(T2), typeof(T3)); } [EditorBrowsable(EditorBrowsableState.Never)] public static class Of { - public static readonly Type[] Value = { typeof(T1), typeof(T2), typeof(T3), typeof(T4) }; + 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 c421d4396a..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,7 +18,7 @@ public sealed class CallRecord /// [EditorBrowsable(EditorBrowsableState.Never)] public CallRecord(int memberId, string memberName, object?[] arguments, long sequenceNumber) - : this(memberId, memberName, arguments, sequenceNumber, null) + : this(memberId, memberName, arguments, sequenceNumber, default) { } @@ -25,7 +26,7 @@ public CallRecord(int memberId, string memberName, object?[] arguments, long seq /// 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, Type[]? typeArguments) + public CallRecord(int memberId, string memberName, object?[] arguments, long sequenceNumber, ImmutableArray typeArguments) { MemberId = memberId; MemberName = memberName; @@ -56,10 +57,10 @@ public CallRecord(int memberId, string memberName, IArgumentStore store, long se public long SequenceNumber { get; } /// - /// For a generic method, the concrete closed type arguments the call was made with; null for a + /// 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 Type[]? TypeArguments { get; } + 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 aceb98839f..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,14 +16,14 @@ public sealed class CallVerificationBuilder : ICallVerification where T : cla private readonly int _memberId; private readonly string _memberName; private readonly IArgumentMatcher[] _matchers; - private readonly Type[]? _typeArguments; + private readonly ImmutableArray _typeArguments; public CallVerificationBuilder(MockEngine engine, int memberId, string memberName, IArgumentMatcher[] matchers) - : this(engine, memberId, memberName, matchers, null) + : this(engine, memberId, memberName, matchers, default) { } - public CallVerificationBuilder(MockEngine engine, int memberId, string memberName, IArgumentMatcher[] matchers, Type[]? typeArguments) + public CallVerificationBuilder(MockEngine engine, int memberId, string memberName, IArgumentMatcher[] matchers, ImmutableArray typeArguments) { _engine = engine; _memberId = memberId; @@ -49,7 +50,7 @@ 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; } @@ -59,7 +60,7 @@ public void WasCalled(Times times, string? message) // 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 && _typeArguments is null) + if (_matchers.Length == 0 && _typeArguments.IsDefault) { var totalCount = _engine.GetCallCountFor(_memberId); if (!times.Matches(totalCount)) @@ -167,17 +168,7 @@ private bool MatchesArguments(object?[] arguments) private string FormatExpectedCall() { var argDescriptions = string.Join(", ", _matchers.Select(m => m.Describe())); - var typeArgs = _typeArguments is { Length: > 0 } ta - ? "<" + string.Join(", ", ta.Select(FriendlyTypeName)) + ">" - : ""; + var typeArgs = TypeArgumentMatching.FormatForDiagnostics(_typeArguments); return $"{_memberName}{typeArgs}({argDescriptions})"; } - - /// 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/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 e8ecab33bc..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,7 +23,7 @@ public sealed class VoidMockMethodCall : IVoidMethodSetup, IVoidSetupChain, ICal private readonly int _memberId; private readonly string _memberName; private readonly IArgumentMatcher[] _matchers; - private readonly Type[]? _typeArguments; + private readonly ImmutableArray _typeArguments; private VoidMethodSetupBuilder? _builder; private bool _builderInitialized; private object? _builderLock; @@ -31,24 +32,24 @@ public sealed class VoidMockMethodCall : IVoidMethodSetup, IVoidSetupChain, ICal // 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, typeArguments: null) + : this(engine, memberId, memberName, matchers, eagerRegister: true, typeArguments: default) { } [EditorBrowsable(EditorBrowsableState.Never)] - public VoidMockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers, Type[]? typeArguments) + 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: null) + : this(engine, memberId, memberName, matchers, eagerRegister, typeArguments: default) { } [EditorBrowsable(EditorBrowsableState.Never)] - internal VoidMockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers, bool eagerRegister, Type[]? typeArguments) + internal VoidMockMethodCall(IMockEngineAccess engine, int memberId, string memberName, IArgumentMatcher[] matchers, bool eagerRegister, ImmutableArray typeArguments) { _engine = engine; _memberId = memberId; @@ -125,14 +126,8 @@ public IVoidMethodSetup Then() // ICallVerification implementation - // Non-generic calls use the public engine surface unchanged; generic calls route through the - // internal type-argument-aware factory (always implemented by MockEngine). A custom - // IMockEngineAccess implementation falls back to the public surface, losing type-argument - // filtering but never throwing. private ICallVerification CreateVerification() - => _typeArguments is not null && _engine is ITypeArgumentVerificationFactory typeArgFactory - ? typeArgFactory.CreateVerification(_memberId, _memberName, _matchers, _typeArguments) - : _engine.CreateVerification(_memberId, _memberName, _matchers); + => MockCallVerification.Create(_engine, _memberId, _memberName, _matchers, _typeArguments); public void WasCalled(Times times) { From 35e82f38a80fd1dc55e645d1e51d76acfb24cf45 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 3 Jun 2026 21:40:26 +0100 Subject: [PATCH 10/10] docs(mocks): note AnyValueType is exact-match-only under additional struct constraints --- TUnit.Mocks/Arguments/AnyType.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TUnit.Mocks/Arguments/AnyType.cs b/TUnit.Mocks/Arguments/AnyType.cs index c8eb3cf4d6..2525571171 100644 --- a/TUnit.Mocks/Arguments/AnyType.cs +++ b/TUnit.Mocks/Arguments/AnyType.cs @@ -20,6 +20,9 @@ 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 {