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
49 changes: 1 addition & 48 deletions Source/Mockolate/MockRegistry.Interactions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -450,54 +450,7 @@ public TResult GetProperty<TResult>(PropertyGetterAccess access, Func<TResult> d
}

/// <summary>
/// Member-id-keyed overload of <see cref="GetProperty{TResult}(string, Func{TResult}, Func{TResult}?)" /> that
/// records via the typed <see cref="FastPropertyGetterBuffer" /> when the mock is wired to a
/// <see cref="FastMockInteractions" />, falling back to the legacy list otherwise.
/// </summary>
/// <typeparam name="TResult">The property's value type.</typeparam>
/// <param name="memberId">The generator-emitted member id for the property getter.</param>
/// <param name="propertyName">The simple property name.</param>
/// <param name="defaultValueGenerator">Producer of the default value when no setup supplies one.</param>
/// <param name="baseValueAccessor">Optional accessor for the base-class getter; when <see langword="null" /> only the default/initial value is considered.</param>
/// <returns>The resolved getter value.</returns>
/// <exception cref="MockNotSetupException">No setup exists for the property and <see cref="MockBehavior.ThrowWhenNotSetup" /> is <see langword="true" />.</exception>
public TResult GetProperty<TResult>(int memberId, string propertyName, Func<TResult> defaultValueGenerator,
Func<TResult>? baseValueAccessor)
{
if (!Behavior.SkipInteractionRecording)
{
RecordPropertyGetter(memberId, propertyName);
}

return ResolveGetterInternal(propertyName, defaultValueGenerator, baseValueAccessor, null);
}

/// <summary>
/// Singleton-aware overload of <see cref="GetProperty{TResult}(int, string, Func{TResult}, Func{TResult}?)" />
/// that records the supplied <paramref name="access" /> directly. The source generator emits one shared
/// <see cref="PropertyGetterAccess" /> per non-indexer property; reusing it keeps every recorded access
/// in the matching <see cref="FastPropertyGetterBuffer" /> pointing at the same singleton.
/// </summary>
/// <typeparam name="TResult">The property's value type.</typeparam>
/// <param name="memberId">The generator-emitted member id for the property getter.</param>
/// <param name="access">The shared <see cref="PropertyGetterAccess" /> singleton for the property.</param>
/// <param name="defaultValueGenerator">Producer of the default value when no setup supplies one.</param>
/// <param name="baseValueAccessor">Optional accessor for the base-class getter; when <see langword="null" /> only the default/initial value is considered.</param>
/// <returns>The resolved getter value.</returns>
/// <exception cref="MockNotSetupException">No setup exists for the property and <see cref="MockBehavior.ThrowWhenNotSetup" /> is <see langword="true" />.</exception>
public TResult GetProperty<TResult>(int memberId, PropertyGetterAccess access,
Func<TResult> defaultValueGenerator, Func<TResult>? baseValueAccessor)
{
if (!Behavior.SkipInteractionRecording)
{
RecordPropertyGetter(memberId, access);
}

return ResolveGetterInternal(access.Name, defaultValueGenerator, baseValueAccessor, null);
}

/// <summary>
/// Allocation-free fast-path overload of <see cref="GetProperty{TResult}(int, string, Func{TResult}, Func{TResult}?)" />.
/// Allocation-free fast-path overload of <see cref="GetProperty{TResult}(string, Func{TResult}, Func{TResult}?)" />.
/// Avoids the per-call closure allocation by accepting a static <see cref="Func{T, TResult}" /> that takes the
/// active <see cref="MockBehavior" /> as its argument — the source generator emits a <c>static</c> lambda so
/// the C# compiler caches the delegate in a static field.
Expand Down
18 changes: 0 additions & 18 deletions Source/Mockolate/Verify/IVerificationResultExtensions.cs

This file was deleted.

23 changes: 20 additions & 3 deletions Source/Mockolate/Verify/VerificationResultExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using Mockolate.Exceptions;
Expand All @@ -18,7 +19,7 @@ namespace Mockolate.Verify;
/// interactions produced on a background thread.
/// </remarks>
#if !DEBUG
[System.Diagnostics.DebuggerNonUserCode]
[DebuggerNonUserCode]
#endif
public static class VerificationResultExtensions
{
Expand All @@ -35,6 +36,18 @@ private static string ToTimes(this int amount, string verb = "")
(_, _) => $"{verb} {amount} times",
};

