Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
134 changes: 93 additions & 41 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
16 changes: 16 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,18 @@ 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 (setup is IVerifiableMethodSetup verifiableMethodSetup &&
verify is Mock<T> mock)
{
return mock.Registrations.Method(mock.Subject, verifiableMethodSetup.GetMatch());
}

throw new MockException("The subject is no mock.");
Comment thread
vbreuss marked this conversation as resolved.
Outdated
}
}
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

Copilot AI Feb 28, 2026

Copy link

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);
}
27 changes: 22 additions & 5 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
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 method)
=> method.Name.Equals(methodName) &&
method.Parameters.Length == parameters.Length &&
!parameters
.Where((parameter, i) => !parameter.Matches(method.Parameters[i]))
.Any();

Comment on lines +20 to +25

Copilot AI Feb 28, 2026

Copy link

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 method)
=> method.Name.Equals(methodName) &&
parameters.Matches(method.Parameters);

/// <inheritdoc cref="object.ToString()" />
public override string ToString()
=> $"{methodName.SubstringAfterLast('.')}({parameters})";
}
6 changes: 5 additions & 1 deletion 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 Down Expand Up @@ -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

Copilot AI Feb 28, 2026

Copy link

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
23 changes: 16 additions & 7 deletions Source/Mockolate/Setup/ReturnMethodSetup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@ namespace Mockolate.Setup;
/// <summary>
/// Sets up a method returning <typeparamref name="TReturn" />.
/// </summary>
public class ReturnMethodSetup<TReturn>(string name) : MethodSetup,
IReturnMethodSetupCallbackBuilder<TReturn>, IReturnMethodSetupReturnBuilder<TReturn>
public class ReturnMethodSetup<TReturn>(string name)
: MethodSetup(new MethodParameterMatch(name, [])),
IReturnMethodSetupCallbackBuilder<TReturn>, IReturnMethodSetupReturnBuilder<TReturn>
{
private readonly List<Callback<Action<int>>> _callbacks = [];
private readonly List<Callback<Func<int, TReturn>>> _returnCallbacks = [];
private bool? _skipBaseClass;
private Callback? _currentCallback;
private int _currentCallbacksIndex;
private Callback? _currentReturnCallback;
private int _currentReturnCallbackIndex;
private bool? _skipBaseClass;

/// <inheritdoc cref="IReturnMethodSetup{TReturn}.SkippingBaseClass(bool)" />
public IReturnMethodSetup<TReturn> SkippingBaseClass(bool skipBaseClass = true)
Expand Down Expand Up @@ -226,21 +227,23 @@ public class ReturnMethodSetup<TReturn, T1> : MethodSetup,
private readonly IParameters? _matches;
private readonly string _name;
private readonly List<Callback<Func<int, T1, TReturn>>> _returnCallbacks = [];
private bool? _skipBaseClass;
private Callback? _currentCallback;
private int _currentCallbacksIndex;
private Callback? _currentReturnCallback;
private int _currentReturnCallbackIndex;
private bool? _skipBaseClass;

/// <inheritdoc cref="ReturnMethodSetup{TReturn, T1}" />
public ReturnMethodSetup(string name, NamedParameter match1)
: base(new MethodParameterMatch(name, [match1,]))
{
_name = name;
_match1 = match1;
}

/// <inheritdoc cref="ReturnMethodSetup{TReturn, T1}" />
public ReturnMethodSetup(string name, IParameters matches)
: base(new MethodParametersMatch(name, matches))
{
_name = name;
_matches = matches;
Expand Down Expand Up @@ -506,14 +509,15 @@ public class ReturnMethodSetup<TReturn, T1, T2> : MethodSetup,
private readonly IParameters? _matches;
private readonly string _name;
private readonly List<Callback<Func<int, T1, T2, TReturn>>> _returnCallbacks = [];
private bool? _skipBaseClass;
private Callback? _currentCallback;
private int _currentCallbacksIndex;
private Callback? _currentReturnCallback;
private int _currentReturnCallbackIndex;
private bool? _skipBaseClass;

/// <inheritdoc cref="ReturnMethodSetup{TReturn, T1, T2}" />
public ReturnMethodSetup(string name, NamedParameter match1, NamedParameter match2)
: base(new MethodParameterMatch(name, [match1, match2,]))
{
_name = name;
_match1 = match1;
Expand All @@ -522,6 +526,7 @@ public ReturnMethodSetup(string name, NamedParameter match1, NamedParameter matc

/// <inheritdoc cref="ReturnMethodSetup{TReturn, T1, T2}" />
public ReturnMethodSetup(string name, IParameters matches)
: base(new MethodParametersMatch(name, matches))
{
_name = name;
_matches = matches;
Expand Down Expand Up @@ -797,18 +802,19 @@ public class ReturnMethodSetup<TReturn, T1, T2, T3> : MethodSetup,
private readonly IParameters? _matches;
private readonly string _name;
private readonly List<Callback<Func<int, T1, T2, T3, TReturn>>> _returnCallbacks = [];
private bool? _skipBaseClass;
private Callback? _currentCallback;
private int _currentCallbacksIndex;
private Callback? _currentReturnCallback;
private int _currentReturnCallbackIndex;
private bool? _skipBaseClass;

/// <inheritdoc cref="ReturnMethodSetup{TReturn, T1, T2, T3}" />
public ReturnMethodSetup(
string name,
NamedParameter match1,
NamedParameter match2,
NamedParameter match3)
: base(new MethodParameterMatch(name, [match1, match2, match3,]))
{
_name = name;
_match1 = match1;
Expand All @@ -818,6 +824,7 @@ public ReturnMethodSetup(

/// <inheritdoc cref="ReturnMethodSetup{TReturn, T1, T2, T3}" />
public ReturnMethodSetup(string name, IParameters matches)
: base(new MethodParametersMatch(name, matches))
{
_name = name;
_matches = matches;
Expand Down Expand Up @@ -1104,11 +1111,11 @@ public class ReturnMethodSetup<TReturn, T1, T2, T3, T4> : MethodSetup,
private readonly IParameters? _matches;
private readonly string _name;
private readonly List<Callback<Func<int, T1, T2, T3, T4, TReturn>>> _returnCallbacks = [];
private bool? _skipBaseClass;
private Callback? _currentCallback;
private int _currentCallbacksIndex;
private Callback? _currentReturnCallback;
private int _currentReturnCallbackIndex;
private bool? _skipBaseClass;

/// <inheritdoc cref="ReturnMethodSetup{TReturn, T1, T2, T3, T4}" />
public ReturnMethodSetup(
Expand All @@ -1117,6 +1124,7 @@ public ReturnMethodSetup(
NamedParameter match2,
NamedParameter match3,
NamedParameter match4)
: base(new MethodParameterMatch(name, [match1, match2, match3, match4,]))
{
_name = name;
_match1 = match1;
Expand All @@ -1127,6 +1135,7 @@ public ReturnMethodSetup(

/// <inheritdoc cref="ReturnMethodSetup{TReturn, T1, T2, T3, T4}" />
public ReturnMethodSetup(string name, IParameters matches)
: base(new MethodParametersMatch(name, matches))
{
_name = name;
_matches = matches;
Expand Down
Loading
Loading