From f8cfde3c811689e03c39443310b675fa79d78e98 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 30 Apr 2026 09:10:48 +0100 Subject: [PATCH 1/3] +semver:minor - feat(mocks): add Arg.AnyArgs() shortcut for setup/verify Mock setup with many parameters required Any() per slot, e.g. mock.Compute(Any(), Any(), Any(), Any(), Any()).Returns(42). The new Arg.AnyArgs() shortcut fills every parameter with AnyMatcher.Instance in one token, so that becomes mock.Compute(AnyArgs()).Returns(42). The generator emits the overload only when the method name is unique on the mocked type and there are no out / ref / ref-struct parameters; for overloaded names the shortcut would be ambiguous, so callers fall back to the explicit form. Generated body uses AnyMatcher.Instance directly rather than Arg.Any().Matcher to skip the CapturingMatcher wrap, since AnyArgs callers have no Arg handle to inspect captured values anyway. --- ...entUI_Shape_Nullable_Warnings.verified.txt | 14 ++ ...ace_With_Keyword_Member_Names.verified.txt | 7 + ..._With_Keyword_Parameter_Names.verified.txt | 7 + ...ble_Reference_Type_Parameters.verified.txt | 14 ++ ...terface_With_Obsolete_Members.verified.txt | 7 + .../Multi_Method_Interface.verified.txt | 14 ++ .../Builders/MockMembersBuilder.cs | 130 ++++++++++++----- TUnit.Mocks.Tests/AnyArgsTests.cs | 135 ++++++++++++++++++ TUnit.Mocks/Arguments/AnyArgs.cs | 19 +++ TUnit.Mocks/Arguments/Arg.cs | 7 + TUnit.Mocks/Matchers/AnyMatcher.cs | 6 +- .../mocking/argument-matchers.md | 28 ++++ 12 files changed, 354 insertions(+), 34 deletions(-) create mode 100644 TUnit.Mocks.Tests/AnyArgsTests.cs create mode 100644 TUnit.Mocks/Arguments/AnyArgs.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 9395671b43..1a39712788 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 @@ -338,6 +338,13 @@ namespace TUnit.Mocks.Generated return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "UpdateDialogAsync", matchers); } + /// Configure the mock setup for UpdateDialogAsync with every argument matched as Any<T>(). + public static global::TUnit.Mocks.MockMethodCall UpdateDialogAsync(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.AnyArgs _) where TData : class + { + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { global::TUnit.Mocks.Matchers.AnyMatcher.Instance, global::TUnit.Mocks.Matchers.AnyMatcher>.Instance }; + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "UpdateDialogAsync", matchers); + } + 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 }; @@ -366,6 +373,13 @@ namespace TUnit.Mocks.Generated return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "ShowDialogAsync", matchers); } + /// Configure the mock setup for ShowDialogAsync with every argument matched as Any<T>(). + public static global::TUnit.Mocks.MockMethodCall ShowDialogAsync(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.AnyArgs _) where TDialog : global::IDialogContentComponent + { + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { global::TUnit.Mocks.Matchers.AnyMatcher.Instance, global::TUnit.Mocks.Matchers.AnyMatcher.Instance }; + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "ShowDialogAsync", matchers); + } + public static void RaiseOnShow(this global::TUnit.Mocks.Mock mock, global::IDialogReference arg1, global::System.Type? arg2, global::DialogParameters arg3, object arg4) { ((global::TUnit.Mocks.IRaisable)global::TUnit.Mocks.MockRegistry.GetEngine(mock).Raisable!).RaiseEvent("OnShow", (object?)new object?[] { arg1, arg2, arg3, arg4 }); diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Keyword_Member_Names.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Keyword_Member_Names.verified.txt index ddb26dd05e..5b7654446e 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Keyword_Member_Names.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Keyword_Member_Names.verified.txt @@ -155,6 +155,13 @@ namespace TUnit.Mocks.Generated return new IEscapedNames_namespace_M3_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 3, "namespace", matchers); } + /// Configure the mock setup for namespace with every argument matched as Any<T>(). + public static IEscapedNames_namespace_M3_MockCall @namespace(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.AnyArgs _) + { + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { global::TUnit.Mocks.Matchers.AnyMatcher.Instance, global::TUnit.Mocks.Matchers.AnyMatcher.Instance }; + return new IEscapedNames_namespace_M3_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 3, "namespace", matchers); + } + extension(global::TUnit.Mocks.Mock mock) { public global::TUnit.Mocks.PropertyMockCall @class 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 e3579eaf07..8075626d5b 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 @@ -134,6 +134,13 @@ namespace TUnit.Mocks.Generated var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_class.Matcher, __fa_return.Matcher }; return new ITest_Get_M1_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Get", matchers); } + + /// Configure the mock setup for Get with every argument matched as Any<T>(). + public static ITest_Get_M1_MockCall Get(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.AnyArgs _) + { + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { global::TUnit.Mocks.Matchers.AnyMatcher.Instance, global::TUnit.Mocks.Matchers.AnyMatcher.Instance }; + return new ITest_Get_M1_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Get", matchers); + } } [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 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 16fadb5171..f81c28f90d 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 @@ -173,6 +173,13 @@ namespace TUnit.Mocks.Generated return new IFoo_GetValue_M1_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "GetValue", matchers); } + /// Configure the mock setup for GetValue with every argument matched as Any<T>(). + public static IFoo_GetValue_M1_MockCall GetValue(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.AnyArgs _) + { + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { global::TUnit.Mocks.Matchers.AnyMatcher.Instance, global::TUnit.Mocks.Matchers.AnyMatcher.Instance }; + return new IFoo_GetValue_M1_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "GetValue", matchers); + } + public static IFoo_Process_M2_MockCall Process(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg nonNull, global::TUnit.Mocks.Arguments.Arg nullable, global::TUnit.Mocks.Arguments.Arg obj) { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { nonNull.Matcher, nullable.Matcher, obj.Matcher }; @@ -233,6 +240,13 @@ namespace TUnit.Mocks.Generated return new IFoo_Process_M2_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "Process", matchers); } + /// Configure the mock setup for Process with every argument matched as Any<T>(). + public static IFoo_Process_M2_MockCall Process(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.AnyArgs _) + { + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { global::TUnit.Mocks.Matchers.AnyMatcher.Instance, global::TUnit.Mocks.Matchers.AnyMatcher.Instance, global::TUnit.Mocks.Matchers.AnyMatcher.Instance }; + return new IFoo_Process_M2_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "Process", matchers); + } + public static IFoo_GetAsync_M3_MockCall GetAsync(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg key) { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { key.Matcher }; 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 75c955cf57..9fd5c76dc1 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 @@ -423,6 +423,13 @@ namespace TUnit.Mocks.Generated return new IDialogService_Show_M0_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "Show", matchers); } + /// Configure the mock setup for Show with every argument matched as Any<T>(). + public static IDialogService_Show_M0_MockCall Show(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.AnyArgs _) + { + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { global::TUnit.Mocks.Matchers.AnyMatcher.Instance, global::TUnit.Mocks.Matchers.AnyMatcher.Instance }; + return new IDialogService_Show_M0_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "Show", matchers); + } + 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 }; 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 a2a21c410c..dee87e6a31 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Method_Interface.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Method_Interface.verified.txt @@ -129,6 +129,13 @@ namespace TUnit.Mocks.Generated return new ICalculator_Add_M0_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "Add", matchers); } + /// Configure the mock setup for Add with every argument matched as Any<T>(). + public static ICalculator_Add_M0_MockCall Add(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.AnyArgs _) + { + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { global::TUnit.Mocks.Matchers.AnyMatcher.Instance, global::TUnit.Mocks.Matchers.AnyMatcher.Instance }; + return new ICalculator_Add_M0_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "Add", matchers); + } + public static ICalculator_Subtract_M1_MockCall Subtract(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg a, global::TUnit.Mocks.Arguments.Arg b) { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { a.Matcher, b.Matcher }; @@ -157,6 +164,13 @@ namespace TUnit.Mocks.Generated return new ICalculator_Subtract_M1_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Subtract", matchers); } + /// Configure the mock setup for Subtract with every argument matched as Any<T>(). + public static ICalculator_Subtract_M1_MockCall Subtract(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.AnyArgs _) + { + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { global::TUnit.Mocks.Matchers.AnyMatcher.Instance, global::TUnit.Mocks.Matchers.AnyMatcher.Instance }; + return new ICalculator_Subtract_M1_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Subtract", matchers); + } + public static global::TUnit.Mocks.VoidMockMethodCall Reset(this global::TUnit.Mocks.Mock mock) { var matchers = global::System.Array.Empty(); diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs index c1545d61dc..e240b27073 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs @@ -676,6 +676,8 @@ private static void GenerateMemberMethod(CodeWriter writer, MockMemberModel meth EmitMemberMethodBody(writer, method, model, safeName, includeRefStructArgs: false, captureModelTypeParameters: false, receiverIsThis: false); EmitFuncOverloads(writer, method, model, safeName, includeRefStructArgs: false, captureModelTypeParameters: false, receiverIsThis: false); } + + EmitAnyArgsOverload(writer, method, model, safeName, captureModelTypeParameters: false, receiverIsThis: false); } private static void GenerateGenericMethodExtensionBlock(CodeWriter writer, MockMemberModel method, MockTypeModel model, string safeName) @@ -716,6 +718,8 @@ private static void GenerateGenericMethodMembersInCurrentBlock(CodeWriter writer EmitMemberMethodBody(writer, method, model, safeName, includeRefStructArgs: false, captureModelTypeParameters: true, receiverIsThis: false); EmitFuncOverloads(writer, method, model, safeName, includeRefStructArgs: false, captureModelTypeParameters: true, receiverIsThis: false); } + + EmitAnyArgsOverload(writer, method, model, safeName, captureModelTypeParameters: true, receiverIsThis: false); } internal static void GenerateGenericMethodMembersForWrapper(CodeWriter writer, MockMemberModel method, MockTypeModel model, string safeName) @@ -735,6 +739,8 @@ internal static void GenerateGenericMethodMembersForWrapper(CodeWriter writer, M EmitMemberMethodBody(writer, method, model, safeName, includeRefStructArgs: false, captureModelTypeParameters: true, receiverIsThis: true); EmitFuncOverloads(writer, method, model, safeName, includeRefStructArgs: false, captureModelTypeParameters: true, receiverIsThis: true); } + + EmitAnyArgsOverload(writer, method, model, safeName, captureModelTypeParameters: true, receiverIsThis: true); } /// @@ -869,23 +875,98 @@ private static void EmitMemberMethodBody(CodeWriter writer, MockMemberModel meth writer.AppendLine($"var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] {{ {matcherArgs} }};"); } - if (useTypedWrapper) - { - var wrapperName = MockImplBuilder.GetGeneratedTypeName(GetWrapperName(safeName, method), model); - writer.AppendLine($"return new {wrapperName}(global::TUnit.Mocks.MockRegistry.GetEngine(mock), {method.MemberId}, \"{method.Name}\", matchers);"); - } - 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);"); - } - else if (method.IsReturnTypeStaticAbstractInterface) - { - writer.AppendLine($"return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), {method.MemberId}, \"{method.Name}\", matchers);"); - } - else + EmitReturnConstruction(writer, method, model, safeName, useTypedWrapper, setupReturnType); + } + } + + /// + /// Emits the dispatch tail shared by every setup overload: build a return statement constructing + /// the right wrapper / mock-call type given the matchers array already in scope as matchers. + /// + private static void EmitReturnConstruction(CodeWriter writer, MockMemberModel method, MockTypeModel model, + string safeName, bool useTypedWrapper, string setupReturnType) + { + if (useTypedWrapper) + { + var wrapperName = MockImplBuilder.GetGeneratedTypeName(GetWrapperName(safeName, method), model); + writer.AppendLine($"return new {wrapperName}(global::TUnit.Mocks.MockRegistry.GetEngine(mock), {method.MemberId}, \"{method.Name}\", matchers);"); + } + 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);"); + } + else if (method.IsReturnTypeStaticAbstractInterface) + { + writer.AppendLine($"return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), {method.MemberId}, \"{method.Name}\", matchers);"); + } + else + { + writer.AppendLine($"return new global::TUnit.Mocks.MockMethodCall<{setupReturnType}>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), {method.MemberId}, \"{method.Name}\", matchers);"); + } + } + + /// + /// Emits a single-argument shortcut overload that accepts AnyArgs and fills every + /// matchable parameter slot with AnyMatcher<T>.Instance. Skipped when the shortcut + /// would be ambiguous (the method name is not unique on the model), unhelpful (zero or one + /// matchable parameter), or unsafe to mirror at this layer (out / ref / ref-struct params). + /// + private static void EmitAnyArgsOverload(CodeWriter writer, MockMemberModel method, MockTypeModel model, + string safeName, bool captureModelTypeParameters, bool receiverIsThis) + { + // Out, ref, and ref-struct params change matcher arity or signature shape; rather than + // try to mirror those variations through the AnyArgs path, defer to the explicit form. + foreach (var p in method.Parameters) + { + if (p.Direction == ParameterDirection.Out || p.Direction == ParameterDirection.Ref) return; + if (p.IsRefStruct) return; + } + + // After the early-return loop above, every parameter is matchable. + if (method.Parameters.Length < 2) return; + + // Name uniqueness: same set of methods that drive extension-method emission. + int sameNameCount = 0; + foreach (var m in model.Methods) + { + if (m.ExplicitInterfaceName is not null && !m.IsStaticAbstract) continue; + if (m.Name == method.Name) sameNameCount++; + } + if (sameNameCount > 1) return; + + var (useTypedWrapper, returnType, setupReturnType) = GetReturnTypeInfo(method, model, safeName); + + var typeParams = captureModelTypeParameters + ? MockImplBuilder.GetTypeParameterList(method) + : GetCombinedTypeParameterList(model, method); + var constraints = captureModelTypeParameters + ? MockImplBuilder.GetConstraintClauses(method) + : GetCombinedConstraintClauses(model, method); + + var safeMemberName = GetSafeMemberName(method.Name); + var paramListInner = "global::TUnit.Mocks.Arguments.AnyArgs _"; + var fullParamList = captureModelTypeParameters + ? paramListInner + : BuildExtensionMethodParameterList(model, paramListInner); + + var methodDeclarationPrefix = captureModelTypeParameters ? "public" : "public static"; + + writer.AppendLine(); + writer.AppendLine($"/// Configure the mock setup for {method.Name} with every argument matched as Any<T>()."); + using (writer.Block($"{methodDeclarationPrefix} {returnType} {safeMemberName}{typeParams}({fullParamList}){constraints}")) + { + if (receiverIsThis) { - writer.AppendLine($"return new global::TUnit.Mocks.MockMethodCall<{setupReturnType}>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), {method.MemberId}, \"{method.Name}\", matchers);"); + writer.AppendLine("var mock = this;"); } + + // AnyMatcher.Instance is stateless and shared — unlike Arg.Any(), it skips the + // CapturingMatcher wrap, so the AnyArgs path doesn't accumulate captured values per call. + var matcherArgs = string.Join(", ", method.Parameters.Select(p => + $"global::TUnit.Mocks.Matchers.AnyMatcher<{p.FullyQualifiedType}>.Instance")); + writer.AppendLine($"var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] {{ {matcherArgs} }};"); + + EmitReturnConstruction(writer, method, model, safeName, useTypedWrapper, setupReturnType); } } @@ -1029,24 +1110,7 @@ private static void EmitSingleFuncOverload(CodeWriter writer, MockMemberModel me writer.AppendLine($"var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] {{ {string.Join(", ", matcherExprs)} }};"); } - // Return statement - if (useTypedWrapper) - { - var wrapperName = MockImplBuilder.GetGeneratedTypeName(GetWrapperName(safeName, method), model); - writer.AppendLine($"return new {wrapperName}(global::TUnit.Mocks.MockRegistry.GetEngine(mock), {method.MemberId}, \"{method.Name}\", matchers);"); - } - 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);"); - } - else if (method.IsReturnTypeStaticAbstractInterface) - { - writer.AppendLine($"return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), {method.MemberId}, \"{method.Name}\", matchers);"); - } - else - { - writer.AppendLine($"return new global::TUnit.Mocks.MockMethodCall<{setupReturnType}>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), {method.MemberId}, \"{method.Name}\", matchers);"); - } + EmitReturnConstruction(writer, method, model, safeName, useTypedWrapper, setupReturnType); } } diff --git a/TUnit.Mocks.Tests/AnyArgsTests.cs b/TUnit.Mocks.Tests/AnyArgsTests.cs new file mode 100644 index 0000000000..a8493458f9 --- /dev/null +++ b/TUnit.Mocks.Tests/AnyArgsTests.cs @@ -0,0 +1,135 @@ +using TUnit.Mocks; +using TUnit.Mocks.Arguments; + +namespace TUnit.Mocks.Tests; + +/// +/// Test interfaces for the Arg.AnyArgs shortcut. The shortcut emits a single +/// setup overload that fills every matchable parameter with Any(), so a user +/// no longer has to write Any() once per parameter. +/// +public interface IFiveParamService +{ + int Compute(int a, int b, int c, int d, int e); + void Log(string source, string level, string message, int code, bool fatal); +} + +public interface ITwoParamService +{ + string Combine(string left, string right); +} + +public interface IOverloadedSumService +{ + int Sum(int a, int b); + int Sum(int a, int b, int c); + void Notify(string message); + void Notify(string source, string message); +} + +public class AnyArgsTests +{ + [Test] + public async Task AnyArgs_Setup_Returns_Value_For_Any_Argument_Combination() + { + var mock = IFiveParamService.Mock(); + mock.Compute(AnyArgs()).Returns(42); + + var svc = mock.Object; + + await Assert.That(svc.Compute(0, 0, 0, 0, 0)).IsEqualTo(42); + await Assert.That(svc.Compute(1, 2, 3, 4, 5)).IsEqualTo(42); + await Assert.That(svc.Compute(-100, int.MaxValue, 0, 7, -1)).IsEqualTo(42); + } + + [Test] + public async Task AnyArgs_Setup_Works_For_Two_Param_Method() + { + var mock = ITwoParamService.Mock(); + mock.Combine(AnyArgs()).Returns("OK"); + + var svc = mock.Object; + + await Assert.That(svc.Combine("a", "b")).IsEqualTo("OK"); + await Assert.That(svc.Combine("", "")).IsEqualTo("OK"); + await Assert.That(svc.Combine(null!, null!)).IsEqualTo("OK"); + } + + [Test] + public async Task AnyArgs_Setup_Works_For_Void_Method() + { + var mock = IFiveParamService.Mock(); + mock.Log(AnyArgs()).Throws(new System.InvalidOperationException("boom")); + + var svc = mock.Object; + + await Assert.That(() => svc.Log("a", "b", "c", 0, false)) + .Throws(); + await Assert.That(() => svc.Log("x", "y", "z", 99, true)) + .Throws(); + } + + [Test] + public async Task AnyArgs_Verify_Catches_Any_Call() + { + var mock = IFiveParamService.Mock(); + var svc = mock.Object; + + svc.Compute(1, 2, 3, 4, 5); + svc.Compute(10, 20, 30, 40, 50); + + mock.Compute(AnyArgs()).WasCalled(Times.Exactly(2)); + } + + [Test] + public async Task AnyArgs_Equivalent_To_All_Any_Slots() + { + var mockA = IFiveParamService.Mock(); + var mockB = IFiveParamService.Mock(); + + mockA.Compute(AnyArgs()).Returns(7); + mockB.Compute(Any(), Any(), Any(), Any(), Any()).Returns(7); + + await Assert.That(mockA.Object.Compute(1, 2, 3, 4, 5)).IsEqualTo(7); + await Assert.That(mockB.Object.Compute(1, 2, 3, 4, 5)).IsEqualTo(7); + } + + [Test] + public async Task AnyArgs_NotEmitted_For_Overloaded_Method_Names() + { + var mock = IOverloadedSumService.Mock(); + mock.Sum(Any(), Any()).Returns(11); + mock.Sum(Any(), Any(), Any()).Returns(111); + + var svc = mock.Object; + + await Assert.That(svc.Sum(1, 2)).IsEqualTo(11); + await Assert.That(svc.Sum(1, 2, 3)).IsEqualTo(111); + + await Assert.That(HasAnyArgsOverload("TUnit_Mocks_Tests_IOverloadedSumService_MockMemberExtensions", "Sum")).IsFalse(); + await Assert.That(HasAnyArgsOverload("TUnit_Mocks_Tests_IOverloadedSumService_MockMemberExtensions", "Notify")).IsFalse(); + } + + [Test] + public async Task AnyArgs_Emitted_For_Unique_Method_Names() + { + await Assert.That(HasAnyArgsOverload("TUnit_Mocks_Tests_IFiveParamService_MockMemberExtensions", "Compute")).IsTrue(); + await Assert.That(HasAnyArgsOverload("TUnit_Mocks_Tests_IFiveParamService_MockMemberExtensions", "Log")).IsTrue(); + } + + // The generated extension class lives in TUnit.Mocks.Generated and is named + // _MockMemberExtensions; the AnyArgs overload appears as a 2-param + // extension method whose second parameter is typed AnyArgs. + private static bool HasAnyArgsOverload(string extensionsTypeName, string methodName) + { + var extensionsType = typeof(AnyArgsTests).Assembly.GetTypes() + .Single(t => t.Name == extensionsTypeName); + return extensionsType.GetMethods() + .Where(m => m.Name == methodName) + .Any(m => + { + var ps = m.GetParameters(); + return ps.Length == 2 && ps[1].ParameterType == typeof(AnyArgs); + }); + } +} diff --git a/TUnit.Mocks/Arguments/AnyArgs.cs b/TUnit.Mocks/Arguments/AnyArgs.cs new file mode 100644 index 0000000000..65f3d368ba --- /dev/null +++ b/TUnit.Mocks/Arguments/AnyArgs.cs @@ -0,0 +1,19 @@ +namespace TUnit.Mocks.Arguments; + +/// +/// Sentinel type returned by . Passed to a generated mock setup or +/// verification overload, it stands in for a complete argument list of +/// matchers — one per parameter — so callers do not have to repeat Any() for each slot. +/// +/// The shortcut overload is only generated when the method's name is unique on the mocked type; +/// for overloaded names it would be ambiguous. In that case use the explicit per-parameter +/// form. +/// +/// +[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] +public sealed class AnyArgs +{ + internal static readonly AnyArgs Instance = new(); + + private AnyArgs() { } +} diff --git a/TUnit.Mocks/Arguments/Arg.cs b/TUnit.Mocks/Arguments/Arg.cs index eedac5d3fc..53b89a4d60 100644 --- a/TUnit.Mocks/Arguments/Arg.cs +++ b/TUnit.Mocks/Arguments/Arg.cs @@ -15,6 +15,13 @@ public static class Arg /// Matches any value — type is inferred from the parameter position. public static AnyArg Any() => AnyArg.Instance; + /// + /// Shortcut for setting up or verifying a mocked method when every argument should match + /// . Equivalent to passing Any() for each parameter. + /// Only available on methods whose name is unique on the mocked type. + /// + public static AnyArgs AnyArgs() => Arguments.AnyArgs.Instance; + /// Matches using exact equality. public static Arg Is(T value) => new(new ExactMatcher(value)); diff --git a/TUnit.Mocks/Matchers/AnyMatcher.cs b/TUnit.Mocks/Matchers/AnyMatcher.cs index 99c1dc73c7..df8dc1f9f9 100644 --- a/TUnit.Mocks/Matchers/AnyMatcher.cs +++ b/TUnit.Mocks/Matchers/AnyMatcher.cs @@ -17,8 +17,10 @@ internal sealed class AnyMatcher : IArgumentMatcher /// /// An argument matcher that matches any value of the specified type, including null. +/// Public for source-generator access; not intended for direct use — call instead. /// -internal sealed class AnyMatcher : IArgumentMatcher +[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] +public sealed class AnyMatcher : IArgumentMatcher { /// /// Cached singleton — is stateless, so a single instance per closed @@ -26,6 +28,8 @@ internal sealed class AnyMatcher : IArgumentMatcher /// public static readonly AnyMatcher Instance = new(); + private AnyMatcher() { } + public bool Matches(T? value) => true; public bool Matches(object? value) => true; diff --git a/docs/docs/writing-tests/mocking/argument-matchers.md b/docs/docs/writing-tests/mocking/argument-matchers.md index bf81df5cb7..548cb46680 100644 --- a/docs/docs/writing-tests/mocking/argument-matchers.md +++ b/docs/docs/writing-tests/mocking/argument-matchers.md @@ -15,6 +15,7 @@ TUnit.Mocks automatically imports the `Arg` class via `global using static`, so | Matcher | Matches | |---|---| | `Any()` / `Any()` | Any value of type T (including null) | +| `AnyArgs()` | Shortcut for matching every argument with `Any()` (only on uniquely named methods) | | `Is(value)` | Exact equality | | `Is(predicate)` | Values satisfying a predicate | | Raw value (e.g. `42`, `"hello"`) | Exact equality (implicit conversion) | @@ -47,6 +48,33 @@ svc.GetUser(1); // matches svc.GetUser(999); // matches ``` +### AnyArgs — match every parameter with one token + +When you only care that a method was called and don't want to constrain any of its arguments, repeating `Any()` for each parameter gets noisy: + +```csharp +mock.Compute(Any(), Any(), Any(), Any(), Any()).Returns(42); +``` + +`AnyArgs()` is a shortcut that fills every matchable parameter with `Any()` in one token: + +```csharp +mock.Compute(AnyArgs()).Returns(42); + +mock.Log(AnyArgs()).Throws(); + +// Works for verification too +mock.Compute(AnyArgs()).WasCalled(Times.Exactly(2)); +``` + +The two forms are equivalent — `AnyArgs()` simply expands to one `Arg.Any()` per parameter. + +:::note When the shortcut is generated +The `AnyArgs()` overload is only emitted when the method's name is unique on the mocked type. If the type has overloads of the same name (for example `Sum(int, int)` and `Sum(int, int, int)`), the shortcut would be ambiguous, so the generator omits it for those methods — use the explicit per-parameter form instead. + +The shortcut is also skipped for methods with `out`, `ref`, or ref-struct parameters, and for methods with fewer than two matchable parameters (where it would save nothing). +::: + ### Exact Value Match a specific value using equality: From cd3b873474b0524f77f5e56d39727bb57d49dfd6 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Fri, 1 May 2026 23:12:38 +0100 Subject: [PATCH 2/3] Address AnyArgs review feedback --- ...entUI_Shape_Nullable_Warnings.verified.txt | 14 ------------- .../Builders/MockMembersBuilder.cs | 7 ++++++- TUnit.Mocks.Tests/AnyArgsTests.cs | 21 +++++++++++++++++-- TUnit.Mocks/Arguments/AnyArgs.cs | 9 +++----- TUnit.Mocks/Arguments/Arg.cs | 4 ++-- .../mocking/argument-matchers.md | 2 +- 6 files changed, 31 insertions(+), 26 deletions(-) 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 1a39712788..9395671b43 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 @@ -338,13 +338,6 @@ namespace TUnit.Mocks.Generated return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "UpdateDialogAsync", matchers); } - /// Configure the mock setup for UpdateDialogAsync with every argument matched as Any<T>(). - public static global::TUnit.Mocks.MockMethodCall UpdateDialogAsync(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.AnyArgs _) where TData : class - { - var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { global::TUnit.Mocks.Matchers.AnyMatcher.Instance, global::TUnit.Mocks.Matchers.AnyMatcher>.Instance }; - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "UpdateDialogAsync", matchers); - } - 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 }; @@ -373,13 +366,6 @@ namespace TUnit.Mocks.Generated return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "ShowDialogAsync", matchers); } - /// Configure the mock setup for ShowDialogAsync with every argument matched as Any<T>(). - public static global::TUnit.Mocks.MockMethodCall ShowDialogAsync(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.AnyArgs _) where TDialog : global::IDialogContentComponent - { - var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { global::TUnit.Mocks.Matchers.AnyMatcher.Instance, global::TUnit.Mocks.Matchers.AnyMatcher.Instance }; - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "ShowDialogAsync", matchers); - } - public static void RaiseOnShow(this global::TUnit.Mocks.Mock mock, global::IDialogReference arg1, global::System.Type? arg2, global::DialogParameters arg3, object arg4) { ((global::TUnit.Mocks.IRaisable)global::TUnit.Mocks.MockRegistry.GetEngine(mock).Raisable!).RaiseEvent("OnShow", (object?)new object?[] { arg1, arg2, arg3, arg4 }); diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs index e240b27073..8c5cd9a2ec 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs @@ -909,11 +909,16 @@ private static void EmitReturnConstruction(CodeWriter writer, MockMemberModel me /// Emits a single-argument shortcut overload that accepts AnyArgs and fills every /// matchable parameter slot with AnyMatcher<T>.Instance. Skipped when the shortcut /// would be ambiguous (the method name is not unique on the model), unhelpful (zero or one - /// matchable parameter), or unsafe to mirror at this layer (out / ref / ref-struct params). + /// matchable parameter), unable to infer method type parameters, or unsafe to mirror at this + /// layer (out / ref / ref-struct params). /// private static void EmitAnyArgsOverload(CodeWriter writer, MockMemberModel method, MockTypeModel model, string safeName, bool captureModelTypeParameters, bool receiverIsThis) { + // AnyArgs carries no typed argument information, so generic method type parameters + // cannot be inferred at the call site. Defer to the explicit per-parameter overload. + if (method.IsGenericMethod) return; + // Out, ref, and ref-struct params change matcher arity or signature shape; rather than // try to mirror those variations through the AnyArgs path, defer to the explicit form. foreach (var p in method.Parameters) diff --git a/TUnit.Mocks.Tests/AnyArgsTests.cs b/TUnit.Mocks.Tests/AnyArgsTests.cs index a8493458f9..50ee15966d 100644 --- a/TUnit.Mocks.Tests/AnyArgsTests.cs +++ b/TUnit.Mocks.Tests/AnyArgsTests.cs @@ -27,6 +27,11 @@ public interface IOverloadedSumService void Notify(string source, string message); } +public interface IGenericAnyArgsService +{ + T Pick(T left, T right); +} + public class AnyArgsTests { [Test] @@ -117,13 +122,25 @@ public async Task AnyArgs_Emitted_For_Unique_Method_Names() await Assert.That(HasAnyArgsOverload("TUnit_Mocks_Tests_IFiveParamService_MockMemberExtensions", "Log")).IsTrue(); } + [Test] + public async Task AnyArgs_NotEmitted_For_Generic_Methods() + { + await Assert.That(HasAnyArgsOverload("TUnit_Mocks_Tests_IGenericAnyArgsService_MockMemberExtensions", "Pick")).IsFalse(); + } + // The generated extension class lives in TUnit.Mocks.Generated and is named // _MockMemberExtensions; the AnyArgs overload appears as a 2-param - // extension method whose second parameter is typed AnyArgs. + // extension method whose second parameter is typed AnyArgs. Keep these literal class + // names in sync with MockImplBuilder.GetSafeName. private static bool HasAnyArgsOverload(string extensionsTypeName, string methodName) { var extensionsType = typeof(AnyArgsTests).Assembly.GetTypes() - .Single(t => t.Name == extensionsTypeName); + .SingleOrDefault(t => t.Name == extensionsTypeName); + if (extensionsType is null) + { + return false; + } + return extensionsType.GetMethods() .Where(m => m.Name == methodName) .Any(m => diff --git a/TUnit.Mocks/Arguments/AnyArgs.cs b/TUnit.Mocks/Arguments/AnyArgs.cs index 65f3d368ba..57c30d393f 100644 --- a/TUnit.Mocks/Arguments/AnyArgs.cs +++ b/TUnit.Mocks/Arguments/AnyArgs.cs @@ -6,14 +6,11 @@ namespace TUnit.Mocks.Arguments; /// matchers — one per parameter — so callers do not have to repeat Any() for each slot. /// /// The shortcut overload is only generated when the method's name is unique on the mocked type; -/// for overloaded names it would be ambiguous. In that case use the explicit per-parameter -/// form. +/// for overloaded names, generic methods, or methods with fewer than two matchable parameters, +/// use the explicit per-parameter form. /// /// [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] -public sealed class AnyArgs +public readonly struct AnyArgs { - internal static readonly AnyArgs Instance = new(); - - private AnyArgs() { } } diff --git a/TUnit.Mocks/Arguments/Arg.cs b/TUnit.Mocks/Arguments/Arg.cs index 53b89a4d60..be2bcb973e 100644 --- a/TUnit.Mocks/Arguments/Arg.cs +++ b/TUnit.Mocks/Arguments/Arg.cs @@ -18,9 +18,9 @@ public static class Arg /// /// Shortcut for setting up or verifying a mocked method when every argument should match /// . Equivalent to passing Any() for each parameter. - /// Only available on methods whose name is unique on the mocked type. + /// Only available on non-generic methods whose name is unique on the mocked type. /// - public static AnyArgs AnyArgs() => Arguments.AnyArgs.Instance; + public static AnyArgs AnyArgs() => default; /// Matches using exact equality. public static Arg Is(T value) => new(new ExactMatcher(value)); diff --git a/docs/docs/writing-tests/mocking/argument-matchers.md b/docs/docs/writing-tests/mocking/argument-matchers.md index 548cb46680..9fbed82606 100644 --- a/docs/docs/writing-tests/mocking/argument-matchers.md +++ b/docs/docs/writing-tests/mocking/argument-matchers.md @@ -72,7 +72,7 @@ The two forms are equivalent — `AnyArgs()` simply expands to one `Arg.Any() :::note When the shortcut is generated The `AnyArgs()` overload is only emitted when the method's name is unique on the mocked type. If the type has overloads of the same name (for example `Sum(int, int)` and `Sum(int, int, int)`), the shortcut would be ambiguous, so the generator omits it for those methods — use the explicit per-parameter form instead. -The shortcut is also skipped for methods with `out`, `ref`, or ref-struct parameters, and for methods with fewer than two matchable parameters (where it would save nothing). +The shortcut is also skipped for generic methods, methods with `out`, `ref`, or ref-struct parameters, and methods with fewer than two matchable parameters. Generic methods need typed arguments so C# can infer their method type parameters, and single-parameter methods are already as short with `Any()`. ::: ### Exact Value From 518fb67d201775ead99cfe624e408567f1a84bc6 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 2 May 2026 00:03:58 +0100 Subject: [PATCH 3/3] fix(mocks): skip reflection-based AnyArgs emission tests under AOT PublishTrimmed strips metadata for the generated *_MockMemberExtensions extension classes, so Assembly.GetTypes()/GetMethods() returns nothing and the IsTrue assertions in AnyArgs_Emitted_For_Unique_Method_Names fail (with the IsFalse counterparts trivially passing). The source generator output is identical between JIT and AOT, so skip the three reflection-only tests when dynamic code is unsupported. --- TUnit.Mocks.Tests/AnyArgsTests.cs | 3 +++ .../SkipIfNotDynamicCodeSupportedAttribute.cs | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 TUnit.Mocks.Tests/SkipIfNotDynamicCodeSupportedAttribute.cs diff --git a/TUnit.Mocks.Tests/AnyArgsTests.cs b/TUnit.Mocks.Tests/AnyArgsTests.cs index 50ee15966d..18e80d5e7b 100644 --- a/TUnit.Mocks.Tests/AnyArgsTests.cs +++ b/TUnit.Mocks.Tests/AnyArgsTests.cs @@ -100,6 +100,7 @@ public async Task AnyArgs_Equivalent_To_All_Any_Slots() } [Test] + [SkipIfNotDynamicCodeSupported("Inspects generated extension class metadata via reflection; trimmer removes it under PublishTrimmed.")] public async Task AnyArgs_NotEmitted_For_Overloaded_Method_Names() { var mock = IOverloadedSumService.Mock(); @@ -116,6 +117,7 @@ public async Task AnyArgs_NotEmitted_For_Overloaded_Method_Names() } [Test] + [SkipIfNotDynamicCodeSupported("Inspects generated extension class metadata via reflection; trimmer removes it under PublishTrimmed.")] public async Task AnyArgs_Emitted_For_Unique_Method_Names() { await Assert.That(HasAnyArgsOverload("TUnit_Mocks_Tests_IFiveParamService_MockMemberExtensions", "Compute")).IsTrue(); @@ -123,6 +125,7 @@ public async Task AnyArgs_Emitted_For_Unique_Method_Names() } [Test] + [SkipIfNotDynamicCodeSupported("Inspects generated extension class metadata via reflection; trimmer removes it under PublishTrimmed.")] public async Task AnyArgs_NotEmitted_For_Generic_Methods() { await Assert.That(HasAnyArgsOverload("TUnit_Mocks_Tests_IGenericAnyArgsService_MockMemberExtensions", "Pick")).IsFalse(); diff --git a/TUnit.Mocks.Tests/SkipIfNotDynamicCodeSupportedAttribute.cs b/TUnit.Mocks.Tests/SkipIfNotDynamicCodeSupportedAttribute.cs new file mode 100644 index 0000000000..2023c6d414 --- /dev/null +++ b/TUnit.Mocks.Tests/SkipIfNotDynamicCodeSupportedAttribute.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; + +namespace TUnit.Mocks.Tests; + +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = true)] +public class SkipIfNotDynamicCodeSupportedAttribute : SkipAttribute +{ + public SkipIfNotDynamicCodeSupportedAttribute(string reason) : base(reason) + { + } + + public override Task ShouldSkip(TestRegisteredContext context) + { +#if NET + return Task.FromResult(!RuntimeFeature.IsDynamicCodeSupported); +#else + return Task.FromResult(false); +#endif + } +}