/// <summary>
/// Routes count terminators to the allocation-free fast path when the result implements
/// <see cref="IFastVerifyCountResult" />; otherwise falls back to materialising the matching
/// interactions through <see cref="IVerificationResult.Verify" /> and counting them. Lets
/// external implementers of <see cref="IVerificationResult" /> remain whole-interface
/// implementable while still letting the framework's own results take the fast path.
/// </summary>
internal static bool VerifyCount(this IVerificationResult result, Func<int, bool> countPredicate)
=> result is IFastVerifyCountResult fast
? fast.VerifyCount(countPredicate)
: result.Verify(arr => countPredicate(arr.Length));

extension<TMock>(VerificationResult<TMock> verificationResult)
{
/// <summary>
Expand Down Expand Up @@ -167,18 +180,21 @@ public void AtMost(int times)
/// or when a <see cref="VerificationResult{TVerify}.Within(TimeSpan)" /> timeout elapses first.
/// </exception>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when <paramref name="minimum" /> is negative or <paramref name="maximum" /> is less than <paramref name="minimum" />.
/// Thrown when <paramref name="minimum" /> is negative or <paramref name="maximum" /> is less than
/// <paramref name="minimum" />.
/// </exception>
public void Between(int minimum, int maximum)
{
if (minimum < 0)
{
// ReSharper disable once LocalizableElement
throw new ArgumentOutOfRangeException(nameof(minimum), "Minimum value must be non-negative.");
}

if (maximum < minimum)
{
throw new ArgumentOutOfRangeException(nameof(maximum),
// ReSharper disable once LocalizableElement
"Maximum value must be greater than or equal to minimum.");
}

Expand Down Expand Up @@ -387,7 +403,8 @@ public void Twice()
/// Verifies that the mock was invoked according to the <paramref name="predicate" />.
/// </summary>
/// <param name="predicate">
/// Receives the actual number of matching interactions and returns <see langword="true" /> if that count is acceptable.
/// Receives the actual number of matching interactions and returns <see langword="true" /> if that count is
/// acceptable.
/// </param>
/// <param name="doNotPopulateThisValue">
/// Populated by the compiler via <see cref="System.Runtime.CompilerServices.CallerArgumentExpressionAttribute" />
Expand Down
2 changes: 0 additions & 2 deletions Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,6 @@ namespace Mockolate
where T : Mockolate.Setup.MethodSetup { }
public TResult GetProperty<TResult>(Mockolate.Interactions.PropertyGetterAccess access, System.Func<TResult> defaultValueGenerator, System.Func<TResult>? baseValueAccessor) { }
public TResult GetProperty<TResult>(string propertyName, System.Func<TResult> defaultValueGenerator, System.Func<TResult>? baseValueAccessor) { }
public TResult GetProperty<TResult>(int memberId, Mockolate.Interactions.PropertyGetterAccess access, System.Func<TResult> defaultValueGenerator, System.Func<TResult>? baseValueAccessor) { }
public TResult GetProperty<TResult>(int memberId, string propertyName, System.Func<TResult> defaultValueGenerator, System.Func<TResult>? baseValueAccessor) { }
public TResult GetPropertyFast<TResult>(int memberId, Mockolate.Interactions.PropertyGetterAccess access, System.Func<Mockolate.MockBehavior, TResult> defaultValueGenerator, System.Func<TResult>? baseValueAccessor = null) { }
public TResult GetPropertyFast<TResult>(int memberId, string propertyName, System.Func<Mockolate.MockBehavior, TResult> defaultValueGenerator, System.Func<TResult>? baseValueAccessor = null) { }
public Mockolate.Setup.PropertySetup? GetPropertySetupSnapshot(int memberId) { }
Expand Down
2 changes: 0 additions & 2 deletions Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,6 @@ namespace Mockolate
where T : Mockolate.Setup.MethodSetup { }
public TResult GetProperty<TResult>(Mockolate.Interactions.PropertyGetterAccess access, System.Func<TResult> defaultValueGenerator, System.Func<TResult>? baseValueAccessor) { }
public TResult GetProperty<TResult>(string propertyName, System.Func<TResult> defaultValueGenerator, System.Func<TResult>? baseValueAccessor) { }
public TResult GetProperty<TResult>(int memberId, Mockolate.Interactions.PropertyGetterAccess access, System.Func<TResult> defaultValueGenerator, System.Func<TResult>? baseValueAccessor) { }
public TResult GetProperty<TResult>(int memberId, string propertyName, System.Func<TResult> defaultValueGenerator, System.Func<TResult>? baseValueAccessor) { }
public TResult GetPropertyFast<TResult>(int memberId, Mockolate.Interactions.PropertyGetterAccess access, System.Func<Mockolate.MockBehavior, TResult> defaultValueGenerator, System.Func<TResult>? baseValueAccessor = null) { }
public TResult GetPropertyFast<TResult>(int memberId, string propertyName, System.Func<Mockolate.MockBehavior, TResult> defaultValueGenerator, System.Func<TResult>? baseValueAccessor = null) { }
public Mockolate.Setup.PropertySetup? GetPropertySetupSnapshot(int memberId) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,6 @@ namespace Mockolate
where T : Mockolate.Setup.MethodSetup { }
public TResult GetProperty<TResult>(Mockolate.Interactions.PropertyGetterAccess access, System.Func<TResult> defaultValueGenerator, System.Func<TResult>? baseValueAccessor) { }
public TResult GetProperty<TResult>(string propertyName, System.Func<TResult> defaultValueGenerator, System.Func<TResult>? baseValueAccessor) { }
public TResult GetProperty<TResult>(int memberId, Mockolate.Interactions.PropertyGetterAccess access, System.Func<TResult> defaultValueGenerator, System.Func<TResult>? baseValueAccessor) { }
public TResult GetProperty<TResult>(int memberId, string propertyName, System.Func<TResult> defaultValueGenerator, System.Func<TResult>? baseValueAccessor) { }
public TResult GetPropertyFast<TResult>(int memberId, Mockolate.Interactions.PropertyGetterAccess access, System.Func<Mockolate.MockBehavior, TResult> defaultValueGenerator, System.Func<TResult>? baseValueAccessor = null) { }
public TResult GetPropertyFast<TResult>(int memberId, string propertyName, System.Func<Mockolate.MockBehavior, TResult> defaultValueGenerator, System.Func<TResult>? baseValueAccessor = null) { }
public Mockolate.Setup.PropertySetup? GetPropertySetupSnapshot(int memberId) { }
Expand Down
70 changes: 69 additions & 1 deletion Tests/Mockolate.Tests/MockRegistryTests.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,41 @@
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using Mockolate.Interactions;
using Mockolate.Setup;
using Mockolate.Tests.TestHelpers;

namespace Mockolate.Tests;

public sealed class MockRegistryTests
{
[Fact]
public async Task AddEvent_WithoutMemberIdAndMatchingSetup_ShouldInvokeSubscribedCallback()
{
MockRegistry registry = new(MockBehavior.Default, new FastMockInteractions(0));
int subscribed = 0;
EventSetup setup = new(registry, "OnFoo");
setup.OnSubscribed.Do(() => subscribed++);
registry.SetupEvent(setup);

registry.AddEvent("OnFoo", this, GetMethodInfo());

await That(subscribed).IsEqualTo(1);
}

[Fact]
public async Task GetProperty_WhenBaseValueAccessorThrows_ShouldRethrowException()
{
MockRegistry registry = new(MockBehavior.Default, new FastMockInteractions(0));
InvalidOperationException expected = new("base failed");

void Act()
{
registry.GetProperty("Foo", () => 0, () => throw expected);
}

await That(Act).Throws<InvalidOperationException>().WithMessage("base failed");
}

[Fact]
public async Task GetUnusedSetups_IndexerSetup_ShouldHaveCorrectString()
{
Expand Down Expand Up @@ -81,4 +109,44 @@ public async Task RegisterInteraction_ShouldBeThreadSafe()

await That(sut.Interactions.Count).IsEqualTo(1000);
}

[Fact]
public async Task RemoveEvent_WithoutMemberIdAndMatchingSetup_ShouldInvokeUnsubscribedCallback()
{
MockRegistry registry = new(MockBehavior.Default, new FastMockInteractions(0));
int unsubscribed = 0;
EventSetup setup = new(registry, "OnFoo");
setup.OnUnsubscribed.Do(() => unsubscribed++);
registry.SetupEvent(setup);

registry.RemoveEvent("OnFoo", this, GetMethodInfo());

await That(unsubscribed).IsEqualTo(1);
}

[Fact]
public async Task SetProperty_WithMemberIdAndNoFastBuffer_ShouldRecordAndStore()
{
MockRegistry registry = new(MockBehavior.Default, new FastMockInteractions(0));

bool result = registry.SetProperty(7, "Foo", 42);

await That(result).IsFalse();
await That(registry.Interactions.Count).IsEqualTo(1);
}

[Fact]
public async Task SetProperty_WithoutMemberId_SkippingBaseClass_ShouldReturnTrue()
{
MockBehavior behavior = MockBehavior.Default.SkippingBaseClass();
MockRegistry registry = new(behavior, new FastMockInteractions(0, behavior.SkipInteractionRecording));

bool result = registry.SetProperty("Foo", 42);

await That(result).IsTrue();
}

private static MethodInfo GetMethodInfo()
=> typeof(MockRegistryTests).GetMethod(nameof(GetMethodInfo),
BindingFlags.Static | BindingFlags.NonPublic)!;
}
28 changes: 28 additions & 0 deletions Tests/Mockolate.Tests/RefStruct/RefStructStorageHelperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#if NET9_0_OR_GREATER
using Mockolate.Setup;

namespace Mockolate.Tests.RefStruct;

public sealed class RefStructStorageHelperTests
{
[Fact]
public async Task TryResolveKey_WithNullProjectionAndNullRawKey_ShouldReturnFalse()
{
bool result = RefStructStorageHelper.TryResolveKey<int>(null, 7, null, out object key);

await That(result).IsFalse();
await That(key).IsNull();
}

[Fact]
public async Task TryResolveKey_WithNullProjectionAndRawKey_ShouldReturnRawKey()
{
object boxed = 42;

bool result = RefStructStorageHelper.TryResolveKey<int>(null, 7, boxed, out object key);

await That(result).IsTrue();
await That(key).IsSameAs(boxed);
}
}
#endif
40 changes: 40 additions & 0 deletions Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,46 @@ await That(result.StatusCode)
.IsEqualTo(expectSuccess ? HttpStatusCode.OK : HttpStatusCode.NotImplemented);
}

[Fact]
public async Task Wrapper_NonGenericInvokeCallbacks_DelegatesToInnerParameter()
{
ItExtensions.IStringContentBodyMatchingParameter sut = It.IsHttpContent().WithStringMatching("foo*");
int invocations = 0;
HttpContent? captured = new MyHttpContent();
sut.Do(content =>
{
invocations++;
captured = content;
});

sut.InvokeCallbacks(null);

await That(invocations).IsEqualTo(1);
await That(captured).IsNull();
}

[Fact]
public async Task Wrapper_NonGenericMatches_DelegatesToInnerParameter()
{
ItExtensions.IStringContentBodyMatchingParameter sut = It.IsHttpContent().WithStringMatching("foo*");

bool resultForNull = sut.Matches(null);
bool resultForUnrelated = sut.Matches("not http content");

await That(resultForNull).IsFalse();
await That(resultForUnrelated).IsFalse();
}

[Fact]
public async Task Wrapper_TypedMatches_ReturnsFalseForNullValue()
{
ItExtensions.IStringContentBodyMatchingParameter sut = It.IsHttpContent().WithStringMatching("foo*");

bool result = ((IParameterMatch<HttpContent?>)sut).Matches(null);

await That(result).IsFalse();
}

private sealed class MyHttpContent : HttpContent
{
protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context)
Expand Down
Loading