Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 95 additions & 63 deletions Source/Mockolate.SourceGenerators/Sources/Sources.MethodSetups.cs

Large diffs are not rendered by default.

13 changes: 7 additions & 6 deletions Source/Mockolate/Mock.Verify.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Mockolate.Parameters;
using Mockolate.Setup;
using Mockolate.Verify;

namespace Mockolate;
Expand All @@ -24,28 +25,28 @@ bool IMockVerify<T>.ThatAllSetupsAreUsed()

/// <inheritdoc cref="IMockVerifyInvokedWithToString{T}.ToString()" />
VerificationResult<T> IMockVerifyInvokedWithToString<T>.ToString()
=> Registrations.Method(Subject, Registrations.Prefix + ".ToString");
=> Registrations.Method(Subject, new MethodParameterMatch(Registrations.Prefix + ".ToString", []));

/// <inheritdoc cref="IMockVerifyInvokedWithEquals{T}.Equals(IParameter{object?})" />
VerificationResult<T> IMockVerifyInvokedWithEquals<T>.Equals(IParameter<object?>? obj)
=> Registrations.Method(Subject, Registrations.Prefix + ".Equals",
new NamedParameter("obj", (IParameter)(obj ?? It.IsNull<object>())));
=> Registrations.Method(Subject, new MethodParameterMatch(Registrations.Prefix + ".Equals",
[new NamedParameter("obj", (IParameter)(obj ?? It.IsNull<object>())),]));

/// <inheritdoc cref="IMockVerifyInvokedWithGetHashCode{T}.GetHashCode()" />
VerificationResult<T> IMockVerifyInvokedWithGetHashCode<T>.GetHashCode()
=> Registrations.Method(Subject, Registrations.Prefix + ".GetHashCode");
=> Registrations.Method(Subject, new MethodParameterMatch(Registrations.Prefix + ".GetHashCode", []));

/// <summary>
/// Counts the invocations of method <paramref name="methodName" /> with matching <paramref name="parameters" />.
/// </summary>
public VerificationResult<T> Method(string methodName, params NamedParameter[] parameters)
=> Registrations.Method(Subject, methodName, parameters);
=> Registrations.Method(Subject, new MethodParameterMatch(methodName, parameters));

/// <summary>
/// Counts the invocations of method <paramref name="methodName" /> with matching <paramref name="parameters" />.
/// </summary>
public VerificationResult<T> Method(string methodName, IParameters parameters)
=> Registrations.Method(Subject, methodName, parameters);
=> Registrations.Method(Subject, new MethodParametersMatch(methodName, parameters));

/// <summary>
/// Counts the getter accesses of property <paramref name="propertyName" />.
Expand Down
20 changes: 20 additions & 0 deletions Source/Mockolate/MockExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Mockolate.Exceptions;
using Mockolate.Setup;
using Mockolate.Verify;

namespace Mockolate;

Expand All @@ -18,4 +20,22 @@ public static void ClearAllInteractions<T>(this IMockSetup<T> mock)
hasMockRegistration.Registrations.ClearAllInteractions();
}
}

