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
88 changes: 73 additions & 15 deletions Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3711,25 +3711,50 @@ private static void AppendMethodSetupImplementation(StringBuilder sb, Method met
}
else
{
sb.Append(".WithParameterCollection(").Append(mockRegistryName).Append(", ")
.Append(method.GetUniqueNameString());
int j = 0;
foreach (MethodParameter parameter in method.Parameters)
{
sb.Append(", ");
if (valueFlags?[j] == true)
// Fused literal path: when the call site is the bare-value overload (every parameter is a
// literal value) we can store the values directly on the setup via WithLiteralValues and
// skip the per-parameter IParameterMatch<T> allocations that WithParameterCollection
// would otherwise force via It.IsValue<T>(...). Gated to 1..4 parameters because that is
// the arity range covered by the WithLiteralValues nested types.
bool useLiteralValues = valueFlags is { Length: > 0 and <= MaxExplicitParameters, } &&
valueFlags.All(x => x) &&
!method.Parameters.Any(p => p.RefKind == RefKind.Out ||
p.RefKind == RefKind.Ref ||
p.RefKind == RefKind.RefReadOnlyParameter);
if (useLiteralValues)
{
sb.Append(".WithLiteralValues(").Append(mockRegistryName).Append(", ")
.Append(method.GetUniqueNameString());
foreach (MethodParameter parameter in method.Parameters)
{
AppendNamedValueParameter(sb, parameter);
sb.Append(", ").Append(parameter.Name);
}
else

sb.Append(");").AppendLine();
}
else
{
sb.Append(".WithParameterCollection(").Append(mockRegistryName).Append(", ")
.Append(method.GetUniqueNameString());
int j = 0;
foreach (MethodParameter parameter in method.Parameters)
{
AppendNamedParameter(sb, parameter);
sb.Append(", ");
if (valueFlags?[j] == true)
{
AppendNamedValueParameter(sb, parameter);
}
else
{
AppendNamedParameter(sb, parameter);
}

j++;
}

j++;
sb.Append(");").AppendLine();
}

sb.Append(");").AppendLine();
sb.Append("\t\t\tthis.").Append(mockRegistryName).Append(".SetupMethod(")
.Append(memberIdRef).Append(", ").Append(scopePrefix).Append(methodSetupVar).Append(");").AppendLine();
sb.Append("\t\t\treturn ").Append(methodSetupVar).Append(';').AppendLine();
Expand Down Expand Up @@ -5211,14 +5236,47 @@ private static void AppendMethodVerifyImplementation(StringBuilder sb, Method me

sb.AppendLine();

bool noRefStruct = !method.Parameters.Any(p
=> p.RefKind == RefKind.Out || p.RefKind == RefKind.Ref ||
p.RefKind == RefKind.RefReadOnlyParameter);
bool baseEligible = !useParameters
&& method.Parameters.Count is > 0 and <= 4
&& (method.GenericParameters is null || method.GenericParameters.Value.Count == 0)
&& noRefStruct;

bool canUseLiteralVerify = baseEligible &&
valueFlags is { Length: > 0 and <= 4, } &&
valueFlags.All(x => x);
bool canUseTypedVerify = useFastForMethod
&& !useParameters
&& method.Parameters.Count <= 4
&& (method.GenericParameters is null || method.GenericParameters.Value.Count == 0)
&& (valueFlags is null || !valueFlags.Any(x => x))
&& !method.Parameters.Any(p
=> p.RefKind == RefKind.Out || p.RefKind == RefKind.Ref ||
p.RefKind == RefKind.RefReadOnlyParameter);
&& noRefStruct;

if (canUseLiteralVerify)
{
// Bare-value verify path: every parameter is a literal value, so we can route through the
// MockRegistry.VerifyMethod overload that takes the values directly (no IParameterMatch<T>
// allocation) and uses Method{N}LiteralCountSource for the allocation-free fast path. The
// memberId may be -1 here when fast buffers are off, which the runtime overload handles by
// dispatching to the slow predicate-based VerifyMethod.
sb.Append("\t\t\t=> this.").Append(mockRegistryName).Append(".VerifyMethod<").Append(verifyName);
foreach (MethodParameter parameter in method.Parameters)
{
sb.Append(", ").Append(parameter.ToTypeOrWrapper());
}

sb.Append(">(this, ").Append(methodMemberId).Append(", ").Append(method.GetUniqueNameString());
foreach (MethodParameter parameter in method.Parameters)
{
sb.Append(", ").Append(parameter.Name);
}

sb.Append(", () => $\"").Append(method.Name).Append("(")
.Append(string.Join(", ", method.Parameters.Select(p => $"{{{p.Name}}}"))).Append(")\");").AppendLine();
return;
}

if (canUseTypedVerify)
{
Expand Down
115 changes: 115 additions & 0 deletions Source/Mockolate/Interactions/FastMethodBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,33 @@ public int ConsumeMatching(IParameterMatch<T1> match1)
return matches;
}

/// <summary>
/// Literal-value variant of <see cref="ConsumeMatching(IParameterMatch{T1})" /> — compares each
/// recorded parameter against <paramref name="value1" /> using
/// <see cref="EqualityComparer{T}.Default" />, sparing the per-call <see cref="IParameterMatch{T1}" />
/// allocation that the matcher overload requires.
/// </summary>
public int ConsumeMatchingLiteral(T1 value1)
{
int matches = 0;
EqualityComparer<T1> cmp = EqualityComparer<T1>.Default;
lock (_storage.Lock)
{
int n = _storage.PublishedUnderLock;
for (int slot = 0; slot < n; slot++)
{
ref Record r = ref _storage.SlotUnderLock(slot);
if (cmp.Equals(value1, r.P1))
{
matches++;
_storage.VerifiedUnderLock(slot) = true;
}
}
}

return matches;
}

internal int ConsumeAll()
{
lock (_storage.Lock)
Expand Down Expand Up @@ -350,6 +377,34 @@ public int ConsumeMatching(IParameterMatch<T1> match1, IParameterMatch<T2> match
return matches;
}

/// <summary>
/// Literal-value variant of
/// <see cref="ConsumeMatching(IParameterMatch{T1}, IParameterMatch{T2})" /> — compares each
/// recorded parameter against <paramref name="value1" /> / <paramref name="value2" /> using
/// <see cref="EqualityComparer{T}.Default" />, sparing the per-call matcher allocations.
/// </summary>
public int ConsumeMatchingLiteral(T1 value1, T2 value2)
{
int matches = 0;
EqualityComparer<T1> cmp1 = EqualityComparer<T1>.Default;
EqualityComparer<T2> cmp2 = EqualityComparer<T2>.Default;
lock (_storage.Lock)
{
int n = _storage.PublishedUnderLock;
for (int slot = 0; slot < n; slot++)
{
ref Record r = ref _storage.SlotUnderLock(slot);
if (cmp1.Equals(value1, r.P1) && cmp2.Equals(value2, r.P2))
{
matches++;
_storage.VerifiedUnderLock(slot) = true;
}
}
}

return matches;
}

internal int ConsumeAll()
{
lock (_storage.Lock)
Expand Down Expand Up @@ -480,6 +535,35 @@ public int ConsumeMatching(IParameterMatch<T1> match1, IParameterMatch<T2> match
return matches;
}

/// <summary>
/// Literal-value variant of
/// <see cref="ConsumeMatching(IParameterMatch{T1}, IParameterMatch{T2}, IParameterMatch{T3})" />
/// — compares each recorded parameter against the supplied values using
/// <see cref="EqualityComparer{T}.Default" />, sparing the per-call matcher allocations.
/// </summary>
public int ConsumeMatchingLiteral(T1 value1, T2 value2, T3 value3)
{
int matches = 0;
EqualityComparer<T1> cmp1 = EqualityComparer<T1>.Default;
EqualityComparer<T2> cmp2 = EqualityComparer<T2>.Default;
EqualityComparer<T3> cmp3 = EqualityComparer<T3>.Default;
lock (_storage.Lock)
{
int n = _storage.PublishedUnderLock;
for (int slot = 0; slot < n; slot++)
{
ref Record r = ref _storage.SlotUnderLock(slot);
if (cmp1.Equals(value1, r.P1) && cmp2.Equals(value2, r.P2) && cmp3.Equals(value3, r.P3))
{
matches++;
_storage.VerifiedUnderLock(slot) = true;
}
}
}

return matches;
}

internal int ConsumeAll()
{
lock (_storage.Lock)
Expand Down Expand Up @@ -612,6 +696,37 @@ public int ConsumeMatching(IParameterMatch<T1> match1, IParameterMatch<T2> match
return matches;
}

/// <summary>
/// Literal-value variant of
/// <see cref="ConsumeMatching(IParameterMatch{T1}, IParameterMatch{T2}, IParameterMatch{T3}, IParameterMatch{T4})" />
/// — compares each recorded parameter against the supplied values using
/// <see cref="EqualityComparer{T}.Default" />, sparing the per-call matcher allocations.
/// </summary>
public int ConsumeMatchingLiteral(T1 value1, T2 value2, T3 value3, T4 value4)
{
int matches = 0;
EqualityComparer<T1> cmp1 = EqualityComparer<T1>.Default;
EqualityComparer<T2> cmp2 = EqualityComparer<T2>.Default;
EqualityComparer<T3> cmp3 = EqualityComparer<T3>.Default;
EqualityComparer<T4> cmp4 = EqualityComparer<T4>.Default;
lock (_storage.Lock)
{
int n = _storage.PublishedUnderLock;
for (int slot = 0; slot < n; slot++)
{
ref Record r = ref _storage.SlotUnderLock(slot);
if (cmp1.Equals(value1, r.P1) && cmp2.Equals(value2, r.P2) &&
cmp3.Equals(value3, r.P3) && cmp4.Equals(value4, r.P4))
{
matches++;
_storage.VerifiedUnderLock(slot) = true;
}
}
}

return matches;
}

internal int ConsumeAll()
{
lock (_storage.Lock)
Expand Down
89 changes: 89 additions & 0 deletions Source/Mockolate/MockRegistry.Verify.cs
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,95 @@ public VerificationResult<T>.IgnoreParameters VerifyMethod<T, T1, T2, T3, T4>(T
expectation);
}

/// <summary>
/// Literal-value fast-path Verify for 1-parameter methods. Avoids allocating an
/// <see cref="IParameterMatch{T1}" /> wrapper around <paramref name="literalValue1" /> by
/// comparing it inline with <see cref="EqualityComparer{T}.Default" />.
/// </summary>
public VerificationResult<T>.IgnoreParameters VerifyMethod<T, T1>(T subject, int memberId, string methodName,
T1 literalValue1, Func<string> expectation)
{
IFastMemberBuffer? buffer = TryGetBuffer(memberId);
if (buffer is FastMethod1Buffer<T1> typed)
{
Method1LiteralCountSource<T1> source = new(typed, literalValue1);
return new VerificationResult<T>.IgnoreParameters(
subject, Interactions, buffer, source, methodName,
source.Matches,
() => $"invoked method {expectation()}");
}
Comment thread
vbreuss marked this conversation as resolved.

return VerifyMethod<T, MethodInvocation<T1>>(subject, methodName,
m => EqualityComparer<T1>.Default.Equals(literalValue1, m.Parameter1), expectation);
}

/// <summary>
/// Literal-value fast-path Verify for 2-parameter methods.
/// </summary>
public VerificationResult<T>.IgnoreParameters VerifyMethod<T, T1, T2>(T subject, int memberId, string methodName,
T1 literalValue1, T2 literalValue2, Func<string> expectation)
{
IFastMemberBuffer? buffer = TryGetBuffer(memberId);
if (buffer is FastMethod2Buffer<T1, T2> typed)
{
Method2LiteralCountSource<T1, T2> source = new(typed, literalValue1, literalValue2);
return new VerificationResult<T>.IgnoreParameters(
subject, Interactions, buffer, source, methodName,
source.Matches,
() => $"invoked method {expectation()}");
}

return VerifyMethod<T, MethodInvocation<T1, T2>>(subject, methodName,
m => EqualityComparer<T1>.Default.Equals(literalValue1, m.Parameter1) &&
EqualityComparer<T2>.Default.Equals(literalValue2, m.Parameter2), expectation);
}

/// <summary>
/// Literal-value fast-path Verify for 3-parameter methods.
/// </summary>
public VerificationResult<T>.IgnoreParameters VerifyMethod<T, T1, T2, T3>(T subject, int memberId, string methodName,
T1 literalValue1, T2 literalValue2, T3 literalValue3, Func<string> expectation)
{
IFastMemberBuffer? buffer = TryGetBuffer(memberId);
if (buffer is FastMethod3Buffer<T1, T2, T3> typed)
{
Method3LiteralCountSource<T1, T2, T3> source = new(typed, literalValue1, literalValue2, literalValue3);
return new VerificationResult<T>.IgnoreParameters(
subject, Interactions, buffer, source, methodName,
source.Matches,
() => $"invoked method {expectation()}");
}

return VerifyMethod<T, MethodInvocation<T1, T2, T3>>(subject, methodName,
m => EqualityComparer<T1>.Default.Equals(literalValue1, m.Parameter1) &&
EqualityComparer<T2>.Default.Equals(literalValue2, m.Parameter2) &&
EqualityComparer<T3>.Default.Equals(literalValue3, m.Parameter3), expectation);
}

/// <summary>
/// Literal-value fast-path Verify for 4-parameter methods.
/// </summary>
public VerificationResult<T>.IgnoreParameters VerifyMethod<T, T1, T2, T3, T4>(T subject, int memberId, string methodName,
T1 literalValue1, T2 literalValue2, T3 literalValue3, T4 literalValue4, Func<string> expectation)
{
IFastMemberBuffer? buffer = TryGetBuffer(memberId);
if (buffer is FastMethod4Buffer<T1, T2, T3, T4> typed)
{
Method4LiteralCountSource<T1, T2, T3, T4> source = new(typed,
literalValue1, literalValue2, literalValue3, literalValue4);
return new VerificationResult<T>.IgnoreParameters(
subject, Interactions, buffer, source, methodName,
source.Matches,
() => $"invoked method {expectation()}");
}

return VerifyMethod<T, MethodInvocation<T1, T2, T3, T4>>(subject, methodName,
m => EqualityComparer<T1>.Default.Equals(literalValue1, m.Parameter1) &&
EqualityComparer<T2>.Default.Equals(literalValue2, m.Parameter2) &&
EqualityComparer<T3>.Default.Equals(literalValue3, m.Parameter3) &&
EqualityComparer<T4>.Default.Equals(literalValue4, m.Parameter4), expectation);
}

/// <summary>
/// Typed fast-path Verify for property getter accesses.
/// </summary>
Expand Down
28 changes: 28 additions & 0 deletions Source/Mockolate/Setup/MethodSetup.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Globalization;
using Mockolate.Interactions;
using Mockolate.Internals;

Expand Down Expand Up @@ -31,6 +32,33 @@ protected MethodSetup(string name)
protected static string FormatType(Type type)
=> type.FormatType();

/// <summary>
/// Renders <paramref name="value" /> for inclusion in a setup's <see cref="object.ToString" />,
/// mirroring the formatting that the <c>It.IsValue&lt;T&gt;</c> matcher uses for diagnostics:
/// strings are quoted, <see cref="IFormattable" /> values are rendered with
/// <see cref="CultureInfo.InvariantCulture" /> (so failure messages don't drift with the host
/// locale), and everything else falls through to <see cref="object.ToString" />.
/// </summary>
protected static string FormatLiteralValue<T>(T value)
{
if (value is null)
{
return "null";
}

if (value is string s)
{
return $"\"{s}\"";
}

if (value is IFormattable formattable)
{
return formattable.ToString(null, CultureInfo.InvariantCulture);
}

return value.ToString() ?? "null";
}

/// <inheritdoc cref="IVerifiableMethodSetup.Matches(IMethodInteraction)" />
bool IVerifiableMethodSetup.Matches(IMethodInteraction interaction)
=> MatchesInteraction(interaction);
Expand Down
Loading
Loading