diff --git a/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs b/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs index 892f3b9940..2702d0f31e 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs +++ b/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs @@ -30,6 +30,32 @@ void M() return VerifyGeneratorOutput(source); } + [Test] + public Task Interface_With_Params_Array_Parameter() + { + var source = """ + using TUnit.Mocks; + + public interface IParamsSink + { + int Sum(params int[] values); + string Render(params object[] args); + string Combine(string prefix, params string[] parts); + T First(params T[] items); + } + + public class TestUsage + { + void M() + { + var mock = Mock.Of(); + } + } + """; + + return VerifyGeneratorOutput(source); + } + [Test] public Task Multi_Method_Interface() { diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Params_Array_Parameter.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Params_Array_Parameter.verified.txt new file mode 100644 index 0000000000..e39fcf7dab --- /dev/null +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Params_Array_Parameter.verified.txt @@ -0,0 +1,687 @@ +// +#pragma warning disable +#nullable enable + +public sealed class IParamsSinkMock : global::TUnit.Mocks.Mock, global::IParamsSink +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal IParamsSinkMock(global::IParamsSink mockObject, global::TUnit.Mocks.MockEngine engine) + : base(mockObject, engine) { } + + int global::IParamsSink.Sum(int[] values) => Object.Sum(values); + + string global::IParamsSink.Render(object[] args) => Object.Render(args); + + string global::IParamsSink.Combine(string prefix, string[] parts) => Object.Combine(prefix, parts); + + T global::IParamsSink.First(T[] items) => Object.First(items); +} + + +// ===== FILE SEPARATOR ===== + +// +#pragma warning disable +#nullable enable + +file sealed class IParamsSinkMockImpl : global::IParamsSink, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject +{ + private readonly global::TUnit.Mocks.MockEngine _engine; + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + + internal IParamsSinkMockImpl(global::TUnit.Mocks.MockEngine engine) + { + _engine = engine; + } + + public int Sum(int[] values) + { + return _engine.HandleCallWithReturn(0, "Sum", values, default); + } + + public string Render(object[] args) + { + return _engine.HandleCallWithReturn(1, "Render", args, ""); + } + + public string Combine(string prefix, string[] parts) + { + return _engine.HandleCallWithReturn(2, "Combine", prefix, parts, ""); + } + + public T First(T[] items) + { + return _engine.HandleCallWithReturn(3, "First", new object?[] { items }, default!, global::TUnit.Mocks.TypeArguments.Of.Value); + } + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public void RaiseEvent(string eventName, object? args) + { + throw new global::System.InvalidOperationException($"No event named '{eventName}' exists on this mock."); + } +} + +internal static class IParamsSinkMockFactory +{ + [global::System.Runtime.CompilerServices.ModuleInitializer] + internal static void Register() + { + global::TUnit.Mocks.MockRegistry.RegisterFactory(Create); + } + + internal static global::TUnit.Mocks.Mock CreateAutoMock(global::TUnit.Mocks.MockBehavior behavior) + { + var engine = new global::TUnit.Mocks.MockEngine(behavior); + var impl = new IParamsSinkMockImpl(engine); + engine.Raisable = impl; + var mock = new IParamsSinkMock(impl, engine); + return mock; + } + + internal static global::TUnit.Mocks.Mock Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs) + { + if (constructorArgs.Length > 0) throw new global::System.ArgumentException($"Interface mock 'global::IParamsSink' does not support constructor arguments, but {constructorArgs.Length} were provided."); + var engine = new global::TUnit.Mocks.MockEngine(behavior); + var impl = new IParamsSinkMockImpl(engine); + engine.Raisable = impl; + var mock = new IParamsSinkMock(impl, engine); + return mock; + } +} + + +// ===== FILE SEPARATOR ===== + +// +#pragma warning disable +#nullable enable + +public static class IParamsSink_MockMemberExtensions +{ + public static IParamsSink_Sum_M0_MockCall Sum(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg values) + { + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { values.Matcher }; + return new IParamsSink_Sum_M0_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "Sum", matchers); + } + + public static IParamsSink_Sum_M0_MockCall Sum(this global::TUnit.Mocks.Mock mock, global::System.Func values) + { + global::TUnit.Mocks.Arguments.Arg __fa_values = values; + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_values.Matcher }; + return new IParamsSink_Sum_M0_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "Sum", matchers); + } + /// Configure the mock setup for Sum with per-element matchers for its params parameter. + public static IParamsSink_Sum_M0_MockCall Sum(this global::TUnit.Mocks.Mock mock, params global::TUnit.Mocks.Arguments.Arg[] values) + { + var __paramsElementMatchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[values.Length]; + for (int __i = 0; __i < values.Length; __i++) + { + __paramsElementMatchers[__i] = values[__i].Matcher; + } + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { new global::TUnit.Mocks.Matchers.ParamsArrayMatcher(__paramsElementMatchers) }; + return new IParamsSink_Sum_M0_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "Sum", matchers); + } + /// Configure the mock setup for Sum with its params parameter matched as a whole array by Any(). + public static IParamsSink_Sum_M0_MockCall Sum(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.AnyArg values) + { + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { global::TUnit.Mocks.Matchers.AnyMatcher.Instance }; + return new IParamsSink_Sum_M0_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "Sum", matchers); + } + + public static IParamsSink_Render_M1_MockCall Render(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg args) + { + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { args.Matcher }; + return new IParamsSink_Render_M1_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Render", matchers); + } + + public static IParamsSink_Render_M1_MockCall Render(this global::TUnit.Mocks.Mock mock, global::System.Func args) + { + global::TUnit.Mocks.Arguments.Arg __fa_args = args; + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_args.Matcher }; + return new IParamsSink_Render_M1_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Render", matchers); + } + /// Configure the mock setup for Render with per-element matchers for its params parameter. + public static IParamsSink_Render_M1_MockCall Render(this global::TUnit.Mocks.Mock mock, params global::TUnit.Mocks.Arguments.Arg[] args) + { + var __paramsElementMatchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[args.Length]; + for (int __i = 0; __i < args.Length; __i++) + { + __paramsElementMatchers[__i] = args[__i].Matcher; + } + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { new global::TUnit.Mocks.Matchers.ParamsArrayMatcher(__paramsElementMatchers) }; + return new IParamsSink_Render_M1_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Render", matchers); + } + /// Configure the mock setup for Render with its params parameter matched as a whole array by Any(). + public static IParamsSink_Render_M1_MockCall Render(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.AnyArg args) + { + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { global::TUnit.Mocks.Matchers.AnyMatcher.Instance }; + return new IParamsSink_Render_M1_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Render", matchers); + } + + public static IParamsSink_Combine_M2_MockCall Combine(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg prefix, global::TUnit.Mocks.Arguments.Arg parts) + { + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { prefix.Matcher, parts.Matcher }; + return new IParamsSink_Combine_M2_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "Combine", matchers); + } + + public static IParamsSink_Combine_M2_MockCall Combine(this global::TUnit.Mocks.Mock mock, global::System.Func prefix, global::TUnit.Mocks.Arguments.Arg parts) + { + global::TUnit.Mocks.Arguments.Arg __fa_prefix = prefix; + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_prefix.Matcher, parts.Matcher }; + return new IParamsSink_Combine_M2_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "Combine", matchers); + } + + public static IParamsSink_Combine_M2_MockCall Combine(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg prefix, global::System.Func parts) + { + global::TUnit.Mocks.Arguments.Arg __fa_parts = parts; + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { prefix.Matcher, __fa_parts.Matcher }; + return new IParamsSink_Combine_M2_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "Combine", matchers); + } + + public static IParamsSink_Combine_M2_MockCall Combine(this global::TUnit.Mocks.Mock mock, global::System.Func prefix, global::System.Func parts) + { + global::TUnit.Mocks.Arguments.Arg __fa_prefix = prefix; + global::TUnit.Mocks.Arguments.Arg __fa_parts = parts; + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_prefix.Matcher, __fa_parts.Matcher }; + return new IParamsSink_Combine_M2_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "Combine", matchers); + } + /// Configure the mock setup for Combine with per-element matchers for its params parameter. + public static IParamsSink_Combine_M2_MockCall Combine(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg prefix, params global::TUnit.Mocks.Arguments.Arg[] parts) + { + var __paramsElementMatchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[parts.Length]; + for (int __i = 0; __i < parts.Length; __i++) + { + __paramsElementMatchers[__i] = parts[__i].Matcher; + } + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { prefix.Matcher, new global::TUnit.Mocks.Matchers.ParamsArrayMatcher(__paramsElementMatchers) }; + return new IParamsSink_Combine_M2_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "Combine", matchers); + } + /// Configure the mock setup for Combine with its params parameter matched as a whole array by Any(). + public static IParamsSink_Combine_M2_MockCall Combine(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg prefix, global::TUnit.Mocks.Arguments.AnyArg parts) + { + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { prefix.Matcher, global::TUnit.Mocks.Matchers.AnyMatcher.Instance }; + return new IParamsSink_Combine_M2_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "Combine", matchers); + } + + /// Configure the mock setup for Combine with every argument matched as Any<T>(). + public static IParamsSink_Combine_M2_MockCall Combine(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 IParamsSink_Combine_M2_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "Combine", matchers); + } + + public static IParamsSink_First_M3_MockCall First(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg items) + { + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { items.Matcher }; + return new IParamsSink_First_M3_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 3, "First", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); + } + + public static IParamsSink_First_M3_MockCall First(this global::TUnit.Mocks.Mock mock, global::System.Func items) + { + global::TUnit.Mocks.Arguments.Arg __fa_items = items; + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_items.Matcher }; + return new IParamsSink_First_M3_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 3, "First", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); + } + /// Configure the mock setup for First with per-element matchers for its params parameter. + public static IParamsSink_First_M3_MockCall First(this global::TUnit.Mocks.Mock mock, params global::TUnit.Mocks.Arguments.Arg[] items) + { + var __paramsElementMatchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[items.Length]; + for (int __i = 0; __i < items.Length; __i++) + { + __paramsElementMatchers[__i] = items[__i].Matcher; + } + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { new global::TUnit.Mocks.Matchers.ParamsArrayMatcher(__paramsElementMatchers) }; + return new IParamsSink_First_M3_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 3, "First", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); + } + /// Configure the mock setup for First with its params parameter matched as a whole array by Any(). + public static IParamsSink_First_M3_MockCall First(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.AnyArg items) + { + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { global::TUnit.Mocks.Matchers.AnyMatcher.Instance }; + return new IParamsSink_First_M3_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 3, "First", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); + } + + #if NET9_0_OR_GREATER + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static void Reset(this global::TUnit.Mocks.Mock mock) + => global::TUnit.Mocks.Mock.Reset(mock); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static void VerifyAll(this global::TUnit.Mocks.Mock mock) + => global::TUnit.Mocks.Mock.VerifyAll(mock); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static void VerifyNoOtherCalls(this global::TUnit.Mocks.Mock mock) + => global::TUnit.Mocks.Mock.VerifyNoOtherCalls(mock); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static void SetupAllProperties(this global::TUnit.Mocks.Mock mock) + => global::TUnit.Mocks.Mock.SetupAllProperties(mock); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static global::TUnit.Mocks.Diagnostics.MockDiagnostics GetDiagnostics(this global::TUnit.Mocks.Mock mock) + => global::TUnit.Mocks.Mock.GetDiagnostics(mock); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static void SetState(this global::TUnit.Mocks.Mock mock, string? stateName) + => global::TUnit.Mocks.Mock.SetState(mock, stateName); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static void InState(this global::TUnit.Mocks.Mock mock, string stateName, global::System.Action> configure) + => global::TUnit.Mocks.Mock.InState(mock, stateName, configure); + + extension(global::TUnit.Mocks.Mock mock) + { + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public global::System.Collections.Generic.IReadOnlyList Invocations => global::TUnit.Mocks.Mock.Invocations(mock); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public global::TUnit.Mocks.MockBehavior Behavior => global::TUnit.Mocks.Mock.Behavior(mock); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public global::TUnit.Mocks.IDefaultValueProvider? DefaultValueProvider + { + get => global::TUnit.Mocks.Mock.GetDefaultValueProvider(mock); + set => global::TUnit.Mocks.Mock.SetDefaultValueProvider(mock, value); + } + } + #endif +} + +[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] +public sealed class IParamsSink_Sum_M0_MockCall : global::TUnit.Mocks.Verification.ICallVerification +{ + private readonly global::TUnit.Mocks.IMockEngineAccess _engine; + private readonly int _memberId; + private readonly string _memberName; + private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; + private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; + + internal IParamsSink_Sum_M0_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) + { + _engine = engine; + _memberId = memberId; + _memberName = memberName; + _matchers = matchers; + } + + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() + { + var existing = global::System.Threading.Volatile.Read(ref _builder); + if (existing is not null) return existing; + return EnsureSetupSlow(); + } + + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetupSlow() + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + var fresh = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + var prev = global::System.Threading.Interlocked.CompareExchange(ref _builder, fresh, null); + if (prev is not null) return prev; + // AddSetup runs only on the CAS winner. Setup is sequential in practice, + // so a concurrent loser observing the builder before registration is benign. + _engine.AddSetup(setup); + return fresh; + } + + /// + public IParamsSink_Sum_M0_MockCall Returns(int value) { EnsureSetup().Returns(value); return this; } + /// + public IParamsSink_Sum_M0_MockCall Returns(global::System.Func factory) { EnsureSetup().Returns(factory); return this; } + /// + public IParamsSink_Sum_M0_MockCall ReturnsSequentially(params int[] values) { EnsureSetup().ReturnsSequentially(values); return this; } + /// + public IParamsSink_Sum_M0_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } + /// + public IParamsSink_Sum_M0_MockCall Throws(global::System.Exception exception) { EnsureSetup().Throws(exception); return this; } + /// + public IParamsSink_Sum_M0_MockCall Callback(global::System.Action callback) { EnsureSetup().Callback(callback); return this; } + /// + public IParamsSink_Sum_M0_MockCall TransitionsTo(string stateName) { EnsureSetup().TransitionsTo(stateName); return this; } + /// + public IParamsSink_Sum_M0_MockCall Then() { EnsureSetup().Then(); return this; } + + /// Configure a typed computed return value using the actual method parameters. + public IParamsSink_Sum_M0_MockCall Returns(global::System.Func factory) + { + EnsureSetup().Returns(args => factory((int[])args[0]!)); + return this; + } + + /// Execute a typed callback using the actual method parameters. + public IParamsSink_Sum_M0_MockCall Callback(global::System.Action callback) + { + EnsureSetup().Callback(callback); + return this; + } + + /// Configure a typed computed exception using the actual method parameters. + public IParamsSink_Sum_M0_MockCall Throws(global::System.Func exceptionFactory) + { + EnsureSetup().Throws(args => exceptionFactory((int[])args[0]!)); + return this; + } + + // ICallVerification + /// + public void WasCalled() => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(); + /// + public void WasCalled(global::TUnit.Mocks.Times times) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times); + /// + public void WasCalled(global::TUnit.Mocks.Times times, string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times, message); + /// + public void WasCalled(string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(message); + /// + public void WasNeverCalled() => _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(); + /// + public void WasNeverCalled(string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(message); +} + +[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] +public sealed class IParamsSink_Render_M1_MockCall : global::TUnit.Mocks.Verification.ICallVerification +{ + private readonly global::TUnit.Mocks.IMockEngineAccess _engine; + private readonly int _memberId; + private readonly string _memberName; + private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; + private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; + + internal IParamsSink_Render_M1_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) + { + _engine = engine; + _memberId = memberId; + _memberName = memberName; + _matchers = matchers; + } + + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() + { + var existing = global::System.Threading.Volatile.Read(ref _builder); + if (existing is not null) return existing; + return EnsureSetupSlow(); + } + + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetupSlow() + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + var fresh = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + var prev = global::System.Threading.Interlocked.CompareExchange(ref _builder, fresh, null); + if (prev is not null) return prev; + // AddSetup runs only on the CAS winner. Setup is sequential in practice, + // so a concurrent loser observing the builder before registration is benign. + _engine.AddSetup(setup); + return fresh; + } + + /// + public IParamsSink_Render_M1_MockCall Returns(string value) { EnsureSetup().Returns(value); return this; } + /// + public IParamsSink_Render_M1_MockCall Returns(global::System.Func factory) { EnsureSetup().Returns(factory); return this; } + /// + public IParamsSink_Render_M1_MockCall ReturnsSequentially(params string[] values) { EnsureSetup().ReturnsSequentially(values); return this; } + /// + public IParamsSink_Render_M1_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } + /// + public IParamsSink_Render_M1_MockCall Throws(global::System.Exception exception) { EnsureSetup().Throws(exception); return this; } + /// + public IParamsSink_Render_M1_MockCall Callback(global::System.Action callback) { EnsureSetup().Callback(callback); return this; } + /// + public IParamsSink_Render_M1_MockCall TransitionsTo(string stateName) { EnsureSetup().TransitionsTo(stateName); return this; } + /// + public IParamsSink_Render_M1_MockCall Then() { EnsureSetup().Then(); return this; } + + /// Configure a typed computed return value using the actual method parameters. + public IParamsSink_Render_M1_MockCall Returns(global::System.Func factory) + { + EnsureSetup().Returns(args => factory((object[])args[0]!)); + return this; + } + + /// Execute a typed callback using the actual method parameters. + public IParamsSink_Render_M1_MockCall Callback(global::System.Action callback) + { + EnsureSetup().Callback(callback); + return this; + } + + /// Configure a typed computed exception using the actual method parameters. + public IParamsSink_Render_M1_MockCall Throws(global::System.Func exceptionFactory) + { + EnsureSetup().Throws(args => exceptionFactory((object[])args[0]!)); + return this; + } + + // ICallVerification + /// + public void WasCalled() => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(); + /// + public void WasCalled(global::TUnit.Mocks.Times times) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times); + /// + public void WasCalled(global::TUnit.Mocks.Times times, string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times, message); + /// + public void WasCalled(string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(message); + /// + public void WasNeverCalled() => _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(); + /// + public void WasNeverCalled(string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(message); +} + +[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] +public sealed class IParamsSink_Combine_M2_MockCall : global::TUnit.Mocks.Verification.ICallVerification +{ + private readonly global::TUnit.Mocks.IMockEngineAccess _engine; + private readonly int _memberId; + private readonly string _memberName; + private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; + private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; + + internal IParamsSink_Combine_M2_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) + { + _engine = engine; + _memberId = memberId; + _memberName = memberName; + _matchers = matchers; + } + + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() + { + var existing = global::System.Threading.Volatile.Read(ref _builder); + if (existing is not null) return existing; + return EnsureSetupSlow(); + } + + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetupSlow() + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + var fresh = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + var prev = global::System.Threading.Interlocked.CompareExchange(ref _builder, fresh, null); + if (prev is not null) return prev; + // AddSetup runs only on the CAS winner. Setup is sequential in practice, + // so a concurrent loser observing the builder before registration is benign. + _engine.AddSetup(setup); + return fresh; + } + + /// + public IParamsSink_Combine_M2_MockCall Returns(string value) { EnsureSetup().Returns(value); return this; } + /// + public IParamsSink_Combine_M2_MockCall Returns(global::System.Func factory) { EnsureSetup().Returns(factory); return this; } + /// + public IParamsSink_Combine_M2_MockCall ReturnsSequentially(params string[] values) { EnsureSetup().ReturnsSequentially(values); return this; } + /// + public IParamsSink_Combine_M2_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } + /// + public IParamsSink_Combine_M2_MockCall Throws(global::System.Exception exception) { EnsureSetup().Throws(exception); return this; } + /// + public IParamsSink_Combine_M2_MockCall Callback(global::System.Action callback) { EnsureSetup().Callback(callback); return this; } + /// + public IParamsSink_Combine_M2_MockCall TransitionsTo(string stateName) { EnsureSetup().TransitionsTo(stateName); return this; } + /// + public IParamsSink_Combine_M2_MockCall Then() { EnsureSetup().Then(); return this; } + + /// Configure a typed computed return value using the actual method parameters. + public IParamsSink_Combine_M2_MockCall Returns(global::System.Func factory) + { + EnsureSetup().Returns(args => factory((string)args[0]!, (string[])args[1]!)); + return this; + } + + /// Execute a typed callback using the actual method parameters. + public IParamsSink_Combine_M2_MockCall Callback(global::System.Action callback) + { + EnsureSetup().Callback(callback); + return this; + } + + /// Configure a typed computed exception using the actual method parameters. + public IParamsSink_Combine_M2_MockCall Throws(global::System.Func exceptionFactory) + { + EnsureSetup().Throws(args => exceptionFactory((string)args[0]!, (string[])args[1]!)); + return this; + } + + // ICallVerification + /// + public void WasCalled() => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(); + /// + public void WasCalled(global::TUnit.Mocks.Times times) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times); + /// + public void WasCalled(global::TUnit.Mocks.Times times, string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times, message); + /// + public void WasCalled(string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(message); + /// + public void WasNeverCalled() => _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(); + /// + public void WasNeverCalled(string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(message); +} + +[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] +public sealed class IParamsSink_First_M3_MockCall : global::TUnit.Mocks.Verification.ICallVerification +{ + private readonly global::TUnit.Mocks.IMockEngineAccess _engine; + private readonly int _memberId; + private readonly string _memberName; + private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; + private readonly global::System.Collections.Immutable.ImmutableArray _typeArguments; + private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; + + internal IParamsSink_First_M3_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers, global::System.Collections.Immutable.ImmutableArray typeArguments) + { + _engine = engine; + _memberId = memberId; + _memberName = memberName; + _matchers = matchers; + _typeArguments = typeArguments; + } + + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() + { + var existing = global::System.Threading.Volatile.Read(ref _builder); + if (existing is not null) return existing; + return EnsureSetupSlow(); + } + + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetupSlow() + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName, _typeArguments); + var fresh = new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + var prev = global::System.Threading.Interlocked.CompareExchange(ref _builder, fresh, null); + if (prev is not null) return prev; + // AddSetup runs only on the CAS winner. Setup is sequential in practice, + // so a concurrent loser observing the builder before registration is benign. + _engine.AddSetup(setup); + return fresh; + } + + /// + public IParamsSink_First_M3_MockCall Returns(T value) { EnsureSetup().Returns(value); return this; } + /// + public IParamsSink_First_M3_MockCall Returns(global::System.Func factory) { EnsureSetup().Returns(factory); return this; } + /// + public IParamsSink_First_M3_MockCall ReturnsSequentially(params T[] values) { EnsureSetup().ReturnsSequentially(values); return this; } + /// + public IParamsSink_First_M3_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } + /// + public IParamsSink_First_M3_MockCall Throws(global::System.Exception exception) { EnsureSetup().Throws(exception); return this; } + /// + public IParamsSink_First_M3_MockCall Callback(global::System.Action callback) { EnsureSetup().Callback(callback); return this; } + /// + public IParamsSink_First_M3_MockCall TransitionsTo(string stateName) { EnsureSetup().TransitionsTo(stateName); return this; } + /// + public IParamsSink_First_M3_MockCall Then() { EnsureSetup().Then(); return this; } + + /// Configure a typed computed return value using the actual method parameters. + public IParamsSink_First_M3_MockCall Returns(global::System.Func factory) + { + EnsureSetup().Returns(args => factory((T[])args[0]!)); + return this; + } + + /// Execute a typed callback using the actual method parameters. + public IParamsSink_First_M3_MockCall Callback(global::System.Action callback) + { + EnsureSetup().Callback(callback); + return this; + } + + /// Configure a typed computed exception using the actual method parameters. + public IParamsSink_First_M3_MockCall Throws(global::System.Func exceptionFactory) + { + EnsureSetup().Throws(args => exceptionFactory((T[])args[0]!)); + return this; + } + + // ICallVerification + /// + public void WasCalled() => new global::TUnit.Mocks.MockMethodCall(_engine, _memberId, _memberName, _matchers, _typeArguments).WasCalled(); + /// + public void WasCalled(global::TUnit.Mocks.Times times) => new global::TUnit.Mocks.MockMethodCall(_engine, _memberId, _memberName, _matchers, _typeArguments).WasCalled(times); + /// + public void WasCalled(global::TUnit.Mocks.Times times, string? message) => new global::TUnit.Mocks.MockMethodCall(_engine, _memberId, _memberName, _matchers, _typeArguments).WasCalled(times, message); + /// + public void WasCalled(string? message) => new global::TUnit.Mocks.MockMethodCall(_engine, _memberId, _memberName, _matchers, _typeArguments).WasCalled(message); + /// + public void WasNeverCalled() => new global::TUnit.Mocks.MockMethodCall(_engine, _memberId, _memberName, _matchers, _typeArguments).WasNeverCalled(); + /// + public void WasNeverCalled(string? message) => new global::TUnit.Mocks.MockMethodCall(_engine, _memberId, _memberName, _matchers, _typeArguments).WasNeverCalled(message); +} + + +// ===== FILE SEPARATOR ===== + +// +#pragma warning disable +#nullable enable + +namespace TUnit.Mocks +{ + public static class IParamsSink_MockStaticExtension + { + extension(global::IParamsSink _) + { + public static global::IParamsSinkMock Mock() + { + return (global::IParamsSinkMock)global::IParamsSinkMockFactory.CreateAutoMock(global::TUnit.Mocks.Mock.DefaultBehavior); + } + + public static global::IParamsSinkMock Mock(global::TUnit.Mocks.MockBehavior behavior) + { + return (global::IParamsSinkMock)global::IParamsSinkMockFactory.CreateAutoMock(behavior); + } + } + } +} + + +// ===== FILE SEPARATOR ===== + +// +#pragma warning disable +#nullable enable + +namespace TUnit.Mocks.Generated; \ No newline at end of file diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs index ee418e43d4..677c80bebf 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs @@ -781,15 +781,18 @@ private static void GenerateMemberMethod(CodeWriter writer, MockMemberModel meth writer.AppendLine("#if NET9_0_OR_GREATER"); EmitMemberMethodBody(writer, method, model, safeName, includeRefStructArgs: true, captureModelTypeParameters: false, receiverIsThis: false); EmitFuncOverloads(writer, method, model, safeName, includeRefStructArgs: true, captureModelTypeParameters: false, receiverIsThis: false); + EmitParamsExpandedOverload(writer, method, model, safeName, includeRefStructArgs: true, captureModelTypeParameters: false, receiverIsThis: false); writer.AppendLine("#else"); EmitMemberMethodBody(writer, method, model, safeName, includeRefStructArgs: false, captureModelTypeParameters: false, receiverIsThis: false); EmitFuncOverloads(writer, method, model, safeName, includeRefStructArgs: false, captureModelTypeParameters: false, receiverIsThis: false); + EmitParamsExpandedOverload(writer, method, model, safeName, includeRefStructArgs: false, captureModelTypeParameters: false, receiverIsThis: false); writer.AppendLine("#endif"); } else { EmitMemberMethodBody(writer, method, model, safeName, includeRefStructArgs: false, captureModelTypeParameters: false, receiverIsThis: false); EmitFuncOverloads(writer, method, model, safeName, includeRefStructArgs: false, captureModelTypeParameters: false, receiverIsThis: false); + EmitParamsExpandedOverload(writer, method, model, safeName, includeRefStructArgs: false, captureModelTypeParameters: false, receiverIsThis: false); } EmitAnyArgsOverload(writer, method, model, safeName, captureModelTypeParameters: false, receiverIsThis: false); @@ -823,15 +826,18 @@ private static void GenerateGenericMethodMembersInCurrentBlock(CodeWriter writer writer.AppendLine("#if NET9_0_OR_GREATER"); EmitMemberMethodBody(writer, method, model, safeName, includeRefStructArgs: true, captureModelTypeParameters: true, receiverIsThis: false); EmitFuncOverloads(writer, method, model, safeName, includeRefStructArgs: true, captureModelTypeParameters: true, receiverIsThis: false); + EmitParamsExpandedOverload(writer, method, model, safeName, includeRefStructArgs: true, captureModelTypeParameters: true, receiverIsThis: false); writer.AppendLine("#else"); EmitMemberMethodBody(writer, method, model, safeName, includeRefStructArgs: false, captureModelTypeParameters: true, receiverIsThis: false); EmitFuncOverloads(writer, method, model, safeName, includeRefStructArgs: false, captureModelTypeParameters: true, receiverIsThis: false); + EmitParamsExpandedOverload(writer, method, model, safeName, includeRefStructArgs: false, captureModelTypeParameters: true, receiverIsThis: false); writer.AppendLine("#endif"); } else { EmitMemberMethodBody(writer, method, model, safeName, includeRefStructArgs: false, captureModelTypeParameters: true, receiverIsThis: false); EmitFuncOverloads(writer, method, model, safeName, includeRefStructArgs: false, captureModelTypeParameters: true, receiverIsThis: false); + EmitParamsExpandedOverload(writer, method, model, safeName, includeRefStructArgs: false, captureModelTypeParameters: true, receiverIsThis: false); } EmitAnyArgsOverload(writer, method, model, safeName, captureModelTypeParameters: true, receiverIsThis: false); @@ -844,15 +850,18 @@ internal static void GenerateGenericMethodMembersForWrapper(CodeWriter writer, M writer.AppendLine("#if NET9_0_OR_GREATER"); EmitMemberMethodBody(writer, method, model, safeName, includeRefStructArgs: true, captureModelTypeParameters: true, receiverIsThis: true); EmitFuncOverloads(writer, method, model, safeName, includeRefStructArgs: true, captureModelTypeParameters: true, receiverIsThis: true); + EmitParamsExpandedOverload(writer, method, model, safeName, includeRefStructArgs: true, captureModelTypeParameters: true, receiverIsThis: true); writer.AppendLine("#else"); EmitMemberMethodBody(writer, method, model, safeName, includeRefStructArgs: false, captureModelTypeParameters: true, receiverIsThis: true); EmitFuncOverloads(writer, method, model, safeName, includeRefStructArgs: false, captureModelTypeParameters: true, receiverIsThis: true); + EmitParamsExpandedOverload(writer, method, model, safeName, includeRefStructArgs: false, captureModelTypeParameters: true, receiverIsThis: true); writer.AppendLine("#endif"); } else { EmitMemberMethodBody(writer, method, model, safeName, includeRefStructArgs: false, captureModelTypeParameters: true, receiverIsThis: true); EmitFuncOverloads(writer, method, model, safeName, includeRefStructArgs: false, captureModelTypeParameters: true, receiverIsThis: true); + EmitParamsExpandedOverload(writer, method, model, safeName, includeRefStructArgs: false, captureModelTypeParameters: true, receiverIsThis: true); } EmitAnyArgsOverload(writer, method, model, safeName, captureModelTypeParameters: true, receiverIsThis: true); @@ -1458,11 +1467,23 @@ private static void GenerateRaiseExtensionMethods(CodeWriter writer, MockTypeMod } } - private static string GetArgParameterList(MockMemberModel method, bool includeRefStructArgs) + /// + /// When non-null, emits the method's last parameter with this renderer instead of the default + /// Arg<T> form — used for the params-expanded (params Arg<TElem>[]) + /// and AnyArg-slotted overloads of params T[] methods. + /// + private static string GetArgParameterList(MockMemberModel method, bool includeRefStructArgs, + Func? lastParamOverride = null) { var parts = new List(); - foreach (var p in method.Parameters) + for (var i = 0; i < method.Parameters.Length; i++) { + var p = method.Parameters[i]; + if (lastParamOverride is not null && i == method.Parameters.Length - 1) + { + parts.Add(lastParamOverride(p)); + continue; + } if (p.Direction == ParameterDirection.Out) { // Normally out params are omitted from the extension signature so callers @@ -1491,6 +1512,145 @@ private static string GetArgParameterList(MockMemberModel method, bool includeRe return string.Join(", ", parts); } + /// + /// Emits a params-expanded setup overload for methods whose last parameter is params T[]: + /// the trailing Arg<T[]> slot becomes params Arg<T>[] so callers can match + /// per element (mock.Sum(Is(1), Is(2))). The element matchers are wrapped into a single + /// ParamsArrayMatcher so matcher arity stays equal to the declared parameter count. + /// The whole-array overload remains and wins normal-form overload resolution for + /// Sum(Any()) / Sum(array), preserving existing behavior. + /// + private static void EmitParamsExpandedOverload(CodeWriter writer, MockMemberModel method, MockTypeModel model, + string safeName, bool includeRefStructArgs, bool captureModelTypeParameters, bool receiverIsThis) + { + var last = method.Parameters.Length > 0 ? method.Parameters[method.Parameters.Length - 1] : null; + if (last is null || last.ParamsElementType is null || last.Direction != ParameterDirection.In) + { + return; + } + + var (useTypedWrapper, returnType, setupReturnType) = GetReturnTypeInfo(method, model, safeName); + + var paramList = GetArgParameterList(method, includeRefStructArgs, + p => $"params global::TUnit.Mocks.Arguments.Arg<{p.ParamsElementType}>[] {p.Name}"); + var typeParams = captureModelTypeParameters + ? MockImplBuilder.GetTypeParameterList(method) + : GetCombinedTypeParameterList(model, method); + var constraints = captureModelTypeParameters + ? MockImplBuilder.GetConstraintClauses(method) + : GetCombinedConstraintClauses(model, method); + + var safeMemberName = GetSafeMemberName(method.Name, model); + var fullParamList = captureModelTypeParameters + ? paramList + : BuildExtensionMethodParameterList(model, paramList); + + var methodDeclarationPrefix = captureModelTypeParameters ? "public" : "public static"; + + writer.AppendLine($"/// Configure the mock setup for {method.Name} with per-element matchers for its params parameter."); + using (writer.Block($"{methodDeclarationPrefix} {returnType} {safeMemberName}{typeParams}({fullParamList}){constraints}")) + { + if (receiverIsThis) + { + writer.AppendLine("var mock = this;"); + } + + EmitOutParamDefaults(writer, method); + + writer.AppendLine($"var __paramsElementMatchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[{last.Name}.Length];"); + using (writer.Block($"for (int __i = 0; __i < {last.Name}.Length; __i++)")) + { + writer.AppendLine($"__paramsElementMatchers[__i] = {last.Name}[__i].Matcher;"); + } + + EmitMatchersArrayWithParamsSlot(writer, method, includeRefStructArgs, + "new global::TUnit.Mocks.Matchers.ParamsArrayMatcher(__paramsElementMatchers)"); + + EmitReturnConstruction(writer, method, model, safeName, useTypedWrapper, setupReturnType); + } + + EmitParamsAnyArgOverload(writer, method, model, safeName, last, includeRefStructArgs, captureModelTypeParameters, receiverIsThis); + } + + /// + /// Emits an overload whose params slot is the untyped AnyArg sentinel, matching the + /// whole packed array with AnyMatcher<T[]>. Without it, Sum(Any()) would be + /// ambiguous between the whole-array Arg<T[]> overload and the params-expanded + /// params Arg<T>[] overload (AnyArg converts to both, and the normal-vs-expanded + /// tie-break only applies to identical parameter lists). The identity conversion to AnyArg + /// beats both user-defined conversions, preserving the pre-existing whole-array semantics. + /// + private static void EmitParamsAnyArgOverload(CodeWriter writer, MockMemberModel method, MockTypeModel model, + string safeName, MockParameterModel last, bool includeRefStructArgs, bool captureModelTypeParameters, bool receiverIsThis) + { + static string AnyArgLastParam(MockParameterModel p) => $"global::TUnit.Mocks.Arguments.AnyArg {p.Name}"; + + var paramList = GetArgParameterList(method, includeRefStructArgs, AnyArgLastParam); + + // Two same-name params methods that differ only in element type (e.g. M(params int[]) and + // M(params string[])) would both produce this AnyArg-slotted signature — skip on collision. + foreach (var m in model.Methods) + { + if (m.MemberId == method.MemberId || m.Name != method.Name) continue; + if (m.ExplicitInterfaceName is not null && !m.IsStaticAbstract) continue; + if (m.TypeParameters.Length != method.TypeParameters.Length) continue; + var mLast = m.Parameters.Length > 0 ? m.Parameters[m.Parameters.Length - 1] : null; + if (mLast is null || mLast.ParamsElementType is null || mLast.Direction != ParameterDirection.In) continue; + if (GetArgParameterList(m, includeRefStructArgs, AnyArgLastParam) == paramList) 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, model); + var fullParamList = captureModelTypeParameters + ? paramList + : BuildExtensionMethodParameterList(model, paramList); + + var methodDeclarationPrefix = captureModelTypeParameters ? "public" : "public static"; + + writer.AppendLine($"/// Configure the mock setup for {method.Name} with its params parameter matched as a whole array by Any()."); + using (writer.Block($"{methodDeclarationPrefix} {returnType} {safeMemberName}{typeParams}({fullParamList}){constraints}")) + { + if (receiverIsThis) + { + writer.AppendLine("var mock = this;"); + } + + EmitOutParamDefaults(writer, method); + + // AnyMatcher.Instance: whole-array Any, identical to the Arg overload with Any(). + EmitMatchersArrayWithParamsSlot(writer, method, includeRefStructArgs, + $"global::TUnit.Mocks.Matchers.AnyMatcher<{last.FullyQualifiedType}>.Instance"); + + EmitReturnConstruction(writer, method, model, safeName, useTypedWrapper, setupReturnType); + } + } + + /// + /// Emits the matchers array for a params-method overload: leading matchable parameters + /// contribute {name}.Matcher, and the trailing params slot is filled with + /// . The params parameter is always In and never + /// a ref struct, so it is always the last matchable parameter. + /// + private static void EmitMatchersArrayWithParamsSlot(CodeWriter writer, MockMemberModel method, + bool includeRefStructArgs, string paramsSlotExpression) + { + var matchableParams = includeRefStructArgs + ? method.Parameters.Where(p => p.Direction != ParameterDirection.Out).ToList() + : method.Parameters.Where(p => p.Direction != ParameterDirection.Out && !p.IsRefStruct).ToList(); + + var matcherArgs = matchableParams + .Select((p, i) => i == matchableParams.Count - 1 ? paramsSlotExpression : $"{p.Name}.Matcher"); + writer.AppendLine($"var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] {{ {string.Join(", ", matcherArgs)} }};"); + } + private static string BuildExtensionMethodParameterList(MockTypeModel model, string paramList) { var mockableType = MockImplBuilder.GetMockableTypeName(model); diff --git a/TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs b/TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs index 526b2a1c27..f7b05b22e2 100644 --- a/TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs +++ b/TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs @@ -634,7 +634,12 @@ private static MockMemberModel CreateMethodModel(IMethodSymbol method, ref int m DefaultValueExpression = p.HasExplicitDefaultValue ? FormatDefaultValue(p) : null, IsValueType = p.Type.IsValueType, IsRefStruct = p.Type.IsRefLikeType, - SpanElementType = GetSpanElementType(p.Type) + SpanElementType = GetSpanElementType(p.Type), + // Only single-dimensional arrays get the params-expanded setup overload; + // params collections (C# 13) and params spans degrade to whole-value matching. + ParamsElementType = p.IsParams && p.Type is IArrayTypeSymbol paramsArray + ? paramsArray.ElementType.GetFullyQualifiedNameWithNullability() + : null }).ToImmutableArray() ), TypeParameters = new EquatableArray( diff --git a/TUnit.Mocks.SourceGenerator/Models/MockParameterModel.cs b/TUnit.Mocks.SourceGenerator/Models/MockParameterModel.cs index ef14348188..5de2a7b4d3 100644 --- a/TUnit.Mocks.SourceGenerator/Models/MockParameterModel.cs +++ b/TUnit.Mocks.SourceGenerator/Models/MockParameterModel.cs @@ -26,6 +26,17 @@ internal sealed record MockParameterModel : IEquatable /// public bool IsNonSpanRefStruct => IsRefStruct && SpanElementType is null; + /// + /// For params T[] parameters, the fully qualified element type with nullability (e.g. + /// "global::System.Object"). Null for non-params parameters and for params collections that + /// are not single-dimensional arrays. Non-null implies the parameter is a params array — there + /// is no separate flag. Used to emit the params-expanded setup overload that accepts + /// per-element Arg<T> matchers. Only populated for methods: indexers cannot + /// declare params parameters that surface in setups, and delegate parameters are only + /// used for event raising. + /// + public string? ParamsElementType { get; init; } + public bool Equals(MockParameterModel? other) { if (other is null) return false; @@ -35,7 +46,8 @@ public bool Equals(MockParameterModel? other) && Direction == other.Direction && IsValueType == other.IsValueType && IsRefStruct == other.IsRefStruct - && SpanElementType == other.SpanElementType; + && SpanElementType == other.SpanElementType + && ParamsElementType == other.ParamsElementType; } public override int GetHashCode() @@ -49,6 +61,7 @@ public override int GetHashCode() hash = hash * 31 + IsValueType.GetHashCode(); hash = hash * 31 + IsRefStruct.GetHashCode(); hash = hash * 31 + (SpanElementType?.GetHashCode() ?? 0); + hash = hash * 31 + (ParamsElementType?.GetHashCode() ?? 0); return hash; } } diff --git a/TUnit.Mocks.Tests/KitchenSinkInterfaceTests.cs b/TUnit.Mocks.Tests/KitchenSinkInterfaceTests.cs index 69731be371..4eb472aa26 100644 --- a/TUnit.Mocks.Tests/KitchenSinkInterfaceTests.cs +++ b/TUnit.Mocks.Tests/KitchenSinkInterfaceTests.cs @@ -67,6 +67,9 @@ public interface IKitchenSink : IEnumerable, IAltNamed // ── Params array ── int Sum(params int[] values); + string Join(params string[] parts); + string Render(params object[] args); + T First(params T[] items); // ── Tuple return ── (int Count, string Label) Describe(); @@ -400,6 +403,120 @@ public async Task Params_Array_Method_Configurable() mock.Sum(Any()).WasCalled(Times.Exactly(2)); } + [Test] + public async Task Params_PerElement_Matchers() + { + var mock = IKitchenSink.Mock(); + mock.Sum(Is(1), Is(2), Is(3)).Returns(6); + + await Assert.That(mock.Object.Sum(1, 2, 3)).IsEqualTo(6); + await Assert.That(mock.Object.Sum(1, 2, 9)).IsEqualTo(0); + await Assert.That(mock.Object.Sum(1, 2)).IsEqualTo(0); + } + + [Test] + public async Task Params_PerElement_Mixed_Any() + { + var mock = IKitchenSink.Mock(); + mock.Sum(Is(1), Any()).Returns(9); + + await Assert.That(mock.Object.Sum(1, 50)).IsEqualTo(9); + await Assert.That(mock.Object.Sum(2, 50)).IsEqualTo(0); + } + + [Test] + public async Task Params_PerElement_Verification() + { + var mock = IKitchenSink.Mock(); + mock.Object.Sum(1, 2); + + mock.Sum(Is(1), Is(2)).WasCalled(Times.Once); + mock.Sum(Is(9), Is(9)).WasNeverCalled(); + } + + [Test] + public async Task Params_Empty_Setup_Matches_Empty_Call_Only() + { + var mock = IKitchenSink.Mock(); + mock.Sum().Returns(42); + + await Assert.That(mock.Object.Sum()).IsEqualTo(42); + await Assert.That(mock.Object.Sum(1)).IsEqualTo(0); + } + + [Test] + public async Task Params_PerElement_Capture() + { + var mock = IKitchenSink.Mock(); + var first = Any(); + mock.Sum(first, Any()).Returns(0); + + mock.Object.Sum(7, 9); + + await Assert.That(first.Latest).IsEqualTo(7); + } + + [Test] + public async Task Params_String_Elements_PerElement() + { + var mock = IKitchenSink.Mock(); + mock.Join(Is("a"), Any()).Returns("x"); + + await Assert.That(mock.Object.Join("a", "b")).IsEqualTo("x"); + await Assert.That(mock.Object.Join("z", "b")).IsNotEqualTo("x"); + } + + [Test] + public async Task Params_Object_Elements_Raw_Values_And_Typed_Is() + { + var mock = IKitchenSink.Mock(); + mock.Render(1, "two").Returns("matched"); + + await Assert.That(mock.Object.Render(1, "two")).IsEqualTo("matched"); + await Assert.That(mock.Object.Render(2, "two")).IsNotEqualTo("matched"); + + var mock2 = IKitchenSink.Mock(); + mock2.Render(Is(1)).Returns("typed"); + + await Assert.That(mock2.Object.Render(1)).IsEqualTo("typed"); + } + + [Test] + public async Task Params_Object_Mistyped_Arg_Throws_Helpful_Error() + { + var mock = IKitchenSink.Mock(); + + // Is(1) is Arg; converting it to the Arg element slot would silently + // never match — the guard must throw instead. + var ex = Assert.Throws(() => mock.Render(Is(1))); + await Assert.That(ex.Message).Contains("Arg"); + + // Same misuse via the explicit Is(value) path, which bypasses the implicit + // operator and constructs the ExactMatcher directly — the guard lives there too. + var ex2 = Assert.Throws(() => mock.Render(Is(Is(1)))); + await Assert.That(ex2.Message).Contains("Arg"); + } + + [Test] + public async Task Params_Generic_PerElement() + { + var mock = IKitchenSink.Mock(); + mock.First(Is(1), Any()).Returns(5); + + await Assert.That(mock.Object.First(1, 9)).IsEqualTo(5); + await Assert.That(mock.Object.First(2, 9)).IsEqualTo(0); + } + + [Test] + public async Task Params_WholeArray_Matchers_Still_Work() + { + var mock = IKitchenSink.Mock(); + mock.Sum(Is(a => a is { Length: > 2 })).Returns(100); + + await Assert.That(mock.Object.Sum(1, 2, 3)).IsEqualTo(100); + await Assert.That(mock.Object.Sum(1, 2)).IsEqualTo(0); + } + // ── Tuple return ── [Test] diff --git a/TUnit.Mocks/Arguments/ArgOfT.cs b/TUnit.Mocks/Arguments/ArgOfT.cs index ff70979240..7eaa2af511 100644 --- a/TUnit.Mocks/Arguments/ArgOfT.cs +++ b/TUnit.Mocks/Arguments/ArgOfT.cs @@ -32,6 +32,11 @@ public Arg(IArgumentMatcher matcher) /// Implicitly converts a raw value to an using exact equality matching. /// The value to match against. + /// + /// When is itself a boxed — e.g. Is(1) + /// converted into an Arg<object> slot, which could never match. Thrown by + /// . + /// public static implicit operator Arg(T value) => new(new ExactMatcher(value)); /// Implicitly converts a predicate to an using predicate matching. diff --git a/TUnit.Mocks/Matchers/ExactMatcher.cs b/TUnit.Mocks/Matchers/ExactMatcher.cs index eef6ed4756..085a3dd8bd 100644 --- a/TUnit.Mocks/Matchers/ExactMatcher.cs +++ b/TUnit.Mocks/Matchers/ExactMatcher.cs @@ -4,7 +4,25 @@ internal sealed class ExactMatcher : Arguments.IArgumentMatcher { private readonly T? _expected; - public ExactMatcher(T? expected) => _expected = expected; + public ExactMatcher(T? expected) + { + // An Arg can reach here boxed as the expected VALUE — e.g. Is(1) implicitly converted + // into an Arg slot, or Is(someArg). Exact-matching the matcher struct + // itself can never succeed, so fail fast with guidance. Arg is a struct, so it can only + // be boxed into a reference-type slot — skip the check (and the GetType() boxing it would + // cost on a Nullable) for value-type T, where it can never trigger. + if (!typeof(T).IsValueType + && expected is not null && expected.GetType() is { IsConstructedGenericType: true } expectedType + && expectedType.GetGenericTypeDefinition() == typeof(Arguments.Arg<>)) + { + throw new ArgumentException( + $"An Arg<{expectedType.GetGenericArguments()[0].Name}> matcher was passed where Arg<{typeof(T).Name}> was expected, " + + $"so it would be matched as a literal value and never succeed. " + + $"Use Arg.Is<{typeof(T).Name}>(value), Arg.Any<{typeof(T).Name}>(), or pass the raw value instead."); + } + + _expected = expected; + } public bool Matches(T? value) => EqualityComparer.Default.Equals(_expected!, value!); diff --git a/TUnit.Mocks/Matchers/ParamsArrayMatcher.cs b/TUnit.Mocks/Matchers/ParamsArrayMatcher.cs new file mode 100644 index 0000000000..2ee1e8e0c0 --- /dev/null +++ b/TUnit.Mocks/Matchers/ParamsArrayMatcher.cs @@ -0,0 +1,106 @@ +using System.ComponentModel; +using TUnit.Mocks.Arguments; + +namespace TUnit.Mocks.Matchers; + +/// +/// Composite matcher for params T[] parameters: matches the packed argument array +/// element-by-element against the per-element matchers supplied at the setup call site. +/// Occupies a single top-level matcher slot so setup arity stays equal to the declared +/// parameter count. Public for generated code access. Not intended for direct use. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class ParamsArrayMatcher : IArgumentMatcher, ICapturingMatcher +{ + private readonly IArgumentMatcher[] _elementMatchers; + + /// Creates a matcher from per-element matchers. Public for generated code access. + public ParamsArrayMatcher(IArgumentMatcher[] elementMatchers) + { + _elementMatchers = elementMatchers ?? throw new ArgumentNullException(nameof(elementMatchers)); + } + + public bool Matches(object? value) + { + // Reference-element params (object[], string[], ...) are object?[]-compatible via array + // covariance — index directly, no per-element GetValue call. + if (value is object?[] referenceArray) + { + if (referenceArray.Length != _elementMatchers.Length) + { + return false; + } + + for (var i = 0; i < _elementMatchers.Length; i++) + { + if (!_elementMatchers[i].Matches(referenceArray[i])) + { + return false; + } + } + + return true; + } + + // Value-element params pack as the concrete array type (e.g. int[] for params int[]), + // which is not object?[] — match via System.Array. + if (value is not Array array || array.Length != _elementMatchers.Length) + { + return false; + } + + for (var i = 0; i < _elementMatchers.Length; i++) + { + if (!_elementMatchers[i].Matches(array.GetValue(i))) + { + return false; + } + } + + return true; + } + + /// + /// Forwards deferred capture to each element matcher so per-element + /// / still work. + /// only walks top-level matchers. + /// + void ICapturingMatcher.ApplyCapture(object? value) + { + // Mirror the index strategy of Matches: covariant fast-path for reference-element arrays, + // System.Array fallback for value-element arrays. + if (value is object?[] referenceArray) + { + if (referenceArray.Length != _elementMatchers.Length) + { + return; + } + + for (var i = 0; i < _elementMatchers.Length; i++) + { + if (_elementMatchers[i] is ICapturingMatcher capturing) + { + capturing.ApplyCapture(referenceArray[i]); + } + } + + return; + } + + if (value is not Array array || array.Length != _elementMatchers.Length) + { + return; + } + + for (var i = 0; i < _elementMatchers.Length; i++) + { + if (_elementMatchers[i] is ICapturingMatcher capturing) + { + capturing.ApplyCapture(array.GetValue(i)); + } + } + } + + public string Describe() => + $"[{string.Join(", ", _elementMatchers.Select(m => m.Describe()))}] ({_elementMatchers.Length} element(s))"; +} diff --git a/docs/docs/writing-tests/mocking/argument-matchers.md b/docs/docs/writing-tests/mocking/argument-matchers.md index 9fbed82606..1bdc892971 100644 --- a/docs/docs/writing-tests/mocking/argument-matchers.md +++ b/docs/docs/writing-tests/mocking/argument-matchers.md @@ -252,6 +252,48 @@ await Assert.That(nameArg.Values).Count().IsEqualTo(3); Capture works in both setup and verification contexts. Store the `Arg` in a variable, then inspect `.Values` or `.Latest` after exercising the code. ::: +## Params Array Parameters + +Methods with a `params T[]` parameter support two matching styles: per-element and whole-array. + +```csharp +public interface ICalculator +{ + int Sum(params int[] values); +} +``` + +**Per-element** — pass one matcher per expected element. The setup matches only calls with exactly that many arguments, each satisfying its matcher: + +```csharp +mock.Sum(Is(1), Is(2), Is(3)).Returns(6); // matches Sum(1, 2, 3) only +mock.Sum(Is(1), Any()).Returns(9); // matches Sum(1, ) +mock.Sum().Returns(0); // matches Sum() — zero arguments +mock.Sum(1, 2).Returns(3); // raw values work too (exact match) +``` + +**Whole-array** — pass a single matcher for the packed array: + +```csharp +mock.Sum(Any()).Returns(100); // any number of arguments, including none +mock.Sum(Is(a => a is { Length: > 2 })).Returns(7); // predicate over the whole array +``` + +Both styles work for verification as well: + +```csharp +mock.Sum(Is(1), Is(2)).WasCalled(Times.Once); +mock.Sum(Any()).WasCalled(Times.Exactly(2)); +``` + +:::tip params object[] +For `params object[]` parameters, use raw values (`mock.Log(1, "two")`) or a typed matcher (`Is(1)`). A bare `Is(1)` creates an `Arg`, which cannot stand in for an `Arg` element — TUnit.Mocks throws a descriptive error at setup time if you try. +::: + +:::note +Per-element matching is available only for `params T[]` **array** parameters. C# 13 `params` collections (e.g. `params IEnumerable`) and `params Span` fall back to whole-value matching — pass a single `Arg` matcher for the whole collection. +::: + ## Ref Struct Parameters Regular `Arg` matchers cannot be used with ref struct types like `ReadOnlySpan` or `Span` because ref structs cannot be generic type arguments. On **.NET 9+**, TUnit.Mocks provides `RefStructArg` which uses the `allows ref struct` anti-constraint to make these parameters visible in the setup and verification API.