/// <summary>
/// Verifies the method invocations for the <paramref name="setup" /> on the mock.
/// </summary>
public static VerificationResult<T> InvokedSetup<T>(this IMockVerify<T> verify, IMethodSetup setup)
{
if (verify is not Mock<T> mock)
{
throw new MockException("The subject is no mock subject.");
}

if (setup is not IVerifiableMethodSetup verifiableMethodSetup)
{
throw new MockException("The setup is not verifiable.");
}

return mock.Registrations.Method(mock.Subject, verifiableMethodSetup.GetMatch());
}
}
27 changes: 4 additions & 23 deletions Source/Mockolate/MockRegistration.Verify.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,37 +12,18 @@ namespace Mockolate;
public partial class MockRegistration
{
/// <summary>
/// Counts the invocations of method <paramref name="methodName" /> with matching <paramref name="parameters" /> on the
/// <paramref name="subject" />.
/// Counts the invocations of methods matching the <paramref name="methodMatch" /> on the <paramref name="subject" />.
/// </summary>
public VerificationResult<T> Method<T>(T subject, string methodName, params NamedParameter[] parameters)
public VerificationResult<T> Method<T>(T subject, IMethodMatch methodMatch)
=> new(
Comment on lines 14 to 18
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MockRegistration.Method<T> is a public API, and (per the API snapshot updates) the overloads that accept (string methodName, params NamedParameter[]) / (string methodName, IParameters) were removed in favor of IMethodMatch. Consider keeping the old overloads (possibly [Obsolete]) and delegating them to the new overload to avoid a breaking change for consumers using MockRegistration directly.

Copilot uses AI. Check for mistakes.
subject,
Interactions,
Interactions.Interactions
.OfType<MethodInvocation>()
.Where(method => method.Name.Equals(methodName) &&
method.Parameters.Length == parameters.Length &&
!parameters
.Where((parameter, i) => !parameter.Matches(method.Parameters[i]))
.Any())
.Where(methodMatch.Matches)
.Cast<IInteraction>()
.ToArray(),
$"invoked method {methodName.SubstringAfterLast('.')}({string.Join(", ", parameters.Select(x => x.Parameter.ToString()))})");

/// <summary>
/// Counts the invocations of method <paramref name="methodName" /> with matching <paramref name="parameters" /> on the
/// <paramref name="subject" />.
/// </summary>
public VerificationResult<T> Method<T>(T subject, string methodName, IParameters parameters) => new(subject,
Interactions,
Interactions.Interactions
.OfType<MethodInvocation>()
.Where(method => method.Name.Equals(methodName) &&
parameters.Matches(method.Parameters))
.Cast<IInteraction>()
.ToArray(),
$"invoked method {methodName.SubstringAfterLast('.')}({parameters})");
$"invoked method {methodMatch}");
Comment thread
vbreuss marked this conversation as resolved.

/// <summary>
/// Counts the getter accesses of property <paramref name="propertyName" /> on the <paramref name="subject" />.
Expand Down
14 changes: 14 additions & 0 deletions Source/Mockolate/Setup/IMethodMatch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Mockolate.Interactions;

namespace Mockolate.Setup;

/// <summary>
/// A method match to verify method invocations from a setup.
/// </summary>
public interface IMethodMatch
{
/// <summary>
/// Checks if the <paramref name="methodInvocation" /> matches.
/// </summary>
bool Matches(MethodInvocation methodInvocation);
}
37 changes: 27 additions & 10 deletions Source/Mockolate/Setup/Interfaces.MethodSetup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@

namespace Mockolate.Setup;

/// <summary>
/// Marker interface for method setups.
/// </summary>
public interface IMethodSetup;

/// <summary>
/// Interface for verifiable method setup. It hides the implementation details to get the underlying
/// <see cref="IMethodMatch" />.
/// </summary>
public interface IVerifiableMethodSetup
{
/// <summary>
/// Gets the <see cref="IMethodMatch" /> used to match against method invocations.
/// </summary>
IMethodMatch GetMatch();
}
Comment thread
vbreuss marked this conversation as resolved.

