From 51c8a6ce3039603700640d6b5da132ddd57ae429 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 5 Apr 2026 13:58:05 +0100 Subject: [PATCH 1/9] perf: eliminate store.ToArray() allocation on mock behavior execution hot path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add IArgumentFreeBehavior interface for behaviors that don't use arguments (ReturnBehavior, CallbackBehavior, VoidReturnBehavior, ThrowBehavior, etc.) so the typed HandleCall methods skip the object?[] array allocation entirely. Add typed callback behaviors (TypedCallbackBehavior) and matching ITypedBehavior interfaces so typed callbacks execute without boxing arguments. Update source generator to emit direct typed callback registration instead of wrapping in an object?[] closure. Benchmarked improvements (vs previous): - Invocation (void): 317ns/208B → 190ns/120B (40% faster, 42% less alloc) - Invocation (string): 271ns/144B → 119ns/88B (56% faster, 39% less alloc) - Invocation (100x): 31μs/21KB → 19μs/12KB (38% faster, 44% less alloc) - Callback (no-args): 765ns → 461ns (40% faster) - Callback (with-args):906ns → 585ns (35% faster) - Setup (single): 494ns → 368ns (25% faster) --- ...Interface_Extension_Discovery.verified.txt | 4 +- ...face_With_Class_Type_Argument.verified.txt | 2 +- ...le_Non_Builtin_Type_Arguments.verified.txt | 2 +- ...ested_Namespace_Type_Argument.verified.txt | 2 +- ...nheriting_Multiple_Interfaces.verified.txt | 2 +- .../Interface_With_Async_Methods.verified.txt | 6 +- .../Interface_With_Events.verified.txt | 2 +- ..._With_Keyword_Parameter_Names.verified.txt | 4 +- .../Interface_With_Mixed_Members.verified.txt | 4 +- ...ble_Reference_Type_Parameters.verified.txt | 8 +- ...rface_With_Out_Ref_Parameters.verified.txt | 4 +- ...rface_With_Overloaded_Methods.verified.txt | 8 +- ...stract_Transitive_Return_Type.verified.txt | 2 +- .../Multi_Method_Interface.verified.txt | 4 +- ...embers_From_External_Assembly.verified.txt | 2 +- ...With_Internal_Signature_Types.verified.txt | 2 +- ...ple_Interface_With_One_Method.verified.txt | 2 +- ...ion_Discovery_Without_Mock_Of.verified.txt | 2 +- .../Builders/MockMembersBuilder.cs | 12 ++- TUnit.Mocks/MockEngine.Typed.cs | 92 +++++++++++------- TUnit.Mocks/MockEngine.cs | 4 +- .../Setup/Behaviors/CallbackBehavior.cs | 8 +- .../Setup/Behaviors/ComputedReturnBehavior.cs | 4 +- TUnit.Mocks/Setup/Behaviors/IBehavior.cs | 9 ++ .../Setup/Behaviors/RawReturnBehavior.cs | 8 +- TUnit.Mocks/Setup/Behaviors/ReturnBehavior.cs | 4 +- TUnit.Mocks/Setup/Behaviors/ThrowBehavior.cs | 4 +- .../Setup/Behaviors/TypedCallbackBehavior.cs | 93 +++++++++++++++++++ .../Setup/Behaviors/VoidReturnBehavior.cs | 4 +- TUnit.Mocks/Setup/MethodSetupBuilder.cs | 56 +++++++++++ TUnit.Mocks/Setup/VoidMethodSetupBuilder.cs | 56 +++++++++++ 31 files changed, 342 insertions(+), 74 deletions(-) create mode 100644 TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_Extension_Discovery.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_Extension_Discovery.verified.txt index 74df5e37c6..2fc30a83fe 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_Extension_Discovery.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_Extension_Discovery.verified.txt @@ -166,7 +166,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public IRepository_string__GetById_M0_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((int)args[0]!)); + EnsureSetup().Callback(callback); return this; } @@ -236,7 +236,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public IRepository_string__Save_M1_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((string)args[0]!)); + EnsureSetup().Callback(callback); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Class_Type_Argument.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Class_Type_Argument.verified.txt index e216d9570d..df55149d05 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Class_Type_Argument.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Class_Type_Argument.verified.txt @@ -149,7 +149,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public Sandbox_IFoo_Sandbox_Bar__Process_M1_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((global::Sandbox.Bar)args[0]!)); + EnsureSetup().Callback(callback); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Multiple_Non_Builtin_Type_Arguments.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Multiple_Non_Builtin_Type_Arguments.verified.txt index 2766fa1f4d..3f17e163f4 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Multiple_Non_Builtin_Type_Arguments.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Multiple_Non_Builtin_Type_Arguments.verified.txt @@ -143,7 +143,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public Sandbox_IMapper_Sandbox_Entity_Sandbox_Status__Map_M0_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((global::Sandbox.Entity)args[0]!)); + EnsureSetup().Callback(callback); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Nested_Namespace_Type_Argument.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Nested_Namespace_Type_Argument.verified.txt index 199200a7fd..1d1f0b1cee 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Nested_Namespace_Type_Argument.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Nested_Namespace_Type_Argument.verified.txt @@ -149,7 +149,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public Sandbox_IService_Outer_Inner_Config__Apply_M1_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((global::Outer.Inner.Config)args[0]!)); + EnsureSetup().Callback(callback); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_Inheriting_Multiple_Interfaces.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_Inheriting_Multiple_Interfaces.verified.txt index 4804dd3118..2bcae59aa1 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_Inheriting_Multiple_Interfaces.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_Inheriting_Multiple_Interfaces.verified.txt @@ -165,7 +165,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public IReadWriter_Write_M2_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((string)args[0]!)); + EnsureSetup().Callback(callback); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Async_Methods.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Async_Methods.verified.txt index 5d526cb8d7..ee1bc297d7 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Async_Methods.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Async_Methods.verified.txt @@ -260,7 +260,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public IAsyncService_GetValueAsync_M0_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((string)args[0]!)); + EnsureSetup().Callback(callback); return this; } @@ -415,7 +415,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public IAsyncService_ComputeAsync_M2_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((int)args[0]!)); + EnsureSetup().Callback(callback); return this; } @@ -492,7 +492,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public IAsyncService_InitializeAsync_M3_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((global::System.Threading.CancellationToken)args[0]!)); + EnsureSetup().Callback(callback); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Events.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Events.verified.txt index c33be9fb39..ac17e57599 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Events.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Events.verified.txt @@ -195,7 +195,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public INotifier_Notify_M0_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((string)args[0]!)); + EnsureSetup().Callback(callback); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Keyword_Parameter_Names.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Keyword_Parameter_Names.verified.txt index edf7852f03..887078a01a 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Keyword_Parameter_Names.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Keyword_Parameter_Names.verified.txt @@ -171,7 +171,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public ITest_Test_M0_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((string)args[0]!)); + EnsureSetup().Callback(callback); return this; } @@ -251,7 +251,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public ITest_Get_M1_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((int)args[0]!, (string)args[1]!)); + EnsureSetup().Callback(callback); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Mixed_Members.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Mixed_Members.verified.txt index 3929abc96c..9e5754d76c 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Mixed_Members.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Mixed_Members.verified.txt @@ -274,7 +274,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public IService_GetAsync_M3_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((int)args[0]!)); + EnsureSetup().Callback(callback); return this; } @@ -347,7 +347,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public IService_Process_M4_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((string)args[0]!)); + EnsureSetup().Callback(callback); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Nullable_Reference_Type_Parameters.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Nullable_Reference_Type_Parameters.verified.txt index 7ec88b80ea..251905ed22 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Nullable_Reference_Type_Parameters.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Nullable_Reference_Type_Parameters.verified.txt @@ -288,7 +288,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public IFoo_Bar_M0_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((object?)args[0])); + EnsureSetup().Callback(callback); return this; } @@ -368,7 +368,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public IFoo_GetValue_M1_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((string?)args[0], (int)args[1]!)); + EnsureSetup().Callback(callback); return this; } @@ -438,7 +438,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public IFoo_Process_M2_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((string)args[0]!, (string?)args[1], (object?)args[2])); + EnsureSetup().Callback(callback); return this; } @@ -530,7 +530,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public IFoo_GetAsync_M3_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((string?)args[0])); + EnsureSetup().Callback(callback); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Out_Ref_Parameters.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Out_Ref_Parameters.verified.txt index ad3c04e3d2..ac808c4c62 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Out_Ref_Parameters.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Out_Ref_Parameters.verified.txt @@ -172,7 +172,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public IDictionary_TryGetValue_M0_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((string)args[0]!)); + EnsureSetup().Callback(callback); return this; } @@ -245,7 +245,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public IDictionary_Swap_M1_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((int)args[0]!, (int)args[1]!)); + EnsureSetup().Callback(callback); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Overloaded_Methods.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Overloaded_Methods.verified.txt index 60705ed74e..1fe1e22119 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Overloaded_Methods.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Overloaded_Methods.verified.txt @@ -265,7 +265,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public IFormatter_Format_M0_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((string)args[0]!)); + EnsureSetup().Callback(callback); return this; } @@ -345,7 +345,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public IFormatter_Format_M1_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((int)args[0]!)); + EnsureSetup().Callback(callback); return this; } @@ -425,7 +425,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public IFormatter_Format_M2_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((string)args[0]!, (string)args[1]!)); + EnsureSetup().Callback(callback); return this; } @@ -505,7 +505,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public IFormatter_Format_M3_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((string)args[0]!, (string)args[1]!, (string)args[2]!)); + EnsureSetup().Callback(callback); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Static_Abstract_Transitive_Return_Type.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Static_Abstract_Transitive_Return_Type.verified.txt index f38f0b6214..42efb69368 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Static_Abstract_Transitive_Return_Type.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Static_Abstract_Transitive_Return_Type.verified.txt @@ -162,7 +162,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public IMyService_GetValue_M0_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((string)args[0]!)); + EnsureSetup().Callback(callback); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Method_Interface.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Method_Interface.verified.txt index 1aed69f470..e5aa3f1404 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Method_Interface.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Method_Interface.verified.txt @@ -209,7 +209,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public ICalculator_Add_M0_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((int)args[0]!, (int)args[1]!)); + EnsureSetup().Callback(callback); return this; } @@ -289,7 +289,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public ICalculator_Subtract_M1_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((int)args[0]!, (int)args[1]!)); + EnsureSetup().Callback(callback); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_Filters_Internal_Virtual_Members_From_External_Assembly.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_Filters_Internal_Virtual_Members_From_External_Assembly.verified.txt index 8221261294..d138493695 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_Filters_Internal_Virtual_Members_From_External_Assembly.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_Filters_Internal_Virtual_Members_From_External_Assembly.verified.txt @@ -168,7 +168,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public ExternalLib_ExternalClient_PublicMethod_M0_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((string)args[0]!)); + EnsureSetup().Callback(callback); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_Filters_Members_With_Internal_Signature_Types.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_Filters_Members_With_Internal_Signature_Types.verified.txt index fb6287604a..d9ee40cc69 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_Filters_Members_With_Internal_Signature_Types.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_Filters_Members_With_Internal_Signature_Types.verified.txt @@ -128,7 +128,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public ExternalLib_ServiceClient_GetValue_M0_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((string)args[0]!)); + EnsureSetup().Callback(callback); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Simple_Interface_With_One_Method.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Simple_Interface_With_One_Method.verified.txt index d8ba2a836a..8ee3000f09 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Simple_Interface_With_One_Method.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Simple_Interface_With_One_Method.verified.txt @@ -143,7 +143,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public IGreeter_Greet_M0_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((string)args[0]!)); + EnsureSetup().Callback(callback); return this; } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Static_Extension_Discovery_Without_Mock_Of.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Static_Extension_Discovery_Without_Mock_Of.verified.txt index 9da98fb638..f4b9505f3f 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Static_Extension_Discovery_Without_Mock_Of.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Static_Extension_Discovery_Without_Mock_Of.verified.txt @@ -149,7 +149,7 @@ namespace TUnit.Mocks.Generated /// Execute a typed callback using the actual method parameters. public INotifier_Notify_M0_MockCall Callback(global::System.Action callback) { - EnsureSetup().Callback(args => callback((string)args[0]!)); + EnsureSetup().Callback(callback); return this; } diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs index cd378696e9..8164a3aca9 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs @@ -432,12 +432,20 @@ private static void GenerateTypedCallbackOverload(CodeWriter writer, List p.FullyQualifiedType)); var actionType = $"global::System.Action<{typeList}>"; - var castArgs = BuildCastArgs(nonOutParams, allNonOutParams); writer.AppendLine("/// Execute a typed callback using the actual method parameters."); using (writer.Block($"public {wrapperName} Callback({actionType} callback)")) { - writer.AppendLine($"EnsureSetup().Callback(args => callback({castArgs}));"); + if (allNonOutParams is null && nonOutParams.Count <= 8) + { + // Direct typed registration — avoids boxing args into object?[] and the wrapping closure + writer.AppendLine("EnsureSetup().Callback(callback);"); + } + else + { + var castArgs = BuildCastArgs(nonOutParams, allNonOutParams); + writer.AppendLine($"EnsureSetup().Callback(args => callback({castArgs}));"); + } writer.AppendLine("return this;"); } } diff --git a/TUnit.Mocks/MockEngine.Typed.cs b/TUnit.Mocks/MockEngine.Typed.cs index b12307eac6..0967cffd8c 100644 --- a/TUnit.Mocks/MockEngine.Typed.cs +++ b/TUnit.Mocks/MockEngine.Typed.cs @@ -9,6 +9,34 @@ namespace TUnit.Mocks; public sealed partial class MockEngine where T : class { // ────────────────────────────────────────────────────────────────────── + // Behavior execution helpers — check IArgumentFreeBehavior, then + // ITypedBehavior, then fall back to store.ToArray(). + // ────────────────────────────────────────────────────────────────────── + + private static object? ExecuteBehavior(IBehavior b, ArgumentStore store, T1 a1) + => b is IArgumentFreeBehavior af ? af.Execute() : b is ITypedBehavior tb ? tb.Execute(a1) : b.Execute(store.ToArray()); + + private static object? ExecuteBehavior(IBehavior b, ArgumentStore store, T1 a1, T2 a2) + => b is IArgumentFreeBehavior af ? af.Execute() : b is ITypedBehavior tb ? tb.Execute(a1, a2) : b.Execute(store.ToArray()); + + private static object? ExecuteBehavior(IBehavior b, ArgumentStore store, T1 a1, T2 a2, T3 a3) + => b is IArgumentFreeBehavior af ? af.Execute() : b is ITypedBehavior tb ? tb.Execute(a1, a2, a3) : b.Execute(store.ToArray()); + + private static object? ExecuteBehavior(IBehavior b, ArgumentStore store, T1 a1, T2 a2, T3 a3, T4 a4) + => b is IArgumentFreeBehavior af ? af.Execute() : b is ITypedBehavior tb ? tb.Execute(a1, a2, a3, a4) : b.Execute(store.ToArray()); + + private static object? ExecuteBehavior(IBehavior b, ArgumentStore store, T1 a1, T2 a2, T3 a3, T4 a4, T5 a5) + => b is IArgumentFreeBehavior af ? af.Execute() : b is ITypedBehavior tb ? tb.Execute(a1, a2, a3, a4, a5) : b.Execute(store.ToArray()); + + private static object? ExecuteBehavior(IBehavior b, ArgumentStore store, T1 a1, T2 a2, T3 a3, T4 a4, T5 a5, T6 a6) + => b is IArgumentFreeBehavior af ? af.Execute() : b is ITypedBehavior tb ? tb.Execute(a1, a2, a3, a4, a5, a6) : b.Execute(store.ToArray()); + + private static object? ExecuteBehavior(IBehavior b, ArgumentStore store, T1 a1, T2 a2, T3 a3, T4 a4, T5 a5, T6 a6, T7 a7) + => b is IArgumentFreeBehavior af ? af.Execute() : b is ITypedBehavior tb ? tb.Execute(a1, a2, a3, a4, a5, a6, a7) : b.Execute(store.ToArray()); + + private static object? ExecuteBehavior(IBehavior b, ArgumentStore store, T1 a1, T2 a2, T3 a3, T4 a4, T5 a5, T6 a6, T7 a7, T8 a8) + => b is IArgumentFreeBehavior af ? af.Execute() : b is ITypedBehavior tb ? tb.Execute(a1, a2, a3, a4, a5, a6, a7, a8) : b.Execute(store.ToArray()); + // ────────────────────────────────────────────────────────────────────── // Arity 1 // ────────────────────────────────────────────────────────────────────── @@ -28,7 +56,7 @@ public void HandleCall(int memberId, string memberName, T1 arg1) if (behavior is not null) { - var behaviorResult = behavior.Execute(store.ToArray()); + var behaviorResult = ExecuteBehavior(behavior, store, arg1); if (behaviorResult is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -56,7 +84,7 @@ public TReturn HandleCallWithReturn(int memberId, string memberName if (behavior is not null) { - var result = behavior.Execute(store.ToArray()); + var result = ExecuteBehavior(behavior, store, arg1); if (result is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -120,7 +148,7 @@ public bool TryHandleCall(int memberId, string memberName, T1 arg1) if (behavior is not null) { - var behaviorResult = behavior.Execute(store.ToArray()); + var behaviorResult = ExecuteBehavior(behavior, store, arg1); if (behaviorResult is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -147,7 +175,7 @@ public bool TryHandleCallWithReturn(int memberId, string memberName if (behavior is not null) { - var behaviorResult = behavior.Execute(store.ToArray()); + var behaviorResult = ExecuteBehavior(behavior, store, arg1); if (behaviorResult is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -186,7 +214,7 @@ public void HandleCall(int memberId, string memberName, T1 arg1, T2 arg2 if (behavior is not null) { - var behaviorResult = behavior.Execute(store.ToArray()); + var behaviorResult = ExecuteBehavior(behavior, store, arg1, arg2); if (behaviorResult is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -214,7 +242,7 @@ public TReturn HandleCallWithReturn(int memberId, string member if (behavior is not null) { - var result = behavior.Execute(store.ToArray()); + var result = ExecuteBehavior(behavior, store, arg1, arg2); if (result is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -278,7 +306,7 @@ public bool TryHandleCall(int memberId, string memberName, T1 arg1, T2 a if (behavior is not null) { - var behaviorResult = behavior.Execute(store.ToArray()); + var behaviorResult = ExecuteBehavior(behavior, store, arg1, arg2); if (behaviorResult is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -305,7 +333,7 @@ public bool TryHandleCallWithReturn(int memberId, string member if (behavior is not null) { - var behaviorResult = behavior.Execute(store.ToArray()); + var behaviorResult = ExecuteBehavior(behavior, store, arg1, arg2); if (behaviorResult is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -344,7 +372,7 @@ public void HandleCall(int memberId, string memberName, T1 arg1, T2 if (behavior is not null) { - var behaviorResult = behavior.Execute(store.ToArray()); + var behaviorResult = ExecuteBehavior(behavior, store, arg1, arg2, arg3); if (behaviorResult is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -372,7 +400,7 @@ public TReturn HandleCallWithReturn(int memberId, string me if (behavior is not null) { - var result = behavior.Execute(store.ToArray()); + var result = ExecuteBehavior(behavior, store, arg1, arg2, arg3); if (result is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -436,7 +464,7 @@ public bool TryHandleCall(int memberId, string memberName, T1 arg1, if (behavior is not null) { - var behaviorResult = behavior.Execute(store.ToArray()); + var behaviorResult = ExecuteBehavior(behavior, store, arg1, arg2, arg3); if (behaviorResult is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -463,7 +491,7 @@ public bool TryHandleCallWithReturn(int memberId, string me if (behavior is not null) { - var behaviorResult = behavior.Execute(store.ToArray()); + var behaviorResult = ExecuteBehavior(behavior, store, arg1, arg2, arg3); if (behaviorResult is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -502,7 +530,7 @@ public void HandleCall(int memberId, string memberName, T1 arg1, if (behavior is not null) { - var behaviorResult = behavior.Execute(store.ToArray()); + var behaviorResult = ExecuteBehavior(behavior, store, arg1, arg2, arg3, arg4); if (behaviorResult is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -530,7 +558,7 @@ public TReturn HandleCallWithReturn(int memberId, strin if (behavior is not null) { - var result = behavior.Execute(store.ToArray()); + var result = ExecuteBehavior(behavior, store, arg1, arg2, arg3, arg4); if (result is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -594,7 +622,7 @@ public bool TryHandleCall(int memberId, string memberName, T1 ar if (behavior is not null) { - var behaviorResult = behavior.Execute(store.ToArray()); + var behaviorResult = ExecuteBehavior(behavior, store, arg1, arg2, arg3, arg4); if (behaviorResult is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -621,7 +649,7 @@ public bool TryHandleCallWithReturn(int memberId, strin if (behavior is not null) { - var behaviorResult = behavior.Execute(store.ToArray()); + var behaviorResult = ExecuteBehavior(behavior, store, arg1, arg2, arg3, arg4); if (behaviorResult is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -660,7 +688,7 @@ public void HandleCall(int memberId, string memberName, T1 a if (behavior is not null) { - var behaviorResult = behavior.Execute(store.ToArray()); + var behaviorResult = ExecuteBehavior(behavior, store, arg1, arg2, arg3, arg4, arg5); if (behaviorResult is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -688,7 +716,7 @@ public TReturn HandleCallWithReturn(int memberId, s if (behavior is not null) { - var result = behavior.Execute(store.ToArray()); + var result = ExecuteBehavior(behavior, store, arg1, arg2, arg3, arg4, arg5); if (result is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -752,7 +780,7 @@ public bool TryHandleCall(int memberId, string memberName, T if (behavior is not null) { - var behaviorResult = behavior.Execute(store.ToArray()); + var behaviorResult = ExecuteBehavior(behavior, store, arg1, arg2, arg3, arg4, arg5); if (behaviorResult is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -779,7 +807,7 @@ public bool TryHandleCallWithReturn(int memberId, s if (behavior is not null) { - var behaviorResult = behavior.Execute(store.ToArray()); + var behaviorResult = ExecuteBehavior(behavior, store, arg1, arg2, arg3, arg4, arg5); if (behaviorResult is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -818,7 +846,7 @@ public void HandleCall(int memberId, string memberName, if (behavior is not null) { - var behaviorResult = behavior.Execute(store.ToArray()); + var behaviorResult = ExecuteBehavior(behavior, store, arg1, arg2, arg3, arg4, arg5, arg6); if (behaviorResult is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -846,7 +874,7 @@ public TReturn HandleCallWithReturn(int memberI if (behavior is not null) { - var result = behavior.Execute(store.ToArray()); + var result = ExecuteBehavior(behavior, store, arg1, arg2, arg3, arg4, arg5, arg6); if (result is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -910,7 +938,7 @@ public bool TryHandleCall(int memberId, string memberNam if (behavior is not null) { - var behaviorResult = behavior.Execute(store.ToArray()); + var behaviorResult = ExecuteBehavior(behavior, store, arg1, arg2, arg3, arg4, arg5, arg6); if (behaviorResult is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -937,7 +965,7 @@ public bool TryHandleCallWithReturn(int memberI if (behavior is not null) { - var behaviorResult = behavior.Execute(store.ToArray()); + var behaviorResult = ExecuteBehavior(behavior, store, arg1, arg2, arg3, arg4, arg5, arg6); if (behaviorResult is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -976,7 +1004,7 @@ public void HandleCall(int memberId, string memberNa if (behavior is not null) { - var behaviorResult = behavior.Execute(store.ToArray()); + var behaviorResult = ExecuteBehavior(behavior, store, arg1, arg2, arg3, arg4, arg5, arg6, arg7); if (behaviorResult is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -1004,7 +1032,7 @@ public TReturn HandleCallWithReturn(int mem if (behavior is not null) { - var result = behavior.Execute(store.ToArray()); + var result = ExecuteBehavior(behavior, store, arg1, arg2, arg3, arg4, arg5, arg6, arg7); if (result is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -1068,7 +1096,7 @@ public bool TryHandleCall(int memberId, string membe if (behavior is not null) { - var behaviorResult = behavior.Execute(store.ToArray()); + var behaviorResult = ExecuteBehavior(behavior, store, arg1, arg2, arg3, arg4, arg5, arg6, arg7); if (behaviorResult is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -1095,7 +1123,7 @@ public bool TryHandleCallWithReturn(int mem if (behavior is not null) { - var behaviorResult = behavior.Execute(store.ToArray()); + var behaviorResult = ExecuteBehavior(behavior, store, arg1, arg2, arg3, arg4, arg5, arg6, arg7); if (behaviorResult is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -1134,7 +1162,7 @@ public void HandleCall(int memberId, string memb if (behavior is not null) { - var behaviorResult = behavior.Execute(store.ToArray()); + var behaviorResult = ExecuteBehavior(behavior, store, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); if (behaviorResult is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -1162,7 +1190,7 @@ public TReturn HandleCallWithReturn(int if (behavior is not null) { - var result = behavior.Execute(store.ToArray()); + var result = ExecuteBehavior(behavior, store, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); if (result is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -1226,7 +1254,7 @@ public bool TryHandleCall(int memberId, string m if (behavior is not null) { - var behaviorResult = behavior.Execute(store.ToArray()); + var behaviorResult = ExecuteBehavior(behavior, store, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); if (behaviorResult is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } @@ -1253,7 +1281,7 @@ public bool TryHandleCallWithReturn(int if (behavior is not null) { - var behaviorResult = behavior.Execute(store.ToArray()); + var behaviorResult = ExecuteBehavior(behavior, store, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); if (behaviorResult is RawReturn raw) RawReturnContext.Set(raw); try { ApplyMatchedSetup(matchedSetup); } catch { RawReturnContext.Clear(); throw; } diff --git a/TUnit.Mocks/MockEngine.cs b/TUnit.Mocks/MockEngine.cs index dae5f94c9c..54cee4b4ec 100644 --- a/TUnit.Mocks/MockEngine.cs +++ b/TUnit.Mocks/MockEngine.cs @@ -202,7 +202,7 @@ public void HandleCall(int memberId, string memberName, object?[] args) if (behavior is not null) { - var behaviorResult = behavior.Execute(args); + var behaviorResult = behavior is IArgumentFreeBehavior argFree ? argFree.Execute() : behavior.Execute(args); if (behaviorResult is RawReturn raw) { RawReturnContext.Set(raw); @@ -250,7 +250,7 @@ public TReturn HandleCallWithReturn(int memberId, string memberName, ob if (behavior is not null) { - var result = behavior.Execute(args); + var result = behavior is IArgumentFreeBehavior argFree ? argFree.Execute() : behavior.Execute(args); if (result is RawReturn raw) { RawReturnContext.Set(raw); diff --git a/TUnit.Mocks/Setup/Behaviors/CallbackBehavior.cs b/TUnit.Mocks/Setup/Behaviors/CallbackBehavior.cs index 7cb7d63aa8..72e9885c7e 100644 --- a/TUnit.Mocks/Setup/Behaviors/CallbackBehavior.cs +++ b/TUnit.Mocks/Setup/Behaviors/CallbackBehavior.cs @@ -1,6 +1,6 @@ namespace TUnit.Mocks.Setup.Behaviors; -internal sealed class CallbackBehavior : IBehavior +internal sealed class CallbackBehavior : IBehavior, IArgumentFreeBehavior { private readonly Action _callback; @@ -11,4 +11,10 @@ internal sealed class CallbackBehavior : IBehavior _callback(); return null; } + + public object? Execute() + { + _callback(); + return null; + } } diff --git a/TUnit.Mocks/Setup/Behaviors/ComputedReturnBehavior.cs b/TUnit.Mocks/Setup/Behaviors/ComputedReturnBehavior.cs index a6faa10864..98c50a2238 100644 --- a/TUnit.Mocks/Setup/Behaviors/ComputedReturnBehavior.cs +++ b/TUnit.Mocks/Setup/Behaviors/ComputedReturnBehavior.cs @@ -1,10 +1,12 @@ namespace TUnit.Mocks.Setup.Behaviors; -internal sealed class ComputedReturnBehavior : IBehavior +internal sealed class ComputedReturnBehavior : IBehavior, IArgumentFreeBehavior { private readonly Func _factory; public ComputedReturnBehavior(Func factory) => _factory = factory; public object? Execute(object?[] arguments) => _factory(); + + public object? Execute() => _factory(); } diff --git a/TUnit.Mocks/Setup/Behaviors/IBehavior.cs b/TUnit.Mocks/Setup/Behaviors/IBehavior.cs index 05914ef70e..6aaf8958cd 100644 --- a/TUnit.Mocks/Setup/Behaviors/IBehavior.cs +++ b/TUnit.Mocks/Setup/Behaviors/IBehavior.cs @@ -11,3 +11,12 @@ public interface IBehavior { object? Execute(object?[] arguments); } + +/// +/// Marker interface for behaviors that do not use the method arguments. +/// Implementing this avoids the allocation of the boxed argument array on the invocation hot path. +/// +internal interface IArgumentFreeBehavior +{ + object? Execute(); +} diff --git a/TUnit.Mocks/Setup/Behaviors/RawReturnBehavior.cs b/TUnit.Mocks/Setup/Behaviors/RawReturnBehavior.cs index a8fc4de305..d09624f213 100644 --- a/TUnit.Mocks/Setup/Behaviors/RawReturnBehavior.cs +++ b/TUnit.Mocks/Setup/Behaviors/RawReturnBehavior.cs @@ -1,21 +1,25 @@ namespace TUnit.Mocks.Setup.Behaviors; -internal sealed class RawReturnBehavior : IBehavior +internal sealed class RawReturnBehavior : IBehavior, IArgumentFreeBehavior { private readonly RawReturn _wrapper; public RawReturnBehavior(object? rawValue) => _wrapper = new RawReturn(rawValue); public object? Execute(object?[] arguments) => _wrapper; + + public object? Execute() => _wrapper; } -internal sealed class ComputedRawReturnBehavior : IBehavior +internal sealed class ComputedRawReturnBehavior : IBehavior, IArgumentFreeBehavior { private readonly Func _factory; public ComputedRawReturnBehavior(Func factory) => _factory = factory; public object? Execute(object?[] arguments) => new RawReturn(_factory()); + + public object? Execute() => new RawReturn(_factory()); } internal sealed class ComputedRawReturnWithArgsBehavior : IBehavior diff --git a/TUnit.Mocks/Setup/Behaviors/ReturnBehavior.cs b/TUnit.Mocks/Setup/Behaviors/ReturnBehavior.cs index f51371182c..5e887eff7c 100644 --- a/TUnit.Mocks/Setup/Behaviors/ReturnBehavior.cs +++ b/TUnit.Mocks/Setup/Behaviors/ReturnBehavior.cs @@ -1,10 +1,12 @@ namespace TUnit.Mocks.Setup.Behaviors; -internal sealed class ReturnBehavior : IBehavior +internal sealed class ReturnBehavior : IBehavior, IArgumentFreeBehavior { private readonly TReturn _value; public ReturnBehavior(TReturn value) => _value = value; public object? Execute(object?[] arguments) => _value; + + public object? Execute() => _value; } diff --git a/TUnit.Mocks/Setup/Behaviors/ThrowBehavior.cs b/TUnit.Mocks/Setup/Behaviors/ThrowBehavior.cs index b401dcddf4..9d314f038d 100644 --- a/TUnit.Mocks/Setup/Behaviors/ThrowBehavior.cs +++ b/TUnit.Mocks/Setup/Behaviors/ThrowBehavior.cs @@ -1,10 +1,12 @@ namespace TUnit.Mocks.Setup.Behaviors; -internal sealed class ThrowBehavior : IBehavior +internal sealed class ThrowBehavior : IBehavior, IArgumentFreeBehavior { private readonly Exception _exception; public ThrowBehavior(Exception exception) => _exception = exception; public object? Execute(object?[] arguments) => throw _exception; + + public object? Execute() => throw _exception; } diff --git a/TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs b/TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs new file mode 100644 index 0000000000..b88b8bb394 --- /dev/null +++ b/TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs @@ -0,0 +1,93 @@ +namespace TUnit.Mocks.Setup.Behaviors; + +// Typed behavior interfaces enable the typed HandleCall methods to invoke callbacks +// without boxing arguments into object?[]. The typed HandleCall checks for the matching +// ITypedBehavior interface and calls Execute(arg1,...) directly. + +internal interface ITypedBehavior +{ + object? Execute(T1 arg1); +} + +internal interface ITypedBehavior +{ + object? Execute(T1 arg1, T2 arg2); +} + +internal interface ITypedBehavior +{ + object? Execute(T1 arg1, T2 arg2, T3 arg3); +} + +internal interface ITypedBehavior +{ + object? Execute(T1 arg1, T2 arg2, T3 arg3, T4 arg4); +} + +internal interface ITypedBehavior +{ + object? Execute(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5); +} + +internal interface ITypedBehavior +{ + object? Execute(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6); +} + +internal interface ITypedBehavior +{ + object? Execute(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7); +} + +internal interface ITypedBehavior +{ + object? Execute(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8); +} + +internal sealed class TypedCallbackBehavior(Action callback) : IBehavior, ITypedBehavior +{ + public object? Execute(object?[] arguments) { callback((T1)arguments[0]!); return null; } + public object? Execute(T1 arg1) { callback(arg1); return null; } +} + +internal sealed class TypedCallbackBehavior(Action callback) : IBehavior, ITypedBehavior +{ + public object? Execute(object?[] arguments) { callback((T1)arguments[0]!, (T2)arguments[1]!); return null; } + public object? Execute(T1 arg1, T2 arg2) { callback(arg1, arg2); return null; } +} + +internal sealed class TypedCallbackBehavior(Action callback) : IBehavior, ITypedBehavior +{ + public object? Execute(object?[] arguments) { callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!); return null; } + public object? Execute(T1 arg1, T2 arg2, T3 arg3) { callback(arg1, arg2, arg3); return null; } +} + +internal sealed class TypedCallbackBehavior(Action callback) : IBehavior, ITypedBehavior +{ + public object? Execute(object?[] arguments) { callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!, (T4)arguments[3]!); return null; } + public object? Execute(T1 arg1, T2 arg2, T3 arg3, T4 arg4) { callback(arg1, arg2, arg3, arg4); return null; } +} + +internal sealed class TypedCallbackBehavior(Action callback) : IBehavior, ITypedBehavior +{ + public object? Execute(object?[] arguments) { callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!, (T4)arguments[3]!, (T5)arguments[4]!); return null; } + public object? Execute(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) { callback(arg1, arg2, arg3, arg4, arg5); return null; } +} + +internal sealed class TypedCallbackBehavior(Action callback) : IBehavior, ITypedBehavior +{ + public object? Execute(object?[] arguments) { callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!, (T4)arguments[3]!, (T5)arguments[4]!, (T6)arguments[5]!); return null; } + public object? Execute(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) { callback(arg1, arg2, arg3, arg4, arg5, arg6); return null; } +} + +internal sealed class TypedCallbackBehavior(Action callback) : IBehavior, ITypedBehavior +{ + public object? Execute(object?[] arguments) { callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!, (T4)arguments[3]!, (T5)arguments[4]!, (T6)arguments[5]!, (T7)arguments[6]!); return null; } + public object? Execute(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) { callback(arg1, arg2, arg3, arg4, arg5, arg6, arg7); return null; } +} + +internal sealed class TypedCallbackBehavior(Action callback) : IBehavior, ITypedBehavior +{ + public object? Execute(object?[] arguments) { callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!, (T4)arguments[3]!, (T5)arguments[4]!, (T6)arguments[5]!, (T7)arguments[6]!, (T8)arguments[7]!); return null; } + public object? Execute(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) { callback(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); return null; } +} diff --git a/TUnit.Mocks/Setup/Behaviors/VoidReturnBehavior.cs b/TUnit.Mocks/Setup/Behaviors/VoidReturnBehavior.cs index f97398055d..abee9d2e2a 100644 --- a/TUnit.Mocks/Setup/Behaviors/VoidReturnBehavior.cs +++ b/TUnit.Mocks/Setup/Behaviors/VoidReturnBehavior.cs @@ -1,8 +1,10 @@ namespace TUnit.Mocks.Setup.Behaviors; -internal sealed class VoidReturnBehavior : IBehavior +internal sealed class VoidReturnBehavior : IBehavior, IArgumentFreeBehavior { public static VoidReturnBehavior Instance { get; } = new(); public object? Execute(object?[] arguments) => null; + + public object? Execute() => null; } diff --git a/TUnit.Mocks/Setup/MethodSetupBuilder.cs b/TUnit.Mocks/Setup/MethodSetupBuilder.cs index c81a4d1145..c7262cdf20 100644 --- a/TUnit.Mocks/Setup/MethodSetupBuilder.cs +++ b/TUnit.Mocks/Setup/MethodSetupBuilder.cs @@ -64,6 +64,62 @@ public ISetupChain Callback(Action callback) return this; } + [EditorBrowsable(EditorBrowsableState.Never)] + public ISetupChain Callback(Action callback) + { + _setup.AddBehavior(new TypedCallbackBehavior(callback)); + return this; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public ISetupChain Callback(Action callback) + { + _setup.AddBehavior(new TypedCallbackBehavior(callback)); + return this; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public ISetupChain Callback(Action callback) + { + _setup.AddBehavior(new TypedCallbackBehavior(callback)); + return this; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public ISetupChain Callback(Action callback) + { + _setup.AddBehavior(new TypedCallbackBehavior(callback)); + return this; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public ISetupChain Callback(Action callback) + { + _setup.AddBehavior(new TypedCallbackBehavior(callback)); + return this; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public ISetupChain Callback(Action callback) + { + _setup.AddBehavior(new TypedCallbackBehavior(callback)); + return this; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public ISetupChain Callback(Action callback) + { + _setup.AddBehavior(new TypedCallbackBehavior(callback)); + return this; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public ISetupChain Callback(Action callback) + { + _setup.AddBehavior(new TypedCallbackBehavior(callback)); + return this; + } + [EditorBrowsable(EditorBrowsableState.Never)] public ISetupChain Returns(Func factory) { diff --git a/TUnit.Mocks/Setup/VoidMethodSetupBuilder.cs b/TUnit.Mocks/Setup/VoidMethodSetupBuilder.cs index cf602bed48..e760e7198e 100644 --- a/TUnit.Mocks/Setup/VoidMethodSetupBuilder.cs +++ b/TUnit.Mocks/Setup/VoidMethodSetupBuilder.cs @@ -48,6 +48,62 @@ public IVoidSetupChain Callback(Action callback) return this; } + [EditorBrowsable(EditorBrowsableState.Never)] + public IVoidSetupChain Callback(Action callback) + { + _setup.AddBehavior(new TypedCallbackBehavior(callback)); + return this; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public IVoidSetupChain Callback(Action callback) + { + _setup.AddBehavior(new TypedCallbackBehavior(callback)); + return this; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public IVoidSetupChain Callback(Action callback) + { + _setup.AddBehavior(new TypedCallbackBehavior(callback)); + return this; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public IVoidSetupChain Callback(Action callback) + { + _setup.AddBehavior(new TypedCallbackBehavior(callback)); + return this; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public IVoidSetupChain Callback(Action callback) + { + _setup.AddBehavior(new TypedCallbackBehavior(callback)); + return this; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public IVoidSetupChain Callback(Action callback) + { + _setup.AddBehavior(new TypedCallbackBehavior(callback)); + return this; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public IVoidSetupChain Callback(Action callback) + { + _setup.AddBehavior(new TypedCallbackBehavior(callback)); + return this; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public IVoidSetupChain Callback(Action callback) + { + _setup.AddBehavior(new TypedCallbackBehavior(callback)); + return this; + } + [EditorBrowsable(EditorBrowsableState.Never)] public IVoidSetupChain Throws(Func exceptionFactory) { From 6194db857e9985acc0ef8d6c04ffb139aab2b21c Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 5 Apr 2026 14:02:51 +0100 Subject: [PATCH 2/9] refactor: simplify behavior implementations and add inlining hints - Have Execute(object?[]) delegate to Execute() in all IArgumentFreeBehavior implementations to eliminate duplicated logic - Add [AggressiveInlining] to ExecuteBehavior helpers to protect against future IL growth pushing past the JIT inline threshold - Remove unnecessary comment in TypedCallbackBehavior.cs --- TUnit.Mocks/MockEngine.Typed.cs | 9 +++++++++ TUnit.Mocks/Setup/Behaviors/CallbackBehavior.cs | 6 +----- TUnit.Mocks/Setup/Behaviors/ComputedReturnBehavior.cs | 2 +- TUnit.Mocks/Setup/Behaviors/RawReturnBehavior.cs | 4 ++-- TUnit.Mocks/Setup/Behaviors/ReturnBehavior.cs | 2 +- TUnit.Mocks/Setup/Behaviors/ThrowBehavior.cs | 2 +- TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs | 4 ---- TUnit.Mocks/Setup/Behaviors/VoidReturnBehavior.cs | 2 +- 8 files changed, 16 insertions(+), 15 deletions(-) diff --git a/TUnit.Mocks/MockEngine.Typed.cs b/TUnit.Mocks/MockEngine.Typed.cs index 0967cffd8c..9415fd823b 100644 --- a/TUnit.Mocks/MockEngine.Typed.cs +++ b/TUnit.Mocks/MockEngine.Typed.cs @@ -3,6 +3,7 @@ using TUnit.Mocks.Setup; using TUnit.Mocks.Setup.Behaviors; using System.ComponentModel; +using System.Runtime.CompilerServices; namespace TUnit.Mocks; @@ -13,27 +14,35 @@ public sealed partial class MockEngine where T : class // ITypedBehavior, then fall back to store.ToArray(). // ────────────────────────────────────────────────────────────────────── + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static object? ExecuteBehavior(IBehavior b, ArgumentStore store, T1 a1) => b is IArgumentFreeBehavior af ? af.Execute() : b is ITypedBehavior tb ? tb.Execute(a1) : b.Execute(store.ToArray()); + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static object? ExecuteBehavior(IBehavior b, ArgumentStore store, T1 a1, T2 a2) => b is IArgumentFreeBehavior af ? af.Execute() : b is ITypedBehavior tb ? tb.Execute(a1, a2) : b.Execute(store.ToArray()); + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static object? ExecuteBehavior(IBehavior b, ArgumentStore store, T1 a1, T2 a2, T3 a3) => b is IArgumentFreeBehavior af ? af.Execute() : b is ITypedBehavior tb ? tb.Execute(a1, a2, a3) : b.Execute(store.ToArray()); + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static object? ExecuteBehavior(IBehavior b, ArgumentStore store, T1 a1, T2 a2, T3 a3, T4 a4) => b is IArgumentFreeBehavior af ? af.Execute() : b is ITypedBehavior tb ? tb.Execute(a1, a2, a3, a4) : b.Execute(store.ToArray()); + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static object? ExecuteBehavior(IBehavior b, ArgumentStore store, T1 a1, T2 a2, T3 a3, T4 a4, T5 a5) => b is IArgumentFreeBehavior af ? af.Execute() : b is ITypedBehavior tb ? tb.Execute(a1, a2, a3, a4, a5) : b.Execute(store.ToArray()); + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static object? ExecuteBehavior(IBehavior b, ArgumentStore store, T1 a1, T2 a2, T3 a3, T4 a4, T5 a5, T6 a6) => b is IArgumentFreeBehavior af ? af.Execute() : b is ITypedBehavior tb ? tb.Execute(a1, a2, a3, a4, a5, a6) : b.Execute(store.ToArray()); + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static object? ExecuteBehavior(IBehavior b, ArgumentStore store, T1 a1, T2 a2, T3 a3, T4 a4, T5 a5, T6 a6, T7 a7) => b is IArgumentFreeBehavior af ? af.Execute() : b is ITypedBehavior tb ? tb.Execute(a1, a2, a3, a4, a5, a6, a7) : b.Execute(store.ToArray()); + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static object? ExecuteBehavior(IBehavior b, ArgumentStore store, T1 a1, T2 a2, T3 a3, T4 a4, T5 a5, T6 a6, T7 a7, T8 a8) => b is IArgumentFreeBehavior af ? af.Execute() : b is ITypedBehavior tb ? tb.Execute(a1, a2, a3, a4, a5, a6, a7, a8) : b.Execute(store.ToArray()); // ────────────────────────────────────────────────────────────────────── diff --git a/TUnit.Mocks/Setup/Behaviors/CallbackBehavior.cs b/TUnit.Mocks/Setup/Behaviors/CallbackBehavior.cs index 72e9885c7e..98ceac185e 100644 --- a/TUnit.Mocks/Setup/Behaviors/CallbackBehavior.cs +++ b/TUnit.Mocks/Setup/Behaviors/CallbackBehavior.cs @@ -6,11 +6,7 @@ internal sealed class CallbackBehavior : IBehavior, IArgumentFreeBehavior public CallbackBehavior(Action callback) => _callback = callback; - public object? Execute(object?[] arguments) - { - _callback(); - return null; - } + public object? Execute(object?[] arguments) => Execute(); public object? Execute() { diff --git a/TUnit.Mocks/Setup/Behaviors/ComputedReturnBehavior.cs b/TUnit.Mocks/Setup/Behaviors/ComputedReturnBehavior.cs index 98c50a2238..11d720d1db 100644 --- a/TUnit.Mocks/Setup/Behaviors/ComputedReturnBehavior.cs +++ b/TUnit.Mocks/Setup/Behaviors/ComputedReturnBehavior.cs @@ -6,7 +6,7 @@ internal sealed class ComputedReturnBehavior : IBehavior, IArgumentFree public ComputedReturnBehavior(Func factory) => _factory = factory; - public object? Execute(object?[] arguments) => _factory(); + public object? Execute(object?[] arguments) => Execute(); public object? Execute() => _factory(); } diff --git a/TUnit.Mocks/Setup/Behaviors/RawReturnBehavior.cs b/TUnit.Mocks/Setup/Behaviors/RawReturnBehavior.cs index d09624f213..86f106cd91 100644 --- a/TUnit.Mocks/Setup/Behaviors/RawReturnBehavior.cs +++ b/TUnit.Mocks/Setup/Behaviors/RawReturnBehavior.cs @@ -6,7 +6,7 @@ internal sealed class RawReturnBehavior : IBehavior, IArgumentFreeBehavior public RawReturnBehavior(object? rawValue) => _wrapper = new RawReturn(rawValue); - public object? Execute(object?[] arguments) => _wrapper; + public object? Execute(object?[] arguments) => Execute(); public object? Execute() => _wrapper; } @@ -17,7 +17,7 @@ internal sealed class ComputedRawReturnBehavior : IBehavior, IArgumentFreeBehavi public ComputedRawReturnBehavior(Func factory) => _factory = factory; - public object? Execute(object?[] arguments) => new RawReturn(_factory()); + public object? Execute(object?[] arguments) => Execute(); public object? Execute() => new RawReturn(_factory()); } diff --git a/TUnit.Mocks/Setup/Behaviors/ReturnBehavior.cs b/TUnit.Mocks/Setup/Behaviors/ReturnBehavior.cs index 5e887eff7c..7320546b21 100644 --- a/TUnit.Mocks/Setup/Behaviors/ReturnBehavior.cs +++ b/TUnit.Mocks/Setup/Behaviors/ReturnBehavior.cs @@ -6,7 +6,7 @@ internal sealed class ReturnBehavior : IBehavior, IArgumentFreeBehavior public ReturnBehavior(TReturn value) => _value = value; - public object? Execute(object?[] arguments) => _value; + public object? Execute(object?[] arguments) => Execute(); public object? Execute() => _value; } diff --git a/TUnit.Mocks/Setup/Behaviors/ThrowBehavior.cs b/TUnit.Mocks/Setup/Behaviors/ThrowBehavior.cs index 9d314f038d..bf5030f1e9 100644 --- a/TUnit.Mocks/Setup/Behaviors/ThrowBehavior.cs +++ b/TUnit.Mocks/Setup/Behaviors/ThrowBehavior.cs @@ -6,7 +6,7 @@ internal sealed class ThrowBehavior : IBehavior, IArgumentFreeBehavior public ThrowBehavior(Exception exception) => _exception = exception; - public object? Execute(object?[] arguments) => throw _exception; + public object? Execute(object?[] arguments) => Execute(); public object? Execute() => throw _exception; } diff --git a/TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs b/TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs index b88b8bb394..305bfa9c99 100644 --- a/TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs +++ b/TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs @@ -1,9 +1,5 @@ namespace TUnit.Mocks.Setup.Behaviors; -// Typed behavior interfaces enable the typed HandleCall methods to invoke callbacks -// without boxing arguments into object?[]. The typed HandleCall checks for the matching -// ITypedBehavior interface and calls Execute(arg1,...) directly. - internal interface ITypedBehavior { object? Execute(T1 arg1); diff --git a/TUnit.Mocks/Setup/Behaviors/VoidReturnBehavior.cs b/TUnit.Mocks/Setup/Behaviors/VoidReturnBehavior.cs index abee9d2e2a..d9bee1200c 100644 --- a/TUnit.Mocks/Setup/Behaviors/VoidReturnBehavior.cs +++ b/TUnit.Mocks/Setup/Behaviors/VoidReturnBehavior.cs @@ -4,7 +4,7 @@ internal sealed class VoidReturnBehavior : IBehavior, IArgumentFreeBehavior { public static VoidReturnBehavior Instance { get; } = new(); - public object? Execute(object?[] arguments) => null; + public object? Execute(object?[] arguments) => Execute(); public object? Execute() => null; } From d1fa2255e83a3adcb513a1852a18706a9d6e7812 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 5 Apr 2026 14:08:46 +0100 Subject: [PATCH 3/9] docs: clarify allNonOutParams null sentinel in source generator Address review feedback: document why allNonOutParams being null means the direct typed callback registration path is safe. --- TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs index 8164a3aca9..79a746192f 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs @@ -436,9 +436,11 @@ private static void GenerateTypedCallbackOverload(CodeWriter writer, ListExecute a typed callback using the actual method parameters."); using (writer.Block($"public {wrapperName} Callback({actionType} callback)")) { + // allNonOutParams is null when this is the primary overload (no out/ref struct subset remapping). + // In that case the callback's parameter types match the typed Callback overload directly, + // so we can register it without a wrapping closure — avoiding the object?[] allocation. if (allNonOutParams is null && nonOutParams.Count <= 8) { - // Direct typed registration — avoids boxing args into object?[] and the wrapping closure writer.AppendLine("EnsureSetup().Callback(callback);"); } else From 777b13de302734e8bab328b04cbca7f21afbc3c5 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 5 Apr 2026 14:16:05 +0100 Subject: [PATCH 4/9] docs: address remaining PR review feedback - Add XML doc on ITypedBehavior interfaces explaining their role in the ExecuteBehavior dispatch chain and extensibility for future typed return behaviors - Add Debug.Assert arity checks in TypedCallbackBehavior.Execute(object?[]) fallback paths for clearer failure diagnostics in debug builds - Mark CallbackWithArgsBehavior, ComputedReturnWithArgsBehavior, ComputedThrowBehavior, and ComputedRawReturnWithArgsBehavior as future ITypedBehavior optimization candidates - Add XML doc summary on typed Callback overloads on both setup builders explaining they are source-generator-emitted --- .../Behaviors/CallbackWithArgsBehavior.cs | 3 +++ .../ComputedReturnWithArgsBehavior.cs | 3 +++ .../Setup/Behaviors/ComputedThrowBehavior.cs | 3 +++ .../Setup/Behaviors/RawReturnBehavior.cs | 1 + .../Setup/Behaviors/TypedCallbackBehavior.cs | 24 ++++++++++++------- TUnit.Mocks/Setup/MethodSetupBuilder.cs | 1 + TUnit.Mocks/Setup/VoidMethodSetupBuilder.cs | 1 + 7 files changed, 28 insertions(+), 8 deletions(-) diff --git a/TUnit.Mocks/Setup/Behaviors/CallbackWithArgsBehavior.cs b/TUnit.Mocks/Setup/Behaviors/CallbackWithArgsBehavior.cs index 3759656696..11ddd683c4 100644 --- a/TUnit.Mocks/Setup/Behaviors/CallbackWithArgsBehavior.cs +++ b/TUnit.Mocks/Setup/Behaviors/CallbackWithArgsBehavior.cs @@ -6,6 +6,9 @@ namespace TUnit.Mocks.Setup.Behaviors; /// Behavior that invokes a callback with the method arguments. /// Public for generated code access. Not intended for direct use. /// +/// +/// Future optimization: implement ITypedBehavior<T...> to avoid store.ToArray() when args are needed. +/// [EditorBrowsable(EditorBrowsableState.Never)] public sealed class CallbackWithArgsBehavior : IBehavior { diff --git a/TUnit.Mocks/Setup/Behaviors/ComputedReturnWithArgsBehavior.cs b/TUnit.Mocks/Setup/Behaviors/ComputedReturnWithArgsBehavior.cs index 2d7060a870..f306245e08 100644 --- a/TUnit.Mocks/Setup/Behaviors/ComputedReturnWithArgsBehavior.cs +++ b/TUnit.Mocks/Setup/Behaviors/ComputedReturnWithArgsBehavior.cs @@ -6,6 +6,9 @@ namespace TUnit.Mocks.Setup.Behaviors; /// Behavior that computes a return value from the method arguments. /// Public for generated code access. Not intended for direct use. /// +/// +/// Future optimization: implement ITypedBehavior<T...> to avoid store.ToArray() when args are needed. +/// [EditorBrowsable(EditorBrowsableState.Never)] public sealed class ComputedReturnWithArgsBehavior : IBehavior { diff --git a/TUnit.Mocks/Setup/Behaviors/ComputedThrowBehavior.cs b/TUnit.Mocks/Setup/Behaviors/ComputedThrowBehavior.cs index beb4e6bba3..af0786b65a 100644 --- a/TUnit.Mocks/Setup/Behaviors/ComputedThrowBehavior.cs +++ b/TUnit.Mocks/Setup/Behaviors/ComputedThrowBehavior.cs @@ -6,6 +6,9 @@ namespace TUnit.Mocks.Setup.Behaviors; /// Behavior that computes an exception from the method arguments and throws it. /// Public for generated code access. Not intended for direct use. /// +/// +/// Future optimization: implement ITypedBehavior<T...> to avoid store.ToArray() when args are needed. +/// [EditorBrowsable(EditorBrowsableState.Never)] public sealed class ComputedThrowBehavior : IBehavior { diff --git a/TUnit.Mocks/Setup/Behaviors/RawReturnBehavior.cs b/TUnit.Mocks/Setup/Behaviors/RawReturnBehavior.cs index 86f106cd91..cf8dba4ba6 100644 --- a/TUnit.Mocks/Setup/Behaviors/RawReturnBehavior.cs +++ b/TUnit.Mocks/Setup/Behaviors/RawReturnBehavior.cs @@ -22,6 +22,7 @@ internal sealed class ComputedRawReturnBehavior : IBehavior, IArgumentFreeBehavi public object? Execute() => new RawReturn(_factory()); } +// Future optimization: implement ITypedBehavior to avoid store.ToArray() when args are needed. internal sealed class ComputedRawReturnWithArgsBehavior : IBehavior { private readonly Func _factory; diff --git a/TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs b/TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs index 305bfa9c99..31368a6431 100644 --- a/TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs +++ b/TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs @@ -1,5 +1,13 @@ +using System.Diagnostics; + namespace TUnit.Mocks.Setup.Behaviors; +/// +/// Typed behavior dispatch interfaces. ExecuteBehavior checks for these after IArgumentFreeBehavior, +/// enabling behaviors to receive typed arguments without boxing into object?[]. +/// Currently implemented by TypedCallbackBehavior; extensible for future typed return behaviors +/// (e.g. a TypedComputedReturnBehavior that takes Func<T1, TReturn>). +/// internal interface ITypedBehavior { object? Execute(T1 arg1); @@ -42,48 +50,48 @@ internal interface ITypedBehavior internal sealed class TypedCallbackBehavior(Action callback) : IBehavior, ITypedBehavior { - public object? Execute(object?[] arguments) { callback((T1)arguments[0]!); return null; } + public object? Execute(object?[] arguments) { Debug.Assert(arguments.Length >= 1); callback((T1)arguments[0]!); return null; } public object? Execute(T1 arg1) { callback(arg1); return null; } } internal sealed class TypedCallbackBehavior(Action callback) : IBehavior, ITypedBehavior { - public object? Execute(object?[] arguments) { callback((T1)arguments[0]!, (T2)arguments[1]!); return null; } + public object? Execute(object?[] arguments) { Debug.Assert(arguments.Length >= 2); callback((T1)arguments[0]!, (T2)arguments[1]!); return null; } public object? Execute(T1 arg1, T2 arg2) { callback(arg1, arg2); return null; } } internal sealed class TypedCallbackBehavior(Action callback) : IBehavior, ITypedBehavior { - public object? Execute(object?[] arguments) { callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!); return null; } + public object? Execute(object?[] arguments) { Debug.Assert(arguments.Length >= 3); callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!); return null; } public object? Execute(T1 arg1, T2 arg2, T3 arg3) { callback(arg1, arg2, arg3); return null; } } internal sealed class TypedCallbackBehavior(Action callback) : IBehavior, ITypedBehavior { - public object? Execute(object?[] arguments) { callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!, (T4)arguments[3]!); return null; } + public object? Execute(object?[] arguments) { Debug.Assert(arguments.Length >= 4); callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!, (T4)arguments[3]!); return null; } public object? Execute(T1 arg1, T2 arg2, T3 arg3, T4 arg4) { callback(arg1, arg2, arg3, arg4); return null; } } internal sealed class TypedCallbackBehavior(Action callback) : IBehavior, ITypedBehavior { - public object? Execute(object?[] arguments) { callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!, (T4)arguments[3]!, (T5)arguments[4]!); return null; } + public object? Execute(object?[] arguments) { Debug.Assert(arguments.Length >= 5); callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!, (T4)arguments[3]!, (T5)arguments[4]!); return null; } public object? Execute(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) { callback(arg1, arg2, arg3, arg4, arg5); return null; } } internal sealed class TypedCallbackBehavior(Action callback) : IBehavior, ITypedBehavior { - public object? Execute(object?[] arguments) { callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!, (T4)arguments[3]!, (T5)arguments[4]!, (T6)arguments[5]!); return null; } + public object? Execute(object?[] arguments) { Debug.Assert(arguments.Length >= 6); callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!, (T4)arguments[3]!, (T5)arguments[4]!, (T6)arguments[5]!); return null; } public object? Execute(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) { callback(arg1, arg2, arg3, arg4, arg5, arg6); return null; } } internal sealed class TypedCallbackBehavior(Action callback) : IBehavior, ITypedBehavior { - public object? Execute(object?[] arguments) { callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!, (T4)arguments[3]!, (T5)arguments[4]!, (T6)arguments[5]!, (T7)arguments[6]!); return null; } + public object? Execute(object?[] arguments) { Debug.Assert(arguments.Length >= 7); callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!, (T4)arguments[3]!, (T5)arguments[4]!, (T6)arguments[5]!, (T7)arguments[6]!); return null; } public object? Execute(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) { callback(arg1, arg2, arg3, arg4, arg5, arg6, arg7); return null; } } internal sealed class TypedCallbackBehavior(Action callback) : IBehavior, ITypedBehavior { - public object? Execute(object?[] arguments) { callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!, (T4)arguments[3]!, (T5)arguments[4]!, (T6)arguments[5]!, (T7)arguments[6]!, (T8)arguments[7]!); return null; } + public object? Execute(object?[] arguments) { Debug.Assert(arguments.Length >= 8); callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!, (T4)arguments[3]!, (T5)arguments[4]!, (T6)arguments[5]!, (T7)arguments[6]!, (T8)arguments[7]!); return null; } public object? Execute(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) { callback(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); return null; } } diff --git a/TUnit.Mocks/Setup/MethodSetupBuilder.cs b/TUnit.Mocks/Setup/MethodSetupBuilder.cs index c7262cdf20..7902985938 100644 --- a/TUnit.Mocks/Setup/MethodSetupBuilder.cs +++ b/TUnit.Mocks/Setup/MethodSetupBuilder.cs @@ -64,6 +64,7 @@ public ISetupChain Callback(Action callback) return this; } + /// Typed callback overload emitted by the source generator. Avoids boxing arguments into object?[]. [EditorBrowsable(EditorBrowsableState.Never)] public ISetupChain Callback(Action callback) { diff --git a/TUnit.Mocks/Setup/VoidMethodSetupBuilder.cs b/TUnit.Mocks/Setup/VoidMethodSetupBuilder.cs index e760e7198e..4d6a7bfb08 100644 --- a/TUnit.Mocks/Setup/VoidMethodSetupBuilder.cs +++ b/TUnit.Mocks/Setup/VoidMethodSetupBuilder.cs @@ -48,6 +48,7 @@ public IVoidSetupChain Callback(Action callback) return this; } + /// Typed callback overload emitted by the source generator. Avoids boxing arguments into object?[]. [EditorBrowsable(EditorBrowsableState.Never)] public IVoidSetupChain Callback(Action callback) { From d3bca1dc1df4521462f84c5eb15581e61bd2d205 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 5 Apr 2026 14:56:54 +0100 Subject: [PATCH 5/9] =?UTF-8?q?refactor:=20address=20PR=20review=20feedbac?= =?UTF-8?q?k=20=E2=80=94=20public=20IArgumentFreeBehavior,=20in=20store=20?= =?UTF-8?q?params,=20named=20constant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Make IArgumentFreeBehavior public with [EditorBrowsable(Never)] for extensibility - Pass ArgumentStore by `in` to avoid struct copies on the fast path - Replace magic `8` with existing MaxTypedParams constant in source generator --- .../Builders/MockMembersBuilder.cs | 2 +- TUnit.Mocks/MockEngine.Typed.cs | 16 ++++++++-------- TUnit.Mocks/Setup/Behaviors/IBehavior.cs | 5 ++++- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs index 79a746192f..465a211023 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs @@ -439,7 +439,7 @@ private static void GenerateTypedCallbackOverload(CodeWriter writer, List overload directly, // so we can register it without a wrapping closure — avoiding the object?[] allocation. - if (allNonOutParams is null && nonOutParams.Count <= 8) + if (allNonOutParams is null && nonOutParams.Count <= MaxTypedParams) { writer.AppendLine("EnsureSetup().Callback(callback);"); } diff --git a/TUnit.Mocks/MockEngine.Typed.cs b/TUnit.Mocks/MockEngine.Typed.cs index 9415fd823b..2be8e6327a 100644 --- a/TUnit.Mocks/MockEngine.Typed.cs +++ b/TUnit.Mocks/MockEngine.Typed.cs @@ -15,35 +15,35 @@ public sealed partial class MockEngine where T : class // ────────────────────────────────────────────────────────────────────── [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static object? ExecuteBehavior(IBehavior b, ArgumentStore store, T1 a1) + private static object? ExecuteBehavior(IBehavior b, in ArgumentStore store, T1 a1) => b is IArgumentFreeBehavior af ? af.Execute() : b is ITypedBehavior tb ? tb.Execute(a1) : b.Execute(store.ToArray()); [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static object? ExecuteBehavior(IBehavior b, ArgumentStore store, T1 a1, T2 a2) + private static object? ExecuteBehavior(IBehavior b, in ArgumentStore store, T1 a1, T2 a2) => b is IArgumentFreeBehavior af ? af.Execute() : b is ITypedBehavior tb ? tb.Execute(a1, a2) : b.Execute(store.ToArray()); [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static object? ExecuteBehavior(IBehavior b, ArgumentStore store, T1 a1, T2 a2, T3 a3) + private static object? ExecuteBehavior(IBehavior b, in ArgumentStore store, T1 a1, T2 a2, T3 a3) => b is IArgumentFreeBehavior af ? af.Execute() : b is ITypedBehavior tb ? tb.Execute(a1, a2, a3) : b.Execute(store.ToArray()); [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static object? ExecuteBehavior(IBehavior b, ArgumentStore store, T1 a1, T2 a2, T3 a3, T4 a4) + private static object? ExecuteBehavior(IBehavior b, in ArgumentStore store, T1 a1, T2 a2, T3 a3, T4 a4) => b is IArgumentFreeBehavior af ? af.Execute() : b is ITypedBehavior tb ? tb.Execute(a1, a2, a3, a4) : b.Execute(store.ToArray()); [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static object? ExecuteBehavior(IBehavior b, ArgumentStore store, T1 a1, T2 a2, T3 a3, T4 a4, T5 a5) + private static object? ExecuteBehavior(IBehavior b, in ArgumentStore store, T1 a1, T2 a2, T3 a3, T4 a4, T5 a5) => b is IArgumentFreeBehavior af ? af.Execute() : b is ITypedBehavior tb ? tb.Execute(a1, a2, a3, a4, a5) : b.Execute(store.ToArray()); [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static object? ExecuteBehavior(IBehavior b, ArgumentStore store, T1 a1, T2 a2, T3 a3, T4 a4, T5 a5, T6 a6) + private static object? ExecuteBehavior(IBehavior b, in ArgumentStore store, T1 a1, T2 a2, T3 a3, T4 a4, T5 a5, T6 a6) => b is IArgumentFreeBehavior af ? af.Execute() : b is ITypedBehavior tb ? tb.Execute(a1, a2, a3, a4, a5, a6) : b.Execute(store.ToArray()); [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static object? ExecuteBehavior(IBehavior b, ArgumentStore store, T1 a1, T2 a2, T3 a3, T4 a4, T5 a5, T6 a6, T7 a7) + private static object? ExecuteBehavior(IBehavior b, in ArgumentStore store, T1 a1, T2 a2, T3 a3, T4 a4, T5 a5, T6 a6, T7 a7) => b is IArgumentFreeBehavior af ? af.Execute() : b is ITypedBehavior tb ? tb.Execute(a1, a2, a3, a4, a5, a6, a7) : b.Execute(store.ToArray()); [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static object? ExecuteBehavior(IBehavior b, ArgumentStore store, T1 a1, T2 a2, T3 a3, T4 a4, T5 a5, T6 a6, T7 a7, T8 a8) + private static object? ExecuteBehavior(IBehavior b, in ArgumentStore store, T1 a1, T2 a2, T3 a3, T4 a4, T5 a5, T6 a6, T7 a7, T8 a8) => b is IArgumentFreeBehavior af ? af.Execute() : b is ITypedBehavior tb ? tb.Execute(a1, a2, a3, a4, a5, a6, a7, a8) : b.Execute(store.ToArray()); // ────────────────────────────────────────────────────────────────────── // Arity 1 diff --git a/TUnit.Mocks/Setup/Behaviors/IBehavior.cs b/TUnit.Mocks/Setup/Behaviors/IBehavior.cs index 6aaf8958cd..15552d50f0 100644 --- a/TUnit.Mocks/Setup/Behaviors/IBehavior.cs +++ b/TUnit.Mocks/Setup/Behaviors/IBehavior.cs @@ -15,8 +15,11 @@ public interface IBehavior /// /// Marker interface for behaviors that do not use the method arguments. /// Implementing this avoids the allocation of the boxed argument array on the invocation hot path. +/// Custom implementations that ignore arguments can implement this +/// to participate in the fast path. /// -internal interface IArgumentFreeBehavior +[EditorBrowsable(EditorBrowsableState.Never)] +public interface IArgumentFreeBehavior { object? Execute(); } From 95265bdf3a942d3a79a026e936e25c97710ff3b0 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 5 Apr 2026 15:13:40 +0100 Subject: [PATCH 6/9] fix: replace Debug.Assert with real bounds checks in TypedCallbackBehavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Debug.Assert is stripped in Release builds, leaving only a bare IndexOutOfRangeException if the fallback Execute(object?[]) path is reached with wrong arity. Use ArgumentException with a clear message instead — zero cost on the typed fast path since this only runs in the object?[] fallback. --- .../Setup/Behaviors/TypedCallbackBehavior.cs | 66 ++++++++++++++++--- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs b/TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs index 31368a6431..f025207c4a 100644 --- a/TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs +++ b/TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs @@ -1,5 +1,3 @@ -using System.Diagnostics; - namespace TUnit.Mocks.Setup.Behaviors; /// @@ -50,48 +48,96 @@ internal interface ITypedBehavior internal sealed class TypedCallbackBehavior(Action callback) : IBehavior, ITypedBehavior { - public object? Execute(object?[] arguments) { Debug.Assert(arguments.Length >= 1); callback((T1)arguments[0]!); return null; } + public object? Execute(object?[] arguments) + { + if (arguments.Length < 1) throw new ArgumentException($"Expected at least 1 argument, got {arguments.Length}.", nameof(arguments)); + callback((T1)arguments[0]!); + return null; + } + public object? Execute(T1 arg1) { callback(arg1); return null; } } internal sealed class TypedCallbackBehavior(Action callback) : IBehavior, ITypedBehavior { - public object? Execute(object?[] arguments) { Debug.Assert(arguments.Length >= 2); callback((T1)arguments[0]!, (T2)arguments[1]!); return null; } + public object? Execute(object?[] arguments) + { + if (arguments.Length < 2) throw new ArgumentException($"Expected at least 2 arguments, got {arguments.Length}.", nameof(arguments)); + callback((T1)arguments[0]!, (T2)arguments[1]!); + return null; + } + public object? Execute(T1 arg1, T2 arg2) { callback(arg1, arg2); return null; } } internal sealed class TypedCallbackBehavior(Action callback) : IBehavior, ITypedBehavior { - public object? Execute(object?[] arguments) { Debug.Assert(arguments.Length >= 3); callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!); return null; } + public object? Execute(object?[] arguments) + { + if (arguments.Length < 3) throw new ArgumentException($"Expected at least 3 arguments, got {arguments.Length}.", nameof(arguments)); + callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!); + return null; + } + public object? Execute(T1 arg1, T2 arg2, T3 arg3) { callback(arg1, arg2, arg3); return null; } } internal sealed class TypedCallbackBehavior(Action callback) : IBehavior, ITypedBehavior { - public object? Execute(object?[] arguments) { Debug.Assert(arguments.Length >= 4); callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!, (T4)arguments[3]!); return null; } + public object? Execute(object?[] arguments) + { + if (arguments.Length < 4) throw new ArgumentException($"Expected at least 4 arguments, got {arguments.Length}.", nameof(arguments)); + callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!, (T4)arguments[3]!); + return null; + } + public object? Execute(T1 arg1, T2 arg2, T3 arg3, T4 arg4) { callback(arg1, arg2, arg3, arg4); return null; } } internal sealed class TypedCallbackBehavior(Action callback) : IBehavior, ITypedBehavior { - public object? Execute(object?[] arguments) { Debug.Assert(arguments.Length >= 5); callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!, (T4)arguments[3]!, (T5)arguments[4]!); return null; } + public object? Execute(object?[] arguments) + { + if (arguments.Length < 5) throw new ArgumentException($"Expected at least 5 arguments, got {arguments.Length}.", nameof(arguments)); + callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!, (T4)arguments[3]!, (T5)arguments[4]!); + return null; + } + public object? Execute(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) { callback(arg1, arg2, arg3, arg4, arg5); return null; } } internal sealed class TypedCallbackBehavior(Action callback) : IBehavior, ITypedBehavior { - public object? Execute(object?[] arguments) { Debug.Assert(arguments.Length >= 6); callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!, (T4)arguments[3]!, (T5)arguments[4]!, (T6)arguments[5]!); return null; } + public object? Execute(object?[] arguments) + { + if (arguments.Length < 6) throw new ArgumentException($"Expected at least 6 arguments, got {arguments.Length}.", nameof(arguments)); + callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!, (T4)arguments[3]!, (T5)arguments[4]!, (T6)arguments[5]!); + return null; + } + public object? Execute(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) { callback(arg1, arg2, arg3, arg4, arg5, arg6); return null; } } internal sealed class TypedCallbackBehavior(Action callback) : IBehavior, ITypedBehavior { - public object? Execute(object?[] arguments) { Debug.Assert(arguments.Length >= 7); callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!, (T4)arguments[3]!, (T5)arguments[4]!, (T6)arguments[5]!, (T7)arguments[6]!); return null; } + public object? Execute(object?[] arguments) + { + if (arguments.Length < 7) throw new ArgumentException($"Expected at least 7 arguments, got {arguments.Length}.", nameof(arguments)); + callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!, (T4)arguments[3]!, (T5)arguments[4]!, (T6)arguments[5]!, (T7)arguments[6]!); + return null; + } + public object? Execute(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) { callback(arg1, arg2, arg3, arg4, arg5, arg6, arg7); return null; } } internal sealed class TypedCallbackBehavior(Action callback) : IBehavior, ITypedBehavior { - public object? Execute(object?[] arguments) { Debug.Assert(arguments.Length >= 8); callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!, (T4)arguments[3]!, (T5)arguments[4]!, (T6)arguments[5]!, (T7)arguments[6]!, (T8)arguments[7]!); return null; } + public object? Execute(object?[] arguments) + { + if (arguments.Length < 8) throw new ArgumentException($"Expected at least 8 arguments, got {arguments.Length}.", nameof(arguments)); + callback((T1)arguments[0]!, (T2)arguments[1]!, (T3)arguments[2]!, (T4)arguments[3]!, (T5)arguments[4]!, (T6)arguments[5]!, (T7)arguments[6]!, (T8)arguments[7]!); + return null; + } + public object? Execute(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) { callback(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); return null; } } From 03a1fc103b3621f6f8837c0dab6308eb349c8c57 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 5 Apr 2026 15:29:49 +0100 Subject: [PATCH 7/9] docs: document ITypedBehavior internal visibility and arity cap coupling - Add remarks on ITypedBehavior explaining why it's intentionally internal (tied to source generator's compile-time type knowledge) vs the public IArgumentFreeBehavior (any behavior can opt in without type knowledge) - Add arity-cap comment in MockEngine.Typed.cs linking to MaxTypedParams in MockMembersBuilder to prevent silent drift --- TUnit.Mocks/MockEngine.Typed.cs | 1 + TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/TUnit.Mocks/MockEngine.Typed.cs b/TUnit.Mocks/MockEngine.Typed.cs index 2be8e6327a..92fa5a60ad 100644 --- a/TUnit.Mocks/MockEngine.Typed.cs +++ b/TUnit.Mocks/MockEngine.Typed.cs @@ -12,6 +12,7 @@ public sealed partial class MockEngine where T : class // ────────────────────────────────────────────────────────────────────── // Behavior execution helpers — check IArgumentFreeBehavior, then // ITypedBehavior, then fall back to store.ToArray(). + // Arity range 1–8; must match MaxTypedParams in MockMembersBuilder. // ────────────────────────────────────────────────────────────────────── [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs b/TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs index f025207c4a..244f4263f0 100644 --- a/TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs +++ b/TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs @@ -6,6 +6,11 @@ namespace TUnit.Mocks.Setup.Behaviors; /// Currently implemented by TypedCallbackBehavior; extensible for future typed return behaviors /// (e.g. a TypedComputedReturnBehavior that takes Func<T1, TReturn>). /// +/// +/// Intentionally internal: the typed dispatch is tightly coupled to the source generator's +/// knowledge of parameter arity — only generated code knows the concrete types at compile time. +/// is public because any behavior can opt in without type knowledge. +/// internal interface ITypedBehavior { object? Execute(T1 arg1); From a866d43ca1997890f159dcdc36bc16098d5e82d3 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 5 Apr 2026 15:42:31 +0100 Subject: [PATCH 8/9] docs: add explicit arity-coupling checklist to typed behavior files Add change-checklist comments in TypedCallbackBehavior.cs and MockEngine.Typed.cs listing all files that must be updated when adding a new arity level, preventing silent drift. --- TUnit.Mocks/MockEngine.Typed.cs | 7 +++++-- TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs | 8 ++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/TUnit.Mocks/MockEngine.Typed.cs b/TUnit.Mocks/MockEngine.Typed.cs index 92fa5a60ad..3e775fced5 100644 --- a/TUnit.Mocks/MockEngine.Typed.cs +++ b/TUnit.Mocks/MockEngine.Typed.cs @@ -9,10 +9,13 @@ namespace TUnit.Mocks; public sealed partial class MockEngine where T : class { - // ────────────────────────────────────────────────────────────────────── + // ── ARITY COUPLING (1–8) ────────────────────────────────────────────── // Behavior execution helpers — check IArgumentFreeBehavior, then // ITypedBehavior, then fall back to store.ToArray(). - // Arity range 1–8; must match MaxTypedParams in MockMembersBuilder. + // If you add an arity (e.g. T9), you MUST also update: + // - ITypedBehavior and TypedCallbackBehavior in TypedCallbackBehavior.cs + // - Callback in MethodSetupBuilder.cs and VoidMethodSetupBuilder.cs + // - MaxTypedParams in MockMembersBuilder.cs (source generator) // ────────────────────────────────────────────────────────────────────── [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs b/TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs index 244f4263f0..e09c54e759 100644 --- a/TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs +++ b/TUnit.Mocks/Setup/Behaviors/TypedCallbackBehavior.cs @@ -51,6 +51,14 @@ internal interface ITypedBehavior object? Execute(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8); } +// ── ARITY COUPLING (1–8) ────────────────────────────────────────────── +// If you add an arity (e.g. T9), you MUST also update: +// - ITypedBehavior above +// - ExecuteBehavior in MockEngine.Typed.cs +// - Callback in MethodSetupBuilder.cs and VoidMethodSetupBuilder.cs +// - MaxTypedParams in MockMembersBuilder.cs (source generator) +// ────────────────────────────────────────────────────────────────────── + internal sealed class TypedCallbackBehavior(Action callback) : IBehavior, ITypedBehavior { public object? Execute(object?[] arguments) From 560ed2f078008b384ec26096f3bee6570eb251bb Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 5 Apr 2026 15:43:18 +0100 Subject: [PATCH 9/9] docs: add arity-coupling comments to setup builder files Complete the arity-coupling checklist by adding sync comments to MethodSetupBuilder and VoidMethodSetupBuilder, so all four coupled files now reference each other explicitly. --- TUnit.Mocks/Setup/MethodSetupBuilder.cs | 3 +++ TUnit.Mocks/Setup/VoidMethodSetupBuilder.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/TUnit.Mocks/Setup/MethodSetupBuilder.cs b/TUnit.Mocks/Setup/MethodSetupBuilder.cs index 7902985938..17fe4bb4ab 100644 --- a/TUnit.Mocks/Setup/MethodSetupBuilder.cs +++ b/TUnit.Mocks/Setup/MethodSetupBuilder.cs @@ -64,6 +64,9 @@ public ISetupChain Callback(Action callback) return this; } + // ── ARITY COUPLING (1–8): keep in sync with VoidMethodSetupBuilder, + // TypedCallbackBehavior.cs, MockEngine.Typed.cs, and MaxTypedParams in MockMembersBuilder.cs + /// Typed callback overload emitted by the source generator. Avoids boxing arguments into object?[]. [EditorBrowsable(EditorBrowsableState.Never)] public ISetupChain Callback(Action callback) diff --git a/TUnit.Mocks/Setup/VoidMethodSetupBuilder.cs b/TUnit.Mocks/Setup/VoidMethodSetupBuilder.cs index 4d6a7bfb08..05ddc34169 100644 --- a/TUnit.Mocks/Setup/VoidMethodSetupBuilder.cs +++ b/TUnit.Mocks/Setup/VoidMethodSetupBuilder.cs @@ -48,6 +48,9 @@ public IVoidSetupChain Callback(Action callback) return this; } + // ── ARITY COUPLING (1–8): keep in sync with MethodSetupBuilder, + // TypedCallbackBehavior.cs, MockEngine.Typed.cs, and MaxTypedParams in MockMembersBuilder.cs + /// Typed callback overload emitted by the source generator. Avoids boxing arguments into object?[]. [EditorBrowsable(EditorBrowsableState.Never)] public IVoidSetupChain Callback(Action callback)