/// <summary>
/// Interface for hiding some implementation details of <see cref="MethodSetup" />.
/// </summary>
Expand Down Expand Up @@ -63,7 +80,7 @@ TResult Invoke<TResult>(MethodInvocation methodInvocation, MockBehavior behavior
/// <summary>
/// Sets up a method returning <typeparamref name="TReturn" />.
/// </summary>
public interface IReturnMethodSetup<in TReturn>
public interface IReturnMethodSetup<in TReturn> : IMethodSetup
{
/// <summary>
/// Specifies if calling the base class implementation should be skipped.
Expand Down Expand Up @@ -209,7 +226,7 @@ public interface IReturnMethodSetupReturnWhenBuilder<in TReturn>
/// <summary>
/// Sets up a method returning <typeparamref name="TReturn" />.
/// </summary>
public interface IReturnMethodSetup<in TReturn, out T1>
public interface IReturnMethodSetup<in TReturn, out T1> : IMethodSetup
{
/// <summary>
/// Specifies if calling the base class implementation should be skipped.
Expand Down Expand Up @@ -370,7 +387,7 @@ public interface IReturnMethodSetupReturnWhenBuilder<in TReturn, out T1>
/// <summary>
/// Sets up a method returning <typeparamref name="TReturn" />.
/// </summary>
public interface IReturnMethodSetup<in TReturn, out T1, out T2>
public interface IReturnMethodSetup<in TReturn, out T1, out T2> : IMethodSetup
{
/// <summary>
/// Specifies if calling the base class implementation should be skipped.
Expand Down Expand Up @@ -531,7 +548,7 @@ public interface IReturnMethodSetupReturnWhenBuilder<in TReturn, out T1, out T2>
/// <summary>
/// Sets up a method returning <typeparamref name="TReturn" />.
/// </summary>
public interface IReturnMethodSetup<in TReturn, out T1, out T2, out T3>
public interface IReturnMethodSetup<in TReturn, out T1, out T2, out T3> : IMethodSetup
{
/// <summary>
/// Specifies if calling the base class implementation should be skipped.
Expand Down Expand Up @@ -693,7 +710,7 @@ public interface IReturnMethodSetupReturnWhenBuilder<in TReturn, out T1, out T2,
/// <summary>
/// Sets up a method returning <typeparamref name="TReturn" />.
/// </summary>
public interface IReturnMethodSetup<in TReturn, out T1, out T2, out T3, out T4>
public interface IReturnMethodSetup<in TReturn, out T1, out T2, out T3, out T4> : IMethodSetup
{
/// <summary>
/// Specifies if calling the base class implementation should be skipped.
Expand Down Expand Up @@ -855,7 +872,7 @@ public interface IReturnMethodSetupReturnWhenBuilder<in TReturn, out T1, out T2,
/// <summary>
/// Sets up a method returning <see langword="void" />.
/// </summary>
public interface IVoidMethodSetup
public interface IVoidMethodSetup : IMethodSetup
{
/// <summary>
/// Specifies if calling the base class implementation should be skipped.
Expand Down Expand Up @@ -988,7 +1005,7 @@ public interface IVoidMethodSetupReturnWhenBuilder : IVoidMethodSetup
/// <summary>
/// Sets up a method returning <see langword="void" />.
/// </summary>
public interface IVoidMethodSetup<out T1>
public interface IVoidMethodSetup<out T1> : IMethodSetup
{
/// <summary>
/// Specifies if calling the base class implementation should be skipped.
Expand Down Expand Up @@ -1135,7 +1152,7 @@ public interface IVoidMethodSetupReturnWhenBuilder<out T1>
/// <summary>
/// Sets up a method returning <see langword="void" />.
/// </summary>
public interface IVoidMethodSetup<out T1, out T2>
public interface IVoidMethodSetup<out T1, out T2> : IMethodSetup
{
/// <summary>
/// Specifies if calling the base class implementation should be skipped.
Expand Down Expand Up @@ -1282,7 +1299,7 @@ public interface IVoidMethodSetupReturnWhenBuilder<out T1, out T2>
/// <summary>
/// Sets up a method returning <see langword="void" />.
/// </summary>
public interface IVoidMethodSetup<out T1, out T2, out T3>
public interface IVoidMethodSetup<out T1, out T2, out T3> : IMethodSetup
{
/// <summary>
/// Specifies if calling the base class implementation should be skipped.
Expand Down Expand Up @@ -1429,7 +1446,7 @@ public interface IVoidMethodSetupReturnWhenBuilder<out T1, out T2, out T3>
/// <summary>
/// Sets up a method returning <see langword="void" />.
/// </summary>
public interface IVoidMethodSetup<out T1, out T2, out T3, out T4>
public interface IVoidMethodSetup<out T1, out T2, out T3, out T4> : IMethodSetup
{
/// <summary>
/// Specifies if calling the base class implementation should be skipped.
Expand Down
29 changes: 29 additions & 0 deletions Source/Mockolate/Setup/MethodParameterMatch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Linq;
using Mockolate.Interactions;
using Mockolate.Internals;
using Mockolate.Parameters;

namespace Mockolate.Setup;

/// <summary>
/// Matches a method by name and parameters.
/// </summary>
/// <remarks>
/// During verification, the <paramref name="methodName" /> is compared to the method name of the method invocation,
/// and the <paramref name="parameters" /> are matched one by one against the corresponding parameter in the method
/// invocation.
/// </remarks>
public readonly struct MethodParameterMatch(string methodName, NamedParameter[] parameters) : IMethodMatch
{
/// <inheritdoc cref="IMethodMatch.Matches(MethodInvocation)" />
public bool Matches(MethodInvocation methodInvocation)
=> methodInvocation.Name.Equals(methodName) &&
methodInvocation.Parameters.Length == parameters.Length &&
!parameters
.Where((parameter, i) => !parameter.Matches(methodInvocation.Parameters[i]))
.Any();

Comment on lines +20 to +25
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MethodParameterMatch.Matches uses LINQ (Where(...).Any()) on each comparison, which adds iterator overhead in a potentially hot verification path. A simple indexed for loop (or All) would avoid the LINQ overhead while keeping the same semantics.

Suggested change
=> methodInvocation.Name.Equals(methodName) &&
methodInvocation.Parameters.Length == parameters.Length &&
!parameters
.Where((parameter, i) => !parameter.Matches(methodInvocation.Parameters[i]))
.Any();
{
if (!methodInvocation.Name.Equals(methodName))
{
return false;
}
if (methodInvocation.Parameters.Length != parameters.Length)
{
return false;
}
for (int index = 0; index < parameters.Length; index++)
{
if (!parameters[index].Matches(methodInvocation.Parameters[index]))
{
return false;
}
}
return true;
}

Copilot uses AI. Check for mistakes.
/// <inheritdoc cref="object.ToString()" />
public override string ToString()
=> $"{methodName.SubstringAfterLast('.')}({string.Join(", ", parameters.Select(x => x.Parameter.ToString()))})";
}
24 changes: 24 additions & 0 deletions Source/Mockolate/Setup/MethodParametersMatch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Mockolate.Interactions;
using Mockolate.Internals;
using Mockolate.Parameters;

namespace Mockolate.Setup;

/// <summary>
/// Matches a method by name and parameters.
/// </summary>
/// <remarks>
/// During verification, the <paramref name="methodName" /> is compared to the method name of the method invocation,
/// and the <paramref name="parameters" /> are matched against the parameters in the method invocation.
/// </remarks>
public readonly struct MethodParametersMatch(string methodName, IParameters parameters) : IMethodMatch
{
/// <inheritdoc cref="IMethodMatch.Matches(MethodInvocation)" />
public bool Matches(MethodInvocation methodInvocation)
=> methodInvocation.Name.Equals(methodName) &&
parameters.Matches(methodInvocation.Parameters);

/// <inheritdoc cref="object.ToString()" />
public override string ToString()
=> $"{methodName.SubstringAfterLast('.')}({parameters})";
}
13 changes: 6 additions & 7 deletions Source/Mockolate/Setup/MethodSetup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Mockolate.Setup;
/// <summary>
/// Base class for method setups.
/// </summary>
public abstract class MethodSetup : IInteractiveMethodSetup
public abstract class MethodSetup(IMethodMatch methodMatch) : IInteractiveMethodSetup, IVerifiableMethodSetup
{
Comment thread
vbreuss marked this conversation as resolved.
/// <inheritdoc cref="IInteractiveMethodSetup.HasReturnCalls()" />
bool IInteractiveMethodSetup.HasReturnCalls()
Expand All @@ -25,7 +25,7 @@ T IInteractiveMethodSetup.SetRefParameter<T>(string parameterName, T value, Mock

/// <inheritdoc cref="IInteractiveMethodSetup.Matches(MethodInvocation)" />
bool IInteractiveMethodSetup.Matches(MethodInvocation methodInvocation)
=> IsMatch(methodInvocation);
=> methodMatch.Matches(methodInvocation);

/// <inheritdoc cref="IInteractiveMethodSetup.SkipBaseClass()" />
bool? IInteractiveMethodSetup.SkipBaseClass()
Expand All @@ -48,6 +48,10 @@ void IInteractiveMethodSetup.Invoke(MethodInvocation methodInvocation, MockBehav
public void TriggerCallbacks(object?[] parameters)
=> TriggerParameterCallbacks(parameters);

/// <inheritdoc cref="IVerifiableMethodSetup.GetMatch()" />
public IMethodMatch GetMatch()
=> methodMatch;
Comment on lines +51 to +53
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MethodParameterMatch/MethodParametersMatch are readonly structs but they’re typically passed/stored as IMethodMatch (e.g., MethodSetup(IMethodMatch methodMatch) / GetMatch()), which forces boxing allocations for each instance. If reducing allocations is a goal, consider making the match types sealed classes (or otherwise avoiding interface boxing).

Copilot uses AI. Check for mistakes.

/// <summary>
/// Gets the flag indicating if the base class implementation should be skipped.
/// </summary>
Expand Down Expand Up @@ -89,11 +93,6 @@ public void TriggerCallbacks(object?[] parameters)
protected abstract TResult GetReturnValue<TResult>(MethodInvocation invocation, MockBehavior behavior,
Func<TResult> defaultValueGenerator);

/// <summary>
/// Checks if the <paramref name="invocation" /> matches the setup.
/// </summary>
protected abstract bool IsMatch(MethodInvocation invocation);

/// <summary>
/// Triggers any configured parameter callbacks for the method setup with the specified <paramref name="parameters" />.
/// </summary>
Expand Down
Loading